msfiddle 2.0.1__tar.gz → 2.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {msfiddle-2.0.1 → msfiddle-2.1.0}/PKG-INFO +45 -11
- {msfiddle-2.0.1 → msfiddle-2.1.0}/README.md +44 -10
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/__init__.py +6 -1
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/api.py +17 -4
- msfiddle-2.1.0/msfiddle/external_results.py +380 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/main.py +17 -2
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle.egg-info/PKG-INFO +45 -11
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle.egg-info/SOURCES.txt +2 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/setup.py +1 -1
- {msfiddle-2.0.1 → msfiddle-2.1.0}/tests/test_api.py +101 -1
- msfiddle-2.1.0/tests/test_external_results.py +282 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/LICENSE +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/config/fiddle_tcn_casmi.yml +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/config/fiddle_tcn_demo.yml +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/config/fiddle_tcn_demo_wo_da.yml +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/config/fiddle_tcn_embl.yml +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/config/fiddle_tcn_orbitrap.yml +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/config/fiddle_tcn_qtof.yml +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/dataset.py +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/demo/input_msms.mgf +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/demo/output_buddy.csv +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/demo/output_fiddle.csv +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/demo/output_fiddle_all.csv +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/demo/output_sirius.csv +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/download.py +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/model_tcn.py +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/utils/__init__.py +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/utils/mol_utils.py +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/utils/msms_utils.py +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/utils/pkl_utils.py +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle/utils/refine_utils.py +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle.egg-info/dependency_links.txt +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle.egg-info/entry_points.txt +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle.egg-info/requires.txt +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/msfiddle.egg-info/top_level.txt +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/setup.cfg +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/tests/test_download.py +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/tests/test_main.py +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/tests/test_mol_utils.py +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/tests/test_msms_utils.py +0 -0
- {msfiddle-2.0.1 → msfiddle-2.1.0}/tests/test_refine_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: msfiddle
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: A package for predicting chemical formulas from tandem mass spectra
|
|
5
5
|
Home-page: https://github.com/JosieHong/msfiddle
|
|
6
6
|
Author: Yuhui Hong
|
|
@@ -64,17 +64,13 @@ For the full experimental codebase, see https://github.com/JosieHong/FIDDLE.
|
|
|
64
64
|
## Installation
|
|
65
65
|
|
|
66
66
|
```bash
|
|
67
|
-
pip install msfiddle
|
|
67
|
+
pip install msfiddle # base install, no PyTorch
|
|
68
|
+
pip install "msfiddle[inference]" # base install + PyTorch (needed for predictions and the CLI)
|
|
68
69
|
```
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
pip install "msfiddle[inference]"
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
See the official PyTorch installation guide for custom CUDA builds:
|
|
77
|
-
https://pytorch.org/get-started/locally/.
|
|
71
|
+
For a custom PyTorch build (CUDA, ROCm, Apple Silicon, etc.), install torch
|
|
72
|
+
first via the [official guide](https://pytorch.org/get-started/locally/), then
|
|
73
|
+
`pip install msfiddle`.
|
|
78
74
|
|
|
79
75
|
## Usage
|
|
80
76
|
|
|
@@ -90,7 +86,7 @@ msfiddle-download-models --destination /path/to/models \
|
|
|
90
86
|
--models fiddle_tcn_qtof fiddle_rescore_qtof
|
|
91
87
|
```
|
|
92
88
|
|
|
93
|
-
`msfiddle` 2.
|
|
89
|
+
All `msfiddle` 2.x releases reuse the FIDDLE `v2.0.0` checkpoint assets.
|
|
94
90
|
|
|
95
91
|
Run the packaged demo:
|
|
96
92
|
```bash
|
|
@@ -190,6 +186,44 @@ The Python `predict_from_spectrum()` API returns a list of candidate dictionarie
|
|
|
190
186
|
`predict_batch()` returns one record per input spectrum with `id`, `candidates`,
|
|
191
187
|
and `metadata`.
|
|
192
188
|
|
|
189
|
+
### Native/original BUDDY and SIRIUS outputs
|
|
190
|
+
|
|
191
|
+
Use the same MGF input file to generate native
|
|
192
|
+
[BUDDY/msbuddy](https://msbuddy.readthedocs.io/en/latest/cmdapi.html) and
|
|
193
|
+
[SIRIUS](https://v6.docs.sirius-ms.io/cli/) outputs. `msfiddle` accepts these
|
|
194
|
+
native/original files directly through `--buddy_path` and `--sirius_path`. The
|
|
195
|
+
older msfiddle-normalized CSV formats documented in `docs/formats.rst` are
|
|
196
|
+
**deprecated** and will be removed in `msfiddle` 3.0.0; loading them emits a
|
|
197
|
+
`DeprecationWarning`.
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
msbuddy -mgf /path/to/data.mgf \
|
|
201
|
+
-output /path/to/buddy_output \
|
|
202
|
+
-ms orbitrap \
|
|
203
|
+
-p -n_cpu 12 \
|
|
204
|
+
-d -hal
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`msbuddy` writes `msbuddy_result_summary.tsv`; `-d` also writes detailed
|
|
208
|
+
per-query `formula_results.tsv` files. Pass either the summary file or the
|
|
209
|
+
output directory to `--buddy_path`. The full output directory is preferred
|
|
210
|
+
because it includes per-candidate FDR scores for ranks beyond rank 1.
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
sirius --input /path/to/data.mgf \
|
|
214
|
+
--project /path/to/sirius_project \
|
|
215
|
+
formulas --profile orbitrap
|
|
216
|
+
|
|
217
|
+
sirius --project /path/to/sirius_project \
|
|
218
|
+
summaries --top-k-summary=5 \
|
|
219
|
+
--output /path/to/sirius_output
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
SIRIUS writes a project space and exports summary files such as
|
|
223
|
+
`formula_identifications.tsv`. Pass either a formula-identification summary file
|
|
224
|
+
or the summary output directory to `--sirius_path`.
|
|
225
|
+
SIRIUS 6 may require `sirius login` before formula computation.
|
|
226
|
+
|
|
193
227
|
### MGF input
|
|
194
228
|
|
|
195
229
|
The required MGF fields are `TITLE`, `PRECURSOR_MZ`, `PRECURSOR_TYPE`, and
|
|
@@ -23,17 +23,13 @@ For the full experimental codebase, see https://github.com/JosieHong/FIDDLE.
|
|
|
23
23
|
## Installation
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
pip install msfiddle
|
|
26
|
+
pip install msfiddle # base install, no PyTorch
|
|
27
|
+
pip install "msfiddle[inference]" # base install + PyTorch (needed for predictions and the CLI)
|
|
27
28
|
```
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
pip install "msfiddle[inference]"
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
See the official PyTorch installation guide for custom CUDA builds:
|
|
36
|
-
https://pytorch.org/get-started/locally/.
|
|
30
|
+
For a custom PyTorch build (CUDA, ROCm, Apple Silicon, etc.), install torch
|
|
31
|
+
first via the [official guide](https://pytorch.org/get-started/locally/), then
|
|
32
|
+
`pip install msfiddle`.
|
|
37
33
|
|
|
38
34
|
## Usage
|
|
39
35
|
|
|
@@ -49,7 +45,7 @@ msfiddle-download-models --destination /path/to/models \
|
|
|
49
45
|
--models fiddle_tcn_qtof fiddle_rescore_qtof
|
|
50
46
|
```
|
|
51
47
|
|
|
52
|
-
`msfiddle` 2.
|
|
48
|
+
All `msfiddle` 2.x releases reuse the FIDDLE `v2.0.0` checkpoint assets.
|
|
53
49
|
|
|
54
50
|
Run the packaged demo:
|
|
55
51
|
```bash
|
|
@@ -149,6 +145,44 @@ The Python `predict_from_spectrum()` API returns a list of candidate dictionarie
|
|
|
149
145
|
`predict_batch()` returns one record per input spectrum with `id`, `candidates`,
|
|
150
146
|
and `metadata`.
|
|
151
147
|
|
|
148
|
+
### Native/original BUDDY and SIRIUS outputs
|
|
149
|
+
|
|
150
|
+
Use the same MGF input file to generate native
|
|
151
|
+
[BUDDY/msbuddy](https://msbuddy.readthedocs.io/en/latest/cmdapi.html) and
|
|
152
|
+
[SIRIUS](https://v6.docs.sirius-ms.io/cli/) outputs. `msfiddle` accepts these
|
|
153
|
+
native/original files directly through `--buddy_path` and `--sirius_path`. The
|
|
154
|
+
older msfiddle-normalized CSV formats documented in `docs/formats.rst` are
|
|
155
|
+
**deprecated** and will be removed in `msfiddle` 3.0.0; loading them emits a
|
|
156
|
+
`DeprecationWarning`.
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
msbuddy -mgf /path/to/data.mgf \
|
|
160
|
+
-output /path/to/buddy_output \
|
|
161
|
+
-ms orbitrap \
|
|
162
|
+
-p -n_cpu 12 \
|
|
163
|
+
-d -hal
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`msbuddy` writes `msbuddy_result_summary.tsv`; `-d` also writes detailed
|
|
167
|
+
per-query `formula_results.tsv` files. Pass either the summary file or the
|
|
168
|
+
output directory to `--buddy_path`. The full output directory is preferred
|
|
169
|
+
because it includes per-candidate FDR scores for ranks beyond rank 1.
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
sirius --input /path/to/data.mgf \
|
|
173
|
+
--project /path/to/sirius_project \
|
|
174
|
+
formulas --profile orbitrap
|
|
175
|
+
|
|
176
|
+
sirius --project /path/to/sirius_project \
|
|
177
|
+
summaries --top-k-summary=5 \
|
|
178
|
+
--output /path/to/sirius_output
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
SIRIUS writes a project space and exports summary files such as
|
|
182
|
+
`formula_identifications.tsv`. Pass either a formula-identification summary file
|
|
183
|
+
or the summary output directory to `--sirius_path`.
|
|
184
|
+
SIRIUS 6 may require `sirius login` before formula computation.
|
|
185
|
+
|
|
152
186
|
### MGF input
|
|
153
187
|
|
|
154
188
|
The required MGF fields are `TITLE`, `PRECURSOR_MZ`, `PRECURSOR_TYPE`, and
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
msfiddle: A package for predicting chemical formulas from tandem mass spectra
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
__version__ = _pkg_version("msfiddle")
|
|
9
|
+
except PackageNotFoundError: # running from a source checkout without install
|
|
10
|
+
__version__ = "0.0.0+unknown"
|
|
6
11
|
|
|
7
12
|
from .api import (
|
|
8
13
|
MsFiddlePredictor,
|
|
@@ -20,6 +20,7 @@ from pyteomics import mgf
|
|
|
20
20
|
|
|
21
21
|
from .download import download_models as download_pretrained_models
|
|
22
22
|
from .download import get_checkpoint_dir
|
|
23
|
+
from .external_results import load_buddy_results, load_sirius_results
|
|
23
24
|
from .utils.mol_utils import (
|
|
24
25
|
ATOMS_INDEX,
|
|
25
26
|
formula_to_dict,
|
|
@@ -198,6 +199,10 @@ class MsFiddlePredictor:
|
|
|
198
199
|
) -> pd.DataFrame:
|
|
199
200
|
"""Predict formulas for spectra in an MGF file.
|
|
200
201
|
|
|
202
|
+
``buddy_path`` and ``sirius_path`` may point to either native/original
|
|
203
|
+
BUDDY/msbuddy and SIRIUS formula-identification outputs or legacy
|
|
204
|
+
msfiddle-normalized CSV files.
|
|
205
|
+
|
|
201
206
|
The returned DataFrame uses the same result columns as the CLI CSV.
|
|
202
207
|
"""
|
|
203
208
|
|
|
@@ -212,8 +217,8 @@ class MsFiddlePredictor:
|
|
|
212
217
|
encoded = self._load_mgf_spectra(test_data_path)
|
|
213
218
|
predictions = self._predict_encoded(encoded, batch_size=batch_size)
|
|
214
219
|
|
|
215
|
-
buddy_df =
|
|
216
|
-
sirius_df =
|
|
220
|
+
buddy_df = load_buddy_results(buddy_path) if buddy_path else None
|
|
221
|
+
sirius_df = load_sirius_results(sirius_path) if sirius_path else None
|
|
217
222
|
|
|
218
223
|
refined_formula = {f"Refined Formula ({k})": [] for k in range(top_k)}
|
|
219
224
|
refined_mass = {f"Refined Mass ({k})": [] for k in range(top_k)}
|
|
@@ -787,7 +792,8 @@ class MsFiddlePredictor:
|
|
|
787
792
|
candidates.extend(
|
|
788
793
|
formula
|
|
789
794
|
for formula, score in zip(formulas, scores)
|
|
790
|
-
if
|
|
795
|
+
if _is_external_formula(formula)
|
|
796
|
+
and pd.notna(score)
|
|
791
797
|
and score < self.config["post_processing"]["buddy_fdr_thr"]
|
|
792
798
|
)
|
|
793
799
|
if sirius_df is not None:
|
|
@@ -815,7 +821,8 @@ class MsFiddlePredictor:
|
|
|
815
821
|
candidates.extend(
|
|
816
822
|
formula
|
|
817
823
|
for formula, score in zip(formulas, scores)
|
|
818
|
-
if
|
|
824
|
+
if _is_external_formula(formula)
|
|
825
|
+
and pd.notna(score)
|
|
819
826
|
and score > self.config["post_processing"]["sirius_score_thr"]
|
|
820
827
|
)
|
|
821
828
|
return candidates
|
|
@@ -934,6 +941,12 @@ def _normalize_instrument_type(instrument_type: str) -> str:
|
|
|
934
941
|
return normalized
|
|
935
942
|
|
|
936
943
|
|
|
944
|
+
def _is_external_formula(value: object) -> bool:
|
|
945
|
+
if pd.isna(value):
|
|
946
|
+
return False
|
|
947
|
+
return str(value).strip().lower() not in {"", "na", "nan", "none", "null", "<na>"}
|
|
948
|
+
|
|
949
|
+
|
|
937
950
|
def _load_config(config_path: Path) -> dict[str, Any]:
|
|
938
951
|
if not config_path.exists():
|
|
939
952
|
raise FileNotFoundError(f"Config file not found: {config_path}")
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""Load external formula candidates from BUDDY/msbuddy and SIRIUS outputs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import warnings
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Iterable
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
_NORMALIZED_CSV_DEPRECATION = (
|
|
13
|
+
"The msfiddle-normalized {tool} CSV format is deprecated and will be "
|
|
14
|
+
"removed in msfiddle 3.0.0. Pass the native/original {tool} output "
|
|
15
|
+
"(directory or summary file) instead."
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
BUDDY_FORMULA_COLUMNS = tuple(f"Pred Formula ({idx})" for idx in range(1, 6))
|
|
19
|
+
BUDDY_SCORE_COLUMNS = tuple(f"BUDDY Score ({idx})" for idx in range(1, 6))
|
|
20
|
+
BUDDY_COLUMNS = ("ID", "Adduct", *BUDDY_FORMULA_COLUMNS, *BUDDY_SCORE_COLUMNS)
|
|
21
|
+
|
|
22
|
+
SIRIUS_FORMULA_COLUMNS = tuple(f"Pred Formula ({idx})" for idx in range(1, 6))
|
|
23
|
+
SIRIUS_ADDUCT_COLUMNS = tuple(f"Pred Adduct ({idx})" for idx in range(1, 6))
|
|
24
|
+
SIRIUS_SCORE_COLUMNS = tuple(f"SIRIUS Score ({idx})" for idx in range(1, 6))
|
|
25
|
+
SIRIUS_COLUMNS = (
|
|
26
|
+
"ID",
|
|
27
|
+
*(
|
|
28
|
+
value
|
|
29
|
+
for idx in range(1, 6)
|
|
30
|
+
for value in (
|
|
31
|
+
f"Pred Formula ({idx})",
|
|
32
|
+
f"Pred Adduct ({idx})",
|
|
33
|
+
f"SIRIUS Score ({idx})",
|
|
34
|
+
)
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
_BUDDY_NATIVE_COLUMNS = (
|
|
39
|
+
"identifier",
|
|
40
|
+
"adduct",
|
|
41
|
+
"formula_rank_1",
|
|
42
|
+
"estimated_fdr",
|
|
43
|
+
)
|
|
44
|
+
_SIRIUS_FORMULA_FILES = (
|
|
45
|
+
"formula_identifications_top-",
|
|
46
|
+
"formula_identifications_all",
|
|
47
|
+
"formula_identifications_adducts_top-",
|
|
48
|
+
"formula_identifications_adducts_all",
|
|
49
|
+
"formula_identifications",
|
|
50
|
+
)
|
|
51
|
+
_SIRIUS_ID_COLUMNS = (
|
|
52
|
+
"name",
|
|
53
|
+
"mappingFeatureId",
|
|
54
|
+
"alignedFeatureId",
|
|
55
|
+
"featureId",
|
|
56
|
+
"compoundId",
|
|
57
|
+
"id",
|
|
58
|
+
)
|
|
59
|
+
_SIRIUS_FORMULA_COLUMNS = ("molecularFormula", "formula", "Formula")
|
|
60
|
+
_SIRIUS_ADDUCT_COLUMNS = ("adduct", "ionType", "ion", "precursorIonType")
|
|
61
|
+
_SIRIUS_SCORE_COLUMNS = (
|
|
62
|
+
"SiriusScore",
|
|
63
|
+
"SIRIUS Score",
|
|
64
|
+
"rankingScore",
|
|
65
|
+
"ZodiacScore",
|
|
66
|
+
"ConfidenceScore",
|
|
67
|
+
"score",
|
|
68
|
+
)
|
|
69
|
+
_SIRIUS_RANK_COLUMNS = ("rank", "formulaRank")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def load_buddy_results(path: str | Path) -> pd.DataFrame:
|
|
73
|
+
"""Load native/original or msfiddle-normalized BUDDY/msbuddy results.
|
|
74
|
+
|
|
75
|
+
Native/original msbuddy inputs may be either the
|
|
76
|
+
``msbuddy_result_summary.tsv`` file or the output directory that contains
|
|
77
|
+
it. If detailed ``formula_results.tsv`` files are present, their
|
|
78
|
+
per-candidate FDR values are used.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
resolved = _resolve_path(path, "BUDDY")
|
|
82
|
+
if resolved.is_dir():
|
|
83
|
+
return _load_msbuddy_directory(resolved)
|
|
84
|
+
|
|
85
|
+
df = _read_table(resolved)
|
|
86
|
+
if _has_columns(df, BUDDY_COLUMNS):
|
|
87
|
+
warnings.warn(
|
|
88
|
+
_NORMALIZED_CSV_DEPRECATION.format(tool="BUDDY"),
|
|
89
|
+
DeprecationWarning,
|
|
90
|
+
stacklevel=2,
|
|
91
|
+
)
|
|
92
|
+
return _normalize_buddy(df)
|
|
93
|
+
if _has_columns(df, _BUDDY_NATIVE_COLUMNS):
|
|
94
|
+
return _normalize_msbuddy_summary(df)
|
|
95
|
+
raise ValueError(
|
|
96
|
+
"Unsupported BUDDY result format. Expected a native/original msbuddy "
|
|
97
|
+
"summary or output directory, or an msfiddle-normalized CSV. Native "
|
|
98
|
+
"summary columns: "
|
|
99
|
+
f"{', '.join(_BUDDY_NATIVE_COLUMNS)}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def load_sirius_results(path: str | Path) -> pd.DataFrame:
|
|
104
|
+
"""Load native/original or msfiddle-normalized SIRIUS results."""
|
|
105
|
+
|
|
106
|
+
resolved = _resolve_path(path, "SIRIUS")
|
|
107
|
+
if resolved.is_dir():
|
|
108
|
+
resolved = _find_sirius_formula_file(resolved)
|
|
109
|
+
|
|
110
|
+
df = _read_table(resolved)
|
|
111
|
+
if _has_columns(df, SIRIUS_COLUMNS):
|
|
112
|
+
warnings.warn(
|
|
113
|
+
_NORMALIZED_CSV_DEPRECATION.format(tool="SIRIUS"),
|
|
114
|
+
DeprecationWarning,
|
|
115
|
+
stacklevel=2,
|
|
116
|
+
)
|
|
117
|
+
return _normalize_sirius(df)
|
|
118
|
+
return _normalize_sirius_native(df)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _resolve_path(path: str | Path, label: str) -> Path:
|
|
122
|
+
resolved = Path(path)
|
|
123
|
+
if not resolved.exists():
|
|
124
|
+
raise FileNotFoundError(f"{label} result path not found: {resolved}")
|
|
125
|
+
return resolved
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _read_table(path: Path) -> pd.DataFrame:
|
|
129
|
+
suffix = path.suffix.lower()
|
|
130
|
+
if suffix in {".xlsx", ".xls"}:
|
|
131
|
+
return _clean_columns(pd.read_excel(path))
|
|
132
|
+
if suffix == ".tsv":
|
|
133
|
+
return _clean_columns(pd.read_csv(path, sep="\t"))
|
|
134
|
+
if suffix == ".csv":
|
|
135
|
+
return _clean_columns(pd.read_csv(path))
|
|
136
|
+
return _clean_columns(pd.read_csv(path, sep=None, engine="python"))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _clean_columns(df: pd.DataFrame) -> pd.DataFrame:
|
|
140
|
+
df = df.copy()
|
|
141
|
+
df.columns = [str(column).strip().lstrip("\ufeff") for column in df.columns]
|
|
142
|
+
return df
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _has_columns(df: pd.DataFrame, columns: Iterable[str]) -> bool:
|
|
146
|
+
df_columns = set(df.columns)
|
|
147
|
+
return all(column in df_columns for column in columns)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _normalize_buddy(df: pd.DataFrame) -> pd.DataFrame:
|
|
151
|
+
normalized = df.loc[:, BUDDY_COLUMNS].copy()
|
|
152
|
+
normalized["ID"] = normalized["ID"].map(_clean_identifier)
|
|
153
|
+
normalized["Adduct"] = normalized["Adduct"].map(_clean_text)
|
|
154
|
+
for column in BUDDY_FORMULA_COLUMNS:
|
|
155
|
+
normalized[column] = normalized[column].map(_clean_formula)
|
|
156
|
+
for column in BUDDY_SCORE_COLUMNS:
|
|
157
|
+
normalized[column] = pd.to_numeric(normalized[column], errors="coerce")
|
|
158
|
+
return normalized.dropna(subset=["ID"]).reset_index(drop=True)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _load_msbuddy_directory(path: Path) -> pd.DataFrame:
|
|
162
|
+
summary_path = path / "msbuddy_result_summary.tsv"
|
|
163
|
+
if not summary_path.exists():
|
|
164
|
+
raise FileNotFoundError(
|
|
165
|
+
"Native msbuddy output directory must contain "
|
|
166
|
+
f"msbuddy_result_summary.tsv: {path}"
|
|
167
|
+
)
|
|
168
|
+
summary = _read_table(summary_path)
|
|
169
|
+
details = _load_msbuddy_details(path, summary)
|
|
170
|
+
return _normalize_msbuddy_summary(summary, details=details)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _normalize_msbuddy_summary(
|
|
174
|
+
df: pd.DataFrame,
|
|
175
|
+
*,
|
|
176
|
+
details: dict[str, list[tuple[str, float]]] | None = None,
|
|
177
|
+
) -> pd.DataFrame:
|
|
178
|
+
if not _has_columns(df, _BUDDY_NATIVE_COLUMNS):
|
|
179
|
+
raise ValueError(
|
|
180
|
+
"Native msbuddy summary is missing required columns: "
|
|
181
|
+
f"{', '.join(_BUDDY_NATIVE_COLUMNS)}"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
rows: list[dict[str, object]] = []
|
|
185
|
+
for _, row in df.iterrows():
|
|
186
|
+
spec_id = _clean_identifier(row["identifier"])
|
|
187
|
+
out: dict[str, object] = {
|
|
188
|
+
"ID": spec_id,
|
|
189
|
+
"Adduct": _clean_text(row.get("adduct", pd.NA)),
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
detail_candidates = details.get(spec_id, []) if details else []
|
|
193
|
+
if detail_candidates:
|
|
194
|
+
for idx, (formula, score) in enumerate(detail_candidates[:5], start=1):
|
|
195
|
+
out[f"Pred Formula ({idx})"] = _clean_formula(formula)
|
|
196
|
+
out[f"BUDDY Score ({idx})"] = score
|
|
197
|
+
else:
|
|
198
|
+
for idx in range(1, 6):
|
|
199
|
+
out[f"Pred Formula ({idx})"] = _clean_formula(
|
|
200
|
+
row.get(f"formula_rank_{idx}", pd.NA)
|
|
201
|
+
)
|
|
202
|
+
# msbuddy summary exposes estimated FDR for rank 1 only.
|
|
203
|
+
out[f"BUDDY Score ({idx})"] = (
|
|
204
|
+
_to_number(row.get("estimated_fdr", pd.NA)) if idx == 1 else pd.NA
|
|
205
|
+
)
|
|
206
|
+
rows.append(out)
|
|
207
|
+
return _normalize_buddy(pd.DataFrame(rows, columns=BUDDY_COLUMNS))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _load_msbuddy_details(
|
|
211
|
+
path: Path, summary: pd.DataFrame
|
|
212
|
+
) -> dict[str, list[tuple[str, float]]]:
|
|
213
|
+
# Detail directories follow `{sanitized_id}_mz_{mz}_rt_{rt}`. Index by the
|
|
214
|
+
# sanitized id so we don't depend on how msbuddy formats mz.
|
|
215
|
+
by_sanitized_id: dict[str, str] = {}
|
|
216
|
+
for _, row in summary.iterrows():
|
|
217
|
+
spec_id = _clean_identifier(row["identifier"])
|
|
218
|
+
if pd.isna(spec_id):
|
|
219
|
+
continue
|
|
220
|
+
by_sanitized_id[_sanitize_msbuddy_identifier(spec_id)] = spec_id
|
|
221
|
+
|
|
222
|
+
details: dict[str, list[tuple[str, float]]] = {}
|
|
223
|
+
for result_path in path.glob("**/formula_results.tsv"):
|
|
224
|
+
folder = result_path.parent.name
|
|
225
|
+
sanitized_id, _, _ = folder.partition("_mz_")
|
|
226
|
+
if not sanitized_id:
|
|
227
|
+
continue
|
|
228
|
+
spec_id = by_sanitized_id.get(sanitized_id)
|
|
229
|
+
if spec_id is None:
|
|
230
|
+
continue
|
|
231
|
+
df = _read_table(result_path)
|
|
232
|
+
if not _has_columns(df, ("rank", "formula", "estimated_fdr")):
|
|
233
|
+
continue
|
|
234
|
+
df = df.copy()
|
|
235
|
+
df["rank"] = pd.to_numeric(df["rank"], errors="coerce")
|
|
236
|
+
df["estimated_fdr"] = pd.to_numeric(df["estimated_fdr"], errors="coerce")
|
|
237
|
+
df = df.sort_values("rank", na_position="last")
|
|
238
|
+
candidates: list[tuple[str, float]] = []
|
|
239
|
+
for _, row in df.iterrows():
|
|
240
|
+
formula = _clean_formula(row.get("formula", pd.NA))
|
|
241
|
+
if pd.isna(formula):
|
|
242
|
+
continue
|
|
243
|
+
candidates.append((str(formula), row.get("estimated_fdr", pd.NA)))
|
|
244
|
+
if len(candidates) == 5:
|
|
245
|
+
break
|
|
246
|
+
if candidates:
|
|
247
|
+
details[spec_id] = candidates
|
|
248
|
+
return details
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _sanitize_msbuddy_identifier(spec_id: str) -> str:
|
|
252
|
+
return spec_id.replace("/", "").replace(":", "").replace(" ", "").strip()
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _find_sirius_formula_file(path: Path) -> Path:
|
|
256
|
+
candidates = [
|
|
257
|
+
file_path
|
|
258
|
+
for file_path in path.glob("**/*")
|
|
259
|
+
if file_path.is_file()
|
|
260
|
+
and file_path.suffix.lower() in {".tsv", ".csv", ".xlsx", ".xls"}
|
|
261
|
+
and file_path.stem.startswith("formula_identifications")
|
|
262
|
+
]
|
|
263
|
+
if not candidates:
|
|
264
|
+
raise FileNotFoundError(
|
|
265
|
+
"SIRIUS output directory must contain a formula_identifications "
|
|
266
|
+
f"summary file: {path}"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def priority(file_path: Path) -> tuple[int, str]:
|
|
270
|
+
stem = file_path.stem
|
|
271
|
+
for idx, prefix in enumerate(_SIRIUS_FORMULA_FILES):
|
|
272
|
+
if stem.startswith(prefix):
|
|
273
|
+
return idx, file_path.name
|
|
274
|
+
return len(_SIRIUS_FORMULA_FILES), file_path.name
|
|
275
|
+
|
|
276
|
+
return sorted(candidates, key=priority)[0]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _normalize_sirius(df: pd.DataFrame) -> pd.DataFrame:
|
|
280
|
+
normalized = df.loc[:, SIRIUS_COLUMNS].copy()
|
|
281
|
+
normalized["ID"] = normalized["ID"].map(_clean_identifier)
|
|
282
|
+
for column in SIRIUS_FORMULA_COLUMNS:
|
|
283
|
+
normalized[column] = normalized[column].map(_clean_formula)
|
|
284
|
+
for column in SIRIUS_ADDUCT_COLUMNS:
|
|
285
|
+
normalized[column] = normalized[column].map(_clean_text)
|
|
286
|
+
for column in SIRIUS_SCORE_COLUMNS:
|
|
287
|
+
normalized[column] = pd.to_numeric(normalized[column], errors="coerce")
|
|
288
|
+
return normalized.dropna(subset=["ID"]).reset_index(drop=True)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _normalize_sirius_native(df: pd.DataFrame) -> pd.DataFrame:
|
|
292
|
+
id_column = _first_existing(df, _SIRIUS_ID_COLUMNS)
|
|
293
|
+
formula_column = _first_existing(df, _SIRIUS_FORMULA_COLUMNS)
|
|
294
|
+
adduct_column = _first_existing(df, _SIRIUS_ADDUCT_COLUMNS)
|
|
295
|
+
score_column = _first_existing(df, _SIRIUS_SCORE_COLUMNS)
|
|
296
|
+
rank_column = _first_existing(df, _SIRIUS_RANK_COLUMNS)
|
|
297
|
+
if not all((id_column, formula_column, adduct_column, score_column)):
|
|
298
|
+
raise ValueError(
|
|
299
|
+
"Unsupported SIRIUS result format. Expected a native/original "
|
|
300
|
+
"formula_identifications summary or output directory, or an "
|
|
301
|
+
"msfiddle-normalized CSV. Native summaries must include "
|
|
302
|
+
"identifier, formula, adduct, and score columns."
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
native = df.copy()
|
|
306
|
+
native["_msfiddle_id"] = native[id_column].map(_clean_identifier)
|
|
307
|
+
native["_msfiddle_formula"] = native[formula_column].map(_clean_formula)
|
|
308
|
+
native["_msfiddle_adduct"] = native[adduct_column].map(_clean_text)
|
|
309
|
+
native["_msfiddle_score"] = pd.to_numeric(native[score_column], errors="coerce")
|
|
310
|
+
if rank_column is not None:
|
|
311
|
+
native["_msfiddle_rank"] = pd.to_numeric(native[rank_column], errors="coerce")
|
|
312
|
+
else:
|
|
313
|
+
native["_msfiddle_rank"] = pd.NA
|
|
314
|
+
native = native.dropna(subset=["_msfiddle_id", "_msfiddle_formula"])
|
|
315
|
+
|
|
316
|
+
rows: list[dict[str, object]] = []
|
|
317
|
+
for spec_id, group in native.groupby("_msfiddle_id", sort=False):
|
|
318
|
+
group = group.sort_values(
|
|
319
|
+
by=["_msfiddle_rank", "_msfiddle_score"],
|
|
320
|
+
ascending=[True, False],
|
|
321
|
+
na_position="last",
|
|
322
|
+
)
|
|
323
|
+
out: dict[str, object] = {"ID": spec_id}
|
|
324
|
+
for idx, (_, row) in enumerate(group.head(5).iterrows(), start=1):
|
|
325
|
+
out[f"Pred Formula ({idx})"] = row["_msfiddle_formula"]
|
|
326
|
+
out[f"Pred Adduct ({idx})"] = row["_msfiddle_adduct"]
|
|
327
|
+
out[f"SIRIUS Score ({idx})"] = row["_msfiddle_score"]
|
|
328
|
+
rows.append(out)
|
|
329
|
+
return _normalize_sirius(pd.DataFrame(rows, columns=SIRIUS_COLUMNS))
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _first_existing(df: pd.DataFrame, candidates: Iterable[str]) -> str | None:
|
|
333
|
+
df_columns = list(df.columns)
|
|
334
|
+
for candidate in candidates:
|
|
335
|
+
if candidate in df_columns:
|
|
336
|
+
return candidate
|
|
337
|
+
canonical_columns = {_canonical_column(column): column for column in df_columns}
|
|
338
|
+
for candidate in candidates:
|
|
339
|
+
match = canonical_columns.get(_canonical_column(candidate))
|
|
340
|
+
if match is not None:
|
|
341
|
+
return match
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _canonical_column(column: object) -> str:
|
|
346
|
+
text = str(column).strip().lstrip("\ufeff")
|
|
347
|
+
if text.lower().startswith("sirius_"):
|
|
348
|
+
text = text[7:]
|
|
349
|
+
return re.sub(r"[^a-z0-9]+", "", text.lower())
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _clean_identifier(value: object) -> object:
|
|
353
|
+
text = _clean_text(value)
|
|
354
|
+
if pd.isna(text):
|
|
355
|
+
return pd.NA
|
|
356
|
+
return str(text)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _clean_formula(value: object) -> object:
|
|
360
|
+
text = _clean_text(value)
|
|
361
|
+
if pd.isna(text):
|
|
362
|
+
return pd.NA
|
|
363
|
+
# SIRIUS summaries may represent adduct-resolved formulas as
|
|
364
|
+
# "[C20H17NO6 + H]+". Keep only the neutral molecular formula.
|
|
365
|
+
match = re.match(r"^\[?([A-Z][A-Za-z0-9]*)", str(text))
|
|
366
|
+
return match.group(1) if match else text
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _clean_text(value: object) -> object:
|
|
370
|
+
if pd.isna(value):
|
|
371
|
+
return pd.NA
|
|
372
|
+
text = str(value).strip()
|
|
373
|
+
if text.lower() in {"", "na", "nan", "none", "null"}:
|
|
374
|
+
return pd.NA
|
|
375
|
+
return text
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _to_number(value: object) -> object:
|
|
379
|
+
number = pd.to_numeric(pd.Series([value]), errors="coerce").iloc[0]
|
|
380
|
+
return number if pd.notna(number) else pd.NA
|
|
@@ -173,10 +173,25 @@ def main():
|
|
|
173
173
|
|
|
174
174
|
# Add optional arguments
|
|
175
175
|
parser.add_argument(
|
|
176
|
-
"--buddy_path",
|
|
176
|
+
"--buddy_path",
|
|
177
|
+
type=str,
|
|
178
|
+
default="",
|
|
179
|
+
help=(
|
|
180
|
+
"Path to BUDDY results: native/original msbuddy output directory "
|
|
181
|
+
"or summary TSV. The msfiddle-normalized CSV is also accepted but "
|
|
182
|
+
"deprecated and will be removed in 3.0.0."
|
|
183
|
+
),
|
|
177
184
|
)
|
|
178
185
|
parser.add_argument(
|
|
179
|
-
"--sirius_path",
|
|
186
|
+
"--sirius_path",
|
|
187
|
+
type=str,
|
|
188
|
+
default="",
|
|
189
|
+
help=(
|
|
190
|
+
"Path to SIRIUS results: native/original formula_identifications "
|
|
191
|
+
"summary or SIRIUS summary output directory. The "
|
|
192
|
+
"msfiddle-normalized CSV is also accepted but deprecated and will "
|
|
193
|
+
"be removed in 3.0.0."
|
|
194
|
+
),
|
|
180
195
|
)
|
|
181
196
|
parser.add_argument(
|
|
182
197
|
"--seed", type=int, default=42, help="Seed for random functions"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: msfiddle
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: A package for predicting chemical formulas from tandem mass spectra
|
|
5
5
|
Home-page: https://github.com/JosieHong/msfiddle
|
|
6
6
|
Author: Yuhui Hong
|
|
@@ -64,17 +64,13 @@ For the full experimental codebase, see https://github.com/JosieHong/FIDDLE.
|
|
|
64
64
|
## Installation
|
|
65
65
|
|
|
66
66
|
```bash
|
|
67
|
-
pip install msfiddle
|
|
67
|
+
pip install msfiddle # base install, no PyTorch
|
|
68
|
+
pip install "msfiddle[inference]" # base install + PyTorch (needed for predictions and the CLI)
|
|
68
69
|
```
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
pip install "msfiddle[inference]"
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
See the official PyTorch installation guide for custom CUDA builds:
|
|
77
|
-
https://pytorch.org/get-started/locally/.
|
|
71
|
+
For a custom PyTorch build (CUDA, ROCm, Apple Silicon, etc.), install torch
|
|
72
|
+
first via the [official guide](https://pytorch.org/get-started/locally/), then
|
|
73
|
+
`pip install msfiddle`.
|
|
78
74
|
|
|
79
75
|
## Usage
|
|
80
76
|
|
|
@@ -90,7 +86,7 @@ msfiddle-download-models --destination /path/to/models \
|
|
|
90
86
|
--models fiddle_tcn_qtof fiddle_rescore_qtof
|
|
91
87
|
```
|
|
92
88
|
|
|
93
|
-
`msfiddle` 2.
|
|
89
|
+
All `msfiddle` 2.x releases reuse the FIDDLE `v2.0.0` checkpoint assets.
|
|
94
90
|
|
|
95
91
|
Run the packaged demo:
|
|
96
92
|
```bash
|
|
@@ -190,6 +186,44 @@ The Python `predict_from_spectrum()` API returns a list of candidate dictionarie
|
|
|
190
186
|
`predict_batch()` returns one record per input spectrum with `id`, `candidates`,
|
|
191
187
|
and `metadata`.
|
|
192
188
|
|
|
189
|
+
### Native/original BUDDY and SIRIUS outputs
|
|
190
|
+
|
|
191
|
+
Use the same MGF input file to generate native
|
|
192
|
+
[BUDDY/msbuddy](https://msbuddy.readthedocs.io/en/latest/cmdapi.html) and
|
|
193
|
+
[SIRIUS](https://v6.docs.sirius-ms.io/cli/) outputs. `msfiddle` accepts these
|
|
194
|
+
native/original files directly through `--buddy_path` and `--sirius_path`. The
|
|
195
|
+
older msfiddle-normalized CSV formats documented in `docs/formats.rst` are
|
|
196
|
+
**deprecated** and will be removed in `msfiddle` 3.0.0; loading them emits a
|
|
197
|
+
`DeprecationWarning`.
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
msbuddy -mgf /path/to/data.mgf \
|
|
201
|
+
-output /path/to/buddy_output \
|
|
202
|
+
-ms orbitrap \
|
|
203
|
+
-p -n_cpu 12 \
|
|
204
|
+
-d -hal
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`msbuddy` writes `msbuddy_result_summary.tsv`; `-d` also writes detailed
|
|
208
|
+
per-query `formula_results.tsv` files. Pass either the summary file or the
|
|
209
|
+
output directory to `--buddy_path`. The full output directory is preferred
|
|
210
|
+
because it includes per-candidate FDR scores for ranks beyond rank 1.
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
sirius --input /path/to/data.mgf \
|
|
214
|
+
--project /path/to/sirius_project \
|
|
215
|
+
formulas --profile orbitrap
|
|
216
|
+
|
|
217
|
+
sirius --project /path/to/sirius_project \
|
|
218
|
+
summaries --top-k-summary=5 \
|
|
219
|
+
--output /path/to/sirius_output
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
SIRIUS writes a project space and exports summary files such as
|
|
223
|
+
`formula_identifications.tsv`. Pass either a formula-identification summary file
|
|
224
|
+
or the summary output directory to `--sirius_path`.
|
|
225
|
+
SIRIUS 6 may require `sirius login` before formula computation.
|
|
226
|
+
|
|
193
227
|
### MGF input
|
|
194
228
|
|
|
195
229
|
The required MGF fields are `TITLE`, `PRECURSOR_MZ`, `PRECURSOR_TYPE`, and
|
|
@@ -5,6 +5,7 @@ msfiddle/__init__.py
|
|
|
5
5
|
msfiddle/api.py
|
|
6
6
|
msfiddle/dataset.py
|
|
7
7
|
msfiddle/download.py
|
|
8
|
+
msfiddle/external_results.py
|
|
8
9
|
msfiddle/main.py
|
|
9
10
|
msfiddle/model_tcn.py
|
|
10
11
|
msfiddle.egg-info/PKG-INFO
|
|
@@ -31,6 +32,7 @@ msfiddle/utils/pkl_utils.py
|
|
|
31
32
|
msfiddle/utils/refine_utils.py
|
|
32
33
|
tests/test_api.py
|
|
33
34
|
tests/test_download.py
|
|
35
|
+
tests/test_external_results.py
|
|
34
36
|
tests/test_main.py
|
|
35
37
|
tests/test_mol_utils.py
|
|
36
38
|
tests/test_msms_utils.py
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name="msfiddle",
|
|
8
|
-
version="2.0
|
|
8
|
+
version="2.1.0",
|
|
9
9
|
author="Yuhui Hong",
|
|
10
10
|
author_email="josieexception@outlook.com",
|
|
11
11
|
description="A package for predicting chemical formulas from tandem mass spectra",
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import pytest
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
2
4
|
|
|
3
5
|
import msfiddle.api as api
|
|
4
|
-
from msfiddle.api import MsFiddlePredictor, _ModelPrediction
|
|
6
|
+
from msfiddle.api import MsFiddlePredictor, _EncodedSpectrum, _ModelPrediction
|
|
5
7
|
|
|
6
8
|
VALID_MZ = [60.0, 85.0, 100.0, 125.0, 150.0]
|
|
7
9
|
VALID_INTENSITY = [10.0, 50.0, 20.0, 35.0, 15.0]
|
|
@@ -160,3 +162,101 @@ def test_predict_batch_preserves_order_ids_and_candidate_shape(monkeypatch, tmp_
|
|
|
160
162
|
assert result[0]["candidates"][0]["mass"] == pytest.approx(122.073)
|
|
161
163
|
assert result[0]["candidates"][0]["metadata"]["raw_rescore"] == 1.2
|
|
162
164
|
assert result[0]["metadata"]["adduct"] == VALID_ADDUCT
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_predict_mgf_loads_native_external_outputs(monkeypatch, tmp_path):
|
|
168
|
+
predictor = MsFiddlePredictor(
|
|
169
|
+
resume_path=tmp_path / "missing_tcn.pt",
|
|
170
|
+
rescore_resume_path=tmp_path / "missing_rescore.pt",
|
|
171
|
+
)
|
|
172
|
+
spectrum = _EncodedSpectrum(
|
|
173
|
+
id="spec-1",
|
|
174
|
+
precursor_type="[M-H]-",
|
|
175
|
+
precursor_mz=100.0,
|
|
176
|
+
collision_energy="50",
|
|
177
|
+
spec=np.zeros(10, dtype=np.float32),
|
|
178
|
+
env=np.zeros(3, dtype=np.float32),
|
|
179
|
+
neutral_add=np.zeros(13, dtype=np.float32),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
buddy_dir = tmp_path / "buddy_output"
|
|
183
|
+
buddy_dir.mkdir()
|
|
184
|
+
pd.DataFrame(
|
|
185
|
+
[
|
|
186
|
+
{
|
|
187
|
+
"identifier": "spec-1",
|
|
188
|
+
"mz": 100.0,
|
|
189
|
+
"rt": "NA",
|
|
190
|
+
"adduct": "[M-H]-",
|
|
191
|
+
"formula_rank_1": "C4H6N4O3",
|
|
192
|
+
"estimated_fdr": 0.2,
|
|
193
|
+
"formula_rank_2": "C5H7NO2",
|
|
194
|
+
"formula_rank_3": "NA",
|
|
195
|
+
"formula_rank_4": "NA",
|
|
196
|
+
"formula_rank_5": "NA",
|
|
197
|
+
}
|
|
198
|
+
]
|
|
199
|
+
).to_csv(buddy_dir / "msbuddy_result_summary.tsv", sep="\t", index=False)
|
|
200
|
+
detail_dir = buddy_dir / "spec-1_mz_100.0_rt_NA"
|
|
201
|
+
detail_dir.mkdir()
|
|
202
|
+
pd.DataFrame(
|
|
203
|
+
[
|
|
204
|
+
{"rank": 1, "formula": "C4H6N4O3", "estimated_fdr": 0.2},
|
|
205
|
+
{"rank": 2, "formula": "C5H7NO2", "estimated_fdr": 0.4},
|
|
206
|
+
]
|
|
207
|
+
).to_csv(detail_dir / "formula_results.tsv", sep="\t", index=False)
|
|
208
|
+
|
|
209
|
+
sirius_path = tmp_path / "formula_identifications_top-5.tsv"
|
|
210
|
+
pd.DataFrame(
|
|
211
|
+
[
|
|
212
|
+
{
|
|
213
|
+
"name": "spec-1",
|
|
214
|
+
"rank": 1,
|
|
215
|
+
"molecularFormula": "C6H12O6",
|
|
216
|
+
"adduct": "[M - H]-",
|
|
217
|
+
"SiriusScore": 1.0,
|
|
218
|
+
}
|
|
219
|
+
]
|
|
220
|
+
).to_csv(sirius_path, sep="\t", index=False)
|
|
221
|
+
|
|
222
|
+
seen_extra_formulas = []
|
|
223
|
+
|
|
224
|
+
def fake_load_mgf_spectra(self, test_data_path):
|
|
225
|
+
return [spectrum]
|
|
226
|
+
|
|
227
|
+
def fake_predict_encoded(self, encoded, *, batch_size):
|
|
228
|
+
return [
|
|
229
|
+
_ModelPrediction(
|
|
230
|
+
spectrum=encoded[0],
|
|
231
|
+
vector_string="1;2;3",
|
|
232
|
+
pred_formula="C3H7NO2",
|
|
233
|
+
pred_mass=89.0,
|
|
234
|
+
pred_atom_count=12.0,
|
|
235
|
+
pred_hc_count=1.5,
|
|
236
|
+
prediction_time=0.01,
|
|
237
|
+
)
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
def fake_refine_and_rescore(self, prediction, top_k, *, extra_formulas=None):
|
|
241
|
+
seen_extra_formulas.extend(extra_formulas or [])
|
|
242
|
+
return (
|
|
243
|
+
{"formula": ["C4H6N4O3"], "mass": [146.0], "rescore": [0.9]},
|
|
244
|
+
146.0,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
monkeypatch.setattr(MsFiddlePredictor, "_load_mgf_spectra", fake_load_mgf_spectra)
|
|
248
|
+
monkeypatch.setattr(MsFiddlePredictor, "_predict_encoded", fake_predict_encoded)
|
|
249
|
+
monkeypatch.setattr(
|
|
250
|
+
MsFiddlePredictor, "_refine_and_rescore", fake_refine_and_rescore
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
result = predictor.predict_mgf(
|
|
254
|
+
tmp_path / "unused.mgf",
|
|
255
|
+
buddy_path=buddy_dir,
|
|
256
|
+
sirius_path=sirius_path,
|
|
257
|
+
top_k=1,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
assert seen_extra_formulas == ["C4H6N4O3", "C5H7NO2", "C6H12O6"]
|
|
261
|
+
assert result.loc[0, "ID"] == "spec-1"
|
|
262
|
+
assert result.loc[0, "Refined Formula (0)"] == "C4H6N4O3"
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from msfiddle.external_results import load_buddy_results, load_sirius_results
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_load_buddy_results_preserves_normalized_csv(tmp_path):
|
|
8
|
+
path = tmp_path / "buddy.csv"
|
|
9
|
+
pd.DataFrame(
|
|
10
|
+
[
|
|
11
|
+
{
|
|
12
|
+
"ID": "spec-1",
|
|
13
|
+
"Adduct": "[M-H]-",
|
|
14
|
+
"Pred Formula (1)": "C4H6N4O3",
|
|
15
|
+
"Pred Formula (2)": "NA",
|
|
16
|
+
"Pred Formula (3)": "NA",
|
|
17
|
+
"Pred Formula (4)": "NA",
|
|
18
|
+
"Pred Formula (5)": "NA",
|
|
19
|
+
"BUDDY Score (1)": "0.1",
|
|
20
|
+
"BUDDY Score (2)": "NA",
|
|
21
|
+
"BUDDY Score (3)": "NA",
|
|
22
|
+
"BUDDY Score (4)": "NA",
|
|
23
|
+
"BUDDY Score (5)": "NA",
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
).to_csv(path, index=False)
|
|
27
|
+
|
|
28
|
+
with pytest.warns(DeprecationWarning, match="msfiddle-normalized BUDDY"):
|
|
29
|
+
result = load_buddy_results(path)
|
|
30
|
+
|
|
31
|
+
assert result.loc[0, "ID"] == "spec-1"
|
|
32
|
+
assert result.loc[0, "Pred Formula (1)"] == "C4H6N4O3"
|
|
33
|
+
assert result.loc[0, "BUDDY Score (1)"] == pytest.approx(0.1)
|
|
34
|
+
assert pd.isna(result.loc[0, "Pred Formula (2)"])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_load_buddy_results_converts_msbuddy_summary(tmp_path):
|
|
38
|
+
path = tmp_path / "msbuddy_result_summary.tsv"
|
|
39
|
+
pd.DataFrame(
|
|
40
|
+
[
|
|
41
|
+
{
|
|
42
|
+
"identifier": "spec-1",
|
|
43
|
+
"mz": 100.0,
|
|
44
|
+
"rt": "NA",
|
|
45
|
+
"adduct": "[M-H]-",
|
|
46
|
+
"formula_rank_1": "C4H6N4O3",
|
|
47
|
+
"estimated_fdr": 0.2,
|
|
48
|
+
"formula_rank_2": "C5H7NO2",
|
|
49
|
+
"formula_rank_3": "NA",
|
|
50
|
+
"formula_rank_4": "NA",
|
|
51
|
+
"formula_rank_5": "NA",
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
).to_csv(path, sep="\t", index=False)
|
|
55
|
+
|
|
56
|
+
result = load_buddy_results(path)
|
|
57
|
+
|
|
58
|
+
assert result.loc[0, "ID"] == "spec-1"
|
|
59
|
+
assert result.loc[0, "Adduct"] == "[M-H]-"
|
|
60
|
+
assert result.loc[0, "Pred Formula (1)"] == "C4H6N4O3"
|
|
61
|
+
assert result.loc[0, "BUDDY Score (1)"] == pytest.approx(0.2)
|
|
62
|
+
assert result.loc[0, "Pred Formula (2)"] == "C5H7NO2"
|
|
63
|
+
assert pd.isna(result.loc[0, "BUDDY Score (2)"])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_load_buddy_results_uses_msbuddy_detail_scores_from_directory(tmp_path):
|
|
67
|
+
summary = tmp_path / "msbuddy_result_summary.tsv"
|
|
68
|
+
pd.DataFrame(
|
|
69
|
+
[
|
|
70
|
+
{
|
|
71
|
+
"identifier": "spec-1",
|
|
72
|
+
"mz": 100.0,
|
|
73
|
+
"rt": "NA",
|
|
74
|
+
"adduct": "[M-H]-",
|
|
75
|
+
"formula_rank_1": "C4H6N4O3",
|
|
76
|
+
"estimated_fdr": 0.2,
|
|
77
|
+
"formula_rank_2": "C5H7NO2",
|
|
78
|
+
"formula_rank_3": "NA",
|
|
79
|
+
"formula_rank_4": "NA",
|
|
80
|
+
"formula_rank_5": "NA",
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
).to_csv(summary, sep="\t", index=False)
|
|
84
|
+
detail_dir = tmp_path / "spec-1_mz_100.0_rt_NA"
|
|
85
|
+
detail_dir.mkdir()
|
|
86
|
+
pd.DataFrame(
|
|
87
|
+
[
|
|
88
|
+
{"rank": 2, "formula": "C5H7NO2", "estimated_fdr": 0.4},
|
|
89
|
+
{"rank": 1, "formula": "C4H6N4O3", "estimated_fdr": 0.2},
|
|
90
|
+
]
|
|
91
|
+
).to_csv(detail_dir / "formula_results.tsv", sep="\t", index=False)
|
|
92
|
+
|
|
93
|
+
result = load_buddy_results(tmp_path)
|
|
94
|
+
|
|
95
|
+
assert result.loc[0, "Pred Formula (1)"] == "C4H6N4O3"
|
|
96
|
+
assert result.loc[0, "BUDDY Score (1)"] == pytest.approx(0.2)
|
|
97
|
+
assert result.loc[0, "Pred Formula (2)"] == "C5H7NO2"
|
|
98
|
+
assert result.loc[0, "BUDDY Score (2)"] == pytest.approx(0.4)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_load_buddy_results_matches_detail_dir_when_summary_mz_format_differs(tmp_path):
|
|
102
|
+
# Real msbuddy rounds mz in the summary, but a future version (or a
|
|
103
|
+
# hand-edited summary) could disagree with the directory name. The
|
|
104
|
+
# directory's identifier prefix alone should be enough to match.
|
|
105
|
+
summary = tmp_path / "msbuddy_result_summary.tsv"
|
|
106
|
+
pd.DataFrame(
|
|
107
|
+
[
|
|
108
|
+
{
|
|
109
|
+
"identifier": "spec-1",
|
|
110
|
+
"mz": 111.020160000, # padded zeros vs. dir name's "111.0202"
|
|
111
|
+
"rt": "NA",
|
|
112
|
+
"adduct": "[M-H]-",
|
|
113
|
+
"formula_rank_1": "C4H4N2O2",
|
|
114
|
+
"estimated_fdr": 0.0,
|
|
115
|
+
"formula_rank_2": "NA",
|
|
116
|
+
"formula_rank_3": "NA",
|
|
117
|
+
"formula_rank_4": "NA",
|
|
118
|
+
"formula_rank_5": "NA",
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
).to_csv(summary, sep="\t", index=False)
|
|
122
|
+
detail_dir = tmp_path / "spec-1_mz_111.0202_rt_NA"
|
|
123
|
+
detail_dir.mkdir()
|
|
124
|
+
pd.DataFrame(
|
|
125
|
+
[
|
|
126
|
+
{"rank": 1, "formula": "C4H4N2O2", "estimated_fdr": 0.0},
|
|
127
|
+
{"rank": 2, "formula": "C5H8O2", "estimated_fdr": 0.5},
|
|
128
|
+
]
|
|
129
|
+
).to_csv(detail_dir / "formula_results.tsv", sep="\t", index=False)
|
|
130
|
+
|
|
131
|
+
result = load_buddy_results(tmp_path)
|
|
132
|
+
|
|
133
|
+
assert result.loc[0, "Pred Formula (1)"] == "C4H4N2O2"
|
|
134
|
+
assert result.loc[0, "Pred Formula (2)"] == "C5H8O2"
|
|
135
|
+
assert result.loc[0, "BUDDY Score (2)"] == pytest.approx(0.5)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_load_buddy_results_drops_rows_with_missing_identifier(tmp_path):
|
|
139
|
+
path = tmp_path / "msbuddy_result_summary.tsv"
|
|
140
|
+
pd.DataFrame(
|
|
141
|
+
[
|
|
142
|
+
{
|
|
143
|
+
"identifier": "NA",
|
|
144
|
+
"mz": 100.0,
|
|
145
|
+
"rt": "NA",
|
|
146
|
+
"adduct": "[M-H]-",
|
|
147
|
+
"formula_rank_1": "C4H6N4O3",
|
|
148
|
+
"estimated_fdr": 0.2,
|
|
149
|
+
"formula_rank_2": "NA",
|
|
150
|
+
"formula_rank_3": "NA",
|
|
151
|
+
"formula_rank_4": "NA",
|
|
152
|
+
"formula_rank_5": "NA",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"identifier": "spec-1",
|
|
156
|
+
"mz": 100.0,
|
|
157
|
+
"rt": "NA",
|
|
158
|
+
"adduct": "[M-H]-",
|
|
159
|
+
"formula_rank_1": "C5H7NO2",
|
|
160
|
+
"estimated_fdr": 0.3,
|
|
161
|
+
"formula_rank_2": "NA",
|
|
162
|
+
"formula_rank_3": "NA",
|
|
163
|
+
"formula_rank_4": "NA",
|
|
164
|
+
"formula_rank_5": "NA",
|
|
165
|
+
},
|
|
166
|
+
]
|
|
167
|
+
).to_csv(path, sep="\t", index=False)
|
|
168
|
+
|
|
169
|
+
result = load_buddy_results(path)
|
|
170
|
+
|
|
171
|
+
assert len(result) == 1
|
|
172
|
+
assert result.loc[0, "ID"] == "spec-1"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_load_sirius_results_preserves_normalized_csv(tmp_path):
|
|
176
|
+
path = tmp_path / "sirius.csv"
|
|
177
|
+
pd.DataFrame(
|
|
178
|
+
[
|
|
179
|
+
{
|
|
180
|
+
"ID": "spec-1",
|
|
181
|
+
"Pred Formula (1)": "C3H7NO2",
|
|
182
|
+
"Pred Adduct (1)": "[M - H]-",
|
|
183
|
+
"SIRIUS Score (1)": "12.5",
|
|
184
|
+
"Pred Formula (2)": "NA",
|
|
185
|
+
"Pred Adduct (2)": "NA",
|
|
186
|
+
"SIRIUS Score (2)": "NA",
|
|
187
|
+
"Pred Formula (3)": "NA",
|
|
188
|
+
"Pred Adduct (3)": "NA",
|
|
189
|
+
"SIRIUS Score (3)": "NA",
|
|
190
|
+
"Pred Formula (4)": "NA",
|
|
191
|
+
"Pred Adduct (4)": "NA",
|
|
192
|
+
"SIRIUS Score (4)": "NA",
|
|
193
|
+
"Pred Formula (5)": "NA",
|
|
194
|
+
"Pred Adduct (5)": "NA",
|
|
195
|
+
"SIRIUS Score (5)": "NA",
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
).to_csv(path, index=False)
|
|
199
|
+
|
|
200
|
+
with pytest.warns(DeprecationWarning, match="msfiddle-normalized SIRIUS"):
|
|
201
|
+
result = load_sirius_results(path)
|
|
202
|
+
|
|
203
|
+
assert result.loc[0, "ID"] == "spec-1"
|
|
204
|
+
assert result.loc[0, "Pred Formula (1)"] == "C3H7NO2"
|
|
205
|
+
assert result.loc[0, "SIRIUS Score (1)"] == pytest.approx(12.5)
|
|
206
|
+
assert pd.isna(result.loc[0, "Pred Formula (2)"])
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_load_sirius_results_converts_native_top_k_summary(tmp_path):
|
|
210
|
+
path = tmp_path / "formula_identifications_top-5.tsv"
|
|
211
|
+
pd.DataFrame(
|
|
212
|
+
[
|
|
213
|
+
{
|
|
214
|
+
"name": "spec-1",
|
|
215
|
+
"rank": 2,
|
|
216
|
+
"molecularFormula": "C5H7NO2",
|
|
217
|
+
"adduct": "[M - H]-",
|
|
218
|
+
"SiriusScore": 5.0,
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"name": "spec-1",
|
|
222
|
+
"rank": 1,
|
|
223
|
+
"molecularFormula": "[C4H6N4O3 - H]-",
|
|
224
|
+
"adduct": "[M - H]-",
|
|
225
|
+
"SiriusScore": 7.0,
|
|
226
|
+
},
|
|
227
|
+
]
|
|
228
|
+
).to_csv(path, sep="\t", index=False)
|
|
229
|
+
|
|
230
|
+
result = load_sirius_results(path)
|
|
231
|
+
|
|
232
|
+
assert result.loc[0, "ID"] == "spec-1"
|
|
233
|
+
assert result.loc[0, "Pred Formula (1)"] == "C4H6N4O3"
|
|
234
|
+
assert result.loc[0, "Pred Adduct (1)"] == "[M - H]-"
|
|
235
|
+
assert result.loc[0, "SIRIUS Score (1)"] == pytest.approx(7.0)
|
|
236
|
+
assert result.loc[0, "Pred Formula (2)"] == "C5H7NO2"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_load_sirius_results_finds_formula_summary_in_directory(tmp_path):
|
|
240
|
+
pd.DataFrame(
|
|
241
|
+
[
|
|
242
|
+
{
|
|
243
|
+
"mappingFeatureId": "spec-1",
|
|
244
|
+
"molecularFormula": "C4H6N4O3",
|
|
245
|
+
"adduct": "[M - H]-",
|
|
246
|
+
"rankingScore": 0.8,
|
|
247
|
+
}
|
|
248
|
+
]
|
|
249
|
+
).to_csv(tmp_path / "formula_identifications.tsv", sep="\t", index=False)
|
|
250
|
+
|
|
251
|
+
result = load_sirius_results(tmp_path)
|
|
252
|
+
|
|
253
|
+
assert result.loc[0, "ID"] == "spec-1"
|
|
254
|
+
assert result.loc[0, "Pred Formula (1)"] == "C4H6N4O3"
|
|
255
|
+
assert result.loc[0, "SIRIUS Score (1)"] == pytest.approx(0.8)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def test_load_sirius_results_accepts_prefixed_column_variants(tmp_path):
|
|
259
|
+
path = tmp_path / "formula_identifications_top-5.tsv"
|
|
260
|
+
pd.DataFrame(
|
|
261
|
+
[
|
|
262
|
+
{
|
|
263
|
+
"SIRIUS_mappingFeatureId": "spec-1",
|
|
264
|
+
"SIRIUS_formula": "C5H7NO2",
|
|
265
|
+
"SIRIUS_ionType": "[M - H]-",
|
|
266
|
+
"SIRIUS_rankingScore": 0.4,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
"SIRIUS_mappingFeatureId": "spec-1",
|
|
270
|
+
"SIRIUS_formula": "C4H6N4O3",
|
|
271
|
+
"SIRIUS_ionType": "[M - H]-",
|
|
272
|
+
"SIRIUS_rankingScore": 0.9,
|
|
273
|
+
},
|
|
274
|
+
]
|
|
275
|
+
).to_csv(path, sep="\t", index=False)
|
|
276
|
+
|
|
277
|
+
result = load_sirius_results(path)
|
|
278
|
+
|
|
279
|
+
assert result.loc[0, "ID"] == "spec-1"
|
|
280
|
+
assert result.loc[0, "Pred Formula (1)"] == "C4H6N4O3"
|
|
281
|
+
assert result.loc[0, "SIRIUS Score (1)"] == pytest.approx(0.9)
|
|
282
|
+
assert result.loc[0, "Pred Formula (2)"] == "C5H7NO2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|