ptmreloc 0.1.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.
ptmreloc/__init__.py ADDED
@@ -0,0 +1,50 @@
1
+ """ptmreloc — prior-knowledge guided relocalization of low-confidence PTM sites.
2
+
3
+ Quick start. The human reference database (PhosphoSitePlus, Gold Standard, Functional
4
+ Landscape, UniProt FASTA) is downloaded once on first use, unless you pass your own tables::
5
+
6
+ import ptmreloc as pr
7
+
8
+ out = pr.reassign(peptides) # default weights, default PSP
9
+ res = pr.optimize(peptides, out_dir="opt/") # optimize against default GS/FUN
10
+ out = pr.reassign(peptides, weights=res) # or weights="opt/weights.json"
11
+ rep = pr.evaluate(peptides, weights=res); print(rep.summary())
12
+
13
+ # own data: any of psp= / gold= / fun= / fasta= (DataFrame or file path)
14
+ out = pr.reassign(peptides, psp="my_psp.tsv")
15
+
16
+ The lower-level building blocks (``build_candidate_table``, ``choose``,
17
+ ``evaluate_selection``, ``optimize_weights``, ``Reassigner``) are exported too.
18
+ """
19
+ from .api import EvaluationReport, compare_weights, evaluate, load_reference, optimize, reassign, resolve_weights
20
+ from .database import (DATABASE_FILES, annotate_residues, cache_dir, database_dir, database_file,
21
+ database_info, default_proteome, default_reference, download_database, read_fasta)
22
+ from .defaults import DEFAULT_THRESHOLD, DEFAULT_WEIGHTS, WEIGHT_PRESETS
23
+ from .engine import choose, site_scores
24
+ from .features import FEATURE_NAMES, WEIGHT_NAMES, CandidateTable, FeatureConfig, build_candidate_table
25
+ from .metrics import composite_score, evaluate_by_ascore, trial_relative_composite
26
+ from .metrics import evaluate as evaluate_selection
27
+ from .optimize import OptimizationResult, normalize_weights, optimize_weights
28
+ from .peptide import format_residue, parse_peptide, parse_residue, relabel
29
+ from .reassign import Reassigner, apply_selection, load_weights
30
+ from .reference import ReferenceDB, read_table, write_table
31
+
32
+ __version__ = "0.1.0"
33
+
34
+ __all__ = [
35
+ # high-level API
36
+ "optimize", "reassign", "evaluate", "compare_weights", "load_reference", "resolve_weights",
37
+ "EvaluationReport", "OptimizationResult",
38
+ # reference database
39
+ "DATABASE_FILES", "download_database", "cache_dir", "database_dir", "database_file", "database_info",
40
+ "default_reference",
41
+ "default_proteome", "read_fasta", "annotate_residues",
42
+ # defaults
43
+ "DEFAULT_WEIGHTS", "DEFAULT_THRESHOLD", "WEIGHT_PRESETS",
44
+ # building blocks
45
+ "ReferenceDB", "read_table", "write_table", "FeatureConfig", "CandidateTable", "build_candidate_table",
46
+ "FEATURE_NAMES", "WEIGHT_NAMES", "choose", "site_scores", "evaluate_selection", "evaluate_by_ascore",
47
+ "composite_score", "trial_relative_composite", "optimize_weights", "normalize_weights",
48
+ "Reassigner", "apply_selection", "load_weights", "parse_peptide", "parse_residue", "format_residue",
49
+ "relabel",
50
+ ]
ptmreloc/api.py ADDED
@@ -0,0 +1,268 @@
1
+ """High-level library API: one call each for optimize / reassign / evaluate.
2
+
3
+ Reference data defaults to the human reference database (PhosphoSitePlus,
4
+ Gold Standard, Functional Landscape, UniProt FASTA), downloaded once on first use. To use your own data, pass
5
+ DataFrames or file paths (pkl/csv/tsv/parquet/xlsx) as ``psp=``, ``gold=``, ``fun=``,
6
+ ``fasta=``. Anything left as ``None`` falls back to the default file; ``False``
7
+ disables an optional table. A prebuilt :class:`ReferenceDB` can be passed as ``psp``
8
+ (its own GS/FUN sites are used unless ``gold``/``fun`` are given explicitly).
9
+
10
+ If the peptide table has no residue column, residues are annotated from the
11
+ protein sequences (``fasta=``, the default human proteome).
12
+
13
+ Weights (``weights=``) may be any of:
14
+
15
+ * ``None`` – the package defaults (:data:`ptmreloc.DEFAULT_WEIGHTS`)
16
+ * a preset name – ``"medians"``, ``"fun_medians"``, ``"gs_medians"``, ``"htp_only"``
17
+ * a path to a ``weights.json`` written by :func:`optimize` (its threshold and
18
+ feature settings are used too, unless overridden)
19
+ * an :class:`OptimizationResult`
20
+ * a mapping ``{"w_H": ..., "w_L": ..., "w_D": ...}`` or a sequence ``(w_H, w_L, w_D)``
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import copy
25
+ import os
26
+ from dataclasses import dataclass, field
27
+ from typing import Dict, Mapping, Optional, Sequence, Tuple, Union
28
+
29
+ import numpy as np
30
+ import pandas as pd
31
+
32
+ from .database import annotate_residues, database_dir, database_file, default_reference
33
+ from .defaults import DEFAULT_THRESHOLD, DEFAULT_WEIGHTS, WEIGHT_PRESETS
34
+ from .engine import as_weight_vector, choose
35
+ from .features import DEFAULT_COLUMNS, WEIGHT_NAMES, FeatureConfig, build_candidate_table
36
+ from .metrics import DEFAULT_ASCORE_BINS, evaluate as _evaluate_selection, evaluate_by_ascore
37
+ from .optimize import OptimizationResult, optimize_weights
38
+ from .reassign import Reassigner, load_weights
39
+ from .reference import ReferenceDB, TableLike, read_table, write_table
40
+
41
+ RefSpec = Union[None, TableLike, ReferenceDB]
42
+ OptTable = Union[None, bool, TableLike]
43
+ WeightsSpec = Union[None, str, os.PathLike, OptimizationResult, Mapping[str, float], Sequence[float], np.ndarray]
44
+
45
+
46
+ # ------------------------------------------------------------------ helpers
47
+ def resolve_weights(weights: WeightsSpec = None) -> Tuple[Dict[str, float], Optional[float], Optional[FeatureConfig]]:
48
+ """Turn any accepted weights spec into ``(weights, threshold or None, FeatureConfig or None)``."""
49
+ if weights is None:
50
+ return dict(DEFAULT_WEIGHTS), None, None
51
+ if isinstance(weights, OptimizationResult):
52
+ return dict(weights.weights), weights.threshold, weights.config
53
+ if isinstance(weights, (str, os.PathLike)):
54
+ key = os.fspath(weights)
55
+ if key.lower() in WEIGHT_PRESETS:
56
+ return dict(WEIGHT_PRESETS[key.lower()]), None, None
57
+ if os.path.isfile(key):
58
+ payload = load_weights(key)
59
+ cfg = payload.get("feature_config")
60
+ return dict(payload["weights"]), payload.get("threshold"), FeatureConfig(**cfg) if cfg else None
61
+ raise ValueError(f"{key!r} is neither a weights file nor a preset {sorted(WEIGHT_PRESETS)}")
62
+ w = as_weight_vector(weights)
63
+ return dict(zip(WEIGHT_NAMES, map(float, w))), None, None
64
+
65
+
66
+ def load_reference(
67
+ psp: RefSpec = None,
68
+ gold: OptTable = None,
69
+ fun: OptTable = None,
70
+ organism: Optional[str] = None,
71
+ database: Optional[Union[str, os.PathLike]] = None,
72
+ ) -> ReferenceDB:
73
+ """Build the reference used for scoring (PSP) and for evaluation (GS, FUN).
74
+
75
+ Each argument may be a DataFrame or file path (your own data), ``None`` (the
76
+ default dataset from ``database`` / the default database folder) or ``False``
77
+ (``gold``/``fun`` only: do not load). If ``psp`` is a :class:`ReferenceDB`, it is
78
+ returned as is, or as a copy extended with explicitly given ``gold``/``fun`` tables.
79
+
80
+ >>> ref = load_reference() # all default
81
+ >>> ref = load_reference(psp="my_psp.tsv") # own PSP, default GS/FUN
82
+ >>> ref = load_reference(gold=False, fun=False) # default PSP only
83
+ """
84
+ if isinstance(psp, ReferenceDB):
85
+ if gold is None and fun is None or gold is False and fun is False:
86
+ return psp
87
+ ref = copy.copy(psp)
88
+ if gold is not None and gold is not False:
89
+ ref.gold = set(psp.gold)
90
+ ref.load_gold(read_table(gold))
91
+ if fun is not None and fun is not False:
92
+ ref.fun = dict(psp.fun)
93
+ ref.load_fun(read_table(fun))
94
+ return ref
95
+
96
+ if psp is None and (gold is None or gold is False) and (fun is None or fun is False):
97
+ return default_reference(gold=gold is None, fun=fun is None, organism=organism, path=database)
98
+
99
+ def pick(x, name):
100
+ if x is False:
101
+ return None
102
+ return database_file(name, database) if x is None else x
103
+
104
+ return ReferenceDB.from_sources(pick(psp, "psp"), pick(gold, "gold"), pick(fun, "fun"), organism=organism)
105
+
106
+
107
+ def _settings(weights, threshold, config):
108
+ w, thr_w, cfg_w = resolve_weights(weights)
109
+ thr = threshold if threshold is not None else (thr_w if thr_w is not None else DEFAULT_THRESHOLD)
110
+ cfg = config if config is not None else (cfg_w if cfg_w is not None else FeatureConfig())
111
+ return w, float(thr), cfg
112
+
113
+
114
+ def _peptides(peptides: TableLike, columns=None, fasta=None, config: Optional[FeatureConfig] = None) -> pd.DataFrame:
115
+ df = read_table(peptides)
116
+ res_col = {**DEFAULT_COLUMNS, **(columns or {})}["residue"]
117
+ if res_col not in df.columns:
118
+ label = config.ptm_label if config is not None else "*"
119
+ df = annotate_residues(df, fasta, columns, label)
120
+ return df
121
+
122
+
123
+ # ------------------------------------------------------------------ optimize
124
+ def optimize(
125
+ peptides: TableLike,
126
+ psp: RefSpec = None,
127
+ gold: OptTable = None,
128
+ fun: OptTable = None,
129
+ threshold: float = DEFAULT_THRESHOLD,
130
+ n_samples: int = 2000,
131
+ config: Optional[FeatureConfig] = None,
132
+ columns: Optional[Mapping[str, str]] = None,
133
+ out_dir: Optional[str] = None,
134
+ organism: Optional[str] = None,
135
+ fasta: Optional[Union[TableLike, Mapping[str, str]]] = None,
136
+ **kwargs,
137
+ ) -> OptimizationResult:
138
+ """Optimize (w_H, w_L, w_D) against Gold Standard / Functional Landscape sites.
139
+
140
+ Extra keyword arguments go to :func:`ptmreloc.optimize_weights` (``refine``,
141
+ ``holdout_frac``, ``top_frac``, ``gs_weight``, ``log10_bounds``, ``seed``, ``verbose`` ...).
142
+ If ``out_dir`` is given, ``weights.json`` and the trial/evaluation tables are written there.
143
+
144
+ >>> res = ptmreloc.optimize("peptides.tsv") # default reference data
145
+ >>> res = ptmreloc.optimize(df, psp="my_psp.tsv", gold="my_gs.tsv") # own data (FUN default)
146
+ >>> res.weights, res.summary()
147
+ """
148
+ ref = load_reference(psp, gold, fun, organism)
149
+ if not ref.has_targets:
150
+ raise ValueError("optimize needs Gold Standard and/or Functional Landscape sites (gold=, fun=)")
151
+ cfg = config or FeatureConfig()
152
+ table = build_candidate_table(_peptides(peptides, columns, fasta, cfg), ref, cfg, columns)
153
+ res = optimize_weights(table, threshold=threshold, n_samples=n_samples, **kwargs)
154
+ if out_dir:
155
+ res.save(out_dir)
156
+ return res
157
+
158
+
159
+ # ------------------------------------------------------------------ reassign
160
+ def reassign(
161
+ peptides: TableLike,
162
+ psp: RefSpec = None,
163
+ weights: WeightsSpec = None,
164
+ threshold: Optional[float] = None,
165
+ config: Optional[FeatureConfig] = None,
166
+ columns: Optional[Mapping[str, str]] = None,
167
+ include_candidates: bool = True,
168
+ output: Optional[str] = None,
169
+ organism: Optional[str] = None,
170
+ fasta: Optional[Union[TableLike, Mapping[str, str]]] = None,
171
+ ) -> pd.DataFrame:
172
+ """Relocalize sites of peptides with AScore <= threshold. Only PSP is required.
173
+
174
+ Without ``weights`` the package defaults are used (threshold 30); without
175
+ ``psp`` the default PhosphoSitePlus table is used.
176
+ Returns the input table plus ``NewPeptide``, ``NewResidue``, ``Reassigned``,
177
+ ``ReassignStatus``, ``OriginalSiteProb``, ``NewSiteProb`` (and ``CandidateSiteProbs``).
178
+
179
+ >>> out = ptmreloc.reassign(df) # default PSP, default weights
180
+ >>> out = ptmreloc.reassign(df, psp="my_psp.tsv", weights="opt/weights.json")
181
+ """
182
+ w, thr, cfg = _settings(weights, threshold, config)
183
+ ref = load_reference(psp, gold=False, fun=False, organism=organism)
184
+ df = _peptides(peptides, columns, fasta, cfg)
185
+ out = Reassigner(ref, w, thr, cfg, columns).reassign(df, include_candidates)
186
+ if output:
187
+ write_table(out, output)
188
+ return out
189
+
190
+
191
+ # ------------------------------------------------------------------ evaluate
192
+ @dataclass
193
+ class EvaluationReport:
194
+ weights: Dict[str, float]
195
+ threshold: float
196
+ overall: Dict[str, float] # metrics on rows with AScore <= threshold
197
+ by_ascore: pd.DataFrame # metrics per AScore bin (selection applied to every bin)
198
+ config: FeatureConfig = field(default_factory=FeatureConfig)
199
+
200
+ def summary(self) -> str:
201
+ keys = ["n_rows", "n_rows_changed", "delta_gs", "random_delta_gs", "oracle_delta_gs", "delta_fun",
202
+ "random_delta_fun", "oracle_delta_fun", "gs_gain", "fun_gain", "composite"]
203
+ head = "\n".join(f" {k:<18}{self.overall[k]:.4g}" for k in keys)
204
+ cols = ["ascore_bin", "n_rows", "frac_rows_changed", "delta_gs", "random_delta_gs", "delta_fun",
205
+ "random_delta_fun", "gs_gain", "fun_gain", "composite"]
206
+ with pd.option_context("display.width", 200, "display.max_columns", 20, "display.precision", 4):
207
+ return (f"weights {self.weights}, AScore <= {self.threshold:g}\n{head}\n\n"
208
+ f"{self.by_ascore[cols].to_string(index=False)}")
209
+
210
+
211
+ def evaluate(
212
+ peptides: TableLike,
213
+ psp: RefSpec = None,
214
+ gold: OptTable = None,
215
+ fun: OptTable = None,
216
+ weights: WeightsSpec = None,
217
+ threshold: Optional[float] = None,
218
+ bins: Sequence[float] = DEFAULT_ASCORE_BINS,
219
+ config: Optional[FeatureConfig] = None,
220
+ columns: Optional[Mapping[str, str]] = None,
221
+ gs_weight: float = 0.5,
222
+ organism: Optional[str] = None,
223
+ fasta: Optional[Union[TableLike, Mapping[str, str]]] = None,
224
+ ) -> EvaluationReport:
225
+ """Score a weight set against GS/FUN sites, overall and per AScore bin.
226
+
227
+ >>> rep = ptmreloc.evaluate(df) # default data, default weights
228
+ >>> print(rep.summary())
229
+ """
230
+ w, thr, cfg = _settings(weights, threshold, config)
231
+ ref = load_reference(psp, gold, fun, organism)
232
+ if not ref.has_targets:
233
+ raise ValueError("evaluate needs Gold Standard and/or Functional Landscape sites (gold=, fun=)")
234
+ table = build_candidate_table(_peptides(peptides, columns, fasta, cfg), ref, cfg, columns)
235
+ sel = choose(table, w)
236
+ overall = _evaluate_selection(table, sel, table.r_ascore <= thr, gs_weight)
237
+ by_bin = evaluate_by_ascore(table, sel, bins=bins, gs_weight=gs_weight)
238
+ return EvaluationReport(w, thr, overall, by_bin, cfg)
239
+
240
+
241
+ def compare_weights(
242
+ peptides: TableLike,
243
+ psp: RefSpec = None,
244
+ gold: OptTable = None,
245
+ fun: OptTable = None,
246
+ weight_sets: Optional[Mapping[str, WeightsSpec]] = None,
247
+ threshold: float = DEFAULT_THRESHOLD,
248
+ config: Optional[FeatureConfig] = None,
249
+ columns: Optional[Mapping[str, str]] = None,
250
+ gs_weight: float = 0.5,
251
+ organism: Optional[str] = None,
252
+ fasta: Optional[Union[TableLike, Mapping[str, str]]] = None,
253
+ ) -> pd.DataFrame:
254
+ """Evaluate several weight sets on the same data (features built once).
255
+
256
+ ``weight_sets`` defaults to all presets. Returns one row per weight set.
257
+ All sets share ``threshold`` and ``config``.
258
+ """
259
+ weight_sets = weight_sets if weight_sets is not None else {k: k for k in WEIGHT_PRESETS}
260
+ ref = load_reference(psp, gold, fun, organism)
261
+ cfg = config or FeatureConfig()
262
+ table = build_candidate_table(_peptides(peptides, columns, fasta, cfg), ref, cfg, columns)
263
+ mask = table.r_ascore <= threshold
264
+ recs = []
265
+ for name, spec in weight_sets.items():
266
+ w, _, _ = resolve_weights(spec)
267
+ recs.append({"weights": name, **w, **_evaluate_selection(table, choose(table, w), mask, gs_weight)})
268
+ return pd.DataFrame(recs)
ptmreloc/cli.py ADDED
@@ -0,0 +1,199 @@
1
+ """Command-line interface: ``ptmreloc optimize | reassign | evaluate``."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import sys
6
+ import time
7
+
8
+ import numpy as np
9
+
10
+ from . import api
11
+ from .defaults import DEFAULT_THRESHOLD, DEFAULT_WEIGHTS, WEIGHT_PRESETS
12
+ from .features import FeatureConfig
13
+ from .reference import read_table, write_table
14
+
15
+
16
+ def _add_common(p: argparse.ArgumentParser, targets: bool) -> None:
17
+ p.add_argument("-i", "--input", required=True, help="peptide table (pkl/csv/tsv/parquet/xlsx)")
18
+ g = p.add_argument_group("reference data (default: human reference database, downloaded on first use)")
19
+ g.add_argument("--psp", default=None, help="own PhosphoSitePlus site table")
20
+ if targets:
21
+ g.add_argument("--gold", default=None, help="own Gold Standard site table ('none' to disable)")
22
+ g.add_argument("--fun", default=None, help="own Functional Landscape site table ('none' to disable)")
23
+ g.add_argument("--fasta", default=None,
24
+ help="own protein FASTA, used only when the input has no residue column")
25
+ g.add_argument("--database", default=None, help="folder with reference files (same names) to use instead")
26
+ g.add_argument("--organism", default=None, help="filter own PSP by ORGANISM (e.g. human)")
27
+ p.add_argument("--sheet", default=None, help="Excel sheet name for --input")
28
+ g = p.add_argument_group("input columns")
29
+ g.add_argument("--col-uniprot", default="UniProt")
30
+ g.add_argument("--col-peptide", default="Peptide")
31
+ g.add_argument("--col-residue", default="Residue")
32
+ g.add_argument("--col-ascore", default="AScore")
33
+
34
+
35
+ def _add_features(p: argparse.ArgumentParser, inherit: bool) -> None:
36
+ g = p.add_argument_group("features" + (" (default: from --weights file, else legacy)" if inherit else ""))
37
+ d = None if inherit else argparse.SUPPRESS
38
+ g.add_argument("--ptm-label", default=d)
39
+ g.add_argument("--candidate-residues", default=d)
40
+ g.add_argument("--evidence-transform", choices=["legacy", "log", "raw"], default=d)
41
+ g.add_argument("--distance-mode", choices=["legacy", "inverse"], default=d)
42
+
43
+
44
+ def _config(a, base: FeatureConfig = FeatureConfig()):
45
+ """Explicit feature flags override ``base``; returns None when nothing was given and base is None."""
46
+ over = {k: getattr(a, k, None) for k in ("ptm_label", "candidate_residues", "evidence_transform", "distance_mode")}
47
+ over = {k: v for k, v in over.items() if v is not None}
48
+ return FeatureConfig(**{**base.to_dict(), **over})
49
+
50
+
51
+ def _opt_table(x):
52
+ return False if isinstance(x, str) and x.lower() == "none" else x
53
+
54
+
55
+ def _ref(a, targets: bool):
56
+ gold = _opt_table(a.gold) if targets else False
57
+ fun = _opt_table(a.fun) if targets else False
58
+ return api.load_reference(a.psp, gold, fun, organism=a.organism, database=a.database)
59
+
60
+
61
+ def _columns(a):
62
+ return {"uniprot": a.col_uniprot, "peptide": a.col_peptide, "residue": a.col_residue, "ascore": a.col_ascore}
63
+
64
+
65
+ def _read_input(a):
66
+ kw = {"sheet_name": a.sheet} if a.sheet else {}
67
+ return read_table(a.input, **kw)
68
+
69
+
70
+ def _weights_spec(a):
71
+ if a.w is not None:
72
+ return list(a.w)
73
+ return a.weights # file, preset name, or None -> defaults
74
+
75
+
76
+ def _resolved(a):
77
+ w, thr, cfg = api.resolve_weights(_weights_spec(a))
78
+ thr = a.threshold if a.threshold is not None else (thr if thr is not None else DEFAULT_THRESHOLD)
79
+ return w, thr, _config(a, cfg or FeatureConfig())
80
+
81
+
82
+ def cmd_optimize(a) -> int:
83
+ t0 = time.time()
84
+ res = api.optimize(
85
+ _read_input(a), _ref(a, True), fasta=a.fasta, threshold=a.threshold, n_samples=a.n_samples,
86
+ config=_config(a), columns=_columns(a), out_dir=a.out, organism=a.organism,
87
+ log10_bounds=(a.log10_low, a.log10_high), refine=not a.no_refine, refine_maxiter=a.refine_maxiter,
88
+ top_frac=a.top_frac, holdout_frac=a.holdout, gs_weight=a.gs_weight, seed=a.seed, verbose=True,
89
+ )
90
+ print(res.summary())
91
+ print(f"\nwrote {a.out}/weights.json, trials.csv, evaluation.csv, metrics_by_ascore.csv "
92
+ f"({time.time() - t0:.1f}s)")
93
+ return 0
94
+
95
+
96
+ def cmd_reassign(a) -> int:
97
+ w, thr, cfg = _resolved(a)
98
+ out = api.reassign(_read_input(a), _ref(a, False), fasta=a.fasta, weights=w, threshold=thr, config=cfg, columns=_columns(a),
99
+ include_candidates=not a.no_candidates, organism=a.organism)
100
+ write_table(out, a.output)
101
+ n_elig = int((out["ReassignStatus"] == "eligible").sum())
102
+ print(f"weights {w}, AScore <= {thr:g}")
103
+ print(f"{len(out)} rows; {n_elig} eligible; {int(out['Reassigned'].sum())} relocalized; "
104
+ f"{int((out['ReassignStatus'] == 'unparsed').sum())} unparsed -> {a.output}")
105
+ return 0
106
+
107
+
108
+ def cmd_evaluate(a) -> int:
109
+ w, thr, cfg = _resolved(a)
110
+ bins = [-np.inf] + [float(x) for x in a.bins.split(",")]
111
+ rep = api.evaluate(_read_input(a), _ref(a, True), fasta=a.fasta, weights=w, threshold=thr, bins=bins,
112
+ config=cfg, columns=_columns(a), organism=a.organism)
113
+ print(rep.summary())
114
+ if a.output:
115
+ write_table(rep.by_ascore, a.output)
116
+ return 0
117
+
118
+
119
+ def cmd_info(a) -> int:
120
+ from .database import _database_url, cache_dir, database_info
121
+
122
+ with __import__("pandas").option_context("display.width", 200, "display.max_colwidth", 120):
123
+ print(database_info(a.database).to_string(index=False))
124
+ print(f"\ndownload cache: {cache_dir()}\ndownload URL: {_database_url() or '(not configured)'}")
125
+ print(f"\ndefault weights: {DEFAULT_WEIGHTS}, AScore threshold {DEFAULT_THRESHOLD:g}")
126
+ print(f"presets: {', '.join(WEIGHT_PRESETS)}")
127
+ return 0
128
+
129
+
130
+ def cmd_download(a) -> int:
131
+ from .database import download_database
132
+
133
+ dest = download_database(url=a.url, dest=a.dest, force=a.force, verify=not a.no_verify)
134
+ print(f"reference data ready in {dest}")
135
+ return 0
136
+
137
+
138
+ def build_parser() -> argparse.ArgumentParser:
139
+ ap = argparse.ArgumentParser(prog="ptmreloc", description="Prior-knowledge guided PTM site relocalization")
140
+ sub = ap.add_subparsers(dest="cmd", required=True)
141
+
142
+ p = sub.add_parser("info", help="show the reference data files in use and the default weights")
143
+ p.add_argument("--database", default=None)
144
+ p.set_defaults(func=cmd_info)
145
+
146
+ p = sub.add_parser("download", help="download the reference data (otherwise fetched on first use)")
147
+ p.add_argument("--url", default=None, help="base URL of the data files (default: the package's)")
148
+ p.add_argument("--dest", default=None, help="target folder (default: the per-user cache)")
149
+ p.add_argument("--force", action="store_true", help="download again even if present")
150
+ p.add_argument("--no-verify", action="store_true", help="skip checksum verification")
151
+ p.set_defaults(func=cmd_download)
152
+
153
+ p = sub.add_parser("optimize", help="optimize weights against Gold Standard / Functional sites")
154
+ _add_common(p, targets=True)
155
+ _add_features(p, inherit=True)
156
+ p.add_argument("-o", "--out", required=True, help="output directory")
157
+ p.add_argument("--threshold", type=float, default=DEFAULT_THRESHOLD,
158
+ help="AScore cutoff (reassign AScore <= cutoff)")
159
+ p.add_argument("--n-samples", type=int, default=2000)
160
+ p.add_argument("--log10-low", type=float, default=-3.0)
161
+ p.add_argument("--log10-high", type=float, default=0.0)
162
+ p.add_argument("--no-refine", action="store_true")
163
+ p.add_argument("--refine-maxiter", type=int, default=30)
164
+ p.add_argument("--top-frac", type=float, default=0.05)
165
+ p.add_argument("--holdout", type=float, default=0.2, help="fraction of proteins held out")
166
+ p.add_argument("--gs-weight", type=float, default=0.5, help="GS weight in the composite score")
167
+ p.add_argument("--seed", type=int, default=0)
168
+ p.set_defaults(func=cmd_optimize)
169
+
170
+ wh = (f"weights.json from `ptmreloc optimize`, or a preset ({', '.join(WEIGHT_PRESETS)}). "
171
+ f"Default: {DEFAULT_WEIGHTS}")
172
+ for name, func, targets, help_ in [
173
+ ("reassign", cmd_reassign, False, "relocalize sites (default weights unless --weights/--w)"),
174
+ ("evaluate", cmd_evaluate, True, "score weights against GS/FUN, per AScore bin"),
175
+ ]:
176
+ p = sub.add_parser(name, help=help_)
177
+ _add_common(p, targets=targets)
178
+ _add_features(p, inherit=True)
179
+ p.add_argument("--weights", default=None, help=wh)
180
+ p.add_argument("--w", nargs=3, type=float, metavar=("W_H", "W_L", "W_D"), help="explicit weights")
181
+ p.add_argument("--threshold", type=float, default=None,
182
+ help=f"AScore cutoff (default: from weights file, else {DEFAULT_THRESHOLD:g})")
183
+ if name == "reassign":
184
+ p.add_argument("-o", "--output", required=True, help="output table (.tsv/.csv/.pkl/.parquet/.xlsx)")
185
+ p.add_argument("--no-candidates", action="store_true", help="omit CandidateSiteProbs column")
186
+ else:
187
+ p.add_argument("--bins", default="0,10,20,30,50,100,inf", help="AScore bin edges")
188
+ p.add_argument("-o", "--output", default=None, help="write per-bin metrics table")
189
+ p.set_defaults(func=func)
190
+ return ap
191
+
192
+
193
+ def main(argv=None) -> int:
194
+ a = build_parser().parse_args(argv)
195
+ return a.func(a)
196
+
197
+
198
+ if __name__ == "__main__":
199
+ sys.exit(main())