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.
Files changed (52) hide show
  1. msfiddle-2.2.0/PKG-INFO +169 -0
  2. msfiddle-2.2.0/README.md +128 -0
  3. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/config/fiddle_tcn_orbitrap.yml +12 -7
  4. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/config/fiddle_tcn_qtof.yml +14 -7
  5. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/dataset.py +32 -6
  6. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/download.py +17 -6
  7. msfiddle-2.2.0/msfiddle/optim.py +160 -0
  8. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/utils/msms_utils.py +134 -103
  9. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/utils/pkl_utils.py +140 -36
  10. msfiddle-2.2.0/msfiddle/utils/refine_utils.py +712 -0
  11. msfiddle-2.2.0/msfiddle.egg-info/PKG-INFO +169 -0
  12. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle.egg-info/SOURCES.txt +6 -0
  13. {msfiddle-2.1.0 → msfiddle-2.2.0}/setup.py +3 -3
  14. msfiddle-2.2.0/tests/test_download.py +29 -0
  15. msfiddle-2.2.0/tests/test_generate_ms.py +52 -0
  16. msfiddle-2.2.0/tests/test_muon.py +94 -0
  17. msfiddle-2.2.0/tests/test_preprocessing_speedups.py +148 -0
  18. msfiddle-2.2.0/tests/test_refine_enumeration.py +136 -0
  19. msfiddle-2.2.0/tests/test_refine_fast_mass.py +106 -0
  20. msfiddle-2.1.0/PKG-INFO +0 -285
  21. msfiddle-2.1.0/README.md +0 -244
  22. msfiddle-2.1.0/msfiddle/utils/refine_utils.py +0 -376
  23. msfiddle-2.1.0/msfiddle.egg-info/PKG-INFO +0 -285
  24. msfiddle-2.1.0/tests/test_download.py +0 -15
  25. {msfiddle-2.1.0 → msfiddle-2.2.0}/LICENSE +0 -0
  26. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/__init__.py +0 -0
  27. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/api.py +0 -0
  28. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/config/fiddle_tcn_casmi.yml +0 -0
  29. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/config/fiddle_tcn_demo.yml +0 -0
  30. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/config/fiddle_tcn_demo_wo_da.yml +0 -0
  31. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/config/fiddle_tcn_embl.yml +0 -0
  32. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/demo/input_msms.mgf +0 -0
  33. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/demo/output_buddy.csv +0 -0
  34. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/demo/output_fiddle.csv +0 -0
  35. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/demo/output_fiddle_all.csv +0 -0
  36. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/demo/output_sirius.csv +0 -0
  37. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/external_results.py +0 -0
  38. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/main.py +0 -0
  39. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/model_tcn.py +0 -0
  40. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/utils/__init__.py +0 -0
  41. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle/utils/mol_utils.py +0 -0
  42. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle.egg-info/dependency_links.txt +0 -0
  43. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle.egg-info/entry_points.txt +0 -0
  44. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle.egg-info/requires.txt +0 -0
  45. {msfiddle-2.1.0 → msfiddle-2.2.0}/msfiddle.egg-info/top_level.txt +0 -0
  46. {msfiddle-2.1.0 → msfiddle-2.2.0}/setup.cfg +0 -0
  47. {msfiddle-2.1.0 → msfiddle-2.2.0}/tests/test_api.py +0 -0
  48. {msfiddle-2.1.0 → msfiddle-2.2.0}/tests/test_external_results.py +0 -0
  49. {msfiddle-2.1.0 → msfiddle-2.2.0}/tests/test_main.py +0 -0
  50. {msfiddle-2.1.0 → msfiddle-2.2.0}/tests/test_mol_utils.py +0 -0
  51. {msfiddle-2.1.0 → msfiddle-2.2.0}/tests/test_msms_utils.py +0 -0
  52. {msfiddle-2.1.0 → msfiddle-2.2.0}/tests/test_refine_utils.py +0 -0
@@ -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
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
45
+ [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.19181279-blue)](https://doi.org/10.5281/zenodo.19181279)
46
+ [![Release](https://img.shields.io/github/v/release/JosieHong/FIDDLE?label=Release)](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
+ ```
@@ -0,0 +1,128 @@
1
+ # FIDDLE
2
+
3
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
4
+ [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.19181279-blue)](https://doi.org/10.5281/zenodo.19181279)
5
+ [![Release](https://img.shields.io/github/v/release/JosieHong/FIDDLE?label=Release)](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: 10
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
- refine_atom_type: ['C', 'O', 'N', 'H']
156
- refine_atom_num: [3, 3, 3, -1]
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: 20.0 # target space
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: 5
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: 10
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: 100
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
- refine_atom_type: ['C', 'O', 'N', 'H']
189
- refine_atom_num: [5, 5, 5, -1]
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: 5
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
- for data_item in tqdm(data, desc="Data Augmentation"):
80
- spec = data_item["spec"][:, 0]
81
- spec_mask = np.where(spec > 0, 1.0, 0.0)
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 major-version FIDDLE release tag that contains checkpoints."""
20
- match = re.match(r"^(\d+)", str(package_version))
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
- return f"v{match.group(1)}.0.0"
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``, a user-level directory that is
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(os.path.expanduser("~"), ".msfiddle", "check_point")
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)