corems.chroma_peak.factory.chroma_peak_classes
1__author__ = "Yuri E. Corilo" 2__date__ = "Jun 12, 2019" 3 4import matplotlib.pyplot as plt 5import numpy as np 6import pandas as pd 7import copy 8 9from corems.chroma_peak.calc.ChromaPeakCalc import ( 10 GCPeakCalculation, 11 LCMSMassFeatureCalculation, 12) 13from corems.mass_spectra.factory.chromat_data import EIC_Data 14from corems.molecular_id.factory.EI_SQL import LowResCompoundRef 15 16 17class ChromaPeakBase: 18 """Base class for chromatographic peak (ChromaPeak) objects. 19 20 Parameters 21 ------- 22 chromatogram_parent : Chromatogram 23 The parent chromatogram object. 24 mass_spectrum_obj : MassSpectrum 25 The mass spectrum object. 26 start_index : int 27 The start index of the peak. 28 index : int 29 The index of the peak. 30 final_index : int 31 The final index of the peak. 32 33 Attributes 34 -------- 35 start_scan : int 36 The start scan of the peak. 37 final_scan : int 38 The final scan of the peak. 39 apex_scan : int 40 The apex scan of the peak. 41 chromatogram_parent : Chromatogram 42 The parent chromatogram object. 43 mass_spectrum : MassSpectrum 44 The mass spectrum object. 45 _area : float 46 The area of the peak. 47 48 Properties 49 -------- 50 * retention_time : float. 51 The retention time of the peak. 52 * tic : float. 53 The total ion current of the peak. 54 * area : float. 55 The area of the peak. 56 * rt_list : list. 57 The list of retention times within the peak. 58 * tic_list : list. 59 The list of total ion currents within the peak. 60 61 Methods 62 -------- 63 * None 64 """ 65 66 def __init__( 67 self, chromatogram_parent, mass_spectrum_obj, start_index, index, final_index 68 ): 69 self.start_scan = start_index 70 self.final_scan = final_index 71 self.apex_scan = int(index) 72 self.chromatogram_parent = chromatogram_parent 73 self.mass_spectrum = mass_spectrum_obj 74 self._area = None 75 76 @property 77 def retention_time(self): 78 """Retention Time""" 79 return self.mass_spectrum.retention_time 80 81 @property 82 def tic(self): 83 """Total Ion Current""" 84 return self.mass_spectrum.tic 85 86 @property 87 def area(self): 88 """Peak Area""" 89 return self._area 90 91 @property 92 def rt_list(self): 93 """Retention Time List""" 94 return [ 95 self.chromatogram_parent.retention_time[i] 96 for i in range(self.start_scan, self.final_scan + 1) 97 ] 98 99 @property 100 def tic_list(self): 101 """Total Ion Current List""" 102 return [ 103 self.chromatogram_parent.tic[i] 104 for i in range(self.start_scan, self.final_scan + 1) 105 ] 106 107 108class LCMSMassFeature(ChromaPeakBase, LCMSMassFeatureCalculation): 109 """Class representing a mass feature in a liquid chromatography (LC) chromatogram. 110 111 Parameters 112 ------- 113 lcms_parent : LCMS 114 The parent LCMSBase object. 115 mz : float 116 The observed mass to charge ratio of the feature. 117 retention_time : float 118 The retention time of the feature (in minutes), at the apex. 119 intensity : float 120 The intensity of the feature. 121 apex_scan : int 122 The scan number of the apex of the feature. 123 persistence : float, optional 124 The persistence of the feature. Default is None. 125 126 Attributes 127 -------- 128 _mz_exp : float 129 The observed mass to charge ratio of the feature. 130 _mz_cal : float 131 The calibrated mass to charge ratio of the feature. 132 _retention_time : float 133 The retention time of the feature (in minutes), at the apex. 134 _apex_scan : int 135 The scan number of the apex of the feature. 136 _intensity : float 137 The intensity of the feature. 138 _persistence : float 139 The persistence of the feature. 140 _eic_data : EIC_Data 141 The EIC data object associated with the feature. 142 _eic_mz : float 143 The m/z value used to extract the EIC data, 144 sometimes different from the observed m/z due to calibration, centroiding, or other processing. 145 _dispersity_index : float 146 The dispersity index of the feature, in minutes. 147 _normalized_dispersity_index : float 148 The normalized dispersity index of the feature (unitless, fraction of total window used to calculate dispersity index). 149 _half_height_width : numpy.ndarray 150 The half height width of the feature (in minutes, as an array of min and max values). 151 _tailing_factor : float 152 The tailing factor of the feature. 153 > 1 indicates tailing, < 1 indicates fronting, = 1 indicates symmetrical peak. 154 _noise_score : tuple 155 The noise score of the feature, as a tuple of (left, right) scores. 156 Each score is a float, with higher values indicating better signal to noise. 157 _gaussian_similarity : float 158 The Gaussian similarity of the feature, as a float between 0 and 1. 159 1 indicates a perfect Gaussian shape, 0 indicates a non-Gaussian shape. 160 _ms_deconvoluted_idx : [int] 161 The indexes of the mass_spectrum attribute in the deconvoluted mass spectrum. 162 _type : str 163 The type of mass feature. Default is "untargeted". 164 Can be "untargeted", "targeted", or another customized type. 165 is_calibrated : bool 166 If True, the feature has been calibrated. Default is False. 167 monoisotopic_mf_id : int 168 Mass feature id that is the monoisotopic version of self. 169 If self.id, then self is the monoisotopic feature). Default is None. 170 isotopologue_type : str 171 The isotopic class of the feature, i.e. "13C1", "13C2", "13C1 37Cl1" etc. 172 Default is None. 173 ms2_scan_numbers : list 174 List of scan numbers of the MS2 spectra associated with the feature. 175 Default is an empty list. 176 ms2_mass_spectra : dict 177 Dictionary of MS2 spectra associated with the feature (key = scan number for DDA). 178 Default is an empty dictionary. 179 ms2_similarity_results : list 180 List of MS2 similarity results associated with the mass feature. 181 Default is an empty list. 182 id : int 183 The ID of the feature, also the key in the parent LCMS object's 184 `mass_features` dictionary. 185 mass_spectrum_deconvoluted_parent : bool 186 If True, the mass feature corresponds to the most intense peak in the deconvoluted mass spectrum. Default is None. 187 associated_mass_features_deconvoluted : list 188 List of mass features associated with the deconvoluted mass spectrum. Default is an empty list. 189 190 """ 191 192 def __init__( 193 self, 194 lcms_parent, 195 mz: float, 196 retention_time: float, 197 intensity: float, 198 apex_scan: int, 199 persistence: float = None, 200 id: int = None 201 ): 202 super().__init__( 203 chromatogram_parent=lcms_parent, 204 mass_spectrum_obj=None, 205 start_index=None, 206 index=apex_scan, 207 final_index=None, 208 ) 209 # Core attributes, marked as private 210 self._mz_exp: float = mz 211 self._mz_cal: float = None 212 self._retention_time: float = retention_time 213 self._apex_scan: int = apex_scan 214 self._intensity: float = intensity 215 self._persistence: float = persistence 216 self._eic_data: EIC_Data = None 217 self._dispersity_index: float = None 218 self._normalized_dispersity_index: float = None 219 self._half_height_width: np.ndarray = None 220 self._ms_deconvoluted_idx = None 221 self._tailing_factor: float = None 222 self._noise_score: tuple = None 223 self._gaussian_similarity: float = None 224 self._type: str = "untargeted" 225 226 # Additional attributes 227 self.monoisotopic_mf_id = None 228 self.isotopologue_type = None 229 self.ms2_scan_numbers = [] 230 self.ms2_mass_spectra = {} 231 self.ms2_similarity_results = [] 232 self.mass_spectrum_deconvoluted_parent: bool = None 233 self.associated_mass_features_deconvoluted = [] 234 235 if id: 236 self.id = id 237 else: 238 # get the parent's mass feature keys and add 1 to the max value to get the new key 239 self.id = ( 240 max(lcms_parent.mass_features.keys()) + 1 241 if lcms_parent.mass_features.keys() 242 else 0 243 ) 244 245 def update_mz(self): 246 """Update the mass to charge ratio from the mass spectrum object.""" 247 if self.mass_spectrum is None: 248 raise ValueError( 249 "The mass spectrum object is not set, cannot update the m/z from the MassSpectrum object" 250 ) 251 if len(self.mass_spectrum.mz_exp) == 0: 252 raise ValueError( 253 "The mass spectrum object has no m/z values, cannot update the m/z from the MassSpectrum object until it is processed" 254 ) 255 new_mz = self.ms1_peak.mz_exp 256 257 # calculate the difference between the new and old m/z, only update if it is close 258 mz_diff = new_mz - self.mz 259 if abs(mz_diff) < 0.01: 260 self._mz_exp = new_mz 261 262 def _plot_ms1_spectrum(self, ax, deconvoluted=False, sample_name=None): 263 """Internal method to plot MS1 spectrum on a given axis. 264 265 Parameters 266 ---------- 267 ax : matplotlib.axes.Axes 268 The axis to plot on. 269 deconvoluted : bool, optional 270 If True and deconvoluted spectrum exists, plot both raw and deconvoluted. Default is False. 271 sample_name : str, optional 272 Sample name to include in title. Default is None. 273 """ 274 if self.mass_spectrum is None: 275 raise ValueError("MS1 spectrum is not available") 276 277 title_prefix = "MS1 (deconvoluted)" if deconvoluted else "MS1 (raw)" 278 if sample_name: 279 ax.set_title(f"{title_prefix} - {sample_name}", loc="left") 280 else: 281 ax.set_title(title_prefix, loc="left") 282 283 if deconvoluted and self._ms_deconvoluted_idx is not None: 284 # Plot both raw and deconvoluted 285 ax.vlines( 286 self.mass_spectrum.mz_exp, 287 0, 288 self.mass_spectrum.abundance, 289 color="k", 290 alpha=0.2, 291 label="Raw MS1", 292 ) 293 ax.vlines( 294 self.mass_spectrum_deconvoluted.mz_exp, 295 0, 296 self.mass_spectrum_deconvoluted.abundance, 297 color="k", 298 label="Deconvoluted MS1", 299 ) 300 ax.set_xlim( 301 self.mass_spectrum_deconvoluted.mz_exp.min() * 0.8, 302 self.mass_spectrum_deconvoluted.mz_exp.max() * 1.1, 303 ) 304 ax.set_ylim( 305 0, self.mass_spectrum_deconvoluted.abundance.max() * 1.1 306 ) 307 else: 308 # Plot raw only 309 ax.vlines( 310 self.mass_spectrum.mz_exp, 311 0, 312 self.mass_spectrum.abundance, 313 color="k", 314 label="Raw MS1", 315 ) 316 ax.set_xlim( 317 self.mass_spectrum.mz_exp.min() * 0.8, 318 self.mass_spectrum.mz_exp.max() * 1.1, 319 ) 320 ax.set_ylim(bottom=0) 321 322 # Highlight the feature m/z if close enough 323 if abs(self.ms1_peak.mz_exp - self.mz) < 0.01: 324 ax.vlines( 325 self.ms1_peak.mz_exp, 326 0, 327 self.ms1_peak.abundance, 328 color="m", 329 label="Feature m/z", 330 ) 331 else: 332 if self.chromatogram_parent.parameters.lc_ms.verbose_processing: 333 print( 334 f"The m/z of the mass feature {self.id} is different from the m/z of MS1 peak, " 335 "the MS1 peak will not be plotted" 336 ) 337 338 ax.legend(loc="upper left") 339 ax.set_ylabel("Intensity") 340 ax.set_xlabel("m/z") 341 # Combining tick_params(labelleft=False) with set_title(loc="left") makes 342 # tight_layout() produce NaN axis positions on matplotlib 3.11/numpy 2.5. 343 ax.set_yticklabels([]) 344 345 def _plot_ms2_spectrum(self, ax, sample_name=None): 346 """Internal method to plot MS2 spectrum on a given axis. 347 348 Parameters 349 ---------- 350 ax : matplotlib.axes.Axes 351 The axis to plot on. 352 sample_name : str, optional 353 Sample name to include in title. Default is None. 354 """ 355 if len(self.ms2_mass_spectra) == 0: 356 raise ValueError("MS2 spectrum is not available") 357 358 if sample_name: 359 ax.set_title(f"MS2 - {sample_name}", loc="left") 360 else: 361 ax.set_title("MS2", loc="left") 362 363 ax.vlines( 364 self.best_ms2.mz_exp, 0, self.best_ms2.abundance, color="k" 365 ) 366 ax.set_ylabel("Intensity") 367 ax.set_xlabel("m/z") 368 ax.set_ylim(bottom=0) 369 ax.yaxis.get_major_formatter().set_scientific(False) 370 ax.yaxis.get_major_formatter().set_useOffset(False) 371 372 def _plot_ms2_mirror(self, ax, molecular_metadata=None, spectral_library=None): 373 """Internal method to plot MS2 mirror spectrum on a given axis. 374 375 Plots experimental MS2 on top (positive) and library MS2 on bottom (negative/mirrored) 376 if MS2 similarity results are available. If no MS2 similarity results exist, 377 falls back to regular MS2 plot. 378 379 Parameters 380 ---------- 381 ax : matplotlib.axes.Axes 382 The axis to plot on. 383 molecular_metadata : dict, optional 384 Dictionary mapping molecular IDs to MetaboliteMetadata objects. 385 If provided, uses metadata for compound names. 386 Default is None. 387 spectral_library : FlashEntropySearch or list of FlashEntropySearch, optional 388 FlashEntropy spectral library (or list of libraries) containing MS2 spectra. 389 If provided, uses library to retrieve MS2 spectra by ref_ms_id. 390 Default is None. 391 392 Raises 393 ------ 394 ValueError 395 If MS2 similarity results exist but molecular_metadata or spectral_library is None. 396 """ 397 if len(self.ms2_mass_spectra) == 0: 398 ax.text(0.5, 0.5, 'No MS2 data available', 399 ha='center', va='center', transform=ax.transAxes, fontsize=12) 400 ax.set_xlabel('m/z', fontsize=10) 401 ax.set_ylabel('Relative Intensity (%)', fontsize=10) 402 return 403 404 # Check if we have MS2 similarity results - if not, fall back to regular MS2 plot 405 if len(self.ms2_similarity_results) == 0: 406 self._plot_ms2_spectrum(ax) 407 return 408 409 # If we have MS2 similarity results, we need both molecular_metadata and spectral_library 410 if molecular_metadata is None or spectral_library is None: 411 raise ValueError( 412 "MS2 mirror plot requires both 'molecular_metadata' and 'spectral_library' " 413 "parameters when MS2 similarity results are present. " 414 "Please provide both parameters to plot_cluster() or plot()." 415 ) 416 417 # Get experimental MS2 418 sample_ms2 = self.best_ms2 419 sample_mz = sample_ms2.mz_exp 420 sample_int = sample_ms2.abundance 421 422 # Normalize sample MS2 423 if len(sample_int) > 0 and max(sample_int) > 0: 424 sample_int = sample_int / max(sample_int) * 100 425 426 # Plot sample MS2 on top (positive) 427 ax.vlines(sample_mz, 0, sample_int, colors='blue', alpha=0.7, linewidths=1.5, label='Sample MS2') 428 429 # Check if we have MS2 similarity results 430 library_ms2_peaks = None 431 entropy_similarity = None 432 molecule_name = None 433 mol_id = None 434 435 if len(self.ms2_similarity_results) > 0: 436 # Get all results as dataframes and find the best match 437 results_df = [x.to_dataframe() for x in self.ms2_similarity_results] 438 results_df = pd.concat(results_df) 439 results_df = results_df.sort_values(by='entropy_similarity', ascending=False) 440 441 # Get the best match 442 best_result = results_df.iloc[0] 443 entropy_similarity = best_result['entropy_similarity'] 444 mol_id = best_result.get('ref_mol_id', None) 445 ref_ms_id = best_result.get('ref_ms_id', None) 446 447 # Get library spectrum from spectral_library using ref_ms_id 448 if spectral_library is not None and ref_ms_id is not None: 449 # Handle both single library and list of libraries 450 libraries = spectral_library if isinstance(spectral_library, list) else [spectral_library] 451 452 # Search through all libraries to find the ref_ms_id 453 for library in libraries: 454 try: 455 # Get the IDs in the spectral library 456 fe_spec_index = [x["id"] for x in library].index(ref_ms_id) 457 library_ms2_peaks = library[fe_spec_index]['peaks'] 458 break # Found the spectrum, exit the loop 459 except ValueError: 460 # ref_ms_id not found in this library, continue to next 461 continue 462 463 # If ref_ms_id was not found in any library, raise an error 464 if library_ms2_peaks is None: 465 raise ValueError( 466 f"Reference MS ID '{ref_ms_id}' not found in any of the provided spectral libraries. " 467 f"Please ensure the spectral library contains the matching reference spectrum." 468 ) 469 470 # Get compound name from molecular_metadata using mol_id 471 if molecular_metadata is not None and mol_id is not None: 472 if mol_id in molecular_metadata: 473 metadata = molecular_metadata[mol_id] 474 # Get compound name from metadata 475 molecule_name = getattr(metadata, 'common_name', getattr(metadata, 'name', 'Unknown')) 476 477 # Plot library MS2 on bottom (negative/mirrored) 478 if library_ms2_peaks is not None and len(library_ms2_peaks) > 0: 479 lib_mz = library_ms2_peaks[:, 0] 480 lib_int = library_ms2_peaks[:, 1] 481 # Normalize 482 if len(lib_int) > 0 and max(lib_int) > 0: 483 lib_int = lib_int / max(lib_int) * 100 484 # Mirror to negative 485 lib_int_mirror = -lib_int 486 487 # Create label with molecule name and molecular ID 488 lib_label = f'Library MS2' 489 if molecule_name: 490 lib_label += f' ({molecule_name})' 491 if mol_id: 492 lib_label += f' [ID: {mol_id}]' 493 494 ax.vlines(lib_mz, 0, lib_int_mirror, colors='red', alpha=0.7, linewidths=1.5, label=lib_label) 495 496 ax.axhline(0, color='black', linewidth=0.5) 497 ax.set_xlabel('m/z', fontsize=10) 498 ax.set_ylabel('Relative Intensity (%)', fontsize=10) 499 ax.legend(fontsize=8, loc='upper right') 500 ax.grid(True, alpha=0.3) 501 502 # Set y-axis to symmetric range 503 ax.set_ylim(-105, 105) 504 505 # Add entropy similarity to the title if available 506 if entropy_similarity is not None: 507 ax.set_title(f'MS2 Mirror Plot (Entropy Similarity: {entropy_similarity:.3f})', loc='left') 508 else: 509 ax.set_title('MS2 Mirror Plot', loc='left') 510 511 def _plot_single_eic(self, ax, plot_smoothed=False, plot_datapoints=False, 512 eic_buffer_time=None, show_ms2_scan=True): 513 """Internal method to plot a single EIC on a given axis. 514 515 Parameters 516 ---------- 517 ax : matplotlib.axes.Axes 518 The axis to plot on. 519 plot_smoothed : bool, optional 520 If True, plot smoothed EIC. Default is False. 521 plot_datapoints : bool, optional 522 If True, plot EIC datapoints. Default is False. 523 eic_buffer_time : float, optional 524 Time buffer around the peak (minutes). If None, uses parameter setting. Default is None. 525 show_ms2_scan : bool, optional 526 If True and MS2 scans exist, show vertical line at MS2 scan time. Default is True. 527 """ 528 if self._eic_data is None: 529 raise ValueError("EIC data is not available") 530 531 if eic_buffer_time is None: 532 eic_buffer_time = self.chromatogram_parent.parameters.lc_ms.eic_buffer_time 533 534 ax.set_title("EIC", loc="left") 535 ax.plot( 536 self._eic_data.time, self._eic_data.eic, c="tab:blue", label="EIC" 537 ) 538 539 if plot_datapoints: 540 ax.scatter( 541 self._eic_data.time, 542 self._eic_data.eic, 543 c="tab:blue", 544 label="EIC Data Points", 545 ) 546 547 if plot_smoothed and hasattr(self._eic_data, 'eic_smoothed'): 548 ax.plot( 549 self._eic_data.time, 550 self._eic_data.eic_smoothed, 551 c="tab:red", 552 label="Smoothed EIC", 553 ) 554 555 # Fill integrated area if available 556 if self.start_scan is not None: 557 ax.fill_between( 558 self.eic_rt_list, self.eic_list, color="b", alpha=0.2 559 ) 560 else: 561 if self.chromatogram_parent.parameters.lc_ms.verbose_processing: 562 print( 563 f"No start and final scan numbers were provided for mass feature {self.id}" 564 ) 565 566 ax.set_ylabel("Intensity") 567 ax.set_xlabel("Time (minutes)") 568 ax.set_ylim(0, self.eic_list.max() * 1.1) 569 ax.set_xlim( 570 self.retention_time - eic_buffer_time, 571 self.retention_time + eic_buffer_time, 572 ) 573 ax.axvline( 574 x=self.retention_time, color="k", label="MS1 scan time (apex)" 575 ) 576 577 # Show MS2 scan time if available and requested 578 if show_ms2_scan and len(self.ms2_scan_numbers) > 0: 579 ax.axvline( 580 x=self.chromatogram_parent.get_time_of_scan_id( 581 self.best_ms2.scan_number 582 ), 583 color="grey", 584 linestyle="--", 585 label="MS2 scan time", 586 ) 587 588 ax.legend(loc="upper left") 589 ax.yaxis.get_major_formatter().set_useOffset(False) 590 591 def plot( 592 self, 593 to_plot=["EIC", "MS1", "MS2"], 594 return_fig=True, 595 plot_smoothed_eic=False, 596 plot_eic_datapoints=False, 597 molecular_metadata=None, 598 spectral_library=None, 599 ): 600 """Plot the mass feature. 601 602 Parameters 603 ---------- 604 to_plot : list, optional 605 List of strings specifying what to plot, any iteration of 606 "EIC", "MS2", "MS2_mirror", and "MS1". 607 Default is ["EIC", "MS1", "MS2"]. 608 return_fig : bool, optional 609 If True, the figure is returned. Default is True. 610 plot_smoothed_eic : bool, optional 611 If True, the smoothed EIC is plotted. Default is False. 612 plot_eic_datapoints : bool, optional 613 If True, the EIC data points are plotted. Default is False. 614 molecular_metadata : dict, optional 615 Dictionary mapping molecular IDs to MetaboliteMetadata objects. 616 Required if "MS2_mirror" is in to_plot. Default is None. 617 spectral_library : FlashEntropySearch, optional 618 FlashEntropy spectral library containing MS2 spectra. 619 Required if "MS2_mirror" is in to_plot. Default is None. 620 621 Returns 622 ------- 623 matplotlib.figure.Figure or None 624 The figure object if `return_fig` is True. 625 Otherwise None and the figure is displayed. 626 """ 627 # Adjust to_plot list if there are not spectra added to the mass features 628 if self.mass_spectrum is None: 629 to_plot = [x for x in to_plot if x != "MS1"] 630 if len(self.ms2_mass_spectra) == 0: 631 to_plot = [x for x in to_plot if x not in ["MS2", "MS2_mirror"]] 632 if self._eic_data is None: 633 to_plot = [x for x in to_plot if x != "EIC"] 634 635 # Check if MS2_mirror is requested without molecular_metadata 636 if "MS2_mirror" in to_plot and molecular_metadata is None: 637 raise ValueError("molecular_metadata is required when 'MS2_mirror' is in to_plot") 638 639 # Check if both MS2 and MS2_mirror are requested (not allowed) 640 if "MS2" in to_plot and "MS2_mirror" in to_plot: 641 # Remove regular MS2 if mirror is requested 642 to_plot = [x for x in to_plot if x != "MS2"] 643 644 deconvoluted = self._ms_deconvoluted_idx is not None 645 646 fig, axs = plt.subplots( 647 len(to_plot), 1, figsize=(9, len(to_plot) * 4), squeeze=False 648 ) 649 fig.suptitle( 650 f"Mass Feature {self.id}: m/z = {round(self.mz, ndigits=4)}; " 651 f"time = {round(self.retention_time, ndigits=1)} minutes" 652 ) 653 654 i = 0 655 # EIC plot 656 if "EIC" in to_plot: 657 self._plot_single_eic( 658 axs[i][0], 659 plot_smoothed=plot_smoothed_eic, 660 plot_datapoints=plot_eic_datapoints 661 ) 662 i += 1 663 664 # MS1 plot 665 if "MS1" in to_plot: 666 self._plot_ms1_spectrum(axs[i][0], deconvoluted=deconvoluted) 667 i += 1 668 669 # MS2 plot 670 if "MS2" in to_plot: 671 self._plot_ms2_spectrum(axs[i][0]) 672 i += 1 673 674 # MS2 mirror plot 675 if "MS2_mirror" in to_plot: 676 self._plot_ms2_mirror(axs[i][0], molecular_metadata=molecular_metadata, spectral_library=spectral_library) 677 i += 1 678 679 # Add space between subplots 680 plt.tight_layout() 681 682 if return_fig: 683 # Close figure 684 plt.close(fig) 685 return fig 686 687 @property 688 def mz(self): 689 """Mass to charge ratio of the mass feature""" 690 # If the mass feature has been calibrated, return the calibrated m/z, otherwise return the measured m/z 691 if self._mz_cal is not None: 692 return self._mz_cal 693 else: 694 return self._mz_exp 695 696 @property 697 def mass_spectrum_deconvoluted(self): 698 """Returns the deconvoluted mass spectrum object associated with the mass feature, if deconvolution has been performed.""" 699 if self._ms_deconvoluted_idx is not None: 700 ms_deconvoluted = copy.deepcopy(self.mass_spectrum) 701 ms_deconvoluted.set_indexes(self._ms_deconvoluted_idx) 702 return ms_deconvoluted 703 else: 704 raise ValueError( 705 "Deconvolution has not been performed for mass feature " + str(self.id) 706 ) 707 708 @property 709 def retention_time(self): 710 """Retention time of the mass feature""" 711 return self._retention_time 712 713 @retention_time.setter 714 def retention_time(self, value): 715 """Set the retention time of the mass feature""" 716 if not isinstance(value, float): 717 raise ValueError("The retention time of the mass feature must be a float") 718 self._retention_time = value 719 720 @property 721 def apex_scan(self): 722 """Apex scan of the mass feature""" 723 return self._apex_scan 724 725 @apex_scan.setter 726 def apex_scan(self, value): 727 """Set the apex scan of the mass feature""" 728 if not isinstance(value, int): 729 raise ValueError("The apex scan of the mass feature must be an integer") 730 self._apex_scan = value 731 732 @property 733 def intensity(self): 734 """Intensity of the mass feature""" 735 return self._intensity 736 737 @intensity.setter 738 def intensity(self, value): 739 """Set the intensity of the mass feature""" 740 if not isinstance(value, float): 741 raise ValueError("The intensity of the mass feature must be a float") 742 self._intensity = value 743 744 @property 745 def persistence(self): 746 """Persistence of the mass feature""" 747 return self._persistence 748 749 @persistence.setter 750 def persistence(self, value): 751 """Set the persistence of the mass feature""" 752 if not isinstance(value, float): 753 raise ValueError("The persistence of the mass feature must be a float") 754 self._persistence = value 755 756 @property 757 def eic_rt_list(self): 758 """Retention time list between the beginning and end of the mass feature""" 759 # Find index of the start and final scans in the EIC data 760 start_index = self._eic_data.scans.tolist().index(self.start_scan) 761 final_index = self._eic_data.scans.tolist().index(self.final_scan) 762 763 # Get the retention time list 764 rt_list = self._eic_data.time[start_index : final_index + 1] 765 return rt_list 766 767 @property 768 def eic_list(self): 769 """EIC List between the beginning and end of the mass feature""" 770 # Find index of the start and final scans in the EIC data 771 start_index = self._eic_data.scans.tolist().index(self.start_scan) 772 final_index = self._eic_data.scans.tolist().index(self.final_scan) 773 774 # Get the retention time list 775 eic = self._eic_data.eic[start_index : final_index + 1] 776 return eic 777 778 @property 779 def ms1_peak(self): 780 """MS1 peak from associated mass spectrum that is closest to the mass feature's m/z""" 781 # Find index array self.mass_spectrum.mz_exp that is closest to self.mz 782 closest_mz = min(self.mass_spectrum.mz_exp, key=lambda x: abs(x - self.mz)) 783 closest_mz_index = self.mass_spectrum.mz_exp.tolist().index(closest_mz) 784 785 return self.mass_spectrum._mspeaks[closest_mz_index] 786 787 @property 788 def tailing_factor(self): 789 """Tailing factor of the mass feature""" 790 return self._tailing_factor 791 792 @tailing_factor.setter 793 def tailing_factor(self, value): 794 """Set the tailing factor of the mass feature""" 795 if not isinstance(value, float): 796 raise ValueError("The tailing factor of the mass feature must be a float") 797 self._tailing_factor = value 798 799 @property 800 def dispersity_index(self): 801 """Dispersity index of the mass feature""" 802 return self._dispersity_index 803 804 @dispersity_index.setter 805 def dispersity_index(self, value): 806 """Set the dispersity index of the mass feature""" 807 if not isinstance(value, float): 808 raise ValueError("The dispersity index of the mass feature must be a float") 809 self._dispersity_index = value 810 811 @property 812 def normalized_dispersity_index(self): 813 """Normalized dispersity index of the mass feature, unitless (fraction of total window used)""" 814 return self._normalized_dispersity_index 815 816 @property 817 def half_height_width(self): 818 """Half height width of the mass feature, average of min and max values, in minutes""" 819 return np.mean(self._half_height_width) 820 821 @property 822 def noise_score(self): 823 """Mean of left and right noise scores. 824 825 Returns 826 ------- 827 float or np.nan 828 Mean noise score, or np.nan if both sides are np.nan. 829 """ 830 if self._noise_score is None: 831 return np.nan 832 833 left, right = self._noise_score 834 # Handle NaN values 835 if np.isnan(left) and np.isnan(right): 836 return np.nan 837 elif np.isnan(left): 838 return right 839 elif np.isnan(right): 840 return left 841 else: 842 return (left + right) / 2.0 843 844 @property 845 def noise_score_min(self): 846 """Minimum of left and right noise scores. 847 848 Returns 849 ------- 850 float or np.nan 851 Minimum noise score, or np.nan if both sides are np.nan. 852 """ 853 if self._noise_score is None: 854 return np.nan 855 856 left, right = self._noise_score 857 # Handle NaN values - nanmin ignores NaN 858 return np.nanmin([left, right]) 859 860 @property 861 def noise_score_max(self): 862 """Maximum of left and right noise scores. 863 864 Returns 865 ------- 866 float or np.nan 867 Maximum noise score, or np.nan if both sides are np.nan. 868 """ 869 if self._noise_score is None: 870 return np.nan 871 872 left, right = self._noise_score 873 # Handle NaN values - nanmax ignores NaN 874 return np.nanmax([left, right]) 875 876 @property 877 def type(self): 878 """Type of the mass feature. 879 880 Returns 881 ------- 882 str 883 The type of mass feature ("untargeted", "targeted", or "internal standard"). 884 """ 885 return self._type 886 887 @type.setter 888 def type(self, value): 889 """Set the type of the mass feature. 890 891 Parameters 892 ---------- 893 value : str 894 The type of mass feature. Should be one of: "untargeted", "targeted", "internal standard". 895 """ 896 if not isinstance(value, str): 897 raise ValueError("The type of the mass feature must be a string") 898 self._type = value 899 900 @property 901 def best_ms2(self): 902 """Points to the best representative MS2 mass spectrum 903 904 Notes 905 ----- 906 If there is only one MS2 mass spectrum, it will be returned 907 If there are MS2 similarity results, this will return the MS2 mass spectrum with the highest entropy similarity score. 908 If there are no MS2 similarity results, the best MS2 mass spectrum is determined by the closest scan time to the apex of the mass feature, with higher resolving power. Checks for and disqualifies possible chimeric spectra. 909 910 Returns 911 ------- 912 MassSpectrum or None 913 The best MS2 mass spectrum. 914 """ 915 if len(self.ms2_similarity_results) > 0: 916 # the scan number with the highest similarity score 917 results_df = [x.to_dataframe() for x in self.ms2_similarity_results] 918 results_df = pd.concat(results_df) 919 results_df = results_df.sort_values( 920 by="entropy_similarity", ascending=False 921 ) 922 best_scan_number = results_df.iloc[0]["query_spectrum_id"] 923 return self.ms2_mass_spectra[best_scan_number] 924 925 ms2_scans = list(self.ms2_mass_spectra.keys()) 926 if len(ms2_scans) > 1: 927 mz_diff_list = [] # List of mz difference between mz of mass feature and mass of nearest mz in each scan 928 res_list = [] # List of maximum resolving power of peaks in each scan 929 time_diff_list = [] # List of time difference between scan and apex scan in each scan 930 for scan in ms2_scans: 931 if len(self.ms2_mass_spectra[scan].mspeaks) > 0: 932 # Find mz closest to mass feature mz, return both the difference in mass and its resolution 933 closest_mz = min( 934 self.ms2_mass_spectra[scan].mz_exp, 935 key=lambda x: abs(x - self.mz), 936 ) 937 if all( 938 np.isnan(self.ms2_mass_spectra[scan].resolving_power) 939 ): # All NA for resolving power in peaks, not uncommon in CID spectra 940 res_list.append(2) # Assumes very low resolving power 941 else: 942 res_list.append( 943 np.nanmax(self.ms2_mass_spectra[scan].resolving_power) 944 ) 945 mz_diff_list.append(np.abs(closest_mz - self.mz)) 946 time_diff_list.append( 947 np.abs( 948 self.chromatogram_parent.get_time_of_scan_id(scan) 949 - self.retention_time 950 ) 951 ) 952 else: 953 res_list.append(np.nan) 954 mz_diff_list.append(np.nan) 955 time_diff_list.append(np.nan) 956 # Convert diff_lists into logical scores (higher is better for each score) 957 time_score = 1 - np.array(time_diff_list) / np.nanmax( 958 np.array(time_diff_list) 959 ) 960 res_score = np.array(res_list) / np.nanmax(np.array(res_list)) 961 # mz_score is 0 for possible chimerics, 1 for all others (already within mass tolerance before assigning) 962 mz_score = np.zeros(len(ms2_scans)) 963 for i in np.arange(0, len(ms2_scans)): 964 if mz_diff_list[i] < 0.8 and mz_diff_list[i] > 0.1: # Possible chimeric 965 mz_score[i] = 0 966 else: 967 mz_score[i] = 1 968 # get the index of the best score and return the mass spectrum 969 if len([np.nanargmax(time_score * res_score * mz_score)]) == 1: 970 return self.ms2_mass_spectra[ 971 ms2_scans[np.nanargmax(time_score * res_score * mz_score)] 972 ] 973 # remove the mz_score condition and try again 974 elif len(np.argmax(time_score * res_score)) == 1: 975 return self.ms2_mass_spectra[ 976 ms2_scans[np.nanargmax(time_score * res_score)] 977 ] 978 else: 979 raise ValueError( 980 "No best MS2 mass spectrum could be found for mass feature " 981 + str(self.id) 982 ) 983 elif len(ms2_scans) == 1: # if only one ms2 spectra, return it 984 return self.ms2_mass_spectra[ms2_scans[0]] 985 else: # if no ms2 spectra, return None 986 return None 987 988 989class GCPeak(ChromaPeakBase, GCPeakCalculation): 990 """Class representing a peak in a gas chromatography (GC) chromatogram. 991 992 Parameters 993 ---------- 994 chromatogram_parent : Chromatogram 995 The parent chromatogram object. 996 mass_spectrum_obj : MassSpectrum 997 The mass spectrum object associated with the peak. 998 indexes : tuple 999 The indexes of the peak in the chromatogram. 1000 1001 Attributes 1002 ---------- 1003 _compounds : list 1004 List of compounds associated with the peak. 1005 _ri : float or None 1006 Retention index of the peak. 1007 1008 Methods 1009 ------- 1010 * __len__(). Returns the number of compounds associated with the peak. 1011 * __getitem__(position). Returns the compound at the specified position. 1012 * remove_compound(compounds_obj). Removes the specified compound from the peak. 1013 * clear_compounds(). Removes all compounds from the peak. 1014 * add_compound(compounds_dict, spectral_similarity_scores, ri_score=None, similarity_score=None). Adds a compound to the peak with the specified attributes. 1015 * ri(). Returns the retention index of the peak. 1016 * highest_ss_compound(). Returns the compound with the highest spectral similarity score. 1017 * highest_score_compound(). Returns the compound with the highest similarity score. 1018 * compound_names(). Returns a list of names of compounds associated with the peak. 1019 """ 1020 1021 def __init__(self, chromatogram_parent, mass_spectrum_obj, indexes): 1022 self._compounds = [] 1023 self._ri = None 1024 super().__init__(chromatogram_parent, mass_spectrum_obj, *indexes) 1025 1026 def __len__(self): 1027 return len(self._compounds) 1028 1029 def __getitem__(self, position): 1030 return self._compounds[position] 1031 1032 def remove_compound(self, compounds_obj): 1033 self._compounds.remove(compounds_obj) 1034 1035 def clear_compounds(self): 1036 self._compounds = [] 1037 1038 def add_compound( 1039 self, 1040 compounds_dict, 1041 spectral_similarity_scores, 1042 ri_score=None, 1043 similarity_score=None, 1044 ): 1045 """Adds a compound to the peak with the specified attributes. 1046 1047 Parameters 1048 ---------- 1049 compounds_dict : dict 1050 Dictionary containing the compound information. 1051 spectral_similarity_scores : dict 1052 Dictionary containing the spectral similarity scores. 1053 ri_score : float or None, optional 1054 The retention index score of the compound. Default is None. 1055 similarity_score : float or None, optional 1056 The similarity score of the compound. Default is None. 1057 """ 1058 compound_obj = LowResCompoundRef(compounds_dict) 1059 compound_obj.spectral_similarity_scores = spectral_similarity_scores 1060 compound_obj.spectral_similarity_score = spectral_similarity_scores.get( 1061 "cosine_correlation" 1062 ) 1063 # TODO check is the above line correct? 1064 compound_obj.ri_score = ri_score 1065 compound_obj.similarity_score = similarity_score 1066 self._compounds.append(compound_obj) 1067 if similarity_score: 1068 self._compounds.sort(key=lambda c: c.similarity_score, reverse=True) 1069 else: 1070 self._compounds.sort( 1071 key=lambda c: c.spectral_similarity_score, reverse=True 1072 ) 1073 1074 @property 1075 def ri(self): 1076 """Returns the retention index of the peak. 1077 1078 Returns 1079 ------- 1080 float or None 1081 The retention index of the peak. 1082 """ 1083 return self._ri 1084 1085 @property 1086 def highest_ss_compound(self): 1087 """Returns the compound with the highest spectral similarity score. 1088 1089 Returns 1090 ------- 1091 LowResCompoundRef or None 1092 The compound with the highest spectral similarity score. 1093 """ 1094 if self: 1095 return max(self, key=lambda c: c.spectral_similarity_score) 1096 else: 1097 return None 1098 1099 @property 1100 def highest_score_compound(self): 1101 """Returns the compound with the highest similarity score. 1102 1103 Returns 1104 ------- 1105 LowResCompoundRef or None 1106 The compound with the highest similarity score. 1107 """ 1108 if self: 1109 return max(self, key=lambda c: c.similarity_score) 1110 else: 1111 return None 1112 1113 @property 1114 def compound_names(self): 1115 """Returns a list of names of compounds associated with the peak. 1116 1117 Returns 1118 ------- 1119 list 1120 List of names of compounds associated with the peak. 1121 """ 1122 if self: 1123 return [c.name for c in self] 1124 else: 1125 return [] 1126 1127 1128class GCPeakDeconvolved(GCPeak): 1129 """Represents a deconvolved peak in a chromatogram. 1130 1131 Parameters 1132 ---------- 1133 chromatogram_parent : Chromatogram 1134 The parent chromatogram object. 1135 mass_spectra : list 1136 List of mass spectra associated with the peak. 1137 apex_index : int 1138 Index of the apex mass spectrum in the `mass_spectra` list. 1139 rt_list : list 1140 List of retention times. 1141 tic_list : list 1142 List of total ion currents. 1143 """ 1144 1145 def __init__( 1146 self, chromatogram_parent, mass_spectra, apex_index, rt_list, tic_list 1147 ): 1148 self._ri = None 1149 self._rt_list = list(rt_list) 1150 self._tic_list = list(tic_list) 1151 self.mass_spectra = list(mass_spectra) 1152 super().__init__( 1153 chromatogram_parent, 1154 self.mass_spectra[apex_index], 1155 (0, apex_index, len(self.mass_spectra) - 1), 1156 ) 1157 1158 @property 1159 def rt_list(self): 1160 """Get the list of retention times. 1161 1162 Returns 1163 ------- 1164 list 1165 The list of retention times. 1166 """ 1167 return self._rt_list 1168 1169 @property 1170 def tic_list(self): 1171 """Get the list of total ion currents. 1172 1173 Returns 1174 ------- 1175 list 1176 The list of total ion currents. 1177 """ 1178 return self._tic_list
18class ChromaPeakBase: 19 """Base class for chromatographic peak (ChromaPeak) objects. 20 21 Parameters 22 ------- 23 chromatogram_parent : Chromatogram 24 The parent chromatogram object. 25 mass_spectrum_obj : MassSpectrum 26 The mass spectrum object. 27 start_index : int 28 The start index of the peak. 29 index : int 30 The index of the peak. 31 final_index : int 32 The final index of the peak. 33 34 Attributes 35 -------- 36 start_scan : int 37 The start scan of the peak. 38 final_scan : int 39 The final scan of the peak. 40 apex_scan : int 41 The apex scan of the peak. 42 chromatogram_parent : Chromatogram 43 The parent chromatogram object. 44 mass_spectrum : MassSpectrum 45 The mass spectrum object. 46 _area : float 47 The area of the peak. 48 49 Properties 50 -------- 51 * retention_time : float. 52 The retention time of the peak. 53 * tic : float. 54 The total ion current of the peak. 55 * area : float. 56 The area of the peak. 57 * rt_list : list. 58 The list of retention times within the peak. 59 * tic_list : list. 60 The list of total ion currents within the peak. 61 62 Methods 63 -------- 64 * None 65 """ 66 67 def __init__( 68 self, chromatogram_parent, mass_spectrum_obj, start_index, index, final_index 69 ): 70 self.start_scan = start_index 71 self.final_scan = final_index 72 self.apex_scan = int(index) 73 self.chromatogram_parent = chromatogram_parent 74 self.mass_spectrum = mass_spectrum_obj 75 self._area = None 76 77 @property 78 def retention_time(self): 79 """Retention Time""" 80 return self.mass_spectrum.retention_time 81 82 @property 83 def tic(self): 84 """Total Ion Current""" 85 return self.mass_spectrum.tic 86 87 @property 88 def area(self): 89 """Peak Area""" 90 return self._area 91 92 @property 93 def rt_list(self): 94 """Retention Time List""" 95 return [ 96 self.chromatogram_parent.retention_time[i] 97 for i in range(self.start_scan, self.final_scan + 1) 98 ] 99 100 @property 101 def tic_list(self): 102 """Total Ion Current List""" 103 return [ 104 self.chromatogram_parent.tic[i] 105 for i in range(self.start_scan, self.final_scan + 1) 106 ]
Base class for chromatographic peak (ChromaPeak) objects.
Parameters
- chromatogram_parent (Chromatogram): The parent chromatogram object.
- mass_spectrum_obj (MassSpectrum): The mass spectrum object.
- start_index (int): The start index of the peak.
- index (int): The index of the peak.
- final_index (int): The final index of the peak.
Attributes
- start_scan (int): The start scan of the peak.
- final_scan (int): The final scan of the peak.
- apex_scan (int): The apex scan of the peak.
- chromatogram_parent (Chromatogram): The parent chromatogram object.
- mass_spectrum (MassSpectrum): The mass spectrum object.
- _area (float): The area of the peak.
Properties
- retention_time : float. The retention time of the peak.
- tic : float. The total ion current of the peak.
- area : float. The area of the peak.
- rt_list : list. The list of retention times within the peak.
- tic_list : list. The list of total ion currents within the peak.
Methods
- None
67 def __init__( 68 self, chromatogram_parent, mass_spectrum_obj, start_index, index, final_index 69 ): 70 self.start_scan = start_index 71 self.final_scan = final_index 72 self.apex_scan = int(index) 73 self.chromatogram_parent = chromatogram_parent 74 self.mass_spectrum = mass_spectrum_obj 75 self._area = None
77 @property 78 def retention_time(self): 79 """Retention Time""" 80 return self.mass_spectrum.retention_time
Retention Time
109class LCMSMassFeature(ChromaPeakBase, LCMSMassFeatureCalculation): 110 """Class representing a mass feature in a liquid chromatography (LC) chromatogram. 111 112 Parameters 113 ------- 114 lcms_parent : LCMS 115 The parent LCMSBase object. 116 mz : float 117 The observed mass to charge ratio of the feature. 118 retention_time : float 119 The retention time of the feature (in minutes), at the apex. 120 intensity : float 121 The intensity of the feature. 122 apex_scan : int 123 The scan number of the apex of the feature. 124 persistence : float, optional 125 The persistence of the feature. Default is None. 126 127 Attributes 128 -------- 129 _mz_exp : float 130 The observed mass to charge ratio of the feature. 131 _mz_cal : float 132 The calibrated mass to charge ratio of the feature. 133 _retention_time : float 134 The retention time of the feature (in minutes), at the apex. 135 _apex_scan : int 136 The scan number of the apex of the feature. 137 _intensity : float 138 The intensity of the feature. 139 _persistence : float 140 The persistence of the feature. 141 _eic_data : EIC_Data 142 The EIC data object associated with the feature. 143 _eic_mz : float 144 The m/z value used to extract the EIC data, 145 sometimes different from the observed m/z due to calibration, centroiding, or other processing. 146 _dispersity_index : float 147 The dispersity index of the feature, in minutes. 148 _normalized_dispersity_index : float 149 The normalized dispersity index of the feature (unitless, fraction of total window used to calculate dispersity index). 150 _half_height_width : numpy.ndarray 151 The half height width of the feature (in minutes, as an array of min and max values). 152 _tailing_factor : float 153 The tailing factor of the feature. 154 > 1 indicates tailing, < 1 indicates fronting, = 1 indicates symmetrical peak. 155 _noise_score : tuple 156 The noise score of the feature, as a tuple of (left, right) scores. 157 Each score is a float, with higher values indicating better signal to noise. 158 _gaussian_similarity : float 159 The Gaussian similarity of the feature, as a float between 0 and 1. 160 1 indicates a perfect Gaussian shape, 0 indicates a non-Gaussian shape. 161 _ms_deconvoluted_idx : [int] 162 The indexes of the mass_spectrum attribute in the deconvoluted mass spectrum. 163 _type : str 164 The type of mass feature. Default is "untargeted". 165 Can be "untargeted", "targeted", or another customized type. 166 is_calibrated : bool 167 If True, the feature has been calibrated. Default is False. 168 monoisotopic_mf_id : int 169 Mass feature id that is the monoisotopic version of self. 170 If self.id, then self is the monoisotopic feature). Default is None. 171 isotopologue_type : str 172 The isotopic class of the feature, i.e. "13C1", "13C2", "13C1 37Cl1" etc. 173 Default is None. 174 ms2_scan_numbers : list 175 List of scan numbers of the MS2 spectra associated with the feature. 176 Default is an empty list. 177 ms2_mass_spectra : dict 178 Dictionary of MS2 spectra associated with the feature (key = scan number for DDA). 179 Default is an empty dictionary. 180 ms2_similarity_results : list 181 List of MS2 similarity results associated with the mass feature. 182 Default is an empty list. 183 id : int 184 The ID of the feature, also the key in the parent LCMS object's 185 `mass_features` dictionary. 186 mass_spectrum_deconvoluted_parent : bool 187 If True, the mass feature corresponds to the most intense peak in the deconvoluted mass spectrum. Default is None. 188 associated_mass_features_deconvoluted : list 189 List of mass features associated with the deconvoluted mass spectrum. Default is an empty list. 190 191 """ 192 193 def __init__( 194 self, 195 lcms_parent, 196 mz: float, 197 retention_time: float, 198 intensity: float, 199 apex_scan: int, 200 persistence: float = None, 201 id: int = None 202 ): 203 super().__init__( 204 chromatogram_parent=lcms_parent, 205 mass_spectrum_obj=None, 206 start_index=None, 207 index=apex_scan, 208 final_index=None, 209 ) 210 # Core attributes, marked as private 211 self._mz_exp: float = mz 212 self._mz_cal: float = None 213 self._retention_time: float = retention_time 214 self._apex_scan: int = apex_scan 215 self._intensity: float = intensity 216 self._persistence: float = persistence 217 self._eic_data: EIC_Data = None 218 self._dispersity_index: float = None 219 self._normalized_dispersity_index: float = None 220 self._half_height_width: np.ndarray = None 221 self._ms_deconvoluted_idx = None 222 self._tailing_factor: float = None 223 self._noise_score: tuple = None 224 self._gaussian_similarity: float = None 225 self._type: str = "untargeted" 226 227 # Additional attributes 228 self.monoisotopic_mf_id = None 229 self.isotopologue_type = None 230 self.ms2_scan_numbers = [] 231 self.ms2_mass_spectra = {} 232 self.ms2_similarity_results = [] 233 self.mass_spectrum_deconvoluted_parent: bool = None 234 self.associated_mass_features_deconvoluted = [] 235 236 if id: 237 self.id = id 238 else: 239 # get the parent's mass feature keys and add 1 to the max value to get the new key 240 self.id = ( 241 max(lcms_parent.mass_features.keys()) + 1 242 if lcms_parent.mass_features.keys() 243 else 0 244 ) 245 246 def update_mz(self): 247 """Update the mass to charge ratio from the mass spectrum object.""" 248 if self.mass_spectrum is None: 249 raise ValueError( 250 "The mass spectrum object is not set, cannot update the m/z from the MassSpectrum object" 251 ) 252 if len(self.mass_spectrum.mz_exp) == 0: 253 raise ValueError( 254 "The mass spectrum object has no m/z values, cannot update the m/z from the MassSpectrum object until it is processed" 255 ) 256 new_mz = self.ms1_peak.mz_exp 257 258 # calculate the difference between the new and old m/z, only update if it is close 259 mz_diff = new_mz - self.mz 260 if abs(mz_diff) < 0.01: 261 self._mz_exp = new_mz 262 263 def _plot_ms1_spectrum(self, ax, deconvoluted=False, sample_name=None): 264 """Internal method to plot MS1 spectrum on a given axis. 265 266 Parameters 267 ---------- 268 ax : matplotlib.axes.Axes 269 The axis to plot on. 270 deconvoluted : bool, optional 271 If True and deconvoluted spectrum exists, plot both raw and deconvoluted. Default is False. 272 sample_name : str, optional 273 Sample name to include in title. Default is None. 274 """ 275 if self.mass_spectrum is None: 276 raise ValueError("MS1 spectrum is not available") 277 278 title_prefix = "MS1 (deconvoluted)" if deconvoluted else "MS1 (raw)" 279 if sample_name: 280 ax.set_title(f"{title_prefix} - {sample_name}", loc="left") 281 else: 282 ax.set_title(title_prefix, loc="left") 283 284 if deconvoluted and self._ms_deconvoluted_idx is not None: 285 # Plot both raw and deconvoluted 286 ax.vlines( 287 self.mass_spectrum.mz_exp, 288 0, 289 self.mass_spectrum.abundance, 290 color="k", 291 alpha=0.2, 292 label="Raw MS1", 293 ) 294 ax.vlines( 295 self.mass_spectrum_deconvoluted.mz_exp, 296 0, 297 self.mass_spectrum_deconvoluted.abundance, 298 color="k", 299 label="Deconvoluted MS1", 300 ) 301 ax.set_xlim( 302 self.mass_spectrum_deconvoluted.mz_exp.min() * 0.8, 303 self.mass_spectrum_deconvoluted.mz_exp.max() * 1.1, 304 ) 305 ax.set_ylim( 306 0, self.mass_spectrum_deconvoluted.abundance.max() * 1.1 307 ) 308 else: 309 # Plot raw only 310 ax.vlines( 311 self.mass_spectrum.mz_exp, 312 0, 313 self.mass_spectrum.abundance, 314 color="k", 315 label="Raw MS1", 316 ) 317 ax.set_xlim( 318 self.mass_spectrum.mz_exp.min() * 0.8, 319 self.mass_spectrum.mz_exp.max() * 1.1, 320 ) 321 ax.set_ylim(bottom=0) 322 323 # Highlight the feature m/z if close enough 324 if abs(self.ms1_peak.mz_exp - self.mz) < 0.01: 325 ax.vlines( 326 self.ms1_peak.mz_exp, 327 0, 328 self.ms1_peak.abundance, 329 color="m", 330 label="Feature m/z", 331 ) 332 else: 333 if self.chromatogram_parent.parameters.lc_ms.verbose_processing: 334 print( 335 f"The m/z of the mass feature {self.id} is different from the m/z of MS1 peak, " 336 "the MS1 peak will not be plotted" 337 ) 338 339 ax.legend(loc="upper left") 340 ax.set_ylabel("Intensity") 341 ax.set_xlabel("m/z") 342 # Combining tick_params(labelleft=False) with set_title(loc="left") makes 343 # tight_layout() produce NaN axis positions on matplotlib 3.11/numpy 2.5. 344 ax.set_yticklabels([]) 345 346 def _plot_ms2_spectrum(self, ax, sample_name=None): 347 """Internal method to plot MS2 spectrum on a given axis. 348 349 Parameters 350 ---------- 351 ax : matplotlib.axes.Axes 352 The axis to plot on. 353 sample_name : str, optional 354 Sample name to include in title. Default is None. 355 """ 356 if len(self.ms2_mass_spectra) == 0: 357 raise ValueError("MS2 spectrum is not available") 358 359 if sample_name: 360 ax.set_title(f"MS2 - {sample_name}", loc="left") 361 else: 362 ax.set_title("MS2", loc="left") 363 364 ax.vlines( 365 self.best_ms2.mz_exp, 0, self.best_ms2.abundance, color="k" 366 ) 367 ax.set_ylabel("Intensity") 368 ax.set_xlabel("m/z") 369 ax.set_ylim(bottom=0) 370 ax.yaxis.get_major_formatter().set_scientific(False) 371 ax.yaxis.get_major_formatter().set_useOffset(False) 372 373 def _plot_ms2_mirror(self, ax, molecular_metadata=None, spectral_library=None): 374 """Internal method to plot MS2 mirror spectrum on a given axis. 375 376 Plots experimental MS2 on top (positive) and library MS2 on bottom (negative/mirrored) 377 if MS2 similarity results are available. If no MS2 similarity results exist, 378 falls back to regular MS2 plot. 379 380 Parameters 381 ---------- 382 ax : matplotlib.axes.Axes 383 The axis to plot on. 384 molecular_metadata : dict, optional 385 Dictionary mapping molecular IDs to MetaboliteMetadata objects. 386 If provided, uses metadata for compound names. 387 Default is None. 388 spectral_library : FlashEntropySearch or list of FlashEntropySearch, optional 389 FlashEntropy spectral library (or list of libraries) containing MS2 spectra. 390 If provided, uses library to retrieve MS2 spectra by ref_ms_id. 391 Default is None. 392 393 Raises 394 ------ 395 ValueError 396 If MS2 similarity results exist but molecular_metadata or spectral_library is None. 397 """ 398 if len(self.ms2_mass_spectra) == 0: 399 ax.text(0.5, 0.5, 'No MS2 data available', 400 ha='center', va='center', transform=ax.transAxes, fontsize=12) 401 ax.set_xlabel('m/z', fontsize=10) 402 ax.set_ylabel('Relative Intensity (%)', fontsize=10) 403 return 404 405 # Check if we have MS2 similarity results - if not, fall back to regular MS2 plot 406 if len(self.ms2_similarity_results) == 0: 407 self._plot_ms2_spectrum(ax) 408 return 409 410 # If we have MS2 similarity results, we need both molecular_metadata and spectral_library 411 if molecular_metadata is None or spectral_library is None: 412 raise ValueError( 413 "MS2 mirror plot requires both 'molecular_metadata' and 'spectral_library' " 414 "parameters when MS2 similarity results are present. " 415 "Please provide both parameters to plot_cluster() or plot()." 416 ) 417 418 # Get experimental MS2 419 sample_ms2 = self.best_ms2 420 sample_mz = sample_ms2.mz_exp 421 sample_int = sample_ms2.abundance 422 423 # Normalize sample MS2 424 if len(sample_int) > 0 and max(sample_int) > 0: 425 sample_int = sample_int / max(sample_int) * 100 426 427 # Plot sample MS2 on top (positive) 428 ax.vlines(sample_mz, 0, sample_int, colors='blue', alpha=0.7, linewidths=1.5, label='Sample MS2') 429 430 # Check if we have MS2 similarity results 431 library_ms2_peaks = None 432 entropy_similarity = None 433 molecule_name = None 434 mol_id = None 435 436 if len(self.ms2_similarity_results) > 0: 437 # Get all results as dataframes and find the best match 438 results_df = [x.to_dataframe() for x in self.ms2_similarity_results] 439 results_df = pd.concat(results_df) 440 results_df = results_df.sort_values(by='entropy_similarity', ascending=False) 441 442 # Get the best match 443 best_result = results_df.iloc[0] 444 entropy_similarity = best_result['entropy_similarity'] 445 mol_id = best_result.get('ref_mol_id', None) 446 ref_ms_id = best_result.get('ref_ms_id', None) 447 448 # Get library spectrum from spectral_library using ref_ms_id 449 if spectral_library is not None and ref_ms_id is not None: 450 # Handle both single library and list of libraries 451 libraries = spectral_library if isinstance(spectral_library, list) else [spectral_library] 452 453 # Search through all libraries to find the ref_ms_id 454 for library in libraries: 455 try: 456 # Get the IDs in the spectral library 457 fe_spec_index = [x["id"] for x in library].index(ref_ms_id) 458 library_ms2_peaks = library[fe_spec_index]['peaks'] 459 break # Found the spectrum, exit the loop 460 except ValueError: 461 # ref_ms_id not found in this library, continue to next 462 continue 463 464 # If ref_ms_id was not found in any library, raise an error 465 if library_ms2_peaks is None: 466 raise ValueError( 467 f"Reference MS ID '{ref_ms_id}' not found in any of the provided spectral libraries. " 468 f"Please ensure the spectral library contains the matching reference spectrum." 469 ) 470 471 # Get compound name from molecular_metadata using mol_id 472 if molecular_metadata is not None and mol_id is not None: 473 if mol_id in molecular_metadata: 474 metadata = molecular_metadata[mol_id] 475 # Get compound name from metadata 476 molecule_name = getattr(metadata, 'common_name', getattr(metadata, 'name', 'Unknown')) 477 478 # Plot library MS2 on bottom (negative/mirrored) 479 if library_ms2_peaks is not None and len(library_ms2_peaks) > 0: 480 lib_mz = library_ms2_peaks[:, 0] 481 lib_int = library_ms2_peaks[:, 1] 482 # Normalize 483 if len(lib_int) > 0 and max(lib_int) > 0: 484 lib_int = lib_int / max(lib_int) * 100 485 # Mirror to negative 486 lib_int_mirror = -lib_int 487 488 # Create label with molecule name and molecular ID 489 lib_label = f'Library MS2' 490 if molecule_name: 491 lib_label += f' ({molecule_name})' 492 if mol_id: 493 lib_label += f' [ID: {mol_id}]' 494 495 ax.vlines(lib_mz, 0, lib_int_mirror, colors='red', alpha=0.7, linewidths=1.5, label=lib_label) 496 497 ax.axhline(0, color='black', linewidth=0.5) 498 ax.set_xlabel('m/z', fontsize=10) 499 ax.set_ylabel('Relative Intensity (%)', fontsize=10) 500 ax.legend(fontsize=8, loc='upper right') 501 ax.grid(True, alpha=0.3) 502 503 # Set y-axis to symmetric range 504 ax.set_ylim(-105, 105) 505 506 # Add entropy similarity to the title if available 507 if entropy_similarity is not None: 508 ax.set_title(f'MS2 Mirror Plot (Entropy Similarity: {entropy_similarity:.3f})', loc='left') 509 else: 510 ax.set_title('MS2 Mirror Plot', loc='left') 511 512 def _plot_single_eic(self, ax, plot_smoothed=False, plot_datapoints=False, 513 eic_buffer_time=None, show_ms2_scan=True): 514 """Internal method to plot a single EIC on a given axis. 515 516 Parameters 517 ---------- 518 ax : matplotlib.axes.Axes 519 The axis to plot on. 520 plot_smoothed : bool, optional 521 If True, plot smoothed EIC. Default is False. 522 plot_datapoints : bool, optional 523 If True, plot EIC datapoints. Default is False. 524 eic_buffer_time : float, optional 525 Time buffer around the peak (minutes). If None, uses parameter setting. Default is None. 526 show_ms2_scan : bool, optional 527 If True and MS2 scans exist, show vertical line at MS2 scan time. Default is True. 528 """ 529 if self._eic_data is None: 530 raise ValueError("EIC data is not available") 531 532 if eic_buffer_time is None: 533 eic_buffer_time = self.chromatogram_parent.parameters.lc_ms.eic_buffer_time 534 535 ax.set_title("EIC", loc="left") 536 ax.plot( 537 self._eic_data.time, self._eic_data.eic, c="tab:blue", label="EIC" 538 ) 539 540 if plot_datapoints: 541 ax.scatter( 542 self._eic_data.time, 543 self._eic_data.eic, 544 c="tab:blue", 545 label="EIC Data Points", 546 ) 547 548 if plot_smoothed and hasattr(self._eic_data, 'eic_smoothed'): 549 ax.plot( 550 self._eic_data.time, 551 self._eic_data.eic_smoothed, 552 c="tab:red", 553 label="Smoothed EIC", 554 ) 555 556 # Fill integrated area if available 557 if self.start_scan is not None: 558 ax.fill_between( 559 self.eic_rt_list, self.eic_list, color="b", alpha=0.2 560 ) 561 else: 562 if self.chromatogram_parent.parameters.lc_ms.verbose_processing: 563 print( 564 f"No start and final scan numbers were provided for mass feature {self.id}" 565 ) 566 567 ax.set_ylabel("Intensity") 568 ax.set_xlabel("Time (minutes)") 569 ax.set_ylim(0, self.eic_list.max() * 1.1) 570 ax.set_xlim( 571 self.retention_time - eic_buffer_time, 572 self.retention_time + eic_buffer_time, 573 ) 574 ax.axvline( 575 x=self.retention_time, color="k", label="MS1 scan time (apex)" 576 ) 577 578 # Show MS2 scan time if available and requested 579 if show_ms2_scan and len(self.ms2_scan_numbers) > 0: 580 ax.axvline( 581 x=self.chromatogram_parent.get_time_of_scan_id( 582 self.best_ms2.scan_number 583 ), 584 color="grey", 585 linestyle="--", 586 label="MS2 scan time", 587 ) 588 589 ax.legend(loc="upper left") 590 ax.yaxis.get_major_formatter().set_useOffset(False) 591 592 def plot( 593 self, 594 to_plot=["EIC", "MS1", "MS2"], 595 return_fig=True, 596 plot_smoothed_eic=False, 597 plot_eic_datapoints=False, 598 molecular_metadata=None, 599 spectral_library=None, 600 ): 601 """Plot the mass feature. 602 603 Parameters 604 ---------- 605 to_plot : list, optional 606 List of strings specifying what to plot, any iteration of 607 "EIC", "MS2", "MS2_mirror", and "MS1". 608 Default is ["EIC", "MS1", "MS2"]. 609 return_fig : bool, optional 610 If True, the figure is returned. Default is True. 611 plot_smoothed_eic : bool, optional 612 If True, the smoothed EIC is plotted. Default is False. 613 plot_eic_datapoints : bool, optional 614 If True, the EIC data points are plotted. Default is False. 615 molecular_metadata : dict, optional 616 Dictionary mapping molecular IDs to MetaboliteMetadata objects. 617 Required if "MS2_mirror" is in to_plot. Default is None. 618 spectral_library : FlashEntropySearch, optional 619 FlashEntropy spectral library containing MS2 spectra. 620 Required if "MS2_mirror" is in to_plot. Default is None. 621 622 Returns 623 ------- 624 matplotlib.figure.Figure or None 625 The figure object if `return_fig` is True. 626 Otherwise None and the figure is displayed. 627 """ 628 # Adjust to_plot list if there are not spectra added to the mass features 629 if self.mass_spectrum is None: 630 to_plot = [x for x in to_plot if x != "MS1"] 631 if len(self.ms2_mass_spectra) == 0: 632 to_plot = [x for x in to_plot if x not in ["MS2", "MS2_mirror"]] 633 if self._eic_data is None: 634 to_plot = [x for x in to_plot if x != "EIC"] 635 636 # Check if MS2_mirror is requested without molecular_metadata 637 if "MS2_mirror" in to_plot and molecular_metadata is None: 638 raise ValueError("molecular_metadata is required when 'MS2_mirror' is in to_plot") 639 640 # Check if both MS2 and MS2_mirror are requested (not allowed) 641 if "MS2" in to_plot and "MS2_mirror" in to_plot: 642 # Remove regular MS2 if mirror is requested 643 to_plot = [x for x in to_plot if x != "MS2"] 644 645 deconvoluted = self._ms_deconvoluted_idx is not None 646 647 fig, axs = plt.subplots( 648 len(to_plot), 1, figsize=(9, len(to_plot) * 4), squeeze=False 649 ) 650 fig.suptitle( 651 f"Mass Feature {self.id}: m/z = {round(self.mz, ndigits=4)}; " 652 f"time = {round(self.retention_time, ndigits=1)} minutes" 653 ) 654 655 i = 0 656 # EIC plot 657 if "EIC" in to_plot: 658 self._plot_single_eic( 659 axs[i][0], 660 plot_smoothed=plot_smoothed_eic, 661 plot_datapoints=plot_eic_datapoints 662 ) 663 i += 1 664 665 # MS1 plot 666 if "MS1" in to_plot: 667 self._plot_ms1_spectrum(axs[i][0], deconvoluted=deconvoluted) 668 i += 1 669 670 # MS2 plot 671 if "MS2" in to_plot: 672 self._plot_ms2_spectrum(axs[i][0]) 673 i += 1 674 675 # MS2 mirror plot 676 if "MS2_mirror" in to_plot: 677 self._plot_ms2_mirror(axs[i][0], molecular_metadata=molecular_metadata, spectral_library=spectral_library) 678 i += 1 679 680 # Add space between subplots 681 plt.tight_layout() 682 683 if return_fig: 684 # Close figure 685 plt.close(fig) 686 return fig 687 688 @property 689 def mz(self): 690 """Mass to charge ratio of the mass feature""" 691 # If the mass feature has been calibrated, return the calibrated m/z, otherwise return the measured m/z 692 if self._mz_cal is not None: 693 return self._mz_cal 694 else: 695 return self._mz_exp 696 697 @property 698 def mass_spectrum_deconvoluted(self): 699 """Returns the deconvoluted mass spectrum object associated with the mass feature, if deconvolution has been performed.""" 700 if self._ms_deconvoluted_idx is not None: 701 ms_deconvoluted = copy.deepcopy(self.mass_spectrum) 702 ms_deconvoluted.set_indexes(self._ms_deconvoluted_idx) 703 return ms_deconvoluted 704 else: 705 raise ValueError( 706 "Deconvolution has not been performed for mass feature " + str(self.id) 707 ) 708 709 @property 710 def retention_time(self): 711 """Retention time of the mass feature""" 712 return self._retention_time 713 714 @retention_time.setter 715 def retention_time(self, value): 716 """Set the retention time of the mass feature""" 717 if not isinstance(value, float): 718 raise ValueError("The retention time of the mass feature must be a float") 719 self._retention_time = value 720 721 @property 722 def apex_scan(self): 723 """Apex scan of the mass feature""" 724 return self._apex_scan 725 726 @apex_scan.setter 727 def apex_scan(self, value): 728 """Set the apex scan of the mass feature""" 729 if not isinstance(value, int): 730 raise ValueError("The apex scan of the mass feature must be an integer") 731 self._apex_scan = value 732 733 @property 734 def intensity(self): 735 """Intensity of the mass feature""" 736 return self._intensity 737 738 @intensity.setter 739 def intensity(self, value): 740 """Set the intensity of the mass feature""" 741 if not isinstance(value, float): 742 raise ValueError("The intensity of the mass feature must be a float") 743 self._intensity = value 744 745 @property 746 def persistence(self): 747 """Persistence of the mass feature""" 748 return self._persistence 749 750 @persistence.setter 751 def persistence(self, value): 752 """Set the persistence of the mass feature""" 753 if not isinstance(value, float): 754 raise ValueError("The persistence of the mass feature must be a float") 755 self._persistence = value 756 757 @property 758 def eic_rt_list(self): 759 """Retention time list between the beginning and end of the mass feature""" 760 # Find index of the start and final scans in the EIC data 761 start_index = self._eic_data.scans.tolist().index(self.start_scan) 762 final_index = self._eic_data.scans.tolist().index(self.final_scan) 763 764 # Get the retention time list 765 rt_list = self._eic_data.time[start_index : final_index + 1] 766 return rt_list 767 768 @property 769 def eic_list(self): 770 """EIC List between the beginning and end of the mass feature""" 771 # Find index of the start and final scans in the EIC data 772 start_index = self._eic_data.scans.tolist().index(self.start_scan) 773 final_index = self._eic_data.scans.tolist().index(self.final_scan) 774 775 # Get the retention time list 776 eic = self._eic_data.eic[start_index : final_index + 1] 777 return eic 778 779 @property 780 def ms1_peak(self): 781 """MS1 peak from associated mass spectrum that is closest to the mass feature's m/z""" 782 # Find index array self.mass_spectrum.mz_exp that is closest to self.mz 783 closest_mz = min(self.mass_spectrum.mz_exp, key=lambda x: abs(x - self.mz)) 784 closest_mz_index = self.mass_spectrum.mz_exp.tolist().index(closest_mz) 785 786 return self.mass_spectrum._mspeaks[closest_mz_index] 787 788 @property 789 def tailing_factor(self): 790 """Tailing factor of the mass feature""" 791 return self._tailing_factor 792 793 @tailing_factor.setter 794 def tailing_factor(self, value): 795 """Set the tailing factor of the mass feature""" 796 if not isinstance(value, float): 797 raise ValueError("The tailing factor of the mass feature must be a float") 798 self._tailing_factor = value 799 800 @property 801 def dispersity_index(self): 802 """Dispersity index of the mass feature""" 803 return self._dispersity_index 804 805 @dispersity_index.setter 806 def dispersity_index(self, value): 807 """Set the dispersity index of the mass feature""" 808 if not isinstance(value, float): 809 raise ValueError("The dispersity index of the mass feature must be a float") 810 self._dispersity_index = value 811 812 @property 813 def normalized_dispersity_index(self): 814 """Normalized dispersity index of the mass feature, unitless (fraction of total window used)""" 815 return self._normalized_dispersity_index 816 817 @property 818 def half_height_width(self): 819 """Half height width of the mass feature, average of min and max values, in minutes""" 820 return np.mean(self._half_height_width) 821 822 @property 823 def noise_score(self): 824 """Mean of left and right noise scores. 825 826 Returns 827 ------- 828 float or np.nan 829 Mean noise score, or np.nan if both sides are np.nan. 830 """ 831 if self._noise_score is None: 832 return np.nan 833 834 left, right = self._noise_score 835 # Handle NaN values 836 if np.isnan(left) and np.isnan(right): 837 return np.nan 838 elif np.isnan(left): 839 return right 840 elif np.isnan(right): 841 return left 842 else: 843 return (left + right) / 2.0 844 845 @property 846 def noise_score_min(self): 847 """Minimum of left and right noise scores. 848 849 Returns 850 ------- 851 float or np.nan 852 Minimum noise score, or np.nan if both sides are np.nan. 853 """ 854 if self._noise_score is None: 855 return np.nan 856 857 left, right = self._noise_score 858 # Handle NaN values - nanmin ignores NaN 859 return np.nanmin([left, right]) 860 861 @property 862 def noise_score_max(self): 863 """Maximum of left and right noise scores. 864 865 Returns 866 ------- 867 float or np.nan 868 Maximum noise score, or np.nan if both sides are np.nan. 869 """ 870 if self._noise_score is None: 871 return np.nan 872 873 left, right = self._noise_score 874 # Handle NaN values - nanmax ignores NaN 875 return np.nanmax([left, right]) 876 877 @property 878 def type(self): 879 """Type of the mass feature. 880 881 Returns 882 ------- 883 str 884 The type of mass feature ("untargeted", "targeted", or "internal standard"). 885 """ 886 return self._type 887 888 @type.setter 889 def type(self, value): 890 """Set the type of the mass feature. 891 892 Parameters 893 ---------- 894 value : str 895 The type of mass feature. Should be one of: "untargeted", "targeted", "internal standard". 896 """ 897 if not isinstance(value, str): 898 raise ValueError("The type of the mass feature must be a string") 899 self._type = value 900 901 @property 902 def best_ms2(self): 903 """Points to the best representative MS2 mass spectrum 904 905 Notes 906 ----- 907 If there is only one MS2 mass spectrum, it will be returned 908 If there are MS2 similarity results, this will return the MS2 mass spectrum with the highest entropy similarity score. 909 If there are no MS2 similarity results, the best MS2 mass spectrum is determined by the closest scan time to the apex of the mass feature, with higher resolving power. Checks for and disqualifies possible chimeric spectra. 910 911 Returns 912 ------- 913 MassSpectrum or None 914 The best MS2 mass spectrum. 915 """ 916 if len(self.ms2_similarity_results) > 0: 917 # the scan number with the highest similarity score 918 results_df = [x.to_dataframe() for x in self.ms2_similarity_results] 919 results_df = pd.concat(results_df) 920 results_df = results_df.sort_values( 921 by="entropy_similarity", ascending=False 922 ) 923 best_scan_number = results_df.iloc[0]["query_spectrum_id"] 924 return self.ms2_mass_spectra[best_scan_number] 925 926 ms2_scans = list(self.ms2_mass_spectra.keys()) 927 if len(ms2_scans) > 1: 928 mz_diff_list = [] # List of mz difference between mz of mass feature and mass of nearest mz in each scan 929 res_list = [] # List of maximum resolving power of peaks in each scan 930 time_diff_list = [] # List of time difference between scan and apex scan in each scan 931 for scan in ms2_scans: 932 if len(self.ms2_mass_spectra[scan].mspeaks) > 0: 933 # Find mz closest to mass feature mz, return both the difference in mass and its resolution 934 closest_mz = min( 935 self.ms2_mass_spectra[scan].mz_exp, 936 key=lambda x: abs(x - self.mz), 937 ) 938 if all( 939 np.isnan(self.ms2_mass_spectra[scan].resolving_power) 940 ): # All NA for resolving power in peaks, not uncommon in CID spectra 941 res_list.append(2) # Assumes very low resolving power 942 else: 943 res_list.append( 944 np.nanmax(self.ms2_mass_spectra[scan].resolving_power) 945 ) 946 mz_diff_list.append(np.abs(closest_mz - self.mz)) 947 time_diff_list.append( 948 np.abs( 949 self.chromatogram_parent.get_time_of_scan_id(scan) 950 - self.retention_time 951 ) 952 ) 953 else: 954 res_list.append(np.nan) 955 mz_diff_list.append(np.nan) 956 time_diff_list.append(np.nan) 957 # Convert diff_lists into logical scores (higher is better for each score) 958 time_score = 1 - np.array(time_diff_list) / np.nanmax( 959 np.array(time_diff_list) 960 ) 961 res_score = np.array(res_list) / np.nanmax(np.array(res_list)) 962 # mz_score is 0 for possible chimerics, 1 for all others (already within mass tolerance before assigning) 963 mz_score = np.zeros(len(ms2_scans)) 964 for i in np.arange(0, len(ms2_scans)): 965 if mz_diff_list[i] < 0.8 and mz_diff_list[i] > 0.1: # Possible chimeric 966 mz_score[i] = 0 967 else: 968 mz_score[i] = 1 969 # get the index of the best score and return the mass spectrum 970 if len([np.nanargmax(time_score * res_score * mz_score)]) == 1: 971 return self.ms2_mass_spectra[ 972 ms2_scans[np.nanargmax(time_score * res_score * mz_score)] 973 ] 974 # remove the mz_score condition and try again 975 elif len(np.argmax(time_score * res_score)) == 1: 976 return self.ms2_mass_spectra[ 977 ms2_scans[np.nanargmax(time_score * res_score)] 978 ] 979 else: 980 raise ValueError( 981 "No best MS2 mass spectrum could be found for mass feature " 982 + str(self.id) 983 ) 984 elif len(ms2_scans) == 1: # if only one ms2 spectra, return it 985 return self.ms2_mass_spectra[ms2_scans[0]] 986 else: # if no ms2 spectra, return None 987 return None
Class representing a mass feature in a liquid chromatography (LC) chromatogram.
Parameters
- lcms_parent (LCMS): The parent LCMSBase object.
- mz (float): The observed mass to charge ratio of the feature.
- retention_time (float): The retention time of the feature (in minutes), at the apex.
- intensity (float): The intensity of the feature.
- apex_scan (int): The scan number of the apex of the feature.
- persistence (float, optional): The persistence of the feature. Default is None.
Attributes
- _mz_exp (float): The observed mass to charge ratio of the feature.
- _mz_cal (float): The calibrated mass to charge ratio of the feature.
- _retention_time (float): The retention time of the feature (in minutes), at the apex.
- _apex_scan (int): The scan number of the apex of the feature.
- _intensity (float): The intensity of the feature.
- _persistence (float): The persistence of the feature.
- _eic_data (EIC_Data): The EIC data object associated with the feature.
- _eic_mz (float): The m/z value used to extract the EIC data, sometimes different from the observed m/z due to calibration, centroiding, or other processing.
- _dispersity_index (float): The dispersity index of the feature, in minutes.
- _normalized_dispersity_index (float): The normalized dispersity index of the feature (unitless, fraction of total window used to calculate dispersity index).
- _half_height_width (numpy.ndarray): The half height width of the feature (in minutes, as an array of min and max values).
- _tailing_factor (float): The tailing factor of the feature. > 1 indicates tailing, < 1 indicates fronting, = 1 indicates symmetrical peak.
- _noise_score (tuple): The noise score of the feature, as a tuple of (left, right) scores. Each score is a float, with higher values indicating better signal to noise.
- _gaussian_similarity (float): The Gaussian similarity of the feature, as a float between 0 and 1. 1 indicates a perfect Gaussian shape, 0 indicates a non-Gaussian shape.
- _ms_deconvoluted_idx ([int]): The indexes of the mass_spectrum attribute in the deconvoluted mass spectrum.
- _type (str): The type of mass feature. Default is "untargeted". Can be "untargeted", "targeted", or another customized type.
- is_calibrated (bool): If True, the feature has been calibrated. Default is False.
- monoisotopic_mf_id (int): Mass feature id that is the monoisotopic version of self. If self.id, then self is the monoisotopic feature). Default is None.
- isotopologue_type (str): The isotopic class of the feature, i.e. "13C1", "13C2", "13C1 37Cl1" etc. Default is None.
- ms2_scan_numbers (list): List of scan numbers of the MS2 spectra associated with the feature. Default is an empty list.
- ms2_mass_spectra (dict): Dictionary of MS2 spectra associated with the feature (key = scan number for DDA). Default is an empty dictionary.
- ms2_similarity_results (list): List of MS2 similarity results associated with the mass feature. Default is an empty list.
- id (int):
The ID of the feature, also the key in the parent LCMS object's
mass_featuresdictionary. - mass_spectrum_deconvoluted_parent (bool): If True, the mass feature corresponds to the most intense peak in the deconvoluted mass spectrum. Default is None.
- associated_mass_features_deconvoluted (list): List of mass features associated with the deconvoluted mass spectrum. Default is an empty list.
193 def __init__( 194 self, 195 lcms_parent, 196 mz: float, 197 retention_time: float, 198 intensity: float, 199 apex_scan: int, 200 persistence: float = None, 201 id: int = None 202 ): 203 super().__init__( 204 chromatogram_parent=lcms_parent, 205 mass_spectrum_obj=None, 206 start_index=None, 207 index=apex_scan, 208 final_index=None, 209 ) 210 # Core attributes, marked as private 211 self._mz_exp: float = mz 212 self._mz_cal: float = None 213 self._retention_time: float = retention_time 214 self._apex_scan: int = apex_scan 215 self._intensity: float = intensity 216 self._persistence: float = persistence 217 self._eic_data: EIC_Data = None 218 self._dispersity_index: float = None 219 self._normalized_dispersity_index: float = None 220 self._half_height_width: np.ndarray = None 221 self._ms_deconvoluted_idx = None 222 self._tailing_factor: float = None 223 self._noise_score: tuple = None 224 self._gaussian_similarity: float = None 225 self._type: str = "untargeted" 226 227 # Additional attributes 228 self.monoisotopic_mf_id = None 229 self.isotopologue_type = None 230 self.ms2_scan_numbers = [] 231 self.ms2_mass_spectra = {} 232 self.ms2_similarity_results = [] 233 self.mass_spectrum_deconvoluted_parent: bool = None 234 self.associated_mass_features_deconvoluted = [] 235 236 if id: 237 self.id = id 238 else: 239 # get the parent's mass feature keys and add 1 to the max value to get the new key 240 self.id = ( 241 max(lcms_parent.mass_features.keys()) + 1 242 if lcms_parent.mass_features.keys() 243 else 0 244 )
246 def update_mz(self): 247 """Update the mass to charge ratio from the mass spectrum object.""" 248 if self.mass_spectrum is None: 249 raise ValueError( 250 "The mass spectrum object is not set, cannot update the m/z from the MassSpectrum object" 251 ) 252 if len(self.mass_spectrum.mz_exp) == 0: 253 raise ValueError( 254 "The mass spectrum object has no m/z values, cannot update the m/z from the MassSpectrum object until it is processed" 255 ) 256 new_mz = self.ms1_peak.mz_exp 257 258 # calculate the difference between the new and old m/z, only update if it is close 259 mz_diff = new_mz - self.mz 260 if abs(mz_diff) < 0.01: 261 self._mz_exp = new_mz
Update the mass to charge ratio from the mass spectrum object.
592 def plot( 593 self, 594 to_plot=["EIC", "MS1", "MS2"], 595 return_fig=True, 596 plot_smoothed_eic=False, 597 plot_eic_datapoints=False, 598 molecular_metadata=None, 599 spectral_library=None, 600 ): 601 """Plot the mass feature. 602 603 Parameters 604 ---------- 605 to_plot : list, optional 606 List of strings specifying what to plot, any iteration of 607 "EIC", "MS2", "MS2_mirror", and "MS1". 608 Default is ["EIC", "MS1", "MS2"]. 609 return_fig : bool, optional 610 If True, the figure is returned. Default is True. 611 plot_smoothed_eic : bool, optional 612 If True, the smoothed EIC is plotted. Default is False. 613 plot_eic_datapoints : bool, optional 614 If True, the EIC data points are plotted. Default is False. 615 molecular_metadata : dict, optional 616 Dictionary mapping molecular IDs to MetaboliteMetadata objects. 617 Required if "MS2_mirror" is in to_plot. Default is None. 618 spectral_library : FlashEntropySearch, optional 619 FlashEntropy spectral library containing MS2 spectra. 620 Required if "MS2_mirror" is in to_plot. Default is None. 621 622 Returns 623 ------- 624 matplotlib.figure.Figure or None 625 The figure object if `return_fig` is True. 626 Otherwise None and the figure is displayed. 627 """ 628 # Adjust to_plot list if there are not spectra added to the mass features 629 if self.mass_spectrum is None: 630 to_plot = [x for x in to_plot if x != "MS1"] 631 if len(self.ms2_mass_spectra) == 0: 632 to_plot = [x for x in to_plot if x not in ["MS2", "MS2_mirror"]] 633 if self._eic_data is None: 634 to_plot = [x for x in to_plot if x != "EIC"] 635 636 # Check if MS2_mirror is requested without molecular_metadata 637 if "MS2_mirror" in to_plot and molecular_metadata is None: 638 raise ValueError("molecular_metadata is required when 'MS2_mirror' is in to_plot") 639 640 # Check if both MS2 and MS2_mirror are requested (not allowed) 641 if "MS2" in to_plot and "MS2_mirror" in to_plot: 642 # Remove regular MS2 if mirror is requested 643 to_plot = [x for x in to_plot if x != "MS2"] 644 645 deconvoluted = self._ms_deconvoluted_idx is not None 646 647 fig, axs = plt.subplots( 648 len(to_plot), 1, figsize=(9, len(to_plot) * 4), squeeze=False 649 ) 650 fig.suptitle( 651 f"Mass Feature {self.id}: m/z = {round(self.mz, ndigits=4)}; " 652 f"time = {round(self.retention_time, ndigits=1)} minutes" 653 ) 654 655 i = 0 656 # EIC plot 657 if "EIC" in to_plot: 658 self._plot_single_eic( 659 axs[i][0], 660 plot_smoothed=plot_smoothed_eic, 661 plot_datapoints=plot_eic_datapoints 662 ) 663 i += 1 664 665 # MS1 plot 666 if "MS1" in to_plot: 667 self._plot_ms1_spectrum(axs[i][0], deconvoluted=deconvoluted) 668 i += 1 669 670 # MS2 plot 671 if "MS2" in to_plot: 672 self._plot_ms2_spectrum(axs[i][0]) 673 i += 1 674 675 # MS2 mirror plot 676 if "MS2_mirror" in to_plot: 677 self._plot_ms2_mirror(axs[i][0], molecular_metadata=molecular_metadata, spectral_library=spectral_library) 678 i += 1 679 680 # Add space between subplots 681 plt.tight_layout() 682 683 if return_fig: 684 # Close figure 685 plt.close(fig) 686 return fig
Plot the mass feature.
Parameters
- to_plot (list, optional): List of strings specifying what to plot, any iteration of "EIC", "MS2", "MS2_mirror", and "MS1". Default is ["EIC", "MS1", "MS2"].
- return_fig (bool, optional): If True, the figure is returned. Default is True.
- plot_smoothed_eic (bool, optional): If True, the smoothed EIC is plotted. Default is False.
- plot_eic_datapoints (bool, optional): If True, the EIC data points are plotted. Default is False.
- molecular_metadata (dict, optional): Dictionary mapping molecular IDs to MetaboliteMetadata objects. Required if "MS2_mirror" is in to_plot. Default is None.
- spectral_library (FlashEntropySearch, optional): FlashEntropy spectral library containing MS2 spectra. Required if "MS2_mirror" is in to_plot. Default is None.
Returns
- matplotlib.figure.Figure or None: The figure object if
return_figis True. Otherwise None and the figure is displayed.
688 @property 689 def mz(self): 690 """Mass to charge ratio of the mass feature""" 691 # If the mass feature has been calibrated, return the calibrated m/z, otherwise return the measured m/z 692 if self._mz_cal is not None: 693 return self._mz_cal 694 else: 695 return self._mz_exp
Mass to charge ratio of the mass feature
697 @property 698 def mass_spectrum_deconvoluted(self): 699 """Returns the deconvoluted mass spectrum object associated with the mass feature, if deconvolution has been performed.""" 700 if self._ms_deconvoluted_idx is not None: 701 ms_deconvoluted = copy.deepcopy(self.mass_spectrum) 702 ms_deconvoluted.set_indexes(self._ms_deconvoluted_idx) 703 return ms_deconvoluted 704 else: 705 raise ValueError( 706 "Deconvolution has not been performed for mass feature " + str(self.id) 707 )
Returns the deconvoluted mass spectrum object associated with the mass feature, if deconvolution has been performed.
709 @property 710 def retention_time(self): 711 """Retention time of the mass feature""" 712 return self._retention_time
Retention time of the mass feature
721 @property 722 def apex_scan(self): 723 """Apex scan of the mass feature""" 724 return self._apex_scan
Apex scan of the mass feature
733 @property 734 def intensity(self): 735 """Intensity of the mass feature""" 736 return self._intensity
Intensity of the mass feature
745 @property 746 def persistence(self): 747 """Persistence of the mass feature""" 748 return self._persistence
Persistence of the mass feature
757 @property 758 def eic_rt_list(self): 759 """Retention time list between the beginning and end of the mass feature""" 760 # Find index of the start and final scans in the EIC data 761 start_index = self._eic_data.scans.tolist().index(self.start_scan) 762 final_index = self._eic_data.scans.tolist().index(self.final_scan) 763 764 # Get the retention time list 765 rt_list = self._eic_data.time[start_index : final_index + 1] 766 return rt_list
Retention time list between the beginning and end of the mass feature
768 @property 769 def eic_list(self): 770 """EIC List between the beginning and end of the mass feature""" 771 # Find index of the start and final scans in the EIC data 772 start_index = self._eic_data.scans.tolist().index(self.start_scan) 773 final_index = self._eic_data.scans.tolist().index(self.final_scan) 774 775 # Get the retention time list 776 eic = self._eic_data.eic[start_index : final_index + 1] 777 return eic
EIC List between the beginning and end of the mass feature
779 @property 780 def ms1_peak(self): 781 """MS1 peak from associated mass spectrum that is closest to the mass feature's m/z""" 782 # Find index array self.mass_spectrum.mz_exp that is closest to self.mz 783 closest_mz = min(self.mass_spectrum.mz_exp, key=lambda x: abs(x - self.mz)) 784 closest_mz_index = self.mass_spectrum.mz_exp.tolist().index(closest_mz) 785 786 return self.mass_spectrum._mspeaks[closest_mz_index]
MS1 peak from associated mass spectrum that is closest to the mass feature's m/z
788 @property 789 def tailing_factor(self): 790 """Tailing factor of the mass feature""" 791 return self._tailing_factor
Tailing factor of the mass feature
800 @property 801 def dispersity_index(self): 802 """Dispersity index of the mass feature""" 803 return self._dispersity_index
Dispersity index of the mass feature
812 @property 813 def normalized_dispersity_index(self): 814 """Normalized dispersity index of the mass feature, unitless (fraction of total window used)""" 815 return self._normalized_dispersity_index
Normalized dispersity index of the mass feature, unitless (fraction of total window used)
817 @property 818 def half_height_width(self): 819 """Half height width of the mass feature, average of min and max values, in minutes""" 820 return np.mean(self._half_height_width)
Half height width of the mass feature, average of min and max values, in minutes
822 @property 823 def noise_score(self): 824 """Mean of left and right noise scores. 825 826 Returns 827 ------- 828 float or np.nan 829 Mean noise score, or np.nan if both sides are np.nan. 830 """ 831 if self._noise_score is None: 832 return np.nan 833 834 left, right = self._noise_score 835 # Handle NaN values 836 if np.isnan(left) and np.isnan(right): 837 return np.nan 838 elif np.isnan(left): 839 return right 840 elif np.isnan(right): 841 return left 842 else: 843 return (left + right) / 2.0
Mean of left and right noise scores.
Returns
- float or np.nan: Mean noise score, or np.nan if both sides are np.nan.
845 @property 846 def noise_score_min(self): 847 """Minimum of left and right noise scores. 848 849 Returns 850 ------- 851 float or np.nan 852 Minimum noise score, or np.nan if both sides are np.nan. 853 """ 854 if self._noise_score is None: 855 return np.nan 856 857 left, right = self._noise_score 858 # Handle NaN values - nanmin ignores NaN 859 return np.nanmin([left, right])
Minimum of left and right noise scores.
Returns
- float or np.nan: Minimum noise score, or np.nan if both sides are np.nan.
861 @property 862 def noise_score_max(self): 863 """Maximum of left and right noise scores. 864 865 Returns 866 ------- 867 float or np.nan 868 Maximum noise score, or np.nan if both sides are np.nan. 869 """ 870 if self._noise_score is None: 871 return np.nan 872 873 left, right = self._noise_score 874 # Handle NaN values - nanmax ignores NaN 875 return np.nanmax([left, right])
Maximum of left and right noise scores.
Returns
- float or np.nan: Maximum noise score, or np.nan if both sides are np.nan.
877 @property 878 def type(self): 879 """Type of the mass feature. 880 881 Returns 882 ------- 883 str 884 The type of mass feature ("untargeted", "targeted", or "internal standard"). 885 """ 886 return self._type
Type of the mass feature.
Returns
- str: The type of mass feature ("untargeted", "targeted", or "internal standard").
901 @property 902 def best_ms2(self): 903 """Points to the best representative MS2 mass spectrum 904 905 Notes 906 ----- 907 If there is only one MS2 mass spectrum, it will be returned 908 If there are MS2 similarity results, this will return the MS2 mass spectrum with the highest entropy similarity score. 909 If there are no MS2 similarity results, the best MS2 mass spectrum is determined by the closest scan time to the apex of the mass feature, with higher resolving power. Checks for and disqualifies possible chimeric spectra. 910 911 Returns 912 ------- 913 MassSpectrum or None 914 The best MS2 mass spectrum. 915 """ 916 if len(self.ms2_similarity_results) > 0: 917 # the scan number with the highest similarity score 918 results_df = [x.to_dataframe() for x in self.ms2_similarity_results] 919 results_df = pd.concat(results_df) 920 results_df = results_df.sort_values( 921 by="entropy_similarity", ascending=False 922 ) 923 best_scan_number = results_df.iloc[0]["query_spectrum_id"] 924 return self.ms2_mass_spectra[best_scan_number] 925 926 ms2_scans = list(self.ms2_mass_spectra.keys()) 927 if len(ms2_scans) > 1: 928 mz_diff_list = [] # List of mz difference between mz of mass feature and mass of nearest mz in each scan 929 res_list = [] # List of maximum resolving power of peaks in each scan 930 time_diff_list = [] # List of time difference between scan and apex scan in each scan 931 for scan in ms2_scans: 932 if len(self.ms2_mass_spectra[scan].mspeaks) > 0: 933 # Find mz closest to mass feature mz, return both the difference in mass and its resolution 934 closest_mz = min( 935 self.ms2_mass_spectra[scan].mz_exp, 936 key=lambda x: abs(x - self.mz), 937 ) 938 if all( 939 np.isnan(self.ms2_mass_spectra[scan].resolving_power) 940 ): # All NA for resolving power in peaks, not uncommon in CID spectra 941 res_list.append(2) # Assumes very low resolving power 942 else: 943 res_list.append( 944 np.nanmax(self.ms2_mass_spectra[scan].resolving_power) 945 ) 946 mz_diff_list.append(np.abs(closest_mz - self.mz)) 947 time_diff_list.append( 948 np.abs( 949 self.chromatogram_parent.get_time_of_scan_id(scan) 950 - self.retention_time 951 ) 952 ) 953 else: 954 res_list.append(np.nan) 955 mz_diff_list.append(np.nan) 956 time_diff_list.append(np.nan) 957 # Convert diff_lists into logical scores (higher is better for each score) 958 time_score = 1 - np.array(time_diff_list) / np.nanmax( 959 np.array(time_diff_list) 960 ) 961 res_score = np.array(res_list) / np.nanmax(np.array(res_list)) 962 # mz_score is 0 for possible chimerics, 1 for all others (already within mass tolerance before assigning) 963 mz_score = np.zeros(len(ms2_scans)) 964 for i in np.arange(0, len(ms2_scans)): 965 if mz_diff_list[i] < 0.8 and mz_diff_list[i] > 0.1: # Possible chimeric 966 mz_score[i] = 0 967 else: 968 mz_score[i] = 1 969 # get the index of the best score and return the mass spectrum 970 if len([np.nanargmax(time_score * res_score * mz_score)]) == 1: 971 return self.ms2_mass_spectra[ 972 ms2_scans[np.nanargmax(time_score * res_score * mz_score)] 973 ] 974 # remove the mz_score condition and try again 975 elif len(np.argmax(time_score * res_score)) == 1: 976 return self.ms2_mass_spectra[ 977 ms2_scans[np.nanargmax(time_score * res_score)] 978 ] 979 else: 980 raise ValueError( 981 "No best MS2 mass spectrum could be found for mass feature " 982 + str(self.id) 983 ) 984 elif len(ms2_scans) == 1: # if only one ms2 spectra, return it 985 return self.ms2_mass_spectra[ms2_scans[0]] 986 else: # if no ms2 spectra, return None 987 return None
Points to the best representative MS2 mass spectrum
Notes
If there is only one MS2 mass spectrum, it will be returned If there are MS2 similarity results, this will return the MS2 mass spectrum with the highest entropy similarity score. If there are no MS2 similarity results, the best MS2 mass spectrum is determined by the closest scan time to the apex of the mass feature, with higher resolving power. Checks for and disqualifies possible chimeric spectra.
Returns
- MassSpectrum or None: The best MS2 mass spectrum.
Inherited Members
990class GCPeak(ChromaPeakBase, GCPeakCalculation): 991 """Class representing a peak in a gas chromatography (GC) chromatogram. 992 993 Parameters 994 ---------- 995 chromatogram_parent : Chromatogram 996 The parent chromatogram object. 997 mass_spectrum_obj : MassSpectrum 998 The mass spectrum object associated with the peak. 999 indexes : tuple 1000 The indexes of the peak in the chromatogram. 1001 1002 Attributes 1003 ---------- 1004 _compounds : list 1005 List of compounds associated with the peak. 1006 _ri : float or None 1007 Retention index of the peak. 1008 1009 Methods 1010 ------- 1011 * __len__(). Returns the number of compounds associated with the peak. 1012 * __getitem__(position). Returns the compound at the specified position. 1013 * remove_compound(compounds_obj). Removes the specified compound from the peak. 1014 * clear_compounds(). Removes all compounds from the peak. 1015 * add_compound(compounds_dict, spectral_similarity_scores, ri_score=None, similarity_score=None). Adds a compound to the peak with the specified attributes. 1016 * ri(). Returns the retention index of the peak. 1017 * highest_ss_compound(). Returns the compound with the highest spectral similarity score. 1018 * highest_score_compound(). Returns the compound with the highest similarity score. 1019 * compound_names(). Returns a list of names of compounds associated with the peak. 1020 """ 1021 1022 def __init__(self, chromatogram_parent, mass_spectrum_obj, indexes): 1023 self._compounds = [] 1024 self._ri = None 1025 super().__init__(chromatogram_parent, mass_spectrum_obj, *indexes) 1026 1027 def __len__(self): 1028 return len(self._compounds) 1029 1030 def __getitem__(self, position): 1031 return self._compounds[position] 1032 1033 def remove_compound(self, compounds_obj): 1034 self._compounds.remove(compounds_obj) 1035 1036 def clear_compounds(self): 1037 self._compounds = [] 1038 1039 def add_compound( 1040 self, 1041 compounds_dict, 1042 spectral_similarity_scores, 1043 ri_score=None, 1044 similarity_score=None, 1045 ): 1046 """Adds a compound to the peak with the specified attributes. 1047 1048 Parameters 1049 ---------- 1050 compounds_dict : dict 1051 Dictionary containing the compound information. 1052 spectral_similarity_scores : dict 1053 Dictionary containing the spectral similarity scores. 1054 ri_score : float or None, optional 1055 The retention index score of the compound. Default is None. 1056 similarity_score : float or None, optional 1057 The similarity score of the compound. Default is None. 1058 """ 1059 compound_obj = LowResCompoundRef(compounds_dict) 1060 compound_obj.spectral_similarity_scores = spectral_similarity_scores 1061 compound_obj.spectral_similarity_score = spectral_similarity_scores.get( 1062 "cosine_correlation" 1063 ) 1064 # TODO check is the above line correct? 1065 compound_obj.ri_score = ri_score 1066 compound_obj.similarity_score = similarity_score 1067 self._compounds.append(compound_obj) 1068 if similarity_score: 1069 self._compounds.sort(key=lambda c: c.similarity_score, reverse=True) 1070 else: 1071 self._compounds.sort( 1072 key=lambda c: c.spectral_similarity_score, reverse=True 1073 ) 1074 1075 @property 1076 def ri(self): 1077 """Returns the retention index of the peak. 1078 1079 Returns 1080 ------- 1081 float or None 1082 The retention index of the peak. 1083 """ 1084 return self._ri 1085 1086 @property 1087 def highest_ss_compound(self): 1088 """Returns the compound with the highest spectral similarity score. 1089 1090 Returns 1091 ------- 1092 LowResCompoundRef or None 1093 The compound with the highest spectral similarity score. 1094 """ 1095 if self: 1096 return max(self, key=lambda c: c.spectral_similarity_score) 1097 else: 1098 return None 1099 1100 @property 1101 def highest_score_compound(self): 1102 """Returns the compound with the highest similarity score. 1103 1104 Returns 1105 ------- 1106 LowResCompoundRef or None 1107 The compound with the highest similarity score. 1108 """ 1109 if self: 1110 return max(self, key=lambda c: c.similarity_score) 1111 else: 1112 return None 1113 1114 @property 1115 def compound_names(self): 1116 """Returns a list of names of compounds associated with the peak. 1117 1118 Returns 1119 ------- 1120 list 1121 List of names of compounds associated with the peak. 1122 """ 1123 if self: 1124 return [c.name for c in self] 1125 else: 1126 return []
Class representing a peak in a gas chromatography (GC) chromatogram.
Parameters
- chromatogram_parent (Chromatogram): The parent chromatogram object.
- mass_spectrum_obj (MassSpectrum): The mass spectrum object associated with the peak.
- indexes (tuple): The indexes of the peak in the chromatogram.
Attributes
- _compounds (list): List of compounds associated with the peak.
- _ri (float or None): Retention index of the peak.
Methods
- __len__(). Returns the number of compounds associated with the peak.
- __getitem__(position). Returns the compound at the specified position.
- remove_compound(compounds_obj). Removes the specified compound from the peak.
- clear_compounds(). Removes all compounds from the peak.
- add_compound(compounds_dict, spectral_similarity_scores, ri_score=None, similarity_score=None). Adds a compound to the peak with the specified attributes.
- ri(). Returns the retention index of the peak.
- highest_ss_compound(). Returns the compound with the highest spectral similarity score.
- highest_score_compound(). Returns the compound with the highest similarity score.
- compound_names(). Returns a list of names of compounds associated with the peak.
1039 def add_compound( 1040 self, 1041 compounds_dict, 1042 spectral_similarity_scores, 1043 ri_score=None, 1044 similarity_score=None, 1045 ): 1046 """Adds a compound to the peak with the specified attributes. 1047 1048 Parameters 1049 ---------- 1050 compounds_dict : dict 1051 Dictionary containing the compound information. 1052 spectral_similarity_scores : dict 1053 Dictionary containing the spectral similarity scores. 1054 ri_score : float or None, optional 1055 The retention index score of the compound. Default is None. 1056 similarity_score : float or None, optional 1057 The similarity score of the compound. Default is None. 1058 """ 1059 compound_obj = LowResCompoundRef(compounds_dict) 1060 compound_obj.spectral_similarity_scores = spectral_similarity_scores 1061 compound_obj.spectral_similarity_score = spectral_similarity_scores.get( 1062 "cosine_correlation" 1063 ) 1064 # TODO check is the above line correct? 1065 compound_obj.ri_score = ri_score 1066 compound_obj.similarity_score = similarity_score 1067 self._compounds.append(compound_obj) 1068 if similarity_score: 1069 self._compounds.sort(key=lambda c: c.similarity_score, reverse=True) 1070 else: 1071 self._compounds.sort( 1072 key=lambda c: c.spectral_similarity_score, reverse=True 1073 )
Adds a compound to the peak with the specified attributes.
Parameters
- compounds_dict (dict): Dictionary containing the compound information.
- spectral_similarity_scores (dict): Dictionary containing the spectral similarity scores.
- ri_score (float or None, optional): The retention index score of the compound. Default is None.
- similarity_score (float or None, optional): The similarity score of the compound. Default is None.
1075 @property 1076 def ri(self): 1077 """Returns the retention index of the peak. 1078 1079 Returns 1080 ------- 1081 float or None 1082 The retention index of the peak. 1083 """ 1084 return self._ri
Returns the retention index of the peak.
Returns
- float or None: The retention index of the peak.
1086 @property 1087 def highest_ss_compound(self): 1088 """Returns the compound with the highest spectral similarity score. 1089 1090 Returns 1091 ------- 1092 LowResCompoundRef or None 1093 The compound with the highest spectral similarity score. 1094 """ 1095 if self: 1096 return max(self, key=lambda c: c.spectral_similarity_score) 1097 else: 1098 return None
Returns the compound with the highest spectral similarity score.
Returns
- LowResCompoundRef or None: The compound with the highest spectral similarity score.
1100 @property 1101 def highest_score_compound(self): 1102 """Returns the compound with the highest similarity score. 1103 1104 Returns 1105 ------- 1106 LowResCompoundRef or None 1107 The compound with the highest similarity score. 1108 """ 1109 if self: 1110 return max(self, key=lambda c: c.similarity_score) 1111 else: 1112 return None
Returns the compound with the highest similarity score.
Returns
- LowResCompoundRef or None: The compound with the highest similarity score.
1114 @property 1115 def compound_names(self): 1116 """Returns a list of names of compounds associated with the peak. 1117 1118 Returns 1119 ------- 1120 list 1121 List of names of compounds associated with the peak. 1122 """ 1123 if self: 1124 return [c.name for c in self] 1125 else: 1126 return []
Returns a list of names of compounds associated with the peak.
Returns
- list: List of names of compounds associated with the peak.
1129class GCPeakDeconvolved(GCPeak): 1130 """Represents a deconvolved peak in a chromatogram. 1131 1132 Parameters 1133 ---------- 1134 chromatogram_parent : Chromatogram 1135 The parent chromatogram object. 1136 mass_spectra : list 1137 List of mass spectra associated with the peak. 1138 apex_index : int 1139 Index of the apex mass spectrum in the `mass_spectra` list. 1140 rt_list : list 1141 List of retention times. 1142 tic_list : list 1143 List of total ion currents. 1144 """ 1145 1146 def __init__( 1147 self, chromatogram_parent, mass_spectra, apex_index, rt_list, tic_list 1148 ): 1149 self._ri = None 1150 self._rt_list = list(rt_list) 1151 self._tic_list = list(tic_list) 1152 self.mass_spectra = list(mass_spectra) 1153 super().__init__( 1154 chromatogram_parent, 1155 self.mass_spectra[apex_index], 1156 (0, apex_index, len(self.mass_spectra) - 1), 1157 ) 1158 1159 @property 1160 def rt_list(self): 1161 """Get the list of retention times. 1162 1163 Returns 1164 ------- 1165 list 1166 The list of retention times. 1167 """ 1168 return self._rt_list 1169 1170 @property 1171 def tic_list(self): 1172 """Get the list of total ion currents. 1173 1174 Returns 1175 ------- 1176 list 1177 The list of total ion currents. 1178 """ 1179 return self._tic_list
Represents a deconvolved peak in a chromatogram.
Parameters
- chromatogram_parent (Chromatogram): The parent chromatogram object.
- mass_spectra (list): List of mass spectra associated with the peak.
- apex_index (int):
Index of the apex mass spectrum in the
mass_spectralist. - rt_list (list): List of retention times.
- tic_list (list): List of total ion currents.
1146 def __init__( 1147 self, chromatogram_parent, mass_spectra, apex_index, rt_list, tic_list 1148 ): 1149 self._ri = None 1150 self._rt_list = list(rt_list) 1151 self._tic_list = list(tic_list) 1152 self.mass_spectra = list(mass_spectra) 1153 super().__init__( 1154 chromatogram_parent, 1155 self.mass_spectra[apex_index], 1156 (0, apex_index, len(self.mass_spectra) - 1), 1157 )
1159 @property 1160 def rt_list(self): 1161 """Get the list of retention times. 1162 1163 Returns 1164 ------- 1165 list 1166 The list of retention times. 1167 """ 1168 return self._rt_list
Get the list of retention times.
Returns
- list: The list of retention times.
1170 @property 1171 def tic_list(self): 1172 """Get the list of total ion currents. 1173 1174 Returns 1175 ------- 1176 list 1177 The list of total ion currents. 1178 """ 1179 return self._tic_list
Get the list of total ion currents.
Returns
- list: The list of total ion currents.