corems.mass_spectrum.calc.Calibration
Created on Wed May 13 02:16:09 2020
@author: Will Kew
1# -*- coding: utf-8 -*- 2""" 3Created on Wed May 13 02:16:09 2020 4 5@author: Will Kew 6""" 7 8# import modules 9import csv 10import warnings 11from io import BytesIO, StringIO 12from pathlib import Path 13 14import numpy as np 15import pandas as pd 16from s3path import S3Path 17 18# import scipy modules for calibration 19from scipy.optimize import minimize 20 21 22class MzDomainCalibration: 23 """MzDomainCalibration class for recalibrating mass spectra 24 25 Parameters 26 ---------- 27 mass_spectrum : CoreMS MassSpectrum Object 28 The mass spectrum to be calibrated. 29 ref_masslist : str 30 The path to a reference mass list. 31 mzsegment : tuple of floats, optional 32 The mz range to recalibrate, or None. Used for calibration of specific parts of the mz domain at a time. 33 Future work - allow multiple mzsegments to be passed. 34 35 Attributes 36 ---------- 37 mass_spectrum : CoreMS MassSpectrum Object 38 The mass spectrum to be calibrated. 39 mzsegment : tuple of floats or None 40 The mz range to recalibrate, or None. 41 ref_mass_list_path : str or Path 42 The path to the reference mass list. 43 44 Methods 45 ------- 46 * run(). 47 Main function to run this class. 48 * load_ref_mass_list(). 49 Load reference mass list (Bruker format). 50 * gen_ref_mass_list_from_assigned(min_conf=0.7). 51 Generate reference mass list from assigned masses. 52 * find_calibration_points(df_ref, calib_ppm_error_threshold=(-1, 1), calib_snr_threshold=5). 53 Find calibration points in the mass spectrum based on the reference mass list. 54 * robust_calib(param, cal_peaks_mz, cal_refs_mz, order=1). 55 Recalibration function. 56 * recalibrate_mass_spectrum(cal_peaks_mz, cal_refs_mz, order=1, diagnostic=False). 57 Main recalibration function which uses a robust linear regression. 58 59 60 """ 61 62 def __init__(self, mass_spectrum, ref_masslist, mzsegment=None): 63 self.mass_spectrum = mass_spectrum 64 self.mzsegment = mzsegment 65 66 # define reference mass list - bruker .ref format 67 self.ref_mass_list_path = ref_masslist 68 if self.mass_spectrum.percentile_assigned(mute_output=True)[0] != 0: 69 warnings.warn( 70 "Warning: calibrating spectra which have already been assigned may yield erroneous results" 71 ) 72 self.mass_spectrum.mz_cal = self.mass_spectrum.mz_exp 73 self.mass_spectrum.mz_cal_profile = self.mass_spectrum._mz_exp 74 75 if self.mass_spectrum.parameters.mass_spectrum.verbose_processing: 76 print( 77 "MS Obj loaded - " + str(len(mass_spectrum.mspeaks)) + " peaks found." 78 ) 79 80 def load_ref_mass_list(self): 81 """ 82 Load reference mass list (Bruker format). 83 84 Loads in a reference mass list from a .ref file. Some versions of 85 Bruker's software produce .ref files with a different format where 86 the header lines (starting with '#' or '##') and delimiters may vary. 87 The file may be located locally or on S3 and will be handled accordingly. 88 89 Returns 90 ------- 91 df_ref : Pandas DataFrame 92 Reference mass list object. 93 """ 94 # Get a Path-like object from the input path string or S3Path 95 refmasslist = ( 96 Path(self.ref_mass_list_path) 97 if isinstance(self.ref_mass_list_path, str) 98 else self.ref_mass_list_path 99 ) 100 101 # Make sure the file exists 102 if not refmasslist.exists(): 103 raise FileExistsError("File does not exist: %s" % refmasslist) 104 105 # Read all lines from the file (handling S3 vs local differently) 106 if isinstance(refmasslist, S3Path): 107 # For S3, read the file in binary, then decode to string and split into lines. 108 content = refmasslist.open("rb").read() 109 all_lines = content.decode("utf-8").splitlines(keepends=True) 110 else: 111 # For a local file, open in text mode and read lines. 112 with refmasslist.open("r") as f: 113 all_lines = f.readlines() 114 115 # Identify the index of the first line of the actual data. 116 # We assume header lines start with '#' (or '##') and ignore blank lines. 117 data_start_idx = 0 118 for idx, line in enumerate(all_lines): 119 if line.strip() and not line.lstrip().startswith("#"): 120 data_start_idx = idx 121 break 122 123 # If there are not enough lines to guess the dialect, throw an error 124 if data_start_idx >= len(all_lines): 125 raise ValueError("The file does not appear to contain any data lines.") 126 127 # Use a couple of the data lines to let csv.Sniffer detect the delimiter 128 sample_lines = "".join(all_lines[data_start_idx : data_start_idx + 2]) 129 try: 130 dialect = csv.Sniffer().sniff(sample_lines) 131 delimiter = dialect.delimiter 132 except csv.Error: 133 # If csv.Sniffer fails, default to a common delimiter (e.g., comma) 134 delimiter = "," 135 136 # Join the lines from the beginning of data (this might include a blank line) 137 joined_data = "".join(all_lines[data_start_idx:]) 138 139 # Depending on whether the file is S3 or local, wrap the data as needed for pandas 140 if isinstance(refmasslist, S3Path): 141 data_buffer = BytesIO(joined_data.encode("utf-8")) 142 else: 143 data_buffer = StringIO(joined_data) 144 145 # Read data into a DataFrame. 146 # Adjust columns and names as needed – here we assume at least 2 columns: 147 df_ref = pd.read_csv( 148 data_buffer, 149 sep=delimiter, 150 header=None, 151 usecols=[0, 1], # Modify if more columns are required. 152 names=["Formula", "m/z"], 153 ) 154 155 df_ref.sort_values(by="m/z", ascending=True, inplace=True) 156 157 if self.mass_spectrum.parameters.mass_spectrum.verbose_processing: 158 print( 159 "Reference mass list loaded - {} calibration masses loaded.".format( 160 len(df_ref) 161 ) 162 ) 163 164 return df_ref 165 166 def gen_ref_mass_list_from_assigned(self, min_conf: float = 0.7): 167 """Generate reference mass list from assigned masses 168 169 This function will generate a ref mass dataframe object from an assigned corems mass spec obj 170 using assigned masses above a certain minimum confidence threshold. 171 172 This function needs to be retested and check it is covered in the unit tests. 173 174 Parameters 175 ---------- 176 min_conf : float, optional 177 minimum confidence score. The default is 0.7. 178 179 Returns 180 ------- 181 df_ref : Pandas DataFrame 182 reference mass list - based on calculated masses. 183 184 """ 185 # TODO this function needs to be retested and check it is covered in the unit tests 186 df = self.mass_spectrum.to_dataframe() 187 df = df[df["Confidence Score"] > min_conf] 188 df_ref = pd.DataFrame(columns=["m/z"]) 189 df_ref["m/z"] = df["Calculated m/z"] 190 if self.mass_spectrum.parameters.mass_spectrum.verbose_processing: 191 print( 192 "Reference mass list generated - " 193 + str(len(df_ref)) 194 + " calibration masses." 195 ) 196 return df_ref 197 198 def find_calibration_points( 199 self, 200 df_ref, 201 calib_ppm_error_threshold: tuple[float, float] = (-1, 1), 202 calib_snr_threshold: float = 5, 203 calibration_ref_match_method: str = "merged", 204 calibration_ref_match_tolerance: float = 0.003, 205 calibration_ref_match_std_raw_error_limit: float = 1.5, 206 ): 207 """Function to find calibration points in the mass spectrum 208 209 Based on the reference mass list. 210 211 Parameters 212 ---------- 213 df_ref : Pandas DataFrame 214 reference mass list for recalibration. 215 calib_ppm_error_threshold : tuple of floats, optional 216 ppm error for finding calibration masses in the spectrum. The default is -1,1. 217 Note: This is based on the calculation of ppm = ((mz_measure - mz_theoretical)/mz_theoretical)*1e6. 218 Some software does this the other way around and value signs must be inverted for that to work. 219 calib_snr_threshold : float, optional 220 snr threshold for finding calibration masses in the spectrum. The default is 5. 221 If SNR data is unavailable, peaks are filtered by intensity percentile using the formula: 222 percentile = max(5, 100 - calib_snr_threshold) 223 calibration_ref_match_method : str, optional 224 method for matching calibration references. The default is "merged". 225 calibration_ref_match_tolerance : float, optional 226 tolerance for matching calibration references. The default is 0.003. 227 calibration_ref_match_std_raw_error_limit : float, optional 228 standard deviation raw error limit for calibration references. The default is 1.5. 229 230 Returns 231 ------- 232 cal_peaks_mz : list of floats 233 masses of measured ions to use in calibration routine 234 cal_refs_mz : list of floats 235 reference mz values of found calibration points. 236 237 """ 238 239 # Check if SNR data is available by testing the first peak 240 use_snr = False 241 if len(self.mass_spectrum.mspeaks) > 0: 242 first_peak = self.mass_spectrum.mspeaks[0] 243 if ( 244 hasattr(first_peak, "signal_to_noise") 245 and first_peak.signal_to_noise is not None 246 and not np.isnan(first_peak.signal_to_noise) 247 and first_peak.signal_to_noise > 0 248 ): 249 use_snr = True 250 251 # This approach is much more efficient and expedient than the original implementation. 252 peaks_mz = [] 253 peaks_intensity = [] 254 255 if use_snr: 256 # Use SNR filtering 257 for x in self.mass_spectrum.mspeaks: 258 if x.signal_to_noise > calib_snr_threshold: 259 if self.mzsegment: 260 if min(self.mzsegment) <= x.mz_exp <= max(self.mzsegment): 261 peaks_mz.append(x.mz_exp) 262 else: 263 peaks_mz.append(x.mz_exp) 264 else: 265 # Fallback to intensity percentile filtering 266 intensity_percentile = max(5, 100 - calib_snr_threshold) 267 warnings.warn( 268 f"SNR data unavailable for calibration. Using intensity-based filtering instead. " 269 f"SNR threshold of {calib_snr_threshold} corresponds to intensity percentile >= {intensity_percentile}%." 270 ) 271 272 # Collect all peaks and their intensities 273 all_peaks_data = [] 274 for x in self.mass_spectrum.mspeaks: 275 if self.mzsegment: 276 if min(self.mzsegment) <= x.mz_exp <= max(self.mzsegment): 277 all_peaks_data.append((x.mz_exp, x.abundance)) 278 else: 279 all_peaks_data.append((x.mz_exp, x.abundance)) 280 281 if all_peaks_data: 282 peaks_mz_list, intensities = zip(*all_peaks_data) 283 intensity_threshold = np.percentile(intensities, intensity_percentile) 284 285 for mz, intensity in all_peaks_data: 286 if intensity >= intensity_threshold: 287 peaks_mz.append(mz) 288 289 peaks_mz = np.asarray(peaks_mz) 290 291 if calibration_ref_match_method == "legacy": 292 # This legacy approach iterates through each reference match and finds the entries within 1 mz and within the user defined PPM error threshold 293 # Then it removes ambiguities - which means the calibration threshold hasto be very tight. 294 cal_peaks_mz = [] 295 cal_refs_mz = [] 296 for mzref in df_ref["m/z"]: 297 tmp_peaks_mz = peaks_mz[abs(peaks_mz - mzref) < 1] 298 for mzmeas in tmp_peaks_mz: 299 delta_mass = ((mzmeas - mzref) / mzref) * 1e6 300 if delta_mass < max(calib_ppm_error_threshold): 301 if delta_mass > min(calib_ppm_error_threshold): 302 cal_peaks_mz.append(mzmeas) 303 cal_refs_mz.append(mzref) 304 305 # To remove entries with duplicated indices (reference masses matching multiple peaks) 306 tmpdf = pd.Series(index=cal_refs_mz, data=cal_peaks_mz, dtype=float) 307 tmpdf = tmpdf[~tmpdf.index.duplicated(keep=False)] 308 309 cal_peaks_mz = list(tmpdf.to_numpy()) 310 cal_refs_mz = list(tmpdf.index) 311 elif calibration_ref_match_method == "merged": 312 # This is a new approach (August 2024) which uses Pandas 'merged_asof' to find the peaks closest in m/z between 313 # reference and measured masses. This is a quicker way to match, and seems to get more matches. 314 # It may not work as well when the data are far from correc initial mass 315 # e.g. if the correct peak is further from the reference than an incorrect peak. 316 meas_df = pd.DataFrame(columns=["meas_m/z"], data=peaks_mz) 317 tolerance = calibration_ref_match_tolerance 318 merged_df = pd.merge_asof( 319 df_ref, 320 meas_df, 321 left_on="m/z", 322 right_on="meas_m/z", 323 tolerance=tolerance, 324 direction="nearest", 325 ) 326 merged_df.dropna(how="any", inplace=True) 327 merged_df["Error_ppm"] = ( 328 (merged_df["meas_m/z"] - merged_df["m/z"]) / merged_df["m/z"] 329 ) * 1e6 330 median_raw_error = merged_df["Error_ppm"].median() 331 std_raw_error = merged_df["Error_ppm"].std() 332 if std_raw_error > calibration_ref_match_std_raw_error_limit: 333 std_raw_error = calibration_ref_match_std_raw_error_limit 334 self.mass_spectrum.calibration_raw_error_median = median_raw_error 335 self.mass_spectrum.calibration_raw_error_stdev = std_raw_error 336 merged_df = merged_df[ 337 (merged_df["Error_ppm"] > (median_raw_error - 1.5 * std_raw_error)) 338 & (merged_df["Error_ppm"] < (median_raw_error + 1.5 * std_raw_error)) 339 ] 340 # merged_df= merged_df[(merged_df['Error_ppm']>min(calib_ppm_error_threshold))&(merged_df['Error_ppm']<max(calib_ppm_error_threshold))] 341 cal_peaks_mz = list(merged_df["meas_m/z"]) 342 cal_refs_mz = list(merged_df["m/z"]) 343 else: 344 raise ValueError(f"{calibration_ref_match_method} not allowed.") 345 346 # it is crucial the mass lists are in same order 347 # corems likes to do masses from high to low. 348 cal_refs_mz.sort(reverse=False) 349 cal_peaks_mz.sort(reverse=False) 350 if self.mass_spectrum.parameters.mass_spectrum.verbose_processing: 351 print( 352 str(len(cal_peaks_mz)) 353 + " calibration points matched within thresholds." 354 ) 355 return cal_peaks_mz, cal_refs_mz 356 357 def robust_calib( 358 self, 359 param: list[float], 360 cal_peaks_mz: list[float], 361 cal_refs_mz: list[float], 362 order: int = 1, 363 ): 364 """Recalibration function 365 366 Computes the rms of m/z errors to minimize when calibrating. 367 This is adapted from from spike. 368 369 Parameters 370 ---------- 371 param : list of floats 372 generated by minimize function from scipy optimize. 373 cal_peaks_mz : list of floats 374 masses of measured peaks to use in mass calibration. 375 cal_peaks_mz : list of floats 376 reference mz values of found calibration points. 377 order : int, optional 378 order of the recalibration function. 1 = linear, 2 = quadratic. The default is 1. 379 380 Returns 381 ------- 382 rmserror : float 383 root mean square mass error for calibration points. 384 385 """ 386 Aterm = param[0] 387 Bterm = param[1] 388 try: 389 Cterm = param[2] 390 except IndexError: 391 pass 392 393 # get the mspeaks from the mass spectrum object which were calibration points 394 # mspeaks = [self.mass_spectrum.mspeaks[x] for x in imzmeas] 395 # get their calibrated mass values 396 # mspeakmzs = [x.mz_cal for x in mspeaks] 397 cal_peaks_mz = np.asarray(cal_peaks_mz) 398 399 # linearz 400 if order == 1: 401 ref_recal_points = (Aterm * cal_peaks_mz) + Bterm 402 # quadratic 403 elif order == 2: 404 ref_recal_points = (Aterm * (cal_peaks_mz)) + ( 405 Bterm * np.power((cal_peaks_mz), 2) + Cterm 406 ) 407 408 # sort both the calibration points (measured, recalibrated) 409 ref_recal_points.sort() 410 # and sort the calibration points (theoretical, predefined) 411 cal_refs_mz.sort() 412 413 # calculate the ppm error for each calibration point 414 error = ((ref_recal_points - cal_refs_mz) / cal_refs_mz) * 1e6 415 # calculate the root mean square error - this is our target to minimize 416 rmserror = np.sqrt(np.mean(error**2)) 417 return rmserror 418 419 def recalibrate_mass_spectrum( 420 self, 421 cal_peaks_mz: list[float], 422 cal_refs_mz: list[float], 423 order: int = 1, 424 diagnostic: bool = False, 425 ): 426 """Main recalibration function which uses a robust linear regression 427 428 This function performs the recalibration of the mass spectrum object. 429 It iteratively applies 430 431 Parameters 432 ---------- 433 cal_peaks_mz : list of float 434 masses of measured peaks to use in mass calibration. 435 cal_refs_mz : list of float 436 reference mz values of found calibration points. 437 order : int, optional 438 order of the recalibration function. 1 = linear, 2 = quadratic. The default is 1. 439 440 Returns 441 ------- 442 mass_spectrum : CoreMS mass spectrum object 443 Calibrated mass spectrum object 444 445 446 Notes 447 ----- 448 This function is adapted, in part, from the SPIKE project [1,2] and is based on the robust linear regression method. 449 450 References 451 ---------- 452 1. Chiron L., Coutouly M-A., Starck J-P., Rolando C., Delsuc M-A. 453 SPIKE a Processing Software dedicated to Fourier Spectroscopies 454 https://arxiv.org/abs/1608.06777 (2016) 455 2. SPIKE - https://github.com/spike-project/spike 456 457 """ 458 # initialise parameters for recalibration 459 # these are the 'Aterm, Bterm, Cterm' 460 # as spectra are already freq->mz calibrated, these terms are very small 461 # may be beneficial to formally separate them from the freq->mz terms 462 if order == 1: 463 Po = [1, 0] 464 elif order == 2: 465 Po = [1, 0, 0] 466 467 if len(cal_peaks_mz) >= 2: 468 if self.mzsegment: # If only part of the spectrum is to be recalibrated 469 mz_exp_peaks = np.array( 470 [mspeak.mz_exp for mspeak in self.mass_spectrum] 471 ) 472 # Split the array into two parts - one to recailbrate, one to keep unchanged. 473 mz_exp_peaks_tocal = mz_exp_peaks[ 474 (mz_exp_peaks >= min(self.mzsegment)) 475 & (mz_exp_peaks <= max(self.mzsegment)) 476 ] 477 mz_exp_peaks_unchanged = mz_exp_peaks[ 478 ~(mz_exp_peaks >= min(self.mzsegment)) 479 | ~(mz_exp_peaks <= max(self.mzsegment)) 480 ] 481 # TODO: - segmented calibration needs a way to better track the calibration args/values... 482 if not self.mass_spectrum.is_centroid: 483 mz_exp_profile = np.array(self.mass_spectrum.mz_exp_profile) 484 # Split the array into two parts - one to recailbrate, one to keep unchanged. 485 mz_exp_profile_tocal = mz_exp_profile[ 486 (mz_exp_profile >= min(self.mzsegment)) 487 & (mz_exp_profile <= max(self.mzsegment)) 488 ] 489 mz_exp_profile_unchanged = mz_exp_profile[ 490 ~(mz_exp_profile >= min(self.mzsegment)) 491 | ~(mz_exp_profile <= max(self.mzsegment)) 492 ] 493 else: # if just recalibrating the whole spectrum 494 mz_exp_peaks_tocal = np.array( 495 [mspeak.mz_exp for mspeak in self.mass_spectrum] 496 ) 497 if not self.mass_spectrum.is_centroid: 498 mz_exp_profile_tocal = np.array(self.mass_spectrum.mz_exp_profile) 499 500 minimize_method = self.mass_spectrum.settings.calib_minimize_method 501 res = minimize( 502 self.robust_calib, 503 Po, 504 args=(cal_peaks_mz, cal_refs_mz, order), 505 method=minimize_method, 506 ) 507 if self.mass_spectrum.parameters.mass_spectrum.verbose_processing: 508 print( 509 "minimize function completed with RMS error of: {:0.3f} ppm".format( 510 res["fun"] 511 ) 512 ) 513 print( 514 "minimize function performed {:1d} fn evals and {:1d} iterations".format( 515 res["nfev"], res["nit"] 516 ) 517 ) 518 Pn = res.x 519 520 # mz_exp_ms = np.array([mspeak.mz_exp for mspeak in self.mass_spectrum]) 521 522 if order == 1: 523 mz_domain = (Pn[0] * mz_exp_peaks_tocal) + Pn[1] 524 if not self.mass_spectrum.is_centroid: 525 mz_profile_calc = (Pn[0] * mz_exp_profile_tocal) + Pn[1] 526 527 elif order == 2: 528 mz_domain = (Pn[0] * (mz_exp_peaks_tocal)) + ( 529 Pn[1] * np.power((mz_exp_peaks_tocal), 2) + Pn[2] 530 ) 531 532 if not self.mass_spectrum.is_centroid: 533 mz_profile_calc = (Pn[0] * (mz_exp_profile_tocal)) + ( 534 Pn[1] * np.power((mz_exp_profile_tocal), 2) + Pn[2] 535 ) 536 537 if self.mzsegment: 538 # Recombine the mass domains 539 mz_domain = np.concatenate([mz_domain, mz_exp_peaks_unchanged]) 540 mz_domain.sort() 541 if not self.mass_spectrum.is_centroid: 542 mz_profile_calc = np.concatenate( 543 [mz_profile_calc, mz_exp_profile_unchanged] 544 ) 545 mz_profile_calc.sort() 546 # Sort them 547 if ( 548 mz_exp_peaks[0] > mz_exp_peaks[1] 549 ): # If originally descending mass order 550 mz_domain = mz_domain[::-1] 551 if not self.mass_spectrum.is_centroid: 552 mz_profile_calc = mz_profile_calc[::-1] 553 554 self.mass_spectrum.mz_cal = mz_domain 555 if not self.mass_spectrum.is_centroid: 556 self.mass_spectrum.mz_cal_profile = mz_profile_calc 557 558 self.mass_spectrum.calibration_order = order 559 self.mass_spectrum.calibration_RMS = float(res["fun"]) 560 self.mass_spectrum.calibration_points = int(len(cal_refs_mz)) 561 self.mass_spectrum.calibration_ref_mzs = cal_refs_mz 562 self.mass_spectrum.calibration_meas_mzs = cal_peaks_mz 563 564 self.mass_spectrum.calibration_segment = self.mzsegment 565 566 if diagnostic: 567 return self.mass_spectrum, res 568 return self.mass_spectrum 569 else: 570 warnings.warn("Too few calibration points - aborting.") 571 return self.mass_spectrum 572 573 def run(self): 574 """Run the calibration routine 575 576 This function runs the calibration routine. 577 578 """ 579 calib_snr_threshold = self.mass_spectrum.settings.calib_sn_threshold 580 max_calib_ppm_error = self.mass_spectrum.settings.max_calib_ppm_error 581 min_calib_ppm_error = self.mass_spectrum.settings.min_calib_ppm_error 582 calib_pol_order = self.mass_spectrum.settings.calib_pol_order 583 calibration_ref_match_method = ( 584 self.mass_spectrum.settings.calibration_ref_match_method 585 ) 586 calibration_ref_match_tolerance = ( 587 self.mass_spectrum.settings.calibration_ref_match_tolerance 588 ) 589 calibration_ref_match_std_raw_error_limit = ( 590 self.mass_spectrum.settings.calibration_ref_match_std_raw_error_limit 591 ) 592 593 # load reference mass list 594 df_ref = self.load_ref_mass_list() 595 596 # find calibration points 597 cal_peaks_mz, cal_refs_mz = self.find_calibration_points( 598 df_ref, 599 calib_ppm_error_threshold=(min_calib_ppm_error, max_calib_ppm_error), 600 calib_snr_threshold=calib_snr_threshold, 601 calibration_ref_match_method=calibration_ref_match_method, 602 calibration_ref_match_tolerance=calibration_ref_match_tolerance, 603 calibration_ref_match_std_raw_error_limit=calibration_ref_match_std_raw_error_limit, 604 ) 605 if len(cal_peaks_mz) == 2: 606 self.mass_spectrum.settings.calib_pol_order = 1 607 calib_pol_order = 1 608 if self.mass_spectrum.parameters.mass_spectrum.verbose_processing: 609 print("Only 2 calibration points found, forcing a linear recalibration") 610 elif len(cal_peaks_mz) < 2: 611 warnings.warn("Too few calibration points found, function will fail") 612 self.recalibrate_mass_spectrum(cal_peaks_mz, cal_refs_mz, order=calib_pol_order)
23class MzDomainCalibration: 24 """MzDomainCalibration class for recalibrating mass spectra 25 26 Parameters 27 ---------- 28 mass_spectrum : CoreMS MassSpectrum Object 29 The mass spectrum to be calibrated. 30 ref_masslist : str 31 The path to a reference mass list. 32 mzsegment : tuple of floats, optional 33 The mz range to recalibrate, or None. Used for calibration of specific parts of the mz domain at a time. 34 Future work - allow multiple mzsegments to be passed. 35 36 Attributes 37 ---------- 38 mass_spectrum : CoreMS MassSpectrum Object 39 The mass spectrum to be calibrated. 40 mzsegment : tuple of floats or None 41 The mz range to recalibrate, or None. 42 ref_mass_list_path : str or Path 43 The path to the reference mass list. 44 45 Methods 46 ------- 47 * run(). 48 Main function to run this class. 49 * load_ref_mass_list(). 50 Load reference mass list (Bruker format). 51 * gen_ref_mass_list_from_assigned(min_conf=0.7). 52 Generate reference mass list from assigned masses. 53 * find_calibration_points(df_ref, calib_ppm_error_threshold=(-1, 1), calib_snr_threshold=5). 54 Find calibration points in the mass spectrum based on the reference mass list. 55 * robust_calib(param, cal_peaks_mz, cal_refs_mz, order=1). 56 Recalibration function. 57 * recalibrate_mass_spectrum(cal_peaks_mz, cal_refs_mz, order=1, diagnostic=False). 58 Main recalibration function which uses a robust linear regression. 59 60 61 """ 62 63 def __init__(self, mass_spectrum, ref_masslist, mzsegment=None): 64 self.mass_spectrum = mass_spectrum 65 self.mzsegment = mzsegment 66 67 # define reference mass list - bruker .ref format 68 self.ref_mass_list_path = ref_masslist 69 if self.mass_spectrum.percentile_assigned(mute_output=True)[0] != 0: 70 warnings.warn( 71 "Warning: calibrating spectra which have already been assigned may yield erroneous results" 72 ) 73 self.mass_spectrum.mz_cal = self.mass_spectrum.mz_exp 74 self.mass_spectrum.mz_cal_profile = self.mass_spectrum._mz_exp 75 76 if self.mass_spectrum.parameters.mass_spectrum.verbose_processing: 77 print( 78 "MS Obj loaded - " + str(len(mass_spectrum.mspeaks)) + " peaks found." 79 ) 80 81 def load_ref_mass_list(self): 82 """ 83 Load reference mass list (Bruker format). 84 85 Loads in a reference mass list from a .ref file. Some versions of 86 Bruker's software produce .ref files with a different format where 87 the header lines (starting with '#' or '##') and delimiters may vary. 88 The file may be located locally or on S3 and will be handled accordingly. 89 90 Returns 91 ------- 92 df_ref : Pandas DataFrame 93 Reference mass list object. 94 """ 95 # Get a Path-like object from the input path string or S3Path 96 refmasslist = ( 97 Path(self.ref_mass_list_path) 98 if isinstance(self.ref_mass_list_path, str) 99 else self.ref_mass_list_path 100 ) 101 102 # Make sure the file exists 103 if not refmasslist.exists(): 104 raise FileExistsError("File does not exist: %s" % refmasslist) 105 106 # Read all lines from the file (handling S3 vs local differently) 107 if isinstance(refmasslist, S3Path): 108 # For S3, read the file in binary, then decode to string and split into lines. 109 content = refmasslist.open("rb").read() 110 all_lines = content.decode("utf-8").splitlines(keepends=True) 111 else: 112 # For a local file, open in text mode and read lines. 113 with refmasslist.open("r") as f: 114 all_lines = f.readlines() 115 116 # Identify the index of the first line of the actual data. 117 # We assume header lines start with '#' (or '##') and ignore blank lines. 118 data_start_idx = 0 119 for idx, line in enumerate(all_lines): 120 if line.strip() and not line.lstrip().startswith("#"): 121 data_start_idx = idx 122 break 123 124 # If there are not enough lines to guess the dialect, throw an error 125 if data_start_idx >= len(all_lines): 126 raise ValueError("The file does not appear to contain any data lines.") 127 128 # Use a couple of the data lines to let csv.Sniffer detect the delimiter 129 sample_lines = "".join(all_lines[data_start_idx : data_start_idx + 2]) 130 try: 131 dialect = csv.Sniffer().sniff(sample_lines) 132 delimiter = dialect.delimiter 133 except csv.Error: 134 # If csv.Sniffer fails, default to a common delimiter (e.g., comma) 135 delimiter = "," 136 137 # Join the lines from the beginning of data (this might include a blank line) 138 joined_data = "".join(all_lines[data_start_idx:]) 139 140 # Depending on whether the file is S3 or local, wrap the data as needed for pandas 141 if isinstance(refmasslist, S3Path): 142 data_buffer = BytesIO(joined_data.encode("utf-8")) 143 else: 144 data_buffer = StringIO(joined_data) 145 146 # Read data into a DataFrame. 147 # Adjust columns and names as needed – here we assume at least 2 columns: 148 df_ref = pd.read_csv( 149 data_buffer, 150 sep=delimiter, 151 header=None, 152 usecols=[0, 1], # Modify if more columns are required. 153 names=["Formula", "m/z"], 154 ) 155 156 df_ref.sort_values(by="m/z", ascending=True, inplace=True) 157 158 if self.mass_spectrum.parameters.mass_spectrum.verbose_processing: 159 print( 160 "Reference mass list loaded - {} calibration masses loaded.".format( 161 len(df_ref) 162 ) 163 ) 164 165 return df_ref 166 167 def gen_ref_mass_list_from_assigned(self, min_conf: float = 0.7): 168 """Generate reference mass list from assigned masses 169 170 This function will generate a ref mass dataframe object from an assigned corems mass spec obj 171 using assigned masses above a certain minimum confidence threshold. 172 173 This function needs to be retested and check it is covered in the unit tests. 174 175 Parameters 176 ---------- 177 min_conf : float, optional 178 minimum confidence score. The default is 0.7. 179 180 Returns 181 ------- 182 df_ref : Pandas DataFrame 183 reference mass list - based on calculated masses. 184 185 """ 186 # TODO this function needs to be retested and check it is covered in the unit tests 187 df = self.mass_spectrum.to_dataframe() 188 df = df[df["Confidence Score"] > min_conf] 189 df_ref = pd.DataFrame(columns=["m/z"]) 190 df_ref["m/z"] = df["Calculated m/z"] 191 if self.mass_spectrum.parameters.mass_spectrum.verbose_processing: 192 print( 193 "Reference mass list generated - " 194 + str(len(df_ref)) 195 + " calibration masses." 196 ) 197 return df_ref 198 199 def find_calibration_points( 200 self, 201 df_ref, 202 calib_ppm_error_threshold: tuple[float, float] = (-1, 1), 203 calib_snr_threshold: float = 5, 204 calibration_ref_match_method: str = "merged", 205 calibration_ref_match_tolerance: float = 0.003, 206 calibration_ref_match_std_raw_error_limit: float = 1.5, 207 ): 208 """Function to find calibration points in the mass spectrum 209 210 Based on the reference mass list. 211 212 Parameters 213 ---------- 214 df_ref : Pandas DataFrame 215 reference mass list for recalibration. 216 calib_ppm_error_threshold : tuple of floats, optional 217 ppm error for finding calibration masses in the spectrum. The default is -1,1. 218 Note: This is based on the calculation of ppm = ((mz_measure - mz_theoretical)/mz_theoretical)*1e6. 219 Some software does this the other way around and value signs must be inverted for that to work. 220 calib_snr_threshold : float, optional 221 snr threshold for finding calibration masses in the spectrum. The default is 5. 222 If SNR data is unavailable, peaks are filtered by intensity percentile using the formula: 223 percentile = max(5, 100 - calib_snr_threshold) 224 calibration_ref_match_method : str, optional 225 method for matching calibration references. The default is "merged". 226 calibration_ref_match_tolerance : float, optional 227 tolerance for matching calibration references. The default is 0.003. 228 calibration_ref_match_std_raw_error_limit : float, optional 229 standard deviation raw error limit for calibration references. The default is 1.5. 230 231 Returns 232 ------- 233 cal_peaks_mz : list of floats 234 masses of measured ions to use in calibration routine 235 cal_refs_mz : list of floats 236 reference mz values of found calibration points. 237 238 """ 239 240 # Check if SNR data is available by testing the first peak 241 use_snr = False 242 if len(self.mass_spectrum.mspeaks) > 0: 243 first_peak = self.mass_spectrum.mspeaks[0] 244 if ( 245 hasattr(first_peak, "signal_to_noise") 246 and first_peak.signal_to_noise is not None 247 and not np.isnan(first_peak.signal_to_noise) 248 and first_peak.signal_to_noise > 0 249 ): 250 use_snr = True 251 252 # This approach is much more efficient and expedient than the original implementation. 253 peaks_mz = [] 254 peaks_intensity = [] 255 256 if use_snr: 257 # Use SNR filtering 258 for x in self.mass_spectrum.mspeaks: 259 if x.signal_to_noise > calib_snr_threshold: 260 if self.mzsegment: 261 if min(self.mzsegment) <= x.mz_exp <= max(self.mzsegment): 262 peaks_mz.append(x.mz_exp) 263 else: 264 peaks_mz.append(x.mz_exp) 265 else: 266 # Fallback to intensity percentile filtering 267 intensity_percentile = max(5, 100 - calib_snr_threshold) 268 warnings.warn( 269 f"SNR data unavailable for calibration. Using intensity-based filtering instead. " 270 f"SNR threshold of {calib_snr_threshold} corresponds to intensity percentile >= {intensity_percentile}%." 271 ) 272 273 # Collect all peaks and their intensities 274 all_peaks_data = [] 275 for x in self.mass_spectrum.mspeaks: 276 if self.mzsegment: 277 if min(self.mzsegment) <= x.mz_exp <= max(self.mzsegment): 278 all_peaks_data.append((x.mz_exp, x.abundance)) 279 else: 280 all_peaks_data.append((x.mz_exp, x.abundance)) 281 282 if all_peaks_data: 283 peaks_mz_list, intensities = zip(*all_peaks_data) 284 intensity_threshold = np.percentile(intensities, intensity_percentile) 285 286 for mz, intensity in all_peaks_data: 287 if intensity >= intensity_threshold: 288 peaks_mz.append(mz) 289 290 peaks_mz = np.asarray(peaks_mz) 291 292 if calibration_ref_match_method == "legacy": 293 # This legacy approach iterates through each reference match and finds the entries within 1 mz and within the user defined PPM error threshold 294 # Then it removes ambiguities - which means the calibration threshold hasto be very tight. 295 cal_peaks_mz = [] 296 cal_refs_mz = [] 297 for mzref in df_ref["m/z"]: 298 tmp_peaks_mz = peaks_mz[abs(peaks_mz - mzref) < 1] 299 for mzmeas in tmp_peaks_mz: 300 delta_mass = ((mzmeas - mzref) / mzref) * 1e6 301 if delta_mass < max(calib_ppm_error_threshold): 302 if delta_mass > min(calib_ppm_error_threshold): 303 cal_peaks_mz.append(mzmeas) 304 cal_refs_mz.append(mzref) 305 306 # To remove entries with duplicated indices (reference masses matching multiple peaks) 307 tmpdf = pd.Series(index=cal_refs_mz, data=cal_peaks_mz, dtype=float) 308 tmpdf = tmpdf[~tmpdf.index.duplicated(keep=False)] 309 310 cal_peaks_mz = list(tmpdf.to_numpy()) 311 cal_refs_mz = list(tmpdf.index) 312 elif calibration_ref_match_method == "merged": 313 # This is a new approach (August 2024) which uses Pandas 'merged_asof' to find the peaks closest in m/z between 314 # reference and measured masses. This is a quicker way to match, and seems to get more matches. 315 # It may not work as well when the data are far from correc initial mass 316 # e.g. if the correct peak is further from the reference than an incorrect peak. 317 meas_df = pd.DataFrame(columns=["meas_m/z"], data=peaks_mz) 318 tolerance = calibration_ref_match_tolerance 319 merged_df = pd.merge_asof( 320 df_ref, 321 meas_df, 322 left_on="m/z", 323 right_on="meas_m/z", 324 tolerance=tolerance, 325 direction="nearest", 326 ) 327 merged_df.dropna(how="any", inplace=True) 328 merged_df["Error_ppm"] = ( 329 (merged_df["meas_m/z"] - merged_df["m/z"]) / merged_df["m/z"] 330 ) * 1e6 331 median_raw_error = merged_df["Error_ppm"].median() 332 std_raw_error = merged_df["Error_ppm"].std() 333 if std_raw_error > calibration_ref_match_std_raw_error_limit: 334 std_raw_error = calibration_ref_match_std_raw_error_limit 335 self.mass_spectrum.calibration_raw_error_median = median_raw_error 336 self.mass_spectrum.calibration_raw_error_stdev = std_raw_error 337 merged_df = merged_df[ 338 (merged_df["Error_ppm"] > (median_raw_error - 1.5 * std_raw_error)) 339 & (merged_df["Error_ppm"] < (median_raw_error + 1.5 * std_raw_error)) 340 ] 341 # merged_df= merged_df[(merged_df['Error_ppm']>min(calib_ppm_error_threshold))&(merged_df['Error_ppm']<max(calib_ppm_error_threshold))] 342 cal_peaks_mz = list(merged_df["meas_m/z"]) 343 cal_refs_mz = list(merged_df["m/z"]) 344 else: 345 raise ValueError(f"{calibration_ref_match_method} not allowed.") 346 347 # it is crucial the mass lists are in same order 348 # corems likes to do masses from high to low. 349 cal_refs_mz.sort(reverse=False) 350 cal_peaks_mz.sort(reverse=False) 351 if self.mass_spectrum.parameters.mass_spectrum.verbose_processing: 352 print( 353 str(len(cal_peaks_mz)) 354 + " calibration points matched within thresholds." 355 ) 356 return cal_peaks_mz, cal_refs_mz 357 358 def robust_calib( 359 self, 360 param: list[float], 361 cal_peaks_mz: list[float], 362 cal_refs_mz: list[float], 363 order: int = 1, 364 ): 365 """Recalibration function 366 367 Computes the rms of m/z errors to minimize when calibrating. 368 This is adapted from from spike. 369 370 Parameters 371 ---------- 372 param : list of floats 373 generated by minimize function from scipy optimize. 374 cal_peaks_mz : list of floats 375 masses of measured peaks to use in mass calibration. 376 cal_peaks_mz : list of floats 377 reference mz values of found calibration points. 378 order : int, optional 379 order of the recalibration function. 1 = linear, 2 = quadratic. The default is 1. 380 381 Returns 382 ------- 383 rmserror : float 384 root mean square mass error for calibration points. 385 386 """ 387 Aterm = param[0] 388 Bterm = param[1] 389 try: 390 Cterm = param[2] 391 except IndexError: 392 pass 393 394 # get the mspeaks from the mass spectrum object which were calibration points 395 # mspeaks = [self.mass_spectrum.mspeaks[x] for x in imzmeas] 396 # get their calibrated mass values 397 # mspeakmzs = [x.mz_cal for x in mspeaks] 398 cal_peaks_mz = np.asarray(cal_peaks_mz) 399 400 # linearz 401 if order == 1: 402 ref_recal_points = (Aterm * cal_peaks_mz) + Bterm 403 # quadratic 404 elif order == 2: 405 ref_recal_points = (Aterm * (cal_peaks_mz)) + ( 406 Bterm * np.power((cal_peaks_mz), 2) + Cterm 407 ) 408 409 # sort both the calibration points (measured, recalibrated) 410 ref_recal_points.sort() 411 # and sort the calibration points (theoretical, predefined) 412 cal_refs_mz.sort() 413 414 # calculate the ppm error for each calibration point 415 error = ((ref_recal_points - cal_refs_mz) / cal_refs_mz) * 1e6 416 # calculate the root mean square error - this is our target to minimize 417 rmserror = np.sqrt(np.mean(error**2)) 418 return rmserror 419 420 def recalibrate_mass_spectrum( 421 self, 422 cal_peaks_mz: list[float], 423 cal_refs_mz: list[float], 424 order: int = 1, 425 diagnostic: bool = False, 426 ): 427 """Main recalibration function which uses a robust linear regression 428 429 This function performs the recalibration of the mass spectrum object. 430 It iteratively applies 431 432 Parameters 433 ---------- 434 cal_peaks_mz : list of float 435 masses of measured peaks to use in mass calibration. 436 cal_refs_mz : list of float 437 reference mz values of found calibration points. 438 order : int, optional 439 order of the recalibration function. 1 = linear, 2 = quadratic. The default is 1. 440 441 Returns 442 ------- 443 mass_spectrum : CoreMS mass spectrum object 444 Calibrated mass spectrum object 445 446 447 Notes 448 ----- 449 This function is adapted, in part, from the SPIKE project [1,2] and is based on the robust linear regression method. 450 451 References 452 ---------- 453 1. Chiron L., Coutouly M-A., Starck J-P., Rolando C., Delsuc M-A. 454 SPIKE a Processing Software dedicated to Fourier Spectroscopies 455 https://arxiv.org/abs/1608.06777 (2016) 456 2. SPIKE - https://github.com/spike-project/spike 457 458 """ 459 # initialise parameters for recalibration 460 # these are the 'Aterm, Bterm, Cterm' 461 # as spectra are already freq->mz calibrated, these terms are very small 462 # may be beneficial to formally separate them from the freq->mz terms 463 if order == 1: 464 Po = [1, 0] 465 elif order == 2: 466 Po = [1, 0, 0] 467 468 if len(cal_peaks_mz) >= 2: 469 if self.mzsegment: # If only part of the spectrum is to be recalibrated 470 mz_exp_peaks = np.array( 471 [mspeak.mz_exp for mspeak in self.mass_spectrum] 472 ) 473 # Split the array into two parts - one to recailbrate, one to keep unchanged. 474 mz_exp_peaks_tocal = mz_exp_peaks[ 475 (mz_exp_peaks >= min(self.mzsegment)) 476 & (mz_exp_peaks <= max(self.mzsegment)) 477 ] 478 mz_exp_peaks_unchanged = mz_exp_peaks[ 479 ~(mz_exp_peaks >= min(self.mzsegment)) 480 | ~(mz_exp_peaks <= max(self.mzsegment)) 481 ] 482 # TODO: - segmented calibration needs a way to better track the calibration args/values... 483 if not self.mass_spectrum.is_centroid: 484 mz_exp_profile = np.array(self.mass_spectrum.mz_exp_profile) 485 # Split the array into two parts - one to recailbrate, one to keep unchanged. 486 mz_exp_profile_tocal = mz_exp_profile[ 487 (mz_exp_profile >= min(self.mzsegment)) 488 & (mz_exp_profile <= max(self.mzsegment)) 489 ] 490 mz_exp_profile_unchanged = mz_exp_profile[ 491 ~(mz_exp_profile >= min(self.mzsegment)) 492 | ~(mz_exp_profile <= max(self.mzsegment)) 493 ] 494 else: # if just recalibrating the whole spectrum 495 mz_exp_peaks_tocal = np.array( 496 [mspeak.mz_exp for mspeak in self.mass_spectrum] 497 ) 498 if not self.mass_spectrum.is_centroid: 499 mz_exp_profile_tocal = np.array(self.mass_spectrum.mz_exp_profile) 500 501 minimize_method = self.mass_spectrum.settings.calib_minimize_method 502 res = minimize( 503 self.robust_calib, 504 Po, 505 args=(cal_peaks_mz, cal_refs_mz, order), 506 method=minimize_method, 507 ) 508 if self.mass_spectrum.parameters.mass_spectrum.verbose_processing: 509 print( 510 "minimize function completed with RMS error of: {:0.3f} ppm".format( 511 res["fun"] 512 ) 513 ) 514 print( 515 "minimize function performed {:1d} fn evals and {:1d} iterations".format( 516 res["nfev"], res["nit"] 517 ) 518 ) 519 Pn = res.x 520 521 # mz_exp_ms = np.array([mspeak.mz_exp for mspeak in self.mass_spectrum]) 522 523 if order == 1: 524 mz_domain = (Pn[0] * mz_exp_peaks_tocal) + Pn[1] 525 if not self.mass_spectrum.is_centroid: 526 mz_profile_calc = (Pn[0] * mz_exp_profile_tocal) + Pn[1] 527 528 elif order == 2: 529 mz_domain = (Pn[0] * (mz_exp_peaks_tocal)) + ( 530 Pn[1] * np.power((mz_exp_peaks_tocal), 2) + Pn[2] 531 ) 532 533 if not self.mass_spectrum.is_centroid: 534 mz_profile_calc = (Pn[0] * (mz_exp_profile_tocal)) + ( 535 Pn[1] * np.power((mz_exp_profile_tocal), 2) + Pn[2] 536 ) 537 538 if self.mzsegment: 539 # Recombine the mass domains 540 mz_domain = np.concatenate([mz_domain, mz_exp_peaks_unchanged]) 541 mz_domain.sort() 542 if not self.mass_spectrum.is_centroid: 543 mz_profile_calc = np.concatenate( 544 [mz_profile_calc, mz_exp_profile_unchanged] 545 ) 546 mz_profile_calc.sort() 547 # Sort them 548 if ( 549 mz_exp_peaks[0] > mz_exp_peaks[1] 550 ): # If originally descending mass order 551 mz_domain = mz_domain[::-1] 552 if not self.mass_spectrum.is_centroid: 553 mz_profile_calc = mz_profile_calc[::-1] 554 555 self.mass_spectrum.mz_cal = mz_domain 556 if not self.mass_spectrum.is_centroid: 557 self.mass_spectrum.mz_cal_profile = mz_profile_calc 558 559 self.mass_spectrum.calibration_order = order 560 self.mass_spectrum.calibration_RMS = float(res["fun"]) 561 self.mass_spectrum.calibration_points = int(len(cal_refs_mz)) 562 self.mass_spectrum.calibration_ref_mzs = cal_refs_mz 563 self.mass_spectrum.calibration_meas_mzs = cal_peaks_mz 564 565 self.mass_spectrum.calibration_segment = self.mzsegment 566 567 if diagnostic: 568 return self.mass_spectrum, res 569 return self.mass_spectrum 570 else: 571 warnings.warn("Too few calibration points - aborting.") 572 return self.mass_spectrum 573 574 def run(self): 575 """Run the calibration routine 576 577 This function runs the calibration routine. 578 579 """ 580 calib_snr_threshold = self.mass_spectrum.settings.calib_sn_threshold 581 max_calib_ppm_error = self.mass_spectrum.settings.max_calib_ppm_error 582 min_calib_ppm_error = self.mass_spectrum.settings.min_calib_ppm_error 583 calib_pol_order = self.mass_spectrum.settings.calib_pol_order 584 calibration_ref_match_method = ( 585 self.mass_spectrum.settings.calibration_ref_match_method 586 ) 587 calibration_ref_match_tolerance = ( 588 self.mass_spectrum.settings.calibration_ref_match_tolerance 589 ) 590 calibration_ref_match_std_raw_error_limit = ( 591 self.mass_spectrum.settings.calibration_ref_match_std_raw_error_limit 592 ) 593 594 # load reference mass list 595 df_ref = self.load_ref_mass_list() 596 597 # find calibration points 598 cal_peaks_mz, cal_refs_mz = self.find_calibration_points( 599 df_ref, 600 calib_ppm_error_threshold=(min_calib_ppm_error, max_calib_ppm_error), 601 calib_snr_threshold=calib_snr_threshold, 602 calibration_ref_match_method=calibration_ref_match_method, 603 calibration_ref_match_tolerance=calibration_ref_match_tolerance, 604 calibration_ref_match_std_raw_error_limit=calibration_ref_match_std_raw_error_limit, 605 ) 606 if len(cal_peaks_mz) == 2: 607 self.mass_spectrum.settings.calib_pol_order = 1 608 calib_pol_order = 1 609 if self.mass_spectrum.parameters.mass_spectrum.verbose_processing: 610 print("Only 2 calibration points found, forcing a linear recalibration") 611 elif len(cal_peaks_mz) < 2: 612 warnings.warn("Too few calibration points found, function will fail") 613 self.recalibrate_mass_spectrum(cal_peaks_mz, cal_refs_mz, order=calib_pol_order)
MzDomainCalibration class for recalibrating mass spectra
Parameters
- mass_spectrum (CoreMS MassSpectrum Object): The mass spectrum to be calibrated.
- ref_masslist (str): The path to a reference mass list.
- mzsegment (tuple of floats, optional): The mz range to recalibrate, or None. Used for calibration of specific parts of the mz domain at a time. Future work - allow multiple mzsegments to be passed.
Attributes
- mass_spectrum (CoreMS MassSpectrum Object): The mass spectrum to be calibrated.
- mzsegment (tuple of floats or None): The mz range to recalibrate, or None.
- ref_mass_list_path (str or Path): The path to the reference mass list.
Methods
- run(). Main function to run this class.
- load_ref_mass_list(). Load reference mass list (Bruker format).
- gen_ref_mass_list_from_assigned(min_conf=0.7). Generate reference mass list from assigned masses.
- find_calibration_points(df_ref, calib_ppm_error_threshold=(-1, 1), calib_snr_threshold=5). Find calibration points in the mass spectrum based on the reference mass list.
- robust_calib(param, cal_peaks_mz, cal_refs_mz, order=1). Recalibration function.
- recalibrate_mass_spectrum(cal_peaks_mz, cal_refs_mz, order=1, diagnostic=False). Main recalibration function which uses a robust linear regression.
63 def __init__(self, mass_spectrum, ref_masslist, mzsegment=None): 64 self.mass_spectrum = mass_spectrum 65 self.mzsegment = mzsegment 66 67 # define reference mass list - bruker .ref format 68 self.ref_mass_list_path = ref_masslist 69 if self.mass_spectrum.percentile_assigned(mute_output=True)[0] != 0: 70 warnings.warn( 71 "Warning: calibrating spectra which have already been assigned may yield erroneous results" 72 ) 73 self.mass_spectrum.mz_cal = self.mass_spectrum.mz_exp 74 self.mass_spectrum.mz_cal_profile = self.mass_spectrum._mz_exp 75 76 if self.mass_spectrum.parameters.mass_spectrum.verbose_processing: 77 print( 78 "MS Obj loaded - " + str(len(mass_spectrum.mspeaks)) + " peaks found." 79 )
81 def load_ref_mass_list(self): 82 """ 83 Load reference mass list (Bruker format). 84 85 Loads in a reference mass list from a .ref file. Some versions of 86 Bruker's software produce .ref files with a different format where 87 the header lines (starting with '#' or '##') and delimiters may vary. 88 The file may be located locally or on S3 and will be handled accordingly. 89 90 Returns 91 ------- 92 df_ref : Pandas DataFrame 93 Reference mass list object. 94 """ 95 # Get a Path-like object from the input path string or S3Path 96 refmasslist = ( 97 Path(self.ref_mass_list_path) 98 if isinstance(self.ref_mass_list_path, str) 99 else self.ref_mass_list_path 100 ) 101 102 # Make sure the file exists 103 if not refmasslist.exists(): 104 raise FileExistsError("File does not exist: %s" % refmasslist) 105 106 # Read all lines from the file (handling S3 vs local differently) 107 if isinstance(refmasslist, S3Path): 108 # For S3, read the file in binary, then decode to string and split into lines. 109 content = refmasslist.open("rb").read() 110 all_lines = content.decode("utf-8").splitlines(keepends=True) 111 else: 112 # For a local file, open in text mode and read lines. 113 with refmasslist.open("r") as f: 114 all_lines = f.readlines() 115 116 # Identify the index of the first line of the actual data. 117 # We assume header lines start with '#' (or '##') and ignore blank lines. 118 data_start_idx = 0 119 for idx, line in enumerate(all_lines): 120 if line.strip() and not line.lstrip().startswith("#"): 121 data_start_idx = idx 122 break 123 124 # If there are not enough lines to guess the dialect, throw an error 125 if data_start_idx >= len(all_lines): 126 raise ValueError("The file does not appear to contain any data lines.") 127 128 # Use a couple of the data lines to let csv.Sniffer detect the delimiter 129 sample_lines = "".join(all_lines[data_start_idx : data_start_idx + 2]) 130 try: 131 dialect = csv.Sniffer().sniff(sample_lines) 132 delimiter = dialect.delimiter 133 except csv.Error: 134 # If csv.Sniffer fails, default to a common delimiter (e.g., comma) 135 delimiter = "," 136 137 # Join the lines from the beginning of data (this might include a blank line) 138 joined_data = "".join(all_lines[data_start_idx:]) 139 140 # Depending on whether the file is S3 or local, wrap the data as needed for pandas 141 if isinstance(refmasslist, S3Path): 142 data_buffer = BytesIO(joined_data.encode("utf-8")) 143 else: 144 data_buffer = StringIO(joined_data) 145 146 # Read data into a DataFrame. 147 # Adjust columns and names as needed – here we assume at least 2 columns: 148 df_ref = pd.read_csv( 149 data_buffer, 150 sep=delimiter, 151 header=None, 152 usecols=[0, 1], # Modify if more columns are required. 153 names=["Formula", "m/z"], 154 ) 155 156 df_ref.sort_values(by="m/z", ascending=True, inplace=True) 157 158 if self.mass_spectrum.parameters.mass_spectrum.verbose_processing: 159 print( 160 "Reference mass list loaded - {} calibration masses loaded.".format( 161 len(df_ref) 162 ) 163 ) 164 165 return df_ref
Load reference mass list (Bruker format).
Loads in a reference mass list from a .ref file. Some versions of Bruker's software produce .ref files with a different format where the header lines (starting with '#' or '##') and delimiters may vary. The file may be located locally or on S3 and will be handled accordingly.
Returns
- df_ref (Pandas DataFrame): Reference mass list object.
167 def gen_ref_mass_list_from_assigned(self, min_conf: float = 0.7): 168 """Generate reference mass list from assigned masses 169 170 This function will generate a ref mass dataframe object from an assigned corems mass spec obj 171 using assigned masses above a certain minimum confidence threshold. 172 173 This function needs to be retested and check it is covered in the unit tests. 174 175 Parameters 176 ---------- 177 min_conf : float, optional 178 minimum confidence score. The default is 0.7. 179 180 Returns 181 ------- 182 df_ref : Pandas DataFrame 183 reference mass list - based on calculated masses. 184 185 """ 186 # TODO this function needs to be retested and check it is covered in the unit tests 187 df = self.mass_spectrum.to_dataframe() 188 df = df[df["Confidence Score"] > min_conf] 189 df_ref = pd.DataFrame(columns=["m/z"]) 190 df_ref["m/z"] = df["Calculated m/z"] 191 if self.mass_spectrum.parameters.mass_spectrum.verbose_processing: 192 print( 193 "Reference mass list generated - " 194 + str(len(df_ref)) 195 + " calibration masses." 196 ) 197 return df_ref
Generate reference mass list from assigned masses
This function will generate a ref mass dataframe object from an assigned corems mass spec obj using assigned masses above a certain minimum confidence threshold.
This function needs to be retested and check it is covered in the unit tests.
Parameters
- min_conf (float, optional): minimum confidence score. The default is 0.7.
Returns
- df_ref (Pandas DataFrame): reference mass list - based on calculated masses.
199 def find_calibration_points( 200 self, 201 df_ref, 202 calib_ppm_error_threshold: tuple[float, float] = (-1, 1), 203 calib_snr_threshold: float = 5, 204 calibration_ref_match_method: str = "merged", 205 calibration_ref_match_tolerance: float = 0.003, 206 calibration_ref_match_std_raw_error_limit: float = 1.5, 207 ): 208 """Function to find calibration points in the mass spectrum 209 210 Based on the reference mass list. 211 212 Parameters 213 ---------- 214 df_ref : Pandas DataFrame 215 reference mass list for recalibration. 216 calib_ppm_error_threshold : tuple of floats, optional 217 ppm error for finding calibration masses in the spectrum. The default is -1,1. 218 Note: This is based on the calculation of ppm = ((mz_measure - mz_theoretical)/mz_theoretical)*1e6. 219 Some software does this the other way around and value signs must be inverted for that to work. 220 calib_snr_threshold : float, optional 221 snr threshold for finding calibration masses in the spectrum. The default is 5. 222 If SNR data is unavailable, peaks are filtered by intensity percentile using the formula: 223 percentile = max(5, 100 - calib_snr_threshold) 224 calibration_ref_match_method : str, optional 225 method for matching calibration references. The default is "merged". 226 calibration_ref_match_tolerance : float, optional 227 tolerance for matching calibration references. The default is 0.003. 228 calibration_ref_match_std_raw_error_limit : float, optional 229 standard deviation raw error limit for calibration references. The default is 1.5. 230 231 Returns 232 ------- 233 cal_peaks_mz : list of floats 234 masses of measured ions to use in calibration routine 235 cal_refs_mz : list of floats 236 reference mz values of found calibration points. 237 238 """ 239 240 # Check if SNR data is available by testing the first peak 241 use_snr = False 242 if len(self.mass_spectrum.mspeaks) > 0: 243 first_peak = self.mass_spectrum.mspeaks[0] 244 if ( 245 hasattr(first_peak, "signal_to_noise") 246 and first_peak.signal_to_noise is not None 247 and not np.isnan(first_peak.signal_to_noise) 248 and first_peak.signal_to_noise > 0 249 ): 250 use_snr = True 251 252 # This approach is much more efficient and expedient than the original implementation. 253 peaks_mz = [] 254 peaks_intensity = [] 255 256 if use_snr: 257 # Use SNR filtering 258 for x in self.mass_spectrum.mspeaks: 259 if x.signal_to_noise > calib_snr_threshold: 260 if self.mzsegment: 261 if min(self.mzsegment) <= x.mz_exp <= max(self.mzsegment): 262 peaks_mz.append(x.mz_exp) 263 else: 264 peaks_mz.append(x.mz_exp) 265 else: 266 # Fallback to intensity percentile filtering 267 intensity_percentile = max(5, 100 - calib_snr_threshold) 268 warnings.warn( 269 f"SNR data unavailable for calibration. Using intensity-based filtering instead. " 270 f"SNR threshold of {calib_snr_threshold} corresponds to intensity percentile >= {intensity_percentile}%." 271 ) 272 273 # Collect all peaks and their intensities 274 all_peaks_data = [] 275 for x in self.mass_spectrum.mspeaks: 276 if self.mzsegment: 277 if min(self.mzsegment) <= x.mz_exp <= max(self.mzsegment): 278 all_peaks_data.append((x.mz_exp, x.abundance)) 279 else: 280 all_peaks_data.append((x.mz_exp, x.abundance)) 281 282 if all_peaks_data: 283 peaks_mz_list, intensities = zip(*all_peaks_data) 284 intensity_threshold = np.percentile(intensities, intensity_percentile) 285 286 for mz, intensity in all_peaks_data: 287 if intensity >= intensity_threshold: 288 peaks_mz.append(mz) 289 290 peaks_mz = np.asarray(peaks_mz) 291 292 if calibration_ref_match_method == "legacy": 293 # This legacy approach iterates through each reference match and finds the entries within 1 mz and within the user defined PPM error threshold 294 # Then it removes ambiguities - which means the calibration threshold hasto be very tight. 295 cal_peaks_mz = [] 296 cal_refs_mz = [] 297 for mzref in df_ref["m/z"]: 298 tmp_peaks_mz = peaks_mz[abs(peaks_mz - mzref) < 1] 299 for mzmeas in tmp_peaks_mz: 300 delta_mass = ((mzmeas - mzref) / mzref) * 1e6 301 if delta_mass < max(calib_ppm_error_threshold): 302 if delta_mass > min(calib_ppm_error_threshold): 303 cal_peaks_mz.append(mzmeas) 304 cal_refs_mz.append(mzref) 305 306 # To remove entries with duplicated indices (reference masses matching multiple peaks) 307 tmpdf = pd.Series(index=cal_refs_mz, data=cal_peaks_mz, dtype=float) 308 tmpdf = tmpdf[~tmpdf.index.duplicated(keep=False)] 309 310 cal_peaks_mz = list(tmpdf.to_numpy()) 311 cal_refs_mz = list(tmpdf.index) 312 elif calibration_ref_match_method == "merged": 313 # This is a new approach (August 2024) which uses Pandas 'merged_asof' to find the peaks closest in m/z between 314 # reference and measured masses. This is a quicker way to match, and seems to get more matches. 315 # It may not work as well when the data are far from correc initial mass 316 # e.g. if the correct peak is further from the reference than an incorrect peak. 317 meas_df = pd.DataFrame(columns=["meas_m/z"], data=peaks_mz) 318 tolerance = calibration_ref_match_tolerance 319 merged_df = pd.merge_asof( 320 df_ref, 321 meas_df, 322 left_on="m/z", 323 right_on="meas_m/z", 324 tolerance=tolerance, 325 direction="nearest", 326 ) 327 merged_df.dropna(how="any", inplace=True) 328 merged_df["Error_ppm"] = ( 329 (merged_df["meas_m/z"] - merged_df["m/z"]) / merged_df["m/z"] 330 ) * 1e6 331 median_raw_error = merged_df["Error_ppm"].median() 332 std_raw_error = merged_df["Error_ppm"].std() 333 if std_raw_error > calibration_ref_match_std_raw_error_limit: 334 std_raw_error = calibration_ref_match_std_raw_error_limit 335 self.mass_spectrum.calibration_raw_error_median = median_raw_error 336 self.mass_spectrum.calibration_raw_error_stdev = std_raw_error 337 merged_df = merged_df[ 338 (merged_df["Error_ppm"] > (median_raw_error - 1.5 * std_raw_error)) 339 & (merged_df["Error_ppm"] < (median_raw_error + 1.5 * std_raw_error)) 340 ] 341 # merged_df= merged_df[(merged_df['Error_ppm']>min(calib_ppm_error_threshold))&(merged_df['Error_ppm']<max(calib_ppm_error_threshold))] 342 cal_peaks_mz = list(merged_df["meas_m/z"]) 343 cal_refs_mz = list(merged_df["m/z"]) 344 else: 345 raise ValueError(f"{calibration_ref_match_method} not allowed.") 346 347 # it is crucial the mass lists are in same order 348 # corems likes to do masses from high to low. 349 cal_refs_mz.sort(reverse=False) 350 cal_peaks_mz.sort(reverse=False) 351 if self.mass_spectrum.parameters.mass_spectrum.verbose_processing: 352 print( 353 str(len(cal_peaks_mz)) 354 + " calibration points matched within thresholds." 355 ) 356 return cal_peaks_mz, cal_refs_mz
Function to find calibration points in the mass spectrum
Based on the reference mass list.
Parameters
- df_ref (Pandas DataFrame): reference mass list for recalibration.
- calib_ppm_error_threshold (tuple of floats, optional): ppm error for finding calibration masses in the spectrum. The default is -1,1. Note: This is based on the calculation of ppm = ((mz_measure - mz_theoretical)/mz_theoretical)*1e6. Some software does this the other way around and value signs must be inverted for that to work.
- calib_snr_threshold (float, optional): snr threshold for finding calibration masses in the spectrum. The default is 5. If SNR data is unavailable, peaks are filtered by intensity percentile using the formula: percentile = max(5, 100 - calib_snr_threshold)
- calibration_ref_match_method (str, optional): method for matching calibration references. The default is "merged".
- calibration_ref_match_tolerance (float, optional): tolerance for matching calibration references. The default is 0.003.
- calibration_ref_match_std_raw_error_limit (float, optional): standard deviation raw error limit for calibration references. The default is 1.5.
Returns
- cal_peaks_mz (list of floats): masses of measured ions to use in calibration routine
- cal_refs_mz (list of floats): reference mz values of found calibration points.
358 def robust_calib( 359 self, 360 param: list[float], 361 cal_peaks_mz: list[float], 362 cal_refs_mz: list[float], 363 order: int = 1, 364 ): 365 """Recalibration function 366 367 Computes the rms of m/z errors to minimize when calibrating. 368 This is adapted from from spike. 369 370 Parameters 371 ---------- 372 param : list of floats 373 generated by minimize function from scipy optimize. 374 cal_peaks_mz : list of floats 375 masses of measured peaks to use in mass calibration. 376 cal_peaks_mz : list of floats 377 reference mz values of found calibration points. 378 order : int, optional 379 order of the recalibration function. 1 = linear, 2 = quadratic. The default is 1. 380 381 Returns 382 ------- 383 rmserror : float 384 root mean square mass error for calibration points. 385 386 """ 387 Aterm = param[0] 388 Bterm = param[1] 389 try: 390 Cterm = param[2] 391 except IndexError: 392 pass 393 394 # get the mspeaks from the mass spectrum object which were calibration points 395 # mspeaks = [self.mass_spectrum.mspeaks[x] for x in imzmeas] 396 # get their calibrated mass values 397 # mspeakmzs = [x.mz_cal for x in mspeaks] 398 cal_peaks_mz = np.asarray(cal_peaks_mz) 399 400 # linearz 401 if order == 1: 402 ref_recal_points = (Aterm * cal_peaks_mz) + Bterm 403 # quadratic 404 elif order == 2: 405 ref_recal_points = (Aterm * (cal_peaks_mz)) + ( 406 Bterm * np.power((cal_peaks_mz), 2) + Cterm 407 ) 408 409 # sort both the calibration points (measured, recalibrated) 410 ref_recal_points.sort() 411 # and sort the calibration points (theoretical, predefined) 412 cal_refs_mz.sort() 413 414 # calculate the ppm error for each calibration point 415 error = ((ref_recal_points - cal_refs_mz) / cal_refs_mz) * 1e6 416 # calculate the root mean square error - this is our target to minimize 417 rmserror = np.sqrt(np.mean(error**2)) 418 return rmserror
Recalibration function
Computes the rms of m/z errors to minimize when calibrating. This is adapted from from spike.
Parameters
- param (list of floats): generated by minimize function from scipy optimize.
- cal_peaks_mz (list of floats): masses of measured peaks to use in mass calibration.
- cal_peaks_mz (list of floats): reference mz values of found calibration points.
- order (int, optional): order of the recalibration function. 1 = linear, 2 = quadratic. The default is 1.
Returns
- rmserror (float): root mean square mass error for calibration points.
420 def recalibrate_mass_spectrum( 421 self, 422 cal_peaks_mz: list[float], 423 cal_refs_mz: list[float], 424 order: int = 1, 425 diagnostic: bool = False, 426 ): 427 """Main recalibration function which uses a robust linear regression 428 429 This function performs the recalibration of the mass spectrum object. 430 It iteratively applies 431 432 Parameters 433 ---------- 434 cal_peaks_mz : list of float 435 masses of measured peaks to use in mass calibration. 436 cal_refs_mz : list of float 437 reference mz values of found calibration points. 438 order : int, optional 439 order of the recalibration function. 1 = linear, 2 = quadratic. The default is 1. 440 441 Returns 442 ------- 443 mass_spectrum : CoreMS mass spectrum object 444 Calibrated mass spectrum object 445 446 447 Notes 448 ----- 449 This function is adapted, in part, from the SPIKE project [1,2] and is based on the robust linear regression method. 450 451 References 452 ---------- 453 1. Chiron L., Coutouly M-A., Starck J-P., Rolando C., Delsuc M-A. 454 SPIKE a Processing Software dedicated to Fourier Spectroscopies 455 https://arxiv.org/abs/1608.06777 (2016) 456 2. SPIKE - https://github.com/spike-project/spike 457 458 """ 459 # initialise parameters for recalibration 460 # these are the 'Aterm, Bterm, Cterm' 461 # as spectra are already freq->mz calibrated, these terms are very small 462 # may be beneficial to formally separate them from the freq->mz terms 463 if order == 1: 464 Po = [1, 0] 465 elif order == 2: 466 Po = [1, 0, 0] 467 468 if len(cal_peaks_mz) >= 2: 469 if self.mzsegment: # If only part of the spectrum is to be recalibrated 470 mz_exp_peaks = np.array( 471 [mspeak.mz_exp for mspeak in self.mass_spectrum] 472 ) 473 # Split the array into two parts - one to recailbrate, one to keep unchanged. 474 mz_exp_peaks_tocal = mz_exp_peaks[ 475 (mz_exp_peaks >= min(self.mzsegment)) 476 & (mz_exp_peaks <= max(self.mzsegment)) 477 ] 478 mz_exp_peaks_unchanged = mz_exp_peaks[ 479 ~(mz_exp_peaks >= min(self.mzsegment)) 480 | ~(mz_exp_peaks <= max(self.mzsegment)) 481 ] 482 # TODO: - segmented calibration needs a way to better track the calibration args/values... 483 if not self.mass_spectrum.is_centroid: 484 mz_exp_profile = np.array(self.mass_spectrum.mz_exp_profile) 485 # Split the array into two parts - one to recailbrate, one to keep unchanged. 486 mz_exp_profile_tocal = mz_exp_profile[ 487 (mz_exp_profile >= min(self.mzsegment)) 488 & (mz_exp_profile <= max(self.mzsegment)) 489 ] 490 mz_exp_profile_unchanged = mz_exp_profile[ 491 ~(mz_exp_profile >= min(self.mzsegment)) 492 | ~(mz_exp_profile <= max(self.mzsegment)) 493 ] 494 else: # if just recalibrating the whole spectrum 495 mz_exp_peaks_tocal = np.array( 496 [mspeak.mz_exp for mspeak in self.mass_spectrum] 497 ) 498 if not self.mass_spectrum.is_centroid: 499 mz_exp_profile_tocal = np.array(self.mass_spectrum.mz_exp_profile) 500 501 minimize_method = self.mass_spectrum.settings.calib_minimize_method 502 res = minimize( 503 self.robust_calib, 504 Po, 505 args=(cal_peaks_mz, cal_refs_mz, order), 506 method=minimize_method, 507 ) 508 if self.mass_spectrum.parameters.mass_spectrum.verbose_processing: 509 print( 510 "minimize function completed with RMS error of: {:0.3f} ppm".format( 511 res["fun"] 512 ) 513 ) 514 print( 515 "minimize function performed {:1d} fn evals and {:1d} iterations".format( 516 res["nfev"], res["nit"] 517 ) 518 ) 519 Pn = res.x 520 521 # mz_exp_ms = np.array([mspeak.mz_exp for mspeak in self.mass_spectrum]) 522 523 if order == 1: 524 mz_domain = (Pn[0] * mz_exp_peaks_tocal) + Pn[1] 525 if not self.mass_spectrum.is_centroid: 526 mz_profile_calc = (Pn[0] * mz_exp_profile_tocal) + Pn[1] 527 528 elif order == 2: 529 mz_domain = (Pn[0] * (mz_exp_peaks_tocal)) + ( 530 Pn[1] * np.power((mz_exp_peaks_tocal), 2) + Pn[2] 531 ) 532 533 if not self.mass_spectrum.is_centroid: 534 mz_profile_calc = (Pn[0] * (mz_exp_profile_tocal)) + ( 535 Pn[1] * np.power((mz_exp_profile_tocal), 2) + Pn[2] 536 ) 537 538 if self.mzsegment: 539 # Recombine the mass domains 540 mz_domain = np.concatenate([mz_domain, mz_exp_peaks_unchanged]) 541 mz_domain.sort() 542 if not self.mass_spectrum.is_centroid: 543 mz_profile_calc = np.concatenate( 544 [mz_profile_calc, mz_exp_profile_unchanged] 545 ) 546 mz_profile_calc.sort() 547 # Sort them 548 if ( 549 mz_exp_peaks[0] > mz_exp_peaks[1] 550 ): # If originally descending mass order 551 mz_domain = mz_domain[::-1] 552 if not self.mass_spectrum.is_centroid: 553 mz_profile_calc = mz_profile_calc[::-1] 554 555 self.mass_spectrum.mz_cal = mz_domain 556 if not self.mass_spectrum.is_centroid: 557 self.mass_spectrum.mz_cal_profile = mz_profile_calc 558 559 self.mass_spectrum.calibration_order = order 560 self.mass_spectrum.calibration_RMS = float(res["fun"]) 561 self.mass_spectrum.calibration_points = int(len(cal_refs_mz)) 562 self.mass_spectrum.calibration_ref_mzs = cal_refs_mz 563 self.mass_spectrum.calibration_meas_mzs = cal_peaks_mz 564 565 self.mass_spectrum.calibration_segment = self.mzsegment 566 567 if diagnostic: 568 return self.mass_spectrum, res 569 return self.mass_spectrum 570 else: 571 warnings.warn("Too few calibration points - aborting.") 572 return self.mass_spectrum
Main recalibration function which uses a robust linear regression
This function performs the recalibration of the mass spectrum object. It iteratively applies
Parameters
- cal_peaks_mz (list of float): masses of measured peaks to use in mass calibration.
- cal_refs_mz (list of float): reference mz values of found calibration points.
- order (int, optional): order of the recalibration function. 1 = linear, 2 = quadratic. The default is 1.
Returns
- mass_spectrum (CoreMS mass spectrum object): Calibrated mass spectrum object
Notes
This function is adapted, in part, from the SPIKE project [1,2] and is based on the robust linear regression method.
References
- Chiron L., Coutouly M-A., Starck J-P., Rolando C., Delsuc M-A. SPIKE a Processing Software dedicated to Fourier Spectroscopies https://arxiv.org/abs/1608.06777 (2016)
- SPIKE - https://github.com/spike-project/spike
574 def run(self): 575 """Run the calibration routine 576 577 This function runs the calibration routine. 578 579 """ 580 calib_snr_threshold = self.mass_spectrum.settings.calib_sn_threshold 581 max_calib_ppm_error = self.mass_spectrum.settings.max_calib_ppm_error 582 min_calib_ppm_error = self.mass_spectrum.settings.min_calib_ppm_error 583 calib_pol_order = self.mass_spectrum.settings.calib_pol_order 584 calibration_ref_match_method = ( 585 self.mass_spectrum.settings.calibration_ref_match_method 586 ) 587 calibration_ref_match_tolerance = ( 588 self.mass_spectrum.settings.calibration_ref_match_tolerance 589 ) 590 calibration_ref_match_std_raw_error_limit = ( 591 self.mass_spectrum.settings.calibration_ref_match_std_raw_error_limit 592 ) 593 594 # load reference mass list 595 df_ref = self.load_ref_mass_list() 596 597 # find calibration points 598 cal_peaks_mz, cal_refs_mz = self.find_calibration_points( 599 df_ref, 600 calib_ppm_error_threshold=(min_calib_ppm_error, max_calib_ppm_error), 601 calib_snr_threshold=calib_snr_threshold, 602 calibration_ref_match_method=calibration_ref_match_method, 603 calibration_ref_match_tolerance=calibration_ref_match_tolerance, 604 calibration_ref_match_std_raw_error_limit=calibration_ref_match_std_raw_error_limit, 605 ) 606 if len(cal_peaks_mz) == 2: 607 self.mass_spectrum.settings.calib_pol_order = 1 608 calib_pol_order = 1 609 if self.mass_spectrum.parameters.mass_spectrum.verbose_processing: 610 print("Only 2 calibration points found, forcing a linear recalibration") 611 elif len(cal_peaks_mz) < 2: 612 warnings.warn("Too few calibration points found, function will fail") 613 self.recalibrate_mass_spectrum(cal_peaks_mz, cal_refs_mz, order=calib_pol_order)
Run the calibration routine
This function runs the calibration routine.