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