corems.mass_spectrum.calc.PeakPicking
@author: Yuri E. Corilo @date: Jun 27, 2019
1""" 2@author: Yuri E. Corilo 3@date: Jun 27, 2019 4""" 5 6import warnings 7from numpy import ( 8 hstack, 9 inf, 10 isnan, 11 where, 12 array, 13 polyfit, 14 nan, 15 pad, 16 zeros, 17 searchsorted, 18) 19from corems.encapsulation.constant import Labels 20from corems.mass_spectra.calc import SignalProcessing as sp 21 22 23class PeakPicking: 24 """Class for peak picking. 25 26 Parameters 27 ---------- 28 None 29 30 Attributes 31 ---------- 32 None 33 34 Methods 35 ------- 36 * prepare_peak_picking_data(). 37 Prepare the mz, abundance, and frequence data for peak picking. 38 * cut_mz_domain_peak_picking(). 39 Cut the m/z domain for peak picking. 40 * extrapolate_axes_for_pp(mz=None, abund=None, freq=None). 41 Extrapolate the m/z axis and fill the abundance axis with 0s. 42 * do_peak_picking(). 43 Perform peak picking. 44 * find_minima(apex_index, abundance, len_abundance, right=True). 45 Find the minima of a peak. 46 * linear_fit_calc(intes, massa, index_term, index_sign). 47 Algebraic solution to a linear fit. 48 * calculate_resolving_power(intes, massa, current_index). 49 Calculate the resolving power of a peak. 50 * cal_minima(mass, abun). 51 Calculate the minima of a peak. 52 * calc_centroid(mass, abund, freq). 53 Calculate the centroid of a peak. 54 * get_threshold(intes). 55 Get the intensity threshold for peak picking. 56 * algebraic_quadratic(list_mass, list_y). 57 Find the apex of a peak - algebraically. 58 * find_apex_fit_quadratic(mass, abund, freq, current_index). 59 Find the apex of a peak. 60 * check_prominence(abun, current_index, len_abundance, peak_height_diff). 61 Check the prominence of a peak. 62 * use_the_max(mass, abund, current_index, len_abundance, peak_height_diff). 63 Use the max peak height as the centroid. 64 * calc_centroid_legacy(mass, abund, freq). 65 Legacy centroid calculation. Deprecated - for deletion. 66 67 """ 68 69 def prepare_peak_picking_data(self): 70 """Prepare the data for peak picking. 71 72 This function will prepare the m/z, abundance, and frequency data for peak picking according to the settings. 73 74 Returns 75 ------- 76 mz : ndarray 77 The m/z axis. 78 abundance : ndarray 79 The abundance axis. 80 freq : ndarray or None 81 The frequency axis, if available. 82 """ 83 # First apply cut_mz_domain_peak_picking 84 mz, abundance, freq = self.cut_mz_domain_peak_picking() 85 86 # Then extrapolate the axes for peak picking 87 if self.settings.picking_point_extrapolate > 0: 88 mz, abundance, freq = self.extrapolate_axes_for_pp(mz, abundance, freq) 89 return mz, abundance, freq 90 91 def cut_mz_domain_peak_picking(self): 92 """ 93 Cut the m/z domain for peak picking. 94 95 Simplified function 96 97 Returns 98 ------- 99 mz_domain_X_low_cutoff : ndarray 100 The m/z values within the specified range. 101 mz_domain_low_Y_cutoff : ndarray 102 The abundance values within the specified range. 103 freq_domain_low_Y_cutoff : ndarray or None 104 The frequency values within the specified range, if available. 105 106 """ 107 max_picking_mz = self.settings.max_picking_mz 108 min_picking_mz = self.settings.min_picking_mz 109 110 # min_start = where(self.mz_exp_profile > min_picking_mz)[0][0] 111 # max_final = where(self.mz_exp_profile < max_picking_mz)[-1][-1] 112 min_start = searchsorted(a=self.mz_exp_profile, v=min_picking_mz) 113 max_final = searchsorted(a=self.mz_exp_profile, v=max_picking_mz) 114 115 if self.has_frequency: 116 if self.freq_exp_profile.any(): 117 return ( 118 self.mz_exp_profile[min_start:max_final], 119 self.abundance_profile[min_start:max_final], 120 self.freq_exp_profile[min_start:max_final], 121 ) 122 123 else: 124 return ( 125 self.mz_exp_profile[min_start:max_final], 126 self.abundance_profile[min_start:max_final], 127 None, 128 ) 129 130 def legacy_cut_mz_domain_peak_picking(self): 131 """ 132 Cut the m/z domain for peak picking. 133 DEPRECATED 134 Returns 135 ------- 136 mz_domain_X_low_cutoff : ndarray 137 The m/z values within the specified range. 138 mz_domain_low_Y_cutoff : ndarray 139 The abundance values within the specified range. 140 freq_domain_low_Y_cutoff : ndarray or None 141 The frequency values within the specified range, if available. 142 143 """ 144 max_picking_mz = self.settings.max_picking_mz 145 min_picking_mz = self.settings.min_picking_mz 146 147 min_final = where(self.mz_exp_profile > min_picking_mz)[-1][-1] 148 min_start = where(self.mz_exp_profile > min_picking_mz)[0][0] 149 150 ( 151 mz_domain_X_low_cutoff, 152 mz_domain_low_Y_cutoff, 153 ) = ( 154 self.mz_exp_profile[min_start:min_final], 155 self.abundance_profile[min_start:min_final], 156 ) 157 158 max_final = where(self.mz_exp_profile < max_picking_mz)[-1][-1] 159 max_start = where(self.mz_exp_profile < max_picking_mz)[0][0] 160 161 if self.has_frequency: 162 if self.freq_exp_profile.any(): 163 freq_domain_low_Y_cutoff = self.freq_exp_profile[min_start:min_final] 164 165 return ( 166 mz_domain_X_low_cutoff[max_start:max_final], 167 mz_domain_low_Y_cutoff[max_start:max_final], 168 freq_domain_low_Y_cutoff[max_start:max_final], 169 ) 170 171 else: 172 return ( 173 mz_domain_X_low_cutoff[max_start:max_final], 174 mz_domain_low_Y_cutoff[max_start:max_final], 175 None, 176 ) 177 178 @staticmethod 179 def extrapolate_axis(initial_array, pts): 180 """ 181 This function will extrapolate an input array in both directions by N pts. 182 183 Parameters 184 ---------- 185 initial_array : ndarray 186 The input array. 187 pts : int 188 The number of points to extrapolate. 189 190 Returns 191 ------- 192 ndarray 193 The extrapolated array. 194 195 Notes 196 -------- 197 This is a static method. 198 """ 199 initial_array_len = len(initial_array) 200 right_delta = initial_array[-1] - initial_array[-2] 201 left_delta = initial_array[1] - initial_array[0] 202 203 # Create an array with extra space for extrapolation 204 pad_array = zeros(initial_array_len + 2 * pts) 205 206 # Copy original array into the middle of the padded array 207 pad_array[pts : pts + initial_array_len] = initial_array 208 209 # Extrapolate the right side 210 for pt in range(pts): 211 final_value = initial_array[-1] 212 value_to_add = right_delta * (pt + 1) 213 new_value = final_value + value_to_add 214 pad_array[initial_array_len + pts + pt] = new_value 215 216 # Extrapolate the left side 217 for pt in range(pts): 218 first_value = initial_array[0] 219 value_to_subtract = left_delta * (pt + 1) 220 new_value = first_value - value_to_subtract 221 pad_array[pts - pt - 1] = new_value 222 223 return pad_array 224 225 def extrapolate_axes_for_pp(self, mz=None, abund=None, freq=None): 226 """Extrapolate the m/z axis and fill the abundance axis with 0s. 227 228 Parameters 229 ---------- 230 mz : ndarray or None 231 The m/z axis, if available. If None, the experimental m/z axis is used. 232 abund : ndarray or None 233 The abundance axis, if available. If None, the experimental abundance axis is used. 234 freq : ndarray or None 235 The frequency axis, if available. If None, the experimental frequency axis is used. 236 237 Returns 238 ------- 239 mz : ndarray 240 The extrapolated m/z axis. 241 abund : ndarray 242 The abundance axis with 0s filled. 243 freq : ndarray or None 244 The extrapolated frequency axis, if available. 245 246 Notes 247 -------- 248 This function will extrapolate the mz axis by the number of datapoints specified in the settings, 249 and fill the abundance axis with 0s. 250 This should prevent peak picking issues at the spectrum edge. 251 252 """ 253 # Check if the input arrays are provided 254 if mz is None or abund is None: 255 mz, abund = self.mz_exp_profile, self.abundance_profile 256 if self.has_frequency: 257 freq = self.freq_exp_profile 258 else: 259 freq = None 260 pts = self.settings.picking_point_extrapolate 261 if pts == 0: 262 return mz, abund, freq 263 264 mz = self.extrapolate_axis(mz, pts) 265 abund = pad(abund, (pts, pts), mode="constant", constant_values=(0, 0)) 266 if freq is not None: 267 freq = self.extrapolate_axis(freq, pts) 268 return mz, abund, freq 269 270 def do_peak_picking(self): 271 """Perform peak picking.""" 272 mz, abundance, freq = self.prepare_peak_picking_data() 273 274 if ( 275 self.label == Labels.bruker_frequency 276 or self.label == Labels.midas_frequency 277 ): 278 self.calc_centroid(mz, abundance, freq) 279 280 elif self.label == Labels.thermo_profile: 281 self.calc_centroid(mz, abundance, freq) 282 283 elif self.label == Labels.bruker_profile: 284 self.calc_centroid(mz, abundance, freq) 285 286 elif self.label == Labels.booster_profile: 287 self.calc_centroid(mz, abundance, freq) 288 289 elif self.label == Labels.simulated_profile: 290 self.calc_centroid(mz, abundance, freq) 291 292 else: 293 raise Exception("Unknow mass spectrum type", self.label) 294 295 def find_minima(self, apex_index, abundance, len_abundance, right=True): 296 """Find the minima of a peak. 297 298 Parameters 299 ---------- 300 apex_index : int 301 The index of the peak apex. 302 abundance : ndarray 303 The abundance values. 304 len_abundance : int 305 The length of the abundance array. 306 right : bool, optional 307 Flag indicating whether to search for minima to the right of the apex (default is True). 308 309 Returns 310 ------- 311 int 312 The index of the minima. 313 314 """ 315 j = apex_index 316 317 if right: 318 minima = abundance[j] > abundance[j + 1] 319 else: 320 minima = abundance[j] > abundance[j - 1] 321 322 while minima: 323 if j == 1 or j == len_abundance - 2: 324 break 325 326 if right: 327 j += 1 328 329 minima = abundance[j] >= abundance[j + 1] 330 331 else: 332 j -= 1 333 minima = abundance[j] >= abundance[j - 1] 334 335 if right: 336 return j 337 else: 338 return j 339 340 @staticmethod 341 def linear_fit_calc(intes, massa, index_term, index_sign): 342 """ 343 Algebraic solution to a linear fit - roughly 25-50x faster than numpy polyfit when passing only two vals and doing a 1st order fit 344 345 Parameters 346 ---------- 347 intes : ndarray 348 The intensity values. 349 massa : ndarray 350 The mass values. 351 index_term : int 352 The index of the current term. 353 index_sign : str 354 The index sign 355 356 Returns 357 ------- 358 ndarray 359 The coefficients of the linear fit. 360 361 Notes 362 -------- 363 This is a static method. 364 """ 365 if index_sign == "+": 366 x1, x2 = massa[index_term], massa[index_term + 1] 367 y1, y2 = intes[index_term], intes[index_term + 1] 368 elif index_sign == "-": 369 x1, x2 = massa[index_term], massa[index_term - 1] 370 y1, y2 = intes[index_term], intes[index_term - 1] 371 else: 372 warnings.warn("error in linear fit calc, unknown index sign") 373 374 # Calculate the slope (m) 375 slope = (y2 - y1) / (x2 - x1) 376 377 # Calculate the intercept (b) 378 intercept = y1 - slope * x1 379 380 # The coefficients array would be [slope, intercept] 381 coefficients = array([slope, intercept]) 382 return coefficients 383 384 def calculate_resolving_power(self, intes, massa, current_index): 385 """Calculate the resolving power of a peak. 386 387 Parameters 388 ---------- 389 intes : ndarray 390 The intensity values. 391 massa : ndarray 392 The mass values. 393 current_index : int 394 The index of the current peak. 395 396 Returns 397 ------- 398 float 399 The resolving power of the peak, or ``nan`` if a half-maximum 400 crossing does not exist on both sides (spectrum edge truncates 401 the peak). 402 403 Notes 404 -------- 405 This is a conservative calculation of resolving power, 406 the peak need to be resolved at least at the half-maximum magnitude, 407 otherwise, the combined full width at half maximum is used to calculate resolving power. 408 409 """ 410 411 peak_height = intes[current_index] 412 target_peak_height = peak_height / 2 413 n_points = len(intes) 414 415 index_minus = current_index 416 while index_minus > 0 and intes[index_minus] >= target_peak_height: 417 index_minus -= 1 418 if intes[index_minus] >= target_peak_height: 419 return nan 420 421 index_plus = current_index 422 while index_plus < n_points - 1 and intes[index_plus] >= target_peak_height: 423 index_plus += 1 424 if intes[index_plus] >= target_peak_height: 425 return nan 426 427 if self.mspeaks_settings.legacy_centroid_polyfit: 428 x = [massa[index_minus], massa[index_minus + 1]] 429 y = [intes[index_minus], intes[index_minus + 1]] 430 coefficients = polyfit(x, y, 1) 431 else: 432 coefficients = self.linear_fit_calc( 433 intes, massa, index_minus, index_sign="+" 434 ) 435 436 a = coefficients[0] 437 b = coefficients[1] 438 if self.mspeaks_settings.legacy_resolving_power: 439 y_intercept = intes[index_minus] + ( 440 (intes[index_minus + 1] - intes[index_minus]) / 2 441 ) 442 else: 443 y_intercept = target_peak_height 444 massa1 = (y_intercept - b) / a 445 446 if self.mspeaks_settings.legacy_centroid_polyfit: 447 x = [massa[index_plus], massa[index_plus - 1]] 448 y = [intes[index_plus], intes[index_plus - 1]] 449 coefficients = polyfit(x, y, 1) 450 else: 451 coefficients = self.linear_fit_calc( 452 intes, massa, index_plus, index_sign="-" 453 ) 454 455 a = coefficients[0] 456 b = coefficients[1] 457 458 if self.mspeaks_settings.legacy_resolving_power: 459 y_intercept = intes[index_plus - 1] + ( 460 (intes[index_plus] - intes[index_plus - 1]) / 2 461 ) 462 else: 463 y_intercept = target_peak_height 464 465 massa2 = (y_intercept - b) / a 466 467 if massa1 > massa2: 468 resolvingpower = massa[current_index] / (massa1 - massa2) 469 470 else: 471 resolvingpower = massa[current_index] / (massa2 - massa1) 472 473 return resolvingpower 474 475 def cal_minima(self, mass, abun): 476 """Calculate the minima of a peak. 477 478 Parameters 479 ---------- 480 mass : ndarray 481 The mass values. 482 abun : ndarray 483 The abundance values. 484 485 Returns 486 ------- 487 ndarray or None 488 The mass values at the minima, if found. 489 490 """ 491 abun = -abun 492 493 dy = abun[1:] - abun[:-1] 494 495 # replaces nan for infinity 496 indices_nan = where(isnan(abun))[0] 497 498 if indices_nan.size: 499 abun[indices_nan] = inf 500 dy[where(isnan(dy))[0]] = inf 501 502 indexes = where((hstack((dy, 0)) < 0) & (hstack((0, dy)) > 0))[0] 503 504 if indexes.size: 505 return mass[indexes], abun[indexes] 506 507 def calc_centroid(self, mass, abund, freq): 508 """Calculate the centroid of a peak. 509 510 Parameters 511 ---------- 512 mass : ndarray 513 The mass values. 514 abund : ndarray 515 The abundance values. 516 freq : ndarray or None 517 The frequency values, if available. 518 519 Returns 520 ------- 521 None 522 523 """ 524 525 max_height = self.mspeaks_settings.peak_height_max_percent 526 max_prominence = self.mspeaks_settings.peak_max_prominence_percent 527 min_peak_datapoints = self.mspeaks_settings.min_peak_datapoints 528 peak_derivative_threshold = self.mspeaks_settings.peak_derivative_threshold 529 max_abun = max(abund) 530 peak_height_diff = lambda hi, li: ((abund[hi] - abund[li]) / max_abun) * 100 531 532 domain = mass 533 signal = abund 534 len_signal = len(signal) 535 536 signal_threshold, factor = self.get_threshold(abund) 537 max_signal = factor 538 539 correct_baseline = False 540 541 include_indexes = sp.peak_picking_first_derivative( 542 domain, 543 signal, 544 max_height, 545 max_prominence, 546 max_signal, 547 min_peak_datapoints, 548 peak_derivative_threshold, 549 signal_threshold=signal_threshold, 550 correct_baseline=correct_baseline, 551 abun_norm=1, 552 plot_res=False, 553 ) 554 555 for indexes_tuple in include_indexes: 556 apex_index = indexes_tuple[1] 557 558 peak_indexes = self.check_prominence( 559 abund, apex_index, len_signal, peak_height_diff 560 ) 561 562 if peak_indexes: 563 mz_exp_centroid, freq_centr, intes_centr = self.find_apex_fit_quadratic( 564 mass, abund, freq, apex_index 565 ) 566 567 if mz_exp_centroid: 568 peak_resolving_power = self.calculate_resolving_power( 569 abund, mass, apex_index 570 ) 571 s2n = intes_centr / self.baseline_noise_std 572 self.add_mspeak( 573 self.polarity, 574 mz_exp_centroid, 575 abund[apex_index], 576 peak_resolving_power, 577 s2n, 578 indexes_tuple, 579 exp_freq=freq_centr, 580 ms_parent=self, 581 ) 582 # pyplot.plot(domain[start_index: final_index + 1], signal[start_index:final_index + 1], c='black') 583 # pyplot.show() 584 585 def get_threshold(self, intes): 586 """Get the intensity threshold for peak picking. 587 588 Parameters 589 ---------- 590 intes : ndarray 591 The intensity values. 592 593 Returns 594 ------- 595 float 596 The intensity threshold. 597 float 598 The factor to multiply the intensity threshold by. 599 """ 600 601 intes = array(intes).astype(float) 602 603 self._resolve_noise_threshold_method() 604 noise_threshold_method = self.settings.noise_threshold_method 605 606 if noise_threshold_method == "minima": 607 if self.is_centroid: 608 warnings.warn( 609 "Auto threshould is disabled for centroid data, returning 0" 610 ) 611 factor = 1 612 abundance_threshold = 1e-20 613 # print(self.settings.noise_threshold_min_std) 614 else: 615 abundance_threshold = self.baseline_noise + ( 616 self.settings.noise_threshold_min_std * self.baseline_noise_std 617 ) 618 factor = 1 619 620 elif noise_threshold_method == "signal_noise": 621 abundance_threshold = self.settings.noise_threshold_min_s2n 622 if self.is_centroid: 623 factor = 1 624 else: 625 factor = self.baseline_noise_std 626 627 elif noise_threshold_method == "relative_abundance": 628 abundance_threshold = self.settings.noise_threshold_min_relative_abundance 629 factor = intes.max() / 100 630 631 elif noise_threshold_method == "absolute_abundance": 632 abundance_threshold = self.settings.noise_threshold_absolute_abundance 633 factor = 1 634 635 elif noise_threshold_method == "log": 636 if self.is_centroid: 637 raise Exception("log noise Not tested for centroid data") 638 abundance_threshold = self.settings.noise_threshold_log_nsigma 639 factor = self.baseline_noise_std 640 641 else: 642 raise Exception( 643 "%s method was not implemented, please refer to corems.mass_spectrum.calc.NoiseCalc Class" 644 % noise_threshold_method 645 ) 646 647 return abundance_threshold, factor 648 649 @staticmethod 650 def algebraic_quadratic(list_mass, list_y): 651 """ 652 Find the apex of a peak - algebraically. 653 Faster than using numpy polyfit by ~28x per fit. 654 655 Parameters 656 ---------- 657 list_mass : ndarray 658 list of m/z values (3 points) 659 list_y : ndarray 660 list of abundance values (3 points) 661 662 Returns 663 ------- 664 a, b, c: float 665 coefficients of the quadratic equation. 666 667 Notes 668 -------- 669 This is a static method. 670 """ 671 x_1, x_2, x_3 = list_mass 672 y_1, y_2, y_3 = list_y 673 674 a = ( 675 y_1 / ((x_1 - x_2) * (x_1 - x_3)) 676 + y_2 / ((x_2 - x_1) * (x_2 - x_3)) 677 + y_3 / ((x_3 - x_1) * (x_3 - x_2)) 678 ) 679 680 b = ( 681 -y_1 * (x_2 + x_3) / ((x_1 - x_2) * (x_1 - x_3)) 682 - y_2 * (x_1 + x_3) / ((x_2 - x_1) * (x_2 - x_3)) 683 - y_3 * (x_1 + x_2) / ((x_3 - x_1) * (x_3 - x_2)) 684 ) 685 686 c = ( 687 y_1 * x_2 * x_3 / ((x_1 - x_2) * (x_1 - x_3)) 688 + y_2 * x_1 * x_3 / ((x_2 - x_1) * (x_2 - x_3)) 689 + y_3 * x_1 * x_2 / ((x_3 - x_1) * (x_3 - x_2)) 690 ) 691 return a, b, c 692 693 def find_apex_fit_quadratic(self, mass, abund, freq, current_index): 694 """ 695 Find the apex of a peak. 696 697 Parameters 698 ---------- 699 mass : ndarray 700 The mass values. 701 abund : ndarray 702 The abundance values. 703 freq : ndarray or None 704 The frequency values, if available. 705 current_index : int 706 The index of the current peak. 707 708 709 Returns 710 ------- 711 float 712 The m/z value of the peak apex. 713 float 714 The frequency value of the peak apex, if available. 715 float 716 The abundance value of the peak apex. 717 718 """ 719 # calc prominence 720 # peak_indexes = self.check_prominence(abund, current_index, len_abundance, peak_height_diff ) 721 722 # if not peak_indexes: 723 724 # return None, None, None, None 725 726 # else: 727 728 # fit parabola to three most abundant datapoints 729 list_mass = [ 730 mass[current_index - 1], 731 mass[current_index], 732 mass[current_index + 1], 733 ] 734 list_y = [ 735 abund[current_index - 1], 736 abund[current_index], 737 abund[current_index + 1], 738 ] 739 740 if self.mspeaks_settings.legacy_centroid_polyfit: 741 z = polyfit(list_mass, list_y, 2) 742 a = z[0] 743 b = z[1] 744 else: 745 a, b, c = self.algebraic_quadratic(list_mass, list_y) 746 747 calculated = -b / (2 * a) 748 749 if calculated < 1 or int(calculated) != int(list_mass[1]): 750 mz_exp_centroid = list_mass[1] 751 752 else: 753 mz_exp_centroid = calculated 754 755 if ( 756 self.label == Labels.bruker_frequency 757 or self.label == Labels.midas_frequency 758 ): 759 # fit parabola to three most abundant frequency datapoints 760 list_freq = [ 761 freq[current_index - 1], 762 freq[current_index], 763 freq[current_index + 1], 764 ] 765 if self.mspeaks_settings.legacy_centroid_polyfit: 766 z = polyfit(list_mass, list_y, 2) 767 a = z[0] 768 b = z[1] 769 else: 770 a, b, c = self.algebraic_quadratic(list_mass, list_y) 771 772 calculated_freq = -b / (2 * a) 773 774 if calculated_freq < 1 or int(calculated_freq) != freq[current_index]: 775 freq_centr = list_freq[1] 776 777 else: 778 freq_centr = calculated_freq 779 780 else: 781 freq_centr = None 782 783 if self.mspeaks_settings.legacy_centroid_polyfit: 784 abundance_centroid = abund[current_index] 785 else: 786 abundance_centroid = a * mz_exp_centroid**2 + b * mz_exp_centroid + c 787 788 return mz_exp_centroid, freq_centr, abundance_centroid 789 790 def check_prominence( 791 self, abun, current_index, len_abundance, peak_height_diff 792 ) -> tuple or False: 793 """Check the prominence of a peak. 794 795 Parameters 796 ---------- 797 abun : ndarray 798 The abundance values. 799 current_index : int 800 The index of the current peak. 801 len_abundance : int 802 The length of the abundance array. 803 peak_height_diff : function 804 The function to calculate the peak height difference. 805 806 Returns 807 ------- 808 tuple or False 809 A tuple containing the indexes of the peak, if the prominence is above the threshold. 810 Otherwise, False. 811 812 """ 813 814 final_index = self.find_minima(current_index, abun, len_abundance, right=True) 815 816 start_index = self.find_minima(current_index, abun, len_abundance, right=False) 817 818 peak_indexes = (current_index - 1, current_index, current_index + 1) 819 820 if ( 821 min( 822 peak_height_diff(current_index, start_index), 823 peak_height_diff(current_index, final_index), 824 ) 825 > self.mspeaks_settings.peak_min_prominence_percent 826 ): 827 return peak_indexes 828 829 else: 830 return False 831 832 def use_the_max(self, mass, abund, current_index, len_abundance, peak_height_diff): 833 """Use the max peak height as the centroid 834 835 Parameters 836 ---------- 837 mass : ndarray 838 The mass values. 839 abund : ndarray 840 The abundance values. 841 current_index : int 842 The index of the current peak. 843 len_abundance : int 844 The length of the abundance array. 845 peak_height_diff : function 846 The function to calculate the peak height difference. 847 848 Returns 849 ------- 850 float 851 The m/z value of the peak apex. 852 float 853 The abundance value of the peak apex. 854 tuple or None 855 A tuple containing the indexes of the peak, if the prominence is above the threshold. 856 Otherwise, None. 857 """ 858 859 peak_indexes = self.check_prominence( 860 abund, current_index, len_abundance, peak_height_diff 861 ) 862 863 if not peak_indexes: 864 return None, None, None 865 866 else: 867 return mass[current_index], abund[current_index], peak_indexes 868 869 def calc_centroid_legacy(self, mass, abund, freq): 870 """Legacy centroid calculation 871 Deprecated - for deletion. 872 873 """ 874 warnings.warn( 875 "Legacy centroid calculation is deprecated. Please use the new centroid calculation method." 876 ) 877 pass 878 if False: 879 len_abundance = len(abund) 880 881 max_abundance = max(abund) 882 883 peak_height_diff = lambda hi, li: ( 884 ((abund[hi] - abund[li]) / max_abundance) * 100 885 ) 886 887 abundance_threshold, factor = self.get_threshold(abund) 888 # print(abundance_threshold, factor) 889 # find indices of all peaks 890 dy = abund[1:] - abund[:-1] 891 892 # replaces nan for infi nity 893 indices_nan = where(isnan(abund))[0] 894 895 if indices_nan.size: 896 abund[indices_nan] = inf 897 dy[where(isnan(dy))[0]] = inf 898 899 indexes = where((hstack((dy, 0)) < 0) & (hstack((0, dy)) > 0))[0] 900 901 # noise threshold 902 if indexes.size and abundance_threshold is not None: 903 indexes = indexes[abund[indexes] / factor >= abundance_threshold] 904 # filter out 'peaks' within 3 points of the spectrum limits 905 # remove entries within 3 points of upper limit 906 indexes = [x for x in indexes if (len_abundance - x) > 3] 907 # remove entries within 3 points of zero 908 indexes = [x for x in indexes if x > 3] 909 910 for current_index in indexes: 911 if self.label == Labels.simulated_profile: 912 mz_exp_centroid, intes_centr, peak_indexes = self.use_the_max( 913 mass, abund, current_index, len_abundance, peak_height_diff 914 ) 915 if mz_exp_centroid: 916 peak_resolving_power = self.calculate_resolving_power( 917 abund, mass, current_index 918 ) 919 s2n = intes_centr / self.baseline_noise_std 920 freq_centr = None 921 self.add_mspeak( 922 self.polarity, 923 mz_exp_centroid, 924 abund[current_index], 925 peak_resolving_power, 926 s2n, 927 peak_indexes, 928 exp_freq=freq_centr, 929 ms_parent=self, 930 ) 931 932 else: 933 mz_exp_centroid, freq_centr, intes_centr, peak_indexes = ( 934 self.find_apex_fit_quadratic( 935 mass, 936 abund, 937 freq, 938 current_index, 939 len_abundance, 940 peak_height_diff, 941 ) 942 ) 943 if mz_exp_centroid: 944 try: 945 peak_resolving_power = self.calculate_resolving_power( 946 abund, mass, current_index 947 ) 948 except IndexError: 949 print("index error, skipping peak") 950 continue 951 952 s2n = intes_centr / self.baseline_noise_std 953 self.add_mspeak( 954 self.polarity, 955 mz_exp_centroid, 956 abund[current_index], 957 peak_resolving_power, 958 s2n, 959 peak_indexes, 960 exp_freq=freq_centr, 961 ms_parent=self, 962 )
24class PeakPicking: 25 """Class for peak picking. 26 27 Parameters 28 ---------- 29 None 30 31 Attributes 32 ---------- 33 None 34 35 Methods 36 ------- 37 * prepare_peak_picking_data(). 38 Prepare the mz, abundance, and frequence data for peak picking. 39 * cut_mz_domain_peak_picking(). 40 Cut the m/z domain for peak picking. 41 * extrapolate_axes_for_pp(mz=None, abund=None, freq=None). 42 Extrapolate the m/z axis and fill the abundance axis with 0s. 43 * do_peak_picking(). 44 Perform peak picking. 45 * find_minima(apex_index, abundance, len_abundance, right=True). 46 Find the minima of a peak. 47 * linear_fit_calc(intes, massa, index_term, index_sign). 48 Algebraic solution to a linear fit. 49 * calculate_resolving_power(intes, massa, current_index). 50 Calculate the resolving power of a peak. 51 * cal_minima(mass, abun). 52 Calculate the minima of a peak. 53 * calc_centroid(mass, abund, freq). 54 Calculate the centroid of a peak. 55 * get_threshold(intes). 56 Get the intensity threshold for peak picking. 57 * algebraic_quadratic(list_mass, list_y). 58 Find the apex of a peak - algebraically. 59 * find_apex_fit_quadratic(mass, abund, freq, current_index). 60 Find the apex of a peak. 61 * check_prominence(abun, current_index, len_abundance, peak_height_diff). 62 Check the prominence of a peak. 63 * use_the_max(mass, abund, current_index, len_abundance, peak_height_diff). 64 Use the max peak height as the centroid. 65 * calc_centroid_legacy(mass, abund, freq). 66 Legacy centroid calculation. Deprecated - for deletion. 67 68 """ 69 70 def prepare_peak_picking_data(self): 71 """Prepare the data for peak picking. 72 73 This function will prepare the m/z, abundance, and frequency data for peak picking according to the settings. 74 75 Returns 76 ------- 77 mz : ndarray 78 The m/z axis. 79 abundance : ndarray 80 The abundance axis. 81 freq : ndarray or None 82 The frequency axis, if available. 83 """ 84 # First apply cut_mz_domain_peak_picking 85 mz, abundance, freq = self.cut_mz_domain_peak_picking() 86 87 # Then extrapolate the axes for peak picking 88 if self.settings.picking_point_extrapolate > 0: 89 mz, abundance, freq = self.extrapolate_axes_for_pp(mz, abundance, freq) 90 return mz, abundance, freq 91 92 def cut_mz_domain_peak_picking(self): 93 """ 94 Cut the m/z domain for peak picking. 95 96 Simplified function 97 98 Returns 99 ------- 100 mz_domain_X_low_cutoff : ndarray 101 The m/z values within the specified range. 102 mz_domain_low_Y_cutoff : ndarray 103 The abundance values within the specified range. 104 freq_domain_low_Y_cutoff : ndarray or None 105 The frequency values within the specified range, if available. 106 107 """ 108 max_picking_mz = self.settings.max_picking_mz 109 min_picking_mz = self.settings.min_picking_mz 110 111 # min_start = where(self.mz_exp_profile > min_picking_mz)[0][0] 112 # max_final = where(self.mz_exp_profile < max_picking_mz)[-1][-1] 113 min_start = searchsorted(a=self.mz_exp_profile, v=min_picking_mz) 114 max_final = searchsorted(a=self.mz_exp_profile, v=max_picking_mz) 115 116 if self.has_frequency: 117 if self.freq_exp_profile.any(): 118 return ( 119 self.mz_exp_profile[min_start:max_final], 120 self.abundance_profile[min_start:max_final], 121 self.freq_exp_profile[min_start:max_final], 122 ) 123 124 else: 125 return ( 126 self.mz_exp_profile[min_start:max_final], 127 self.abundance_profile[min_start:max_final], 128 None, 129 ) 130 131 def legacy_cut_mz_domain_peak_picking(self): 132 """ 133 Cut the m/z domain for peak picking. 134 DEPRECATED 135 Returns 136 ------- 137 mz_domain_X_low_cutoff : ndarray 138 The m/z values within the specified range. 139 mz_domain_low_Y_cutoff : ndarray 140 The abundance values within the specified range. 141 freq_domain_low_Y_cutoff : ndarray or None 142 The frequency values within the specified range, if available. 143 144 """ 145 max_picking_mz = self.settings.max_picking_mz 146 min_picking_mz = self.settings.min_picking_mz 147 148 min_final = where(self.mz_exp_profile > min_picking_mz)[-1][-1] 149 min_start = where(self.mz_exp_profile > min_picking_mz)[0][0] 150 151 ( 152 mz_domain_X_low_cutoff, 153 mz_domain_low_Y_cutoff, 154 ) = ( 155 self.mz_exp_profile[min_start:min_final], 156 self.abundance_profile[min_start:min_final], 157 ) 158 159 max_final = where(self.mz_exp_profile < max_picking_mz)[-1][-1] 160 max_start = where(self.mz_exp_profile < max_picking_mz)[0][0] 161 162 if self.has_frequency: 163 if self.freq_exp_profile.any(): 164 freq_domain_low_Y_cutoff = self.freq_exp_profile[min_start:min_final] 165 166 return ( 167 mz_domain_X_low_cutoff[max_start:max_final], 168 mz_domain_low_Y_cutoff[max_start:max_final], 169 freq_domain_low_Y_cutoff[max_start:max_final], 170 ) 171 172 else: 173 return ( 174 mz_domain_X_low_cutoff[max_start:max_final], 175 mz_domain_low_Y_cutoff[max_start:max_final], 176 None, 177 ) 178 179 @staticmethod 180 def extrapolate_axis(initial_array, pts): 181 """ 182 This function will extrapolate an input array in both directions by N pts. 183 184 Parameters 185 ---------- 186 initial_array : ndarray 187 The input array. 188 pts : int 189 The number of points to extrapolate. 190 191 Returns 192 ------- 193 ndarray 194 The extrapolated array. 195 196 Notes 197 -------- 198 This is a static method. 199 """ 200 initial_array_len = len(initial_array) 201 right_delta = initial_array[-1] - initial_array[-2] 202 left_delta = initial_array[1] - initial_array[0] 203 204 # Create an array with extra space for extrapolation 205 pad_array = zeros(initial_array_len + 2 * pts) 206 207 # Copy original array into the middle of the padded array 208 pad_array[pts : pts + initial_array_len] = initial_array 209 210 # Extrapolate the right side 211 for pt in range(pts): 212 final_value = initial_array[-1] 213 value_to_add = right_delta * (pt + 1) 214 new_value = final_value + value_to_add 215 pad_array[initial_array_len + pts + pt] = new_value 216 217 # Extrapolate the left side 218 for pt in range(pts): 219 first_value = initial_array[0] 220 value_to_subtract = left_delta * (pt + 1) 221 new_value = first_value - value_to_subtract 222 pad_array[pts - pt - 1] = new_value 223 224 return pad_array 225 226 def extrapolate_axes_for_pp(self, mz=None, abund=None, freq=None): 227 """Extrapolate the m/z axis and fill the abundance axis with 0s. 228 229 Parameters 230 ---------- 231 mz : ndarray or None 232 The m/z axis, if available. If None, the experimental m/z axis is used. 233 abund : ndarray or None 234 The abundance axis, if available. If None, the experimental abundance axis is used. 235 freq : ndarray or None 236 The frequency axis, if available. If None, the experimental frequency axis is used. 237 238 Returns 239 ------- 240 mz : ndarray 241 The extrapolated m/z axis. 242 abund : ndarray 243 The abundance axis with 0s filled. 244 freq : ndarray or None 245 The extrapolated frequency axis, if available. 246 247 Notes 248 -------- 249 This function will extrapolate the mz axis by the number of datapoints specified in the settings, 250 and fill the abundance axis with 0s. 251 This should prevent peak picking issues at the spectrum edge. 252 253 """ 254 # Check if the input arrays are provided 255 if mz is None or abund is None: 256 mz, abund = self.mz_exp_profile, self.abundance_profile 257 if self.has_frequency: 258 freq = self.freq_exp_profile 259 else: 260 freq = None 261 pts = self.settings.picking_point_extrapolate 262 if pts == 0: 263 return mz, abund, freq 264 265 mz = self.extrapolate_axis(mz, pts) 266 abund = pad(abund, (pts, pts), mode="constant", constant_values=(0, 0)) 267 if freq is not None: 268 freq = self.extrapolate_axis(freq, pts) 269 return mz, abund, freq 270 271 def do_peak_picking(self): 272 """Perform peak picking.""" 273 mz, abundance, freq = self.prepare_peak_picking_data() 274 275 if ( 276 self.label == Labels.bruker_frequency 277 or self.label == Labels.midas_frequency 278 ): 279 self.calc_centroid(mz, abundance, freq) 280 281 elif self.label == Labels.thermo_profile: 282 self.calc_centroid(mz, abundance, freq) 283 284 elif self.label == Labels.bruker_profile: 285 self.calc_centroid(mz, abundance, freq) 286 287 elif self.label == Labels.booster_profile: 288 self.calc_centroid(mz, abundance, freq) 289 290 elif self.label == Labels.simulated_profile: 291 self.calc_centroid(mz, abundance, freq) 292 293 else: 294 raise Exception("Unknow mass spectrum type", self.label) 295 296 def find_minima(self, apex_index, abundance, len_abundance, right=True): 297 """Find the minima of a peak. 298 299 Parameters 300 ---------- 301 apex_index : int 302 The index of the peak apex. 303 abundance : ndarray 304 The abundance values. 305 len_abundance : int 306 The length of the abundance array. 307 right : bool, optional 308 Flag indicating whether to search for minima to the right of the apex (default is True). 309 310 Returns 311 ------- 312 int 313 The index of the minima. 314 315 """ 316 j = apex_index 317 318 if right: 319 minima = abundance[j] > abundance[j + 1] 320 else: 321 minima = abundance[j] > abundance[j - 1] 322 323 while minima: 324 if j == 1 or j == len_abundance - 2: 325 break 326 327 if right: 328 j += 1 329 330 minima = abundance[j] >= abundance[j + 1] 331 332 else: 333 j -= 1 334 minima = abundance[j] >= abundance[j - 1] 335 336 if right: 337 return j 338 else: 339 return j 340 341 @staticmethod 342 def linear_fit_calc(intes, massa, index_term, index_sign): 343 """ 344 Algebraic solution to a linear fit - roughly 25-50x faster than numpy polyfit when passing only two vals and doing a 1st order fit 345 346 Parameters 347 ---------- 348 intes : ndarray 349 The intensity values. 350 massa : ndarray 351 The mass values. 352 index_term : int 353 The index of the current term. 354 index_sign : str 355 The index sign 356 357 Returns 358 ------- 359 ndarray 360 The coefficients of the linear fit. 361 362 Notes 363 -------- 364 This is a static method. 365 """ 366 if index_sign == "+": 367 x1, x2 = massa[index_term], massa[index_term + 1] 368 y1, y2 = intes[index_term], intes[index_term + 1] 369 elif index_sign == "-": 370 x1, x2 = massa[index_term], massa[index_term - 1] 371 y1, y2 = intes[index_term], intes[index_term - 1] 372 else: 373 warnings.warn("error in linear fit calc, unknown index sign") 374 375 # Calculate the slope (m) 376 slope = (y2 - y1) / (x2 - x1) 377 378 # Calculate the intercept (b) 379 intercept = y1 - slope * x1 380 381 # The coefficients array would be [slope, intercept] 382 coefficients = array([slope, intercept]) 383 return coefficients 384 385 def calculate_resolving_power(self, intes, massa, current_index): 386 """Calculate the resolving power of a peak. 387 388 Parameters 389 ---------- 390 intes : ndarray 391 The intensity values. 392 massa : ndarray 393 The mass values. 394 current_index : int 395 The index of the current peak. 396 397 Returns 398 ------- 399 float 400 The resolving power of the peak, or ``nan`` if a half-maximum 401 crossing does not exist on both sides (spectrum edge truncates 402 the peak). 403 404 Notes 405 -------- 406 This is a conservative calculation of resolving power, 407 the peak need to be resolved at least at the half-maximum magnitude, 408 otherwise, the combined full width at half maximum is used to calculate resolving power. 409 410 """ 411 412 peak_height = intes[current_index] 413 target_peak_height = peak_height / 2 414 n_points = len(intes) 415 416 index_minus = current_index 417 while index_minus > 0 and intes[index_minus] >= target_peak_height: 418 index_minus -= 1 419 if intes[index_minus] >= target_peak_height: 420 return nan 421 422 index_plus = current_index 423 while index_plus < n_points - 1 and intes[index_plus] >= target_peak_height: 424 index_plus += 1 425 if intes[index_plus] >= target_peak_height: 426 return nan 427 428 if self.mspeaks_settings.legacy_centroid_polyfit: 429 x = [massa[index_minus], massa[index_minus + 1]] 430 y = [intes[index_minus], intes[index_minus + 1]] 431 coefficients = polyfit(x, y, 1) 432 else: 433 coefficients = self.linear_fit_calc( 434 intes, massa, index_minus, index_sign="+" 435 ) 436 437 a = coefficients[0] 438 b = coefficients[1] 439 if self.mspeaks_settings.legacy_resolving_power: 440 y_intercept = intes[index_minus] + ( 441 (intes[index_minus + 1] - intes[index_minus]) / 2 442 ) 443 else: 444 y_intercept = target_peak_height 445 massa1 = (y_intercept - b) / a 446 447 if self.mspeaks_settings.legacy_centroid_polyfit: 448 x = [massa[index_plus], massa[index_plus - 1]] 449 y = [intes[index_plus], intes[index_plus - 1]] 450 coefficients = polyfit(x, y, 1) 451 else: 452 coefficients = self.linear_fit_calc( 453 intes, massa, index_plus, index_sign="-" 454 ) 455 456 a = coefficients[0] 457 b = coefficients[1] 458 459 if self.mspeaks_settings.legacy_resolving_power: 460 y_intercept = intes[index_plus - 1] + ( 461 (intes[index_plus] - intes[index_plus - 1]) / 2 462 ) 463 else: 464 y_intercept = target_peak_height 465 466 massa2 = (y_intercept - b) / a 467 468 if massa1 > massa2: 469 resolvingpower = massa[current_index] / (massa1 - massa2) 470 471 else: 472 resolvingpower = massa[current_index] / (massa2 - massa1) 473 474 return resolvingpower 475 476 def cal_minima(self, mass, abun): 477 """Calculate the minima of a peak. 478 479 Parameters 480 ---------- 481 mass : ndarray 482 The mass values. 483 abun : ndarray 484 The abundance values. 485 486 Returns 487 ------- 488 ndarray or None 489 The mass values at the minima, if found. 490 491 """ 492 abun = -abun 493 494 dy = abun[1:] - abun[:-1] 495 496 # replaces nan for infinity 497 indices_nan = where(isnan(abun))[0] 498 499 if indices_nan.size: 500 abun[indices_nan] = inf 501 dy[where(isnan(dy))[0]] = inf 502 503 indexes = where((hstack((dy, 0)) < 0) & (hstack((0, dy)) > 0))[0] 504 505 if indexes.size: 506 return mass[indexes], abun[indexes] 507 508 def calc_centroid(self, mass, abund, freq): 509 """Calculate the centroid of a peak. 510 511 Parameters 512 ---------- 513 mass : ndarray 514 The mass values. 515 abund : ndarray 516 The abundance values. 517 freq : ndarray or None 518 The frequency values, if available. 519 520 Returns 521 ------- 522 None 523 524 """ 525 526 max_height = self.mspeaks_settings.peak_height_max_percent 527 max_prominence = self.mspeaks_settings.peak_max_prominence_percent 528 min_peak_datapoints = self.mspeaks_settings.min_peak_datapoints 529 peak_derivative_threshold = self.mspeaks_settings.peak_derivative_threshold 530 max_abun = max(abund) 531 peak_height_diff = lambda hi, li: ((abund[hi] - abund[li]) / max_abun) * 100 532 533 domain = mass 534 signal = abund 535 len_signal = len(signal) 536 537 signal_threshold, factor = self.get_threshold(abund) 538 max_signal = factor 539 540 correct_baseline = False 541 542 include_indexes = sp.peak_picking_first_derivative( 543 domain, 544 signal, 545 max_height, 546 max_prominence, 547 max_signal, 548 min_peak_datapoints, 549 peak_derivative_threshold, 550 signal_threshold=signal_threshold, 551 correct_baseline=correct_baseline, 552 abun_norm=1, 553 plot_res=False, 554 ) 555 556 for indexes_tuple in include_indexes: 557 apex_index = indexes_tuple[1] 558 559 peak_indexes = self.check_prominence( 560 abund, apex_index, len_signal, peak_height_diff 561 ) 562 563 if peak_indexes: 564 mz_exp_centroid, freq_centr, intes_centr = self.find_apex_fit_quadratic( 565 mass, abund, freq, apex_index 566 ) 567 568 if mz_exp_centroid: 569 peak_resolving_power = self.calculate_resolving_power( 570 abund, mass, apex_index 571 ) 572 s2n = intes_centr / self.baseline_noise_std 573 self.add_mspeak( 574 self.polarity, 575 mz_exp_centroid, 576 abund[apex_index], 577 peak_resolving_power, 578 s2n, 579 indexes_tuple, 580 exp_freq=freq_centr, 581 ms_parent=self, 582 ) 583 # pyplot.plot(domain[start_index: final_index + 1], signal[start_index:final_index + 1], c='black') 584 # pyplot.show() 585 586 def get_threshold(self, intes): 587 """Get the intensity threshold for peak picking. 588 589 Parameters 590 ---------- 591 intes : ndarray 592 The intensity values. 593 594 Returns 595 ------- 596 float 597 The intensity threshold. 598 float 599 The factor to multiply the intensity threshold by. 600 """ 601 602 intes = array(intes).astype(float) 603 604 self._resolve_noise_threshold_method() 605 noise_threshold_method = self.settings.noise_threshold_method 606 607 if noise_threshold_method == "minima": 608 if self.is_centroid: 609 warnings.warn( 610 "Auto threshould is disabled for centroid data, returning 0" 611 ) 612 factor = 1 613 abundance_threshold = 1e-20 614 # print(self.settings.noise_threshold_min_std) 615 else: 616 abundance_threshold = self.baseline_noise + ( 617 self.settings.noise_threshold_min_std * self.baseline_noise_std 618 ) 619 factor = 1 620 621 elif noise_threshold_method == "signal_noise": 622 abundance_threshold = self.settings.noise_threshold_min_s2n 623 if self.is_centroid: 624 factor = 1 625 else: 626 factor = self.baseline_noise_std 627 628 elif noise_threshold_method == "relative_abundance": 629 abundance_threshold = self.settings.noise_threshold_min_relative_abundance 630 factor = intes.max() / 100 631 632 elif noise_threshold_method == "absolute_abundance": 633 abundance_threshold = self.settings.noise_threshold_absolute_abundance 634 factor = 1 635 636 elif noise_threshold_method == "log": 637 if self.is_centroid: 638 raise Exception("log noise Not tested for centroid data") 639 abundance_threshold = self.settings.noise_threshold_log_nsigma 640 factor = self.baseline_noise_std 641 642 else: 643 raise Exception( 644 "%s method was not implemented, please refer to corems.mass_spectrum.calc.NoiseCalc Class" 645 % noise_threshold_method 646 ) 647 648 return abundance_threshold, factor 649 650 @staticmethod 651 def algebraic_quadratic(list_mass, list_y): 652 """ 653 Find the apex of a peak - algebraically. 654 Faster than using numpy polyfit by ~28x per fit. 655 656 Parameters 657 ---------- 658 list_mass : ndarray 659 list of m/z values (3 points) 660 list_y : ndarray 661 list of abundance values (3 points) 662 663 Returns 664 ------- 665 a, b, c: float 666 coefficients of the quadratic equation. 667 668 Notes 669 -------- 670 This is a static method. 671 """ 672 x_1, x_2, x_3 = list_mass 673 y_1, y_2, y_3 = list_y 674 675 a = ( 676 y_1 / ((x_1 - x_2) * (x_1 - x_3)) 677 + y_2 / ((x_2 - x_1) * (x_2 - x_3)) 678 + y_3 / ((x_3 - x_1) * (x_3 - x_2)) 679 ) 680 681 b = ( 682 -y_1 * (x_2 + x_3) / ((x_1 - x_2) * (x_1 - x_3)) 683 - y_2 * (x_1 + x_3) / ((x_2 - x_1) * (x_2 - x_3)) 684 - y_3 * (x_1 + x_2) / ((x_3 - x_1) * (x_3 - x_2)) 685 ) 686 687 c = ( 688 y_1 * x_2 * x_3 / ((x_1 - x_2) * (x_1 - x_3)) 689 + y_2 * x_1 * x_3 / ((x_2 - x_1) * (x_2 - x_3)) 690 + y_3 * x_1 * x_2 / ((x_3 - x_1) * (x_3 - x_2)) 691 ) 692 return a, b, c 693 694 def find_apex_fit_quadratic(self, mass, abund, freq, current_index): 695 """ 696 Find the apex of a peak. 697 698 Parameters 699 ---------- 700 mass : ndarray 701 The mass values. 702 abund : ndarray 703 The abundance values. 704 freq : ndarray or None 705 The frequency values, if available. 706 current_index : int 707 The index of the current peak. 708 709 710 Returns 711 ------- 712 float 713 The m/z value of the peak apex. 714 float 715 The frequency value of the peak apex, if available. 716 float 717 The abundance value of the peak apex. 718 719 """ 720 # calc prominence 721 # peak_indexes = self.check_prominence(abund, current_index, len_abundance, peak_height_diff ) 722 723 # if not peak_indexes: 724 725 # return None, None, None, None 726 727 # else: 728 729 # fit parabola to three most abundant datapoints 730 list_mass = [ 731 mass[current_index - 1], 732 mass[current_index], 733 mass[current_index + 1], 734 ] 735 list_y = [ 736 abund[current_index - 1], 737 abund[current_index], 738 abund[current_index + 1], 739 ] 740 741 if self.mspeaks_settings.legacy_centroid_polyfit: 742 z = polyfit(list_mass, list_y, 2) 743 a = z[0] 744 b = z[1] 745 else: 746 a, b, c = self.algebraic_quadratic(list_mass, list_y) 747 748 calculated = -b / (2 * a) 749 750 if calculated < 1 or int(calculated) != int(list_mass[1]): 751 mz_exp_centroid = list_mass[1] 752 753 else: 754 mz_exp_centroid = calculated 755 756 if ( 757 self.label == Labels.bruker_frequency 758 or self.label == Labels.midas_frequency 759 ): 760 # fit parabola to three most abundant frequency datapoints 761 list_freq = [ 762 freq[current_index - 1], 763 freq[current_index], 764 freq[current_index + 1], 765 ] 766 if self.mspeaks_settings.legacy_centroid_polyfit: 767 z = polyfit(list_mass, list_y, 2) 768 a = z[0] 769 b = z[1] 770 else: 771 a, b, c = self.algebraic_quadratic(list_mass, list_y) 772 773 calculated_freq = -b / (2 * a) 774 775 if calculated_freq < 1 or int(calculated_freq) != freq[current_index]: 776 freq_centr = list_freq[1] 777 778 else: 779 freq_centr = calculated_freq 780 781 else: 782 freq_centr = None 783 784 if self.mspeaks_settings.legacy_centroid_polyfit: 785 abundance_centroid = abund[current_index] 786 else: 787 abundance_centroid = a * mz_exp_centroid**2 + b * mz_exp_centroid + c 788 789 return mz_exp_centroid, freq_centr, abundance_centroid 790 791 def check_prominence( 792 self, abun, current_index, len_abundance, peak_height_diff 793 ) -> tuple or False: 794 """Check the prominence of a peak. 795 796 Parameters 797 ---------- 798 abun : ndarray 799 The abundance values. 800 current_index : int 801 The index of the current peak. 802 len_abundance : int 803 The length of the abundance array. 804 peak_height_diff : function 805 The function to calculate the peak height difference. 806 807 Returns 808 ------- 809 tuple or False 810 A tuple containing the indexes of the peak, if the prominence is above the threshold. 811 Otherwise, False. 812 813 """ 814 815 final_index = self.find_minima(current_index, abun, len_abundance, right=True) 816 817 start_index = self.find_minima(current_index, abun, len_abundance, right=False) 818 819 peak_indexes = (current_index - 1, current_index, current_index + 1) 820 821 if ( 822 min( 823 peak_height_diff(current_index, start_index), 824 peak_height_diff(current_index, final_index), 825 ) 826 > self.mspeaks_settings.peak_min_prominence_percent 827 ): 828 return peak_indexes 829 830 else: 831 return False 832 833 def use_the_max(self, mass, abund, current_index, len_abundance, peak_height_diff): 834 """Use the max peak height as the centroid 835 836 Parameters 837 ---------- 838 mass : ndarray 839 The mass values. 840 abund : ndarray 841 The abundance values. 842 current_index : int 843 The index of the current peak. 844 len_abundance : int 845 The length of the abundance array. 846 peak_height_diff : function 847 The function to calculate the peak height difference. 848 849 Returns 850 ------- 851 float 852 The m/z value of the peak apex. 853 float 854 The abundance value of the peak apex. 855 tuple or None 856 A tuple containing the indexes of the peak, if the prominence is above the threshold. 857 Otherwise, None. 858 """ 859 860 peak_indexes = self.check_prominence( 861 abund, current_index, len_abundance, peak_height_diff 862 ) 863 864 if not peak_indexes: 865 return None, None, None 866 867 else: 868 return mass[current_index], abund[current_index], peak_indexes 869 870 def calc_centroid_legacy(self, mass, abund, freq): 871 """Legacy centroid calculation 872 Deprecated - for deletion. 873 874 """ 875 warnings.warn( 876 "Legacy centroid calculation is deprecated. Please use the new centroid calculation method." 877 ) 878 pass 879 if False: 880 len_abundance = len(abund) 881 882 max_abundance = max(abund) 883 884 peak_height_diff = lambda hi, li: ( 885 ((abund[hi] - abund[li]) / max_abundance) * 100 886 ) 887 888 abundance_threshold, factor = self.get_threshold(abund) 889 # print(abundance_threshold, factor) 890 # find indices of all peaks 891 dy = abund[1:] - abund[:-1] 892 893 # replaces nan for infi nity 894 indices_nan = where(isnan(abund))[0] 895 896 if indices_nan.size: 897 abund[indices_nan] = inf 898 dy[where(isnan(dy))[0]] = inf 899 900 indexes = where((hstack((dy, 0)) < 0) & (hstack((0, dy)) > 0))[0] 901 902 # noise threshold 903 if indexes.size and abundance_threshold is not None: 904 indexes = indexes[abund[indexes] / factor >= abundance_threshold] 905 # filter out 'peaks' within 3 points of the spectrum limits 906 # remove entries within 3 points of upper limit 907 indexes = [x for x in indexes if (len_abundance - x) > 3] 908 # remove entries within 3 points of zero 909 indexes = [x for x in indexes if x > 3] 910 911 for current_index in indexes: 912 if self.label == Labels.simulated_profile: 913 mz_exp_centroid, intes_centr, peak_indexes = self.use_the_max( 914 mass, abund, current_index, len_abundance, peak_height_diff 915 ) 916 if mz_exp_centroid: 917 peak_resolving_power = self.calculate_resolving_power( 918 abund, mass, current_index 919 ) 920 s2n = intes_centr / self.baseline_noise_std 921 freq_centr = None 922 self.add_mspeak( 923 self.polarity, 924 mz_exp_centroid, 925 abund[current_index], 926 peak_resolving_power, 927 s2n, 928 peak_indexes, 929 exp_freq=freq_centr, 930 ms_parent=self, 931 ) 932 933 else: 934 mz_exp_centroid, freq_centr, intes_centr, peak_indexes = ( 935 self.find_apex_fit_quadratic( 936 mass, 937 abund, 938 freq, 939 current_index, 940 len_abundance, 941 peak_height_diff, 942 ) 943 ) 944 if mz_exp_centroid: 945 try: 946 peak_resolving_power = self.calculate_resolving_power( 947 abund, mass, current_index 948 ) 949 except IndexError: 950 print("index error, skipping peak") 951 continue 952 953 s2n = intes_centr / self.baseline_noise_std 954 self.add_mspeak( 955 self.polarity, 956 mz_exp_centroid, 957 abund[current_index], 958 peak_resolving_power, 959 s2n, 960 peak_indexes, 961 exp_freq=freq_centr, 962 ms_parent=self, 963 )
Class for peak picking.
Parameters
- None
Attributes
- None
Methods
- prepare_peak_picking_data(). Prepare the mz, abundance, and frequence data for peak picking.
- cut_mz_domain_peak_picking(). Cut the m/z domain for peak picking.
- extrapolate_axes_for_pp(mz=None, abund=None, freq=None). Extrapolate the m/z axis and fill the abundance axis with 0s.
- do_peak_picking(). Perform peak picking.
- find_minima(apex_index, abundance, len_abundance, right=True). Find the minima of a peak.
- linear_fit_calc(intes, massa, index_term, index_sign). Algebraic solution to a linear fit.
- calculate_resolving_power(intes, massa, current_index). Calculate the resolving power of a peak.
- cal_minima(mass, abun). Calculate the minima of a peak.
- calc_centroid(mass, abund, freq). Calculate the centroid of a peak.
- get_threshold(intes). Get the intensity threshold for peak picking.
- algebraic_quadratic(list_mass, list_y). Find the apex of a peak - algebraically.
- find_apex_fit_quadratic(mass, abund, freq, current_index). Find the apex of a peak.
- check_prominence(abun, current_index, len_abundance, peak_height_diff). Check the prominence of a peak.
- use_the_max(mass, abund, current_index, len_abundance, peak_height_diff). Use the max peak height as the centroid.
- calc_centroid_legacy(mass, abund, freq). Legacy centroid calculation. Deprecated - for deletion.
70 def prepare_peak_picking_data(self): 71 """Prepare the data for peak picking. 72 73 This function will prepare the m/z, abundance, and frequency data for peak picking according to the settings. 74 75 Returns 76 ------- 77 mz : ndarray 78 The m/z axis. 79 abundance : ndarray 80 The abundance axis. 81 freq : ndarray or None 82 The frequency axis, if available. 83 """ 84 # First apply cut_mz_domain_peak_picking 85 mz, abundance, freq = self.cut_mz_domain_peak_picking() 86 87 # Then extrapolate the axes for peak picking 88 if self.settings.picking_point_extrapolate > 0: 89 mz, abundance, freq = self.extrapolate_axes_for_pp(mz, abundance, freq) 90 return mz, abundance, freq
Prepare the data for peak picking.
This function will prepare the m/z, abundance, and frequency data for peak picking according to the settings.
Returns
- mz (ndarray): The m/z axis.
- abundance (ndarray): The abundance axis.
- freq (ndarray or None): The frequency axis, if available.
92 def cut_mz_domain_peak_picking(self): 93 """ 94 Cut the m/z domain for peak picking. 95 96 Simplified function 97 98 Returns 99 ------- 100 mz_domain_X_low_cutoff : ndarray 101 The m/z values within the specified range. 102 mz_domain_low_Y_cutoff : ndarray 103 The abundance values within the specified range. 104 freq_domain_low_Y_cutoff : ndarray or None 105 The frequency values within the specified range, if available. 106 107 """ 108 max_picking_mz = self.settings.max_picking_mz 109 min_picking_mz = self.settings.min_picking_mz 110 111 # min_start = where(self.mz_exp_profile > min_picking_mz)[0][0] 112 # max_final = where(self.mz_exp_profile < max_picking_mz)[-1][-1] 113 min_start = searchsorted(a=self.mz_exp_profile, v=min_picking_mz) 114 max_final = searchsorted(a=self.mz_exp_profile, v=max_picking_mz) 115 116 if self.has_frequency: 117 if self.freq_exp_profile.any(): 118 return ( 119 self.mz_exp_profile[min_start:max_final], 120 self.abundance_profile[min_start:max_final], 121 self.freq_exp_profile[min_start:max_final], 122 ) 123 124 else: 125 return ( 126 self.mz_exp_profile[min_start:max_final], 127 self.abundance_profile[min_start:max_final], 128 None, 129 )
Cut the m/z domain for peak picking.
Simplified function
Returns
- mz_domain_X_low_cutoff (ndarray): The m/z values within the specified range.
- mz_domain_low_Y_cutoff (ndarray): The abundance values within the specified range.
- freq_domain_low_Y_cutoff (ndarray or None): The frequency values within the specified range, if available.
131 def legacy_cut_mz_domain_peak_picking(self): 132 """ 133 Cut the m/z domain for peak picking. 134 DEPRECATED 135 Returns 136 ------- 137 mz_domain_X_low_cutoff : ndarray 138 The m/z values within the specified range. 139 mz_domain_low_Y_cutoff : ndarray 140 The abundance values within the specified range. 141 freq_domain_low_Y_cutoff : ndarray or None 142 The frequency values within the specified range, if available. 143 144 """ 145 max_picking_mz = self.settings.max_picking_mz 146 min_picking_mz = self.settings.min_picking_mz 147 148 min_final = where(self.mz_exp_profile > min_picking_mz)[-1][-1] 149 min_start = where(self.mz_exp_profile > min_picking_mz)[0][0] 150 151 ( 152 mz_domain_X_low_cutoff, 153 mz_domain_low_Y_cutoff, 154 ) = ( 155 self.mz_exp_profile[min_start:min_final], 156 self.abundance_profile[min_start:min_final], 157 ) 158 159 max_final = where(self.mz_exp_profile < max_picking_mz)[-1][-1] 160 max_start = where(self.mz_exp_profile < max_picking_mz)[0][0] 161 162 if self.has_frequency: 163 if self.freq_exp_profile.any(): 164 freq_domain_low_Y_cutoff = self.freq_exp_profile[min_start:min_final] 165 166 return ( 167 mz_domain_X_low_cutoff[max_start:max_final], 168 mz_domain_low_Y_cutoff[max_start:max_final], 169 freq_domain_low_Y_cutoff[max_start:max_final], 170 ) 171 172 else: 173 return ( 174 mz_domain_X_low_cutoff[max_start:max_final], 175 mz_domain_low_Y_cutoff[max_start:max_final], 176 None, 177 )
Cut the m/z domain for peak picking. DEPRECATED
Returns
- mz_domain_X_low_cutoff (ndarray): The m/z values within the specified range.
- mz_domain_low_Y_cutoff (ndarray): The abundance values within the specified range.
- freq_domain_low_Y_cutoff (ndarray or None): The frequency values within the specified range, if available.
179 @staticmethod 180 def extrapolate_axis(initial_array, pts): 181 """ 182 This function will extrapolate an input array in both directions by N pts. 183 184 Parameters 185 ---------- 186 initial_array : ndarray 187 The input array. 188 pts : int 189 The number of points to extrapolate. 190 191 Returns 192 ------- 193 ndarray 194 The extrapolated array. 195 196 Notes 197 -------- 198 This is a static method. 199 """ 200 initial_array_len = len(initial_array) 201 right_delta = initial_array[-1] - initial_array[-2] 202 left_delta = initial_array[1] - initial_array[0] 203 204 # Create an array with extra space for extrapolation 205 pad_array = zeros(initial_array_len + 2 * pts) 206 207 # Copy original array into the middle of the padded array 208 pad_array[pts : pts + initial_array_len] = initial_array 209 210 # Extrapolate the right side 211 for pt in range(pts): 212 final_value = initial_array[-1] 213 value_to_add = right_delta * (pt + 1) 214 new_value = final_value + value_to_add 215 pad_array[initial_array_len + pts + pt] = new_value 216 217 # Extrapolate the left side 218 for pt in range(pts): 219 first_value = initial_array[0] 220 value_to_subtract = left_delta * (pt + 1) 221 new_value = first_value - value_to_subtract 222 pad_array[pts - pt - 1] = new_value 223 224 return pad_array
This function will extrapolate an input array in both directions by N pts.
Parameters
- initial_array (ndarray): The input array.
- pts (int): The number of points to extrapolate.
Returns
- ndarray: The extrapolated array.
Notes
This is a static method.
226 def extrapolate_axes_for_pp(self, mz=None, abund=None, freq=None): 227 """Extrapolate the m/z axis and fill the abundance axis with 0s. 228 229 Parameters 230 ---------- 231 mz : ndarray or None 232 The m/z axis, if available. If None, the experimental m/z axis is used. 233 abund : ndarray or None 234 The abundance axis, if available. If None, the experimental abundance axis is used. 235 freq : ndarray or None 236 The frequency axis, if available. If None, the experimental frequency axis is used. 237 238 Returns 239 ------- 240 mz : ndarray 241 The extrapolated m/z axis. 242 abund : ndarray 243 The abundance axis with 0s filled. 244 freq : ndarray or None 245 The extrapolated frequency axis, if available. 246 247 Notes 248 -------- 249 This function will extrapolate the mz axis by the number of datapoints specified in the settings, 250 and fill the abundance axis with 0s. 251 This should prevent peak picking issues at the spectrum edge. 252 253 """ 254 # Check if the input arrays are provided 255 if mz is None or abund is None: 256 mz, abund = self.mz_exp_profile, self.abundance_profile 257 if self.has_frequency: 258 freq = self.freq_exp_profile 259 else: 260 freq = None 261 pts = self.settings.picking_point_extrapolate 262 if pts == 0: 263 return mz, abund, freq 264 265 mz = self.extrapolate_axis(mz, pts) 266 abund = pad(abund, (pts, pts), mode="constant", constant_values=(0, 0)) 267 if freq is not None: 268 freq = self.extrapolate_axis(freq, pts) 269 return mz, abund, freq
Extrapolate the m/z axis and fill the abundance axis with 0s.
Parameters
- mz (ndarray or None): The m/z axis, if available. If None, the experimental m/z axis is used.
- abund (ndarray or None): The abundance axis, if available. If None, the experimental abundance axis is used.
- freq (ndarray or None): The frequency axis, if available. If None, the experimental frequency axis is used.
Returns
- mz (ndarray): The extrapolated m/z axis.
- abund (ndarray): The abundance axis with 0s filled.
- freq (ndarray or None): The extrapolated frequency axis, if available.
Notes
This function will extrapolate the mz axis by the number of datapoints specified in the settings, and fill the abundance axis with 0s. This should prevent peak picking issues at the spectrum edge.
271 def do_peak_picking(self): 272 """Perform peak picking.""" 273 mz, abundance, freq = self.prepare_peak_picking_data() 274 275 if ( 276 self.label == Labels.bruker_frequency 277 or self.label == Labels.midas_frequency 278 ): 279 self.calc_centroid(mz, abundance, freq) 280 281 elif self.label == Labels.thermo_profile: 282 self.calc_centroid(mz, abundance, freq) 283 284 elif self.label == Labels.bruker_profile: 285 self.calc_centroid(mz, abundance, freq) 286 287 elif self.label == Labels.booster_profile: 288 self.calc_centroid(mz, abundance, freq) 289 290 elif self.label == Labels.simulated_profile: 291 self.calc_centroid(mz, abundance, freq) 292 293 else: 294 raise Exception("Unknow mass spectrum type", self.label)
Perform peak picking.
296 def find_minima(self, apex_index, abundance, len_abundance, right=True): 297 """Find the minima of a peak. 298 299 Parameters 300 ---------- 301 apex_index : int 302 The index of the peak apex. 303 abundance : ndarray 304 The abundance values. 305 len_abundance : int 306 The length of the abundance array. 307 right : bool, optional 308 Flag indicating whether to search for minima to the right of the apex (default is True). 309 310 Returns 311 ------- 312 int 313 The index of the minima. 314 315 """ 316 j = apex_index 317 318 if right: 319 minima = abundance[j] > abundance[j + 1] 320 else: 321 minima = abundance[j] > abundance[j - 1] 322 323 while minima: 324 if j == 1 or j == len_abundance - 2: 325 break 326 327 if right: 328 j += 1 329 330 minima = abundance[j] >= abundance[j + 1] 331 332 else: 333 j -= 1 334 minima = abundance[j] >= abundance[j - 1] 335 336 if right: 337 return j 338 else: 339 return j
Find the minima of a peak.
Parameters
- apex_index (int): The index of the peak apex.
- abundance (ndarray): The abundance values.
- len_abundance (int): The length of the abundance array.
- right (bool, optional): Flag indicating whether to search for minima to the right of the apex (default is True).
Returns
- int: The index of the minima.
341 @staticmethod 342 def linear_fit_calc(intes, massa, index_term, index_sign): 343 """ 344 Algebraic solution to a linear fit - roughly 25-50x faster than numpy polyfit when passing only two vals and doing a 1st order fit 345 346 Parameters 347 ---------- 348 intes : ndarray 349 The intensity values. 350 massa : ndarray 351 The mass values. 352 index_term : int 353 The index of the current term. 354 index_sign : str 355 The index sign 356 357 Returns 358 ------- 359 ndarray 360 The coefficients of the linear fit. 361 362 Notes 363 -------- 364 This is a static method. 365 """ 366 if index_sign == "+": 367 x1, x2 = massa[index_term], massa[index_term + 1] 368 y1, y2 = intes[index_term], intes[index_term + 1] 369 elif index_sign == "-": 370 x1, x2 = massa[index_term], massa[index_term - 1] 371 y1, y2 = intes[index_term], intes[index_term - 1] 372 else: 373 warnings.warn("error in linear fit calc, unknown index sign") 374 375 # Calculate the slope (m) 376 slope = (y2 - y1) / (x2 - x1) 377 378 # Calculate the intercept (b) 379 intercept = y1 - slope * x1 380 381 # The coefficients array would be [slope, intercept] 382 coefficients = array([slope, intercept]) 383 return coefficients
Algebraic solution to a linear fit - roughly 25-50x faster than numpy polyfit when passing only two vals and doing a 1st order fit
Parameters
- intes (ndarray): The intensity values.
- massa (ndarray): The mass values.
- index_term (int): The index of the current term.
- index_sign (str): The index sign
Returns
- ndarray: The coefficients of the linear fit.
Notes
This is a static method.
385 def calculate_resolving_power(self, intes, massa, current_index): 386 """Calculate the resolving power of a peak. 387 388 Parameters 389 ---------- 390 intes : ndarray 391 The intensity values. 392 massa : ndarray 393 The mass values. 394 current_index : int 395 The index of the current peak. 396 397 Returns 398 ------- 399 float 400 The resolving power of the peak, or ``nan`` if a half-maximum 401 crossing does not exist on both sides (spectrum edge truncates 402 the peak). 403 404 Notes 405 -------- 406 This is a conservative calculation of resolving power, 407 the peak need to be resolved at least at the half-maximum magnitude, 408 otherwise, the combined full width at half maximum is used to calculate resolving power. 409 410 """ 411 412 peak_height = intes[current_index] 413 target_peak_height = peak_height / 2 414 n_points = len(intes) 415 416 index_minus = current_index 417 while index_minus > 0 and intes[index_minus] >= target_peak_height: 418 index_minus -= 1 419 if intes[index_minus] >= target_peak_height: 420 return nan 421 422 index_plus = current_index 423 while index_plus < n_points - 1 and intes[index_plus] >= target_peak_height: 424 index_plus += 1 425 if intes[index_plus] >= target_peak_height: 426 return nan 427 428 if self.mspeaks_settings.legacy_centroid_polyfit: 429 x = [massa[index_minus], massa[index_minus + 1]] 430 y = [intes[index_minus], intes[index_minus + 1]] 431 coefficients = polyfit(x, y, 1) 432 else: 433 coefficients = self.linear_fit_calc( 434 intes, massa, index_minus, index_sign="+" 435 ) 436 437 a = coefficients[0] 438 b = coefficients[1] 439 if self.mspeaks_settings.legacy_resolving_power: 440 y_intercept = intes[index_minus] + ( 441 (intes[index_minus + 1] - intes[index_minus]) / 2 442 ) 443 else: 444 y_intercept = target_peak_height 445 massa1 = (y_intercept - b) / a 446 447 if self.mspeaks_settings.legacy_centroid_polyfit: 448 x = [massa[index_plus], massa[index_plus - 1]] 449 y = [intes[index_plus], intes[index_plus - 1]] 450 coefficients = polyfit(x, y, 1) 451 else: 452 coefficients = self.linear_fit_calc( 453 intes, massa, index_plus, index_sign="-" 454 ) 455 456 a = coefficients[0] 457 b = coefficients[1] 458 459 if self.mspeaks_settings.legacy_resolving_power: 460 y_intercept = intes[index_plus - 1] + ( 461 (intes[index_plus] - intes[index_plus - 1]) / 2 462 ) 463 else: 464 y_intercept = target_peak_height 465 466 massa2 = (y_intercept - b) / a 467 468 if massa1 > massa2: 469 resolvingpower = massa[current_index] / (massa1 - massa2) 470 471 else: 472 resolvingpower = massa[current_index] / (massa2 - massa1) 473 474 return resolvingpower
Calculate the resolving power of a peak.
Parameters
- intes (ndarray): The intensity values.
- massa (ndarray): The mass values.
- current_index (int): The index of the current peak.
Returns
- float: The resolving power of the peak, or
nanif a half-maximum crossing does not exist on both sides (spectrum edge truncates the peak).
Notes
This is a conservative calculation of resolving power, the peak need to be resolved at least at the half-maximum magnitude, otherwise, the combined full width at half maximum is used to calculate resolving power.
476 def cal_minima(self, mass, abun): 477 """Calculate the minima of a peak. 478 479 Parameters 480 ---------- 481 mass : ndarray 482 The mass values. 483 abun : ndarray 484 The abundance values. 485 486 Returns 487 ------- 488 ndarray or None 489 The mass values at the minima, if found. 490 491 """ 492 abun = -abun 493 494 dy = abun[1:] - abun[:-1] 495 496 # replaces nan for infinity 497 indices_nan = where(isnan(abun))[0] 498 499 if indices_nan.size: 500 abun[indices_nan] = inf 501 dy[where(isnan(dy))[0]] = inf 502 503 indexes = where((hstack((dy, 0)) < 0) & (hstack((0, dy)) > 0))[0] 504 505 if indexes.size: 506 return mass[indexes], abun[indexes]
Calculate the minima of a peak.
Parameters
- mass (ndarray): The mass values.
- abun (ndarray): The abundance values.
Returns
- ndarray or None: The mass values at the minima, if found.
508 def calc_centroid(self, mass, abund, freq): 509 """Calculate the centroid of a peak. 510 511 Parameters 512 ---------- 513 mass : ndarray 514 The mass values. 515 abund : ndarray 516 The abundance values. 517 freq : ndarray or None 518 The frequency values, if available. 519 520 Returns 521 ------- 522 None 523 524 """ 525 526 max_height = self.mspeaks_settings.peak_height_max_percent 527 max_prominence = self.mspeaks_settings.peak_max_prominence_percent 528 min_peak_datapoints = self.mspeaks_settings.min_peak_datapoints 529 peak_derivative_threshold = self.mspeaks_settings.peak_derivative_threshold 530 max_abun = max(abund) 531 peak_height_diff = lambda hi, li: ((abund[hi] - abund[li]) / max_abun) * 100 532 533 domain = mass 534 signal = abund 535 len_signal = len(signal) 536 537 signal_threshold, factor = self.get_threshold(abund) 538 max_signal = factor 539 540 correct_baseline = False 541 542 include_indexes = sp.peak_picking_first_derivative( 543 domain, 544 signal, 545 max_height, 546 max_prominence, 547 max_signal, 548 min_peak_datapoints, 549 peak_derivative_threshold, 550 signal_threshold=signal_threshold, 551 correct_baseline=correct_baseline, 552 abun_norm=1, 553 plot_res=False, 554 ) 555 556 for indexes_tuple in include_indexes: 557 apex_index = indexes_tuple[1] 558 559 peak_indexes = self.check_prominence( 560 abund, apex_index, len_signal, peak_height_diff 561 ) 562 563 if peak_indexes: 564 mz_exp_centroid, freq_centr, intes_centr = self.find_apex_fit_quadratic( 565 mass, abund, freq, apex_index 566 ) 567 568 if mz_exp_centroid: 569 peak_resolving_power = self.calculate_resolving_power( 570 abund, mass, apex_index 571 ) 572 s2n = intes_centr / self.baseline_noise_std 573 self.add_mspeak( 574 self.polarity, 575 mz_exp_centroid, 576 abund[apex_index], 577 peak_resolving_power, 578 s2n, 579 indexes_tuple, 580 exp_freq=freq_centr, 581 ms_parent=self, 582 ) 583 # pyplot.plot(domain[start_index: final_index + 1], signal[start_index:final_index + 1], c='black') 584 # pyplot.show()
Calculate the centroid of a peak.
Parameters
- mass (ndarray): The mass values.
- abund (ndarray): The abundance values.
- freq (ndarray or None): The frequency values, if available.
Returns
- None
586 def get_threshold(self, intes): 587 """Get the intensity threshold for peak picking. 588 589 Parameters 590 ---------- 591 intes : ndarray 592 The intensity values. 593 594 Returns 595 ------- 596 float 597 The intensity threshold. 598 float 599 The factor to multiply the intensity threshold by. 600 """ 601 602 intes = array(intes).astype(float) 603 604 self._resolve_noise_threshold_method() 605 noise_threshold_method = self.settings.noise_threshold_method 606 607 if noise_threshold_method == "minima": 608 if self.is_centroid: 609 warnings.warn( 610 "Auto threshould is disabled for centroid data, returning 0" 611 ) 612 factor = 1 613 abundance_threshold = 1e-20 614 # print(self.settings.noise_threshold_min_std) 615 else: 616 abundance_threshold = self.baseline_noise + ( 617 self.settings.noise_threshold_min_std * self.baseline_noise_std 618 ) 619 factor = 1 620 621 elif noise_threshold_method == "signal_noise": 622 abundance_threshold = self.settings.noise_threshold_min_s2n 623 if self.is_centroid: 624 factor = 1 625 else: 626 factor = self.baseline_noise_std 627 628 elif noise_threshold_method == "relative_abundance": 629 abundance_threshold = self.settings.noise_threshold_min_relative_abundance 630 factor = intes.max() / 100 631 632 elif noise_threshold_method == "absolute_abundance": 633 abundance_threshold = self.settings.noise_threshold_absolute_abundance 634 factor = 1 635 636 elif noise_threshold_method == "log": 637 if self.is_centroid: 638 raise Exception("log noise Not tested for centroid data") 639 abundance_threshold = self.settings.noise_threshold_log_nsigma 640 factor = self.baseline_noise_std 641 642 else: 643 raise Exception( 644 "%s method was not implemented, please refer to corems.mass_spectrum.calc.NoiseCalc Class" 645 % noise_threshold_method 646 ) 647 648 return abundance_threshold, factor
Get the intensity threshold for peak picking.
Parameters
- intes (ndarray): The intensity values.
Returns
- float: The intensity threshold.
- float: The factor to multiply the intensity threshold by.
650 @staticmethod 651 def algebraic_quadratic(list_mass, list_y): 652 """ 653 Find the apex of a peak - algebraically. 654 Faster than using numpy polyfit by ~28x per fit. 655 656 Parameters 657 ---------- 658 list_mass : ndarray 659 list of m/z values (3 points) 660 list_y : ndarray 661 list of abundance values (3 points) 662 663 Returns 664 ------- 665 a, b, c: float 666 coefficients of the quadratic equation. 667 668 Notes 669 -------- 670 This is a static method. 671 """ 672 x_1, x_2, x_3 = list_mass 673 y_1, y_2, y_3 = list_y 674 675 a = ( 676 y_1 / ((x_1 - x_2) * (x_1 - x_3)) 677 + y_2 / ((x_2 - x_1) * (x_2 - x_3)) 678 + y_3 / ((x_3 - x_1) * (x_3 - x_2)) 679 ) 680 681 b = ( 682 -y_1 * (x_2 + x_3) / ((x_1 - x_2) * (x_1 - x_3)) 683 - y_2 * (x_1 + x_3) / ((x_2 - x_1) * (x_2 - x_3)) 684 - y_3 * (x_1 + x_2) / ((x_3 - x_1) * (x_3 - x_2)) 685 ) 686 687 c = ( 688 y_1 * x_2 * x_3 / ((x_1 - x_2) * (x_1 - x_3)) 689 + y_2 * x_1 * x_3 / ((x_2 - x_1) * (x_2 - x_3)) 690 + y_3 * x_1 * x_2 / ((x_3 - x_1) * (x_3 - x_2)) 691 ) 692 return a, b, c
Find the apex of a peak - algebraically. Faster than using numpy polyfit by ~28x per fit.
Parameters
- list_mass (ndarray): list of m/z values (3 points)
- list_y (ndarray): list of abundance values (3 points)
Returns
- a, b, c (float): coefficients of the quadratic equation.
Notes
This is a static method.
694 def find_apex_fit_quadratic(self, mass, abund, freq, current_index): 695 """ 696 Find the apex of a peak. 697 698 Parameters 699 ---------- 700 mass : ndarray 701 The mass values. 702 abund : ndarray 703 The abundance values. 704 freq : ndarray or None 705 The frequency values, if available. 706 current_index : int 707 The index of the current peak. 708 709 710 Returns 711 ------- 712 float 713 The m/z value of the peak apex. 714 float 715 The frequency value of the peak apex, if available. 716 float 717 The abundance value of the peak apex. 718 719 """ 720 # calc prominence 721 # peak_indexes = self.check_prominence(abund, current_index, len_abundance, peak_height_diff ) 722 723 # if not peak_indexes: 724 725 # return None, None, None, None 726 727 # else: 728 729 # fit parabola to three most abundant datapoints 730 list_mass = [ 731 mass[current_index - 1], 732 mass[current_index], 733 mass[current_index + 1], 734 ] 735 list_y = [ 736 abund[current_index - 1], 737 abund[current_index], 738 abund[current_index + 1], 739 ] 740 741 if self.mspeaks_settings.legacy_centroid_polyfit: 742 z = polyfit(list_mass, list_y, 2) 743 a = z[0] 744 b = z[1] 745 else: 746 a, b, c = self.algebraic_quadratic(list_mass, list_y) 747 748 calculated = -b / (2 * a) 749 750 if calculated < 1 or int(calculated) != int(list_mass[1]): 751 mz_exp_centroid = list_mass[1] 752 753 else: 754 mz_exp_centroid = calculated 755 756 if ( 757 self.label == Labels.bruker_frequency 758 or self.label == Labels.midas_frequency 759 ): 760 # fit parabola to three most abundant frequency datapoints 761 list_freq = [ 762 freq[current_index - 1], 763 freq[current_index], 764 freq[current_index + 1], 765 ] 766 if self.mspeaks_settings.legacy_centroid_polyfit: 767 z = polyfit(list_mass, list_y, 2) 768 a = z[0] 769 b = z[1] 770 else: 771 a, b, c = self.algebraic_quadratic(list_mass, list_y) 772 773 calculated_freq = -b / (2 * a) 774 775 if calculated_freq < 1 or int(calculated_freq) != freq[current_index]: 776 freq_centr = list_freq[1] 777 778 else: 779 freq_centr = calculated_freq 780 781 else: 782 freq_centr = None 783 784 if self.mspeaks_settings.legacy_centroid_polyfit: 785 abundance_centroid = abund[current_index] 786 else: 787 abundance_centroid = a * mz_exp_centroid**2 + b * mz_exp_centroid + c 788 789 return mz_exp_centroid, freq_centr, abundance_centroid
Find the apex of a peak.
Parameters
- mass (ndarray): The mass values.
- abund (ndarray): The abundance values.
- freq (ndarray or None): The frequency values, if available.
- current_index (int): The index of the current peak.
Returns
- float: The m/z value of the peak apex.
- float: The frequency value of the peak apex, if available.
- float: The abundance value of the peak apex.
791 def check_prominence( 792 self, abun, current_index, len_abundance, peak_height_diff 793 ) -> tuple or False: 794 """Check the prominence of a peak. 795 796 Parameters 797 ---------- 798 abun : ndarray 799 The abundance values. 800 current_index : int 801 The index of the current peak. 802 len_abundance : int 803 The length of the abundance array. 804 peak_height_diff : function 805 The function to calculate the peak height difference. 806 807 Returns 808 ------- 809 tuple or False 810 A tuple containing the indexes of the peak, if the prominence is above the threshold. 811 Otherwise, False. 812 813 """ 814 815 final_index = self.find_minima(current_index, abun, len_abundance, right=True) 816 817 start_index = self.find_minima(current_index, abun, len_abundance, right=False) 818 819 peak_indexes = (current_index - 1, current_index, current_index + 1) 820 821 if ( 822 min( 823 peak_height_diff(current_index, start_index), 824 peak_height_diff(current_index, final_index), 825 ) 826 > self.mspeaks_settings.peak_min_prominence_percent 827 ): 828 return peak_indexes 829 830 else: 831 return False
Check the prominence of a peak.
Parameters
- abun (ndarray): The abundance values.
- current_index (int): The index of the current peak.
- len_abundance (int): The length of the abundance array.
- peak_height_diff (function): The function to calculate the peak height difference.
Returns
- tuple or False: A tuple containing the indexes of the peak, if the prominence is above the threshold. Otherwise, False.
833 def use_the_max(self, mass, abund, current_index, len_abundance, peak_height_diff): 834 """Use the max peak height as the centroid 835 836 Parameters 837 ---------- 838 mass : ndarray 839 The mass values. 840 abund : ndarray 841 The abundance values. 842 current_index : int 843 The index of the current peak. 844 len_abundance : int 845 The length of the abundance array. 846 peak_height_diff : function 847 The function to calculate the peak height difference. 848 849 Returns 850 ------- 851 float 852 The m/z value of the peak apex. 853 float 854 The abundance value of the peak apex. 855 tuple or None 856 A tuple containing the indexes of the peak, if the prominence is above the threshold. 857 Otherwise, None. 858 """ 859 860 peak_indexes = self.check_prominence( 861 abund, current_index, len_abundance, peak_height_diff 862 ) 863 864 if not peak_indexes: 865 return None, None, None 866 867 else: 868 return mass[current_index], abund[current_index], peak_indexes
Use the max peak height as the centroid
Parameters
- mass (ndarray): The mass values.
- abund (ndarray): The abundance values.
- current_index (int): The index of the current peak.
- len_abundance (int): The length of the abundance array.
- peak_height_diff (function): The function to calculate the peak height difference.
Returns
- float: The m/z value of the peak apex.
- float: The abundance value of the peak apex.
- tuple or None: A tuple containing the indexes of the peak, if the prominence is above the threshold. Otherwise, None.
870 def calc_centroid_legacy(self, mass, abund, freq): 871 """Legacy centroid calculation 872 Deprecated - for deletion. 873 874 """ 875 warnings.warn( 876 "Legacy centroid calculation is deprecated. Please use the new centroid calculation method." 877 ) 878 pass 879 if False: 880 len_abundance = len(abund) 881 882 max_abundance = max(abund) 883 884 peak_height_diff = lambda hi, li: ( 885 ((abund[hi] - abund[li]) / max_abundance) * 100 886 ) 887 888 abundance_threshold, factor = self.get_threshold(abund) 889 # print(abundance_threshold, factor) 890 # find indices of all peaks 891 dy = abund[1:] - abund[:-1] 892 893 # replaces nan for infi nity 894 indices_nan = where(isnan(abund))[0] 895 896 if indices_nan.size: 897 abund[indices_nan] = inf 898 dy[where(isnan(dy))[0]] = inf 899 900 indexes = where((hstack((dy, 0)) < 0) & (hstack((0, dy)) > 0))[0] 901 902 # noise threshold 903 if indexes.size and abundance_threshold is not None: 904 indexes = indexes[abund[indexes] / factor >= abundance_threshold] 905 # filter out 'peaks' within 3 points of the spectrum limits 906 # remove entries within 3 points of upper limit 907 indexes = [x for x in indexes if (len_abundance - x) > 3] 908 # remove entries within 3 points of zero 909 indexes = [x for x in indexes if x > 3] 910 911 for current_index in indexes: 912 if self.label == Labels.simulated_profile: 913 mz_exp_centroid, intes_centr, peak_indexes = self.use_the_max( 914 mass, abund, current_index, len_abundance, peak_height_diff 915 ) 916 if mz_exp_centroid: 917 peak_resolving_power = self.calculate_resolving_power( 918 abund, mass, current_index 919 ) 920 s2n = intes_centr / self.baseline_noise_std 921 freq_centr = None 922 self.add_mspeak( 923 self.polarity, 924 mz_exp_centroid, 925 abund[current_index], 926 peak_resolving_power, 927 s2n, 928 peak_indexes, 929 exp_freq=freq_centr, 930 ms_parent=self, 931 ) 932 933 else: 934 mz_exp_centroid, freq_centr, intes_centr, peak_indexes = ( 935 self.find_apex_fit_quadratic( 936 mass, 937 abund, 938 freq, 939 current_index, 940 len_abundance, 941 peak_height_diff, 942 ) 943 ) 944 if mz_exp_centroid: 945 try: 946 peak_resolving_power = self.calculate_resolving_power( 947 abund, mass, current_index 948 ) 949 except IndexError: 950 print("index error, skipping peak") 951 continue 952 953 s2n = intes_centr / self.baseline_noise_std 954 self.add_mspeak( 955 self.polarity, 956 mz_exp_centroid, 957 abund[current_index], 958 peak_resolving_power, 959 s2n, 960 peak_indexes, 961 exp_freq=freq_centr, 962 ms_parent=self, 963 )
Legacy centroid calculation Deprecated - for deletion.