msfiddle 2.1.0__tar.gz → 2.2.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.2.0/PKG-INFO +169 -0
- msfiddle-2.2.0/README.md +128 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/config/fiddle_tcn_orbitrap.yml +12 -7
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/config/fiddle_tcn_qtof.yml +14 -7
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/dataset.py +32 -6
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/download.py +17 -6
- msfiddle-2.2.0/msfiddle/optim.py +160 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/utils/msms_utils.py +134 -103
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/utils/pkl_utils.py +140 -36
- msfiddle-2.2.0/msfiddle/utils/refine_utils.py +712 -0
- msfiddle-2.2.0/msfiddle.egg-info/PKG-INFO +169 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle.egg-info/SOURCES.txt +6 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/setup.py +3 -3
- msfiddle-2.2.0/tests/test_download.py +29 -0
- msfiddle-2.2.0/tests/test_generate_ms.py +52 -0
- msfiddle-2.2.0/tests/test_muon.py +94 -0
- msfiddle-2.2.0/tests/test_preprocessing_speedups.py +148 -0
- msfiddle-2.2.0/tests/test_refine_enumeration.py +136 -0
- msfiddle-2.2.0/tests/test_refine_fast_mass.py +106 -0
- msfiddle-2.1.0/PKG-INFO +0 -285
- msfiddle-2.1.0/README.md +0 -244
- msfiddle-2.1.0/msfiddle/utils/refine_utils.py +0 -376
- msfiddle-2.1.0/msfiddle.egg-info/PKG-INFO +0 -285
- msfiddle-2.1.0/tests/test_download.py +0 -15
- {msfiddle-2.1.0 → msfiddle-2.2.0}/LICENSE +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/__init__.py +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/api.py +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/config/fiddle_tcn_casmi.yml +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/config/fiddle_tcn_demo.yml +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/config/fiddle_tcn_demo_wo_da.yml +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/config/fiddle_tcn_embl.yml +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/demo/input_msms.mgf +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/demo/output_buddy.csv +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/demo/output_fiddle.csv +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/demo/output_fiddle_all.csv +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/demo/output_sirius.csv +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/external_results.py +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/main.py +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/model_tcn.py +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/utils/__init__.py +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/utils/mol_utils.py +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle.egg-info/dependency_links.txt +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle.egg-info/entry_points.txt +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle.egg-info/requires.txt +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle.egg-info/top_level.txt +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/setup.cfg +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/tests/test_api.py +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/tests/test_external_results.py +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/tests/test_main.py +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/tests/test_mol_utils.py +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/tests/test_msms_utils.py +0 -0
- {msfiddle-2.1.0 → msfiddle-2.2.0}/tests/test_refine_utils.py +0 -0
msfiddle-2.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: msfiddle
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: A package for predicting chemical formulas from tandem mass spectra
|
|
5
|
+
Home-page: https://github.com/JosieHong/FIDDLE
|
|
6
|
+
Author: Yuhui Hong
|
|
7
|
+
Author-email: josieexception@outlook.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: numpy<2.0.0,>=1.20.0
|
|
19
|
+
Requires-Dist: pandas>=2.0.0
|
|
20
|
+
Requires-Dist: tqdm>=4.60.0
|
|
21
|
+
Requires-Dist: pyyaml>=6.0
|
|
22
|
+
Requires-Dist: scikit-learn>=1.0.0
|
|
23
|
+
Requires-Dist: scipy>=1.8.0
|
|
24
|
+
Requires-Dist: pyarrow>=10.0.0
|
|
25
|
+
Requires-Dist: rdkit>=2022.03.5
|
|
26
|
+
Requires-Dist: molmass
|
|
27
|
+
Requires-Dist: pyteomics
|
|
28
|
+
Provides-Extra: inference
|
|
29
|
+
Requires-Dist: torch; extra == "inference"
|
|
30
|
+
Dynamic: author
|
|
31
|
+
Dynamic: author-email
|
|
32
|
+
Dynamic: classifier
|
|
33
|
+
Dynamic: description
|
|
34
|
+
Dynamic: description-content-type
|
|
35
|
+
Dynamic: home-page
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
Dynamic: provides-extra
|
|
38
|
+
Dynamic: requires-dist
|
|
39
|
+
Dynamic: requires-python
|
|
40
|
+
Dynamic: summary
|
|
41
|
+
|
|
42
|
+
# FIDDLE
|
|
43
|
+
|
|
44
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
45
|
+
[](https://doi.org/10.5281/zenodo.19181279)
|
|
46
|
+
[](https://github.com/JosieHong/FIDDLE/releases)
|
|
47
|
+
|
|
48
|
+
[<img src="https://raw.githubusercontent.com/JosieHong/FIDDLE/main/img/fiddle_logo.png" align="right" width="220">](https://github.com/JosieHong/FIDDLE)
|
|
49
|
+
|
|
50
|
+
FIDDLE is a deep learning method for predicting molecular formulas from MS/MS spectra. This repository contains the full research codebase for model training, evaluation, and paper reproduction, plus the [`msfiddle`](https://pypi.org/project/msfiddle/) PyPI package (in [`./msfiddle/`](https://github.com/JosieHong/FIDDLE/tree/main/msfiddle)) for inference-only use.
|
|
51
|
+
|
|
52
|
+
- **Paper:** [Nature Communications (2025)](https://www.nature.com/articles/s41467-025-66060-9)
|
|
53
|
+
- **CLI and Python API:** `pip install "msfiddle[inference]"` — see the [package docs](https://msfiddle.readthedocs.io)
|
|
54
|
+
- **Try this demo!** [FIDDLE on Hugging Face](https://huggingface.co/spaces/J0siee/FIDDLE)
|
|
55
|
+
|
|
56
|
+
<br clear="right"/>
|
|
57
|
+
|
|
58
|
+
> **Breaking change (v2.0.0):** The rescore model has been redesigned (Siamese architecture), see details in [CHANGELOG.md](https://github.com/JosieHong/FIDDLE/blob/main/CHANGELOG.md).
|
|
59
|
+
|
|
60
|
+
## Set up
|
|
61
|
+
|
|
62
|
+
### Requirements
|
|
63
|
+
|
|
64
|
+
1. Install [Anaconda](https://docs.anaconda.com/anaconda/install/), if not already installed.
|
|
65
|
+
|
|
66
|
+
2. Create the environment with the necessary packages:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
conda env create -f environment.yml
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
On Apple Silicon (macOS ARM64), use `environment_mac.yml` instead.
|
|
73
|
+
|
|
74
|
+
3. (optional) Install [BUDDY](https://github.com/Philipbear/msbuddy) and [SIRIUS](https://v6.docs.sirius-ms.io/) following the respective installation instructions provided in each tool's documentation.
|
|
75
|
+
|
|
76
|
+
### Pre-trained Model Weights
|
|
77
|
+
|
|
78
|
+
To use the pre-trained models, please use the following scripts to download the weights from the [release page](https://github.com/JosieHong/FIDDLE/releases/tag/v2.2.0) and place them in the `./check_point/` directory:
|
|
79
|
+
|
|
80
|
+
- **Orbitrap models**:
|
|
81
|
+
- `fiddle_tcn_orbitrap.pt`: formula prediction model on Orbitrap spectra
|
|
82
|
+
- `fiddle_rescore_orbitrap.pt`: rescore model on Orbitrap spectra
|
|
83
|
+
- **Q-TOF models**:
|
|
84
|
+
- `fiddle_tcn_qtof.pt`: formula prediction model on Q-TOF spectra
|
|
85
|
+
- `fiddle_rescore_qtof.pt`: rescore model on Q-TOF spectra
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
bash ./running_scripts/download_models.sh
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Usage
|
|
92
|
+
|
|
93
|
+
The input format is `mgf`, where `title`, `precursor_mz`, `precursor_type`, `collision_energy` fields are required. Here, we sampled 21 spectra from the EMBL-MCF 2.0 dataset as an example.
|
|
94
|
+
|
|
95
|
+
```mgf
|
|
96
|
+
BEGIN IONS
|
|
97
|
+
TITLE=EMBL_MCF_2_0_HRMS_Library000531
|
|
98
|
+
PEPMASS=129.01941
|
|
99
|
+
CHARGE=1-
|
|
100
|
+
PRECURSOR_TYPE=[M-H]-
|
|
101
|
+
PRECURSOR_MZ=129.01941
|
|
102
|
+
COLLISION_ENERGY=50.0
|
|
103
|
+
SMILES=[H]OC(=O)C([H])=C(C(=O)O[H])C([H])([H])[H]
|
|
104
|
+
FORMULA=C5H6O4
|
|
105
|
+
THEORETICAL_PRECURSOR_MZ=129.018785
|
|
106
|
+
PPM=4.844255818912111
|
|
107
|
+
SIMULATED_PRECURSOR_MZ=129.02032113281717
|
|
108
|
+
41.2041 0.410228
|
|
109
|
+
55.7698 0.503672
|
|
110
|
+
56.8647 0.461943
|
|
111
|
+
85.0296 100.0
|
|
112
|
+
129.0196 8.036902
|
|
113
|
+
END IONS
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Run FIDDLE!**
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
python scripts/run_fiddle.py --test_data ./msfiddle/demo/input_msms.mgf \
|
|
120
|
+
--config_path ./msfiddle/config/fiddle_tcn_orbitrap.yml \
|
|
121
|
+
--resume_path ./check_point/fiddle_tcn_orbitrap.pt \
|
|
122
|
+
--rescore_resume_path ./check_point/fiddle_rescore_orbitrap.pt \
|
|
123
|
+
--result_path ./msfiddle/demo/output_fiddle.csv --device 0
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
If you'd like to integrate the results from SIRIUS and BUDDY, please organize the results in the format shown in `./msfiddle/demo/buddy_output.csv` and `./msfiddle/demo/sirius_output.csv`, and provide them to run FIDDLE:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
python scripts/run_fiddle.py --test_data ./msfiddle/demo/input_msms.mgf \
|
|
130
|
+
--config_path ./msfiddle/config/fiddle_tcn_orbitrap.yml \
|
|
131
|
+
--resume_path ./check_point/fiddle_tcn_orbitrap.pt \
|
|
132
|
+
--rescore_resume_path ./check_point/fiddle_rescore_orbitrap.pt \
|
|
133
|
+
--buddy_path ./msfiddle/demo/output_buddy.csv \
|
|
134
|
+
--sirius_path ./msfiddle/demo/output_sirius.csv \
|
|
135
|
+
--result_path ./msfiddle/demo/output_fiddle_all.csv --device 0
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
See [`scripts/test_caffeine.py`](https://github.com/JosieHong/FIDDLE/blob/main/scripts/test_caffeine.py) for a worked example running FIDDLE on a caffeine Orbitrap spectrum fetched live from GNPS.
|
|
139
|
+
|
|
140
|
+
## Reproduce paper results
|
|
141
|
+
|
|
142
|
+
All scripts should be run from the repository root (`FIDDLE/`).
|
|
143
|
+
|
|
144
|
+
| Script | Description |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `running_scripts/experiments_test_benchmark.sh` | Evaluate on external benchmarks (CASMI 2016, CASMI 2017, EMBL-MCF 2.0) |
|
|
147
|
+
| `running_scripts/experiments_test_nist23.sh` | Evaluate on NIST23 |
|
|
148
|
+
| `running_scripts/experiments_test_chimeric.sh` | Evaluate on chimeric spectra |
|
|
149
|
+
| `running_scripts/experiments_test_noised.sh` | Evaluate under noise conditions |
|
|
150
|
+
| `running_scripts/experiments_ablation_study.sh` | Run ablation study |
|
|
151
|
+
| `running_scripts/experiments_demo.sh` | Run demo experiment |
|
|
152
|
+
| `running_scripts/train_released_models.sh` | Train TCN and rescore models for both Orbitrap and Q-TOF |
|
|
153
|
+
|
|
154
|
+
For training from scratch, see the train scripts (`scripts/train_tcn_gpus.py`, `scripts/train_tcn_gpus_cl.py`, `scripts/train_rescore.py`) and the corresponding config files in `./msfiddle/config/`.
|
|
155
|
+
|
|
156
|
+
## Citation
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
@article{hong2025fiddle,
|
|
160
|
+
title={FIDDLE: a deep learning method for chemical formulas prediction from tandem mass spectra},
|
|
161
|
+
author={Hong, Yuhui and Li, Sujun and Ye, Yuzhen and Tang, Haixu},
|
|
162
|
+
journal={Nature Communications},
|
|
163
|
+
volume={16},
|
|
164
|
+
number={1},
|
|
165
|
+
pages={11102},
|
|
166
|
+
year={2025},
|
|
167
|
+
publisher={Nature Publishing Group UK London}
|
|
168
|
+
}
|
|
169
|
+
```
|
msfiddle-2.2.0/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# FIDDLE
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
4
|
+
[](https://doi.org/10.5281/zenodo.19181279)
|
|
5
|
+
[](https://github.com/JosieHong/FIDDLE/releases)
|
|
6
|
+
|
|
7
|
+
[<img src="https://raw.githubusercontent.com/JosieHong/FIDDLE/main/img/fiddle_logo.png" align="right" width="220">](https://github.com/JosieHong/FIDDLE)
|
|
8
|
+
|
|
9
|
+
FIDDLE is a deep learning method for predicting molecular formulas from MS/MS spectra. This repository contains the full research codebase for model training, evaluation, and paper reproduction, plus the [`msfiddle`](https://pypi.org/project/msfiddle/) PyPI package (in [`./msfiddle/`](https://github.com/JosieHong/FIDDLE/tree/main/msfiddle)) for inference-only use.
|
|
10
|
+
|
|
11
|
+
- **Paper:** [Nature Communications (2025)](https://www.nature.com/articles/s41467-025-66060-9)
|
|
12
|
+
- **CLI and Python API:** `pip install "msfiddle[inference]"` — see the [package docs](https://msfiddle.readthedocs.io)
|
|
13
|
+
- **Try this demo!** [FIDDLE on Hugging Face](https://huggingface.co/spaces/J0siee/FIDDLE)
|
|
14
|
+
|
|
15
|
+
<br clear="right"/>
|
|
16
|
+
|
|
17
|
+
> **Breaking change (v2.0.0):** The rescore model has been redesigned (Siamese architecture), see details in [CHANGELOG.md](https://github.com/JosieHong/FIDDLE/blob/main/CHANGELOG.md).
|
|
18
|
+
|
|
19
|
+
## Set up
|
|
20
|
+
|
|
21
|
+
### Requirements
|
|
22
|
+
|
|
23
|
+
1. Install [Anaconda](https://docs.anaconda.com/anaconda/install/), if not already installed.
|
|
24
|
+
|
|
25
|
+
2. Create the environment with the necessary packages:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
conda env create -f environment.yml
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
On Apple Silicon (macOS ARM64), use `environment_mac.yml` instead.
|
|
32
|
+
|
|
33
|
+
3. (optional) Install [BUDDY](https://github.com/Philipbear/msbuddy) and [SIRIUS](https://v6.docs.sirius-ms.io/) following the respective installation instructions provided in each tool's documentation.
|
|
34
|
+
|
|
35
|
+
### Pre-trained Model Weights
|
|
36
|
+
|
|
37
|
+
To use the pre-trained models, please use the following scripts to download the weights from the [release page](https://github.com/JosieHong/FIDDLE/releases/tag/v2.2.0) and place them in the `./check_point/` directory:
|
|
38
|
+
|
|
39
|
+
- **Orbitrap models**:
|
|
40
|
+
- `fiddle_tcn_orbitrap.pt`: formula prediction model on Orbitrap spectra
|
|
41
|
+
- `fiddle_rescore_orbitrap.pt`: rescore model on Orbitrap spectra
|
|
42
|
+
- **Q-TOF models**:
|
|
43
|
+
- `fiddle_tcn_qtof.pt`: formula prediction model on Q-TOF spectra
|
|
44
|
+
- `fiddle_rescore_qtof.pt`: rescore model on Q-TOF spectra
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
bash ./running_scripts/download_models.sh
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
The input format is `mgf`, where `title`, `precursor_mz`, `precursor_type`, `collision_energy` fields are required. Here, we sampled 21 spectra from the EMBL-MCF 2.0 dataset as an example.
|
|
53
|
+
|
|
54
|
+
```mgf
|
|
55
|
+
BEGIN IONS
|
|
56
|
+
TITLE=EMBL_MCF_2_0_HRMS_Library000531
|
|
57
|
+
PEPMASS=129.01941
|
|
58
|
+
CHARGE=1-
|
|
59
|
+
PRECURSOR_TYPE=[M-H]-
|
|
60
|
+
PRECURSOR_MZ=129.01941
|
|
61
|
+
COLLISION_ENERGY=50.0
|
|
62
|
+
SMILES=[H]OC(=O)C([H])=C(C(=O)O[H])C([H])([H])[H]
|
|
63
|
+
FORMULA=C5H6O4
|
|
64
|
+
THEORETICAL_PRECURSOR_MZ=129.018785
|
|
65
|
+
PPM=4.844255818912111
|
|
66
|
+
SIMULATED_PRECURSOR_MZ=129.02032113281717
|
|
67
|
+
41.2041 0.410228
|
|
68
|
+
55.7698 0.503672
|
|
69
|
+
56.8647 0.461943
|
|
70
|
+
85.0296 100.0
|
|
71
|
+
129.0196 8.036902
|
|
72
|
+
END IONS
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Run FIDDLE!**
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python scripts/run_fiddle.py --test_data ./msfiddle/demo/input_msms.mgf \
|
|
79
|
+
--config_path ./msfiddle/config/fiddle_tcn_orbitrap.yml \
|
|
80
|
+
--resume_path ./check_point/fiddle_tcn_orbitrap.pt \
|
|
81
|
+
--rescore_resume_path ./check_point/fiddle_rescore_orbitrap.pt \
|
|
82
|
+
--result_path ./msfiddle/demo/output_fiddle.csv --device 0
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
If you'd like to integrate the results from SIRIUS and BUDDY, please organize the results in the format shown in `./msfiddle/demo/buddy_output.csv` and `./msfiddle/demo/sirius_output.csv`, and provide them to run FIDDLE:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
python scripts/run_fiddle.py --test_data ./msfiddle/demo/input_msms.mgf \
|
|
89
|
+
--config_path ./msfiddle/config/fiddle_tcn_orbitrap.yml \
|
|
90
|
+
--resume_path ./check_point/fiddle_tcn_orbitrap.pt \
|
|
91
|
+
--rescore_resume_path ./check_point/fiddle_rescore_orbitrap.pt \
|
|
92
|
+
--buddy_path ./msfiddle/demo/output_buddy.csv \
|
|
93
|
+
--sirius_path ./msfiddle/demo/output_sirius.csv \
|
|
94
|
+
--result_path ./msfiddle/demo/output_fiddle_all.csv --device 0
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
See [`scripts/test_caffeine.py`](https://github.com/JosieHong/FIDDLE/blob/main/scripts/test_caffeine.py) for a worked example running FIDDLE on a caffeine Orbitrap spectrum fetched live from GNPS.
|
|
98
|
+
|
|
99
|
+
## Reproduce paper results
|
|
100
|
+
|
|
101
|
+
All scripts should be run from the repository root (`FIDDLE/`).
|
|
102
|
+
|
|
103
|
+
| Script | Description |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `running_scripts/experiments_test_benchmark.sh` | Evaluate on external benchmarks (CASMI 2016, CASMI 2017, EMBL-MCF 2.0) |
|
|
106
|
+
| `running_scripts/experiments_test_nist23.sh` | Evaluate on NIST23 |
|
|
107
|
+
| `running_scripts/experiments_test_chimeric.sh` | Evaluate on chimeric spectra |
|
|
108
|
+
| `running_scripts/experiments_test_noised.sh` | Evaluate under noise conditions |
|
|
109
|
+
| `running_scripts/experiments_ablation_study.sh` | Run ablation study |
|
|
110
|
+
| `running_scripts/experiments_demo.sh` | Run demo experiment |
|
|
111
|
+
| `running_scripts/train_released_models.sh` | Train TCN and rescore models for both Orbitrap and Q-TOF |
|
|
112
|
+
|
|
113
|
+
For training from scratch, see the train scripts (`scripts/train_tcn_gpus.py`, `scripts/train_tcn_gpus_cl.py`, `scripts/train_rescore.py`) and the corresponding config files in `./msfiddle/config/`.
|
|
114
|
+
|
|
115
|
+
## Citation
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
@article{hong2025fiddle,
|
|
119
|
+
title={FIDDLE: a deep learning method for chemical formulas prediction from tandem mass spectra},
|
|
120
|
+
author={Hong, Yuhui and Li, Sujun and Ye, Yuzhen and Tang, Haixu},
|
|
121
|
+
journal={Nature Communications},
|
|
122
|
+
volume={16},
|
|
123
|
+
number={1},
|
|
124
|
+
pages={11102},
|
|
125
|
+
year={2025},
|
|
126
|
+
publisher={Nature Publishing Group UK London}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
@@ -131,12 +131,14 @@ model:
|
|
|
131
131
|
# ------------------------
|
|
132
132
|
train:
|
|
133
133
|
epochs: 500
|
|
134
|
-
batch_size: 512 # 128
|
|
134
|
+
batch_size: 512 # 128 fits 11GB GPUs (CL training holds 2 forward graphs, memory ~ 2x batch)
|
|
135
|
+
grad_accum_steps: 1 # optimizer step every N batches; effective batch = batch_size x N (256 x 2 on 2 x 11GB GPUs = 512)
|
|
135
136
|
num_workers: 8
|
|
136
137
|
lr: 0.001
|
|
137
138
|
weight_decay: 0.01
|
|
138
139
|
warmup_ratio: 0.1
|
|
139
|
-
early_stop_step:
|
|
140
|
+
early_stop_step: 15 # ~3x plateau patience so >=2 LR halvings get a chance
|
|
141
|
+
early_stop_min_epochs: 5 # patience does not count before this epoch
|
|
140
142
|
patience: 5
|
|
141
143
|
noised_times: 0
|
|
142
144
|
|
|
@@ -150,15 +152,18 @@ train_rescore:
|
|
|
150
152
|
# ------------------------
|
|
151
153
|
# postprocessing settings
|
|
152
154
|
# ------------------------
|
|
153
|
-
post_processing:
|
|
155
|
+
post_processing:
|
|
154
156
|
# Orbitrap
|
|
155
|
-
|
|
156
|
-
|
|
157
|
+
# D0c: halogens in the refinement search so a missed halogen can be introduced and
|
|
158
|
+
# H rebalanced to the exact precursor mass. (Orbitrap is halogen-rich, so the model
|
|
159
|
+
# usually predicts halogens already; this mainly helps the cases it misses.)
|
|
160
|
+
refine_atom_type: ['C', 'O', 'N', 'H', 'Cl', 'Br', 'F', 'I', 'S', 'P']
|
|
161
|
+
refine_atom_num: [5, 5, 5, -1, 2, 1, 2, 1, 2, 1] # config E caps (08/2026 sweep): recall@20 88.5% -> 94.3% at 10ppm, 95.7% if 5ppm window validates
|
|
157
162
|
|
|
158
|
-
mass_tolerance:
|
|
163
|
+
mass_tolerance: 5.0 # config E (08/2026 sweep): matches the data filters' ppm_tolerance 5 (no admitted spectrum can be orphaned) and Orbitrap instrument accuracy; halves the decoy count vs 10 ppm, recall@20 ceiling 95.7%
|
|
159
164
|
ppm_mode: True
|
|
160
165
|
time_out: 300
|
|
161
166
|
maxium_miss_atom_num: 20 # search space
|
|
162
|
-
top_k:
|
|
167
|
+
top_k: 20 # config E (08/2026 sweep): +2 points of rescorer ceiling over K=10; keep pair-generation and inference matched
|
|
163
168
|
buddy_fdr_thr: 0.6
|
|
164
169
|
sirius_score_thr: -5
|
|
@@ -165,17 +165,19 @@ model:
|
|
|
165
165
|
# ------------------------
|
|
166
166
|
train:
|
|
167
167
|
epochs: 200
|
|
168
|
-
batch_size: 512 # 128
|
|
168
|
+
batch_size: 512 # 128 fits 11GB GPUs (CL training holds 2 forward graphs, memory ~ 2x batch)
|
|
169
|
+
grad_accum_steps: 1 # optimizer step every N batches; effective batch = batch_size x N (256 x 2 on 2 x 11GB GPUs = 512)
|
|
169
170
|
num_workers: 8
|
|
170
171
|
lr: 0.002 # 0.001
|
|
171
172
|
weight_decay: 0.01
|
|
172
173
|
warmup_ratio: 0.1
|
|
173
|
-
early_stop_step:
|
|
174
|
+
early_stop_step: 15 # ~3x plateau patience so >=2 LR halvings get a chance
|
|
175
|
+
early_stop_min_epochs: 5 # patience does not count before this epoch
|
|
174
176
|
patience: 5
|
|
175
177
|
noised_times: 3
|
|
176
178
|
|
|
177
179
|
train_rescore:
|
|
178
|
-
epochs:
|
|
180
|
+
epochs: 20
|
|
179
181
|
batch_size: 128
|
|
180
182
|
num_workers: 8
|
|
181
183
|
lr: 0.0001
|
|
@@ -184,14 +186,19 @@ train_rescore:
|
|
|
184
186
|
# ------------------------
|
|
185
187
|
# postprocessing settings
|
|
186
188
|
# ------------------------
|
|
187
|
-
post_processing:
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
post_processing:
|
|
190
|
+
# D0c (tuned): let refinement INTRODUCE a missed halogen and rebalance H to the
|
|
191
|
+
# exact precursor mass. Base was ['C','O','N','H'] -> halogens unreachable.
|
|
192
|
+
# S/P deliberately NOT in base (already reachable when the model predicts them;
|
|
193
|
+
# adding them only created spurious candidates). Halogen caps kept tight to limit
|
|
194
|
+
# near-isobaric false positives on the CONH majority.
|
|
195
|
+
refine_atom_type: ['C', 'O', 'N', 'H', 'Cl', 'Br', 'F', 'I', 'S', 'P']
|
|
196
|
+
refine_atom_num: [5, 5, 5, -1, 2, 1, 2, 1, 2, 1] # config B (08/2026 sweep): S/P reachable; recall@20 92.3% -> 94.0%
|
|
190
197
|
|
|
191
198
|
mass_tolerance: 10.0 # target space
|
|
192
199
|
ppm_mode: True
|
|
193
200
|
time_out: -1 # do not limit the time
|
|
194
201
|
maxium_miss_atom_num: 50 # search space
|
|
195
|
-
top_k:
|
|
202
|
+
top_k: 20 # config B (08/2026 sweep): +2 points of rescorer ceiling over K=10; keep pair-generation and inference matched
|
|
196
203
|
buddy_fdr_thr: 0.6
|
|
197
204
|
sirius_score_thr: -5
|
|
@@ -55,7 +55,7 @@ class MS2FDataset_CL(Dataset):
|
|
|
55
55
|
for data_item in self.data:
|
|
56
56
|
spec = data_item["spec"]
|
|
57
57
|
data_item["spec"] = np.concatenate(
|
|
58
|
-
(spec, np.zeros((padding_dim, spec.shape[1]))), axis=0
|
|
58
|
+
(spec, np.zeros((padding_dim, spec.shape[1]), dtype=spec.dtype)), axis=0
|
|
59
59
|
)
|
|
60
60
|
|
|
61
61
|
def augment_with_noise(self, data, pairs, noised_times):
|
|
@@ -76,11 +76,16 @@ class MS2FDataset_CL(Dataset):
|
|
|
76
76
|
"""
|
|
77
77
|
original_length = len(data)
|
|
78
78
|
augmented_data = copy.deepcopy(data)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
# Copies are appended in whole-dataset blocks (round t of every
|
|
80
|
+
# spectrum, then round t+1, ...) so that augmented copy t of item j
|
|
81
|
+
# sits at index original_length * (t + 1) + j — the layout the pair
|
|
82
|
+
# index shift below assumes. Interleaving copies per spectrum instead
|
|
83
|
+
# silently pairs unrelated molecules once noised_times >= 2.
|
|
84
|
+
for _ in range(noised_times):
|
|
85
|
+
for data_item in tqdm(data, desc="Data Augmentation"):
|
|
86
|
+
spec = data_item["spec"][:, 0]
|
|
87
|
+
spec_mask = np.where(spec > 0, 1.0, 0.0)
|
|
82
88
|
|
|
83
|
-
for _ in range(noised_times):
|
|
84
89
|
noise = np.random.normal(0, 0.1, len(spec))
|
|
85
90
|
new_spec = spec + noise * spec_mask
|
|
86
91
|
|
|
@@ -151,7 +156,7 @@ class MS2FDataset(Dataset):
|
|
|
151
156
|
for data_item in self.data:
|
|
152
157
|
spec = data_item["spec"]
|
|
153
158
|
data_item["spec"] = np.concatenate(
|
|
154
|
-
(spec, np.zeros((padding_dim, spec.shape[1]))), axis=0
|
|
159
|
+
(spec, np.zeros((padding_dim, spec.shape[1]), dtype=spec.dtype)), axis=0
|
|
155
160
|
)
|
|
156
161
|
|
|
157
162
|
def augment_with_noise(self, data, noised_times):
|
|
@@ -293,6 +298,18 @@ class MGFDataset(Dataset):
|
|
|
293
298
|
'precursor_type', and 'use_simulated_precursor_mz'.
|
|
294
299
|
"""
|
|
295
300
|
for spectrum in spectra:
|
|
301
|
+
# Skip (not crash on) adducts the encoder does not support: user
|
|
302
|
+
# MGFs routinely contain a few, and one unsupported PRECURSOR_TYPE
|
|
303
|
+
# must not abort the whole run with a KeyError.
|
|
304
|
+
precursor_type = spectrum["params"]["precursor_type"]
|
|
305
|
+
if precursor_type not in encoder["type2charge"]:
|
|
306
|
+
print(
|
|
307
|
+
"Skip {}: unsupported precursor type {}".format(
|
|
308
|
+
spectrum["params"].get("title", "?"), precursor_type
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
continue
|
|
312
|
+
|
|
296
313
|
good_spec, _, _, spec_arr = generate_ms(
|
|
297
314
|
x=spectrum["m/z array"],
|
|
298
315
|
y=spectrum["intensity array"],
|
|
@@ -309,6 +326,15 @@ class MGFDataset(Dataset):
|
|
|
309
326
|
adjust_neutral_add_vec, adjust_precursor_type = self.melt_neutral_precursor(
|
|
310
327
|
spectrum["params"]["precursor_type"]
|
|
311
328
|
)
|
|
329
|
+
if adjust_precursor_type not in encoder["precursor_type"]:
|
|
330
|
+
print(
|
|
331
|
+
"Skip {}: unsupported precursor type {} (from {})".format(
|
|
332
|
+
spectrum["params"].get("title", "?"),
|
|
333
|
+
adjust_precursor_type,
|
|
334
|
+
precursor_type,
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
continue
|
|
312
338
|
|
|
313
339
|
ce, nce = parse_collision_energy(
|
|
314
340
|
ce_str=spectrum["params"]["collision_energy"],
|
|
@@ -16,11 +16,18 @@ from importlib.metadata import (
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def checkpoint_release_for_package_version(package_version):
|
|
19
|
-
"""Return the
|
|
20
|
-
|
|
19
|
+
"""Return the FIDDLE release tag that holds this version's checkpoints.
|
|
20
|
+
|
|
21
|
+
From 2.2 on, each minor version has its own checkpoints (tag vX.Y.0,
|
|
22
|
+
shared by its patch releases); 2.0 and 2.1 share the v2.0.0 assets.
|
|
23
|
+
"""
|
|
24
|
+
match = re.match(r"^(\d+)(?:\.(\d+))?", str(package_version))
|
|
21
25
|
if match is None:
|
|
22
26
|
return f"v{package_version}"
|
|
23
|
-
|
|
27
|
+
major, minor = int(match.group(1)), int(match.group(2) or 0)
|
|
28
|
+
if (major, minor) < (2, 2):
|
|
29
|
+
return f"v{major}.0.0"
|
|
30
|
+
return f"v{major}.{minor}.0"
|
|
24
31
|
|
|
25
32
|
|
|
26
33
|
try:
|
|
@@ -56,10 +63,14 @@ def download_url(url, output_path):
|
|
|
56
63
|
def get_checkpoint_dir():
|
|
57
64
|
"""Get the directory where model checkpoints are stored.
|
|
58
65
|
|
|
59
|
-
Returns ``~/.msfiddle/check_point
|
|
60
|
-
consistent regardless of working directory or install method.
|
|
66
|
+
Returns ``~/.msfiddle/check_point/<release tag>``, a user-level directory
|
|
67
|
+
that is consistent regardless of working directory or install method. The
|
|
68
|
+
tag keeps checkpoints from different releases apart, so an upgrade never
|
|
69
|
+
silently reuses an older release's weights.
|
|
61
70
|
"""
|
|
62
|
-
return os.path.join(
|
|
71
|
+
return os.path.join(
|
|
72
|
+
os.path.expanduser("~"), ".msfiddle", "check_point", FIDDLE_RELEASE
|
|
73
|
+
)
|
|
63
74
|
|
|
64
75
|
|
|
65
76
|
def download_models(destination=None, models=None):
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Muon optimizer (MomentUm Orthogonalized by Newton-Schulz).
|
|
2
|
+
|
|
3
|
+
Muon applies momentum SGD to matrix-shaped hidden weights and orthogonalizes
|
|
4
|
+
each update with a Newton-Schulz iteration before applying it, which keeps the
|
|
5
|
+
update's singular values near 1 (an approximate steepest descent under the
|
|
6
|
+
spectral norm). Reference: Keller Jordan et al., 2024
|
|
7
|
+
(https://kellerjordan.github.io/posts/muon/).
|
|
8
|
+
|
|
9
|
+
Only 2D+ hidden weights benefit; embeddings, biases, and per-channel gains
|
|
10
|
+
(weight_norm's weight_g) keep AdamW. The Muon class below therefore carries
|
|
11
|
+
two param groups, one per update rule, so schedulers and checkpointing see a
|
|
12
|
+
single ordinary optimizer.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import torch
|
|
16
|
+
import torch.nn as nn
|
|
17
|
+
from torch.optim import Optimizer
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def zeropower_via_newtonschulz5(G, steps=5, eps=1e-7):
|
|
21
|
+
"""Approximate UV^T of the SVD of G via a quintic Newton-Schulz iteration.
|
|
22
|
+
|
|
23
|
+
Runs in float32 (not bfloat16 as in the reference) for correctness on
|
|
24
|
+
pre-Ampere GPUs; the matrices here are small enough that this is cheap.
|
|
25
|
+
The iteration's singular values land in roughly [0.7, 1.2], which is all
|
|
26
|
+
Muon needs.
|
|
27
|
+
"""
|
|
28
|
+
assert G.ndim == 2
|
|
29
|
+
a, b, c = (3.4445, -4.7750, 2.0315)
|
|
30
|
+
X = G.float()
|
|
31
|
+
transposed = G.size(0) > G.size(1)
|
|
32
|
+
if transposed:
|
|
33
|
+
X = X.T
|
|
34
|
+
X = X / (X.norm() + eps)
|
|
35
|
+
for _ in range(steps):
|
|
36
|
+
A = X @ X.T
|
|
37
|
+
B = b * A + c * A @ A
|
|
38
|
+
X = a * X + B @ X
|
|
39
|
+
if transposed:
|
|
40
|
+
X = X.T
|
|
41
|
+
return X.to(G.dtype)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def split_matrix_params(model):
|
|
45
|
+
"""Partition parameters into (muon_params, adamw_params).
|
|
46
|
+
|
|
47
|
+
Muon gets matrix-shaped hidden weights: ndim >= 2, not an nn.Embedding
|
|
48
|
+
weight, not weight_norm's per-channel gain (weight_g). Everything else
|
|
49
|
+
(biases, gains, embeddings) goes to AdamW.
|
|
50
|
+
"""
|
|
51
|
+
if isinstance(model, nn.DataParallel):
|
|
52
|
+
model = model.module
|
|
53
|
+
emb_ids = set()
|
|
54
|
+
for m in model.modules():
|
|
55
|
+
if isinstance(m, nn.Embedding):
|
|
56
|
+
emb_ids.update(id(p) for p in m.parameters(recurse=False))
|
|
57
|
+
muon_params, adamw_params = [], []
|
|
58
|
+
for name, p in model.named_parameters():
|
|
59
|
+
if not p.requires_grad:
|
|
60
|
+
continue
|
|
61
|
+
if p.ndim >= 2 and id(p) not in emb_ids and not name.endswith("weight_g"):
|
|
62
|
+
muon_params.append(p)
|
|
63
|
+
else:
|
|
64
|
+
adamw_params.append(p)
|
|
65
|
+
return muon_params, adamw_params
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Muon(Optimizer):
|
|
69
|
+
"""Muon for matrix params, decoupled AdamW for everything else.
|
|
70
|
+
|
|
71
|
+
Conv kernels (out, in, k) are flattened to (out, in*k) for the
|
|
72
|
+
orthogonalization, following the Moonlight/Kimi treatment. The update is
|
|
73
|
+
scaled by sqrt(max(1, rows/cols)) so its RMS matches AdamW's, letting one
|
|
74
|
+
LR schedule drive both groups.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
muon_params,
|
|
80
|
+
adamw_params,
|
|
81
|
+
lr=0.02,
|
|
82
|
+
momentum=0.95,
|
|
83
|
+
nesterov=True,
|
|
84
|
+
ns_steps=5,
|
|
85
|
+
adamw_lr=1e-3,
|
|
86
|
+
adamw_betas=(0.9, 0.999),
|
|
87
|
+
adamw_eps=1e-8,
|
|
88
|
+
weight_decay=0.01,
|
|
89
|
+
):
|
|
90
|
+
groups = [
|
|
91
|
+
dict(
|
|
92
|
+
params=list(muon_params),
|
|
93
|
+
lr=lr,
|
|
94
|
+
momentum=momentum,
|
|
95
|
+
nesterov=nesterov,
|
|
96
|
+
ns_steps=ns_steps,
|
|
97
|
+
weight_decay=weight_decay,
|
|
98
|
+
use_muon=True,
|
|
99
|
+
),
|
|
100
|
+
dict(
|
|
101
|
+
params=list(adamw_params),
|
|
102
|
+
lr=adamw_lr,
|
|
103
|
+
betas=adamw_betas,
|
|
104
|
+
eps=adamw_eps,
|
|
105
|
+
weight_decay=weight_decay,
|
|
106
|
+
use_muon=False,
|
|
107
|
+
),
|
|
108
|
+
]
|
|
109
|
+
super().__init__(groups, {})
|
|
110
|
+
|
|
111
|
+
@torch.no_grad()
|
|
112
|
+
def step(self, closure=None):
|
|
113
|
+
loss = None
|
|
114
|
+
if closure is not None:
|
|
115
|
+
with torch.enable_grad():
|
|
116
|
+
loss = closure()
|
|
117
|
+
for group in self.param_groups:
|
|
118
|
+
if group["use_muon"]:
|
|
119
|
+
self._muon_group_step(group)
|
|
120
|
+
else:
|
|
121
|
+
self._adamw_group_step(group)
|
|
122
|
+
return loss
|
|
123
|
+
|
|
124
|
+
def _muon_group_step(self, group):
|
|
125
|
+
for p in group["params"]:
|
|
126
|
+
g = p.grad
|
|
127
|
+
if g is None:
|
|
128
|
+
continue
|
|
129
|
+
state = self.state[p]
|
|
130
|
+
if "momentum_buffer" not in state:
|
|
131
|
+
state["momentum_buffer"] = torch.zeros_like(g)
|
|
132
|
+
buf = state["momentum_buffer"]
|
|
133
|
+
buf.mul_(group["momentum"]).add_(g)
|
|
134
|
+
update = g.add(buf, alpha=group["momentum"]) if group["nesterov"] else buf
|
|
135
|
+
u2 = update.reshape(update.size(0), -1)
|
|
136
|
+
u2 = zeropower_via_newtonschulz5(u2, steps=group["ns_steps"])
|
|
137
|
+
scale = max(1.0, u2.size(0) / u2.size(1)) ** 0.5
|
|
138
|
+
p.mul_(1.0 - group["lr"] * group["weight_decay"])
|
|
139
|
+
p.add_(u2.reshape_as(p), alpha=-group["lr"] * scale)
|
|
140
|
+
|
|
141
|
+
def _adamw_group_step(self, group):
|
|
142
|
+
beta1, beta2 = group["betas"]
|
|
143
|
+
for p in group["params"]:
|
|
144
|
+
g = p.grad
|
|
145
|
+
if g is None:
|
|
146
|
+
continue
|
|
147
|
+
state = self.state[p]
|
|
148
|
+
if "exp_avg" not in state:
|
|
149
|
+
state["step"] = 0
|
|
150
|
+
state["exp_avg"] = torch.zeros_like(g)
|
|
151
|
+
state["exp_avg_sq"] = torch.zeros_like(g)
|
|
152
|
+
state["step"] += 1
|
|
153
|
+
exp_avg, exp_avg_sq = state["exp_avg"], state["exp_avg_sq"]
|
|
154
|
+
exp_avg.mul_(beta1).add_(g, alpha=1 - beta1)
|
|
155
|
+
exp_avg_sq.mul_(beta2).addcmul_(g, g, value=1 - beta2)
|
|
156
|
+
bc1 = 1 - beta1 ** state["step"]
|
|
157
|
+
bc2 = 1 - beta2 ** state["step"]
|
|
158
|
+
denom = (exp_avg_sq / bc2).sqrt_().add_(group["eps"])
|
|
159
|
+
p.mul_(1.0 - group["lr"] * group["weight_decay"])
|
|
160
|
+
p.addcdiv_(exp_avg, denom, value=-group["lr"] / bc1)
|