pyLoopSage 1.1.5__tar.gz → 2.0.0rc2__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 (128) hide show
  1. pyloopsage-2.0.0rc2/MANIFEST.in +2 -0
  2. pyloopsage-2.0.0rc2/PKG-INFO +132 -0
  3. pyloopsage-2.0.0rc2/README.md +84 -0
  4. {pyloopsage-1.1.5 → pyloopsage-2.0.0rc2}/setup.py +14 -6
  5. pyloopsage-2.0.0rc2/src/analysis/__init__.py +1 -0
  6. pyloopsage-2.0.0rc2/src/analysis/ensemble.py +202 -0
  7. pyloopsage-2.0.0rc2/src/analysis/inputs.py +127 -0
  8. pyloopsage-2.0.0rc2/src/analysis/maps.py +286 -0
  9. pyloopsage-2.0.0rc2/src/analysis/nesting.py +84 -0
  10. pyloopsage-2.0.0rc2/src/analysis/speed.py +248 -0
  11. pyloopsage-2.0.0rc2/src/analysis/structure.py +307 -0
  12. pyloopsage-2.0.0rc2/src/common/__init__.py +1 -0
  13. pyloopsage-2.0.0rc2/src/common/banner.py +122 -0
  14. pyloopsage-2.0.0rc2/src/common/genome.py +94 -0
  15. pyloopsage-2.0.0rc2/src/common/runtime.py +214 -0
  16. pyloopsage-2.0.0rc2/src/common/utils.py +100 -0
  17. pyloopsage-2.0.0rc2/src/html/style.css +53 -0
  18. pyloopsage-2.0.0rc2/src/html/template.html +10 -0
  19. pyloopsage-2.0.0rc2/src/html/viewer_template.html +148 -0
  20. pyloopsage-2.0.0rc2/src/knots/__init__.py +1 -0
  21. pyloopsage-2.0.0rc2/src/knots/crossing.py +100 -0
  22. pyloopsage-2.0.0rc2/src/knots/figures.py +185 -0
  23. pyloopsage-2.0.0rc2/src/knots/linking.py +231 -0
  24. pyloopsage-2.0.0rc2/src/knots/stage.py +106 -0
  25. pyloopsage-2.0.0rc2/src/logger.py +69 -0
  26. pyloopsage-2.0.0rc2/src/molecular_dynamics/__init__.py +1 -0
  27. pyloopsage-2.0.0rc2/src/molecular_dynamics/engine.py +316 -0
  28. {pyloopsage-1.1.5/loopsage → pyloopsage-2.0.0rc2/src/molecular_dynamics}/forcefields/classic_sm_ff.xml +1 -1
  29. pyloopsage-2.0.0rc2/src/molecular_dynamics/forces.py +79 -0
  30. pyloopsage-2.0.0rc2/src/molecular_dynamics/structures.py +165 -0
  31. pyloopsage-2.0.0rc2/src/molecular_dynamics/topology.py +48 -0
  32. pyloopsage-2.0.0rc2/src/monte_carlo/__init__.py +1 -0
  33. pyloopsage-2.0.0rc2/src/monte_carlo/energy.py +274 -0
  34. pyloopsage-2.0.0rc2/src/monte_carlo/sampler.py +192 -0
  35. pyloopsage-2.0.0rc2/src/monte_carlo/simulation.py +143 -0
  36. pyloopsage-2.0.0rc2/src/motif_finder/__init__.py +5 -0
  37. pyloopsage-2.0.0rc2/src/motif_finder/annotate.py +180 -0
  38. pyloopsage-2.0.0rc2/src/motif_finder/data/MA0139.1.pfm +4 -0
  39. pyloopsage-2.0.0rc2/src/motif_finder/fasta.py +94 -0
  40. pyloopsage-2.0.0rc2/src/motif_finder/pssm.py +92 -0
  41. pyloopsage-2.0.0rc2/src/optimizer/__init__.py +1 -0
  42. pyloopsage-2.0.0rc2/src/optimizer/nelder_mead.py +196 -0
  43. pyloopsage-2.0.0rc2/src/optimizer/objective.py +202 -0
  44. pyloopsage-2.0.0rc2/src/pipeline/__init__.py +1 -0
  45. pyloopsage-2.0.0rc2/src/pipeline/runner.py +139 -0
  46. pyloopsage-2.0.0rc2/src/pipeline/stage_analysis.py +91 -0
  47. pyloopsage-2.0.0rc2/src/pipeline/stage_mc.py +201 -0
  48. pyloopsage-2.0.0rc2/src/pipeline/stage_md.py +68 -0
  49. pyloopsage-2.0.0rc2/src/pipeline/stage_optimize.py +205 -0
  50. pyloopsage-2.0.0rc2/src/pipeline/stage_preproc.py +199 -0
  51. pyloopsage-2.0.0rc2/src/pipeline/stage_report.py +30 -0
  52. pyloopsage-2.0.0rc2/src/pipeline/stage_structure.py +133 -0
  53. pyloopsage-2.0.0rc2/src/preproc/__init__.py +1 -0
  54. pyloopsage-2.0.0rc2/src/preproc/hic.py +165 -0
  55. pyloopsage-2.0.0rc2/src/preproc/loader.py +153 -0
  56. pyloopsage-2.0.0rc2/src/preproc/readers.py +261 -0
  57. pyloopsage-2.0.0rc2/src/pyLoopSage.egg-info/PKG-INFO +132 -0
  58. pyloopsage-2.0.0rc2/src/pyLoopSage.egg-info/SOURCES.txt +107 -0
  59. pyloopsage-2.0.0rc2/src/pyLoopSage.egg-info/entry_points.txt +2 -0
  60. {pyloopsage-1.1.5 → pyloopsage-2.0.0rc2/src}/pyLoopSage.egg-info/requires.txt +1 -0
  61. pyloopsage-2.0.0rc2/src/pyLoopSage.egg-info/top_level.txt +18 -0
  62. pyloopsage-2.0.0rc2/src/reporting/__init__.py +1 -0
  63. pyloopsage-2.0.0rc2/src/reporting/explanations.py +485 -0
  64. pyloopsage-2.0.0rc2/src/reporting/report.py +204 -0
  65. pyloopsage-2.0.0rc2/src/run.py +20 -0
  66. pyloopsage-2.0.0rc2/src/settings/__init__.py +1 -0
  67. pyloopsage-2.0.0rc2/src/settings/loader.py +266 -0
  68. pyloopsage-2.0.0rc2/src/settings/schema.py +174 -0
  69. pyloopsage-2.0.0rc2/src/validation/__init__.py +1 -0
  70. pyloopsage-2.0.0rc2/src/validation/compare.py +284 -0
  71. pyloopsage-2.0.0rc2/src/validation/hic_compare.py +68 -0
  72. pyloopsage-2.0.0rc2/src/version.py +2 -0
  73. pyloopsage-2.0.0rc2/src/viz/__init__.py +1 -0
  74. pyloopsage-2.0.0rc2/src/viz/hic_figs.py +94 -0
  75. pyloopsage-2.0.0rc2/src/viz/input_figs.py +236 -0
  76. pyloopsage-2.0.0rc2/src/viz/map_figs.py +180 -0
  77. pyloopsage-2.0.0rc2/src/viz/mc_figs.py +456 -0
  78. pyloopsage-2.0.0rc2/src/viz/motif_figs.py +39 -0
  79. pyloopsage-2.0.0rc2/src/viz/nesting_figs.py +97 -0
  80. pyloopsage-2.0.0rc2/src/viz/optimizer_figs.py +58 -0
  81. pyloopsage-2.0.0rc2/src/viz/speed_figs.py +217 -0
  82. pyloopsage-2.0.0rc2/src/viz/structure_figs.py +276 -0
  83. pyloopsage-2.0.0rc2/src/viz/surface3d.py +83 -0
  84. pyloopsage-2.0.0rc2/src/viz/theme.py +206 -0
  85. pyloopsage-2.0.0rc2/src/viz/validation_figs.py +207 -0
  86. pyloopsage-2.0.0rc2/src/viz3d/__init__.py +1 -0
  87. pyloopsage-2.0.0rc2/src/viz3d/render.py +166 -0
  88. pyloopsage-2.0.0rc2/tests/test_correlation.py +22 -0
  89. pyloopsage-2.0.0rc2/tests/test_energy.py +14 -0
  90. pyloopsage-2.0.0rc2/tests/test_preproc.py +31 -0
  91. pyloopsage-2.0.0rc2/tests/test_src_3d_knots.py +110 -0
  92. pyloopsage-2.0.0rc2/tests/test_src_analysis.py +164 -0
  93. pyloopsage-2.0.0rc2/tests/test_src_config.py +66 -0
  94. pyloopsage-2.0.0rc2/tests/test_src_config_contract.py +90 -0
  95. pyloopsage-2.0.0rc2/tests/test_src_console.py +65 -0
  96. pyloopsage-2.0.0rc2/tests/test_src_energy.py +237 -0
  97. pyloopsage-2.0.0rc2/tests/test_src_hic.py +165 -0
  98. pyloopsage-2.0.0rc2/tests/test_src_inputs_viz.py +79 -0
  99. pyloopsage-2.0.0rc2/tests/test_src_motif.py +240 -0
  100. pyloopsage-2.0.0rc2/tests/test_src_nesting.py +73 -0
  101. pyloopsage-2.0.0rc2/tests/test_src_optimizer.py +197 -0
  102. pyloopsage-2.0.0rc2/tests/test_src_pipeline.py +134 -0
  103. pyloopsage-2.0.0rc2/tests/test_src_preproc.py +75 -0
  104. pyloopsage-2.0.0rc2/tests/test_src_speed.py +116 -0
  105. pyloopsage-2.0.0rc2/tests/test_src_topology.py +153 -0
  106. pyloopsage-2.0.0rc2/tests/test_src_units_stats.py +57 -0
  107. pyloopsage-1.1.5/MANIFEST.in +0 -1
  108. pyloopsage-1.1.5/PKG-INFO +0 -394
  109. pyloopsage-1.1.5/README.md +0 -347
  110. pyloopsage-1.1.5/loopsage/__init__.py +0 -4
  111. pyloopsage-1.1.5/loopsage/args_definition.py +0 -218
  112. pyloopsage-1.1.5/loopsage/em.py +0 -135
  113. pyloopsage-1.1.5/loopsage/initial_structures.py +0 -137
  114. pyloopsage-1.1.5/loopsage/knots.py +0 -210
  115. pyloopsage-1.1.5/loopsage/md.py +0 -182
  116. pyloopsage-1.1.5/loopsage/plots.py +0 -254
  117. pyloopsage-1.1.5/loopsage/preproc.py +0 -124
  118. pyloopsage-1.1.5/loopsage/run.py +0 -102
  119. pyloopsage-1.1.5/loopsage/stochastic_simulation.py +0 -380
  120. pyloopsage-1.1.5/loopsage/utils.py +0 -476
  121. pyloopsage-1.1.5/loopsage/vizualization_tools.py +0 -153
  122. pyloopsage-1.1.5/pyLoopSage.egg-info/PKG-INFO +0 -394
  123. pyloopsage-1.1.5/pyLoopSage.egg-info/SOURCES.txt +0 -23
  124. pyloopsage-1.1.5/pyLoopSage.egg-info/entry_points.txt +0 -2
  125. pyloopsage-1.1.5/pyLoopSage.egg-info/top_level.txt +0 -1
  126. {pyloopsage-1.1.5 → pyloopsage-2.0.0rc2}/LICENSE +0 -0
  127. {pyloopsage-1.1.5 → pyloopsage-2.0.0rc2}/setup.cfg +0 -0
  128. {pyloopsage-1.1.5 → pyloopsage-2.0.0rc2/src}/pyLoopSage.egg-info/dependency_links.txt +0 -0
@@ -0,0 +1,2 @@
1
+ exclude Dockerfile
2
+ recursive-include src *.html *.css *.xml *.pfm
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyLoopSage
3
+ Version: 2.0.0rc2
4
+ Summary: An energy-based stochastic model of loop extrusion in chromatin.
5
+ Home-page: https://github.com/SFGLab/pyLoopSage
6
+ Author: Sebastian Korsak
7
+ Author-email: s.korsak@datascience.edu.pl
8
+ License: GNU General Public License v3.0
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: numpy<2.0,>1.2
16
+ Requires-Dist: numba
17
+ Requires-Dist: scipy
18
+ Requires-Dist: pandas
19
+ Requires-Dist: argparse
20
+ Requires-Dist: matplotlib
21
+ Requires-Dist: mdtraj
22
+ Requires-Dist: seaborn
23
+ Requires-Dist: scikit-learn
24
+ Requires-Dist: configparser
25
+ Requires-Dist: typing-extensions
26
+ Requires-Dist: tqdm
27
+ Requires-Dist: pyvista[all]
28
+ Requires-Dist: OpenMM
29
+ Requires-Dist: OpenMM-cuda
30
+ Requires-Dist: statsmodels
31
+ Requires-Dist: imageio
32
+ Requires-Dist: imageio[ffmpeg]
33
+ Requires-Dist: pillow
34
+ Requires-Dist: pyBigWig
35
+ Requires-Dist: hic-straw
36
+ Requires-Dist: topoly
37
+ Dynamic: author
38
+ Dynamic: author-email
39
+ Dynamic: classifier
40
+ Dynamic: description
41
+ Dynamic: description-content-type
42
+ Dynamic: home-page
43
+ Dynamic: license
44
+ Dynamic: license-file
45
+ Dynamic: requires-dist
46
+ Dynamic: requires-python
47
+ Dynamic: summary
48
+
49
+ <p align="center">
50
+ <img src="assets/logo.svg" alt="LoopSage" width="760">
51
+ </p>
52
+
53
+ <p align="center">
54
+ <a href="https://www.python.org"><img alt="Python" src="https://img.shields.io/badge/Python-3.10%2B-3776AB?logo=python&logoColor=white"></a>
55
+ <a href="https://openmm.org"><img alt="OpenMM" src="https://img.shields.io/badge/OpenMM-8.5%2B-6a3de8"></a>
56
+ <a href="https://numba.pydata.org"><img alt="Numba" src="https://img.shields.io/badge/Numba-JIT%20sampler-00A3E0?logo=numba&logoColor=white"></a>
57
+ <a href="https://pyvista.org"><img alt="PyVista" src="https://img.shields.io/badge/PyVista-3D%20viewer-FF7F00"></a>
58
+ <a href="https://www.docker.com"><img alt="Docker" src="https://img.shields.io/badge/Docker-ready-2496ED?logo=docker&logoColor=white"></a>
59
+ <a href="https://numpy.org"><img alt="NumPy" src="https://img.shields.io/badge/NumPy-013243?logo=numpy&logoColor=white"></a>
60
+ <a href="https://scipy.org"><img alt="SciPy" src="https://img.shields.io/badge/SciPy-8CAAE6?logo=scipy&logoColor=white"></a>
61
+ <a href="https://mdtraj.org"><img alt="MDTraj" src="https://img.shields.io/badge/MDTraj-trajectories-1a1a1f"></a>
62
+ <a href="tests"><img alt="pytest" src="https://img.shields.io/badge/tested%20with-pytest-0A9EDC?logo=pytest&logoColor=white"></a>
63
+ <a href="https://www.gnu.org/licenses/gpl-3.0.html"><img alt="GPL v3 license" src="https://img.shields.io/badge/License-GPL%20v3-blue"></a>
64
+ </p>
65
+
66
+ **LoopSage** is an energy-based stochastic model of chromatin loop extrusion. Cohesin complexes are modelled as loop-extruding factors (LEFs) that slide along a chromatin fibre, are blocked by CTCF sites and rebind elsewhere. A Metropolis / simulated-annealing Monte Carlo samples ensembles of loop configurations that agree with your loop data (e.g. ChIA-PET, HiChIP, Hi-C loops, or CTCF peaks). Each ensemble can be turned into 3D structures with a short OpenMM molecular-dynamics run.
67
+
68
+ From one command you get an HTML report with the contact maps, loop statistics, 3D structures and diagnostics.
69
+
70
+ ## Installation
71
+
72
+ Python 3.10 or newer is required. `openmm` and `pyBigWig` install most reliably from conda. `hic-straw` (reads `.hic` files for the optional `--hic` reference) is installed with the package. It needs a C++ compiler and libcurl when no wheel exists for your system.
73
+
74
+ ```bash
75
+ git clone https://github.com/SFGLab/pyLoopSage.git
76
+ cd pyLoopSage
77
+ conda create -n loopsage python=3.11 -y && conda activate loopsage
78
+ conda install -c conda-forge "openmm>=8.5.1" pybigwig -y
79
+ pip install -r requirements.txt && pip install -e .
80
+ ```
81
+
82
+ Then install the package (`pip install -e .`) to get the `loopsage` command (version 2.0.0rc2).
83
+
84
+ ## Usage
85
+
86
+ ```bash
87
+ # Minimal run: loops file + region
88
+ loopsage -f loops.bedpe -r chr1:77000000-95000000
89
+
90
+ # Settings from a config file, with command-line overrides
91
+ loopsage -c config.ini -t 4 --n-replicas 3
92
+
93
+ # BEDPE without orientation columns: read CTCF orientation from the reference genome, keep loops with >= 3 PET
94
+ loopsage -f loops.bedpe -r chr1:77000000-95000000 --motif-genome hg38.fa --min-pet 3
95
+
96
+ # See every option
97
+ loopsage -h
98
+ ```
99
+
100
+ - **Input:** a `.bedpe` loop file, or `.bed` / `.narrowPeak` CTCF sites, plus the genomic region.
101
+ - **CTCF motif search (optional):** with `--motif-genome FASTA` every loop anchor is scanned for the CTCF motif (bundled JASPAR MA0139.1, or your own `--motif-pfm`) and the orientation probabilities are added automatically; `--min-pet N` keeps only loops with at least N PET counts (works without a genome too). Needs no extra packages; the genome is read through a `.fai` index and never loaded into memory. Hits are accepted at `--motif-pvalue` (default 1e-5). The report shows a progress bar while searching and a diagnostics section (loops kept, motif scores, orientation calls). The annotated loops are saved in the run's `data` folder.
102
+ - **Config:** `config_example.ini` lists every parameter with a short description; command-line flags override it.
103
+ - **Output:** `results/<chrom>_<start>_<end>_<run>/`, with the report in `report/report.html`.
104
+ - **MD platform:** `--platform auto` (default) tries CUDA, then OpenCL, then the multi-threaded CPU platform, and the chosen platform is always printed. Force one with `--platform CUDA|OpenCL|CPU`.
105
+ - **Monte Carlo:** `--method Metropolis|Annealing`, `--t-init`, `--t-final`, `--mc-steps`, `--sample-every` and `--burnin` control the sampler (all also keys in the config file).
106
+ - **Parallelism:** `-t/--threads` sets worker threads and `--n-replicas` runs independent Monte Carlo chains (default 1).
107
+ - **Demo:** `python demo/run_demo.py quick` runs a small example in seconds.
108
+
109
+ Detailed documentation of the model, parameters and outputs is in [`markdown/`](markdown/).
110
+
111
+ ## Tests
112
+
113
+ Run the whole suite in parallel, with a per-module report:
114
+
115
+ ```bash
116
+ python tools/run_tests.py # all tests, one worker per two cores
117
+ python tools/run_tests.py -j 8 --html # 8 workers, also writes test_report/report.html
118
+ python tools/run_tests.py -k contrast # only tests whose name matches
119
+ ```
120
+
121
+ Plain `PYTHONPATH=src python -m pytest -q` also works, but runs the files one after another (about 15 minutes). Tests of optional packages (`hicstraw`, `pyBigWig`) are skipped when those are not installed.
122
+
123
+ ## Citation
124
+
125
+ If you use LoopSage, please cite:
126
+
127
+ - Korsak, S. and Plewczynski, D. *LoopSage: An energy-based Monte Carlo approach for the loop extrusion modelling of chromatin.* Methods (2024).
128
+ - Jodkowska, K., Parteka-Tojek, Z., Agarwal, A., Denkiewicz, M., Korsak, S., Chiliński, M., Banecki, K., & Plewczynski, D. *Improved cohesin HiChIP protocol and bioinformatic analysis for robust detection of chromatin loops and stripes.* bioRxiv (2024). https://doi.org/10.1101/2024.05.16.594268
129
+
130
+ ## License
131
+
132
+ GNU General Public License v3.0.
@@ -0,0 +1,84 @@
1
+ <p align="center">
2
+ <img src="assets/logo.svg" alt="LoopSage" width="760">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <a href="https://www.python.org"><img alt="Python" src="https://img.shields.io/badge/Python-3.10%2B-3776AB?logo=python&logoColor=white"></a>
7
+ <a href="https://openmm.org"><img alt="OpenMM" src="https://img.shields.io/badge/OpenMM-8.5%2B-6a3de8"></a>
8
+ <a href="https://numba.pydata.org"><img alt="Numba" src="https://img.shields.io/badge/Numba-JIT%20sampler-00A3E0?logo=numba&logoColor=white"></a>
9
+ <a href="https://pyvista.org"><img alt="PyVista" src="https://img.shields.io/badge/PyVista-3D%20viewer-FF7F00"></a>
10
+ <a href="https://www.docker.com"><img alt="Docker" src="https://img.shields.io/badge/Docker-ready-2496ED?logo=docker&logoColor=white"></a>
11
+ <a href="https://numpy.org"><img alt="NumPy" src="https://img.shields.io/badge/NumPy-013243?logo=numpy&logoColor=white"></a>
12
+ <a href="https://scipy.org"><img alt="SciPy" src="https://img.shields.io/badge/SciPy-8CAAE6?logo=scipy&logoColor=white"></a>
13
+ <a href="https://mdtraj.org"><img alt="MDTraj" src="https://img.shields.io/badge/MDTraj-trajectories-1a1a1f"></a>
14
+ <a href="tests"><img alt="pytest" src="https://img.shields.io/badge/tested%20with-pytest-0A9EDC?logo=pytest&logoColor=white"></a>
15
+ <a href="https://www.gnu.org/licenses/gpl-3.0.html"><img alt="GPL v3 license" src="https://img.shields.io/badge/License-GPL%20v3-blue"></a>
16
+ </p>
17
+
18
+ **LoopSage** is an energy-based stochastic model of chromatin loop extrusion. Cohesin complexes are modelled as loop-extruding factors (LEFs) that slide along a chromatin fibre, are blocked by CTCF sites and rebind elsewhere. A Metropolis / simulated-annealing Monte Carlo samples ensembles of loop configurations that agree with your loop data (e.g. ChIA-PET, HiChIP, Hi-C loops, or CTCF peaks). Each ensemble can be turned into 3D structures with a short OpenMM molecular-dynamics run.
19
+
20
+ From one command you get an HTML report with the contact maps, loop statistics, 3D structures and diagnostics.
21
+
22
+ ## Installation
23
+
24
+ Python 3.10 or newer is required. `openmm` and `pyBigWig` install most reliably from conda. `hic-straw` (reads `.hic` files for the optional `--hic` reference) is installed with the package. It needs a C++ compiler and libcurl when no wheel exists for your system.
25
+
26
+ ```bash
27
+ git clone https://github.com/SFGLab/pyLoopSage.git
28
+ cd pyLoopSage
29
+ conda create -n loopsage python=3.11 -y && conda activate loopsage
30
+ conda install -c conda-forge "openmm>=8.5.1" pybigwig -y
31
+ pip install -r requirements.txt && pip install -e .
32
+ ```
33
+
34
+ Then install the package (`pip install -e .`) to get the `loopsage` command (version 2.0.0rc2).
35
+
36
+ ## Usage
37
+
38
+ ```bash
39
+ # Minimal run: loops file + region
40
+ loopsage -f loops.bedpe -r chr1:77000000-95000000
41
+
42
+ # Settings from a config file, with command-line overrides
43
+ loopsage -c config.ini -t 4 --n-replicas 3
44
+
45
+ # BEDPE without orientation columns: read CTCF orientation from the reference genome, keep loops with >= 3 PET
46
+ loopsage -f loops.bedpe -r chr1:77000000-95000000 --motif-genome hg38.fa --min-pet 3
47
+
48
+ # See every option
49
+ loopsage -h
50
+ ```
51
+
52
+ - **Input:** a `.bedpe` loop file, or `.bed` / `.narrowPeak` CTCF sites, plus the genomic region.
53
+ - **CTCF motif search (optional):** with `--motif-genome FASTA` every loop anchor is scanned for the CTCF motif (bundled JASPAR MA0139.1, or your own `--motif-pfm`) and the orientation probabilities are added automatically; `--min-pet N` keeps only loops with at least N PET counts (works without a genome too). Needs no extra packages; the genome is read through a `.fai` index and never loaded into memory. Hits are accepted at `--motif-pvalue` (default 1e-5). The report shows a progress bar while searching and a diagnostics section (loops kept, motif scores, orientation calls). The annotated loops are saved in the run's `data` folder.
54
+ - **Config:** `config_example.ini` lists every parameter with a short description; command-line flags override it.
55
+ - **Output:** `results/<chrom>_<start>_<end>_<run>/`, with the report in `report/report.html`.
56
+ - **MD platform:** `--platform auto` (default) tries CUDA, then OpenCL, then the multi-threaded CPU platform, and the chosen platform is always printed. Force one with `--platform CUDA|OpenCL|CPU`.
57
+ - **Monte Carlo:** `--method Metropolis|Annealing`, `--t-init`, `--t-final`, `--mc-steps`, `--sample-every` and `--burnin` control the sampler (all also keys in the config file).
58
+ - **Parallelism:** `-t/--threads` sets worker threads and `--n-replicas` runs independent Monte Carlo chains (default 1).
59
+ - **Demo:** `python demo/run_demo.py quick` runs a small example in seconds.
60
+
61
+ Detailed documentation of the model, parameters and outputs is in [`markdown/`](markdown/).
62
+
63
+ ## Tests
64
+
65
+ Run the whole suite in parallel, with a per-module report:
66
+
67
+ ```bash
68
+ python tools/run_tests.py # all tests, one worker per two cores
69
+ python tools/run_tests.py -j 8 --html # 8 workers, also writes test_report/report.html
70
+ python tools/run_tests.py -k contrast # only tests whose name matches
71
+ ```
72
+
73
+ Plain `PYTHONPATH=src python -m pytest -q` also works, but runs the files one after another (about 15 minutes). Tests of optional packages (`hicstraw`, `pyBigWig`) are skipped when those are not installed.
74
+
75
+ ## Citation
76
+
77
+ If you use LoopSage, please cite:
78
+
79
+ - Korsak, S. and Plewczynski, D. *LoopSage: An energy-based Monte Carlo approach for the loop extrusion modelling of chromatin.* Methods (2024).
80
+ - Jodkowska, K., Parteka-Tojek, Z., Agarwal, A., Denkiewicz, M., Korsak, S., Chiliński, M., Banecki, K., & Plewczynski, D. *Improved cohesin HiChIP protocol and bioinformatic analysis for robust detection of chromatin loops and stripes.* bioRxiv (2024). https://doi.org/10.1101/2024.05.16.594268
81
+
82
+ ## License
83
+
84
+ GNU General Public License v3.0.
@@ -1,12 +1,15 @@
1
- from setuptools import setup, find_packages
1
+ from setuptools import setup, find_namespace_packages
2
+ import re
2
3
  from pathlib import Path
3
4
 
4
5
  with open('README.md', 'r', encoding='utf-8') as f:
5
6
  long_description = f.read()
6
7
 
8
+ VERSION = re.search(r'"([^"]+)"', Path('src/version.py').read_text().split('__version__')[1]).group(1)
9
+
7
10
  setup(
8
11
  name='pyLoopSage', # Package name
9
- version='1.1.5', # Version of the software
12
+ version=VERSION, # read from src/version.py
10
13
  description='An energy-based stochastic model of loop extrusion in chromatin.',
11
14
  long_description=long_description,
12
15
  long_description_content_type='text/markdown',
@@ -14,10 +17,14 @@ setup(
14
17
  author_email='s.korsak@datascience.edu.pl',
15
18
  url='https://github.com/SFGLab/pyLoopSage', # GitHub repository URL
16
19
  license='GNU General Public License v3.0',
17
- packages=find_packages(include=['loopsage', 'loopsage.*']),
20
+ package_dir={'': 'src'},
21
+ packages=find_namespace_packages('src', exclude=['*__pycache__*']),
22
+ py_modules=['run', 'logger', 'version'],
18
23
  include_package_data=True,
19
24
  package_data={
20
- 'loopsage': ['forcefields/*'],
25
+ 'html': ['*.html', '*.css'],
26
+ 'molecular_dynamics.forcefields': ['*.xml'],
27
+ 'motif_finder.data': ['*.pfm'],
21
28
  },
22
29
  install_requires=[ # List your package dependencies here
23
30
  'numpy>1.2,<2.0',
@@ -40,11 +47,12 @@ setup(
40
47
  'imageio[ffmpeg]',
41
48
  'pillow',
42
49
  'pyBigWig',
50
+ 'hic-straw', # reads .hic files (--hic)
43
51
  'topoly',
44
52
  ],
45
53
  entry_points={
46
54
  'console_scripts': [
47
- 'loopsage=loopsage.run:main', # loopsage command points to run.py's main function
55
+ 'loopsage=run:main', # the `loopsage` command runs src/run.py
48
56
  ],
49
57
  },
50
58
  classifiers=[
@@ -53,4 +61,4 @@ setup(
53
61
  'Operating System :: POSIX :: Linux', # General OS classifier
54
62
  ],
55
63
  python_requires='>=3.10', # Specify Python version compatibility
56
- )
64
+ )
@@ -0,0 +1 @@
1
+ """Post-processing of Monte Carlo / MD results (no plotting here)."""
@@ -0,0 +1,202 @@
1
+ """Loop statistics of the equilibrium ensemble.
2
+
3
+ - Convergence and equilibrium diagnostics of the Monte Carlo run.
4
+ - Extra insight metrics for the loop ensemble: anchor recovery, LEF dynamics, length agreement.
5
+ - Agreement between independent MC chains (replicas).
6
+ - Epigenetic model: average state, same-state map, agreement of loop ends, energy over time.
7
+ """
8
+ import numpy as np
9
+ from scipy.ndimage import gaussian_filter1d
10
+ from scipy.stats import ks_2samp, spearmanr
11
+
12
+
13
+ # ---- Loop statistics of the equilibrium ensemble ---------------------------
14
+
15
+ _MOVES = ("Rebinding", "Sliding", "Epigenetic-state change")
16
+
17
+
18
+ def loop_lengths(res) -> np.ndarray:
19
+ """All loop lengths (beads) of every LEF in every post-burn-in snapshot."""
20
+ return (res.production(res.Ns) - res.production(res.Ms)).ravel()
21
+
22
+
23
+ def occupancy(res, n_beads: int) -> np.ndarray:
24
+ """Average number of LEF legs sitting on each bead (anchor probability)."""
25
+ legs = np.concatenate([res.production(res.Ms).ravel(), res.production(res.Ns).ravel()])
26
+ return np.bincount(legs, minlength=n_beads) / res.production(res.Es).shape[0]
27
+
28
+
29
+ def acceptance_table(res) -> list:
30
+ """[(move, proposed, accepted, rate)] for each move type that was used."""
31
+ return [(name, int(p), int(a), a / p) for name, (p, a) in zip(_MOVES, res.acc) if p > 0]
32
+
33
+
34
+ def loop_pairs_of_frame(ms, ns) -> np.ndarray:
35
+ """Unique loops (left, right) of one MC snapshot as an (k, 2) int array; zero-length loops dropped."""
36
+ lo, hi = np.minimum(ms, ns), np.maximum(ms, ns)
37
+ pairs = np.unique(np.c_[lo, hi][hi > lo], axis=0)
38
+ return pairs.astype(int)
39
+
40
+
41
+ # ---- Convergence and equilibrium diagnostics of the Monte Carlo run --------
42
+
43
+ TERMS = {"Es": "Total energy", "Bs": "Binding energy", "Ks": "Crossing energy",
44
+ "Fs": "Folding energy", "ufs": "Unfolded fraction"}
45
+
46
+
47
+ def acf(x, max_lag=None) -> np.ndarray:
48
+ """Normalised autocorrelation function (FFT based), lag 0 = 1."""
49
+ x = np.asarray(x, float) - np.mean(x)
50
+ n = len(x)
51
+ f = np.fft.rfft(x, 2 * n)
52
+ a = np.fft.irfft(f * np.conj(f))[:n]
53
+ a = a / a[0] if a[0] > 0 else np.zeros(n)
54
+ return a[: (max_lag or n // 2) + 1]
55
+
56
+
57
+ def integrated_time(x) -> float:
58
+ """Integrated autocorrelation time in samples (Sokal self-consistent window)."""
59
+ rho = acf(x, len(x) - 1)
60
+ tau = 1.0
61
+ for m in range(1, len(rho)):
62
+ tau = 1 + 2 * rho[1:m + 1].sum()
63
+ if m >= 5 * tau:
64
+ break
65
+ return float(max(tau, 1.0))
66
+
67
+
68
+ def detrend(x, deg: int = 3) -> np.ndarray:
69
+ """Remove a polynomial trend (annealing drifts the energy)."""
70
+ t = np.arange(len(x))
71
+ return np.asarray(x) - np.polyval(np.polyfit(t, x, deg), t)
72
+
73
+
74
+ def equilibrium_summary(res) -> dict:
75
+ """Post-burn-in mean/std per term, drift between halves, and effective sample size."""
76
+ out, annealing = {}, res.meta["method"] == "Annealing"
77
+ for key in TERMS:
78
+ x = res.production(getattr(res, key))
79
+ half = len(x) // 2
80
+ drift = (x[half:].mean() - x[:half].mean()) / (x.std() + 1e-12) if half > 1 else 0.0
81
+ out[key] = dict(mean=float(x.mean()), std=float(x.std()), drift=float(drift))
82
+ x = res.production(res.Fs)
83
+ tau = integrated_time(detrend(x) if annealing else x) if len(x) > 10 else 1.0
84
+ out["tau"], out["ess"], out["n"] = tau, len(x) / tau, len(x)
85
+ return out
86
+
87
+
88
+ # ---- Extra insight metrics for the loop ensemble: anchor recovery, LEF dynamics, length agreement ----
89
+
90
+ def anchor_profile(inputs) -> np.ndarray:
91
+ """Input anchor strength per bead (L + R)."""
92
+ return inputs.loops.L + inputs.loops.R
93
+
94
+
95
+ def anchor_recovery(res, inputs, sigma: float = 1.0) -> dict:
96
+ """How well LEF legs sit on the strongest input anchors.
97
+
98
+ Anchors = beads above the 75th percentile of the non-zero input signal.
99
+ recall = anchors that hold a LEF leg; precision = occupied beads that are anchors.
100
+ """
101
+ occ, sig = occupancy(res, len(inputs.loops.L)), anchor_profile(inputs)
102
+ nz = sig[sig > 0]
103
+ anchors = sig >= (np.percentile(nz, 75) if nz.size else np.inf)
104
+ used = occ > 0.5 * occ.max() if occ.max() > 0 else occ > 0
105
+ rho = spearmanr(gaussian_filter1d(occ, sigma), gaussian_filter1d(sig, sigma))[0] if sig.std() > 0 else np.nan
106
+ return dict(n_anchor_beads=int(anchors.sum()), n_used_beads=int(used.sum()),
107
+ recall=float((anchors & used).sum() / max(anchors.sum(), 1)),
108
+ precision=float((anchors & used).sum() / max(used.sum(), 1)), spearman=float(rho))
109
+
110
+
111
+ def lef_dynamics(res, jump_beads: int = 20) -> dict:
112
+ """Stability of loops between consecutive post-burn-in snapshots."""
113
+ ms, ns = res.production(res.Ms), res.production(res.Ns)
114
+ same = (ms[:, 1:] == ms[:, :-1]) & (ns[:, 1:] == ns[:, :-1])
115
+ jumps = (np.abs(ms[:, 1:] - ms[:, :-1]) > jump_beads) | (np.abs(ns[:, 1:] - ns[:, :-1]) > jump_beads)
116
+ step, n_int = res.meta["mc_step"], max(same.shape[1], 1)
117
+ changes = (~same).sum(axis=1)
118
+ return dict(stable_fraction=float(same.mean()),
119
+ dwell_steps=float(n_int * step / np.maximum(changes, 1).mean()),
120
+ rebinding_per_1000=float(jumps.sum(axis=1).mean() / (n_int * step) * 1000),
121
+ frozen_lefs=float((changes == 0).mean()))
122
+
123
+
124
+ def length_agreement(res, inputs, geom):
125
+ """Input vs simulated loop lengths in kb: (input_kb, sim_kb, KS statistic, p-value)."""
126
+ inp = geom.beads_to_bp(inputs.loops.loop_lengths) / 1e3
127
+ sim = geom.beads_to_bp(loop_lengths(res)) / 1e3
128
+ if inp.size < 2 or sim.size < 2:
129
+ return inp, sim, np.nan, np.nan
130
+ ks = ks_2samp(inp, sim)
131
+ return inp, sim, float(ks.statistic), float(ks.pvalue)
132
+
133
+
134
+ # ---- Agreement between independent MC chains (replicas) --------------------
135
+
136
+ def r_hat(chains) -> float:
137
+ """Gelman-Rubin potential scale reduction of a scalar series per chain (1 = chains agree)."""
138
+ n = min(len(c) for c in chains)
139
+ x = np.array([c[-n:] for c in chains], float)
140
+ m = len(x)
141
+ w, b = x.var(axis=1, ddof=1).mean(), n * x.mean(axis=1).var(ddof=1)
142
+ return float(np.sqrt(((n - 1) / n * w + b / n) / w)) if w > 0 else float("nan")
143
+
144
+
145
+ def replica_summary(res) -> dict:
146
+ """Per-chain energy statistics, occupancy profiles and their pairwise correlation."""
147
+ chains = res.chains
148
+ prod = [c.production(c.Es) for c in chains]
149
+ occ = np.array([occupancy(c, c.meta["n_beads"]) for c in chains])
150
+ corr = np.corrcoef(occ) if len(chains) > 1 else np.ones((1, 1))
151
+ iu = np.triu_indices(len(chains), 1)
152
+ return dict(energy=prod, mean=np.array([e.mean() for e in prod]), std=np.array([e.std() for e in prod]), occ=occ, corr=corr,
153
+ rhat=r_hat(prod) if len(chains) > 1 else float("nan"), min_corr=float(corr[iu].min()) if len(iu[0]) else float("nan"))
154
+
155
+
156
+ # ---- Epigenetic model: states of beads and their link to the loops ----------
157
+
158
+ def state_fractions(S, n_states: int) -> np.ndarray:
159
+ """(n_states, n_beads): fraction of snapshots in which each bead is in each state."""
160
+ return np.stack([(S == s).mean(axis=1) for s in range(n_states)])
161
+
162
+
163
+ def same_state_map(S, n_states: int) -> np.ndarray:
164
+ """P(S_i == S_j) over snapshots for every pair of beads (1 on the diagonal)."""
165
+ out = np.zeros((S.shape[0],) * 2)
166
+ for s in range(n_states):
167
+ x = (S == s).astype(float)
168
+ out += x @ x.T
169
+ return out / S.shape[1]
170
+
171
+
172
+ def loop_state_agreement(res, n_states: int, n_bins: int = 10) -> dict:
173
+ """Do the two ends of a loop share an epigenetic state more often than expected at that distance?
174
+
175
+ Returns bin centres (beads), observed rate for loop ends and baseline rate for any two beads
176
+ the same distance apart (baseline = mean of the same-state map along the matching diagonal).
177
+ """
178
+ S = res.production(res.epi)
179
+ M = same_state_map(S, n_states)
180
+ m, n = res.production(res.Ms), res.production(res.Ns) # (n_lef, n_snapshots)
181
+ d = (n - m).ravel()
182
+ same = (np.take_along_axis(S, m, 0) == np.take_along_axis(S, n, 0)).ravel()
183
+ base_d = np.array([np.diagonal(M, k).mean() if k < M.shape[0] else np.nan for k in range(M.shape[0])])
184
+ edges = np.unique(np.geomspace(max(d.min(), 1), d.max() + 1, n_bins + 1).astype(int))
185
+ idx = np.clip(np.digitize(d, edges) - 1, 0, len(edges) - 2)
186
+ keep = [b for b in range(len(edges) - 1) if (idx == b).any()]
187
+ return dict(
188
+ sep=np.array([d[idx == b].mean() for b in keep]),
189
+ loops=np.array([same[idx == b].mean() for b in keep]),
190
+ baseline=np.array([np.nanmean(base_d[d[idx == b]]) for b in keep]),
191
+ overall=float(same.mean()), overall_base=float(np.nanmean(base_d[d])))
192
+
193
+
194
+ def epigenetic_energy_series(p, res) -> tuple:
195
+ """Epigenetic-model energy at every saved snapshot: (MC steps, energies)."""
196
+ from monte_carlo.energy import E_epi, E_epi_dyn
197
+ out = np.empty(res.n_samples)
198
+ for k in range(res.n_samples):
199
+ S = res.epi[:, k].astype(np.int64)
200
+ ms, ns = res.Ms[:, k].astype(np.int64), res.Ns[:, k].astype(np.int64)
201
+ out[k] = E_epi_dyn(S, ms, ns, p.h, p.epi_norm) if p.epi_dynamic else E_epi(S, p.J, p.h, p.epi_norm)
202
+ return np.arange(res.n_samples) * res.meta["mc_step"], out
@@ -0,0 +1,127 @@
1
+ """Anchor-level view of a BEDPE: position, strength, CTCF orientation and its confidence.
2
+
3
+ - Compartment (A/B) domains and their relation to loop anchors.
4
+ """
5
+ from dataclasses import dataclass
6
+
7
+ import numpy as np
8
+ from scipy.stats import mannwhitneyu
9
+
10
+ from preproc.readers import numeric_col, read_region_table, valid_prob
11
+
12
+
13
+ # ---- Anchor-level view of a BEDPE: position, strength, CTCF orientation and its confidence ----
14
+
15
+ CONF_MIN = 0.2 # below this an orientation call is treated as uncertain
16
+ CONFIGS = ("convergent", "tandem", "divergent", "uncertain")
17
+
18
+
19
+ @dataclass
20
+ class Anchors:
21
+ bp: np.ndarray # anchor centre (bp)
22
+ strength: np.ndarray # loop weight carried by the anchor
23
+ p_rev: np.ndarray # P(reverse), -1 = unknown
24
+ loop: np.ndarray # index of the loop each anchor belongs to
25
+ left_bp: np.ndarray # per loop: left / right anchor centre (bp)
26
+ right_bp: np.ndarray
27
+ loop_strength: np.ndarray
28
+ config: np.ndarray # per loop: one of CONFIGS
29
+
30
+ @property
31
+ def confidence(self) -> np.ndarray:
32
+ return np.where(self.p_rev >= 0, np.abs(2 * self.p_rev - 1), 0.0)
33
+
34
+ @property
35
+ def orient(self) -> np.ndarray:
36
+ """'forward' / 'reverse' / 'ambiguous' / 'unknown' per anchor."""
37
+ o = np.where(self.p_rev < 0.5, "forward", "reverse").astype(object)
38
+ o[self.confidence < CONF_MIN] = "ambiguous"
39
+ o[self.p_rev < 0] = "unknown"
40
+ return o
41
+
42
+ @property
43
+ def loop_len_bp(self) -> np.ndarray:
44
+ return self.right_bp - self.left_bp
45
+
46
+
47
+ def _config(p_left, p_right) -> np.ndarray:
48
+ """Loop configuration from the orientations of its left and right anchors."""
49
+ sure = lambda p: (p >= 0) & (np.abs(2 * p - 1) >= CONF_MIN)
50
+ ok = sure(p_left) & sure(p_right)
51
+ lf, rf = p_left < 0.5, p_right < 0.5
52
+ out = np.full(len(p_left), "uncertain", dtype=object)
53
+ out[ok & lf & ~rf] = "convergent" # left forward, right reverse: -> ... <-
54
+ out[ok & ~lf & rf] = "divergent"
55
+ out[ok & (lf == rf)] = "tandem"
56
+ return out
57
+
58
+
59
+ def anchor_table(path, geom):
60
+ """Anchors of every BEDPE loop in the region, or None if the file has no loops."""
61
+ df = read_region_table(path, geom, paired=True)
62
+ if df.empty:
63
+ return None
64
+ c1, c2 = (df[1] + df[2]).to_numpy() / 2, (df[4] + df[5]).to_numpy() / 2
65
+ w = numeric_col(df, 6, 1.0)
66
+ p1, p2 = numeric_col(df, 7, -1.0), numeric_col(df, 8, -1.0)
67
+ if not (valid_prob(p1) and valid_prob(p2)):
68
+ p1 = p2 = np.full(len(df), -1.0)
69
+ swap = c1 > c2 # make anchor 1 the left one
70
+ cl, cr = np.where(swap, c2, c1), np.where(swap, c1, c2)
71
+ pl, pr = np.where(swap, p2, p1), np.where(swap, p1, p2)
72
+ n = len(df)
73
+ return Anchors(bp=np.concatenate([cl, cr]), strength=np.concatenate([w, w]), p_rev=np.concatenate([pl, pr]),
74
+ loop=np.concatenate([np.arange(n), np.arange(n)]), left_bp=cl, right_bp=cr,
75
+ loop_strength=w, config=_config(pl, pr))
76
+
77
+
78
+ def orientation_summary(a: Anchors) -> list:
79
+ """Rows [class, anchors, share, median strength, mean confidence]."""
80
+ rows, o = [], a.orient
81
+ for name in ("forward", "reverse", "ambiguous", "unknown"):
82
+ m = o == name
83
+ rows.append([name, int(m.sum()), m.mean(), float(np.median(a.strength[m])) if m.any() else 0.0,
84
+ float(a.confidence[m].mean()) if m.any() else 0.0])
85
+ return rows
86
+
87
+
88
+ def config_summary(a: Anchors) -> list:
89
+ """Rows [configuration, loops, share, mean length (kb), mean strength]."""
90
+ rows = []
91
+ for name in CONFIGS:
92
+ m = a.config == name
93
+ rows.append([name, int(m.sum()), m.mean(), float(a.loop_len_bp[m].mean() / 1e3) if m.any() else 0.0,
94
+ float(a.loop_strength[m].mean()) if m.any() else 0.0])
95
+ return rows
96
+
97
+
98
+ # ---- Compartment (A/B) domains and their relation to loop anchors ----------
99
+
100
+ def domains(comp, min_beads: int = 2):
101
+ """Runs of equal sign: list of (first_bead, last_bead, 'A' or 'B'); runs shorter than min_beads are merged away."""
102
+ sign = np.where(np.asarray(comp) >= 0, 1, -1)
103
+ edges = np.flatnonzero(np.diff(sign)) + 1
104
+ out = []
105
+ for s, e in zip(np.r_[0, edges], np.r_[edges, len(sign)]):
106
+ if e - s >= min_beads or not out:
107
+ out.append([s, e - 1, "A" if sign[s] > 0 else "B"])
108
+ else:
109
+ out[-1][1] = e - 1 # absorb a flicker into the previous domain
110
+ merged = []
111
+ for d in out: # fuse neighbours of the same type
112
+ if merged and merged[-1][2] == d[2]:
113
+ merged[-1][1] = d[1]
114
+ else:
115
+ merged.append(d)
116
+ return [tuple(d) for d in merged]
117
+
118
+
119
+ def compartment_summary(comp, anchor_strength, geom) -> dict:
120
+ """Shares of A/B, domain sizes (kb) and anchor strength per compartment."""
121
+ doms, bp = domains(comp), geom.bp_per_bead
122
+ size = lambda t: np.array([(e - s + 1) * bp / 1e3 for s, e, k in doms if k == t])
123
+ a_mask = np.asarray(comp) >= 0
124
+ in_a, in_b = anchor_strength[a_mask], anchor_strength[~a_mask]
125
+ p = mannwhitneyu(in_a, in_b).pvalue if len(in_a) > 2 and len(in_b) > 2 and anchor_strength.std() > 0 else np.nan
126
+ return dict(frac_a=float(a_mask.mean()), n_domains=len(doms), size_a=size("A"), size_b=size("B"),
127
+ anchors_a=in_a, anchors_b=in_b, p=float(p), domains=doms)