msfiddle 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,246 @@
1
+ import re
2
+ import argparse
3
+ import yaml
4
+ from molmass import Formula
5
+ import time
6
+
7
+ from .mol_utils import ATOMS_WEIGHT, ATOMS_VALENCE
8
+
9
+
10
+
11
+ def parse_formula(formula):
12
+ pattern = r'([A-Z][a-z]?)(\d*)'
13
+ matches = re.findall(pattern, formula)
14
+
15
+ atom_counts = {}
16
+ for atom, count in matches:
17
+ count = int(count) if count else 1
18
+ atom_counts[atom] = atom_counts.get(atom, 0) + count
19
+ return atom_counts
20
+
21
+ def format_formula(atom_counts):
22
+ # Sort atoms alphabetically and format the formula
23
+ return ''.join([atom + (str(count) if count > 1 else '') for atom, count in sorted(atom_counts.items())])
24
+
25
+ def passes_senior_rule(formula):
26
+ atom_counts = parse_formula(formula)
27
+ total_valence = 0
28
+ max_valence = 0
29
+
30
+ for atom, count in atom_counts.items():
31
+ valence = ATOMS_VALENCE.get(atom, 0)
32
+ total_valence += valence * count
33
+ max_valence = max(max_valence, valence)
34
+
35
+ num_atoms = sum(atom_counts.values())
36
+
37
+ sum_odd_valences = sum(valence * count for atom, count in atom_counts.items() if ATOMS_VALENCE.get(atom, 0) % 2 != 0)
38
+ if sum_odd_valences % 2 != 0:
39
+ return False
40
+
41
+ if total_valence < 2 * max_valence:
42
+ return False
43
+
44
+ if total_valence < 2 * (num_atoms - 1):
45
+ return False
46
+
47
+ return True
48
+
49
+ def exceed_refine_atom_limit(refined_counts, formulas, refine_atom_type, refine_atom_num):
50
+ """Check if the refined formula exceeds the refinement limit for a given atom type."""
51
+ # refined_counts = parse_formula(refined_formula)
52
+ for formula in formulas:
53
+ original_counts = parse_formula(formula)
54
+
55
+ for t, n in zip(refine_atom_type, refine_atom_num):
56
+ if n == -1: # No limitation
57
+ continue
58
+
59
+ refined_n = refined_counts.get(t, 0)
60
+ original_n = original_counts.get(t, 0)
61
+ # Check if the difference in atom counts exceeds the allowed number
62
+ if abs(refined_n - original_n) > n:
63
+ return True # Exceeds the limit for at least one formula
64
+
65
+ return False # Does not exceed the limit for any formula
66
+
67
+ def adjust_hydrogen(atom_counts, M):
68
+ delta_h = round((M - Formula(format_formula(atom_counts)).isotope.mass) / 1.007941)
69
+ if 'H' not in atom_counts.keys():
70
+ atom_counts['H'] = delta_h
71
+ else:
72
+ atom_counts['H'] += delta_h
73
+ if atom_counts['H'] <= 0: del atom_counts['H']
74
+ return atom_counts
75
+
76
+ def remove_duplicates_preserve_order(seq):
77
+ seen = set()
78
+ return [x for x in seq if not (x in seen or seen.add(x))]
79
+
80
+ def candidate_formulas_generation(f, M, f0_list, refine_atom_type, refine_atom_num):
81
+ atom_counts = parse_formula(f)
82
+ formulas = []
83
+
84
+ for a in refine_atom_type:
85
+ if a == 'H': continue # Skip 'H'
86
+
87
+ # Add one atom
88
+ new_atom_counts = dict(atom_counts)
89
+ new_atom_counts[a] = new_atom_counts.get(a, 0) + 1
90
+ if not exceed_refine_atom_limit(new_atom_counts, f0_list, refine_atom_type, refine_atom_num) and 'H' in atom_counts.keys():
91
+ new_atom_counts = adjust_hydrogen(new_atom_counts, M) # Ajust 'H'
92
+ formulas.append(format_formula(new_atom_counts))
93
+
94
+ # Remove one atom if it exists
95
+ if a in atom_counts and atom_counts[a] > 0:
96
+ new_atom_counts = dict(atom_counts)
97
+ new_atom_counts[a] -= 1
98
+ if new_atom_counts[a] == 0:
99
+ del new_atom_counts[a]
100
+ if not exceed_refine_atom_limit(new_atom_counts, f0_list, refine_atom_type, refine_atom_num) and 'H' in atom_counts.keys():
101
+ new_atom_counts = adjust_hydrogen(new_atom_counts, M) # Ajust 'H'
102
+ formulas.append(format_formula(new_atom_counts))
103
+
104
+ # Sort based on proximity to M
105
+ formulas.sort(key=lambda formula: abs(Formula(formula).isotope.mass - M))
106
+
107
+ return formulas
108
+
109
+ def formula_refinement(f0_list, M, delta_M, ppm_mode, K, D, T, refine_atom_type, refine_atom_num):
110
+ start_time = time.time()
111
+ refine_f = [] # List of refined formulas
112
+ trace_f = set() # Keep track of all formulas that have been focused on
113
+
114
+ if ppm_mode:
115
+ delta_M = delta_M * M / 10 ** 6
116
+
117
+ # If no halogen in prediction, no halogen in refinement
118
+ # pred_atom_types = set()
119
+ # for f0 in f0_list:
120
+ # pred_atom_types.update([atom for atom, n in parse_formula(f0).items() if n > 0])
121
+ # for atom in ['P', 'S', 'F', 'Cl', 'B', 'Br', 'I', 'Na', 'K']:
122
+ # if atom not in pred_atom_types and atom in refine_atom_type:
123
+ # i = refine_atom_type.index(atom)
124
+ # refine_atom_type.pop(i)
125
+ # refine_atom_num.pop(i)
126
+
127
+ candidate_f = [format_formula(adjust_hydrogen(parse_formula(f0), M)) for f0 in f0_list] # Initialize candidate_f
128
+ candidate_d = [0] * len(candidate_f)
129
+ search_deep = 0
130
+ # print('Init {} candidate formulas from predictions'.format(len(candidate_f)))
131
+ # print(candidate_f)
132
+ # print([(Formula(f).isotope.mass, abs(Formula(f).isotope.mass - M)) for f in candidate_f])
133
+
134
+ # Search >>>
135
+ while len(refine_f) < K and (candidate_f or search_deep==0): # It is okay if at begining there is no formula in candidate_f
136
+ if search_deep == 0:
137
+ # Check if the predicted formula falls into the target space
138
+ for f0 in candidate_f:
139
+ m = Formula(f0).isotope.mass
140
+ if M - delta_M <= m <= M + delta_M and passes_senior_rule(f0) and f0 not in refine_f: # Passed SENIOR rule
141
+ refine_f.append(f0) # Append the refined formula for return
142
+ # print('Got one refined formula')
143
+
144
+ # Generate more candidates from the init formula list
145
+ new_candidates = []
146
+ for f0 in f0_list:
147
+ new_candidates += candidate_formulas_generation(f0, M, f0_list, refine_atom_type, refine_atom_num)
148
+ new_candidates = remove_duplicates_preserve_order(new_candidates) # Remove duplicates
149
+ new_candidates.sort(key=lambda formula: abs(Formula(formula).isotope.mass - M)) # Need resort, because they are extend from multiple candidate list
150
+ new_depths = [search_deep+1] * len(new_candidates)
151
+
152
+ candidate_f = new_candidates + candidate_f
153
+ candidate_d = new_depths + candidate_d
154
+
155
+ elif search_deep <= D:
156
+ # Generate more candidates from the current focus formula f
157
+ new_candidates = candidate_formulas_generation(f, M, f0_list, refine_atom_type, refine_atom_num)
158
+ new_candidates = remove_duplicates_preserve_order(new_candidates) # Remove duplicates
159
+ new_candidates = [i for i in new_candidates if i not in trace_f and i not in candidate_f]
160
+ new_depths = [search_deep+1] * len(new_candidates)
161
+
162
+ while len(new_candidates) == 0 and len(candidate_f) > 0:
163
+ f = candidate_f.pop(0) # Reject to continue (no new candidate), trace back
164
+ search_deep = candidate_d.pop(0)
165
+ trace_f.add(f)
166
+ # print('Trackback')
167
+
168
+ # Generate more candidates from the current focus formula f
169
+ new_candidates = candidate_formulas_generation(f, M, f0_list, refine_atom_type, refine_atom_num)
170
+ new_candidates = remove_duplicates_preserve_order(new_candidates) # Remove duplicates
171
+ new_candidates = [i for i in new_candidates if i not in trace_f and i not in candidate_f]
172
+ new_depths = [search_deep+1] * len(new_candidates)
173
+ if len(new_candidates) == 0: # No other candidates exit
174
+ break
175
+
176
+ candidate_f = new_candidates + candidate_f
177
+ candidate_d = new_depths + candidate_d
178
+
179
+ else: # While exceed search limitation, the candidate_f will be depleted
180
+ if len(candidate_f) == 0: break
181
+ f = candidate_f.pop(0) # Reject to continue (exceed search limitation), trace back
182
+ search_deep = candidate_d.pop(0)
183
+ trace_f.add(f)
184
+ # print('Trackback')
185
+
186
+ if len(candidate_f) == 0: break
187
+ f = candidate_f.pop(0) # Select the next formula to focus on
188
+ search_deep = candidate_d.pop(0)
189
+ m = Formula(f).isotope.mass
190
+ trace_f.add(f)
191
+ # print('\nNext step: {} ({}, {})'.format(f, m, abs(m - M)))
192
+
193
+ if M - delta_M <= m <= M + delta_M:
194
+ if passes_senior_rule(f) and f not in refine_f: # Passed SENIOR rule
195
+ refine_f.append(f) # Append the refined formula for return
196
+ # print('Got one refined formula')
197
+
198
+ # Check if timeout
199
+ running_time = time.time() - start_time
200
+ if T > 0 and running_time > T: # Timeout
201
+ break
202
+ # End >>>
203
+
204
+ refine_f.sort(key=lambda formula: abs(Formula(formula).isotope.mass - M))
205
+ refine_m = [Formula(formula).isotope.mass for formula in refine_f]
206
+
207
+ # Pad refine_f and refine_m with None if necessary
208
+ if len(refine_f) < K:
209
+ refine_f += [None] * (K - len(refine_f))
210
+ refine_m += [None] * (K - len(refine_m))
211
+
212
+ return {'formula': refine_f, 'mass': refine_m}
213
+
214
+
215
+
216
+ if __name__ == "__main__":
217
+ # Post-process settings
218
+ # to check `formula_refinement`: python refine_utils.py --config_path ./config/fiddle_tcn.yml
219
+ parser = argparse.ArgumentParser(description='Mass Spectra to formula (post-process)')
220
+ parser.add_argument('--config_path', type=str, required=True,
221
+ help='Path to configuration (.yml)')
222
+ args = parser.parse_args()
223
+
224
+ # Load configuration
225
+ with open(args.config_path, 'r') as f:
226
+ config = yaml.safe_load(f)
227
+
228
+ refine_atom_type = config['post_processing']['refine_atom_type']
229
+ refine_atom_num = config['post_processing']['refine_atom_num']
230
+ mass_tolerance = config['post_processing']['mass_tolerance']
231
+ ppm_mode = config['post_processing']['ppm_mode']
232
+ top_k = config['post_processing']['top_k']
233
+ maxium_miss_atom_num = config['post_processing']['maxium_miss_atom_num']
234
+ time_out = config['post_processing']['time_out']
235
+
236
+ predicted_formula = ["C6H7O2N2"]
237
+ target_formula = "C5H9NO4"
238
+ compound_mass = Formula(target_formula).isotope.mass
239
+
240
+ print('predicted formulas:', predicted_formula)
241
+ print('target formula: {} ({})'.format(target_formula, compound_mass))
242
+
243
+ result = formula_refinement(predicted_formula, compound_mass,
244
+ mass_tolerance, ppm_mode, top_k, maxium_miss_atom_num, time_out,
245
+ refine_atom_type, refine_atom_num)
246
+ print('restults:', result)
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: msfiddle
3
+ Version: 0.1.0
4
+ Summary: A package for predicting chemical formulas from tandem mass spectra
5
+ Home-page: https://github.com/JosieHong/msfiddle
6
+ Author: Yuhui Hong
7
+ Author-email: josieexception@outlook.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3
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.6
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
+ Dynamic: author
29
+ Dynamic: author-email
30
+ Dynamic: classifier
31
+ Dynamic: description
32
+ Dynamic: description-content-type
33
+ Dynamic: home-page
34
+ Dynamic: license-file
35
+ Dynamic: requires-dist
36
+ Dynamic: requires-python
37
+ Dynamic: summary
38
+
39
+ # msfiddle
40
+
41
+ Source code for the FIDDLE PyPI package featuring:
42
+
43
+ * Chemical formula prediction from MS/MS spectra
44
+ * Formula refinement with confidence score estimation
45
+ * Seamless integration with BUDDY and SIRIUS tools
46
+
47
+ Preprint: https://doi.org/10.1101/2024.11.25.625316
48
+
49
+ For the complete experimenal codes, please visit the GitHub repository: https://github.com/JosieHong/FIDDLE
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ # Install msfiddle (without PyTorch)
55
+ pip install msfiddle
56
+ ```
57
+
58
+ To use `msfiddle`, you need to install `torch>=1.13.0,<2.0.0` separately with the appropriate version for your system. Please refer to the official PyTorch installation guide:
59
+ 🔗 [PyTorch Installation Guide](https://pytorch.org/get-started/previous-versions/#v1130).
60
+
61
+ ## Usage
62
+
63
+ **Step 1**: Download pre-trained models
64
+
65
+ ```bash
66
+ # Download models to the default location (inside the package directory)
67
+ msfiddle-download-models
68
+
69
+ # Or specify a custom location and models
70
+ msfiddle-download-models --destination /path/to/models \
71
+ --models fiddle_tcn_qtof fiddle_fdr_qtof
72
+ ```
73
+
74
+ **Step 2**: Run predictions
75
+
76
+ Using demo data (simplest option)
77
+
78
+ ```bash
79
+ # Run prediction with the built-in demo data
80
+ msfiddle --demo --result_path ./output_demo.csv --device 0
81
+ ```
82
+
83
+ Using your own data:
84
+
85
+ ```bash
86
+ # Run prediction with your data - automatically selects appropriate model
87
+ msfiddle --test_data /path/to/data.mgf \
88
+ --instrument_type orbitrap \
89
+ --result_path /path/to/results.csv \
90
+ --device 0
91
+ ```
92
+
93
+ The `--instrument_type` parameter can be either `orbitrap` (default) or `qtof`.
94
+
95
+ Below is an example of input MS/MS data formatted in `.mgf`. The fields `TITLE`, `PRECURSOR_MZ`, `PRECURSOR_TYPE`, and `COLLISION_ENERGY` are required for msfiddle processing:
96
+
97
+ ```mgf
98
+ BEGIN IONS
99
+ TITLE=EMBL_MCF_2_0_HRMS_Library000529
100
+ PEPMASS=111.02016
101
+ CHARGE=1-
102
+ PRECURSOR_TYPE=[M-H]-
103
+ PRECURSOR_MZ=111.02016
104
+ COLLISION_ENERGY=50.0
105
+ SMILES=[H]c1c([H])n([H])c(=O)n([H])c1=O
106
+ FORMULA=C4H4N2O2
107
+ THEORETICAL_PRECURSOR_MZ=111.019453
108
+ PPM=6.368253318682487
109
+ SIMULATED_PRECURSOR_MZ=111.01946768634916
110
+ 41.0148 0.329893
111
+ 41.9986 89.226766
112
+ 55.8055 0.200544
113
+ 56.2625 0.194617
114
+ 67.0304 0.330612
115
+ 68.0258 0.402906
116
+ 111.0203 100.0
117
+ 112.0515 1.2809
118
+ END IONS
119
+ ```
120
+
121
+ ### Additional Options
122
+
123
+ Show all model paths:
124
+
125
+ ```bash
126
+ msfiddle-checkpoint-paths
127
+ ```
128
+
129
+ Advanced usage with custom paths:
130
+
131
+ ```bash
132
+ msfiddle --test_data /path/to/data.mgf \
133
+ --config_path /path/to/config.yml \
134
+ --resume_path /path/to/tcn_model.pt \
135
+ --fdr_resume_path /path/to/fdr_model.pt \
136
+ --result_path /path/to/results.csv \
137
+ --device 0
138
+ ```
139
+
140
+ ## Citation
141
+
142
+ ```
143
+ @article{hong2024fiddle,
144
+ title={FIDDLE: a deep learning method for chemical formulas prediction from tandem mass spectra},
145
+ author={Hong, Yuhui and Li, Sujun and Ye, Yuzhen and Tang, Haixu},
146
+ journal={bioRxiv},
147
+ pages={2024--11},
148
+ year={2024},
149
+ publisher={Cold Spring Harbor Laboratory}
150
+ }
151
+ ```
@@ -0,0 +1,27 @@
1
+ msfiddle/__init__.py,sha256=9fp9FuPN7dZ5VjAie0hVqY8VtAXFitXUWMD4lb4yjb4,1327
2
+ msfiddle/dataset.py,sha256=Tc8b0JqHk4-PI1LKSxbvL7IKca-aXCCsOkWkpAM9XbQ,5601
3
+ msfiddle/download.py,sha256=sSAw5IWXhoM-0ASJC4ELBY6GPySvoWDPbmNfKKAzD6Q,7314
4
+ msfiddle/main.py,sha256=bUHMJ26PZxMM-eKqyDjNaTfjohFbdEIJWI1epNx0xYg,16832
5
+ msfiddle/model_tcn.py,sha256=zOkC8RY6YFj-BNv69Xq0oKNbXodNoR8hULno_0mRizI,8094
6
+ msfiddle/config/fiddle_tcn_casmi.yml,sha256=UtLialE8V8KuoaO3vd4zEimDnaZfC83JRGcPIP1RylQ,2731
7
+ msfiddle/config/fiddle_tcn_demo.yml,sha256=GieQU_9Mr0ETDTF2o-b1bbyftjfRAZur5_43bBasX7Y,3096
8
+ msfiddle/config/fiddle_tcn_demo_wo_da.yml,sha256=puzdqehyX-EOFlwgSYH0atjPOiquYEG9eGs4FsPO13s,3095
9
+ msfiddle/config/fiddle_tcn_embl.yml,sha256=wTyM8tmkj4R1QG3CRrivtC7tU5MoRZkecpFwq03lDe8,2620
10
+ msfiddle/config/fiddle_tcn_orbitrap.yml,sha256=z6eqKIi6RN6adAtmrxft4KlEAi4bQYxO1g_PDCFjnnA,4645
11
+ msfiddle/config/fiddle_tcn_qtof.yml,sha256=jyvXu1RQlJww7i9vfwm8mIKbB2p_1j_mNXHvT51w4CA,5759
12
+ msfiddle/demo/input_msms.mgf,sha256=ZMSrWib4ruLm7_W5YfzYEQ3VMYCoMKxioznvMcDiQeE,9728
13
+ msfiddle/demo/output_buddy.csv,sha256=U36nN98k9gANWE4PFCmpO6iefl4tD5aI7nByDTGtGzg,1800
14
+ msfiddle/demo/output_fiddle.csv,sha256=LeUJnP7PYFOkBDClXNtL5LkP2JFBrmmVzwpfIoVgaH0,8586
15
+ msfiddle/demo/output_fiddle_all.csv,sha256=zu7E5_W-eQHSZA7ctUIay3Hi0wvY5EhnTMkdYZ98kOc,8524
16
+ msfiddle/demo/output_sirius.csv,sha256=2z0gx_TN7KZYxYz07lmu_qh8GiQuYPktaq2Le38G3dI,1867
17
+ msfiddle/utils/__init__.py,sha256=uSVIp_bUvo8LYG5sxpGuVyrgW1xGOddOJAadCXNfwoU,1125
18
+ msfiddle/utils/mol_utils.py,sha256=GhicKfSNqPxt6qiJGKSCPKW_qobRAwmWdUa_SRW0PFU,2776
19
+ msfiddle/utils/msms_utils.py,sha256=ItiwVxgRNNMWVvN-vXRuJfUHQfqHO6E3jl8BuuGTsRQ,12166
20
+ msfiddle/utils/pkl_utils.py,sha256=nBsUSXwrF1RRSgocT4-EGtqtgJLHsKQ2D8OiETQwKnY,14893
21
+ msfiddle/utils/refine_utils.py,sha256=d5Nr0EHZBwScmnVHcCN0IfbRAFa7U7iTeP5XaCi2L8c,9629
22
+ msfiddle-0.1.0.dist-info/licenses/LICENSE,sha256=T2JIdijrbGLQADQx_qdJIs6PC4By9Ew3NXNb-G5aR0o,34553
23
+ msfiddle-0.1.0.dist-info/METADATA,sha256=KzV47TE3pUcZARESyFkgmlkOgPKia2SO5SBXvRf3JTk,4198
24
+ msfiddle-0.1.0.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
25
+ msfiddle-0.1.0.dist-info/entry_points.txt,sha256=KyPVa2Z8HqvrVfrhfpanoIUyavlgCbe3U1Cnm4ZTCWM,186
26
+ msfiddle-0.1.0.dist-info/top_level.txt,sha256=x3Q69x9woIbchXb1bx6oEug-TdnpT_3FLmvdoXhxaIc,9
27
+ msfiddle-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (77.0.3)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ msfiddle = msfiddle.main:main
3
+ msfiddle-checkpoint-paths = msfiddle.download:print_checkpoint_paths_cli
4
+ msfiddle-download-models = msfiddle.download:download_models_cli