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.
msfiddle/download.py ADDED
@@ -0,0 +1,194 @@
1
+ """
2
+ Module for downloading and locating pre-trained models for msfiddle.
3
+ """
4
+
5
+ import os
6
+ import sys
7
+ import shutil
8
+ import tempfile
9
+ import urllib.request
10
+ import zipfile
11
+ import argparse
12
+ from pathlib import Path
13
+ from tqdm import tqdm
14
+
15
+
16
+ MODEL_URLS = {
17
+ "fiddle_tcn_qtof": "https://github.com/JosieHong/FIDDLE/releases/download/v1.0.0/fiddle_tcn_qtof.zip",
18
+ "fiddle_fdr_qtof": "https://github.com/JosieHong/FIDDLE/releases/download/v1.0.0/fiddle_fdr_qtof.zip",
19
+ "fiddle_tcn_orbitrap": "https://github.com/JosieHong/FIDDLE/releases/download/v1.0.0/fiddle_tcn_orbitrap.zip",
20
+ "fiddle_fdr_orbitrap": "https://github.com/JosieHong/FIDDLE/releases/download/v1.0.0/fiddle_fdr_orbitrap.zip"
21
+ }
22
+
23
+
24
+ class DownloadProgressBar(tqdm):
25
+ """Progress bar for downloads using tqdm."""
26
+
27
+ def update_to(self, b=1, bsize=1, tsize=None):
28
+ if tsize is not None:
29
+ self.total = tsize
30
+ self.update(b * bsize - self.n)
31
+
32
+
33
+ def download_url(url, output_path):
34
+ """Download a file with a progress bar."""
35
+ with DownloadProgressBar(unit='B', unit_scale=True, miniters=1, desc=url.split('/')[-1]) as t:
36
+ urllib.request.urlretrieve(url, filename=output_path, reporthook=t.update_to)
37
+
38
+
39
+ def get_package_dir():
40
+ """Get the directory where the package is installed."""
41
+ import msfiddle
42
+ return os.path.dirname(os.path.abspath(msfiddle.__file__))
43
+
44
+
45
+ def get_checkpoint_dir():
46
+ """Get the directory where model checkpoints are stored."""
47
+ package_dir = get_package_dir()
48
+ return os.path.join(package_dir, "check_point")
49
+
50
+
51
+ def download_models(destination=None, models=None):
52
+ """
53
+ Download pre-trained models for msfiddle.
54
+
55
+ Args:
56
+ destination (str, optional): Path to save the models. If None, uses the package directory.
57
+ models (list, optional): List of model names to download. If None, downloads all models.
58
+ """
59
+ if destination is None:
60
+ destination = get_checkpoint_dir()
61
+ else:
62
+ destination = os.path.abspath(destination)
63
+
64
+ # Create the destination directory if it doesn't exist
65
+ os.makedirs(destination, exist_ok=True)
66
+
67
+ print(f"Models will be downloaded to: {destination}")
68
+
69
+ # Determine which models to download
70
+ if models is None:
71
+ models = list(MODEL_URLS.keys())
72
+
73
+ # Create a temporary directory for downloads
74
+ with tempfile.TemporaryDirectory() as temp_dir:
75
+ for model_name in models:
76
+ if model_name not in MODEL_URLS:
77
+ print(f"Warning: Unknown model '{model_name}', skipping.")
78
+ continue
79
+
80
+ url = MODEL_URLS[model_name]
81
+ zip_path = os.path.join(temp_dir, f"{model_name}.zip")
82
+
83
+ # Download the model
84
+ print(f"Downloading {model_name} model...")
85
+ try:
86
+ download_url(url, zip_path)
87
+ except Exception as e:
88
+ print(f"Error downloading {model_name}: {e}")
89
+ print(f"You can manually download it from {url}")
90
+ continue
91
+
92
+ # Extract the model
93
+ try:
94
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
95
+ zip_ref.extractall(temp_dir)
96
+
97
+ # Move the model file to the destination
98
+ model_file = f"{model_name}.pt"
99
+ src_path = os.path.join(temp_dir, model_file)
100
+ dest_path = os.path.join(destination, model_file)
101
+
102
+ if os.path.exists(src_path):
103
+ shutil.move(src_path, dest_path)
104
+ print(f"Installed {model_file} to {dest_path}")
105
+ else:
106
+ print(f"Warning: Model file {model_file} not found after extraction")
107
+ except Exception as e:
108
+ print(f"Error extracting {model_name}: {e}")
109
+ print(f"You can manually download and extract it from {url}")
110
+
111
+ print("\nDownload complete!")
112
+ print(f"Models are installed in: {destination}")
113
+
114
+
115
+ def check_models_exist():
116
+ """Check if models exist in the package directory."""
117
+ model_dir = get_checkpoint_dir()
118
+
119
+ if not os.path.exists(model_dir):
120
+ return False
121
+
122
+ # Check if all model files exist
123
+ all_exist = True
124
+ for model_name in MODEL_URLS.keys():
125
+ model_path = os.path.join(model_dir, f"{model_name}.pt")
126
+ if not os.path.exists(model_path):
127
+ all_exist = False
128
+ break
129
+
130
+ return all_exist
131
+
132
+
133
+ def get_model_path(model_name):
134
+ """Get the path to a specific model file."""
135
+ model_dir = get_checkpoint_dir()
136
+ return os.path.join(model_dir, f"{model_name}.pt")
137
+
138
+
139
+ def print_checkpoint_paths():
140
+ """Print the checkpoint directory path and available models."""
141
+ checkpoint_dir = get_checkpoint_dir()
142
+ print(f"Checkpoint directory: {checkpoint_dir}")
143
+
144
+ if os.path.exists(checkpoint_dir):
145
+ print("\nAvailable models:")
146
+ for model_name in MODEL_URLS.keys():
147
+ model_path = os.path.join(checkpoint_dir, f"{model_name}.pt")
148
+ if os.path.exists(model_path):
149
+ print(f" [+] {model_name}.pt")
150
+ else:
151
+ print(f" [-] {model_name}.pt (not downloaded)")
152
+ else:
153
+ print("\nCheckpoint directory does not exist. No models are downloaded.")
154
+ print("Run 'msfiddle-download-models' to download the models.")
155
+
156
+
157
+ # Command-line interface for the download_models function
158
+ def download_models_cli():
159
+ parser = argparse.ArgumentParser(description="Download pre-trained models for msfiddle")
160
+ parser.add_argument("-d", "--destination", help="Path to save the models (optional, defaults to package directory)")
161
+ parser.add_argument("-m", "--models", nargs="+", help="Specific model names to download (optional, defaults to all models)")
162
+ args = parser.parse_args()
163
+
164
+ # If destination is not provided, it will be None and download_models will use get_checkpoint_dir()
165
+ download_models(destination=args.destination, models=args.models)
166
+
167
+
168
+ # Command-line interface for the print_checkpoint_paths function
169
+ def print_checkpoint_paths_cli():
170
+ parser = argparse.ArgumentParser(description="Print checkpoint paths and available models")
171
+ parser.parse_args() # This just consumes any arguments passed
172
+ print_checkpoint_paths()
173
+
174
+
175
+ if __name__ == "__main__":
176
+ if len(sys.argv) < 2:
177
+ print("Usage: python download.py [command]")
178
+ print("Commands:")
179
+ print(" print_checkpoint_paths - Print the checkpoint directory path and available models")
180
+ print(" download_models [destination] - Download pre-trained models to the specified destination")
181
+ sys.exit(1)
182
+
183
+ if sys.argv[1] == "print_checkpoint_paths":
184
+ print_checkpoint_paths()
185
+
186
+ elif sys.argv[1] == "download_models":
187
+ # If the first argument is 'download', treat the second as destination
188
+ if len(sys.argv) > 2:
189
+ download_models(destination=sys.argv[2])
190
+ else:
191
+ download_models()
192
+
193
+ else:
194
+ raise ValueError(f"Unknown command: {sys.argv[1]}")
msfiddle/main.py ADDED
@@ -0,0 +1,356 @@
1
+ import os
2
+ import argparse
3
+ from tqdm import tqdm
4
+ import yaml
5
+ import time
6
+ import sys
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+ from collections import OrderedDict
11
+
12
+ import torch
13
+ import torch.nn as nn
14
+ from torch.utils.data import DataLoader
15
+
16
+ from .dataset import MGFDataset
17
+ from .model_tcn import MS2FNet_tcn, FDRNet
18
+ from .utils.mol_utils import vector_to_formula, formula_to_vector
19
+ from .utils.msms_utils import mass_calculator
20
+ from .utils.refine_utils import formula_refinement
21
+ from .download import get_checkpoint_dir, check_models_exist, download_models
22
+
23
+
24
+ def test_step(model, loader, device):
25
+ model.eval()
26
+ spec_ids = []
27
+ y_pred = []
28
+ exp_precursor_mz = []
29
+ exp_precursor_type = []
30
+ mass_pred = []
31
+ atomnum_pred = []
32
+ hcnum_pred = []
33
+ with tqdm(total=len(loader)) as bar:
34
+ for _, batch in enumerate(loader):
35
+ spec_id, exp_pre_type, x, env, neutral_add = batch
36
+ x = x.to(device, dtype=torch.float32)
37
+ env = env.to(device, dtype=torch.float32)
38
+ neutral_add = neutral_add.to(device, dtype=torch.float32)
39
+ exp_pre_mz = env[:, 0]
40
+
41
+ with torch.no_grad():
42
+ _, pred_f, pred_mass, pred_atomnum, pred_hcnum = model(x, env)
43
+ pred_f = pred_f - neutral_add # add the neutral adduct
44
+
45
+ bar.set_description('Eval')
46
+ bar.update(1)
47
+
48
+ spec_ids = spec_ids + list(spec_id)
49
+ y_pred.append(pred_f.detach().cpu())
50
+ exp_precursor_mz.append(exp_pre_mz.detach().cpu())
51
+ exp_precursor_type = exp_precursor_type + list(exp_pre_type)
52
+ mass_pred.append(pred_mass.detach().cpu())
53
+ atomnum_pred.append(pred_atomnum.detach().cpu())
54
+ hcnum_pred.append(pred_hcnum.detach().cpu())
55
+
56
+ y_pred = torch.cat(y_pred, dim = 0)
57
+ exp_precursor_mz = torch.cat(exp_precursor_mz, dim = 0)
58
+ mass_pred = torch.cat(mass_pred, dim = 0)
59
+ atomnum_pred = torch.cat(atomnum_pred, dim = 0)
60
+ hcnum_pred = torch.cat(hcnum_pred, dim = 0)
61
+ return spec_ids, y_pred, exp_precursor_mz, exp_precursor_type, mass_pred, atomnum_pred, hcnum_pred
62
+
63
+
64
+ def rerank_by_fdr(fdr_model, spec, env, refined_results, device, K):
65
+ fdr_model.eval()
66
+
67
+ refine_f = [f for f in refined_results['formula'] if f != None]
68
+ refine_m = [m for m in refined_results['mass'] if m != None]
69
+ if len(refine_f) == 0:
70
+ refined_results['fdr'] = [0.] * K
71
+ return refined_results
72
+
73
+ # convert refine_f (formula strings) to f (formula vectors)
74
+ f = [formula_to_vector(f_str) for f_str in refine_f]
75
+ f = torch.from_numpy(np.array(f))
76
+
77
+ spec = spec.to(device, dtype=torch.float32).repeat(f.size(0), 1)
78
+ env = env.to(device, dtype=torch.float32).repeat(f.size(0), 1)
79
+ f = f.to(device, dtype=torch.float32)
80
+
81
+ with torch.no_grad():
82
+ fdr = fdr_model(spec, env, f)
83
+ fdr = torch.sigmoid(fdr).detach().cpu().numpy()
84
+
85
+ # Create a list of tuples with (fdr_value, refine_f_value, mass_value) pairs
86
+ combined_list = list(zip(fdr, refine_f, refine_m))
87
+ # Sort the combined list based on the fdr_value
88
+ sorted_combined_list = sorted(combined_list, key=lambda x: x[0], reverse=True)
89
+ # Unpack the sorted lists
90
+ sorted_fdr, sorted_refine_f, sorted_mass = zip(*sorted_combined_list)
91
+ sorted_fdr, sorted_refine_f, sorted_mass = list(sorted_fdr), list(sorted_refine_f), list(sorted_mass)
92
+
93
+ # Pad sorted_refine_f and sorted_fdr with None if necessary
94
+ if len(sorted_refine_f) < K:
95
+ sorted_refine_f += [None] * (K - len(sorted_refine_f))
96
+ sorted_fdr += [0.] * (K - len(sorted_fdr))
97
+ sorted_mass += [None] * (K - len(sorted_mass))
98
+
99
+ return {'formula': sorted_refine_f, 'mass': sorted_mass, 'fdr': sorted_fdr}
100
+
101
+
102
+ def init_random_seed(seed):
103
+ np.random.seed(seed)
104
+ torch.manual_seed(seed)
105
+ torch.cuda.manual_seed(seed)
106
+ return
107
+
108
+
109
+ def main():
110
+ parser = argparse.ArgumentParser(description='msfiddle')
111
+
112
+ # Define two exclusive argument groups
113
+ mode_group = parser.add_mutually_exclusive_group(required=True)
114
+ mode_group.add_argument('--demo', action='store_true',
115
+ help='Run prediction on demo data')
116
+ mode_group.add_argument('--test_data', type=str,
117
+ help='Path to data (.mgf)')
118
+
119
+ # Add instrument type option
120
+ parser.add_argument('--instrument_type', type=str, choices=['qtof', 'orbitrap'], default='orbitrap',
121
+ help='Instrument type: qtof or orbitrap (default: orbitrap)')
122
+
123
+ # Add other required arguments
124
+ parser.add_argument('--result_path', type=str, required=True,
125
+ help='Path to save predicted results')
126
+
127
+ # Add optional arguments
128
+ parser.add_argument('--buddy_path', type=str, default='',
129
+ help='Path to saved BUDDY\'s results')
130
+ parser.add_argument('--sirius_path', type=str, default='',
131
+ help='Path to saved SIRIUS\'s results')
132
+ parser.add_argument('--seed', type=int, default=42,
133
+ help='Seed for random functions')
134
+ parser.add_argument('--device', type=int, nargs='+', default=[0],
135
+ help='Which GPUs to use if any (default: [0]). Accepts multiple values separated by space.')
136
+ parser.add_argument('--no_cuda', action='store_true',
137
+ help='Disables CUDA')
138
+
139
+ # Add advanced arguments for custom configurations
140
+ advanced_group = parser.add_argument_group('Advanced options')
141
+ advanced_group.add_argument('--config_path', type=str,
142
+ help='Custom path to configuration (.yaml)')
143
+ advanced_group.add_argument('--resume_path', type=str,
144
+ help='Custom path to pretrained TCN model')
145
+ advanced_group.add_argument('--fdr_resume_path', type=str,
146
+ help='Custom path to pretrained FDR model')
147
+
148
+ args = parser.parse_args()
149
+
150
+ # Initialize random seed
151
+ init_random_seed(args.seed)
152
+ start_time = time.time()
153
+
154
+ # Get package directory
155
+ package_dir = os.path.dirname(os.path.abspath(__file__))
156
+
157
+ # Make sure models are downloaded
158
+ if not check_models_exist():
159
+ print("Pre-trained models not found. Downloading now...")
160
+ download_models()
161
+ print("Models downloaded successfully.")
162
+
163
+ # Set default paths based on mode and instrument type
164
+ if args.demo:
165
+ test_data_path = os.path.join(package_dir, 'demo', 'input_msms.mgf')
166
+ print(f"Using demo data: {test_data_path}")
167
+ else:
168
+ test_data_path = args.test_data
169
+ print(f"Using custom data: {test_data_path}")
170
+
171
+ # Set paths based on instrument type
172
+ instrument_suffix = args.instrument_type
173
+
174
+ # Set config path
175
+ if args.config_path:
176
+ config_path = args.config_path
177
+ else:
178
+ config_path = os.path.join(package_dir, 'config', f'fiddle_tcn_{instrument_suffix}.yml')
179
+ print(f"Using config: {config_path}")
180
+
181
+ # Set model paths
182
+ checkpoint_dir = get_checkpoint_dir()
183
+
184
+ if args.resume_path:
185
+ resume_path = args.resume_path
186
+ else:
187
+ resume_path = os.path.join(checkpoint_dir, f'fiddle_tcn_{instrument_suffix}.pt')
188
+ print(f"Using TCN model: {resume_path}")
189
+
190
+ if args.fdr_resume_path:
191
+ fdr_resume_path = args.fdr_resume_path
192
+ else:
193
+ fdr_resume_path = os.path.join(checkpoint_dir, f'fiddle_fdr_{instrument_suffix}.pt')
194
+ print(f"Using FDR model: {fdr_resume_path}")
195
+
196
+ # Load config
197
+ with open(config_path, 'r') as f:
198
+ config = yaml.load(f, Loader=yaml.FullLoader)
199
+ print('Loaded model & training configuration from {}'.format(config_path))
200
+
201
+ # Set device
202
+ device_1st = torch.device("cuda:" + str(args.device[0])) if torch.cuda.is_available() and not args.no_cuda else torch.device("cpu")
203
+ print(f'Device(s): {args.device}')
204
+
205
+ # 1. Data
206
+ valid_set = MGFDataset(test_data_path, config['encoding'])
207
+ valid_loader = DataLoader(
208
+ valid_set,
209
+ batch_size=1,
210
+ shuffle=False,
211
+ num_workers=0,
212
+ drop_last=True)
213
+
214
+ # 2. Model
215
+ # 2.1 MS2F
216
+ model = MS2FNet_tcn(config['model']).to(device_1st)
217
+ num_params = sum(p.numel() for p in model.parameters())
218
+ print(f'# MS2FNet_tcn Params: {num_params}')
219
+ if len(args.device) > 1: # Wrap the model with nn.DataParallel
220
+ model = nn.DataParallel(model, device_ids=args.device)
221
+
222
+ print('Loading the formula prediction model...')
223
+ state_dict = torch.load(resume_path, map_location=device_1st, weights_only=False)['model_state_dict']
224
+ is_multi_gpu = any(key.startswith('module.') for key in state_dict.keys())
225
+ if is_multi_gpu and len(args.device) == 1: # Convert the model to single GPU
226
+ new_state_dict = OrderedDict()
227
+ for key, value in state_dict.items():
228
+ if key.startswith('module.'):
229
+ new_key = key[7:] # Remove the 'module.' prefix
230
+ new_state_dict[new_key] = value
231
+ else:
232
+ new_state_dict[key] = value
233
+ model.load_state_dict(new_state_dict)
234
+ else:
235
+ model.load_state_dict(state_dict)
236
+
237
+ # 2.2 FDR
238
+ fdr_model = FDRNet(config['model']).to(device_1st)
239
+ num_params = sum(p.numel() for p in fdr_model.parameters())
240
+ print(f'# FDRNet Params: {num_params}')
241
+ if len(args.device) > 1: # Wrap the model with nn.DataParallel
242
+ fdr_model = nn.DataParallel(fdr_model, device_ids=args.device)
243
+
244
+ print('Loading the FDR prediction model...')
245
+ state_dict = torch.load(fdr_resume_path, map_location=device_1st, weights_only=False)['model_state_dict']
246
+ is_multi_gpu = any(key.startswith('module.') for key in state_dict.keys())
247
+ if is_multi_gpu and len(args.device) == 1: # Convert the fdr_model to single GPU
248
+ new_state_dict = OrderedDict()
249
+ for key, value in state_dict.items():
250
+ if key.startswith('module.'):
251
+ new_key = key[7:] # Remove the 'module.' prefix
252
+ new_state_dict[new_key] = value
253
+ else:
254
+ new_state_dict[key] = value
255
+ fdr_model.load_state_dict(new_state_dict)
256
+ else:
257
+ fdr_model.load_state_dict(state_dict)
258
+
259
+ # 3. Formula Prediction
260
+ spec_ids, y_pred, exp_precursor_mz, exp_precursor_type, mass_pred, atomnum_pred, hcnum_pred = test_step(model, valid_loader, device_1st)
261
+
262
+ prediction_time = time.time() - start_time
263
+ prediction_time /= len(valid_set)
264
+
265
+ formula_pred = [vector_to_formula(y) for y in y_pred] # calculate the formula strings
266
+ y_pred = [';'.join(y) for y in y_pred.numpy().astype('str')]
267
+
268
+ spectra = []
269
+ environments = []
270
+ for batch in valid_loader:
271
+ _, _, spec, env, _ = batch
272
+ spectra.append(spec)
273
+ environments.append(env)
274
+
275
+ # 4. Post-processing
276
+ if args.buddy_path != '':
277
+ buddy_df = pd.read_csv(args.buddy_path)
278
+ if args.sirius_path != '':
279
+ sirius_df = pd.read_csv(args.sirius_path)
280
+
281
+ formula_redined = {'Refined Formula ({})'.format(str(k)): [] for k in range(config['post_processing']['top_k'])}
282
+ mass_redined = {'Refined Mass ({})'.format(str(k)): [] for k in range(config['post_processing']['top_k'])}
283
+ fdr_refined = {'FDR ({})'.format(str(k)): [] for k in range(config['post_processing']['top_k'])}
284
+ running_time = []
285
+ exp_mass = []
286
+ # Please note that here we use the experimental precursor m/z, rather than the theoretic precursor m/z.
287
+ for idx, pred_f, exp_pre_mz, exp_pre_type, spec, env in tqdm(zip(spec_ids,
288
+ formula_pred,
289
+ exp_precursor_mz,
290
+ exp_precursor_type, spectra, environments), total=len(exp_precursor_mz), desc='Post'):
291
+ m = mass_calculator(exp_pre_type, exp_pre_mz) # Use experimental precursor m/z and precursor type to calculate molmass
292
+ exp_mass.append(m.item())
293
+
294
+ f0_list = [pred_f]
295
+ if args.buddy_path != '' and len(buddy_df.loc[buddy_df['ID'] == idx]) > 0:
296
+ buddy_f = buddy_df.loc[buddy_df['ID'] == idx].iloc[0][['Pred Formula (1)',
297
+ 'Pred Formula (2)',
298
+ 'Pred Formula (3)',
299
+ 'Pred Formula (4)',
300
+ 'Pred Formula (5)',]].tolist()
301
+ buddy_fdr = buddy_df.loc[buddy_df['ID'] == idx].iloc[0][['BUDDY Score (1)',
302
+ 'BUDDY Score (2)',
303
+ 'BUDDY Score (3)',
304
+ 'BUDDY Score (4)',
305
+ 'BUDDY Score (5)',]].tolist()
306
+ buddy_f = [x for x, fdr in zip(buddy_f, buddy_fdr) if str(x) != 'nan' and fdr < config['post_processing']['buddy_fdr_thr']]
307
+ f0_list.extend(buddy_f)
308
+ if args.sirius_path != '':
309
+ sirius_f = sirius_df.loc[sirius_df['ID'] == idx].iloc[0][['Pred Formula (1)',
310
+ 'Pred Formula (2)',
311
+ 'Pred Formula (3)',
312
+ 'Pred Formula (4)',
313
+ 'Pred Formula (5)',]].tolist()
314
+ sirius_score = sirius_df.loc[sirius_df['ID'] == idx].iloc[0][['SIRIUS Score (1)',
315
+ 'SIRIUS Score (2)',
316
+ 'SIRIUS Score (3)',
317
+ 'SIRIUS Score (4)',
318
+ 'SIRIUS Score (5)',]].tolist()
319
+ sirius_f = [x for x, score in zip(sirius_f, sirius_score) if str(x) != 'nan' and score > config['post_processing']['sirius_score_thr']]
320
+ f0_list.extend(sirius_f)
321
+
322
+ f0_list = list(set(f0_list)) # deduplicates
323
+ start_time = time.time()
324
+ refined_results = formula_refinement(f0_list, m.item(),
325
+ config['post_processing']['mass_tolerance'],
326
+ config['post_processing']['ppm_mode'],
327
+ config['post_processing']['top_k'],
328
+ config['post_processing']['maxium_miss_atom_num'],
329
+ config['post_processing']['time_out'],
330
+ config['post_processing']['refine_atom_type'],
331
+ config['post_processing']['refine_atom_num'],
332
+ )
333
+
334
+ # Rerank the results by predicted FDR
335
+ refined_results = rerank_by_fdr(fdr_model, spec, env, refined_results, device_1st, config['post_processing']['top_k'])
336
+
337
+ for i, (refined_f, refined_m, refined_fdr) in enumerate(zip(refined_results['formula'], refined_results['mass'], refined_results['fdr'])):
338
+ formula_redined[f'Refined Formula ({i})'].append(refined_f)
339
+ mass_redined[f'Refined Mass ({i})'].append(refined_m)
340
+ fdr_refined[f'FDR ({i})'].append(refined_fdr)
341
+ refinement_time = time.time() - start_time
342
+ running_time.append(prediction_time + refinement_time)
343
+
344
+ # 5. Save the final results
345
+ print('\nSaving predicted results...')
346
+ out_dict = {'ID': spec_ids, 'Y Pred': y_pred, 'Mass': exp_mass,
347
+ 'Pred Formula': formula_pred, 'Pred Mass': mass_pred.tolist(),
348
+ 'Pred Atom Num': atomnum_pred.tolist(), 'Pred H/C Num': hcnum_pred.tolist(),
349
+ 'Running Time': running_time}
350
+ res_df = pd.DataFrame({**out_dict, **formula_redined, **mass_redined, **fdr_refined})
351
+ res_df.to_csv(args.result_path, index=False)
352
+ print(f'Done! Results saved to {args.result_path}')
353
+
354
+
355
+ if __name__ == "__main__":
356
+ main()