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.
- pyloopsage-2.0.0rc2/MANIFEST.in +2 -0
- pyloopsage-2.0.0rc2/PKG-INFO +132 -0
- pyloopsage-2.0.0rc2/README.md +84 -0
- {pyloopsage-1.1.5 → pyloopsage-2.0.0rc2}/setup.py +14 -6
- pyloopsage-2.0.0rc2/src/analysis/__init__.py +1 -0
- pyloopsage-2.0.0rc2/src/analysis/ensemble.py +202 -0
- pyloopsage-2.0.0rc2/src/analysis/inputs.py +127 -0
- pyloopsage-2.0.0rc2/src/analysis/maps.py +286 -0
- pyloopsage-2.0.0rc2/src/analysis/nesting.py +84 -0
- pyloopsage-2.0.0rc2/src/analysis/speed.py +248 -0
- pyloopsage-2.0.0rc2/src/analysis/structure.py +307 -0
- pyloopsage-2.0.0rc2/src/common/__init__.py +1 -0
- pyloopsage-2.0.0rc2/src/common/banner.py +122 -0
- pyloopsage-2.0.0rc2/src/common/genome.py +94 -0
- pyloopsage-2.0.0rc2/src/common/runtime.py +214 -0
- pyloopsage-2.0.0rc2/src/common/utils.py +100 -0
- pyloopsage-2.0.0rc2/src/html/style.css +53 -0
- pyloopsage-2.0.0rc2/src/html/template.html +10 -0
- pyloopsage-2.0.0rc2/src/html/viewer_template.html +148 -0
- pyloopsage-2.0.0rc2/src/knots/__init__.py +1 -0
- pyloopsage-2.0.0rc2/src/knots/crossing.py +100 -0
- pyloopsage-2.0.0rc2/src/knots/figures.py +185 -0
- pyloopsage-2.0.0rc2/src/knots/linking.py +231 -0
- pyloopsage-2.0.0rc2/src/knots/stage.py +106 -0
- pyloopsage-2.0.0rc2/src/logger.py +69 -0
- pyloopsage-2.0.0rc2/src/molecular_dynamics/__init__.py +1 -0
- pyloopsage-2.0.0rc2/src/molecular_dynamics/engine.py +316 -0
- {pyloopsage-1.1.5/loopsage → pyloopsage-2.0.0rc2/src/molecular_dynamics}/forcefields/classic_sm_ff.xml +1 -1
- pyloopsage-2.0.0rc2/src/molecular_dynamics/forces.py +79 -0
- pyloopsage-2.0.0rc2/src/molecular_dynamics/structures.py +165 -0
- pyloopsage-2.0.0rc2/src/molecular_dynamics/topology.py +48 -0
- pyloopsage-2.0.0rc2/src/monte_carlo/__init__.py +1 -0
- pyloopsage-2.0.0rc2/src/monte_carlo/energy.py +274 -0
- pyloopsage-2.0.0rc2/src/monte_carlo/sampler.py +192 -0
- pyloopsage-2.0.0rc2/src/monte_carlo/simulation.py +143 -0
- pyloopsage-2.0.0rc2/src/motif_finder/__init__.py +5 -0
- pyloopsage-2.0.0rc2/src/motif_finder/annotate.py +180 -0
- pyloopsage-2.0.0rc2/src/motif_finder/data/MA0139.1.pfm +4 -0
- pyloopsage-2.0.0rc2/src/motif_finder/fasta.py +94 -0
- pyloopsage-2.0.0rc2/src/motif_finder/pssm.py +92 -0
- pyloopsage-2.0.0rc2/src/optimizer/__init__.py +1 -0
- pyloopsage-2.0.0rc2/src/optimizer/nelder_mead.py +196 -0
- pyloopsage-2.0.0rc2/src/optimizer/objective.py +202 -0
- pyloopsage-2.0.0rc2/src/pipeline/__init__.py +1 -0
- pyloopsage-2.0.0rc2/src/pipeline/runner.py +139 -0
- pyloopsage-2.0.0rc2/src/pipeline/stage_analysis.py +91 -0
- pyloopsage-2.0.0rc2/src/pipeline/stage_mc.py +201 -0
- pyloopsage-2.0.0rc2/src/pipeline/stage_md.py +68 -0
- pyloopsage-2.0.0rc2/src/pipeline/stage_optimize.py +205 -0
- pyloopsage-2.0.0rc2/src/pipeline/stage_preproc.py +199 -0
- pyloopsage-2.0.0rc2/src/pipeline/stage_report.py +30 -0
- pyloopsage-2.0.0rc2/src/pipeline/stage_structure.py +133 -0
- pyloopsage-2.0.0rc2/src/preproc/__init__.py +1 -0
- pyloopsage-2.0.0rc2/src/preproc/hic.py +165 -0
- pyloopsage-2.0.0rc2/src/preproc/loader.py +153 -0
- pyloopsage-2.0.0rc2/src/preproc/readers.py +261 -0
- pyloopsage-2.0.0rc2/src/pyLoopSage.egg-info/PKG-INFO +132 -0
- pyloopsage-2.0.0rc2/src/pyLoopSage.egg-info/SOURCES.txt +107 -0
- pyloopsage-2.0.0rc2/src/pyLoopSage.egg-info/entry_points.txt +2 -0
- {pyloopsage-1.1.5 → pyloopsage-2.0.0rc2/src}/pyLoopSage.egg-info/requires.txt +1 -0
- pyloopsage-2.0.0rc2/src/pyLoopSage.egg-info/top_level.txt +18 -0
- pyloopsage-2.0.0rc2/src/reporting/__init__.py +1 -0
- pyloopsage-2.0.0rc2/src/reporting/explanations.py +485 -0
- pyloopsage-2.0.0rc2/src/reporting/report.py +204 -0
- pyloopsage-2.0.0rc2/src/run.py +20 -0
- pyloopsage-2.0.0rc2/src/settings/__init__.py +1 -0
- pyloopsage-2.0.0rc2/src/settings/loader.py +266 -0
- pyloopsage-2.0.0rc2/src/settings/schema.py +174 -0
- pyloopsage-2.0.0rc2/src/validation/__init__.py +1 -0
- pyloopsage-2.0.0rc2/src/validation/compare.py +284 -0
- pyloopsage-2.0.0rc2/src/validation/hic_compare.py +68 -0
- pyloopsage-2.0.0rc2/src/version.py +2 -0
- pyloopsage-2.0.0rc2/src/viz/__init__.py +1 -0
- pyloopsage-2.0.0rc2/src/viz/hic_figs.py +94 -0
- pyloopsage-2.0.0rc2/src/viz/input_figs.py +236 -0
- pyloopsage-2.0.0rc2/src/viz/map_figs.py +180 -0
- pyloopsage-2.0.0rc2/src/viz/mc_figs.py +456 -0
- pyloopsage-2.0.0rc2/src/viz/motif_figs.py +39 -0
- pyloopsage-2.0.0rc2/src/viz/nesting_figs.py +97 -0
- pyloopsage-2.0.0rc2/src/viz/optimizer_figs.py +58 -0
- pyloopsage-2.0.0rc2/src/viz/speed_figs.py +217 -0
- pyloopsage-2.0.0rc2/src/viz/structure_figs.py +276 -0
- pyloopsage-2.0.0rc2/src/viz/surface3d.py +83 -0
- pyloopsage-2.0.0rc2/src/viz/theme.py +206 -0
- pyloopsage-2.0.0rc2/src/viz/validation_figs.py +207 -0
- pyloopsage-2.0.0rc2/src/viz3d/__init__.py +1 -0
- pyloopsage-2.0.0rc2/src/viz3d/render.py +166 -0
- pyloopsage-2.0.0rc2/tests/test_correlation.py +22 -0
- pyloopsage-2.0.0rc2/tests/test_energy.py +14 -0
- pyloopsage-2.0.0rc2/tests/test_preproc.py +31 -0
- pyloopsage-2.0.0rc2/tests/test_src_3d_knots.py +110 -0
- pyloopsage-2.0.0rc2/tests/test_src_analysis.py +164 -0
- pyloopsage-2.0.0rc2/tests/test_src_config.py +66 -0
- pyloopsage-2.0.0rc2/tests/test_src_config_contract.py +90 -0
- pyloopsage-2.0.0rc2/tests/test_src_console.py +65 -0
- pyloopsage-2.0.0rc2/tests/test_src_energy.py +237 -0
- pyloopsage-2.0.0rc2/tests/test_src_hic.py +165 -0
- pyloopsage-2.0.0rc2/tests/test_src_inputs_viz.py +79 -0
- pyloopsage-2.0.0rc2/tests/test_src_motif.py +240 -0
- pyloopsage-2.0.0rc2/tests/test_src_nesting.py +73 -0
- pyloopsage-2.0.0rc2/tests/test_src_optimizer.py +197 -0
- pyloopsage-2.0.0rc2/tests/test_src_pipeline.py +134 -0
- pyloopsage-2.0.0rc2/tests/test_src_preproc.py +75 -0
- pyloopsage-2.0.0rc2/tests/test_src_speed.py +116 -0
- pyloopsage-2.0.0rc2/tests/test_src_topology.py +153 -0
- pyloopsage-2.0.0rc2/tests/test_src_units_stats.py +57 -0
- pyloopsage-1.1.5/MANIFEST.in +0 -1
- pyloopsage-1.1.5/PKG-INFO +0 -394
- pyloopsage-1.1.5/README.md +0 -347
- pyloopsage-1.1.5/loopsage/__init__.py +0 -4
- pyloopsage-1.1.5/loopsage/args_definition.py +0 -218
- pyloopsage-1.1.5/loopsage/em.py +0 -135
- pyloopsage-1.1.5/loopsage/initial_structures.py +0 -137
- pyloopsage-1.1.5/loopsage/knots.py +0 -210
- pyloopsage-1.1.5/loopsage/md.py +0 -182
- pyloopsage-1.1.5/loopsage/plots.py +0 -254
- pyloopsage-1.1.5/loopsage/preproc.py +0 -124
- pyloopsage-1.1.5/loopsage/run.py +0 -102
- pyloopsage-1.1.5/loopsage/stochastic_simulation.py +0 -380
- pyloopsage-1.1.5/loopsage/utils.py +0 -476
- pyloopsage-1.1.5/loopsage/vizualization_tools.py +0 -153
- pyloopsage-1.1.5/pyLoopSage.egg-info/PKG-INFO +0 -394
- pyloopsage-1.1.5/pyLoopSage.egg-info/SOURCES.txt +0 -23
- pyloopsage-1.1.5/pyLoopSage.egg-info/entry_points.txt +0 -2
- pyloopsage-1.1.5/pyLoopSage.egg-info/top_level.txt +0 -1
- {pyloopsage-1.1.5 → pyloopsage-2.0.0rc2}/LICENSE +0 -0
- {pyloopsage-1.1.5 → pyloopsage-2.0.0rc2}/setup.cfg +0 -0
- {pyloopsage-1.1.5 → pyloopsage-2.0.0rc2/src}/pyLoopSage.egg-info/dependency_links.txt +0 -0
|
@@ -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,
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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)
|