phasepred 0.9.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.
- phasepred/__init__.py +3 -0
- phasepred/cli.py +1053 -0
- phasepred/data/models/PdPS/8f_model_0.joblib +0 -0
- phasepred/data/models/PdPS/8f_model_1.joblib +0 -0
- phasepred/data/models/PdPS/8f_model_2.joblib +0 -0
- phasepred/data/models/PdPS/8f_model_3.joblib +0 -0
- phasepred/data/models/PdPS/8f_model_4.joblib +0 -0
- phasepred/data/models/PdPS/8f_model_5.joblib +0 -0
- phasepred/data/models/PdPS/8f_model_6.joblib +0 -0
- phasepred/data/models/PdPS/8f_model_7.joblib +0 -0
- phasepred/data/models/PdPS/8f_model_8.joblib +0 -0
- phasepred/data/models/PdPS/8f_model_9.joblib +0 -0
- phasepred/data/models/PdPS/manifest.json +25 -0
- phasepred/data/models/SaPS/8f_model_0.joblib +0 -0
- phasepred/data/models/SaPS/8f_model_1.joblib +0 -0
- phasepred/data/models/SaPS/8f_model_2.joblib +0 -0
- phasepred/data/models/SaPS/8f_model_3.joblib +0 -0
- phasepred/data/models/SaPS/8f_model_4.joblib +0 -0
- phasepred/data/models/SaPS/8f_model_5.joblib +0 -0
- phasepred/data/models/SaPS/8f_model_6.joblib +0 -0
- phasepred/data/models/SaPS/8f_model_7.joblib +0 -0
- phasepred/data/models/SaPS/8f_model_8.joblib +0 -0
- phasepred/data/models/SaPS/8f_model_9.joblib +0 -0
- phasepred/data/models/SaPS/manifest.json +25 -0
- phasepred/data/models/hPdPS/8f_model_0.joblib +0 -0
- phasepred/data/models/hPdPS/8f_model_1.joblib +0 -0
- phasepred/data/models/hPdPS/8f_model_2.joblib +0 -0
- phasepred/data/models/hPdPS/8f_model_3.joblib +0 -0
- phasepred/data/models/hPdPS/8f_model_4.joblib +0 -0
- phasepred/data/models/hPdPS/8f_model_5.joblib +0 -0
- phasepred/data/models/hPdPS/8f_model_6.joblib +0 -0
- phasepred/data/models/hPdPS/8f_model_7.joblib +0 -0
- phasepred/data/models/hPdPS/8f_model_8.joblib +0 -0
- phasepred/data/models/hPdPS/8f_model_9.joblib +0 -0
- phasepred/data/models/hPdPS/manifest.json +27 -0
- phasepred/data/models/hSaPS/8f_model_0.joblib +0 -0
- phasepred/data/models/hSaPS/8f_model_1.joblib +0 -0
- phasepred/data/models/hSaPS/8f_model_2.joblib +0 -0
- phasepred/data/models/hSaPS/8f_model_3.joblib +0 -0
- phasepred/data/models/hSaPS/8f_model_4.joblib +0 -0
- phasepred/data/models/hSaPS/8f_model_5.joblib +0 -0
- phasepred/data/models/hSaPS/8f_model_6.joblib +0 -0
- phasepred/data/models/hSaPS/8f_model_7.joblib +0 -0
- phasepred/data/models/hSaPS/8f_model_8.joblib +0 -0
- phasepred/data/models/hSaPS/8f_model_9.joblib +0 -0
- phasepred/data/models/hSaPS/manifest.json +27 -0
- phasepred/data.py +65 -0
- phasepred/espritz.py +192 -0
- phasepred/features.py +158 -0
- phasepred/legacy_features.py +224 -0
- phasepred/models.py +141 -0
- phasepred/paper.py +160 -0
- phasepred/predictor.py +215 -0
- phasepred/sequences.py +220 -0
- phasepred/tool_features.py +218 -0
- phasepred/tool_paths.py +429 -0
- phasepred/tools.py +602 -0
- phasepred/updated_data.py +349 -0
- phasepred-0.9.0.dist-info/METADATA +283 -0
- phasepred-0.9.0.dist-info/RECORD +64 -0
- phasepred-0.9.0.dist-info/WHEEL +5 -0
- phasepred-0.9.0.dist-info/entry_points.txt +2 -0
- phasepred-0.9.0.dist-info/licenses/LICENSE +35 -0
- phasepred-0.9.0.dist-info/top_level.txt +1 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"task": "PdPS",
|
|
3
|
+
"feature_columns": [
|
|
4
|
+
"Hydropathy",
|
|
5
|
+
"FCR",
|
|
6
|
+
"IDR",
|
|
7
|
+
"LCR",
|
|
8
|
+
"PScore",
|
|
9
|
+
"PLAAC",
|
|
10
|
+
"catGRANULE",
|
|
11
|
+
"DeepCoil"
|
|
12
|
+
],
|
|
13
|
+
"feature_definitions_version": "v2022",
|
|
14
|
+
"protocol": "paper-default",
|
|
15
|
+
"n_models": 10,
|
|
16
|
+
"n_neg_ratio": 2,
|
|
17
|
+
"n_folds": 5,
|
|
18
|
+
"seed": 42,
|
|
19
|
+
"train_sheet": "PdPS",
|
|
20
|
+
"trained_at": "2026-09-07T07:34:28Z",
|
|
21
|
+
"xgboost_version": "3.2.0",
|
|
22
|
+
"auc_cv": 0.7378839637531593,
|
|
23
|
+
"auc_cv_ensemble": 0.7653199192061899,
|
|
24
|
+
"auc_test_s3": 0.717539200076712
|
|
25
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"task": "SaPS",
|
|
3
|
+
"feature_columns": [
|
|
4
|
+
"Hydropathy",
|
|
5
|
+
"FCR",
|
|
6
|
+
"IDR",
|
|
7
|
+
"LCR",
|
|
8
|
+
"PScore",
|
|
9
|
+
"PLAAC",
|
|
10
|
+
"catGRANULE",
|
|
11
|
+
"DeepCoil"
|
|
12
|
+
],
|
|
13
|
+
"feature_definitions_version": "v2022",
|
|
14
|
+
"protocol": "paper-default",
|
|
15
|
+
"n_models": 10,
|
|
16
|
+
"n_neg_ratio": 2,
|
|
17
|
+
"n_folds": 5,
|
|
18
|
+
"seed": 42,
|
|
19
|
+
"train_sheet": "SaPS",
|
|
20
|
+
"trained_at": "2026-09-07T07:34:28Z",
|
|
21
|
+
"xgboost_version": "3.2.0",
|
|
22
|
+
"auc_cv": 0.8607288284023668,
|
|
23
|
+
"auc_cv_ensemble": 0.8826724260355029,
|
|
24
|
+
"auc_test_s3": 0.8147863025163637
|
|
25
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"task": "hPdPS",
|
|
3
|
+
"feature_columns": [
|
|
4
|
+
"Hydropathy",
|
|
5
|
+
"FCR",
|
|
6
|
+
"IDR",
|
|
7
|
+
"LCR",
|
|
8
|
+
"PScore",
|
|
9
|
+
"PLAAC",
|
|
10
|
+
"catGRANULE",
|
|
11
|
+
"DeepCoil",
|
|
12
|
+
"Phos freq",
|
|
13
|
+
"DeepPhase"
|
|
14
|
+
],
|
|
15
|
+
"feature_definitions_version": "v2022",
|
|
16
|
+
"protocol": "paper-default",
|
|
17
|
+
"n_models": 10,
|
|
18
|
+
"n_neg_ratio": 2,
|
|
19
|
+
"n_folds": 5,
|
|
20
|
+
"seed": 42,
|
|
21
|
+
"train_sheet": "hPdPS",
|
|
22
|
+
"trained_at": "2026-09-07T07:34:28Z",
|
|
23
|
+
"xgboost_version": "3.2.0",
|
|
24
|
+
"auc_cv": 0.8161708448753462,
|
|
25
|
+
"auc_cv_ensemble": 0.8406024930747922,
|
|
26
|
+
"auc_test_s3": 0.7988882803943045
|
|
27
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"task": "hSaPS",
|
|
3
|
+
"feature_columns": [
|
|
4
|
+
"Hydropathy",
|
|
5
|
+
"FCR",
|
|
6
|
+
"IDR",
|
|
7
|
+
"LCR",
|
|
8
|
+
"PScore",
|
|
9
|
+
"PLAAC",
|
|
10
|
+
"catGRANULE",
|
|
11
|
+
"DeepCoil",
|
|
12
|
+
"Phos freq",
|
|
13
|
+
"DeepPhase"
|
|
14
|
+
],
|
|
15
|
+
"feature_definitions_version": "v2022",
|
|
16
|
+
"protocol": "paper-default",
|
|
17
|
+
"n_models": 10,
|
|
18
|
+
"n_neg_ratio": 2,
|
|
19
|
+
"n_folds": 5,
|
|
20
|
+
"seed": 42,
|
|
21
|
+
"train_sheet": "hSaPS",
|
|
22
|
+
"trained_at": "2026-09-07T07:34:28Z",
|
|
23
|
+
"xgboost_version": "3.2.0",
|
|
24
|
+
"auc_cv": 0.9175152089072544,
|
|
25
|
+
"auc_cv_ensemble": 0.9333333333333332,
|
|
26
|
+
"auc_test_s3": 0.8157655502392344
|
|
27
|
+
}
|
phasepred/data.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Canonical root / data-file resolution for the phasepred package.
|
|
2
|
+
|
|
3
|
+
Every repo-local or shipped data path used by the package goes through the
|
|
4
|
+
helpers here so that the CLI and API work from any working directory:
|
|
5
|
+
|
|
6
|
+
- ``PHASEPRED_DATA_ROOT`` overrides where repo-local data lives
|
|
7
|
+
(``data/``, ``tools/``, ``products/``, ``.external_envs/``). When the
|
|
8
|
+
package is pip-installed without a checkout, point this at a directory
|
|
9
|
+
laid out like the repository root.
|
|
10
|
+
- Without an override, the repository root is inferred from this module's
|
|
11
|
+
location (package is installed from ``src/phasepred/`` inside a checkout,
|
|
12
|
+
so ``parents[2]`` is the repo root).
|
|
13
|
+
- Package-bundled resources (JSON calibration constants, etc.) are resolved
|
|
14
|
+
relative to the module itself.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def repo_root() -> Path:
|
|
24
|
+
"""The repository root, inferred from the module location."""
|
|
25
|
+
return Path(__file__).resolve().parents[2]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def data_root() -> Path:
|
|
29
|
+
"""The directory that holds repo-local data (override-able).
|
|
30
|
+
|
|
31
|
+
Preference: ``PHASEPRED_DATA_ROOT`` env var > inferred repository root.
|
|
32
|
+
"""
|
|
33
|
+
override = os.environ.get("PHASEPRED_DATA_ROOT")
|
|
34
|
+
if override:
|
|
35
|
+
return Path(override).expanduser().resolve()
|
|
36
|
+
return repo_root()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def data_path(*parts: str) -> Path:
|
|
40
|
+
"""Resolve a path under the data root (e.g. ``data_path("data", "raw", ...)``)."""
|
|
41
|
+
return data_root().joinpath(*parts)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def bundled_path(*parts: str) -> Path:
|
|
45
|
+
"""Resolve a package-bundled resource (inside ``src/phasepred/``)."""
|
|
46
|
+
return Path(__file__).parent.joinpath(*parts)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def models_root() -> Path:
|
|
50
|
+
"""Default Product A artifacts root.
|
|
51
|
+
|
|
52
|
+
Preference: ``PHASEPRED_DATA_ROOT`` override (external data root laid out
|
|
53
|
+
like the repo) > package-bundled models (``src/phasepred/data/models``,
|
|
54
|
+
shipped in the wheel) > inferred repository ``products/`` layout (dev
|
|
55
|
+
checkout fallback).
|
|
56
|
+
"""
|
|
57
|
+
override = os.environ.get("PHASEPRED_DATA_ROOT")
|
|
58
|
+
if override:
|
|
59
|
+
return Path(override).expanduser().resolve().joinpath(
|
|
60
|
+
"products", "A_paper_split_recomputed", "models"
|
|
61
|
+
)
|
|
62
|
+
bundled = bundled_path("data", "models")
|
|
63
|
+
if bundled.is_dir():
|
|
64
|
+
return bundled
|
|
65
|
+
return data_path("products", "A_paper_split_recomputed", "models")
|
phasepred/espritz.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
8
|
+
from dataclasses import asdict, dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
ESPRITZ_RUNNER = Path(__file__).resolve().parents[2] / "tools" / "ESpritz" / "run"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class ESpritzFeature:
|
|
17
|
+
accession: str
|
|
18
|
+
espritz_idr: float
|
|
19
|
+
mean_score: float
|
|
20
|
+
max_score: float
|
|
21
|
+
disorder_threshold: float
|
|
22
|
+
residues: int
|
|
23
|
+
model: str
|
|
24
|
+
sw: int
|
|
25
|
+
|
|
26
|
+
def to_json(self) -> dict[str, object]:
|
|
27
|
+
return asdict(self)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_json(cls, payload: dict[str, Any]) -> ESpritzFeature:
|
|
31
|
+
return cls(
|
|
32
|
+
accession=str(payload["accession"]),
|
|
33
|
+
espritz_idr=float(payload["espritz_idr"]),
|
|
34
|
+
mean_score=float(payload["mean_score"]),
|
|
35
|
+
max_score=float(payload["max_score"]),
|
|
36
|
+
disorder_threshold=float(payload["disorder_threshold"]),
|
|
37
|
+
residues=int(payload["residues"]),
|
|
38
|
+
model=str(payload["model"]),
|
|
39
|
+
sw=int(payload["sw"]),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ESpritzFeatureCache:
|
|
44
|
+
def __init__(self, path: str | Path) -> None:
|
|
45
|
+
self.path = Path(path)
|
|
46
|
+
|
|
47
|
+
def _read_all(self) -> dict[str, ESpritzFeature]:
|
|
48
|
+
if not self.path.exists():
|
|
49
|
+
return {}
|
|
50
|
+
features: dict[str, ESpritzFeature] = {}
|
|
51
|
+
with self.path.open("r", encoding="utf-8") as handle:
|
|
52
|
+
for line in handle:
|
|
53
|
+
stripped = line.strip()
|
|
54
|
+
if not stripped:
|
|
55
|
+
continue
|
|
56
|
+
feature = ESpritzFeature.from_json(json.loads(stripped))
|
|
57
|
+
features[feature.accession] = feature
|
|
58
|
+
return features
|
|
59
|
+
|
|
60
|
+
def append_new(self, feature: ESpritzFeature) -> None:
|
|
61
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
with self.path.open("a", encoding="utf-8") as handle:
|
|
63
|
+
handle.write(json.dumps(feature.to_json(), sort_keys=True) + "\n")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def resolve_espritz_features(
|
|
67
|
+
sequence_rows: Iterable[dict[str, object]],
|
|
68
|
+
*,
|
|
69
|
+
cache_path: str | Path,
|
|
70
|
+
model: str = "D",
|
|
71
|
+
sw: int = 0,
|
|
72
|
+
chunk_size: int = 250,
|
|
73
|
+
workers: int = 1,
|
|
74
|
+
) -> list[ESpritzFeature]:
|
|
75
|
+
cache = ESpritzFeatureCache(cache_path)
|
|
76
|
+
cached_features = cache._read_all()
|
|
77
|
+
rows = list(sequence_rows)
|
|
78
|
+
features: list[ESpritzFeature] = []
|
|
79
|
+
pending_rows: list[dict[str, object]] = []
|
|
80
|
+
for row in rows:
|
|
81
|
+
accession = str(row["UniprotEntry"]).strip()
|
|
82
|
+
cached = cached_features.get(accession)
|
|
83
|
+
if cached is not None and cached.model == model and cached.sw == sw:
|
|
84
|
+
features.append(cached)
|
|
85
|
+
continue
|
|
86
|
+
pending_rows.append(row)
|
|
87
|
+
|
|
88
|
+
chunks = [
|
|
89
|
+
pending_rows[start : start + chunk_size]
|
|
90
|
+
for start in range(0, len(pending_rows), chunk_size)
|
|
91
|
+
]
|
|
92
|
+
if workers <= 1:
|
|
93
|
+
for chunk in chunks:
|
|
94
|
+
for feature in compute_espritz_features(chunk, model=model, sw=sw):
|
|
95
|
+
cache.append_new(feature)
|
|
96
|
+
cached_features[feature.accession] = feature
|
|
97
|
+
features.append(feature)
|
|
98
|
+
else:
|
|
99
|
+
with ProcessPoolExecutor(max_workers=workers) as executor:
|
|
100
|
+
futures = [
|
|
101
|
+
executor.submit(compute_espritz_features, chunk, model=model, sw=sw)
|
|
102
|
+
for chunk in chunks
|
|
103
|
+
]
|
|
104
|
+
for future in as_completed(futures):
|
|
105
|
+
for feature in future.result():
|
|
106
|
+
cache.append_new(feature)
|
|
107
|
+
cached_features[feature.accession] = feature
|
|
108
|
+
features.append(feature)
|
|
109
|
+
|
|
110
|
+
by_accession = {feature.accession: feature for feature in features}
|
|
111
|
+
return [
|
|
112
|
+
by_accession[str(row["UniprotEntry"]).strip()]
|
|
113
|
+
for row in rows
|
|
114
|
+
if str(row["UniprotEntry"]).strip() in by_accession
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def compute_espritz_features(
|
|
119
|
+
sequence_rows: Iterable[dict[str, object]],
|
|
120
|
+
*,
|
|
121
|
+
model: str = "D",
|
|
122
|
+
sw: int = 0,
|
|
123
|
+
) -> list[ESpritzFeature]:
|
|
124
|
+
if not ESPRITZ_RUNNER.exists():
|
|
125
|
+
raise FileNotFoundError(f"ESpritz runner not found: {ESPRITZ_RUNNER}")
|
|
126
|
+
rows = list(sequence_rows)
|
|
127
|
+
with tempfile.TemporaryDirectory(prefix="phasepred_espritz_") as tmp:
|
|
128
|
+
workdir = Path(tmp)
|
|
129
|
+
accessions: list[str] = []
|
|
130
|
+
for row in rows:
|
|
131
|
+
accession = str(row["UniprotEntry"]).strip()
|
|
132
|
+
sequence = "".join(str(row["sequence"]).split()).upper()
|
|
133
|
+
if not accession or not sequence:
|
|
134
|
+
continue
|
|
135
|
+
accessions.append(accession)
|
|
136
|
+
(workdir / f"{_safe_stem(accession)}.fasta").write_text(
|
|
137
|
+
f">{accession}\n{sequence}\n",
|
|
138
|
+
encoding="utf-8",
|
|
139
|
+
)
|
|
140
|
+
if not accessions:
|
|
141
|
+
return []
|
|
142
|
+
subprocess.run(
|
|
143
|
+
[str(ESPRITZ_RUNNER), str(workdir), model, str(sw)],
|
|
144
|
+
check=True,
|
|
145
|
+
cwd=str(ESPRITZ_RUNNER.parents[2]),
|
|
146
|
+
stdout=subprocess.PIPE,
|
|
147
|
+
stderr=subprocess.STDOUT,
|
|
148
|
+
text=True,
|
|
149
|
+
)
|
|
150
|
+
features = []
|
|
151
|
+
for accession in accessions:
|
|
152
|
+
output = workdir / f"{_safe_stem(accession)}.espritz"
|
|
153
|
+
if output.exists():
|
|
154
|
+
features.append(parse_espritz_output(output, accession, model=model, sw=sw))
|
|
155
|
+
return features
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def parse_espritz_output(
|
|
159
|
+
path: str | Path,
|
|
160
|
+
accession: str,
|
|
161
|
+
*,
|
|
162
|
+
model: str = "D",
|
|
163
|
+
sw: int = 0,
|
|
164
|
+
) -> ESpritzFeature:
|
|
165
|
+
states: list[str] = []
|
|
166
|
+
scores: list[float] = []
|
|
167
|
+
for line in Path(path).read_text(encoding="utf-8", errors="replace").splitlines():
|
|
168
|
+
parts = line.strip().split()
|
|
169
|
+
if len(parts) != 2 or parts[0] not in {"D", "O"}:
|
|
170
|
+
continue
|
|
171
|
+
try:
|
|
172
|
+
score = float(parts[1])
|
|
173
|
+
except ValueError:
|
|
174
|
+
continue
|
|
175
|
+
states.append(parts[0])
|
|
176
|
+
scores.append(score)
|
|
177
|
+
if not scores:
|
|
178
|
+
raise ValueError(f"No ESpritz residue scores parsed from {path}")
|
|
179
|
+
return ESpritzFeature(
|
|
180
|
+
accession=accession,
|
|
181
|
+
espritz_idr=sum(state == "D" for state in states) / len(states),
|
|
182
|
+
mean_score=sum(scores) / len(scores),
|
|
183
|
+
max_score=max(scores),
|
|
184
|
+
disorder_threshold=0.5072 if model == "D" and sw == 0 else float("nan"),
|
|
185
|
+
residues=len(scores),
|
|
186
|
+
model=model,
|
|
187
|
+
sw=sw,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _safe_stem(accession: str) -> str:
|
|
192
|
+
return "".join(char if char.isalnum() or char in "._-" else "_" for char in accession)
|
phasepred/features.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from localcider.sequenceParameters import SequenceParameters
|
|
7
|
+
|
|
8
|
+
BASE_FEATURE_COLUMNS = [
|
|
9
|
+
"Hydropathy",
|
|
10
|
+
"FCR",
|
|
11
|
+
"IDR",
|
|
12
|
+
"LCR",
|
|
13
|
+
"PScore",
|
|
14
|
+
"PLAAC",
|
|
15
|
+
"catGRANULE",
|
|
16
|
+
"DeepCoil",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
HUMAN_FEATURE_COLUMNS = BASE_FEATURE_COLUMNS + ["Phos freq", "DeepPhase"]
|
|
20
|
+
|
|
21
|
+
# DeepCoil v2022 definition (FEATURES.md §8): binary indicator with
|
|
22
|
+
# threshold 0.82 on the raw coiled-coil probability.
|
|
23
|
+
DEEPCOIL_THRESHOLD = 0.82
|
|
24
|
+
|
|
25
|
+
# catGRANULE residue profile window (CATGRANULE_SPEC §5 / FEATURES.md §7):
|
|
26
|
+
# a 51-residue centered window, covering the interior positions
|
|
27
|
+
# 25..L-26 (1-indexed) → length L-50, offset 24.
|
|
28
|
+
CATGRANULE_RESIDUE_WINDOW = 51
|
|
29
|
+
CATGRANULE_RESIDUE_OFFSET = 24
|
|
30
|
+
|
|
31
|
+
KYTE_DOOLITTLE = {
|
|
32
|
+
"A": 1.8,
|
|
33
|
+
"C": 2.5,
|
|
34
|
+
"D": -3.5,
|
|
35
|
+
"E": -3.5,
|
|
36
|
+
"F": 2.8,
|
|
37
|
+
"G": -0.4,
|
|
38
|
+
"H": -3.2,
|
|
39
|
+
"I": 4.5,
|
|
40
|
+
"K": -3.9,
|
|
41
|
+
"L": 3.8,
|
|
42
|
+
"M": 1.9,
|
|
43
|
+
"N": -3.5,
|
|
44
|
+
"P": -1.6,
|
|
45
|
+
"Q": -3.5,
|
|
46
|
+
"R": -4.5,
|
|
47
|
+
"S": -0.8,
|
|
48
|
+
"T": -0.7,
|
|
49
|
+
"V": 4.2,
|
|
50
|
+
"W": -0.9,
|
|
51
|
+
"Y": -1.3,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
CHARGED_RESIDUES = frozenset({"D", "E", "K", "R"})
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class FeatureSchemaError(ValueError):
|
|
58
|
+
"""Raised when feature inputs do not satisfy a model schema."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def compute_native_features(sequence: str) -> dict[str, float]:
|
|
62
|
+
normalized = sequence.upper()
|
|
63
|
+
if not normalized:
|
|
64
|
+
raise FeatureSchemaError("Cannot compute features for an empty sequence")
|
|
65
|
+
unknown = sorted({residue for residue in normalized if residue not in KYTE_DOOLITTLE})
|
|
66
|
+
if unknown:
|
|
67
|
+
raise FeatureSchemaError(f"Unsupported residues in sequence: {', '.join(unknown)}")
|
|
68
|
+
length = len(normalized)
|
|
69
|
+
return {
|
|
70
|
+
"length": float(length),
|
|
71
|
+
"Hydropathy": SequenceParameters(normalized).get_uversky_hydropathy(),
|
|
72
|
+
"FCR": sum(1 for residue in normalized if residue in CHARGED_RESIDUES) / length,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def build_feature_matrix(
|
|
77
|
+
sequence_records: pd.DataFrame,
|
|
78
|
+
imported_features: pd.DataFrame | None = None,
|
|
79
|
+
*,
|
|
80
|
+
feature_columns: Sequence[str] = BASE_FEATURE_COLUMNS,
|
|
81
|
+
) -> pd.DataFrame:
|
|
82
|
+
_require_columns(sequence_records, {"UniprotEntry", "sequence"}, "sequence records")
|
|
83
|
+
native_rows = []
|
|
84
|
+
for row in sequence_records.itertuples(index=False):
|
|
85
|
+
features = compute_native_features(str(row.sequence))
|
|
86
|
+
native_rows.append({"UniprotEntry": str(row.UniprotEntry), **features})
|
|
87
|
+
native = pd.DataFrame(native_rows)
|
|
88
|
+
|
|
89
|
+
if imported_features is None:
|
|
90
|
+
matrix = native
|
|
91
|
+
else:
|
|
92
|
+
_require_columns(imported_features, {"UniprotEntry"}, "imported features")
|
|
93
|
+
matrix = native.merge(
|
|
94
|
+
imported_features,
|
|
95
|
+
on="UniprotEntry",
|
|
96
|
+
how="left",
|
|
97
|
+
suffixes=("", "_imported"),
|
|
98
|
+
)
|
|
99
|
+
for column in ["Hydropathy", "FCR", "length"]:
|
|
100
|
+
imported_column = f"{column}_imported"
|
|
101
|
+
if imported_column in matrix.columns:
|
|
102
|
+
matrix = matrix.drop(columns=[imported_column])
|
|
103
|
+
|
|
104
|
+
missing = [column for column in feature_columns if column not in matrix.columns]
|
|
105
|
+
if missing:
|
|
106
|
+
raise FeatureSchemaError(f"Missing required feature columns: {', '.join(missing)}")
|
|
107
|
+
return matrix
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def validate_feature_columns(frame: pd.DataFrame, feature_columns: Sequence[str]) -> None:
|
|
111
|
+
missing = [column for column in feature_columns if column not in frame.columns]
|
|
112
|
+
if missing:
|
|
113
|
+
raise FeatureSchemaError(f"Missing required feature columns: {', '.join(missing)}")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _require_columns(frame: pd.DataFrame, columns: set[str], name: str) -> None:
|
|
117
|
+
missing = sorted(columns - set(frame.columns))
|
|
118
|
+
if missing:
|
|
119
|
+
raise FeatureSchemaError(f"{name} missing columns: {', '.join(missing)}")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# Residue-level alignment utilities (v2022 definitions)
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def catgranule_profile_alignment(sequence_length: int) -> range:
|
|
128
|
+
"""Position range (0-indexed) covered by a catGRANULE residue profile.
|
|
129
|
+
|
|
130
|
+
The profile is a 51-residue sliding-window mean (radius 25) over the
|
|
131
|
+
interior span: 0-indexed ``24 .. L-27`` (i.e. offset 24, length ``L-50``;
|
|
132
|
+
FEATURES.md §7 / CATGRANULE_SPEC §5). Empty range when the sequence is
|
|
133
|
+
too short for the window.
|
|
134
|
+
"""
|
|
135
|
+
radius = (CATGRANULE_RESIDUE_WINDOW - 1) // 2
|
|
136
|
+
start = radius - 1
|
|
137
|
+
stop = sequence_length - radius - 1
|
|
138
|
+
if stop <= start:
|
|
139
|
+
return range(0, 0)
|
|
140
|
+
return range(start, stop)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def pscore_alignment(sequence_length: int, residue_length: int) -> range:
|
|
144
|
+
"""Position range (0-indexed) covered by a PScore residue array.
|
|
145
|
+
|
|
146
|
+
PScore omits a sequence-dependent number of head residues (empirically
|
|
147
|
+
1..L-delta; FEATURES.md §5): align the shorter array to the *suffix* of
|
|
148
|
+
the full sequence. Returns an empty range when the residue array is
|
|
149
|
+
longer than the sequence.
|
|
150
|
+
"""
|
|
151
|
+
if residue_length < 0 or residue_length > sequence_length:
|
|
152
|
+
return range(0, 0)
|
|
153
|
+
return range(sequence_length - residue_length, sequence_length)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def binary_deepcoil(raw_cc: Sequence[float]) -> float:
|
|
157
|
+
"""v2022 DeepCoil feature value: ``1.0 if max(raw_cc) >= 0.82 else 0.0``."""
|
|
158
|
+
return 1.0 if max(raw_cc) >= DEEPCOIL_THRESHOLD else 0.0
|