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.
Files changed (64) hide show
  1. phasepred/__init__.py +3 -0
  2. phasepred/cli.py +1053 -0
  3. phasepred/data/models/PdPS/8f_model_0.joblib +0 -0
  4. phasepred/data/models/PdPS/8f_model_1.joblib +0 -0
  5. phasepred/data/models/PdPS/8f_model_2.joblib +0 -0
  6. phasepred/data/models/PdPS/8f_model_3.joblib +0 -0
  7. phasepred/data/models/PdPS/8f_model_4.joblib +0 -0
  8. phasepred/data/models/PdPS/8f_model_5.joblib +0 -0
  9. phasepred/data/models/PdPS/8f_model_6.joblib +0 -0
  10. phasepred/data/models/PdPS/8f_model_7.joblib +0 -0
  11. phasepred/data/models/PdPS/8f_model_8.joblib +0 -0
  12. phasepred/data/models/PdPS/8f_model_9.joblib +0 -0
  13. phasepred/data/models/PdPS/manifest.json +25 -0
  14. phasepred/data/models/SaPS/8f_model_0.joblib +0 -0
  15. phasepred/data/models/SaPS/8f_model_1.joblib +0 -0
  16. phasepred/data/models/SaPS/8f_model_2.joblib +0 -0
  17. phasepred/data/models/SaPS/8f_model_3.joblib +0 -0
  18. phasepred/data/models/SaPS/8f_model_4.joblib +0 -0
  19. phasepred/data/models/SaPS/8f_model_5.joblib +0 -0
  20. phasepred/data/models/SaPS/8f_model_6.joblib +0 -0
  21. phasepred/data/models/SaPS/8f_model_7.joblib +0 -0
  22. phasepred/data/models/SaPS/8f_model_8.joblib +0 -0
  23. phasepred/data/models/SaPS/8f_model_9.joblib +0 -0
  24. phasepred/data/models/SaPS/manifest.json +25 -0
  25. phasepred/data/models/hPdPS/8f_model_0.joblib +0 -0
  26. phasepred/data/models/hPdPS/8f_model_1.joblib +0 -0
  27. phasepred/data/models/hPdPS/8f_model_2.joblib +0 -0
  28. phasepred/data/models/hPdPS/8f_model_3.joblib +0 -0
  29. phasepred/data/models/hPdPS/8f_model_4.joblib +0 -0
  30. phasepred/data/models/hPdPS/8f_model_5.joblib +0 -0
  31. phasepred/data/models/hPdPS/8f_model_6.joblib +0 -0
  32. phasepred/data/models/hPdPS/8f_model_7.joblib +0 -0
  33. phasepred/data/models/hPdPS/8f_model_8.joblib +0 -0
  34. phasepred/data/models/hPdPS/8f_model_9.joblib +0 -0
  35. phasepred/data/models/hPdPS/manifest.json +27 -0
  36. phasepred/data/models/hSaPS/8f_model_0.joblib +0 -0
  37. phasepred/data/models/hSaPS/8f_model_1.joblib +0 -0
  38. phasepred/data/models/hSaPS/8f_model_2.joblib +0 -0
  39. phasepred/data/models/hSaPS/8f_model_3.joblib +0 -0
  40. phasepred/data/models/hSaPS/8f_model_4.joblib +0 -0
  41. phasepred/data/models/hSaPS/8f_model_5.joblib +0 -0
  42. phasepred/data/models/hSaPS/8f_model_6.joblib +0 -0
  43. phasepred/data/models/hSaPS/8f_model_7.joblib +0 -0
  44. phasepred/data/models/hSaPS/8f_model_8.joblib +0 -0
  45. phasepred/data/models/hSaPS/8f_model_9.joblib +0 -0
  46. phasepred/data/models/hSaPS/manifest.json +27 -0
  47. phasepred/data.py +65 -0
  48. phasepred/espritz.py +192 -0
  49. phasepred/features.py +158 -0
  50. phasepred/legacy_features.py +224 -0
  51. phasepred/models.py +141 -0
  52. phasepred/paper.py +160 -0
  53. phasepred/predictor.py +215 -0
  54. phasepred/sequences.py +220 -0
  55. phasepred/tool_features.py +218 -0
  56. phasepred/tool_paths.py +429 -0
  57. phasepred/tools.py +602 -0
  58. phasepred/updated_data.py +349 -0
  59. phasepred-0.9.0.dist-info/METADATA +283 -0
  60. phasepred-0.9.0.dist-info/RECORD +64 -0
  61. phasepred-0.9.0.dist-info/WHEEL +5 -0
  62. phasepred-0.9.0.dist-info/entry_points.txt +2 -0
  63. phasepred-0.9.0.dist-info/licenses/LICENSE +35 -0
  64. phasepred-0.9.0.dist-info/top_level.txt +1 -0
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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