tfmindi 1.0.0__py3-none-any.whl
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.
- tfmindi/__init__.py +46 -0
- tfmindi/backends/__init__.py +151 -0
- tfmindi/datasets.py +416 -0
- tfmindi/io.py +213 -0
- tfmindi/pl/__init__.py +21 -0
- tfmindi/pl/_utils.py +125 -0
- tfmindi/pl/contributions.py +269 -0
- tfmindi/pl/dbd_heatmap.py +133 -0
- tfmindi/pl/logo.py +313 -0
- tfmindi/pl/region_topics.py +234 -0
- tfmindi/pl/tsne.py +343 -0
- tfmindi/pp/__init__.py +5 -0
- tfmindi/pp/mappings.py +116 -0
- tfmindi/pp/seqlets.py +700 -0
- tfmindi/tl/__init__.py +19 -0
- tfmindi/tl/cluster.py +184 -0
- tfmindi/tl/patterns.py +231 -0
- tfmindi/tl/topic_modeling.py +260 -0
- tfmindi/types.py +161 -0
- tfmindi-1.0.0.dist-info/METADATA +204 -0
- tfmindi-1.0.0.dist-info/RECORD +23 -0
- tfmindi-1.0.0.dist-info/WHEEL +4 -0
- tfmindi-1.0.0.dist-info/licenses/LICENSE +21 -0
tfmindi/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Package initialization for tfmindi."""
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
|
|
6
|
+
# Suppress numba hashing warning from tangermeme
|
|
7
|
+
warnings.filterwarnings("ignore", message=".*FNV hashing is not implemented in Numba.*", category=UserWarning)
|
|
8
|
+
|
|
9
|
+
# Pre-import rapids_singlecell to establish proper CUDA environment
|
|
10
|
+
# This must happen BEFORE any other CUDA libraries are imported
|
|
11
|
+
try:
|
|
12
|
+
import rapids_singlecell # type: ignore
|
|
13
|
+
except ImportError:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
from tfmindi import pl, pp, tl # noqa: E402
|
|
17
|
+
from tfmindi.backends import get_backend, is_gpu_available, set_backend # noqa: E402
|
|
18
|
+
from tfmindi.datasets import ( # noqa: E402
|
|
19
|
+
fetch_motif_annotations,
|
|
20
|
+
fetch_motif_collection,
|
|
21
|
+
load_motif_annotations,
|
|
22
|
+
load_motif_collection,
|
|
23
|
+
load_motif_to_dbd,
|
|
24
|
+
)
|
|
25
|
+
from tfmindi.io import load_h5ad, save_h5ad # noqa: E402
|
|
26
|
+
from tfmindi.types import Pattern, Seqlet # noqa: E402
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"pl",
|
|
30
|
+
"pp",
|
|
31
|
+
"tl",
|
|
32
|
+
"Pattern",
|
|
33
|
+
"Seqlet",
|
|
34
|
+
"get_backend",
|
|
35
|
+
"set_backend",
|
|
36
|
+
"is_gpu_available",
|
|
37
|
+
"fetch_motif_collection",
|
|
38
|
+
"fetch_motif_annotations",
|
|
39
|
+
"load_motif_collection",
|
|
40
|
+
"load_motif_annotations",
|
|
41
|
+
"load_motif_to_dbd",
|
|
42
|
+
"save_h5ad",
|
|
43
|
+
"load_h5ad",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
__version__ = version("tfmindi")
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Backend detection and configuration for CPU/GPU acceleration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import warnings
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
Backend = Literal["cpu", "gpu"]
|
|
10
|
+
|
|
11
|
+
# Global backend state
|
|
12
|
+
_backend: Backend | None = None
|
|
13
|
+
_gpu_available: bool | None = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _check_gpu_availability() -> bool:
|
|
17
|
+
"""Check if GPU acceleration packages are available."""
|
|
18
|
+
global _gpu_available
|
|
19
|
+
if _gpu_available is not None:
|
|
20
|
+
return _gpu_available
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import cupy as cp # type: ignore
|
|
24
|
+
|
|
25
|
+
device_count = cp.cuda.runtime.getDeviceCount()
|
|
26
|
+
_gpu_available = device_count > 0
|
|
27
|
+
return _gpu_available # type: ignore
|
|
28
|
+
except ImportError:
|
|
29
|
+
_gpu_available = False
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_backend() -> str:
|
|
34
|
+
"""
|
|
35
|
+
Get the current computational backend.
|
|
36
|
+
|
|
37
|
+
Returns the backend based on the following priority:
|
|
38
|
+
1. Explicitly set backend via set_backend()
|
|
39
|
+
2. Environment variable TFMINDI_BACKEND
|
|
40
|
+
3. Automatic detection based on GPU availability
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
Backend type: "cpu" or "gpu"
|
|
45
|
+
"""
|
|
46
|
+
global _backend
|
|
47
|
+
|
|
48
|
+
if _backend is not None:
|
|
49
|
+
return _backend
|
|
50
|
+
|
|
51
|
+
# Check environment variable
|
|
52
|
+
env_backend = os.getenv("TFMINDI_BACKEND", "").lower()
|
|
53
|
+
if env_backend in ["cpu", "gpu"]:
|
|
54
|
+
_backend = env_backend # type: ignore
|
|
55
|
+
if _backend == "gpu" and not _check_gpu_availability():
|
|
56
|
+
warnings.warn(
|
|
57
|
+
"GPU backend requested but GPU packages not available. "
|
|
58
|
+
"Install with 'pip install tfmindi[gpu]'. Falling back to CPU.",
|
|
59
|
+
UserWarning,
|
|
60
|
+
stacklevel=2,
|
|
61
|
+
)
|
|
62
|
+
_backend = "cpu"
|
|
63
|
+
return _backend # type: ignore
|
|
64
|
+
|
|
65
|
+
# Auto-detect based on availability
|
|
66
|
+
_backend = "gpu" if _check_gpu_availability() else "cpu"
|
|
67
|
+
return _backend
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def set_backend(backend: Backend) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Explicitly set the computational backend.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
backend
|
|
77
|
+
Backend type: "cpu" or "gpu"
|
|
78
|
+
|
|
79
|
+
Raises
|
|
80
|
+
------
|
|
81
|
+
ValueError
|
|
82
|
+
If backend is not supported
|
|
83
|
+
ImportError
|
|
84
|
+
If GPU backend is requested but packages are not available
|
|
85
|
+
"""
|
|
86
|
+
global _backend
|
|
87
|
+
|
|
88
|
+
if backend not in ["cpu", "gpu"]:
|
|
89
|
+
raise ValueError(f"Invalid backend: {backend}. Must be 'cpu' or 'gpu'.")
|
|
90
|
+
|
|
91
|
+
if backend == "gpu" and not _check_gpu_availability():
|
|
92
|
+
raise ImportError(
|
|
93
|
+
"GPU backend requested but required packages not available. Install with 'pip install tfmindi[gpu]'."
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
_backend = backend
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def is_gpu_available() -> bool:
|
|
100
|
+
"""Check if GPU acceleration is available."""
|
|
101
|
+
return _check_gpu_availability()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_array_module() -> Any:
|
|
105
|
+
"""Get the appropriate array module (numpy or cupy) based on backend."""
|
|
106
|
+
if get_backend() == "gpu":
|
|
107
|
+
try:
|
|
108
|
+
import cupy as cp
|
|
109
|
+
|
|
110
|
+
return cp
|
|
111
|
+
except ImportError:
|
|
112
|
+
warnings.warn("CuPy not available, falling back to NumPy", UserWarning, stacklevel=2)
|
|
113
|
+
import numpy as np
|
|
114
|
+
|
|
115
|
+
return np
|
|
116
|
+
else:
|
|
117
|
+
import numpy as np
|
|
118
|
+
|
|
119
|
+
return np
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def to_cpu(array: Any) -> Any:
|
|
123
|
+
"""Transfer array to CPU memory if it's on GPU."""
|
|
124
|
+
if hasattr(array, "get"): # CuPy array
|
|
125
|
+
return array.get()
|
|
126
|
+
return array
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def to_gpu(array: Any) -> Any:
|
|
130
|
+
"""Transfer array to GPU memory if GPU backend is active."""
|
|
131
|
+
if get_backend() == "gpu":
|
|
132
|
+
try:
|
|
133
|
+
import cupy as cp
|
|
134
|
+
|
|
135
|
+
if hasattr(array, "get"): # Already on GPU
|
|
136
|
+
return array
|
|
137
|
+
return cp.asarray(array)
|
|
138
|
+
except ImportError:
|
|
139
|
+
warnings.warn("CuPy not available, keeping array on CPU", UserWarning, stacklevel=2)
|
|
140
|
+
return array
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
__all__ = [
|
|
144
|
+
"Backend",
|
|
145
|
+
"get_backend",
|
|
146
|
+
"set_backend",
|
|
147
|
+
"is_gpu_available",
|
|
148
|
+
"get_array_module",
|
|
149
|
+
"to_cpu",
|
|
150
|
+
"to_gpu",
|
|
151
|
+
]
|
tfmindi/datasets.py
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""Dataset functions for TF-MInDi: fetching and loading motif collections and annotations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import zipfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
import pooch
|
|
12
|
+
|
|
13
|
+
_motif_index = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_motif_index():
|
|
17
|
+
"""
|
|
18
|
+
Set up the pooch motif collection registry from pycistarget.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
pooch.Pooch
|
|
23
|
+
The motif collection registry.
|
|
24
|
+
"""
|
|
25
|
+
global _motif_index
|
|
26
|
+
|
|
27
|
+
if _motif_index is None:
|
|
28
|
+
_motif_index = pooch.create(
|
|
29
|
+
path=pooch.os_cache("tfmindi"),
|
|
30
|
+
base_url="https://resources.aertslab.org/cistarget/",
|
|
31
|
+
env="TFMINDI_DATA_DIR",
|
|
32
|
+
registry={
|
|
33
|
+
# Motif collections
|
|
34
|
+
"motif_collections/v10nr_clust_public/v10nr_clust_public.zip": "sha256:70dab42794f42471a3c22f5efe78ec2c8af96127656607cb0a929b4adffc2b97",
|
|
35
|
+
# Motif annotations (motifs-v{version}-nr.{species}-m0.001-o0.0.tbl)
|
|
36
|
+
# v8 (only Drosophila)
|
|
37
|
+
"motif2tf/motifs-v8-nr.flybase-m0.001-o0.0.tbl": "sha256:ffc550325334507c8ab2f6f79170fd0dfbcbecb33aee96040bf1f386fb3f982d",
|
|
38
|
+
# v9 annotations
|
|
39
|
+
"motif2tf/motifs-v9-nr.flybase-m0.001-o0.0.tbl": "sha256:db3458fdb38616758ea54543a42c7fc0e0ebb292cbf928d2648aefbf2cd59314",
|
|
40
|
+
"motif2tf/motifs-v9-nr.hgnc-m0.001-o0.0.tbl": "sha256:9e085e8e6ecd6f73a47fe435ed50d583b92bf7814d1bf38ae358e73c6207da2b",
|
|
41
|
+
"motif2tf/motifs-v9-nr.mgi-m0.001-o0.0.tbl": "sha256:cfab1245dbe770b1073f8048b1316285c9ce4b1a55d8ebca78962a2f406a172d",
|
|
42
|
+
# v10nr_clust annotations
|
|
43
|
+
"motif2tf/motifs-v10nr_clust-nr.chicken-m0.001-o0.0.tbl": "sha256:1ede59e4a737d822b7d6713243b83bf8f618f978c4fae1810fab265a30dfe3ba",
|
|
44
|
+
"motif2tf/motifs-v10nr_clust-nr.flybase-m0.001-o0.0.tbl": "sha256:91284e94b0317b764dc2f8d8147d30db707605df757ff7de16fb0953c63fda2a",
|
|
45
|
+
"motif2tf/motifs-v10nr_clust-nr.hgnc-m0.001-o0.0.tbl": "sha256:81eb754118e27e854974301b1400fcf519489f8be5249239671fb288cb501c31",
|
|
46
|
+
"motif2tf/motifs-v10nr_clust-nr.mgi-m0.001-o0.0.tbl": "sha256:5b64aad9df9804d50c50484c92d5192bdd5d2056cb105bdd343c0af2f94cce83",
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return _motif_index
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def fetch_motif_collection() -> str:
|
|
54
|
+
"""
|
|
55
|
+
Download motif collection (motif names and PWM) from aertslab' pycistarget resources.
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
Path to downloaded motif collection folder
|
|
60
|
+
|
|
61
|
+
Examples
|
|
62
|
+
--------
|
|
63
|
+
>>> motif_dir = fetch_motif_collection()
|
|
64
|
+
>>> print(motif_dir)
|
|
65
|
+
"""
|
|
66
|
+
# Mapping of collection names to registry keys
|
|
67
|
+
name = "v10nr_clust" # only one collection name
|
|
68
|
+
collection_mapping = {
|
|
69
|
+
"v10nr_clust": "motif_collections/v10nr_clust_public/v10nr_clust_public.zip",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
registry_key = collection_mapping[name]
|
|
73
|
+
|
|
74
|
+
def _extract_singletons(fname, action, pooch_obj):
|
|
75
|
+
"""Extract only the singletons folder from the zip file."""
|
|
76
|
+
extract_dir = Path(fname).parent / Path(fname).stem / "singletons"
|
|
77
|
+
extract_dir.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
|
|
79
|
+
with zipfile.ZipFile(fname, "r") as zip_file:
|
|
80
|
+
singletons_files = [
|
|
81
|
+
f for f in zip_file.namelist() if f.startswith("v10nr_clust_public/singletons/") and not f.endswith("/")
|
|
82
|
+
]
|
|
83
|
+
for file_path in singletons_files:
|
|
84
|
+
file_name = Path(file_path).name
|
|
85
|
+
with zip_file.open(file_path) as source:
|
|
86
|
+
with open(extract_dir / file_name, "wb") as target:
|
|
87
|
+
target.write(source.read())
|
|
88
|
+
return str(extract_dir)
|
|
89
|
+
|
|
90
|
+
motif_dir = _get_motif_index().fetch(registry_key, processor=_extract_singletons, progressbar=True)
|
|
91
|
+
|
|
92
|
+
return motif_dir
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def fetch_motif_annotations(species: str = "hgnc", version: str = "v10nr_clust") -> str:
|
|
96
|
+
"""
|
|
97
|
+
Download motif annotations from aertslab resources.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
species
|
|
102
|
+
Species name. Available options:
|
|
103
|
+
- 'hgnc' (human): v9, v10nr_clust
|
|
104
|
+
- 'mgi' (mouse): v9, v10nr_clust
|
|
105
|
+
- 'flybase' (fly): v8, v9, v10nr_clust
|
|
106
|
+
- 'chicken': v10nr_clust only
|
|
107
|
+
version
|
|
108
|
+
Motif collection version. Available options: 'v8', 'v9', 'v10nr_clust'
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
Path to downloaded annotations file
|
|
113
|
+
|
|
114
|
+
Examples
|
|
115
|
+
--------
|
|
116
|
+
>>> annotations_file = fetch_motif_annotations("hgnc", "v10nr_clust")
|
|
117
|
+
>>> print(annotations_file)
|
|
118
|
+
/path/to/cache/motifs-v10nr_clust-nr.hgnc-m0.001-o0.0.tbl
|
|
119
|
+
"""
|
|
120
|
+
# Mapping of species and versions to registry keys
|
|
121
|
+
annotation_mapping = {
|
|
122
|
+
("flybase", "v8"): "motif2tf/motifs-v8-nr.flybase-m0.001-o0.0.tbl",
|
|
123
|
+
("flybase", "v9"): "motif2tf/motifs-v9-nr.flybase-m0.001-o0.0.tbl",
|
|
124
|
+
("hgnc", "v9"): "motif2tf/motifs-v9-nr.hgnc-m0.001-o0.0.tbl",
|
|
125
|
+
("mgi", "v9"): "motif2tf/motifs-v9-nr.mgi-m0.001-o0.0.tbl",
|
|
126
|
+
("chicken", "v10nr_clust"): "motif2tf/motifs-v10nr_clust-nr.chicken-m0.001-o0.0.tbl",
|
|
127
|
+
("flybase", "v10nr_clust"): "motif2tf/motifs-v10nr_clust-nr.flybase-m0.001-o0.0.tbl",
|
|
128
|
+
("hgnc", "v10nr_clust"): "motif2tf/motifs-v10nr_clust-nr.hgnc-m0.001-o0.0.tbl",
|
|
129
|
+
("mgi", "v10nr_clust"): "motif2tf/motifs-v10nr_clust-nr.mgi-m0.001-o0.0.tbl",
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
key = (species, version)
|
|
133
|
+
assert key in annotation_mapping, (
|
|
134
|
+
f"Species {species} with version {version} is not recognised. "
|
|
135
|
+
f"Available combinations: {list(annotation_mapping.keys())}"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
registry_key = annotation_mapping[key]
|
|
139
|
+
|
|
140
|
+
annotations_file = _get_motif_index().fetch(registry_key, progressbar=True)
|
|
141
|
+
|
|
142
|
+
return annotations_file
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def load_motif(file_name: str) -> dict[tuple[str, str], np.ndarray]:
|
|
146
|
+
"""
|
|
147
|
+
Load motif(s) from a single .cb file.
|
|
148
|
+
|
|
149
|
+
Scales the PWM values to sum to 1 across each position (PPM).
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
----------
|
|
153
|
+
file_name
|
|
154
|
+
Path to the .cb motif file
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
dict[tuple[str, str], np.ndarray]
|
|
159
|
+
Dictionary mapping motif file names and names to PWM matrices (4 x length)
|
|
160
|
+
|
|
161
|
+
Examples
|
|
162
|
+
--------
|
|
163
|
+
>>> motifs = load_motif("./motif1.cb")
|
|
164
|
+
>>> print(list(motifs.keys()))
|
|
165
|
+
[("filename_1", "motif_1"), ("filename_2", "motif_2")]
|
|
166
|
+
>>> print(motifs[("filename_1", "motif_1")].shape)
|
|
167
|
+
(4, 12)
|
|
168
|
+
"""
|
|
169
|
+
motifs: dict[tuple[str, str], np.ndarray] = {}
|
|
170
|
+
with open(file_name) as f:
|
|
171
|
+
name = f.readline().strip()
|
|
172
|
+
if not name.startswith(">"):
|
|
173
|
+
raise ValueError(f"First line of {file_name} does not start with '>'.")
|
|
174
|
+
name = name.replace(">", "")
|
|
175
|
+
key = (os.path.basename(file_name).replace(".cb", ""), name)
|
|
176
|
+
pwm: list[list[float]] = []
|
|
177
|
+
for line in f:
|
|
178
|
+
line = line.strip()
|
|
179
|
+
if line.startswith(">"):
|
|
180
|
+
# we are at the start of a new motif
|
|
181
|
+
motifs[key] = np.array(pwm)
|
|
182
|
+
# scale values of motif
|
|
183
|
+
motifs[key] = motifs[key].T / motifs[key].sum(1)
|
|
184
|
+
# reset pwm and read new name
|
|
185
|
+
name = line.replace(">", "")
|
|
186
|
+
key = (os.path.basename(file_name).replace(".cb", ""), name)
|
|
187
|
+
pwm = []
|
|
188
|
+
else:
|
|
189
|
+
# we are in the middle of reading the pwm values
|
|
190
|
+
pwm.append([float(v) for v in line.split()])
|
|
191
|
+
# add last motif
|
|
192
|
+
motifs[key] = np.array(pwm)
|
|
193
|
+
# scale values of motif
|
|
194
|
+
motifs[key] = motifs[key].T / motifs[key].sum(1)
|
|
195
|
+
return motifs
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def load_motif_collection(motif_dir: str, motif_names: list[str] | None = None) -> dict[tuple[str, str], np.ndarray]:
|
|
199
|
+
"""
|
|
200
|
+
Load motif collection from directory of .cb files.
|
|
201
|
+
|
|
202
|
+
Converts motif PWM matrices to PPM (position probability matrix) format.
|
|
203
|
+
|
|
204
|
+
Parameters
|
|
205
|
+
----------
|
|
206
|
+
motif_dir
|
|
207
|
+
Directory path containing .cb motif files
|
|
208
|
+
motif_names
|
|
209
|
+
Optional list of specific motif names to load. If None, loads all motifs.
|
|
210
|
+
|
|
211
|
+
Returns
|
|
212
|
+
-------
|
|
213
|
+
dict[tuple[str, str], np.ndarray]
|
|
214
|
+
Dictionary mapping motif names to PWM matrices (4 x length)
|
|
215
|
+
|
|
216
|
+
Examples
|
|
217
|
+
--------
|
|
218
|
+
>>> motifs = load_motif_collection("./motif_collection/")
|
|
219
|
+
>>> print(list(motifs.keys()))
|
|
220
|
+
[("filename_1", "motif_1"), ("filename_1", "motif_2"), ("filename_1", "motif_4")]
|
|
221
|
+
>>> print(motifs[("filename_1", "motif_1")].shape)
|
|
222
|
+
(4, 12)
|
|
223
|
+
|
|
224
|
+
>>> # Load only specific motifs
|
|
225
|
+
>>> selected_motifs = load_motif_collection("./motif_collection/", ["motif1", "motif2"])
|
|
226
|
+
>>> print(list(selected_motifs.keys()))
|
|
227
|
+
['motif1', 'motif2']
|
|
228
|
+
"""
|
|
229
|
+
motif_dir_path = Path(motif_dir)
|
|
230
|
+
|
|
231
|
+
if not motif_dir_path.exists():
|
|
232
|
+
raise FileNotFoundError(f"Directory {motif_dir_path} does not exist")
|
|
233
|
+
|
|
234
|
+
motifs: dict[tuple[str, str], np.ndarray] = {}
|
|
235
|
+
|
|
236
|
+
cb_files = list(motif_dir_path.glob("*.cb"))
|
|
237
|
+
|
|
238
|
+
for cb_file in cb_files:
|
|
239
|
+
try:
|
|
240
|
+
# Load all motifs from this file
|
|
241
|
+
file_motifs = load_motif(str(cb_file))
|
|
242
|
+
motifs.update(file_motifs)
|
|
243
|
+
except (ValueError, IndexError):
|
|
244
|
+
# Skip malformed files
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
# Filter motifs if specific names are provided
|
|
248
|
+
if motif_names is not None:
|
|
249
|
+
motif_names_set = set(motif_names)
|
|
250
|
+
motifs = {(filename, name): pwm for (filename, name), pwm in motifs.items() if name in motif_names_set}
|
|
251
|
+
|
|
252
|
+
return motifs
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def load_motif_annotations(
|
|
256
|
+
annotations_file: str,
|
|
257
|
+
motif_similarity_fdr: float = 0.001,
|
|
258
|
+
orthologous_identity_threshold: float = 0.0,
|
|
259
|
+
column_names: tuple[str, ...] = (
|
|
260
|
+
"#motif_id",
|
|
261
|
+
"gene_name",
|
|
262
|
+
"motif_similarity_qvalue",
|
|
263
|
+
"orthologous_identity",
|
|
264
|
+
"description",
|
|
265
|
+
),
|
|
266
|
+
) -> pd.DataFrame:
|
|
267
|
+
"""
|
|
268
|
+
Load motif annotations from a motif2TF TSV file with filtering and categorization.
|
|
269
|
+
|
|
270
|
+
Parameters
|
|
271
|
+
----------
|
|
272
|
+
annotations_file
|
|
273
|
+
Path to the annotations TSV file
|
|
274
|
+
motif_similarity_fdr
|
|
275
|
+
Maximum False Discovery Rate for enriched motifs (default: 0.001)
|
|
276
|
+
orthologous_identity_threshold
|
|
277
|
+
Minimum orthologous identity for enriched motifs (default: 0.0)
|
|
278
|
+
column_names
|
|
279
|
+
Column names to load from the TSV file
|
|
280
|
+
|
|
281
|
+
Returns
|
|
282
|
+
-------
|
|
283
|
+
DataFrame with motif annotations categorized by annotation type:
|
|
284
|
+
- Direct_annot: Direct gene annotations
|
|
285
|
+
- Motif_similarity_annot: Annotations by motif similarity
|
|
286
|
+
- Orthology_annot: Annotations by orthology
|
|
287
|
+
- Motif_similarity_and_Orthology_annot: Combined annotations
|
|
288
|
+
|
|
289
|
+
Examples
|
|
290
|
+
--------
|
|
291
|
+
>>> annotations = load_motif_annotations("./annotations.tbl")
|
|
292
|
+
>>> print(annotations.columns.tolist())
|
|
293
|
+
['Direct_annot', 'Motif_similarity_annot', 'Orthology_annot', 'Motif_similarity_and_Orthology_annot']
|
|
294
|
+
"""
|
|
295
|
+
# Load as pandas DataFrame
|
|
296
|
+
df = pd.read_csv(annotations_file, sep="\t", usecols=column_names)
|
|
297
|
+
df.rename(
|
|
298
|
+
columns={
|
|
299
|
+
"#motif_id": "MotifID",
|
|
300
|
+
"gene_name": "TF",
|
|
301
|
+
"motif_similarity_qvalue": "MotifSimilarityQvalue",
|
|
302
|
+
"orthologous_identity": "OrthologousIdentity",
|
|
303
|
+
"description": "Annotation",
|
|
304
|
+
},
|
|
305
|
+
inplace=True,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# Filter based on thresholds
|
|
309
|
+
df = df[
|
|
310
|
+
(df["MotifSimilarityQvalue"] <= motif_similarity_fdr)
|
|
311
|
+
& (df["OrthologousIdentity"] >= orthologous_identity_threshold)
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
# Direct annotation
|
|
315
|
+
df_direct_annot = df[df["Annotation"] == "gene is directly annotated"]
|
|
316
|
+
df_direct_annot = df_direct_annot.groupby(["MotifID"])["TF"].apply(lambda x: ", ".join(list(set(x)))).reset_index()
|
|
317
|
+
df_direct_annot = df_direct_annot.set_index("MotifID")
|
|
318
|
+
df_direct_annot = pd.DataFrame(df_direct_annot["TF"])
|
|
319
|
+
df_direct_annot.columns = ["Direct_annot"]
|
|
320
|
+
|
|
321
|
+
# Indirect annotation - by motif similarity
|
|
322
|
+
motif_similarity_annot = df[
|
|
323
|
+
df["Annotation"].str.contains("similar") & ~df["Annotation"].str.contains("orthologous")
|
|
324
|
+
]
|
|
325
|
+
motif_similarity_annot = (
|
|
326
|
+
motif_similarity_annot.groupby(["MotifID"])["TF"].apply(lambda x: ", ".join(list(set(x)))).reset_index()
|
|
327
|
+
)
|
|
328
|
+
motif_similarity_annot = motif_similarity_annot.set_index("MotifID")
|
|
329
|
+
motif_similarity_annot = pd.DataFrame(motif_similarity_annot["TF"])
|
|
330
|
+
motif_similarity_annot.columns = ["Motif_similarity_annot"]
|
|
331
|
+
|
|
332
|
+
# Indirect annotation - by orthology
|
|
333
|
+
orthology_annot = df[~df["Annotation"].str.contains("similar") & df["Annotation"].str.contains("orthologous")]
|
|
334
|
+
orthology_annot = orthology_annot.groupby(["MotifID"])["TF"].apply(lambda x: ", ".join(list(set(x)))).reset_index()
|
|
335
|
+
orthology_annot = orthology_annot.set_index("MotifID")
|
|
336
|
+
orthology_annot = pd.DataFrame(orthology_annot["TF"])
|
|
337
|
+
orthology_annot.columns = ["Orthology_annot"]
|
|
338
|
+
|
|
339
|
+
# Indirect annotation - by motif similarity and orthology
|
|
340
|
+
motif_similarity_and_orthology_annot = df[
|
|
341
|
+
df["Annotation"].str.contains("similar") & df["Annotation"].str.contains("orthologous")
|
|
342
|
+
]
|
|
343
|
+
motif_similarity_and_orthology_annot = (
|
|
344
|
+
motif_similarity_and_orthology_annot.groupby(["MotifID"])["TF"]
|
|
345
|
+
.apply(lambda x: ", ".join(list(set(x))))
|
|
346
|
+
.reset_index()
|
|
347
|
+
)
|
|
348
|
+
motif_similarity_and_orthology_annot = motif_similarity_and_orthology_annot.set_index("MotifID")
|
|
349
|
+
motif_similarity_and_orthology_annot = pd.DataFrame(motif_similarity_and_orthology_annot["TF"])
|
|
350
|
+
motif_similarity_and_orthology_annot.columns = ["Motif_similarity_and_Orthology_annot"]
|
|
351
|
+
|
|
352
|
+
# Combine all annotation types
|
|
353
|
+
result = pd.concat(
|
|
354
|
+
[df_direct_annot, motif_similarity_annot, orthology_annot, motif_similarity_and_orthology_annot],
|
|
355
|
+
axis=1,
|
|
356
|
+
sort=False,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
return result
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def load_motif_to_dbd(motif_annotations: pd.DataFrame) -> dict[str, str]:
|
|
363
|
+
"""
|
|
364
|
+
Create motif-to-DNA-binding-domain mapping for human TFs.
|
|
365
|
+
|
|
366
|
+
Takes motif annotations and maps motifs to their DNA-binding domains
|
|
367
|
+
based on TF annotations and human TF database information.
|
|
368
|
+
|
|
369
|
+
Parameters
|
|
370
|
+
----------
|
|
371
|
+
motif_annotations
|
|
372
|
+
DataFrame with motif annotations as returned by load_motif_annotations()
|
|
373
|
+
|
|
374
|
+
Returns
|
|
375
|
+
-------
|
|
376
|
+
dict[str, str]
|
|
377
|
+
Dictionary mapping motif IDs to DNA-binding domain names
|
|
378
|
+
|
|
379
|
+
Examples
|
|
380
|
+
--------
|
|
381
|
+
>>> annotations_file = fetch_motif_annotations("hgnc", "v10nr_clust")
|
|
382
|
+
>>> motif_annotations = load_motif_annotations(annotations_file)
|
|
383
|
+
>>> motif_to_dbd = load_motif_to_dbd(motif_annotations)
|
|
384
|
+
>>> print(motif_to_dbd["hocomoco__FOXO1_HUMAN.H11MO.0.A"])
|
|
385
|
+
'Forkhead'
|
|
386
|
+
"""
|
|
387
|
+
motif_to_tf = motif_annotations.copy()
|
|
388
|
+
|
|
389
|
+
# Flatten all TF annotations into individual TF names
|
|
390
|
+
motif_to_tf = (
|
|
391
|
+
motif_to_tf.apply(lambda row: ", ".join(row.dropna()), axis=1)
|
|
392
|
+
.str.split(", ")
|
|
393
|
+
.explode()
|
|
394
|
+
.reset_index()
|
|
395
|
+
.rename({0: "TF"}, axis=1)
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# Download human TF annotations with DNA-binding domains
|
|
399
|
+
human_tf_annot = pd.read_csv(
|
|
400
|
+
"https://humantfs.ccbr.utoronto.ca/download/v_1.01/DatabaseExtract_v_1.01.csv",
|
|
401
|
+
index_col=0,
|
|
402
|
+
)[["HGNC symbol", "DBD"]]
|
|
403
|
+
|
|
404
|
+
motif_to_tf = motif_to_tf.merge(right=human_tf_annot, how="left", left_on="TF", right_on="HGNC symbol")
|
|
405
|
+
|
|
406
|
+
# For each motif, take the most common (mode) DBD annotation
|
|
407
|
+
motif_to_dbd = (
|
|
408
|
+
motif_to_tf.dropna()
|
|
409
|
+
.groupby("MotifID")["DBD"]
|
|
410
|
+
.agg(lambda x: x.mode().iat[0]) # take the first mode if there's a tie
|
|
411
|
+
.reset_index()
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
motif_to_dbd = motif_to_dbd.set_index("MotifID")["DBD"].to_dict()
|
|
415
|
+
|
|
416
|
+
return motif_to_dbd
|