corems.mass_spectra.output.export
1__author__ = "Yuri E. Corilo" 2__date__ = "Dec 14, 2010" 3 4 5import csv 6import json 7import re 8import uuid 9import warnings 10from datetime import datetime, timezone 11from pathlib import Path 12 13import h5py 14import numpy as np 15import pandas as pd 16from openpyxl import load_workbook 17from pandas import DataFrame, ExcelWriter, read_excel 18 19from corems import __version__, corems_md5 20from corems.encapsulation.output import parameter_to_dict 21from corems.encapsulation.output.parameter_to_json import ( 22 dump_lcms_settings_json, 23 dump_lcms_settings_toml, 24 dump_lcms_collection_settings_json, 25 dump_lcms_collection_settings_toml, 26) 27from corems.mass_spectrum.output.export import HighResMassSpecExport 28from corems.molecular_formula.factory.MolecularFormulaFactory import MolecularFormula 29from corems.molecular_id.calc.SpectralSimilarity import methods_name 30 31ion_type_dict = { 32 # adduct : [atoms to add, atoms to subtract when calculating formula of ion 33 "M+": [{}, {}], 34 "[M]+": [{}, {}], 35 "protonated": [{"H": 1}, {}], 36 "[M+H]+": [{"H": 1}, {}], 37 "[M+NH4]+": [{"N": 1, "H": 4}, {}], # ammonium 38 "[M+Na]+": [{"Na": 1}, {}], 39 "[M+K]+": [{"K": 1}, {}], 40 "[M+2Na+Cl]+": [{"Na": 2, "Cl": 1}, {}], 41 "[M+2Na-H]+": [{"Na": 2}, {"H": 1}], 42 "[M+C2H3Na2O2]+": [{"C": 2, "H": 3, "Na": 2, "O": 2}, {}], 43 "[M+C4H10N3]+": [{"C": 4, "H": 10, "N": 3}, {}], 44 "[M+NH4+ACN]+": [{"C": 2, "H": 7, "N": 2}, {}], 45 "[M+H-H2O]+": [{}, {"H": 1, "O": 1}], 46 "de-protonated": [{}, {"H": 1}], 47 "[M-H]-": [{}, {"H": 1}], 48 "[M+Cl]-": [{"Cl": 1}, {}], 49 "[M+HCOO]-": [{"C": 1, "H": 1, "O": 2}, {}], # formate 50 "[M+CH3COO]-": [{"C": 2, "H": 3, "O": 2}, {}], # acetate 51 "[M+2NaAc+Cl]-": [{"Na": 2, "C": 2, "H": 3, "O": 2, "Cl": 1}, {}], 52 "[M+K-2H]-": [{"K": 1}, {"H": 2}], 53 "[M+Na-2H]-": [{"Na": 1}, {"H": 2}], 54} 55 56 57class LowResGCMSExport: 58 """A class to export low resolution GC-MS data. 59 60 This class provides methods to export low resolution GC-MS data to various formats such as Excel, CSV, HDF5, and Pandas DataFrame. 61 62 Parameters: 63 ---------- 64 out_file_path : str 65 The output file path. 66 gcms : object 67 The low resolution GCMS object. 68 69 Attributes: 70 ---------- 71 output_file : Path 72 The output file path as a Path object. 73 gcms : object 74 The low resolution GCMS object. 75 76 Methods: 77 ------- 78 * get_pandas_df(id_label="corems:"). Get the exported data as a Pandas DataFrame. 79 * get_json(nan=False, id_label="corems:"). Get the exported data as a JSON string. 80 * to_pandas(write_metadata=True, id_label="corems:"). Export the data to a Pandas DataFrame and save it as a pickle file. 81 * to_excel(write_mode='a', write_metadata=True, id_label="corems:"), 82 Export the data to an Excel file. 83 * to_csv(separate_output=False, write_mode="w", write_metadata=True, id_label="corems:"). 84 Export the data to a CSV file. 85 * to_hdf(id_label="corems:"). 86 Export the data to an HDF5 file. 87 * get_data_stats(gcms). 88 Get statistics about the GCMS data. 89 90 """ 91 92 def __init__(self, out_file_path, gcms): 93 self.output_file = Path(out_file_path) 94 95 self.gcms = gcms 96 97 self._init_columns() 98 99 def _init_columns(self): 100 """Initialize the column names for the exported data. 101 102 Returns: 103 ------- 104 list 105 The list of column names. 106 """ 107 108 columns = [ 109 "Sample name", 110 "Peak Index", 111 "Retention Time", 112 "Retention Time Ref", 113 "Peak Height", 114 "Peak Area", 115 "Retention index", 116 "Retention index Ref", 117 "Retention Index Score", 118 "Similarity Score", 119 "Spectral Similarity Score", 120 "Compound Name", 121 "Chebi ID", 122 "Kegg Compound ID", 123 "Inchi", 124 "Inchi Key", 125 "Smiles", 126 "Molecular Formula", 127 "IUPAC Name", 128 "Traditional Name", 129 "Common Name", 130 "Derivatization", 131 ] 132 133 if self.gcms.molecular_search_settings.exploratory_mode: 134 columns.extend( 135 [ 136 "Weighted Cosine Correlation", 137 "Cosine Correlation", 138 "Stein Scott Similarity", 139 "Pearson Correlation", 140 "Spearman Correlation", 141 "Kendall Tau Correlation", 142 "Euclidean Distance", 143 "Manhattan Distance", 144 "Jaccard Distance", 145 "DWT Correlation", 146 "DFT Correlation", 147 ] 148 ) 149 150 columns.extend(list(methods_name.values())) 151 152 return columns 153 154 def get_pandas_df(self, id_label="corems:"): 155 """Get the exported data as a Pandas DataFrame. 156 157 Parameters: 158 ---------- 159 id_label : str, optional 160 The ID label for the data. Default is "corems:". 161 162 Returns: 163 ------- 164 DataFrame 165 The exported data as a Pandas DataFrame. 166 """ 167 168 columns = self._init_columns() 169 170 dict_data_list = self.get_list_dict_data(self.gcms) 171 172 df = DataFrame(dict_data_list, columns=columns) 173 174 df.name = self.gcms.sample_name 175 176 return df 177 178 def get_json(self, nan=False, id_label="corems:"): 179 """Get the exported data as a JSON string. 180 181 Parameters: 182 ---------- 183 nan : bool, optional 184 Whether to include NaN values in the JSON string. Default is False. 185 id_label : str, optional 186 The ID label for the data. Default is "corems:". 187 188 """ 189 190 import json 191 192 dict_data_list = self.get_list_dict_data(self.gcms) 193 194 return json.dumps( 195 dict_data_list, sort_keys=False, indent=4, separators=(",", ": ") 196 ) 197 198 def to_pandas(self, write_metadata=True, id_label="corems:"): 199 """Export the data to a Pandas DataFrame and save it as a pickle file. 200 201 Parameters: 202 ---------- 203 write_metadata : bool, optional 204 Whether to write metadata to the output file. 205 id_label : str, optional 206 The ID label for the data. 207 """ 208 209 columns = self._init_columns() 210 211 dict_data_list = self.get_list_dict_data(self.gcms) 212 213 df = DataFrame(dict_data_list, columns=columns) 214 215 df.to_pickle(self.output_file.with_suffix(".pkl")) 216 217 if write_metadata: 218 self.write_settings( 219 self.output_file.with_suffix(".pkl"), self.gcms, id_label="corems:" 220 ) 221 222 def to_excel(self, write_mode="a", write_metadata=True, id_label="corems:"): 223 """Export the data to an Excel file. 224 225 Parameters: 226 ---------- 227 write_mode : str, optional 228 The write mode for the Excel file. Default is 'a' (append). 229 write_metadata : bool, optional 230 Whether to write metadata to the output file. Default is True. 231 id_label : str, optional 232 The ID label for the data. Default is "corems:". 233 """ 234 235 out_put_path = self.output_file.with_suffix(".xlsx") 236 237 columns = self._init_columns() 238 239 dict_data_list = self.get_list_dict_data(self.gcms) 240 241 df = DataFrame(dict_data_list, columns=columns) 242 243 if write_mode == "a" and out_put_path.exists(): 244 writer = ExcelWriter(out_put_path, engine="openpyxl") 245 # try to open an existing workbook 246 writer.book = load_workbook(out_put_path) 247 # copy existing sheets 248 writer.sheets = dict((ws.title, ws) for ws in writer.book.worksheets) 249 # read existing file 250 reader = read_excel(out_put_path) 251 # write out the new sheet 252 df.to_excel(writer, index=False, header=False, startrow=len(reader) + 1) 253 254 writer.close() 255 else: 256 df.to_excel( 257 self.output_file.with_suffix(".xlsx"), index=False, engine="openpyxl" 258 ) 259 260 if write_metadata: 261 self.write_settings(out_put_path, self.gcms, id_label=id_label) 262 263 def to_csv( 264 self, 265 separate_output=False, 266 write_mode="w", 267 write_metadata=True, 268 id_label="corems:", 269 ): 270 """Export the data to a CSV file. 271 272 Parameters: 273 ---------- 274 separate_output : bool, optional 275 Whether to separate the output into multiple files. Default is False. 276 write_mode : str, optional 277 The write mode for the CSV file. Default is 'w' (write). 278 write_metadata : bool, optional 279 Whether to write metadata to the output file. Default is True. 280 id_label : str, optional 281 The ID label for the data. Default is "corems:". 282 """ 283 284 if separate_output: 285 # set write mode to write 286 # this mode will overwrite the file without warning 287 write_mode = "w" 288 else: 289 # set write mode to append 290 write_mode = "a" 291 292 columns = self._init_columns() 293 294 dict_data_list = self.get_list_dict_data(self.gcms) 295 296 out_put_path = self.output_file.with_suffix(".csv") 297 298 write_header = not out_put_path.exists() 299 300 try: 301 with open(out_put_path, write_mode, newline="") as csvfile: 302 writer = csv.DictWriter(csvfile, fieldnames=columns) 303 if write_header: 304 writer.writeheader() 305 for data in dict_data_list: 306 writer.writerow(data) 307 308 if write_metadata: 309 self.write_settings(out_put_path, self.gcms, id_label=id_label) 310 311 except IOError as ioerror: 312 print(ioerror) 313 314 def to_hdf(self, id_label="corems:"): 315 """Export the data to an HDF5 file. 316 317 Parameters: 318 ---------- 319 id_label : str, optional 320 The ID label for the data. Default is "corems:". 321 """ 322 323 # save sample at a time 324 def add_compound(gc_peak, compound_obj): 325 modifier = compound_obj.classify if compound_obj.classify else "" 326 compound_group = compound_obj.name.replace("/", "") + " " + modifier 327 328 if compound_group not in peak_group: 329 compound_group = peak_group.create_group(compound_group) 330 331 # compound_group.attrs["retention_time"] = compound_obj.retention_time 332 compound_group.attrs["retention_index"] = compound_obj.ri 333 compound_group.attrs["retention_index_score"] = compound_obj.ri_score 334 compound_group.attrs["spectral_similarity_score"] = ( 335 compound_obj.spectral_similarity_score 336 ) 337 compound_group.attrs["similarity_score"] = compound_obj.similarity_score 338 339 compond_mz = compound_group.create_dataset( 340 "mz", data=np.array(compound_obj.mz), dtype="f8" 341 ) 342 compond_abundance = compound_group.create_dataset( 343 "abundance", data=np.array(compound_obj.abundance), dtype="f8" 344 ) 345 346 if self.gcms.molecular_search_settings.exploratory_mode: 347 compound_group.attrs["Spectral Similarities"] = json.dumps( 348 compound_obj.spectral_similarity_scores, 349 sort_keys=False, 350 indent=4, 351 separators=(",", ":"), 352 ) 353 else: 354 warnings.warn("Skipping duplicate reference compound.") 355 356 import json 357 from datetime import datetime, timezone 358 359 import h5py 360 import numpy as np 361 362 output_path = self.output_file.with_suffix(".hdf5") 363 364 with h5py.File(output_path, "w") as hdf_handle: 365 timenow = str(datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M:%S %Z")) 366 hdf_handle.attrs["time_stamp"] = timenow 367 hdf_handle.attrs["data_structure"] = "gcms" 368 hdf_handle.attrs["analyzer"] = self.gcms.analyzer 369 hdf_handle.attrs["instrument_label"] = self.gcms.instrument_label 370 371 hdf_handle.attrs["sample_id"] = "self.gcms.id" 372 hdf_handle.attrs["sample_name"] = self.gcms.sample_name 373 hdf_handle.attrs["input_data"] = str(self.gcms.file_location) 374 hdf_handle.attrs["output_data"] = str(output_path) 375 hdf_handle.attrs["output_data_id"] = id_label + uuid.uuid4().hex 376 hdf_handle.attrs["corems_version"] = __version__ 377 378 hdf_handle.attrs["Stats"] = json.dumps( 379 self.get_data_stats(self.gcms), 380 sort_keys=False, 381 indent=4, 382 separators=(",", ": "), 383 ) 384 hdf_handle.attrs["Calibration"] = json.dumps( 385 self.get_calibration_stats(self.gcms, id_label), 386 sort_keys=False, 387 indent=4, 388 separators=(",", ": "), 389 ) 390 hdf_handle.attrs["Blank"] = json.dumps( 391 self.get_blank_stats(self.gcms), 392 sort_keys=False, 393 indent=4, 394 separators=(",", ": "), 395 ) 396 397 corems_dict_setting = parameter_to_dict.get_dict_data_gcms(self.gcms) 398 hdf_handle.attrs["CoreMSParameters"] = json.dumps( 399 corems_dict_setting, sort_keys=False, indent=4, separators=(",", ": ") 400 ) 401 402 scans_dataset = hdf_handle.create_dataset( 403 "scans", data=np.array(self.gcms.scans_number), dtype="f8" 404 ) 405 rt_dataset = hdf_handle.create_dataset( 406 "rt", data=np.array(self.gcms.retention_time), dtype="f8" 407 ) 408 tic_dataset = hdf_handle.create_dataset( 409 "tic", data=np.array(self.gcms.tic), dtype="f8" 410 ) 411 processed_tic_dataset = hdf_handle.create_dataset( 412 "processed_tic", data=np.array(self.gcms.processed_tic), dtype="f8" 413 ) 414 415 output_score_method = ( 416 self.gcms.molecular_search_settings.output_score_method 417 ) 418 419 for gc_peak in self.gcms: 420 # print(gc_peak.retention_time) 421 # print(gc_peak.tic) 422 423 # check if there is a compound candidate 424 peak_group = hdf_handle.create_group(str(gc_peak.retention_time)) 425 peak_group.attrs["deconvolution"] = int( 426 self.gcms.chromatogram_settings.use_deconvolution 427 ) 428 429 peak_group.attrs["start_scan"] = gc_peak.start_scan 430 peak_group.attrs["apex_scan"] = gc_peak.apex_scan 431 peak_group.attrs["final_scan"] = gc_peak.final_scan 432 433 peak_group.attrs["retention_index"] = gc_peak.ri 434 peak_group.attrs["retention_time"] = gc_peak.retention_time 435 peak_group.attrs["area"] = gc_peak.area 436 437 mz = peak_group.create_dataset( 438 "mz", data=np.array(gc_peak.mass_spectrum.mz_exp), dtype="f8" 439 ) 440 abundance = peak_group.create_dataset( 441 "abundance", 442 data=np.array(gc_peak.mass_spectrum.abundance), 443 dtype="f8", 444 ) 445 446 if gc_peak: 447 if output_score_method == "highest_sim_score": 448 compound_obj = gc_peak.highest_score_compound 449 add_compound(gc_peak, compound_obj) 450 451 elif output_score_method == "highest_ss": 452 compound_obj = gc_peak.highest_ss_compound 453 add_compound(gc_peak, compound_obj) 454 455 else: 456 for compound_obj in gc_peak: 457 add_compound(gc_peak, compound_obj) 458 459 def get_data_stats(self, gcms): 460 """Get statistics about the GCMS data. 461 462 Parameters: 463 ---------- 464 gcms : object 465 The low resolution GCMS object. 466 467 Returns: 468 ------- 469 dict 470 A dictionary containing the data statistics. 471 """ 472 473 matched_peaks = gcms.matched_peaks 474 no_matched_peaks = gcms.no_matched_peaks 475 unique_metabolites = gcms.unique_metabolites 476 477 peak_matchs_above_0p85 = 0 478 unique_peak_match_above_0p85 = 0 479 for match_peak in matched_peaks: 480 gc_peak_above_85 = 0 481 matches_above_85 = list( 482 filter(lambda m: m.similarity_score >= 0.85, match_peak) 483 ) 484 if matches_above_85: 485 peak_matchs_above_0p85 += 1 486 if len(matches_above_85) == 1: 487 unique_peak_match_above_0p85 += 1 488 489 data_stats = {} 490 data_stats["average_signal_noise"] = "ni" 491 data_stats["chromatogram_dynamic_range"] = gcms.dynamic_range 492 data_stats["total_number_peaks"] = len(gcms) 493 data_stats["total_peaks_matched"] = len(matched_peaks) 494 data_stats["total_peaks_without_matches"] = len(no_matched_peaks) 495 data_stats["total_matches_above_similarity_score_0.85"] = peak_matchs_above_0p85 496 data_stats["single_matches_above_similarity_score_0.85"] = ( 497 unique_peak_match_above_0p85 498 ) 499 data_stats["unique_metabolites"] = len(unique_metabolites) 500 501 return data_stats 502 503 def get_calibration_stats(self, gcms, id_label): 504 """Get statistics about the GC-MS calibration. 505 506 Parameters: 507 ---------- 508 """ 509 calibration_parameters = {} 510 511 calibration_parameters["calibration_rt_ri_pairs_ref"] = gcms.ri_pairs_ref 512 calibration_parameters["data_url"] = str(gcms.cal_file_path) 513 calibration_parameters["has_input"] = id_label + corems_md5(gcms.cal_file_path) 514 calibration_parameters["data_name"] = str(gcms.cal_file_path.stem) 515 calibration_parameters["calibration_method"] = "" 516 517 return calibration_parameters 518 519 def get_blank_stats(self, gcms): 520 """Get statistics about the GC-MS blank.""" 521 blank_parameters = {} 522 523 blank_parameters["data_name"] = "ni" 524 blank_parameters["blank_id"] = "ni" 525 blank_parameters["data_url"] = "ni" 526 blank_parameters["has_input"] = "ni" 527 blank_parameters["common_features_to_blank"] = "ni" 528 529 return blank_parameters 530 531 def get_instrument_metadata(self, gcms): 532 """Get metadata about the GC-MS instrument.""" 533 instrument_metadata = {} 534 535 instrument_metadata["analyzer"] = gcms.analyzer 536 instrument_metadata["instrument_label"] = gcms.instrument_label 537 instrument_metadata["instrument_id"] = uuid.uuid4().hex 538 539 return instrument_metadata 540 541 def get_data_metadata(self, gcms, id_label, output_path): 542 """Get metadata about the GC-MS data. 543 544 Parameters: 545 ---------- 546 gcms : object 547 The low resolution GCMS object. 548 id_label : str 549 The ID label for the data. 550 output_path : str 551 The output file path. 552 553 Returns: 554 ------- 555 dict 556 A dictionary containing the data metadata. 557 """ 558 if isinstance(output_path, str): 559 output_path = Path(output_path) 560 561 paramaters_path = output_path.with_suffix(".json") 562 563 if paramaters_path.exists(): 564 with paramaters_path.open() as current_param: 565 metadata = json.load(current_param) 566 data_metadata = metadata.get("Data") 567 else: 568 data_metadata = {} 569 data_metadata["data_name"] = [] 570 data_metadata["input_data_url"] = [] 571 data_metadata["has_input"] = [] 572 573 data_metadata["data_name"].append(gcms.sample_name) 574 data_metadata["input_data_url"].append(str(gcms.file_location)) 575 data_metadata["has_input"].append(id_label + corems_md5(gcms.file_location)) 576 577 data_metadata["output_data_name"] = str(output_path.stem) 578 data_metadata["output_data_url"] = str(output_path) 579 data_metadata["has_output"] = id_label + corems_md5(output_path) 580 581 return data_metadata 582 583 def get_parameters_json(self, gcms, id_label, output_path): 584 """Get the parameters as a JSON string. 585 586 Parameters: 587 ---------- 588 gcms : GCMS object 589 The low resolution GCMS object. 590 id_label : str 591 The ID label for the data. 592 output_path : str 593 The output file path. 594 595 Returns: 596 ------- 597 str 598 The parameters as a JSON string. 599 """ 600 601 output_parameters_dict = {} 602 output_parameters_dict["Data"] = self.get_data_metadata( 603 gcms, id_label, output_path 604 ) 605 output_parameters_dict["Stats"] = self.get_data_stats(gcms) 606 output_parameters_dict["Calibration"] = self.get_calibration_stats( 607 gcms, id_label 608 ) 609 output_parameters_dict["Blank"] = self.get_blank_stats(gcms) 610 output_parameters_dict["Instrument"] = self.get_instrument_metadata(gcms) 611 corems_dict_setting = parameter_to_dict.get_dict_data_gcms(gcms) 612 corems_dict_setting["corems_version"] = __version__ 613 output_parameters_dict["CoreMSParameters"] = corems_dict_setting 614 output_parameters_dict["has_metabolite"] = gcms.metabolites_data 615 output = json.dumps( 616 output_parameters_dict, sort_keys=False, indent=4, separators=(",", ": ") 617 ) 618 619 return output 620 621 def write_settings(self, output_path, gcms, id_label="emsl:"): 622 """Write the settings to a JSON file. 623 624 Parameters: 625 ---------- 626 output_path : str 627 The output file path. 628 gcms : GCMS object 629 The low resolution GCMS object. 630 id_label : str 631 The ID label for the data. Default is "emsl:". 632 633 """ 634 635 output = self.get_parameters_json(gcms, id_label, output_path) 636 637 with open( 638 output_path.with_suffix(".json"), 639 "w", 640 encoding="utf8", 641 ) as outfile: 642 outfile.write(output) 643 644 def get_list_dict_data(self, gcms, include_no_match=True, no_match_inline=False): 645 """Get the exported data as a list of dictionaries. 646 647 Parameters: 648 ---------- 649 gcms : object 650 The low resolution GCMS object. 651 include_no_match : bool, optional 652 Whether to include no match data. Default is True. 653 no_match_inline : bool, optional 654 Whether to include no match data inline. Default is False. 655 656 Returns: 657 ------- 658 list 659 The exported data as a list of dictionaries. 660 """ 661 662 output_score_method = gcms.molecular_search_settings.output_score_method 663 664 dict_data_list = [] 665 666 def add_match_dict_data(): 667 derivatization = "{}:{}:{}".format( 668 compound_obj.classify, 669 compound_obj.derivativenum, 670 compound_obj.derivatization, 671 ) 672 out_dict = { 673 "Sample name": gcms.sample_name, 674 "Peak Index": gcpeak_index, 675 "Retention Time": gc_peak.retention_time, 676 "Retention Time Ref": compound_obj.retention_time, 677 "Peak Height": gc_peak.tic, 678 "Peak Area": gc_peak.area, 679 "Retention index": gc_peak.ri, 680 "Retention index Ref": compound_obj.ri, 681 "Retention Index Score": compound_obj.ri_score, 682 "Spectral Similarity Score": compound_obj.spectral_similarity_score, 683 "Similarity Score": compound_obj.similarity_score, 684 "Compound Name": compound_obj.name, 685 "Chebi ID": compound_obj.metadata.chebi, 686 "Kegg Compound ID": compound_obj.metadata.kegg, 687 "Inchi": compound_obj.metadata.inchi, 688 "Inchi Key": compound_obj.metadata.inchikey, 689 "Smiles": compound_obj.metadata.smiles, 690 "Molecular Formula": compound_obj.formula, 691 "IUPAC Name": compound_obj.metadata.iupac_name, 692 "Traditional Name": compound_obj.metadata.traditional_name, 693 "Common Name": compound_obj.metadata.common_name, 694 "Derivatization": derivatization, 695 } 696 697 if self.gcms.molecular_search_settings.exploratory_mode: 698 out_dict.update( 699 { 700 "Weighted Cosine Correlation": compound_obj.spectral_similarity_scores.get( 701 "weighted_cosine_correlation" 702 ), 703 "Cosine Correlation": compound_obj.spectral_similarity_scores.get( 704 "cosine_correlation" 705 ), 706 "Stein Scott Similarity": compound_obj.spectral_similarity_scores.get( 707 "stein_scott_similarity" 708 ), 709 "Pearson Correlation": compound_obj.spectral_similarity_scores.get( 710 "pearson_correlation" 711 ), 712 "Spearman Correlation": compound_obj.spectral_similarity_scores.get( 713 "spearman_correlation" 714 ), 715 "Kendall Tau Correlation": compound_obj.spectral_similarity_scores.get( 716 "kendall_tau_correlation" 717 ), 718 "DFT Correlation": compound_obj.spectral_similarity_scores.get( 719 "dft_correlation" 720 ), 721 "DWT Correlation": compound_obj.spectral_similarity_scores.get( 722 "dwt_correlation" 723 ), 724 "Euclidean Distance": compound_obj.spectral_similarity_scores.get( 725 "euclidean_distance" 726 ), 727 "Manhattan Distance": compound_obj.spectral_similarity_scores.get( 728 "manhattan_distance" 729 ), 730 "Jaccard Distance": compound_obj.spectral_similarity_scores.get( 731 "jaccard_distance" 732 ), 733 } 734 ) 735 for method in methods_name: 736 out_dict[methods_name.get(method)] = ( 737 compound_obj.spectral_similarity_scores.get(method) 738 ) 739 740 dict_data_list.append(out_dict) 741 742 def add_no_match_dict_data(): 743 dict_data_list.append( 744 { 745 "Sample name": gcms.sample_name, 746 "Peak Index": gcpeak_index, 747 "Retention Time": gc_peak.retention_time, 748 "Peak Height": gc_peak.tic, 749 "Peak Area": gc_peak.area, 750 "Retention index": gc_peak.ri, 751 } 752 ) 753 754 for gcpeak_index, gc_peak in enumerate(gcms.sorted_gcpeaks): 755 # check if there is a compound candidate 756 if gc_peak: 757 if output_score_method == "highest_sim_score": 758 compound_obj = gc_peak.highest_score_compound 759 add_match_dict_data() 760 761 elif output_score_method == "highest_ss": 762 compound_obj = gc_peak.highest_ss_compound 763 add_match_dict_data() 764 765 else: 766 for compound_obj in gc_peak: 767 add_match_dict_data() # add monoisotopic peak 768 769 else: 770 # include not_match 771 if include_no_match and no_match_inline: 772 add_no_match_dict_data() 773 774 if include_no_match and not no_match_inline: 775 for gcpeak_index, gc_peak in enumerate(gcms.sorted_gcpeaks): 776 if not gc_peak: 777 add_no_match_dict_data() 778 779 return dict_data_list 780 781 782class HighResMassSpectraExport(HighResMassSpecExport): 783 """A class to export high resolution mass spectra data. 784 785 This class provides methods to export high resolution mass spectra data to various formats 786 such as Excel, CSV, HDF5, and Pandas DataFrame. 787 788 Parameters 789 ---------- 790 out_file_path : str | Path 791 The output file path. 792 mass_spectra : object 793 The high resolution mass spectra object. 794 output_type : str, optional 795 The output type. Default is 'excel'. 796 797 Attributes 798 ---------- 799 output_file : Path 800 The output file path without suffix 801 dir_loc : Path 802 The directory location for the output file, 803 by default this will be the output_file + ".corems" and all output files will be 804 written into this location 805 mass_spectra : MassSpectraBase 806 The high resolution mass spectra object. 807 """ 808 809 def __init__(self, out_file_path, mass_spectra, output_type="excel"): 810 super().__init__( 811 out_file_path=out_file_path, mass_spectrum=None, output_type=output_type 812 ) 813 814 self.dir_loc = Path(out_file_path + ".corems") 815 self.dir_loc.mkdir(exist_ok=True) 816 # Place the output file in the directory 817 self.output_file = self.dir_loc / Path(out_file_path).name 818 self._output_type = output_type # 'excel', 'csv', 'pandas' or 'hdf5' 819 self.mass_spectra = mass_spectra 820 self.atoms_order_list = None 821 self._init_columns() 822 823 def get_pandas_df(self): 824 """Get the mass spectra as a list of Pandas DataFrames.""" 825 826 list_df = [] 827 828 for mass_spectrum in self.mass_spectra: 829 columns = self.columns_label + self.get_all_used_atoms_in_order( 830 mass_spectrum 831 ) 832 833 dict_data_list = self.get_list_dict_data(mass_spectrum) 834 835 df = DataFrame(dict_data_list, columns=columns) 836 837 scan_number = mass_spectrum.scan_number 838 839 df.name = str(self.output_file) + "_" + str(scan_number) 840 841 list_df.append(df) 842 843 return list_df 844 845 def to_pandas(self, write_metadata=True): 846 """Export the data to a Pandas DataFrame and save it as a pickle file. 847 848 Parameters: 849 ---------- 850 write_metadata : bool, optional 851 Whether to write metadata to the output file. Default is True. 852 """ 853 854 for mass_spectrum in self.mass_spectra: 855 columns = self.columns_label + self.get_all_used_atoms_in_order( 856 mass_spectrum 857 ) 858 859 dict_data_list = self.get_list_dict_data(mass_spectrum) 860 861 df = DataFrame(dict_data_list, columns=columns) 862 863 scan_number = mass_spectrum.scan_number 864 865 out_filename = Path( 866 "%s_scan%s%s" % (self.output_file, str(scan_number), ".pkl") 867 ) 868 869 df.to_pickle(self.dir_loc / out_filename) 870 871 if write_metadata: 872 self.write_settings( 873 self.dir_loc / out_filename.with_suffix(""), mass_spectrum 874 ) 875 876 def to_excel(self, write_metadata=True): 877 """Export the data to an Excel file. 878 879 Parameters: 880 ---------- 881 write_metadata : bool, optional 882 Whether to write metadata to the output file. Default is True. 883 """ 884 for mass_spectrum in self.mass_spectra: 885 columns = self.columns_label + self.get_all_used_atoms_in_order( 886 mass_spectrum 887 ) 888 889 dict_data_list = self.get_list_dict_data(mass_spectrum) 890 891 df = DataFrame(dict_data_list, columns=columns) 892 893 scan_number = mass_spectrum.scan_number 894 895 out_filename = Path( 896 "%s_scan%s%s" % (self.output_file, str(scan_number), ".xlsx") 897 ) 898 899 df.to_excel(self.dir_loc / out_filename) 900 901 if write_metadata: 902 self.write_settings( 903 self.dir_loc / out_filename.with_suffix(""), mass_spectrum 904 ) 905 906 def to_csv(self, write_metadata=True): 907 """Export the data to a CSV file. 908 909 Parameters: 910 ---------- 911 write_metadata : bool, optional 912 Whether to write metadata to the output file. Default is True. 913 """ 914 import csv 915 916 for mass_spectrum in self.mass_spectra: 917 columns = self.columns_label + self.get_all_used_atoms_in_order( 918 mass_spectrum 919 ) 920 921 scan_number = mass_spectrum.scan_number 922 923 dict_data_list = self.get_list_dict_data(mass_spectrum) 924 925 out_filename = Path( 926 "%s_scan%s%s" % (self.output_file, str(scan_number), ".csv") 927 ) 928 929 with open(self.dir_loc / out_filename, "w", newline="") as csvfile: 930 writer = csv.DictWriter(csvfile, fieldnames=columns) 931 writer.writeheader() 932 for data in dict_data_list: 933 writer.writerow(data) 934 935 if write_metadata: 936 self.write_settings( 937 self.dir_loc / out_filename.with_suffix(""), mass_spectrum 938 ) 939 940 def get_mass_spectra_attrs(self): 941 """Get the mass spectra attributes as a JSON string. 942 943 Parameters: 944 ---------- 945 mass_spectra : object 946 The high resolution mass spectra object. 947 948 Returns: 949 ------- 950 str 951 The mass spectra attributes as a JSON string. 952 """ 953 dict_ms_attrs = {} 954 dict_ms_attrs["analyzer"] = self.mass_spectra.analyzer 955 dict_ms_attrs["instrument_label"] = self.mass_spectra.instrument_label 956 dict_ms_attrs["sample_name"] = self.mass_spectra.sample_name 957 958 return json.dumps( 959 dict_ms_attrs, sort_keys=False, indent=4, separators=(",", ": ") 960 ) 961 962 def to_hdf(self, overwrite=False, export_raw=True): 963 """Export the data to an HDF5 file. 964 965 Parameters 966 ---------- 967 overwrite : bool, optional 968 Whether to overwrite the output file. Default is False. 969 export_raw : bool, optional 970 Whether to export the raw mass spectra data. Default is True. 971 """ 972 if overwrite: 973 if self.output_file.with_suffix(".hdf5").exists(): 974 self.output_file.with_suffix(".hdf5").unlink() 975 976 with h5py.File(self.output_file.with_suffix(".hdf5"), "a") as hdf_handle: 977 if not hdf_handle.attrs.get("date_utc"): 978 # Set metadata for all mass spectra 979 timenow = str( 980 datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M:%S %Z") 981 ) 982 hdf_handle.attrs["date_utc"] = timenow 983 hdf_handle.attrs["filename"] = self.mass_spectra.file_location.name 984 hdf_handle.attrs["data_structure"] = "mass_spectra" 985 hdf_handle.attrs["analyzer"] = self.mass_spectra.analyzer 986 hdf_handle.attrs["instrument_label"] = ( 987 self.mass_spectra.instrument_label 988 ) 989 hdf_handle.attrs["sample_name"] = self.mass_spectra.sample_name 990 hdf_handle.attrs["polarity"] = self.mass_spectra.polarity 991 hdf_handle.attrs["parser_type"] = ( 992 self.mass_spectra.spectra_parser_class.__name__ 993 ) 994 hdf_handle.attrs["original_file_location"] = ( 995 self.mass_spectra.file_location._str 996 ) 997 998 # Save creation time from original parser if available 999 try: 1000 if hasattr(self.mass_spectra, 'spectra_parser') and self.mass_spectra.spectra_parser is not None: 1001 creation_time = self.mass_spectra.spectra_parser.get_creation_time() 1002 if creation_time is not None: 1003 hdf_handle.attrs["creation_time"] = creation_time.isoformat() 1004 except Exception: 1005 pass # If creation time cannot be retrieved, skip it 1006 1007 if "mass_spectra" not in hdf_handle: 1008 mass_spectra_group = hdf_handle.create_group("mass_spectra") 1009 else: 1010 mass_spectra_group = hdf_handle.get("mass_spectra") 1011 1012 for mass_spectrum in self.mass_spectra: 1013 group_key = str(int(mass_spectrum.scan_number)) 1014 1015 self.add_mass_spectrum_to_hdf5( 1016 hdf_handle, mass_spectrum, group_key, mass_spectra_group, export_raw 1017 ) 1018 1019 1020class LCMSExport(HighResMassSpectraExport): 1021 """A class to export high resolution LC-MS data. 1022 1023 This class provides methods to export high resolution LC-MS data to HDF5. 1024 1025 Parameters 1026 ---------- 1027 out_file_path : str | Path 1028 The output file path, do not include the file extension. 1029 lcms_object : LCMSBase 1030 The high resolution lc-ms object. 1031 """ 1032 1033 def __init__(self, out_file_path, mass_spectra): 1034 super().__init__(out_file_path, mass_spectra, output_type="hdf5") 1035 1036 @staticmethod 1037 def _save_mass_features_dict_to_hdf5(mass_features_dict, mass_features_group, overwrite=False): 1038 """Save a dictionary of mass features to an HDF5 group. 1039 1040 This is a helper method that can be reused by different export classes. 1041 1042 Parameters 1043 ---------- 1044 mass_features_dict : dict 1045 Dictionary of mass features to save, keyed by mass feature ID. 1046 mass_features_group : h5py.Group 1047 The HDF5 group to save the mass features to. 1048 overwrite : bool, optional 1049 Whether to overwrite existing mass features. Default is False. 1050 """ 1051 1052 # Create group for each mass feature, with key as the mass feature id 1053 for k, v in mass_features_dict.items(): 1054 if str(k) not in mass_features_group or overwrite: 1055 if str(k) in mass_features_group and overwrite: 1056 del mass_features_group[str(k)] 1057 mass_features_group.create_group(str(k)) 1058 # Loop through each of the mass feature attributes and add them as attributes (if single value) or datasets (if array) 1059 for k2, v2 in v.__dict__.items(): 1060 if v2 is not None: 1061 # Check if the attribute is an integer or float and set as an attribute in the mass feature group 1062 if k2 not in [ 1063 "chromatogram_parent", 1064 "ms2_mass_spectra", 1065 "mass_spectrum", 1066 "_eic_data", 1067 "ms2_similarity_results", 1068 ]: 1069 if k2 == "ms2_scan_numbers": 1070 array = np.array(v2) 1071 # Convert int64 to int32 1072 if array.dtype == np.int64: 1073 array = array.astype(np.int32) 1074 mass_features_group[str(k)].create_dataset( 1075 str(k2), data=array, compression="gzip", compression_opts=9, chunks=True 1076 ) 1077 elif k2 == "_half_height_width": 1078 array = np.array(v2) 1079 # Convert float64 to float32 1080 if array.dtype == np.float64: 1081 array = array.astype(np.float32) 1082 mass_features_group[str(k)].create_dataset( 1083 str(k2), data=array, compression="gzip", compression_opts=9, chunks=True 1084 ) 1085 elif k2 == "_ms_deconvoluted_idx": 1086 array = np.array(v2) 1087 # Convert int64 to int32 1088 if array.dtype == np.int64: 1089 array = array.astype(np.int32) 1090 mass_features_group[str(k)].create_dataset( 1091 str(k2), data=array, compression="gzip", compression_opts=9, chunks=True 1092 ) 1093 elif k2 == "associated_mass_features_deconvoluted": 1094 array = np.array(v2) 1095 # Convert int64 to int32 1096 if array.dtype == np.int64: 1097 array = array.astype(np.int32) 1098 mass_features_group[str(k)].create_dataset( 1099 str(k2), data=array, compression="gzip", compression_opts=9, chunks=True 1100 ) 1101 elif k2 == "_noise_score": 1102 array = np.array(v2) 1103 # Convert float64 to float32 1104 if array.dtype == np.float64: 1105 array = array.astype(np.float32) 1106 mass_features_group[str(k)].create_dataset( 1107 str(k2), data=array, compression="gzip", compression_opts=9, chunks=True 1108 ) 1109 elif ( 1110 isinstance(v2, int) 1111 or isinstance(v2, float) 1112 or isinstance(v2, str) 1113 or isinstance(v2, np.integer) 1114 or isinstance(v2, np.float32) 1115 or isinstance(v2, np.float64) 1116 or isinstance(v2, np.bool_) 1117 ): 1118 # Convert numpy types to smaller precision for storage 1119 if isinstance(v2, np.int64): 1120 v2 = np.int32(v2) 1121 elif isinstance(v2, np.float64): 1122 v2 = np.float32(v2) 1123 mass_features_group[str(k)].attrs[str(k2)] = v2 1124 1125 @staticmethod 1126 def _save_eics_dict_to_hdf5(eics_dict, eics_group, overwrite=False): 1127 """Save a dictionary of EICs to an HDF5 group. 1128 1129 This is a static helper method that can be reused by different export classes 1130 to save EIC data in a consistent format. 1131 1132 Parameters 1133 ---------- 1134 eics_dict : dict 1135 Dictionary of EIC_Data objects, keyed by m/z value. 1136 eics_group : h5py.Group 1137 The HDF5 group to save the EICs to. 1138 overwrite : bool, optional 1139 Whether to overwrite existing EICs. Default is False. 1140 """ 1141 for mz, eic_data in eics_dict.items(): 1142 mz_str = str(mz) 1143 if mz_str not in eics_group or overwrite: 1144 if mz_str in eics_group and overwrite: 1145 del eics_group[mz_str] 1146 eic_grp = eics_group.create_group(mz_str) 1147 eic_grp.attrs["mz"] = mz 1148 1149 # Save all EIC_Data attributes as datasets 1150 for attr_name, attr_value in eic_data.__dict__.items(): 1151 if attr_value is not None: 1152 array = np.array(attr_value) 1153 # Apply data type optimization and compression 1154 if array.dtype == np.int64: 1155 array = array.astype(np.int32) 1156 elif array.dtype == np.float64: 1157 array = array.astype(np.float32) 1158 elif array.dtype.str[0:2] == "<U": 1159 # Convert Unicode strings to UTF-8 encoded strings 1160 string_data = [str(item) for item in array] 1161 string_dtype = h5py.string_dtype(encoding='utf-8') 1162 eic_grp.create_dataset(str(attr_name), data=string_data, dtype=string_dtype, compression="gzip", compression_opts=9, chunks=True) 1163 continue 1164 eic_grp.create_dataset(str(attr_name), data=array, compression="gzip", compression_opts=9, chunks=True) 1165 1166 def _save_mass_features_to_hdf5(self, hdf_handle, group_name = "mass_features", overwrite=False): 1167 """Save the mass features to the HDF5 file. 1168 1169 Parameters 1170 ---------- 1171 hdf_handle : h5py.File 1172 The HDF5 file handle. 1173 group_name : str, optional 1174 The name of the group to save the mass features to. Default is 'mass_features'. 1175 overwrite : bool, optional 1176 Whether to overwrite the group if it exists. Default is False. 1177 """ 1178 # Determine which mass features to save based on group_name 1179 if group_name == "induced_mass_features": 1180 if len(self.mass_spectra.induced_mass_features) == 0: 1181 return # No induced mass features to save 1182 mass_features_dict = self.mass_spectra.induced_mass_features 1183 else: 1184 if len(self.mass_spectra.mass_features) == 0: 1185 return # No mass features to save 1186 mass_features_dict = self.mass_spectra.mass_features 1187 1188 # Add LCMS mass features to hdf5 file 1189 if group_name not in hdf_handle: 1190 mass_features_group = hdf_handle.create_group(group_name) 1191 else: 1192 mass_features_group = hdf_handle.get(group_name) 1193 1194 # Use the static helper method to save the mass features 1195 self._save_mass_features_dict_to_hdf5(mass_features_dict, mass_features_group, overwrite) 1196 1197 def to_hdf(self, overwrite=False, save_parameters=True, parameter_format="toml"): 1198 """Export the data to an HDF5. 1199 1200 Parameters 1201 ---------- 1202 overwrite : bool, optional 1203 Whether to overwrite the output file. Default is False. 1204 save_parameters : bool, optional 1205 Whether to save the parameters as a separate json or toml file. Default is True. 1206 parameter_format : str, optional 1207 The format to save the parameters in. Default is 'toml'. 1208 1209 Raises 1210 ------ 1211 ValueError 1212 If parameter_format is not 'json' or 'toml'. 1213 """ 1214 export_profile_spectra = ( 1215 self.mass_spectra.parameters.lc_ms.export_profile_spectra 1216 ) 1217 1218 # Write the mass spectra data to the hdf5 file 1219 super().to_hdf(overwrite=overwrite, export_raw=export_profile_spectra) 1220 1221 # Write scan info, ms_unprocessed, mass features, eics, and ms2_search results to the hdf5 file 1222 with h5py.File(self.output_file.with_suffix(".hdf5"), "a") as hdf_handle: 1223 # Add scan_info to hdf5 file 1224 if "scan_info" not in hdf_handle or overwrite: 1225 if "scan_info" in hdf_handle and overwrite: 1226 del hdf_handle["scan_info"] 1227 scan_info_group = hdf_handle.create_group("scan_info") 1228 for k, v in self.mass_spectra._scan_info.items(): 1229 array = np.array(list(v.values())) 1230 if array.dtype.str[0:2] == "<U": 1231 array = array.astype("S") 1232 scan_info_group.create_dataset(k, data=array) 1233 1234 # Add ms_unprocessed to hdf5 file 1235 export_unprocessed_ms1 = ( 1236 self.mass_spectra.parameters.lc_ms.export_unprocessed_ms1 1237 ) 1238 if self.mass_spectra._ms_unprocessed and export_unprocessed_ms1: 1239 if "ms_unprocessed" not in hdf_handle or overwrite: 1240 if "ms_unprocessed" in hdf_handle and overwrite: 1241 del hdf_handle["ms_unprocessed"] 1242 ms_unprocessed_group = hdf_handle.create_group("ms_unprocessed") 1243 else: 1244 ms_unprocessed_group = hdf_handle.get("ms_unprocessed") 1245 for k, v in self.mass_spectra._ms_unprocessed.items(): 1246 if str(k) not in ms_unprocessed_group or overwrite: 1247 if str(k) in ms_unprocessed_group and overwrite: 1248 del ms_unprocessed_group[str(k)] 1249 array = np.array(v) 1250 ms_unprocessed_group.create_dataset(str(k), data=array) 1251 1252 # Add LCMS mass features to hdf5 file 1253 self._save_mass_features_to_hdf5(hdf_handle, group_name="mass_features", overwrite=overwrite) 1254 self._save_mass_features_to_hdf5(hdf_handle, group_name="induced_mass_features", overwrite=overwrite) 1255 1256 # Add EIC data to hdf5 file 1257 export_eics = self.mass_spectra.parameters.lc_ms.export_eics 1258 if len(self.mass_spectra.eics) > 0 and export_eics: 1259 if "eics" not in hdf_handle or overwrite: 1260 if "eics" in hdf_handle and overwrite: 1261 del hdf_handle["eics"] 1262 eic_group = hdf_handle.create_group("eics") 1263 else: 1264 eic_group = hdf_handle.get("eics") 1265 1266 # Use the static helper method to save the EICs 1267 self._save_eics_dict_to_hdf5(self.mass_spectra.eics, eic_group, overwrite) 1268 1269 # Add ms2_search results to hdf5 file (parameterized) 1270 if len(self.mass_spectra.spectral_search_results) > 0: 1271 if "spectral_search_results" not in hdf_handle or overwrite: 1272 if "spectral_search_results" in hdf_handle and overwrite: 1273 del hdf_handle["spectral_search_results"] 1274 spectral_search_results = hdf_handle.create_group( 1275 "spectral_search_results" 1276 ) 1277 else: 1278 spectral_search_results = hdf_handle.get("spectral_search_results") 1279 # Create group for each search result by ms2_scan / precursor_mz 1280 for k, v in self.mass_spectra.spectral_search_results.items(): 1281 #TODO KRH: Fix to handle if export_only_relevant and k not in relevant_scan_numbers: continue! 1282 if str(k) not in spectral_search_results or overwrite: 1283 if str(k) in spectral_search_results and overwrite: 1284 del spectral_search_results[str(k)] 1285 spectral_search_results.create_group(str(k)) 1286 for k2, v2 in v.items(): 1287 spectral_search_results[str(k)].create_group(str(k2)) 1288 spectral_search_results[str(k)][str(k2)].attrs[ 1289 "precursor_mz" 1290 ] = v2.precursor_mz 1291 spectral_search_results[str(k)][str(k2)].attrs[ 1292 "query_spectrum_id" 1293 ] = v2.query_spectrum_id 1294 # Loop through each of the attributes and add them as datasets (if array) 1295 for k3, v3 in v2.__dict__.items(): 1296 if v3 is not None and k3 not in [ 1297 "query_spectrum", 1298 "precursor_mz", 1299 "query_spectrum_id", 1300 ]: 1301 if k3 == "query_frag_types" or k3 == "ref_frag_types": 1302 v3 = [", ".join(x) for x in v3] 1303 if all(v3 is not None for v3 in v3): 1304 array = np.array(v3) 1305 if array.dtype.str[0:2] == "<U": 1306 array = array.astype("S") 1307 spectral_search_results[str(k)][str(k2)].create_dataset( 1308 str(k3), data=array 1309 ) 1310 1311 # Save parameters as separate json 1312 if save_parameters: 1313 # Check if parameter_format is valid 1314 if parameter_format not in ["json", "toml"]: 1315 raise ValueError("parameter_format must be 'json' or 'toml'") 1316 1317 if parameter_format == "json": 1318 dump_lcms_settings_json( 1319 filename=self.output_file.with_suffix(".json"), 1320 lcms_obj=self.mass_spectra, 1321 ) 1322 elif parameter_format == "toml": 1323 dump_lcms_settings_toml( 1324 filename=self.output_file.with_suffix(".toml"), 1325 lcms_obj=self.mass_spectra, 1326 ) 1327 1328class LCMSMetabolomicsExport(LCMSExport): 1329 """A class to export LCMS metabolite data. 1330 1331 This class provides methods to export LCMS metabolite data to various formats and summarize the metabolite report. 1332 1333 Parameters 1334 ---------- 1335 out_file_path : str | Path 1336 The output file path, do not include the file extension. 1337 mass_spectra : object 1338 The high resolution mass spectra object. 1339 """ 1340 1341 def __init__(self, out_file_path, mass_spectra): 1342 super().__init__(out_file_path, mass_spectra) 1343 self.ion_type_dict = ion_type_dict 1344 1345 @staticmethod 1346 def get_ion_formula(neutral_formula, ion_type): 1347 """From a neutral formula and an ion type, return the formula of the ion. 1348 1349 Notes 1350 ----- 1351 This is a static method. 1352 If the neutral_formula is not a string, this method will return None. 1353 1354 Parameters 1355 ---------- 1356 neutral_formula : str 1357 The neutral formula, this should be a string form from the MolecularFormula class 1358 (e.g. 'C2 H4 O2', isotopes OK), or simple string (e.g. 'C2H4O2', no isotope handling in this case). 1359 In the case of a simple string, the atoms are parsed based on the presence of capital letters, 1360 e.g. MgCl2 is parsed as 'Mg Cl2. 1361 ion_type : str 1362 The ion type, e.g. 'protonated', '[M+H]+', '[M+Na]+', etc. 1363 See the self.ion_type_dict for the available ion types. 1364 1365 Returns 1366 ------- 1367 str 1368 The formula of the ion as a string (like 'C2 H4 O2'); or None if the neutral_formula is not a string. 1369 """ 1370 # If neutral_formula is not a string, return None 1371 if not isinstance(neutral_formula, str): 1372 return None 1373 1374 # Check if there are spaces in the formula (these are outputs of the MolecularFormula class and do not need to be processed before being passed to the class) 1375 if re.search(r"\s", neutral_formula): 1376 neutral_formula = MolecularFormula(neutral_formula, ion_charge=0) 1377 else: 1378 form_pre = re.sub(r"([A-Z])", r" \1", neutral_formula)[1:] 1379 elements = [re.findall(r"[A-Z][a-z]*", x) for x in form_pre.split()] 1380 counts = [re.findall(r"\d+", x) for x in form_pre.split()] 1381 neutral_formula = MolecularFormula( 1382 dict( 1383 zip( 1384 [x[0] for x in elements], 1385 [int(x[0]) if x else 1 for x in counts], 1386 ) 1387 ), 1388 ion_charge=0, 1389 ) 1390 neutral_formula_dict = neutral_formula.to_dict().copy() 1391 1392 adduct_add_dict = ion_type_dict[ion_type][0] 1393 for key in adduct_add_dict: 1394 if key in neutral_formula_dict.keys(): 1395 neutral_formula_dict[key] += adduct_add_dict[key] 1396 else: 1397 neutral_formula_dict[key] = adduct_add_dict[key] 1398 1399 adduct_subtract = ion_type_dict[ion_type][1] 1400 for key in adduct_subtract: 1401 neutral_formula_dict[key] -= adduct_subtract[key] 1402 1403 return MolecularFormula(neutral_formula_dict, ion_charge=0).string 1404 1405 @staticmethod 1406 def get_isotope_type(ion_formula): 1407 """From an ion formula, return the 13C isotope type of the ion. 1408 1409 Notes 1410 ----- 1411 This is a static method. 1412 If the ion_formula is not a string, this method will return None. 1413 This is currently only functional for 13C isotopes. 1414 1415 Parameters 1416 ---------- 1417 ion_formula : str 1418 The formula of the ion, expected to be a string like 'C2 H4 O2'. 1419 1420 Returns 1421 ------- 1422 str 1423 The isotope type of the ion, e.g. '13C1', '13C2', etc; or None if the ion_formula does not contain a 13C isotope. 1424 1425 Raises 1426 ------ 1427 ValueError 1428 If the ion_formula is not a string. 1429 """ 1430 if not isinstance(ion_formula, str): 1431 return None 1432 1433 if re.search(r"\s", ion_formula): 1434 ion_formula = MolecularFormula(ion_formula, ion_charge=0) 1435 else: 1436 raise ValueError('ion_formula should be a string like "C2 H4 O2"') 1437 ion_formula_dict = ion_formula.to_dict().copy() 1438 1439 try: 1440 iso_class = "13C" + str(ion_formula_dict.pop("13C")) 1441 except KeyError: 1442 iso_class = None 1443 1444 return iso_class 1445 1446 def report_to_csv(self, molecular_metadata=None): 1447 """Create a report of the mass features and their annotations and save it as a CSV file. 1448 1449 Parameters 1450 ---------- 1451 molecular_metadata : dict, optional 1452 The molecular metadata. Default is None. 1453 """ 1454 report = self.to_report(molecular_metadata=molecular_metadata) 1455 out_file = self.output_file.with_suffix(".csv") 1456 report.to_csv(out_file, index=False) 1457 1458 def clean_ms1_report(self, ms1_summary_full): 1459 """Clean the MS1 report. 1460 1461 Parameters 1462 ---------- 1463 ms1_summary_full : DataFrame 1464 The full MS1 summary DataFrame. 1465 1466 Returns 1467 ------- 1468 DataFrame 1469 The cleaned MS1 summary DataFrame. 1470 """ 1471 ms1_summary_full = ms1_summary_full.reset_index() 1472 cols_to_keep = [ 1473 "mf_id", 1474 "Molecular Formula", 1475 "Ion Type", 1476 "Calculated m/z", 1477 "m/z Error (ppm)", 1478 "m/z Error Score", 1479 "Is Isotopologue", 1480 "Isotopologue Similarity", 1481 "Confidence Score", 1482 ] 1483 ms1_summary = ms1_summary_full[cols_to_keep].copy() 1484 ms1_summary["ion_formula"] = [ 1485 self.get_ion_formula(f, a) 1486 for f, a in zip(ms1_summary["Molecular Formula"], ms1_summary["Ion Type"]) 1487 ] 1488 ms1_summary["isotopologue_type"] = [ 1489 self.get_isotope_type(f) for f in ms1_summary["ion_formula"].tolist() 1490 ] 1491 1492 # Reorder columns 1493 ms1_summary = ms1_summary[ 1494 [ 1495 "mf_id", 1496 "ion_formula", 1497 "isotopologue_type", 1498 "Calculated m/z", 1499 "m/z Error (ppm)", 1500 "m/z Error Score", 1501 "Isotopologue Similarity", 1502 "Confidence Score", 1503 ] 1504 ] 1505 1506 # Set the index to mf_id 1507 ms1_summary = ms1_summary.set_index("mf_id") 1508 1509 return ms1_summary 1510 1511 def summarize_ms2_report(self, ms2_annot_report): 1512 """ 1513 Summarize the MS2 report. 1514 1515 Parameters 1516 ---------- 1517 ms2_annot_report : DataFrame 1518 The MS2 annotation DataFrame with all annotations, output of mass_features_ms2_annot_to_df. 1519 1520 Returns 1521 ------- 1522 """ 1523 1524 def summarize_metabolomics_report(self, ms2_annot_report): 1525 """Summarize the MS2 hits for a metabolomics report 1526 1527 Parameters 1528 ---------- 1529 ms2_annot : DataFrame 1530 The MS2 annotation DataFrame with all annotations. 1531 1532 Returns 1533 ------- 1534 DataFrame 1535 The summarized metabolomics report. 1536 """ 1537 columns_to_drop = [ 1538 "precursor_mz", 1539 "precursor_mz_error_ppm", 1540 "cas", 1541 "data_id", 1542 "iupac_name", 1543 "traditional_name", 1544 "common_name", 1545 "casno", 1546 ] 1547 ms2_annot = ms2_annot_report.drop( 1548 columns=[col for col in columns_to_drop if col in ms2_annot_report.columns] 1549 ) 1550 1551 # Prepare information about the search results, pulling out the best hit for the single report 1552 # Group by mf_id,ref_mol_id grab row with highest entropy similarity 1553 ms2_annot = ms2_annot.reset_index() 1554 # Add column called "n_spectra_contributing" that is the number of unique values in query_spectrum_id per mf_id,ref_mol_id 1555 ms2_annot["n_spectra_contributing"] = ( 1556 ms2_annot.groupby(["mf_id", "ref_mol_id"])["query_spectrum_id"] 1557 .transform("nunique") 1558 ) 1559 # Sort by entropy similarity 1560 ms2_annot = ms2_annot.sort_values( 1561 by=["mf_id", "ref_mol_id", "entropy_similarity"], ascending=[True, True, False] 1562 ) 1563 best_entropy = ms2_annot.drop_duplicates( 1564 subset=["mf_id", "ref_mol_id"], keep="first" 1565 ) 1566 1567 return best_entropy 1568 1569 def clean_ms2_report(self, metabolite_summary): 1570 """Clean the MS2 report. 1571 1572 Parameters 1573 ---------- 1574 metabolite_summary : DataFrame 1575 The full metabolomics summary DataFrame. 1576 1577 Returns 1578 ------- 1579 DataFrame 1580 The cleaned metabolomics summary DataFrame. 1581 """ 1582 metabolite_summary = metabolite_summary.reset_index() 1583 metabolite_summary["ion_formula"] = [ 1584 self.get_ion_formula(f, a) 1585 for f, a in zip(metabolite_summary["formula"], metabolite_summary["ref_ion_type"]) 1586 ] 1587 1588 col_order = [ 1589 "mf_id", 1590 "ion_formula", 1591 "ref_ion_type", 1592 "formula", 1593 "inchikey", 1594 "name", 1595 "inchi", 1596 "chebi", 1597 "smiles", 1598 "kegg", 1599 "cas", 1600 "database_name", 1601 "ref_ms_id", 1602 "entropy_similarity", 1603 "ref_mz_in_query_fract", 1604 "n_spectra_contributing", 1605 ] 1606 1607 # Reorder columns 1608 metabolite_summary = metabolite_summary[ 1609 [col for col in col_order if col in metabolite_summary.columns] 1610 ] 1611 1612 # Convert chebi (if present) to int: 1613 if "chebi" in metabolite_summary.columns: 1614 metabolite_summary["chebi"] = metabolite_summary["chebi"].astype( 1615 "Int64", errors="ignore" 1616 ) 1617 1618 # Set the index to mf_id 1619 metabolite_summary = metabolite_summary.set_index("mf_id") 1620 1621 return metabolite_summary 1622 1623 def combine_reports(self, mf_report, ms1_annot_report, ms2_annot_report): 1624 """Combine the mass feature report with the MS1 and MS2 reports. 1625 1626 Parameters 1627 ---------- 1628 mf_report : DataFrame 1629 The mass feature report DataFrame. 1630 ms1_annot_report : DataFrame 1631 The MS1 annotation report DataFrame. 1632 ms2_annot_report : DataFrame 1633 The MS2 annotation report DataFrame. 1634 """ 1635 # If there is an ms1_annot_report, merge it with the mf_report 1636 if ms1_annot_report is not None and not ms1_annot_report.empty: 1637 # MS1 has been run and has molecular formula information 1638 mf_report = pd.merge( 1639 mf_report, 1640 ms1_annot_report, 1641 how="left", 1642 on=["mf_id", "isotopologue_type"], 1643 ) 1644 if ms2_annot_report is not None: 1645 # If both reports contain 'ion_formula', prefer a merge that respects it. 1646 # Otherwise fall back to merging on 'mf_id' only to remain robust when 1647 # MS1 formula assignment wasn't performed or MS2 summary lacks the field. 1648 if "ion_formula" in mf_report.columns and "ion_formula" in ms2_annot_report.columns: 1649 # pull out the records without ion_formula and merge on mf_id only 1650 mf_no_ion_formula = mf_report[mf_report["ion_formula"].isna()] 1651 mf_no_ion_formula = mf_no_ion_formula.drop(columns=["ion_formula"]) if "ion_formula" in mf_no_ion_formula.columns else mf_no_ion_formula 1652 mf_no_ion_formula = pd.merge( 1653 mf_no_ion_formula, ms2_annot_report, how="left", on=["mf_id"] 1654 ) 1655 1656 # pull out the records with ion_formula and merge on mf_id + ion_formula 1657 mf_with_ion_formula = mf_report[~mf_report["ion_formula"].isna()] 1658 mf_with_ion_formula = pd.merge( 1659 mf_with_ion_formula, 1660 ms2_annot_report, 1661 how="left", 1662 on=["mf_id", "ion_formula"], 1663 ) 1664 1665 # put back together 1666 mf_report = pd.concat([mf_no_ion_formula, mf_with_ion_formula]) 1667 else: 1668 # Fall back to merging on mf_id only (robust when ion_formula missing) 1669 mf_report = pd.merge( 1670 mf_report, ms2_annot_report, how="left", on=["mf_id"] 1671 ) 1672 1673 # Rename colums 1674 rename_dict = { 1675 "mf_id": "Mass Feature ID", 1676 "scan_time": "Retention Time (min)", 1677 "mz": "m/z", 1678 "apex_scan": "Apex Scan Number", 1679 "intensity": "Intensity", 1680 "persistence": "Persistence", 1681 "area": "Area", 1682 "half_height_width": "Half Height Width (min)", 1683 "tailing_factor": "Tailing Factor", 1684 "dispersity_index": "Dispersity Index", 1685 "ms2_spectrum": "MS2 Spectrum", 1686 "monoisotopic_mf_id": "Monoisotopic Mass Feature ID", 1687 "isotopologue_type": "Isotopologue Type", 1688 "mass_spectrum_deconvoluted_parent": "Is Largest Ion after Deconvolution", 1689 "associated_mass_features": "Associated Mass Features after Deconvolution", 1690 "ion_formula": "Ion Formula", 1691 "formula": "Molecular Formula", 1692 "ref_ion_type": "Ion Type", 1693 "annot_level": "Lipid Annotation Level", 1694 "lipid_molecular_species_id": "Lipid Molecular Species", 1695 "lipid_summed_name": "Lipid Species", 1696 "lipid_subclass": "Lipid Subclass", 1697 "lipid_class": "Lipid Class", 1698 "lipid_category": "Lipid Category", 1699 "entropy_similarity": "Entropy Similarity", 1700 "ref_mz_in_query_fract": "Library mzs in Query (fraction)", 1701 "n_spectra_contributing": "Spectra with Annotation (n)", 1702 } 1703 mf_report = mf_report.rename(columns=rename_dict) 1704 mf_report["Sample Name"] = self.mass_spectra.sample_name 1705 mf_report["Polarity"] = self.mass_spectra.polarity 1706 mf_report = mf_report[ 1707 ["Mass Feature ID", "Sample Name", "Polarity"] 1708 + [ 1709 col 1710 for col in mf_report.columns 1711 if col not in ["Mass Feature ID", "Sample Name", "Polarity"] 1712 ] 1713 ] 1714 1715 # Reorder rows by "Mass Feature ID", then "Entropy Similarity" (descending), then "Confidence Score" (descending) 1716 if "Entropy Similarity" in mf_report.columns and "Confidence Score" in mf_report.columns: 1717 mf_report = mf_report.sort_values( 1718 by=["Mass Feature ID", "Entropy Similarity", "Confidence Score"], 1719 ascending=[True, False, False], 1720 ) 1721 elif "Entropy Similarity" in mf_report.columns: 1722 mf_report = mf_report.sort_values( 1723 by=["Mass Feature ID", "Entropy Similarity"], 1724 ascending=[True, False], 1725 ) 1726 elif "Confidence Score" in mf_report.columns: 1727 mf_report = mf_report.sort_values( 1728 by=["Mass Feature ID", "Confidence Score"], 1729 ascending=[True, False], 1730 ) 1731 # If neither "Entropy Similarity" nor "Confidence Score" are in the columns, just sort by "Mass Feature ID" 1732 else: 1733 mf_report = mf_report.sort_values("Mass Feature ID") 1734 1735 # Reset index 1736 mf_report = mf_report.reset_index(drop=True) 1737 1738 return mf_report 1739 1740 def to_report(self, molecular_metadata=None, suppress_warnings=False): 1741 """Create a report of the mass features and their annotations. 1742 1743 Parameters 1744 ---------- 1745 molecular_metadata : dict, optional 1746 The molecular metadata. Default is None. 1747 suppress_warnings : bool, optional 1748 If True, suppresses warnings from mass_features_ms2_annot_to_df. 1749 Default is False. 1750 1751 Returns 1752 ------- 1753 DataFrame 1754 The report as a Pandas DataFrame. 1755 """ 1756 # Get mass feature dataframe 1757 mf_report = self.mass_spectra.mass_features_to_df() 1758 mf_report = mf_report.reset_index(drop=False) 1759 1760 # Get and clean ms1 annotation dataframe 1761 ms1_annot_report = self.mass_spectra.mass_features_ms1_annot_to_df(suppress_warnings=suppress_warnings) 1762 if ms1_annot_report is not None: 1763 ms1_annot_report = ms1_annot_report.copy() 1764 ms1_annot_report = self.clean_ms1_report(ms1_annot_report) 1765 ms1_annot_report = ms1_annot_report.reset_index(drop=False) 1766 else: 1767 ms1_annot_report = None 1768 1769 # Get, summarize, and clean ms2 annotation dataframe 1770 ms2_annot_report = self.mass_spectra.mass_features_ms2_annot_to_df( 1771 molecular_metadata=molecular_metadata, 1772 suppress_warnings=suppress_warnings 1773 ) 1774 if ms2_annot_report is not None and molecular_metadata is not None: 1775 ms2_annot_report = self.summarize_metabolomics_report(ms2_annot_report) 1776 ms2_annot_report = self.clean_ms2_report(ms2_annot_report) 1777 ms2_annot_report = ms2_annot_report.dropna(axis=1, how="all") 1778 ms2_annot_report = ms2_annot_report.reset_index(drop=False) 1779 else: 1780 ms2_annot_report = None 1781 1782 report = self.combine_reports( 1783 mf_report=mf_report, 1784 ms1_annot_report=ms1_annot_report, 1785 ms2_annot_report=ms2_annot_report 1786 ) 1787 1788 return report 1789class LipidomicsExport(LCMSMetabolomicsExport): 1790 """A class to export lipidomics data. 1791 1792 This class provides methods to export lipidomics data to various formats and summarize the lipid report. 1793 1794 Parameters 1795 ---------- 1796 out_file_path : str | Path 1797 The output file path, do not include the file extension. 1798 mass_spectra : object 1799 The high resolution mass spectra object. 1800 """ 1801 1802 def __init__(self, out_file_path, mass_spectra): 1803 super().__init__(out_file_path, mass_spectra) 1804 1805 def summarize_lipid_report(self, ms2_annot): 1806 """Summarize the lipid report. 1807 1808 Parameters 1809 ---------- 1810 ms2_annot : DataFrame 1811 The MS2 annotation DataFrame with all annotations. 1812 1813 Returns 1814 ------- 1815 DataFrame 1816 The summarized lipid report. 1817 """ 1818 # Drop unnecessary columns for easier viewing 1819 columns_to_drop = [ 1820 "precursor_mz", 1821 "precursor_mz_error_ppm", 1822 "ref_mol_id", 1823 "ref_precursor_mz", 1824 "cas", 1825 "inchikey", 1826 "inchi", 1827 "chebi", 1828 "smiles", 1829 "kegg", 1830 "data_id", 1831 "iupac_name", 1832 "traditional_name", 1833 "common_name", 1834 "casno", 1835 ] 1836 ms2_annot = ms2_annot.drop( 1837 columns=[col for col in columns_to_drop if col in ms2_annot.columns] 1838 ) 1839 1840 # If ion_types_excluded is not empty, remove those ion types 1841 ion_types_excluded = self.mass_spectra.parameters.mass_spectrum[ 1842 "ms2" 1843 ].molecular_search.ion_types_excluded 1844 if len(ion_types_excluded) > 0: 1845 ms2_annot = ms2_annot[~ms2_annot["ref_ion_type"].isin(ion_types_excluded)] 1846 1847 # If mf_id is not present, check that the index name is mf_id and reset the index 1848 if "mf_id" not in ms2_annot.columns: 1849 if ms2_annot.index.name == "mf_id": 1850 ms2_annot = ms2_annot.reset_index() 1851 else: 1852 raise ValueError("mf_id is not present in the dataframe") 1853 1854 # Attempt to get consensus annotations to the MLF level 1855 mlf_results_all = [] 1856 for mf_id in ms2_annot["mf_id"].unique(): 1857 mlf_results_perid = [] 1858 ms2_annot_mf = ms2_annot[ms2_annot["mf_id"] == mf_id].copy() 1859 ms2_annot_mf["n_spectra_contributing"] = ms2_annot_mf.query_spectrum_id.nunique() 1860 1861 for query_scan in ms2_annot["query_spectrum_id"].unique(): 1862 ms2_annot_sub = ms2_annot_mf[ 1863 ms2_annot_mf["query_spectrum_id"] == query_scan 1864 ].copy() 1865 1866 if ms2_annot_sub["lipid_summed_name"].nunique() == 1: 1867 # If there is only one lipid_summed_name, let's try to get consensus molecular species annotation 1868 if ms2_annot_sub["lipid_summed_name"].nunique() == 1: 1869 ms2_annot_sub["entropy_max"] = ( 1870 ms2_annot_sub["entropy_similarity"] 1871 == ms2_annot_sub["entropy_similarity"].max() 1872 ) 1873 ms2_annot_sub["ref_match_fract_max"] = ( 1874 ms2_annot_sub["ref_mz_in_query_fract"] 1875 == ms2_annot_sub["ref_mz_in_query_fract"].max() 1876 ) 1877 ms2_annot_sub["frag_max"] = ms2_annot_sub[ 1878 "query_frag_types" 1879 ].apply(lambda x: True if "MLF" in x else False) 1880 1881 # New column that looks if there is a consensus between the ranks (one row that is highest in all ranks) 1882 ms2_annot_sub["consensus"] = ms2_annot_sub[ 1883 ["entropy_max", "ref_match_fract_max", "frag_max"] 1884 ].all(axis=1) 1885 1886 # If there is a consensus, take the row with the highest entropy_similarity 1887 if ms2_annot_sub["consensus"].any(): 1888 ms2_annot_sub = ms2_annot_sub[ 1889 ms2_annot_sub["entropy_similarity"] 1890 == ms2_annot_sub["entropy_similarity"].max() 1891 ].head(1) 1892 mlf_results_perid.append(ms2_annot_sub) 1893 if len(mlf_results_perid) == 0: 1894 mlf_results_perid = pd.DataFrame() 1895 else: 1896 mlf_results_perid = pd.concat(mlf_results_perid) 1897 if mlf_results_perid["name"].nunique() == 1: 1898 mlf_results_perid = mlf_results_perid[ 1899 mlf_results_perid["entropy_similarity"] 1900 == mlf_results_perid["entropy_similarity"].max() 1901 ].head(1) 1902 else: 1903 mlf_results_perid = pd.DataFrame() 1904 mlf_results_all.append(mlf_results_perid) 1905 1906 # These are the consensus annotations to the MLF level 1907 if len(mlf_results_all) > 0: 1908 mlf_results_all = pd.concat(mlf_results_all) 1909 mlf_results_all["annot_level"] = mlf_results_all["structure_level"] 1910 else: 1911 # Make an empty dataframe 1912 mlf_results_all = ms2_annot.head(0) 1913 1914 # For remaining mf_ids, try to get a consensus annotation to the species level 1915 species_results_all = [] 1916 # Remove mf_ids that have consensus annotations to the MLF level 1917 ms2_annot_spec = ms2_annot[ 1918 ~ms2_annot["mf_id"].isin(mlf_results_all["mf_id"].unique()) 1919 ] 1920 for mf_id in ms2_annot_spec["mf_id"].unique(): 1921 # Do all the hits have the same lipid_summed_name? 1922 ms2_annot_sub = ms2_annot_spec[ms2_annot_spec["mf_id"] == mf_id].copy() 1923 ms2_annot_sub["n_spectra_contributing"] = len(ms2_annot_sub) 1924 1925 if ms2_annot_sub["lipid_summed_name"].nunique() == 1: 1926 # Grab the highest entropy_similarity result 1927 ms2_annot_sub = ms2_annot_sub[ 1928 ms2_annot_sub["entropy_similarity"] 1929 == ms2_annot_sub["entropy_similarity"].max() 1930 ].head(1) 1931 species_results_all.append(ms2_annot_sub) 1932 1933 # These are the consensus annotations to the species level 1934 if len(species_results_all) > 0: 1935 species_results_all = pd.concat(species_results_all) 1936 species_results_all["annot_level"] = "species" 1937 else: 1938 # Make an empty dataframe 1939 species_results_all = ms2_annot.head(0) 1940 1941 # Deal with the remaining mf_ids that do not have consensus annotations to the species level or MLF level 1942 # Remove mf_ids that have consensus annotations to the species level 1943 ms2_annot_remaining = ms2_annot_spec[ 1944 ~ms2_annot_spec["mf_id"].isin(species_results_all["mf_id"].unique()) 1945 ] 1946 no_consensus = [] 1947 for mf_id in ms2_annot_remaining["mf_id"].unique(): 1948 id_sub = [] 1949 id_no_con = [] 1950 ms2_annot_sub_mf = ms2_annot_remaining[ 1951 ms2_annot_remaining["mf_id"] == mf_id 1952 ].copy() 1953 for query_scan in ms2_annot_sub_mf["query_spectrum_id"].unique(): 1954 ms2_annot_sub = ms2_annot_sub_mf[ 1955 ms2_annot_sub_mf["query_spectrum_id"] == query_scan 1956 ].copy() 1957 1958 # New columns for ranking [HIGHER RANK = BETTER] 1959 ms2_annot_sub["entropy_max"] = ( 1960 ms2_annot_sub["entropy_similarity"] 1961 == ms2_annot_sub["entropy_similarity"].max() 1962 ) 1963 ms2_annot_sub["ref_match_fract_max"] = ( 1964 ms2_annot_sub["ref_mz_in_query_fract"] 1965 == ms2_annot_sub["ref_mz_in_query_fract"].max() 1966 ) 1967 ms2_annot_sub["frag_max"] = ms2_annot_sub["query_frag_types"].apply( 1968 lambda x: True if "MLF" in x else False 1969 ) 1970 1971 # New column that looks if there is a consensus between the ranks (one row that is highest in all ranks) 1972 ms2_annot_sub["consensus"] = ms2_annot_sub[ 1973 ["entropy_max", "ref_match_fract_max", "frag_max"] 1974 ].all(axis=1) 1975 ms2_annot_sub_con = ms2_annot_sub[ms2_annot_sub["consensus"]] 1976 id_sub.append(ms2_annot_sub_con) 1977 id_no_con.append(ms2_annot_sub) 1978 id_sub = pd.concat(id_sub) 1979 id_no_con = pd.concat(id_no_con) 1980 1981 # Scenario 1: Multiple scans are being resolved to different MLFs [could be coelutions and should both be kept and annotated to MS level] 1982 if ( 1983 id_sub["query_frag_types"] 1984 .apply(lambda x: True if "MLF" in x else False) 1985 .all() 1986 and len(id_sub) > 0 1987 ): 1988 idx = id_sub.groupby("name")["entropy_similarity"].idxmax() 1989 id_sub = id_sub.loc[idx] 1990 # Reorder so highest entropy_similarity is first 1991 id_sub = id_sub.sort_values("entropy_similarity", ascending=False) 1992 id_sub["annot_level"] = id_sub["structure_level"] 1993 no_consensus.append(id_sub) 1994 1995 # Scenario 2: Multiple scans are being resolved to different species, keep both and annotate to appropriate level 1996 elif len(id_sub) == 0: 1997 for lipid_summed_name in id_no_con["lipid_summed_name"].unique(): 1998 summed_sub = id_no_con[ 1999 id_no_con["lipid_summed_name"] == lipid_summed_name 2000 ] 2001 # Any consensus to MLF? 2002 if summed_sub["consensus"].any(): 2003 summed_sub = summed_sub[summed_sub["consensus"]] 2004 summed_sub["annot_level"] = summed_sub["structure_level"] 2005 no_consensus.append(summed_sub) 2006 else: 2007 # Grab the highest entropy_similarity, if there are multiple, grab the first one 2008 summed_sub = summed_sub[ 2009 summed_sub["entropy_similarity"] 2010 == summed_sub["entropy_similarity"].max() 2011 ].head(1) 2012 # get first row 2013 summed_sub["annot_level"] = "species" 2014 summed_sub["name"] = "" 2015 no_consensus.append(summed_sub) 2016 else: 2017 raise ValueError("Unexpected scenario for summarizing mf_id: ", mf_id) 2018 2019 if len(no_consensus) > 0: 2020 no_consensus = pd.concat(no_consensus) 2021 else: 2022 no_consensus = ms2_annot.head(0) 2023 2024 # Combine all the consensus annotations and reformat the dataframe for output 2025 species_results_all = species_results_all.drop(columns=["name"]) 2026 species_results_all["lipid_molecular_species_id"] = "" 2027 mlf_results_all["lipid_molecular_species_id"] = mlf_results_all["name"] 2028 no_consensus["lipid_molecular_species_id"] = no_consensus["name"] 2029 consensus_annotations = pd.concat( 2030 [mlf_results_all, species_results_all, no_consensus] 2031 ) 2032 consensus_annotations = consensus_annotations.sort_values( 2033 "mf_id", ascending=True 2034 ) 2035 cols_to_keep = [ 2036 "mf_id", 2037 "ref_ion_type", 2038 "entropy_similarity", 2039 "ref_mz_in_query_fract", 2040 "lipid_molecular_species_id", 2041 "lipid_summed_name", 2042 "lipid_subclass", 2043 "lipid_class", 2044 "lipid_category", 2045 "formula", 2046 "annot_level", 2047 "n_spectra_contributing", 2048 ] 2049 consensus_annotations = consensus_annotations[cols_to_keep] 2050 consensus_annotations = consensus_annotations.set_index("mf_id") 2051 2052 return consensus_annotations 2053 2054 def clean_ms2_report(self, lipid_summary): 2055 """Clean the MS2 report. 2056 2057 Parameters 2058 ---------- 2059 lipid_summary : DataFrame 2060 The full lipid summary DataFrame. 2061 2062 Returns 2063 ------- 2064 DataFrame 2065 The cleaned lipid summary DataFrame. 2066 """ 2067 lipid_summary = lipid_summary.reset_index() 2068 lipid_summary["ion_formula"] = [ 2069 self.get_ion_formula(f, a) 2070 for f, a in zip(lipid_summary["formula"], lipid_summary["ref_ion_type"]) 2071 ] 2072 2073 # Reorder columns 2074 lipid_summary = lipid_summary[ 2075 [ 2076 "mf_id", 2077 "ion_formula", 2078 "ref_ion_type", 2079 "formula", 2080 "annot_level", 2081 "lipid_molecular_species_id", 2082 "lipid_summed_name", 2083 "lipid_subclass", 2084 "lipid_class", 2085 "lipid_category", 2086 "entropy_similarity", 2087 "ref_mz_in_query_fract", 2088 "n_spectra_contributing", 2089 ] 2090 ] 2091 2092 # Set the index to mf_id 2093 lipid_summary = lipid_summary.set_index("mf_id") 2094 2095 return lipid_summary 2096 2097 def to_report(self, molecular_metadata=None): 2098 """Create a report of the mass features and their annotations. 2099 2100 Parameters 2101 ---------- 2102 molecular_metadata : dict, optional 2103 The molecular metadata. Default is None. 2104 2105 Returns 2106 ------- 2107 DataFrame 2108 The report of the mass features and their annotations. 2109 2110 Notes 2111 ----- 2112 The report will contain the mass features and their annotations from MS1 and MS2 (if available). 2113 """ 2114 # Get mass feature dataframe 2115 mf_report = self.mass_spectra.mass_features_to_df() 2116 mf_report = mf_report.reset_index(drop=False) 2117 2118 # Get and clean ms1 annotation dataframe 2119 ms1_annot_report = self.mass_spectra.mass_features_ms1_annot_to_df().copy() 2120 ms1_annot_report = self.clean_ms1_report(ms1_annot_report) 2121 ms1_annot_report = ms1_annot_report.reset_index(drop=False) 2122 2123 # Get, summarize, and clean ms2 annotation dataframe 2124 ms2_annot_report = self.mass_spectra.mass_features_ms2_annot_to_df( 2125 molecular_metadata=molecular_metadata 2126 ) 2127 if ms2_annot_report is not None and molecular_metadata is not None: 2128 ms2_annot_report = self.summarize_lipid_report(ms2_annot_report) 2129 ms2_annot_report = self.clean_ms2_report(ms2_annot_report) 2130 ms2_annot_report = ms2_annot_report.dropna(axis=1, how="all") 2131 ms2_annot_report = ms2_annot_report.reset_index(drop=False) 2132 report = self.combine_reports( 2133 mf_report=mf_report, 2134 ms1_annot_report=ms1_annot_report, 2135 ms2_annot_report=ms2_annot_report 2136 ) 2137 return report 2138 2139 2140class LCMSCollectionExport(): 2141 """A class to export an LCMS collection to HDF5 format. 2142 2143 This class provides methods to export collection-level data from multi-sample LC-MS 2144 experiments to HDF5 files. It handles the export of metadata, retention time alignments, 2145 cluster assignments, and induced mass features (gap-filled features) across the collection. 2146 2147 The exporter is designed to work with LCMSCollection objects and complements the individual 2148 LCMSExport class by focusing on collection-wide data rather than individual sample data. 2149 2150 Parameters 2151 ---------- 2152 out_file_path : str | Path 2153 The output file path, do not include the file extension. The .hdf5 extension 2154 will be added automatically. 2155 mass_spectra_collection : LCMSCollection 2156 The LCMS collection object containing multiple LCMS samples with processed mass features, 2157 alignments, and clustering information. 2158 2159 Attributes 2160 ---------- 2161 out_file_path : Path 2162 The output file path as a Path object. 2163 mass_spectra_collection : LCMSCollection 2164 The LCMS collection object to be exported. 2165 2166 Methods 2167 ------- 2168 export_to_hdf5(overwrite=False) 2169 Export the LCMS collection to an HDF5 file with collection-level data. 2170 2171 Notes 2172 ----- 2173 This class exports collection-level data including: 2174 - Sample manifest (metadata about all samples in the collection) 2175 - Retention time alignment data (if RT alignment has been performed) 2176 - Cluster assignments (consensus mass feature groupings across samples) 2177 - Induced mass features (gap-filled features saved to individual LCMS object HDF5 files) 2178 2179 Individual sample data (mass spectra, mass features, EICs, etc.) should be exported 2180 separately using the LCMSExport class for each LCMS object in the collection. 2181 2182 Examples 2183 -------- 2184 Export a collection after clustering and gap-filling: 2185 2186 >>> from corems.mass_spectra.output.export import LCMSCollectionExporter 2187 >>> exporter = LCMSCollectionExporter("my_collection", lcms_collection) 2188 >>> exporter.export_to_hdf5(overwrite=True) 2189 2190 The resulting HDF5 file will contain collection-level metadata and can be used 2191 to reconstruct the collection state for further analysis. 2192 2193 See Also 2194 -------- 2195 LCMSExport : Export individual LCMS objects to HDF5 2196 LCMSCollection : The collection object being exported 2197 """ 2198 def __init__(self, out_file_path, mass_spectra_collection): 2199 self.out_file_path = Path(out_file_path) 2200 self.mass_spectra_collection = mass_spectra_collection 2201 2202 def export_to_hdf5( 2203 self, 2204 overwrite = False, 2205 save_parameters=True, 2206 parameter_format="toml", 2207 update_lcms_objects=True): 2208 """Export the LCMS collection to an HDF5 file. 2209 2210 This method saves the collection-level data to an HDF5 file, including: 2211 - Basic metadata (date, folder location, gap-filling status) 2212 - Sample manifest 2213 - Retention time alignments (if available) 2214 - Cluster assignments (if available) 2215 - Induced mass features for each LCMS object (if gap-filling was performed) 2216 2217 Individual LCMS objects in the collection are not exported by this method. 2218 Use LCMSExport for exporting individual LCMS objects. 2219 2220 Parameters 2221 ---------- 2222 overwrite : bool, optional 2223 If True, overwrites the output file if it already exists and replaces 2224 existing groups within the HDF5 file. If False, appends new data to 2225 existing file without overwriting existing groups. Default is False. 2226 save_parameters : bool, optional 2227 If True, saves the collection-level parameters to a separate file in the specified format. 2228 Default is True. 2229 parameter_format : str, optional 2230 The format for saving parameters, either "json" or "toml". Default is "toml". 2231 update_lcms_objects : bool, optional 2232 If True, updates the individual LCMS object HDF5 files with new raw file locations and any additional 2233 information produced during the processing of the collection (e.g. cluster mass feature associations). Default is True. 2234 2235 Notes 2236 ----- 2237 The HDF5 file structure includes: 2238 - Attributes: date_utc, lcms_objects_folder, missing_mass_features_searched, manifest 2239 - Groups: rt_alignments, cluster_assignments (if available) 2240 2241 Induced mass features are saved to the individual LCMS object HDF5 files 2242 within the .corems folder structure, not in the collection-level HDF5 file. 2243 2244 Examples 2245 -------- 2246 >>> exporter = LCMSCollectionExporter("my_collection", lcms_collection) 2247 >>> exporter.export_to_hdf5(overwrite=True) 2248 """ 2249 if overwrite: 2250 if self.out_file_path.with_suffix(".hdf5").exists(): 2251 self.out_file_path.with_suffix(".hdf5").unlink() 2252 2253 with h5py.File(self.out_file_path.with_suffix(".hdf5"), "a") as hdf_handle: 2254 # Add basic attributes to the HDF5 file, always overwrite these 2255 timenow = str( 2256 datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M:%S %Z") 2257 ) 2258 hdf_handle.attrs["date_utc"] = timenow 2259 hdf_handle.attrs["lcms_objects_folder"] = str(self.mass_spectra_collection.collection_parser.folder_location) 2260 hdf_handle.attrs["missing_mass_features_searched"] = self.mass_spectra_collection.missing_mass_features_searched 2261 hdf_handle.attrs["rt_aligned"] = self.mass_spectra_collection.rt_aligned 2262 hdf_handle.attrs["rt_alignment_attempted"] = self.mass_spectra_collection.rt_alignment_attempted 2263 2264 # Add the manifest to the HDF5 file, always overwrite this 2265 hdf_handle.attrs["manifest"] = self._convert_manifest_to_json() 2266 2267 # Save retention time alignments if they exist, only overwrite if specified 2268 self._save_rt_alignments_to_hdf5(hdf_handle, overwrite) 2269 2270 # Save cluster assignments if they exist, only overwrite if specified 2271 self._save_cluster_assignments_to_hdf5(hdf_handle, overwrite) 2272 2273 # Save new raw file locations to each LCMS object's HDF5 file if needed 2274 if hasattr(self.mass_spectra_collection, 'raw_files_relocated') and self.mass_spectra_collection.raw_files_relocated: 2275 self._update_raw_file_locations_in_hdf5() 2276 2277 # Save induced mass features to the collection with associations to each individual, only if lcms_collection.missing_mass_features_searched is True 2278 if self.mass_spectra_collection.missing_mass_features_searched: 2279 self._save_induced_mass_features_to_hdf5(overwrite) 2280 # Save EICs for induced mass features at collection level 2281 self._save_induced_eics_to_hdf5(overwrite) 2282 2283 # Build cluster mass feature map to know which features to update 2284 # This uses the same logic as process_consensus_features to determine loaded features 2285 cluster_mf_map = self._build_cluster_mf_map() 2286 2287 # Save updated mass features for each LCMS object 2288 # This implements selective update: only loaded features are updated, non-cluster features are preserved 2289 if update_lcms_objects: 2290 self._save_lcms_objects_to_hdf5(cluster_mf_map, overwrite) 2291 2292 # Save collection-level parameters as separate file 2293 if save_parameters: 2294 # Check if parameter_format is valid 2295 if parameter_format not in ["json", "toml"]: 2296 raise ValueError("parameter_format must be 'json' or 'toml'") 2297 2298 if parameter_format == "json": 2299 dump_lcms_collection_settings_json( 2300 filename=self.out_file_path.with_suffix(".json"), 2301 lcms_collection=self.mass_spectra_collection, 2302 ) 2303 elif parameter_format == "toml": 2304 dump_lcms_collection_settings_toml( 2305 filename=self.out_file_path.with_suffix(".toml"), 2306 lcms_collection=self.mass_spectra_collection, 2307 ) 2308 2309 def _save_rt_alignments_to_hdf5(self, hdf_handle, overwrite): 2310 """Save retention time alignments to HDF5 file.""" 2311 # If no rt_alignments, return early 2312 if not self.mass_spectra_collection.rt_aligned: 2313 return 2314 2315 # If rt_alignments exist, save them 2316 if self.mass_spectra_collection.rt_aligned: 2317 group_name = "rt_alignments" 2318 # grab dictionary of rt_alignments 2319 rt_alignments = self.mass_spectra_collection.rt_alignments 2320 2321 if rt_alignments: 2322 # Check if group exists and handle overwrite logic 2323 if group_name in hdf_handle: 2324 if not overwrite: 2325 return 2326 del hdf_handle[group_name] 2327 2328 grp = hdf_handle.create_group(group_name) 2329 2330 # Save each alignment as a dataset 2331 for sample_idx, alignment_data in rt_alignments.items(): 2332 grp.create_dataset(str(sample_idx), data=alignment_data) 2333 2334 def _convert_manifest_to_json(self): 2335 """Clean the manifest for export to HDF5.""" 2336 manifest = self.mass_spectra_collection.collection_parser.manifest 2337 2338 # Process the manifest to convert numpy.bool_ or bool values for the 'use_rt_alignment' key 2339 def convert_bool_values(data): 2340 if isinstance(data, dict): 2341 # Process each key-value pair recursively 2342 return {k: (int(v) if k == 'use_rt_alignment' and isinstance(v, (bool, np.bool_)) else convert_bool_values(v)) for k, v in data.items()} 2343 elif isinstance(data, list): 2344 # Recursively process lists 2345 return [convert_bool_values(item) for item in data] 2346 else: 2347 # Return non-dict/list types unchanged 2348 return data 2349 2350 # Clean the whole manifest 2351 cleaned_manifest = convert_bool_values(manifest) 2352 2353 # Serialize the cleaned manifest into JSON format 2354 json_manifest = json.dumps(cleaned_manifest) 2355 return json_manifest 2356 2357 def _save_cluster_assignments_to_hdf5(self, hdf_handle, overwrite): 2358 """Save cluster assignments to HDF5 file.""" 2359 # Check if column "cluster" is present in self.mass_features_dataframe 2360 if "cluster" in self.mass_spectra_collection.mass_features_dataframe.columns: 2361 group_name = "cluster_assignments" 2362 cluster_assignments = self.mass_spectra_collection.mass_features_dataframe[["cluster"]].copy() 2363 2364 # Check if group exists and handle overwrite logic 2365 if group_name in hdf_handle: 2366 if not overwrite: 2367 return 2368 del hdf_handle[group_name] 2369 2370 grp = hdf_handle.create_group(group_name) 2371 2372 # Save the index, converting strings to bytes 2373 grp.create_dataset("index", data=cluster_assignments.index.astype(str).values.astype('S')) 2374 2375 # Save the "cluster" column 2376 grp.create_dataset("cluster", data=cluster_assignments["cluster"].values) 2377 2378 def _build_cluster_mf_map(self): 2379 """Build a mapping of which mass features should be saved for each sample. 2380 2381 This uses the same logic as process_consensus_features to determine which 2382 mass features were loaded and should be updated in HDF5 files. 2383 2384 Returns 2385 ------- 2386 dict 2387 Dictionary mapping sample_id to list of tuples (mf_id, cluster_id). 2388 Only includes samples that have loaded representative features. 2389 Returns empty dict if no clusters exist. 2390 2391 Notes 2392 ----- 2393 This follows the DRY principle by using the same get_sample_mf_map_for_representatives 2394 method used by process_consensus_features and ReadSavedLCMSCollection. 2395 """ 2396 # Check if clusters exist 2397 if "cluster" not in self.mass_spectra_collection.mass_features_dataframe.columns: 2398 return {} 2399 2400 # Check if cluster_summary_dataframe exists (needed by get_sample_mf_map_for_representatives) 2401 if not hasattr(self.mass_spectra_collection, 'cluster_summary_dataframe') or \ 2402 self.mass_spectra_collection.cluster_summary_dataframe is None: 2403 return {} 2404 2405 # Use the same DRY helper method that process_consensus_features uses 2406 # This ensures consistency across the codebase 2407 cluster_mf_map = self.mass_spectra_collection.get_sample_mf_map_for_representatives( 2408 include_cluster_id=True 2409 ) 2410 2411 return cluster_mf_map 2412 2413 def _update_raw_file_locations_in_hdf5(self): 2414 """Update raw file locations in each LCMS object's HDF5 file. 2415 2416 This method updates the 'original_file_location' attribute in each LCMS object's 2417 HDF5 file to reflect the new raw file location after files have been relocated. 2418 """ 2419 for lcms_obj in self.mass_spectra_collection: 2420 # Get the HDF5 file path for this LCMS object 2421 hdf5_path = lcms_obj.file_location.with_suffix('.hdf5') 2422 2423 if hdf5_path.exists(): 2424 with h5py.File(hdf5_path, 'a') as hdf_handle: 2425 # Update the original_file_location attribute 2426 if 'original_file_location' in hdf_handle.attrs: 2427 hdf_handle.attrs['original_file_location'] = str(lcms_obj.raw_file_location) 2428 # If the attribute does not exist, create it 2429 else: 2430 hdf_handle.attrs.create('original_file_location', str(lcms_obj.raw_file_location)) 2431 2432 def _save_induced_mass_features_to_hdf5(self, overwrite): 2433 """Save induced mass features to the collection HDF5 file. 2434 2435 Induced mass features are gap-filled features that only exist at the collection level. 2436 They are saved with full detail (all attributes and datasets) in the collection HDF5 file 2437 and distributed to individual LCMS objects when the collection is loaded. 2438 2439 The induced mass features are stored in the collection's induced_mass_features_dataframe 2440 and are regenerated as LCMSMassFeature objects for saving. 2441 2442 Parameters 2443 ---------- 2444 overwrite : bool 2445 If True, overwrites existing induced mass features group. If False, skips if group exists. 2446 """ 2447 # Check if we have any induced mass features to save 2448 if (self.mass_spectra_collection.induced_mass_features_dataframe is None or 2449 self.mass_spectra_collection.induced_mass_features_dataframe.empty): 2450 return 2451 2452 # Open the collection HDF5 file to save induced mass features 2453 with h5py.File(self.out_file_path.with_suffix(".hdf5"), "a") as hdf_handle: 2454 group_name = "induced_mass_features" 2455 2456 # Check if group exists and handle overwrite logic 2457 if group_name in hdf_handle: 2458 if not overwrite: 2459 return 2460 del hdf_handle[group_name] 2461 2462 # Create top-level group for induced mass features 2463 imf_group = hdf_handle.create_group(group_name) 2464 2465 # Get the induced mass features dataframe 2466 induced_df = self.mass_spectra_collection.induced_mass_features_dataframe 2467 2468 # Get unique sample IDs from the dataframe 2469 sample_ids = induced_df['sample_id'].unique() 2470 2471 # Iterate through each sample and save its induced mass features 2472 for sample_id in sample_ids: 2473 # Filter dataframe to this sample 2474 sample_df = induced_df[induced_df['sample_id'] == sample_id].copy() 2475 2476 if sample_df.empty: 2477 continue 2478 2479 # Regenerate mass features from the dataframe 2480 regenerated_features = self._regenerate_mass_features_from_sample_df( 2481 sample_df, sample_id 2482 ) 2483 2484 if not regenerated_features: 2485 continue 2486 2487 # Create a subgroup for this sample's induced mass features 2488 sample_group = imf_group.create_group(str(sample_id)) 2489 2490 # Use the static helper method from LCMSExport to save the mass features 2491 LCMSExport._save_mass_features_dict_to_hdf5( 2492 regenerated_features, 2493 sample_group, 2494 overwrite=overwrite 2495 ) 2496 2497 def _save_induced_eics_to_hdf5(self, overwrite): 2498 """Save EICs for induced mass features to the collection HDF5 file. 2499 2500 Induced mass features are gap-filled features created during process_consensus_features. 2501 Their associated EICs need to be saved at the collection level so they can be reloaded. 2502 2503 The induced mass features are identified from the collection's induced_mass_features_dataframe, 2504 and their EICs are retrieved from the individual LCMS objects. 2505 2506 Parameters 2507 ---------- 2508 overwrite : bool 2509 If True, overwrites existing induced EICs group. If False, skips if group exists. 2510 """ 2511 # Check if we have any induced mass features to save 2512 if (self.mass_spectra_collection.induced_mass_features_dataframe is None or 2513 self.mass_spectra_collection.induced_mass_features_dataframe.empty): 2514 return 2515 2516 # Open the collection HDF5 file to save induced EICs 2517 with h5py.File(self.out_file_path.with_suffix(".hdf5"), "a") as hdf_handle: 2518 group_name = "induced_eics" 2519 2520 # Check if group exists and handle overwrite logic 2521 if group_name in hdf_handle: 2522 if not overwrite: 2523 return 2524 del hdf_handle[group_name] 2525 2526 # Create top-level group for induced EICs 2527 induced_eics_group = hdf_handle.create_group(group_name) 2528 2529 # Get the induced mass features dataframe 2530 induced_df = self.mass_spectra_collection.induced_mass_features_dataframe 2531 2532 # Get unique sample IDs from the dataframe 2533 sample_ids = induced_df['sample_id'].unique() 2534 2535 # Iterate through each sample and save EICs for its induced mass features 2536 for sample_id in sample_ids: 2537 lcms_obj = self.mass_spectra_collection[sample_id] 2538 2539 # Filter dataframe to this sample 2540 sample_df = induced_df[induced_df['sample_id'] == sample_id].copy() 2541 2542 if sample_df.empty: 2543 continue 2544 2545 # Collect EICs for induced mass features using _eic_mz from dataframe 2546 induced_eics = {} 2547 for _, row in sample_df.iterrows(): 2548 # Get the EIC m/z from the dataframe 2549 eic_mz = row.get('_eic_mz') 2550 2551 if eic_mz is not None and pd.notna(eic_mz): 2552 # Try to get the EIC from the LCMS object 2553 if hasattr(lcms_obj, 'eics') and lcms_obj.eics and eic_mz in lcms_obj.eics: 2554 induced_eics[eic_mz] = lcms_obj.eics[eic_mz] 2555 2556 if not induced_eics: 2557 continue 2558 2559 # Create a subgroup for this sample's induced EICs 2560 sample_group = induced_eics_group.create_group(str(sample_id)) 2561 2562 # Use the static helper method from LCMSExport to save the EICs 2563 LCMSExport._save_eics_dict_to_hdf5(induced_eics, sample_group, overwrite) 2564 2565 def _regenerate_mass_features_from_sample_df(self, sample_df, sample_id): 2566 """Regenerate induced mass features from a sample-specific dataframe. 2567 2568 This method creates LCMSMassFeature objects from rows in the induced_mass_features_dataframe 2569 for a specific sample. The regenerated features are used for saving to HDF5. 2570 2571 Parameters 2572 ---------- 2573 sample_df : pd.DataFrame 2574 DataFrame containing induced mass features for a specific sample. 2575 sample_id : int 2576 The sample ID (index in the collection). 2577 2578 Returns 2579 ------- 2580 dict 2581 Dictionary of regenerated LCMSMassFeature objects keyed by feature ID. 2582 """ 2583 from corems.chroma_peak.factory.chroma_peak_classes import LCMSMassFeature 2584 2585 if sample_df.empty: 2586 return {} 2587 2588 # Get the corresponding LCMS object for proper parent reference 2589 lcms_obj = self.mass_spectra_collection[sample_id] 2590 2591 # Regenerate mass features from the dataframe 2592 regenerated_features = {} 2593 2594 for _, row in sample_df.iterrows(): 2595 # Extract the original ID from mf_id (format: c{cluster}_{index}_i) 2596 # This is the ID used in lcms_obj.induced_mass_features dict 2597 original_id = row['mf_id'] 2598 2599 # Create a new LCMSMassFeature with proper parent reference 2600 # Note: dataframe uses 'scan_time' but __init__ parameter is 'retention_time' 2601 mass_feature = LCMSMassFeature( 2602 lcms_parent=lcms_obj, 2603 mz=row['mz'], 2604 retention_time=row['scan_time'], # Column is 'scan_time' in dataframe 2605 intensity=row['intensity'], 2606 apex_scan=int(row['apex_scan']), 2607 persistence=row.get('persistence', None) if 'persistence' in row else None, 2608 id=original_id # Use the original string ID from gap-filling 2609 ) 2610 2611 # Set additional attributes dynamically from dataframe columns 2612 # Skip columns already handled in __init__ or structural metadata 2613 skip_cols = { 2614 'sample_id', 'mf_id', 'mz', 'scan_time', 'scan_time_aligned', 2615 'intensity', 'apex_scan', 'persistence'} 2616 2617 # Iterate through all columns and set via property setters 2618 for col_name in row.index: 2619 if col_name in skip_cols: 2620 continue 2621 value = row[col_name] 2622 try: 2623 if pd.isna(value): 2624 continue 2625 except (TypeError, ValueError): 2626 pass # value is array-like; not NA, proceed 2627 2628 # Set via property (public interface handles private attributes) 2629 # Don't save empty lists 2630 if isinstance(value, list) and len(value) == 0: 2631 continue 2632 try: 2633 setattr(mass_feature, col_name, value) 2634 except (AttributeError, TypeError): 2635 pass # Skip attributes that don't exist or can't be set 2636 2637 # Set cluster_index if present 2638 if 'cluster' in row and pd.notna(row['cluster']): 2639 mass_feature.cluster_index = int(row['cluster']) 2640 2641 regenerated_features[mass_feature.id] = mass_feature 2642 2643 return regenerated_features 2644 2645 def _save_lcms_objects_to_hdf5(self, cluster_mf_map, overwrite): 2646 """Save updated mass features for each LCMS object. 2647 2648 This method implements a "selective update" strategy for mass features: 2649 - For mass features specified in cluster_mf_map (loaded representatives), we selectively 2650 update them by deleting their old entries and re-saving with new attributes. 2651 - Non-cluster features (not loaded) are never touched/overwritten. 2652 2653 Note: EICs are NOT saved here. Induced feature EICs are saved at the collection level. 2654 2655 Parameters 2656 ---------- 2657 cluster_mf_map : dict 2658 Dictionary mapping sample_id to list of tuples (mf_id, cluster_id). 2659 This explicitly defines which mass features should be updated. 2660 overwrite : bool 2661 If True, allows overwriting of existing data. If False, skips if data exists. 2662 """ 2663 for sample_id, lcms_obj in enumerate(self.mass_spectra_collection): 2664 hdf5_path = lcms_obj.file_location.with_suffix('.hdf5') 2665 2666 if not hdf5_path.exists(): 2667 # If HDF5 doesn't exist, we can't do selective update, raise error 2668 raise FileNotFoundError( 2669 f"HDF5 file for LCMS object {lcms_obj.sample_name} not found at {hdf5_path}" 2670 ) 2671 2672 # Check if this sample has any loaded features in the map 2673 if sample_id not in cluster_mf_map or not cluster_mf_map[sample_id]: 2674 # Nothing loaded for this sample, nothing to update 2675 continue 2676 2677 # Extract mf_ids from the map (cluster_mf_map contains tuples of (mf_id, cluster_id)) 2678 mf_ids_to_update = [mf_id for mf_id, cluster_id in cluster_mf_map[sample_id]] 2679 2680 # Perform selective update of mass features 2681 self._selective_update_mass_features(lcms_obj, hdf5_path, mf_ids_to_update, overwrite) 2682 2683 # Save any new mass spectra that were added during processing 2684 self._save_new_mass_spectra(lcms_obj, hdf5_path, overwrite) 2685 2686 def _save_new_mass_spectra(self, lcms_obj, hdf5_path, overwrite): 2687 """Save new mass spectra that were added during processing. 2688 2689 This method checks what mass spectra are in lcms_obj._ms and saves any 2690 that aren't already in the HDF5 file's mass_spectra group. Uses the 2691 existing add_mass_spectrum_to_hdf5 method for consistency with original 2692 export logic. 2693 2694 Parameters 2695 ---------- 2696 lcms_obj : LCMSBase 2697 The LCMS object with potentially new mass spectra. 2698 hdf5_path : Path 2699 Path to the HDF5 file. 2700 overwrite : bool 2701 If True, allows overwriting existing spectra. 2702 """ 2703 # Check if there are any mass spectra to save 2704 if not hasattr(lcms_obj, '_ms') or not lcms_obj._ms: 2705 return 2706 2707 # Create an LCMS exporter instance for this LCMS object 2708 # This gives us access to add_mass_spectrum_to_hdf5 method inherited from HighResMassSpecExport 2709 # Turn hdf5_path into str without suffix for LCMSExport 2710 hdf5_path_str = str(hdf5_path.with_suffix('')) 2711 exporter = LCMSExport( 2712 out_file_path=hdf5_path_str, 2713 mass_spectra=lcms_obj 2714 ) 2715 2716 # Open HDF5 file and check existing mass spectra 2717 with h5py.File(hdf5_path, 'a') as hdf_handle: 2718 # Create mass_spectra group if it doesn't exist 2719 if 'mass_spectra' not in hdf_handle: 2720 ms_group = hdf_handle.create_group('mass_spectra') 2721 existing_scan_numbers = set() 2722 else: 2723 ms_group = hdf_handle['mass_spectra'] 2724 existing_scan_numbers = set(int(k) for k in ms_group.keys()) 2725 2726 # Find new mass spectra (in _ms but not in HDF5) 2727 new_scan_numbers = set(lcms_obj._ms.keys()) - existing_scan_numbers 2728 2729 if not new_scan_numbers: 2730 return 2731 2732 # Save new mass spectra using existing add_mass_spectrum_to_hdf5 method 2733 export_profile = lcms_obj.parameters.lc_ms.export_profile_spectra 2734 for scan_number in new_scan_numbers: 2735 mass_spec = lcms_obj._ms[scan_number] 2736 scan_group_name = str(scan_number) 2737 2738 # Delete existing group if overwrite is True 2739 if scan_group_name in ms_group and overwrite: 2740 del ms_group[scan_group_name] 2741 elif scan_group_name in ms_group: 2742 continue 2743 2744 # Use the existing method from HighResMassSpecExport 2745 exporter.add_mass_spectrum_to_hdf5( 2746 hdf_handle=hdf_handle, 2747 mass_spectrum=mass_spec, 2748 group_key=scan_group_name, 2749 mass_spectra_group=ms_group, 2750 export_raw=export_profile 2751 ) 2752 2753 def _selective_update_mass_features(self, lcms_obj, hdf5_path, mf_ids_to_update, overwrite): 2754 """Selectively update mass features in HDF5 file. 2755 2756 This method deletes only the mass features specified in mf_ids_to_update, 2757 then re-saves them with their potentially updated attributes. Non-cluster features 2758 in the HDF5 file are left untouched. 2759 2760 Parameters 2761 ---------- 2762 lcms_obj : LCMSBase 2763 The LCMS object with mass features to update. 2764 hdf5_path : Path 2765 Path to the HDF5 file. 2766 mf_ids_to_update : list of int 2767 List of mass feature IDs that should be updated. This explicitly defines 2768 which features were loaded and should be saved. 2769 overwrite : bool 2770 If True, allows overwriting. If False, skips if group exists. 2771 """ 2772 if not mf_ids_to_update: 2773 return 2774 2775 # Open HDF5 file and delete specified feature IDs, then re-save 2776 with h5py.File(hdf5_path, 'a') as hdf_handle: 2777 if 'mass_features' not in hdf_handle: 2778 return 2779 2780 mf_group = hdf_handle['mass_features'] 2781 2782 # Delete features that are being updated 2783 for feature_id in mf_ids_to_update: 2784 feature_id_str = str(feature_id) 2785 if feature_id_str in mf_group: 2786 del mf_group[feature_id_str] 2787 2788 # Re-save updated features (only those that exist in mass_features dict) 2789 updated_features = { 2790 mf.id: mf for mf in lcms_obj.mass_features.values() 2791 if mf.id in mf_ids_to_update 2792 } 2793 2794 if updated_features: 2795 LCMSExport._save_mass_features_dict_to_hdf5( 2796 updated_features, 2797 mf_group, 2798 overwrite=overwrite 2799 ) 2800
58class LowResGCMSExport: 59 """A class to export low resolution GC-MS data. 60 61 This class provides methods to export low resolution GC-MS data to various formats such as Excel, CSV, HDF5, and Pandas DataFrame. 62 63 Parameters: 64 ---------- 65 out_file_path : str 66 The output file path. 67 gcms : object 68 The low resolution GCMS object. 69 70 Attributes: 71 ---------- 72 output_file : Path 73 The output file path as a Path object. 74 gcms : object 75 The low resolution GCMS object. 76 77 Methods: 78 ------- 79 * get_pandas_df(id_label="corems:"). Get the exported data as a Pandas DataFrame. 80 * get_json(nan=False, id_label="corems:"). Get the exported data as a JSON string. 81 * to_pandas(write_metadata=True, id_label="corems:"). Export the data to a Pandas DataFrame and save it as a pickle file. 82 * to_excel(write_mode='a', write_metadata=True, id_label="corems:"), 83 Export the data to an Excel file. 84 * to_csv(separate_output=False, write_mode="w", write_metadata=True, id_label="corems:"). 85 Export the data to a CSV file. 86 * to_hdf(id_label="corems:"). 87 Export the data to an HDF5 file. 88 * get_data_stats(gcms). 89 Get statistics about the GCMS data. 90 91 """ 92 93 def __init__(self, out_file_path, gcms): 94 self.output_file = Path(out_file_path) 95 96 self.gcms = gcms 97 98 self._init_columns() 99 100 def _init_columns(self): 101 """Initialize the column names for the exported data. 102 103 Returns: 104 ------- 105 list 106 The list of column names. 107 """ 108 109 columns = [ 110 "Sample name", 111 "Peak Index", 112 "Retention Time", 113 "Retention Time Ref", 114 "Peak Height", 115 "Peak Area", 116 "Retention index", 117 "Retention index Ref", 118 "Retention Index Score", 119 "Similarity Score", 120 "Spectral Similarity Score", 121 "Compound Name", 122 "Chebi ID", 123 "Kegg Compound ID", 124 "Inchi", 125 "Inchi Key", 126 "Smiles", 127 "Molecular Formula", 128 "IUPAC Name", 129 "Traditional Name", 130 "Common Name", 131 "Derivatization", 132 ] 133 134 if self.gcms.molecular_search_settings.exploratory_mode: 135 columns.extend( 136 [ 137 "Weighted Cosine Correlation", 138 "Cosine Correlation", 139 "Stein Scott Similarity", 140 "Pearson Correlation", 141 "Spearman Correlation", 142 "Kendall Tau Correlation", 143 "Euclidean Distance", 144 "Manhattan Distance", 145 "Jaccard Distance", 146 "DWT Correlation", 147 "DFT Correlation", 148 ] 149 ) 150 151 columns.extend(list(methods_name.values())) 152 153 return columns 154 155 def get_pandas_df(self, id_label="corems:"): 156 """Get the exported data as a Pandas DataFrame. 157 158 Parameters: 159 ---------- 160 id_label : str, optional 161 The ID label for the data. Default is "corems:". 162 163 Returns: 164 ------- 165 DataFrame 166 The exported data as a Pandas DataFrame. 167 """ 168 169 columns = self._init_columns() 170 171 dict_data_list = self.get_list_dict_data(self.gcms) 172 173 df = DataFrame(dict_data_list, columns=columns) 174 175 df.name = self.gcms.sample_name 176 177 return df 178 179 def get_json(self, nan=False, id_label="corems:"): 180 """Get the exported data as a JSON string. 181 182 Parameters: 183 ---------- 184 nan : bool, optional 185 Whether to include NaN values in the JSON string. Default is False. 186 id_label : str, optional 187 The ID label for the data. Default is "corems:". 188 189 """ 190 191 import json 192 193 dict_data_list = self.get_list_dict_data(self.gcms) 194 195 return json.dumps( 196 dict_data_list, sort_keys=False, indent=4, separators=(",", ": ") 197 ) 198 199 def to_pandas(self, write_metadata=True, id_label="corems:"): 200 """Export the data to a Pandas DataFrame and save it as a pickle file. 201 202 Parameters: 203 ---------- 204 write_metadata : bool, optional 205 Whether to write metadata to the output file. 206 id_label : str, optional 207 The ID label for the data. 208 """ 209 210 columns = self._init_columns() 211 212 dict_data_list = self.get_list_dict_data(self.gcms) 213 214 df = DataFrame(dict_data_list, columns=columns) 215 216 df.to_pickle(self.output_file.with_suffix(".pkl")) 217 218 if write_metadata: 219 self.write_settings( 220 self.output_file.with_suffix(".pkl"), self.gcms, id_label="corems:" 221 ) 222 223 def to_excel(self, write_mode="a", write_metadata=True, id_label="corems:"): 224 """Export the data to an Excel file. 225 226 Parameters: 227 ---------- 228 write_mode : str, optional 229 The write mode for the Excel file. Default is 'a' (append). 230 write_metadata : bool, optional 231 Whether to write metadata to the output file. Default is True. 232 id_label : str, optional 233 The ID label for the data. Default is "corems:". 234 """ 235 236 out_put_path = self.output_file.with_suffix(".xlsx") 237 238 columns = self._init_columns() 239 240 dict_data_list = self.get_list_dict_data(self.gcms) 241 242 df = DataFrame(dict_data_list, columns=columns) 243 244 if write_mode == "a" and out_put_path.exists(): 245 writer = ExcelWriter(out_put_path, engine="openpyxl") 246 # try to open an existing workbook 247 writer.book = load_workbook(out_put_path) 248 # copy existing sheets 249 writer.sheets = dict((ws.title, ws) for ws in writer.book.worksheets) 250 # read existing file 251 reader = read_excel(out_put_path) 252 # write out the new sheet 253 df.to_excel(writer, index=False, header=False, startrow=len(reader) + 1) 254 255 writer.close() 256 else: 257 df.to_excel( 258 self.output_file.with_suffix(".xlsx"), index=False, engine="openpyxl" 259 ) 260 261 if write_metadata: 262 self.write_settings(out_put_path, self.gcms, id_label=id_label) 263 264 def to_csv( 265 self, 266 separate_output=False, 267 write_mode="w", 268 write_metadata=True, 269 id_label="corems:", 270 ): 271 """Export the data to a CSV file. 272 273 Parameters: 274 ---------- 275 separate_output : bool, optional 276 Whether to separate the output into multiple files. Default is False. 277 write_mode : str, optional 278 The write mode for the CSV file. Default is 'w' (write). 279 write_metadata : bool, optional 280 Whether to write metadata to the output file. Default is True. 281 id_label : str, optional 282 The ID label for the data. Default is "corems:". 283 """ 284 285 if separate_output: 286 # set write mode to write 287 # this mode will overwrite the file without warning 288 write_mode = "w" 289 else: 290 # set write mode to append 291 write_mode = "a" 292 293 columns = self._init_columns() 294 295 dict_data_list = self.get_list_dict_data(self.gcms) 296 297 out_put_path = self.output_file.with_suffix(".csv") 298 299 write_header = not out_put_path.exists() 300 301 try: 302 with open(out_put_path, write_mode, newline="") as csvfile: 303 writer = csv.DictWriter(csvfile, fieldnames=columns) 304 if write_header: 305 writer.writeheader() 306 for data in dict_data_list: 307 writer.writerow(data) 308 309 if write_metadata: 310 self.write_settings(out_put_path, self.gcms, id_label=id_label) 311 312 except IOError as ioerror: 313 print(ioerror) 314 315 def to_hdf(self, id_label="corems:"): 316 """Export the data to an HDF5 file. 317 318 Parameters: 319 ---------- 320 id_label : str, optional 321 The ID label for the data. Default is "corems:". 322 """ 323 324 # save sample at a time 325 def add_compound(gc_peak, compound_obj): 326 modifier = compound_obj.classify if compound_obj.classify else "" 327 compound_group = compound_obj.name.replace("/", "") + " " + modifier 328 329 if compound_group not in peak_group: 330 compound_group = peak_group.create_group(compound_group) 331 332 # compound_group.attrs["retention_time"] = compound_obj.retention_time 333 compound_group.attrs["retention_index"] = compound_obj.ri 334 compound_group.attrs["retention_index_score"] = compound_obj.ri_score 335 compound_group.attrs["spectral_similarity_score"] = ( 336 compound_obj.spectral_similarity_score 337 ) 338 compound_group.attrs["similarity_score"] = compound_obj.similarity_score 339 340 compond_mz = compound_group.create_dataset( 341 "mz", data=np.array(compound_obj.mz), dtype="f8" 342 ) 343 compond_abundance = compound_group.create_dataset( 344 "abundance", data=np.array(compound_obj.abundance), dtype="f8" 345 ) 346 347 if self.gcms.molecular_search_settings.exploratory_mode: 348 compound_group.attrs["Spectral Similarities"] = json.dumps( 349 compound_obj.spectral_similarity_scores, 350 sort_keys=False, 351 indent=4, 352 separators=(",", ":"), 353 ) 354 else: 355 warnings.warn("Skipping duplicate reference compound.") 356 357 import json 358 from datetime import datetime, timezone 359 360 import h5py 361 import numpy as np 362 363 output_path = self.output_file.with_suffix(".hdf5") 364 365 with h5py.File(output_path, "w") as hdf_handle: 366 timenow = str(datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M:%S %Z")) 367 hdf_handle.attrs["time_stamp"] = timenow 368 hdf_handle.attrs["data_structure"] = "gcms" 369 hdf_handle.attrs["analyzer"] = self.gcms.analyzer 370 hdf_handle.attrs["instrument_label"] = self.gcms.instrument_label 371 372 hdf_handle.attrs["sample_id"] = "self.gcms.id" 373 hdf_handle.attrs["sample_name"] = self.gcms.sample_name 374 hdf_handle.attrs["input_data"] = str(self.gcms.file_location) 375 hdf_handle.attrs["output_data"] = str(output_path) 376 hdf_handle.attrs["output_data_id"] = id_label + uuid.uuid4().hex 377 hdf_handle.attrs["corems_version"] = __version__ 378 379 hdf_handle.attrs["Stats"] = json.dumps( 380 self.get_data_stats(self.gcms), 381 sort_keys=False, 382 indent=4, 383 separators=(",", ": "), 384 ) 385 hdf_handle.attrs["Calibration"] = json.dumps( 386 self.get_calibration_stats(self.gcms, id_label), 387 sort_keys=False, 388 indent=4, 389 separators=(",", ": "), 390 ) 391 hdf_handle.attrs["Blank"] = json.dumps( 392 self.get_blank_stats(self.gcms), 393 sort_keys=False, 394 indent=4, 395 separators=(",", ": "), 396 ) 397 398 corems_dict_setting = parameter_to_dict.get_dict_data_gcms(self.gcms) 399 hdf_handle.attrs["CoreMSParameters"] = json.dumps( 400 corems_dict_setting, sort_keys=False, indent=4, separators=(",", ": ") 401 ) 402 403 scans_dataset = hdf_handle.create_dataset( 404 "scans", data=np.array(self.gcms.scans_number), dtype="f8" 405 ) 406 rt_dataset = hdf_handle.create_dataset( 407 "rt", data=np.array(self.gcms.retention_time), dtype="f8" 408 ) 409 tic_dataset = hdf_handle.create_dataset( 410 "tic", data=np.array(self.gcms.tic), dtype="f8" 411 ) 412 processed_tic_dataset = hdf_handle.create_dataset( 413 "processed_tic", data=np.array(self.gcms.processed_tic), dtype="f8" 414 ) 415 416 output_score_method = ( 417 self.gcms.molecular_search_settings.output_score_method 418 ) 419 420 for gc_peak in self.gcms: 421 # print(gc_peak.retention_time) 422 # print(gc_peak.tic) 423 424 # check if there is a compound candidate 425 peak_group = hdf_handle.create_group(str(gc_peak.retention_time)) 426 peak_group.attrs["deconvolution"] = int( 427 self.gcms.chromatogram_settings.use_deconvolution 428 ) 429 430 peak_group.attrs["start_scan"] = gc_peak.start_scan 431 peak_group.attrs["apex_scan"] = gc_peak.apex_scan 432 peak_group.attrs["final_scan"] = gc_peak.final_scan 433 434 peak_group.attrs["retention_index"] = gc_peak.ri 435 peak_group.attrs["retention_time"] = gc_peak.retention_time 436 peak_group.attrs["area"] = gc_peak.area 437 438 mz = peak_group.create_dataset( 439 "mz", data=np.array(gc_peak.mass_spectrum.mz_exp), dtype="f8" 440 ) 441 abundance = peak_group.create_dataset( 442 "abundance", 443 data=np.array(gc_peak.mass_spectrum.abundance), 444 dtype="f8", 445 ) 446 447 if gc_peak: 448 if output_score_method == "highest_sim_score": 449 compound_obj = gc_peak.highest_score_compound 450 add_compound(gc_peak, compound_obj) 451 452 elif output_score_method == "highest_ss": 453 compound_obj = gc_peak.highest_ss_compound 454 add_compound(gc_peak, compound_obj) 455 456 else: 457 for compound_obj in gc_peak: 458 add_compound(gc_peak, compound_obj) 459 460 def get_data_stats(self, gcms): 461 """Get statistics about the GCMS data. 462 463 Parameters: 464 ---------- 465 gcms : object 466 The low resolution GCMS object. 467 468 Returns: 469 ------- 470 dict 471 A dictionary containing the data statistics. 472 """ 473 474 matched_peaks = gcms.matched_peaks 475 no_matched_peaks = gcms.no_matched_peaks 476 unique_metabolites = gcms.unique_metabolites 477 478 peak_matchs_above_0p85 = 0 479 unique_peak_match_above_0p85 = 0 480 for match_peak in matched_peaks: 481 gc_peak_above_85 = 0 482 matches_above_85 = list( 483 filter(lambda m: m.similarity_score >= 0.85, match_peak) 484 ) 485 if matches_above_85: 486 peak_matchs_above_0p85 += 1 487 if len(matches_above_85) == 1: 488 unique_peak_match_above_0p85 += 1 489 490 data_stats = {} 491 data_stats["average_signal_noise"] = "ni" 492 data_stats["chromatogram_dynamic_range"] = gcms.dynamic_range 493 data_stats["total_number_peaks"] = len(gcms) 494 data_stats["total_peaks_matched"] = len(matched_peaks) 495 data_stats["total_peaks_without_matches"] = len(no_matched_peaks) 496 data_stats["total_matches_above_similarity_score_0.85"] = peak_matchs_above_0p85 497 data_stats["single_matches_above_similarity_score_0.85"] = ( 498 unique_peak_match_above_0p85 499 ) 500 data_stats["unique_metabolites"] = len(unique_metabolites) 501 502 return data_stats 503 504 def get_calibration_stats(self, gcms, id_label): 505 """Get statistics about the GC-MS calibration. 506 507 Parameters: 508 ---------- 509 """ 510 calibration_parameters = {} 511 512 calibration_parameters["calibration_rt_ri_pairs_ref"] = gcms.ri_pairs_ref 513 calibration_parameters["data_url"] = str(gcms.cal_file_path) 514 calibration_parameters["has_input"] = id_label + corems_md5(gcms.cal_file_path) 515 calibration_parameters["data_name"] = str(gcms.cal_file_path.stem) 516 calibration_parameters["calibration_method"] = "" 517 518 return calibration_parameters 519 520 def get_blank_stats(self, gcms): 521 """Get statistics about the GC-MS blank.""" 522 blank_parameters = {} 523 524 blank_parameters["data_name"] = "ni" 525 blank_parameters["blank_id"] = "ni" 526 blank_parameters["data_url"] = "ni" 527 blank_parameters["has_input"] = "ni" 528 blank_parameters["common_features_to_blank"] = "ni" 529 530 return blank_parameters 531 532 def get_instrument_metadata(self, gcms): 533 """Get metadata about the GC-MS instrument.""" 534 instrument_metadata = {} 535 536 instrument_metadata["analyzer"] = gcms.analyzer 537 instrument_metadata["instrument_label"] = gcms.instrument_label 538 instrument_metadata["instrument_id"] = uuid.uuid4().hex 539 540 return instrument_metadata 541 542 def get_data_metadata(self, gcms, id_label, output_path): 543 """Get metadata about the GC-MS data. 544 545 Parameters: 546 ---------- 547 gcms : object 548 The low resolution GCMS object. 549 id_label : str 550 The ID label for the data. 551 output_path : str 552 The output file path. 553 554 Returns: 555 ------- 556 dict 557 A dictionary containing the data metadata. 558 """ 559 if isinstance(output_path, str): 560 output_path = Path(output_path) 561 562 paramaters_path = output_path.with_suffix(".json") 563 564 if paramaters_path.exists(): 565 with paramaters_path.open() as current_param: 566 metadata = json.load(current_param) 567 data_metadata = metadata.get("Data") 568 else: 569 data_metadata = {} 570 data_metadata["data_name"] = [] 571 data_metadata["input_data_url"] = [] 572 data_metadata["has_input"] = [] 573 574 data_metadata["data_name"].append(gcms.sample_name) 575 data_metadata["input_data_url"].append(str(gcms.file_location)) 576 data_metadata["has_input"].append(id_label + corems_md5(gcms.file_location)) 577 578 data_metadata["output_data_name"] = str(output_path.stem) 579 data_metadata["output_data_url"] = str(output_path) 580 data_metadata["has_output"] = id_label + corems_md5(output_path) 581 582 return data_metadata 583 584 def get_parameters_json(self, gcms, id_label, output_path): 585 """Get the parameters as a JSON string. 586 587 Parameters: 588 ---------- 589 gcms : GCMS object 590 The low resolution GCMS object. 591 id_label : str 592 The ID label for the data. 593 output_path : str 594 The output file path. 595 596 Returns: 597 ------- 598 str 599 The parameters as a JSON string. 600 """ 601 602 output_parameters_dict = {} 603 output_parameters_dict["Data"] = self.get_data_metadata( 604 gcms, id_label, output_path 605 ) 606 output_parameters_dict["Stats"] = self.get_data_stats(gcms) 607 output_parameters_dict["Calibration"] = self.get_calibration_stats( 608 gcms, id_label 609 ) 610 output_parameters_dict["Blank"] = self.get_blank_stats(gcms) 611 output_parameters_dict["Instrument"] = self.get_instrument_metadata(gcms) 612 corems_dict_setting = parameter_to_dict.get_dict_data_gcms(gcms) 613 corems_dict_setting["corems_version"] = __version__ 614 output_parameters_dict["CoreMSParameters"] = corems_dict_setting 615 output_parameters_dict["has_metabolite"] = gcms.metabolites_data 616 output = json.dumps( 617 output_parameters_dict, sort_keys=False, indent=4, separators=(",", ": ") 618 ) 619 620 return output 621 622 def write_settings(self, output_path, gcms, id_label="emsl:"): 623 """Write the settings to a JSON file. 624 625 Parameters: 626 ---------- 627 output_path : str 628 The output file path. 629 gcms : GCMS object 630 The low resolution GCMS object. 631 id_label : str 632 The ID label for the data. Default is "emsl:". 633 634 """ 635 636 output = self.get_parameters_json(gcms, id_label, output_path) 637 638 with open( 639 output_path.with_suffix(".json"), 640 "w", 641 encoding="utf8", 642 ) as outfile: 643 outfile.write(output) 644 645 def get_list_dict_data(self, gcms, include_no_match=True, no_match_inline=False): 646 """Get the exported data as a list of dictionaries. 647 648 Parameters: 649 ---------- 650 gcms : object 651 The low resolution GCMS object. 652 include_no_match : bool, optional 653 Whether to include no match data. Default is True. 654 no_match_inline : bool, optional 655 Whether to include no match data inline. Default is False. 656 657 Returns: 658 ------- 659 list 660 The exported data as a list of dictionaries. 661 """ 662 663 output_score_method = gcms.molecular_search_settings.output_score_method 664 665 dict_data_list = [] 666 667 def add_match_dict_data(): 668 derivatization = "{}:{}:{}".format( 669 compound_obj.classify, 670 compound_obj.derivativenum, 671 compound_obj.derivatization, 672 ) 673 out_dict = { 674 "Sample name": gcms.sample_name, 675 "Peak Index": gcpeak_index, 676 "Retention Time": gc_peak.retention_time, 677 "Retention Time Ref": compound_obj.retention_time, 678 "Peak Height": gc_peak.tic, 679 "Peak Area": gc_peak.area, 680 "Retention index": gc_peak.ri, 681 "Retention index Ref": compound_obj.ri, 682 "Retention Index Score": compound_obj.ri_score, 683 "Spectral Similarity Score": compound_obj.spectral_similarity_score, 684 "Similarity Score": compound_obj.similarity_score, 685 "Compound Name": compound_obj.name, 686 "Chebi ID": compound_obj.metadata.chebi, 687 "Kegg Compound ID": compound_obj.metadata.kegg, 688 "Inchi": compound_obj.metadata.inchi, 689 "Inchi Key": compound_obj.metadata.inchikey, 690 "Smiles": compound_obj.metadata.smiles, 691 "Molecular Formula": compound_obj.formula, 692 "IUPAC Name": compound_obj.metadata.iupac_name, 693 "Traditional Name": compound_obj.metadata.traditional_name, 694 "Common Name": compound_obj.metadata.common_name, 695 "Derivatization": derivatization, 696 } 697 698 if self.gcms.molecular_search_settings.exploratory_mode: 699 out_dict.update( 700 { 701 "Weighted Cosine Correlation": compound_obj.spectral_similarity_scores.get( 702 "weighted_cosine_correlation" 703 ), 704 "Cosine Correlation": compound_obj.spectral_similarity_scores.get( 705 "cosine_correlation" 706 ), 707 "Stein Scott Similarity": compound_obj.spectral_similarity_scores.get( 708 "stein_scott_similarity" 709 ), 710 "Pearson Correlation": compound_obj.spectral_similarity_scores.get( 711 "pearson_correlation" 712 ), 713 "Spearman Correlation": compound_obj.spectral_similarity_scores.get( 714 "spearman_correlation" 715 ), 716 "Kendall Tau Correlation": compound_obj.spectral_similarity_scores.get( 717 "kendall_tau_correlation" 718 ), 719 "DFT Correlation": compound_obj.spectral_similarity_scores.get( 720 "dft_correlation" 721 ), 722 "DWT Correlation": compound_obj.spectral_similarity_scores.get( 723 "dwt_correlation" 724 ), 725 "Euclidean Distance": compound_obj.spectral_similarity_scores.get( 726 "euclidean_distance" 727 ), 728 "Manhattan Distance": compound_obj.spectral_similarity_scores.get( 729 "manhattan_distance" 730 ), 731 "Jaccard Distance": compound_obj.spectral_similarity_scores.get( 732 "jaccard_distance" 733 ), 734 } 735 ) 736 for method in methods_name: 737 out_dict[methods_name.get(method)] = ( 738 compound_obj.spectral_similarity_scores.get(method) 739 ) 740 741 dict_data_list.append(out_dict) 742 743 def add_no_match_dict_data(): 744 dict_data_list.append( 745 { 746 "Sample name": gcms.sample_name, 747 "Peak Index": gcpeak_index, 748 "Retention Time": gc_peak.retention_time, 749 "Peak Height": gc_peak.tic, 750 "Peak Area": gc_peak.area, 751 "Retention index": gc_peak.ri, 752 } 753 ) 754 755 for gcpeak_index, gc_peak in enumerate(gcms.sorted_gcpeaks): 756 # check if there is a compound candidate 757 if gc_peak: 758 if output_score_method == "highest_sim_score": 759 compound_obj = gc_peak.highest_score_compound 760 add_match_dict_data() 761 762 elif output_score_method == "highest_ss": 763 compound_obj = gc_peak.highest_ss_compound 764 add_match_dict_data() 765 766 else: 767 for compound_obj in gc_peak: 768 add_match_dict_data() # add monoisotopic peak 769 770 else: 771 # include not_match 772 if include_no_match and no_match_inline: 773 add_no_match_dict_data() 774 775 if include_no_match and not no_match_inline: 776 for gcpeak_index, gc_peak in enumerate(gcms.sorted_gcpeaks): 777 if not gc_peak: 778 add_no_match_dict_data() 779 780 return dict_data_list
A class to export low resolution GC-MS data.
This class provides methods to export low resolution GC-MS data to various formats such as Excel, CSV, HDF5, and Pandas DataFrame.
Parameters:
out_file_path : str The output file path. gcms : object The low resolution GCMS object.
Attributes:
output_file : Path The output file path as a Path object. gcms : object The low resolution GCMS object.
Methods:
- get_pandas_df(id_label="corems:"). Get the exported data as a Pandas DataFrame.
- get_json(nan=False, id_label="corems:"). Get the exported data as a JSON string.
- to_pandas(write_metadata=True, id_label="corems:"). Export the data to a Pandas DataFrame and save it as a pickle file.
- to_excel(write_mode='a', write_metadata=True, id_label="corems:"), Export the data to an Excel file.
- to_csv(separate_output=False, write_mode="w", write_metadata=True, id_label="corems:"). Export the data to a CSV file.
- to_hdf(id_label="corems:"). Export the data to an HDF5 file.
- get_data_stats(gcms). Get statistics about the GCMS data.
155 def get_pandas_df(self, id_label="corems:"): 156 """Get the exported data as a Pandas DataFrame. 157 158 Parameters: 159 ---------- 160 id_label : str, optional 161 The ID label for the data. Default is "corems:". 162 163 Returns: 164 ------- 165 DataFrame 166 The exported data as a Pandas DataFrame. 167 """ 168 169 columns = self._init_columns() 170 171 dict_data_list = self.get_list_dict_data(self.gcms) 172 173 df = DataFrame(dict_data_list, columns=columns) 174 175 df.name = self.gcms.sample_name 176 177 return df
Get the exported data as a Pandas DataFrame.
Parameters:
id_label : str, optional The ID label for the data. Default is "corems:".
Returns:
DataFrame The exported data as a Pandas DataFrame.
179 def get_json(self, nan=False, id_label="corems:"): 180 """Get the exported data as a JSON string. 181 182 Parameters: 183 ---------- 184 nan : bool, optional 185 Whether to include NaN values in the JSON string. Default is False. 186 id_label : str, optional 187 The ID label for the data. Default is "corems:". 188 189 """ 190 191 import json 192 193 dict_data_list = self.get_list_dict_data(self.gcms) 194 195 return json.dumps( 196 dict_data_list, sort_keys=False, indent=4, separators=(",", ": ") 197 )
Get the exported data as a JSON string.
Parameters:
nan : bool, optional Whether to include NaN values in the JSON string. Default is False. id_label : str, optional The ID label for the data. Default is "corems:".
199 def to_pandas(self, write_metadata=True, id_label="corems:"): 200 """Export the data to a Pandas DataFrame and save it as a pickle file. 201 202 Parameters: 203 ---------- 204 write_metadata : bool, optional 205 Whether to write metadata to the output file. 206 id_label : str, optional 207 The ID label for the data. 208 """ 209 210 columns = self._init_columns() 211 212 dict_data_list = self.get_list_dict_data(self.gcms) 213 214 df = DataFrame(dict_data_list, columns=columns) 215 216 df.to_pickle(self.output_file.with_suffix(".pkl")) 217 218 if write_metadata: 219 self.write_settings( 220 self.output_file.with_suffix(".pkl"), self.gcms, id_label="corems:" 221 )
Export the data to a Pandas DataFrame and save it as a pickle file.
Parameters:
write_metadata : bool, optional Whether to write metadata to the output file. id_label : str, optional The ID label for the data.
223 def to_excel(self, write_mode="a", write_metadata=True, id_label="corems:"): 224 """Export the data to an Excel file. 225 226 Parameters: 227 ---------- 228 write_mode : str, optional 229 The write mode for the Excel file. Default is 'a' (append). 230 write_metadata : bool, optional 231 Whether to write metadata to the output file. Default is True. 232 id_label : str, optional 233 The ID label for the data. Default is "corems:". 234 """ 235 236 out_put_path = self.output_file.with_suffix(".xlsx") 237 238 columns = self._init_columns() 239 240 dict_data_list = self.get_list_dict_data(self.gcms) 241 242 df = DataFrame(dict_data_list, columns=columns) 243 244 if write_mode == "a" and out_put_path.exists(): 245 writer = ExcelWriter(out_put_path, engine="openpyxl") 246 # try to open an existing workbook 247 writer.book = load_workbook(out_put_path) 248 # copy existing sheets 249 writer.sheets = dict((ws.title, ws) for ws in writer.book.worksheets) 250 # read existing file 251 reader = read_excel(out_put_path) 252 # write out the new sheet 253 df.to_excel(writer, index=False, header=False, startrow=len(reader) + 1) 254 255 writer.close() 256 else: 257 df.to_excel( 258 self.output_file.with_suffix(".xlsx"), index=False, engine="openpyxl" 259 ) 260 261 if write_metadata: 262 self.write_settings(out_put_path, self.gcms, id_label=id_label)
Export the data to an Excel file.
Parameters:
write_mode : str, optional The write mode for the Excel file. Default is 'a' (append). write_metadata : bool, optional Whether to write metadata to the output file. Default is True. id_label : str, optional The ID label for the data. Default is "corems:".
264 def to_csv( 265 self, 266 separate_output=False, 267 write_mode="w", 268 write_metadata=True, 269 id_label="corems:", 270 ): 271 """Export the data to a CSV file. 272 273 Parameters: 274 ---------- 275 separate_output : bool, optional 276 Whether to separate the output into multiple files. Default is False. 277 write_mode : str, optional 278 The write mode for the CSV file. Default is 'w' (write). 279 write_metadata : bool, optional 280 Whether to write metadata to the output file. Default is True. 281 id_label : str, optional 282 The ID label for the data. Default is "corems:". 283 """ 284 285 if separate_output: 286 # set write mode to write 287 # this mode will overwrite the file without warning 288 write_mode = "w" 289 else: 290 # set write mode to append 291 write_mode = "a" 292 293 columns = self._init_columns() 294 295 dict_data_list = self.get_list_dict_data(self.gcms) 296 297 out_put_path = self.output_file.with_suffix(".csv") 298 299 write_header = not out_put_path.exists() 300 301 try: 302 with open(out_put_path, write_mode, newline="") as csvfile: 303 writer = csv.DictWriter(csvfile, fieldnames=columns) 304 if write_header: 305 writer.writeheader() 306 for data in dict_data_list: 307 writer.writerow(data) 308 309 if write_metadata: 310 self.write_settings(out_put_path, self.gcms, id_label=id_label) 311 312 except IOError as ioerror: 313 print(ioerror)
Export the data to a CSV file.
Parameters:
separate_output : bool, optional Whether to separate the output into multiple files. Default is False. write_mode : str, optional The write mode for the CSV file. Default is 'w' (write). write_metadata : bool, optional Whether to write metadata to the output file. Default is True. id_label : str, optional The ID label for the data. Default is "corems:".
315 def to_hdf(self, id_label="corems:"): 316 """Export the data to an HDF5 file. 317 318 Parameters: 319 ---------- 320 id_label : str, optional 321 The ID label for the data. Default is "corems:". 322 """ 323 324 # save sample at a time 325 def add_compound(gc_peak, compound_obj): 326 modifier = compound_obj.classify if compound_obj.classify else "" 327 compound_group = compound_obj.name.replace("/", "") + " " + modifier 328 329 if compound_group not in peak_group: 330 compound_group = peak_group.create_group(compound_group) 331 332 # compound_group.attrs["retention_time"] = compound_obj.retention_time 333 compound_group.attrs["retention_index"] = compound_obj.ri 334 compound_group.attrs["retention_index_score"] = compound_obj.ri_score 335 compound_group.attrs["spectral_similarity_score"] = ( 336 compound_obj.spectral_similarity_score 337 ) 338 compound_group.attrs["similarity_score"] = compound_obj.similarity_score 339 340 compond_mz = compound_group.create_dataset( 341 "mz", data=np.array(compound_obj.mz), dtype="f8" 342 ) 343 compond_abundance = compound_group.create_dataset( 344 "abundance", data=np.array(compound_obj.abundance), dtype="f8" 345 ) 346 347 if self.gcms.molecular_search_settings.exploratory_mode: 348 compound_group.attrs["Spectral Similarities"] = json.dumps( 349 compound_obj.spectral_similarity_scores, 350 sort_keys=False, 351 indent=4, 352 separators=(",", ":"), 353 ) 354 else: 355 warnings.warn("Skipping duplicate reference compound.") 356 357 import json 358 from datetime import datetime, timezone 359 360 import h5py 361 import numpy as np 362 363 output_path = self.output_file.with_suffix(".hdf5") 364 365 with h5py.File(output_path, "w") as hdf_handle: 366 timenow = str(datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M:%S %Z")) 367 hdf_handle.attrs["time_stamp"] = timenow 368 hdf_handle.attrs["data_structure"] = "gcms" 369 hdf_handle.attrs["analyzer"] = self.gcms.analyzer 370 hdf_handle.attrs["instrument_label"] = self.gcms.instrument_label 371 372 hdf_handle.attrs["sample_id"] = "self.gcms.id" 373 hdf_handle.attrs["sample_name"] = self.gcms.sample_name 374 hdf_handle.attrs["input_data"] = str(self.gcms.file_location) 375 hdf_handle.attrs["output_data"] = str(output_path) 376 hdf_handle.attrs["output_data_id"] = id_label + uuid.uuid4().hex 377 hdf_handle.attrs["corems_version"] = __version__ 378 379 hdf_handle.attrs["Stats"] = json.dumps( 380 self.get_data_stats(self.gcms), 381 sort_keys=False, 382 indent=4, 383 separators=(",", ": "), 384 ) 385 hdf_handle.attrs["Calibration"] = json.dumps( 386 self.get_calibration_stats(self.gcms, id_label), 387 sort_keys=False, 388 indent=4, 389 separators=(",", ": "), 390 ) 391 hdf_handle.attrs["Blank"] = json.dumps( 392 self.get_blank_stats(self.gcms), 393 sort_keys=False, 394 indent=4, 395 separators=(",", ": "), 396 ) 397 398 corems_dict_setting = parameter_to_dict.get_dict_data_gcms(self.gcms) 399 hdf_handle.attrs["CoreMSParameters"] = json.dumps( 400 corems_dict_setting, sort_keys=False, indent=4, separators=(",", ": ") 401 ) 402 403 scans_dataset = hdf_handle.create_dataset( 404 "scans", data=np.array(self.gcms.scans_number), dtype="f8" 405 ) 406 rt_dataset = hdf_handle.create_dataset( 407 "rt", data=np.array(self.gcms.retention_time), dtype="f8" 408 ) 409 tic_dataset = hdf_handle.create_dataset( 410 "tic", data=np.array(self.gcms.tic), dtype="f8" 411 ) 412 processed_tic_dataset = hdf_handle.create_dataset( 413 "processed_tic", data=np.array(self.gcms.processed_tic), dtype="f8" 414 ) 415 416 output_score_method = ( 417 self.gcms.molecular_search_settings.output_score_method 418 ) 419 420 for gc_peak in self.gcms: 421 # print(gc_peak.retention_time) 422 # print(gc_peak.tic) 423 424 # check if there is a compound candidate 425 peak_group = hdf_handle.create_group(str(gc_peak.retention_time)) 426 peak_group.attrs["deconvolution"] = int( 427 self.gcms.chromatogram_settings.use_deconvolution 428 ) 429 430 peak_group.attrs["start_scan"] = gc_peak.start_scan 431 peak_group.attrs["apex_scan"] = gc_peak.apex_scan 432 peak_group.attrs["final_scan"] = gc_peak.final_scan 433 434 peak_group.attrs["retention_index"] = gc_peak.ri 435 peak_group.attrs["retention_time"] = gc_peak.retention_time 436 peak_group.attrs["area"] = gc_peak.area 437 438 mz = peak_group.create_dataset( 439 "mz", data=np.array(gc_peak.mass_spectrum.mz_exp), dtype="f8" 440 ) 441 abundance = peak_group.create_dataset( 442 "abundance", 443 data=np.array(gc_peak.mass_spectrum.abundance), 444 dtype="f8", 445 ) 446 447 if gc_peak: 448 if output_score_method == "highest_sim_score": 449 compound_obj = gc_peak.highest_score_compound 450 add_compound(gc_peak, compound_obj) 451 452 elif output_score_method == "highest_ss": 453 compound_obj = gc_peak.highest_ss_compound 454 add_compound(gc_peak, compound_obj) 455 456 else: 457 for compound_obj in gc_peak: 458 add_compound(gc_peak, compound_obj)
Export the data to an HDF5 file.
Parameters:
id_label : str, optional The ID label for the data. Default is "corems:".
460 def get_data_stats(self, gcms): 461 """Get statistics about the GCMS data. 462 463 Parameters: 464 ---------- 465 gcms : object 466 The low resolution GCMS object. 467 468 Returns: 469 ------- 470 dict 471 A dictionary containing the data statistics. 472 """ 473 474 matched_peaks = gcms.matched_peaks 475 no_matched_peaks = gcms.no_matched_peaks 476 unique_metabolites = gcms.unique_metabolites 477 478 peak_matchs_above_0p85 = 0 479 unique_peak_match_above_0p85 = 0 480 for match_peak in matched_peaks: 481 gc_peak_above_85 = 0 482 matches_above_85 = list( 483 filter(lambda m: m.similarity_score >= 0.85, match_peak) 484 ) 485 if matches_above_85: 486 peak_matchs_above_0p85 += 1 487 if len(matches_above_85) == 1: 488 unique_peak_match_above_0p85 += 1 489 490 data_stats = {} 491 data_stats["average_signal_noise"] = "ni" 492 data_stats["chromatogram_dynamic_range"] = gcms.dynamic_range 493 data_stats["total_number_peaks"] = len(gcms) 494 data_stats["total_peaks_matched"] = len(matched_peaks) 495 data_stats["total_peaks_without_matches"] = len(no_matched_peaks) 496 data_stats["total_matches_above_similarity_score_0.85"] = peak_matchs_above_0p85 497 data_stats["single_matches_above_similarity_score_0.85"] = ( 498 unique_peak_match_above_0p85 499 ) 500 data_stats["unique_metabolites"] = len(unique_metabolites) 501 502 return data_stats
Get statistics about the GCMS data.
Parameters:
gcms : object The low resolution GCMS object.
Returns:
dict A dictionary containing the data statistics.
504 def get_calibration_stats(self, gcms, id_label): 505 """Get statistics about the GC-MS calibration. 506 507 Parameters: 508 ---------- 509 """ 510 calibration_parameters = {} 511 512 calibration_parameters["calibration_rt_ri_pairs_ref"] = gcms.ri_pairs_ref 513 calibration_parameters["data_url"] = str(gcms.cal_file_path) 514 calibration_parameters["has_input"] = id_label + corems_md5(gcms.cal_file_path) 515 calibration_parameters["data_name"] = str(gcms.cal_file_path.stem) 516 calibration_parameters["calibration_method"] = "" 517 518 return calibration_parameters
Get statistics about the GC-MS calibration.
Parameters:
520 def get_blank_stats(self, gcms): 521 """Get statistics about the GC-MS blank.""" 522 blank_parameters = {} 523 524 blank_parameters["data_name"] = "ni" 525 blank_parameters["blank_id"] = "ni" 526 blank_parameters["data_url"] = "ni" 527 blank_parameters["has_input"] = "ni" 528 blank_parameters["common_features_to_blank"] = "ni" 529 530 return blank_parameters
Get statistics about the GC-MS blank.
532 def get_instrument_metadata(self, gcms): 533 """Get metadata about the GC-MS instrument.""" 534 instrument_metadata = {} 535 536 instrument_metadata["analyzer"] = gcms.analyzer 537 instrument_metadata["instrument_label"] = gcms.instrument_label 538 instrument_metadata["instrument_id"] = uuid.uuid4().hex 539 540 return instrument_metadata
Get metadata about the GC-MS instrument.
542 def get_data_metadata(self, gcms, id_label, output_path): 543 """Get metadata about the GC-MS data. 544 545 Parameters: 546 ---------- 547 gcms : object 548 The low resolution GCMS object. 549 id_label : str 550 The ID label for the data. 551 output_path : str 552 The output file path. 553 554 Returns: 555 ------- 556 dict 557 A dictionary containing the data metadata. 558 """ 559 if isinstance(output_path, str): 560 output_path = Path(output_path) 561 562 paramaters_path = output_path.with_suffix(".json") 563 564 if paramaters_path.exists(): 565 with paramaters_path.open() as current_param: 566 metadata = json.load(current_param) 567 data_metadata = metadata.get("Data") 568 else: 569 data_metadata = {} 570 data_metadata["data_name"] = [] 571 data_metadata["input_data_url"] = [] 572 data_metadata["has_input"] = [] 573 574 data_metadata["data_name"].append(gcms.sample_name) 575 data_metadata["input_data_url"].append(str(gcms.file_location)) 576 data_metadata["has_input"].append(id_label + corems_md5(gcms.file_location)) 577 578 data_metadata["output_data_name"] = str(output_path.stem) 579 data_metadata["output_data_url"] = str(output_path) 580 data_metadata["has_output"] = id_label + corems_md5(output_path) 581 582 return data_metadata
Get metadata about the GC-MS data.
Parameters:
gcms : object The low resolution GCMS object. id_label : str The ID label for the data. output_path : str The output file path.
Returns:
dict A dictionary containing the data metadata.
584 def get_parameters_json(self, gcms, id_label, output_path): 585 """Get the parameters as a JSON string. 586 587 Parameters: 588 ---------- 589 gcms : GCMS object 590 The low resolution GCMS object. 591 id_label : str 592 The ID label for the data. 593 output_path : str 594 The output file path. 595 596 Returns: 597 ------- 598 str 599 The parameters as a JSON string. 600 """ 601 602 output_parameters_dict = {} 603 output_parameters_dict["Data"] = self.get_data_metadata( 604 gcms, id_label, output_path 605 ) 606 output_parameters_dict["Stats"] = self.get_data_stats(gcms) 607 output_parameters_dict["Calibration"] = self.get_calibration_stats( 608 gcms, id_label 609 ) 610 output_parameters_dict["Blank"] = self.get_blank_stats(gcms) 611 output_parameters_dict["Instrument"] = self.get_instrument_metadata(gcms) 612 corems_dict_setting = parameter_to_dict.get_dict_data_gcms(gcms) 613 corems_dict_setting["corems_version"] = __version__ 614 output_parameters_dict["CoreMSParameters"] = corems_dict_setting 615 output_parameters_dict["has_metabolite"] = gcms.metabolites_data 616 output = json.dumps( 617 output_parameters_dict, sort_keys=False, indent=4, separators=(",", ": ") 618 ) 619 620 return output
Get the parameters as a JSON string.
Parameters:
gcms : GCMS object The low resolution GCMS object. id_label : str The ID label for the data. output_path : str The output file path.
Returns:
str The parameters as a JSON string.
622 def write_settings(self, output_path, gcms, id_label="emsl:"): 623 """Write the settings to a JSON file. 624 625 Parameters: 626 ---------- 627 output_path : str 628 The output file path. 629 gcms : GCMS object 630 The low resolution GCMS object. 631 id_label : str 632 The ID label for the data. Default is "emsl:". 633 634 """ 635 636 output = self.get_parameters_json(gcms, id_label, output_path) 637 638 with open( 639 output_path.with_suffix(".json"), 640 "w", 641 encoding="utf8", 642 ) as outfile: 643 outfile.write(output)
Write the settings to a JSON file.
Parameters:
output_path : str The output file path. gcms : GCMS object The low resolution GCMS object. id_label : str The ID label for the data. Default is "emsl:".
645 def get_list_dict_data(self, gcms, include_no_match=True, no_match_inline=False): 646 """Get the exported data as a list of dictionaries. 647 648 Parameters: 649 ---------- 650 gcms : object 651 The low resolution GCMS object. 652 include_no_match : bool, optional 653 Whether to include no match data. Default is True. 654 no_match_inline : bool, optional 655 Whether to include no match data inline. Default is False. 656 657 Returns: 658 ------- 659 list 660 The exported data as a list of dictionaries. 661 """ 662 663 output_score_method = gcms.molecular_search_settings.output_score_method 664 665 dict_data_list = [] 666 667 def add_match_dict_data(): 668 derivatization = "{}:{}:{}".format( 669 compound_obj.classify, 670 compound_obj.derivativenum, 671 compound_obj.derivatization, 672 ) 673 out_dict = { 674 "Sample name": gcms.sample_name, 675 "Peak Index": gcpeak_index, 676 "Retention Time": gc_peak.retention_time, 677 "Retention Time Ref": compound_obj.retention_time, 678 "Peak Height": gc_peak.tic, 679 "Peak Area": gc_peak.area, 680 "Retention index": gc_peak.ri, 681 "Retention index Ref": compound_obj.ri, 682 "Retention Index Score": compound_obj.ri_score, 683 "Spectral Similarity Score": compound_obj.spectral_similarity_score, 684 "Similarity Score": compound_obj.similarity_score, 685 "Compound Name": compound_obj.name, 686 "Chebi ID": compound_obj.metadata.chebi, 687 "Kegg Compound ID": compound_obj.metadata.kegg, 688 "Inchi": compound_obj.metadata.inchi, 689 "Inchi Key": compound_obj.metadata.inchikey, 690 "Smiles": compound_obj.metadata.smiles, 691 "Molecular Formula": compound_obj.formula, 692 "IUPAC Name": compound_obj.metadata.iupac_name, 693 "Traditional Name": compound_obj.metadata.traditional_name, 694 "Common Name": compound_obj.metadata.common_name, 695 "Derivatization": derivatization, 696 } 697 698 if self.gcms.molecular_search_settings.exploratory_mode: 699 out_dict.update( 700 { 701 "Weighted Cosine Correlation": compound_obj.spectral_similarity_scores.get( 702 "weighted_cosine_correlation" 703 ), 704 "Cosine Correlation": compound_obj.spectral_similarity_scores.get( 705 "cosine_correlation" 706 ), 707 "Stein Scott Similarity": compound_obj.spectral_similarity_scores.get( 708 "stein_scott_similarity" 709 ), 710 "Pearson Correlation": compound_obj.spectral_similarity_scores.get( 711 "pearson_correlation" 712 ), 713 "Spearman Correlation": compound_obj.spectral_similarity_scores.get( 714 "spearman_correlation" 715 ), 716 "Kendall Tau Correlation": compound_obj.spectral_similarity_scores.get( 717 "kendall_tau_correlation" 718 ), 719 "DFT Correlation": compound_obj.spectral_similarity_scores.get( 720 "dft_correlation" 721 ), 722 "DWT Correlation": compound_obj.spectral_similarity_scores.get( 723 "dwt_correlation" 724 ), 725 "Euclidean Distance": compound_obj.spectral_similarity_scores.get( 726 "euclidean_distance" 727 ), 728 "Manhattan Distance": compound_obj.spectral_similarity_scores.get( 729 "manhattan_distance" 730 ), 731 "Jaccard Distance": compound_obj.spectral_similarity_scores.get( 732 "jaccard_distance" 733 ), 734 } 735 ) 736 for method in methods_name: 737 out_dict[methods_name.get(method)] = ( 738 compound_obj.spectral_similarity_scores.get(method) 739 ) 740 741 dict_data_list.append(out_dict) 742 743 def add_no_match_dict_data(): 744 dict_data_list.append( 745 { 746 "Sample name": gcms.sample_name, 747 "Peak Index": gcpeak_index, 748 "Retention Time": gc_peak.retention_time, 749 "Peak Height": gc_peak.tic, 750 "Peak Area": gc_peak.area, 751 "Retention index": gc_peak.ri, 752 } 753 ) 754 755 for gcpeak_index, gc_peak in enumerate(gcms.sorted_gcpeaks): 756 # check if there is a compound candidate 757 if gc_peak: 758 if output_score_method == "highest_sim_score": 759 compound_obj = gc_peak.highest_score_compound 760 add_match_dict_data() 761 762 elif output_score_method == "highest_ss": 763 compound_obj = gc_peak.highest_ss_compound 764 add_match_dict_data() 765 766 else: 767 for compound_obj in gc_peak: 768 add_match_dict_data() # add monoisotopic peak 769 770 else: 771 # include not_match 772 if include_no_match and no_match_inline: 773 add_no_match_dict_data() 774 775 if include_no_match and not no_match_inline: 776 for gcpeak_index, gc_peak in enumerate(gcms.sorted_gcpeaks): 777 if not gc_peak: 778 add_no_match_dict_data() 779 780 return dict_data_list
Get the exported data as a list of dictionaries.
Parameters:
gcms : object The low resolution GCMS object. include_no_match : bool, optional Whether to include no match data. Default is True. no_match_inline : bool, optional Whether to include no match data inline. Default is False.
Returns:
list The exported data as a list of dictionaries.
783class HighResMassSpectraExport(HighResMassSpecExport): 784 """A class to export high resolution mass spectra data. 785 786 This class provides methods to export high resolution mass spectra data to various formats 787 such as Excel, CSV, HDF5, and Pandas DataFrame. 788 789 Parameters 790 ---------- 791 out_file_path : str | Path 792 The output file path. 793 mass_spectra : object 794 The high resolution mass spectra object. 795 output_type : str, optional 796 The output type. Default is 'excel'. 797 798 Attributes 799 ---------- 800 output_file : Path 801 The output file path without suffix 802 dir_loc : Path 803 The directory location for the output file, 804 by default this will be the output_file + ".corems" and all output files will be 805 written into this location 806 mass_spectra : MassSpectraBase 807 The high resolution mass spectra object. 808 """ 809 810 def __init__(self, out_file_path, mass_spectra, output_type="excel"): 811 super().__init__( 812 out_file_path=out_file_path, mass_spectrum=None, output_type=output_type 813 ) 814 815 self.dir_loc = Path(out_file_path + ".corems") 816 self.dir_loc.mkdir(exist_ok=True) 817 # Place the output file in the directory 818 self.output_file = self.dir_loc / Path(out_file_path).name 819 self._output_type = output_type # 'excel', 'csv', 'pandas' or 'hdf5' 820 self.mass_spectra = mass_spectra 821 self.atoms_order_list = None 822 self._init_columns() 823 824 def get_pandas_df(self): 825 """Get the mass spectra as a list of Pandas DataFrames.""" 826 827 list_df = [] 828 829 for mass_spectrum in self.mass_spectra: 830 columns = self.columns_label + self.get_all_used_atoms_in_order( 831 mass_spectrum 832 ) 833 834 dict_data_list = self.get_list_dict_data(mass_spectrum) 835 836 df = DataFrame(dict_data_list, columns=columns) 837 838 scan_number = mass_spectrum.scan_number 839 840 df.name = str(self.output_file) + "_" + str(scan_number) 841 842 list_df.append(df) 843 844 return list_df 845 846 def to_pandas(self, write_metadata=True): 847 """Export the data to a Pandas DataFrame and save it as a pickle file. 848 849 Parameters: 850 ---------- 851 write_metadata : bool, optional 852 Whether to write metadata to the output file. Default is True. 853 """ 854 855 for mass_spectrum in self.mass_spectra: 856 columns = self.columns_label + self.get_all_used_atoms_in_order( 857 mass_spectrum 858 ) 859 860 dict_data_list = self.get_list_dict_data(mass_spectrum) 861 862 df = DataFrame(dict_data_list, columns=columns) 863 864 scan_number = mass_spectrum.scan_number 865 866 out_filename = Path( 867 "%s_scan%s%s" % (self.output_file, str(scan_number), ".pkl") 868 ) 869 870 df.to_pickle(self.dir_loc / out_filename) 871 872 if write_metadata: 873 self.write_settings( 874 self.dir_loc / out_filename.with_suffix(""), mass_spectrum 875 ) 876 877 def to_excel(self, write_metadata=True): 878 """Export the data to an Excel file. 879 880 Parameters: 881 ---------- 882 write_metadata : bool, optional 883 Whether to write metadata to the output file. Default is True. 884 """ 885 for mass_spectrum in self.mass_spectra: 886 columns = self.columns_label + self.get_all_used_atoms_in_order( 887 mass_spectrum 888 ) 889 890 dict_data_list = self.get_list_dict_data(mass_spectrum) 891 892 df = DataFrame(dict_data_list, columns=columns) 893 894 scan_number = mass_spectrum.scan_number 895 896 out_filename = Path( 897 "%s_scan%s%s" % (self.output_file, str(scan_number), ".xlsx") 898 ) 899 900 df.to_excel(self.dir_loc / out_filename) 901 902 if write_metadata: 903 self.write_settings( 904 self.dir_loc / out_filename.with_suffix(""), mass_spectrum 905 ) 906 907 def to_csv(self, write_metadata=True): 908 """Export the data to a CSV file. 909 910 Parameters: 911 ---------- 912 write_metadata : bool, optional 913 Whether to write metadata to the output file. Default is True. 914 """ 915 import csv 916 917 for mass_spectrum in self.mass_spectra: 918 columns = self.columns_label + self.get_all_used_atoms_in_order( 919 mass_spectrum 920 ) 921 922 scan_number = mass_spectrum.scan_number 923 924 dict_data_list = self.get_list_dict_data(mass_spectrum) 925 926 out_filename = Path( 927 "%s_scan%s%s" % (self.output_file, str(scan_number), ".csv") 928 ) 929 930 with open(self.dir_loc / out_filename, "w", newline="") as csvfile: 931 writer = csv.DictWriter(csvfile, fieldnames=columns) 932 writer.writeheader() 933 for data in dict_data_list: 934 writer.writerow(data) 935 936 if write_metadata: 937 self.write_settings( 938 self.dir_loc / out_filename.with_suffix(""), mass_spectrum 939 ) 940 941 def get_mass_spectra_attrs(self): 942 """Get the mass spectra attributes as a JSON string. 943 944 Parameters: 945 ---------- 946 mass_spectra : object 947 The high resolution mass spectra object. 948 949 Returns: 950 ------- 951 str 952 The mass spectra attributes as a JSON string. 953 """ 954 dict_ms_attrs = {} 955 dict_ms_attrs["analyzer"] = self.mass_spectra.analyzer 956 dict_ms_attrs["instrument_label"] = self.mass_spectra.instrument_label 957 dict_ms_attrs["sample_name"] = self.mass_spectra.sample_name 958 959 return json.dumps( 960 dict_ms_attrs, sort_keys=False, indent=4, separators=(",", ": ") 961 ) 962 963 def to_hdf(self, overwrite=False, export_raw=True): 964 """Export the data to an HDF5 file. 965 966 Parameters 967 ---------- 968 overwrite : bool, optional 969 Whether to overwrite the output file. Default is False. 970 export_raw : bool, optional 971 Whether to export the raw mass spectra data. Default is True. 972 """ 973 if overwrite: 974 if self.output_file.with_suffix(".hdf5").exists(): 975 self.output_file.with_suffix(".hdf5").unlink() 976 977 with h5py.File(self.output_file.with_suffix(".hdf5"), "a") as hdf_handle: 978 if not hdf_handle.attrs.get("date_utc"): 979 # Set metadata for all mass spectra 980 timenow = str( 981 datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M:%S %Z") 982 ) 983 hdf_handle.attrs["date_utc"] = timenow 984 hdf_handle.attrs["filename"] = self.mass_spectra.file_location.name 985 hdf_handle.attrs["data_structure"] = "mass_spectra" 986 hdf_handle.attrs["analyzer"] = self.mass_spectra.analyzer 987 hdf_handle.attrs["instrument_label"] = ( 988 self.mass_spectra.instrument_label 989 ) 990 hdf_handle.attrs["sample_name"] = self.mass_spectra.sample_name 991 hdf_handle.attrs["polarity"] = self.mass_spectra.polarity 992 hdf_handle.attrs["parser_type"] = ( 993 self.mass_spectra.spectra_parser_class.__name__ 994 ) 995 hdf_handle.attrs["original_file_location"] = ( 996 self.mass_spectra.file_location._str 997 ) 998 999 # Save creation time from original parser if available 1000 try: 1001 if hasattr(self.mass_spectra, 'spectra_parser') and self.mass_spectra.spectra_parser is not None: 1002 creation_time = self.mass_spectra.spectra_parser.get_creation_time() 1003 if creation_time is not None: 1004 hdf_handle.attrs["creation_time"] = creation_time.isoformat() 1005 except Exception: 1006 pass # If creation time cannot be retrieved, skip it 1007 1008 if "mass_spectra" not in hdf_handle: 1009 mass_spectra_group = hdf_handle.create_group("mass_spectra") 1010 else: 1011 mass_spectra_group = hdf_handle.get("mass_spectra") 1012 1013 for mass_spectrum in self.mass_spectra: 1014 group_key = str(int(mass_spectrum.scan_number)) 1015 1016 self.add_mass_spectrum_to_hdf5( 1017 hdf_handle, mass_spectrum, group_key, mass_spectra_group, export_raw 1018 )
A class to export high resolution mass spectra data.
This class provides methods to export high resolution mass spectra data to various formats such as Excel, CSV, HDF5, and Pandas DataFrame.
Parameters
- out_file_path (str | Path): The output file path.
- mass_spectra (object): The high resolution mass spectra object.
- output_type (str, optional): The output type. Default is 'excel'.
Attributes
- output_file (Path): The output file path without suffix
- dir_loc (Path): The directory location for the output file, by default this will be the output_file + ".corems" and all output files will be written into this location
- mass_spectra (MassSpectraBase): The high resolution mass spectra object.
810 def __init__(self, out_file_path, mass_spectra, output_type="excel"): 811 super().__init__( 812 out_file_path=out_file_path, mass_spectrum=None, output_type=output_type 813 ) 814 815 self.dir_loc = Path(out_file_path + ".corems") 816 self.dir_loc.mkdir(exist_ok=True) 817 # Place the output file in the directory 818 self.output_file = self.dir_loc / Path(out_file_path).name 819 self._output_type = output_type # 'excel', 'csv', 'pandas' or 'hdf5' 820 self.mass_spectra = mass_spectra 821 self.atoms_order_list = None 822 self._init_columns()
This constructor should always be called with keyword arguments. Arguments are:
group should be None; reserved for future extension when a ThreadGroup class is implemented.
target is the callable object to be invoked by the run() method. Defaults to None, meaning nothing is called.
name is the thread name. By default, a unique name is constructed of the form "Thread-N" where N is a small decimal number.
args is a list or tuple of arguments for the target invocation. Defaults to ().
kwargs is a dictionary of keyword arguments for the target invocation. Defaults to {}.
If a subclass overrides the constructor, it must make sure to invoke the base class constructor (Thread.__init__()) before doing anything else to the thread.
824 def get_pandas_df(self): 825 """Get the mass spectra as a list of Pandas DataFrames.""" 826 827 list_df = [] 828 829 for mass_spectrum in self.mass_spectra: 830 columns = self.columns_label + self.get_all_used_atoms_in_order( 831 mass_spectrum 832 ) 833 834 dict_data_list = self.get_list_dict_data(mass_spectrum) 835 836 df = DataFrame(dict_data_list, columns=columns) 837 838 scan_number = mass_spectrum.scan_number 839 840 df.name = str(self.output_file) + "_" + str(scan_number) 841 842 list_df.append(df) 843 844 return list_df
Get the mass spectra as a list of Pandas DataFrames.
846 def to_pandas(self, write_metadata=True): 847 """Export the data to a Pandas DataFrame and save it as a pickle file. 848 849 Parameters: 850 ---------- 851 write_metadata : bool, optional 852 Whether to write metadata to the output file. Default is True. 853 """ 854 855 for mass_spectrum in self.mass_spectra: 856 columns = self.columns_label + self.get_all_used_atoms_in_order( 857 mass_spectrum 858 ) 859 860 dict_data_list = self.get_list_dict_data(mass_spectrum) 861 862 df = DataFrame(dict_data_list, columns=columns) 863 864 scan_number = mass_spectrum.scan_number 865 866 out_filename = Path( 867 "%s_scan%s%s" % (self.output_file, str(scan_number), ".pkl") 868 ) 869 870 df.to_pickle(self.dir_loc / out_filename) 871 872 if write_metadata: 873 self.write_settings( 874 self.dir_loc / out_filename.with_suffix(""), mass_spectrum 875 )
Export the data to a Pandas DataFrame and save it as a pickle file.
Parameters:
write_metadata : bool, optional Whether to write metadata to the output file. Default is True.
877 def to_excel(self, write_metadata=True): 878 """Export the data to an Excel file. 879 880 Parameters: 881 ---------- 882 write_metadata : bool, optional 883 Whether to write metadata to the output file. Default is True. 884 """ 885 for mass_spectrum in self.mass_spectra: 886 columns = self.columns_label + self.get_all_used_atoms_in_order( 887 mass_spectrum 888 ) 889 890 dict_data_list = self.get_list_dict_data(mass_spectrum) 891 892 df = DataFrame(dict_data_list, columns=columns) 893 894 scan_number = mass_spectrum.scan_number 895 896 out_filename = Path( 897 "%s_scan%s%s" % (self.output_file, str(scan_number), ".xlsx") 898 ) 899 900 df.to_excel(self.dir_loc / out_filename) 901 902 if write_metadata: 903 self.write_settings( 904 self.dir_loc / out_filename.with_suffix(""), mass_spectrum 905 )
Export the data to an Excel file.
Parameters:
write_metadata : bool, optional Whether to write metadata to the output file. Default is True.
907 def to_csv(self, write_metadata=True): 908 """Export the data to a CSV file. 909 910 Parameters: 911 ---------- 912 write_metadata : bool, optional 913 Whether to write metadata to the output file. Default is True. 914 """ 915 import csv 916 917 for mass_spectrum in self.mass_spectra: 918 columns = self.columns_label + self.get_all_used_atoms_in_order( 919 mass_spectrum 920 ) 921 922 scan_number = mass_spectrum.scan_number 923 924 dict_data_list = self.get_list_dict_data(mass_spectrum) 925 926 out_filename = Path( 927 "%s_scan%s%s" % (self.output_file, str(scan_number), ".csv") 928 ) 929 930 with open(self.dir_loc / out_filename, "w", newline="") as csvfile: 931 writer = csv.DictWriter(csvfile, fieldnames=columns) 932 writer.writeheader() 933 for data in dict_data_list: 934 writer.writerow(data) 935 936 if write_metadata: 937 self.write_settings( 938 self.dir_loc / out_filename.with_suffix(""), mass_spectrum 939 )
Export the data to a CSV file.
Parameters:
write_metadata : bool, optional Whether to write metadata to the output file. Default is True.
941 def get_mass_spectra_attrs(self): 942 """Get the mass spectra attributes as a JSON string. 943 944 Parameters: 945 ---------- 946 mass_spectra : object 947 The high resolution mass spectra object. 948 949 Returns: 950 ------- 951 str 952 The mass spectra attributes as a JSON string. 953 """ 954 dict_ms_attrs = {} 955 dict_ms_attrs["analyzer"] = self.mass_spectra.analyzer 956 dict_ms_attrs["instrument_label"] = self.mass_spectra.instrument_label 957 dict_ms_attrs["sample_name"] = self.mass_spectra.sample_name 958 959 return json.dumps( 960 dict_ms_attrs, sort_keys=False, indent=4, separators=(",", ": ") 961 )
Get the mass spectra attributes as a JSON string.
Parameters:
mass_spectra : object The high resolution mass spectra object.
Returns:
str The mass spectra attributes as a JSON string.
963 def to_hdf(self, overwrite=False, export_raw=True): 964 """Export the data to an HDF5 file. 965 966 Parameters 967 ---------- 968 overwrite : bool, optional 969 Whether to overwrite the output file. Default is False. 970 export_raw : bool, optional 971 Whether to export the raw mass spectra data. Default is True. 972 """ 973 if overwrite: 974 if self.output_file.with_suffix(".hdf5").exists(): 975 self.output_file.with_suffix(".hdf5").unlink() 976 977 with h5py.File(self.output_file.with_suffix(".hdf5"), "a") as hdf_handle: 978 if not hdf_handle.attrs.get("date_utc"): 979 # Set metadata for all mass spectra 980 timenow = str( 981 datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M:%S %Z") 982 ) 983 hdf_handle.attrs["date_utc"] = timenow 984 hdf_handle.attrs["filename"] = self.mass_spectra.file_location.name 985 hdf_handle.attrs["data_structure"] = "mass_spectra" 986 hdf_handle.attrs["analyzer"] = self.mass_spectra.analyzer 987 hdf_handle.attrs["instrument_label"] = ( 988 self.mass_spectra.instrument_label 989 ) 990 hdf_handle.attrs["sample_name"] = self.mass_spectra.sample_name 991 hdf_handle.attrs["polarity"] = self.mass_spectra.polarity 992 hdf_handle.attrs["parser_type"] = ( 993 self.mass_spectra.spectra_parser_class.__name__ 994 ) 995 hdf_handle.attrs["original_file_location"] = ( 996 self.mass_spectra.file_location._str 997 ) 998 999 # Save creation time from original parser if available 1000 try: 1001 if hasattr(self.mass_spectra, 'spectra_parser') and self.mass_spectra.spectra_parser is not None: 1002 creation_time = self.mass_spectra.spectra_parser.get_creation_time() 1003 if creation_time is not None: 1004 hdf_handle.attrs["creation_time"] = creation_time.isoformat() 1005 except Exception: 1006 pass # If creation time cannot be retrieved, skip it 1007 1008 if "mass_spectra" not in hdf_handle: 1009 mass_spectra_group = hdf_handle.create_group("mass_spectra") 1010 else: 1011 mass_spectra_group = hdf_handle.get("mass_spectra") 1012 1013 for mass_spectrum in self.mass_spectra: 1014 group_key = str(int(mass_spectrum.scan_number)) 1015 1016 self.add_mass_spectrum_to_hdf5( 1017 hdf_handle, mass_spectrum, group_key, mass_spectra_group, export_raw 1018 )
Export the data to an HDF5 file.
Parameters
- overwrite (bool, optional): Whether to overwrite the output file. Default is False.
- export_raw (bool, optional): Whether to export the raw mass spectra data. Default is True.
1021class LCMSExport(HighResMassSpectraExport): 1022 """A class to export high resolution LC-MS data. 1023 1024 This class provides methods to export high resolution LC-MS data to HDF5. 1025 1026 Parameters 1027 ---------- 1028 out_file_path : str | Path 1029 The output file path, do not include the file extension. 1030 lcms_object : LCMSBase 1031 The high resolution lc-ms object. 1032 """ 1033 1034 def __init__(self, out_file_path, mass_spectra): 1035 super().__init__(out_file_path, mass_spectra, output_type="hdf5") 1036 1037 @staticmethod 1038 def _save_mass_features_dict_to_hdf5(mass_features_dict, mass_features_group, overwrite=False): 1039 """Save a dictionary of mass features to an HDF5 group. 1040 1041 This is a helper method that can be reused by different export classes. 1042 1043 Parameters 1044 ---------- 1045 mass_features_dict : dict 1046 Dictionary of mass features to save, keyed by mass feature ID. 1047 mass_features_group : h5py.Group 1048 The HDF5 group to save the mass features to. 1049 overwrite : bool, optional 1050 Whether to overwrite existing mass features. Default is False. 1051 """ 1052 1053 # Create group for each mass feature, with key as the mass feature id 1054 for k, v in mass_features_dict.items(): 1055 if str(k) not in mass_features_group or overwrite: 1056 if str(k) in mass_features_group and overwrite: 1057 del mass_features_group[str(k)] 1058 mass_features_group.create_group(str(k)) 1059 # Loop through each of the mass feature attributes and add them as attributes (if single value) or datasets (if array) 1060 for k2, v2 in v.__dict__.items(): 1061 if v2 is not None: 1062 # Check if the attribute is an integer or float and set as an attribute in the mass feature group 1063 if k2 not in [ 1064 "chromatogram_parent", 1065 "ms2_mass_spectra", 1066 "mass_spectrum", 1067 "_eic_data", 1068 "ms2_similarity_results", 1069 ]: 1070 if k2 == "ms2_scan_numbers": 1071 array = np.array(v2) 1072 # Convert int64 to int32 1073 if array.dtype == np.int64: 1074 array = array.astype(np.int32) 1075 mass_features_group[str(k)].create_dataset( 1076 str(k2), data=array, compression="gzip", compression_opts=9, chunks=True 1077 ) 1078 elif k2 == "_half_height_width": 1079 array = np.array(v2) 1080 # Convert float64 to float32 1081 if array.dtype == np.float64: 1082 array = array.astype(np.float32) 1083 mass_features_group[str(k)].create_dataset( 1084 str(k2), data=array, compression="gzip", compression_opts=9, chunks=True 1085 ) 1086 elif k2 == "_ms_deconvoluted_idx": 1087 array = np.array(v2) 1088 # Convert int64 to int32 1089 if array.dtype == np.int64: 1090 array = array.astype(np.int32) 1091 mass_features_group[str(k)].create_dataset( 1092 str(k2), data=array, compression="gzip", compression_opts=9, chunks=True 1093 ) 1094 elif k2 == "associated_mass_features_deconvoluted": 1095 array = np.array(v2) 1096 # Convert int64 to int32 1097 if array.dtype == np.int64: 1098 array = array.astype(np.int32) 1099 mass_features_group[str(k)].create_dataset( 1100 str(k2), data=array, compression="gzip", compression_opts=9, chunks=True 1101 ) 1102 elif k2 == "_noise_score": 1103 array = np.array(v2) 1104 # Convert float64 to float32 1105 if array.dtype == np.float64: 1106 array = array.astype(np.float32) 1107 mass_features_group[str(k)].create_dataset( 1108 str(k2), data=array, compression="gzip", compression_opts=9, chunks=True 1109 ) 1110 elif ( 1111 isinstance(v2, int) 1112 or isinstance(v2, float) 1113 or isinstance(v2, str) 1114 or isinstance(v2, np.integer) 1115 or isinstance(v2, np.float32) 1116 or isinstance(v2, np.float64) 1117 or isinstance(v2, np.bool_) 1118 ): 1119 # Convert numpy types to smaller precision for storage 1120 if isinstance(v2, np.int64): 1121 v2 = np.int32(v2) 1122 elif isinstance(v2, np.float64): 1123 v2 = np.float32(v2) 1124 mass_features_group[str(k)].attrs[str(k2)] = v2 1125 1126 @staticmethod 1127 def _save_eics_dict_to_hdf5(eics_dict, eics_group, overwrite=False): 1128 """Save a dictionary of EICs to an HDF5 group. 1129 1130 This is a static helper method that can be reused by different export classes 1131 to save EIC data in a consistent format. 1132 1133 Parameters 1134 ---------- 1135 eics_dict : dict 1136 Dictionary of EIC_Data objects, keyed by m/z value. 1137 eics_group : h5py.Group 1138 The HDF5 group to save the EICs to. 1139 overwrite : bool, optional 1140 Whether to overwrite existing EICs. Default is False. 1141 """ 1142 for mz, eic_data in eics_dict.items(): 1143 mz_str = str(mz) 1144 if mz_str not in eics_group or overwrite: 1145 if mz_str in eics_group and overwrite: 1146 del eics_group[mz_str] 1147 eic_grp = eics_group.create_group(mz_str) 1148 eic_grp.attrs["mz"] = mz 1149 1150 # Save all EIC_Data attributes as datasets 1151 for attr_name, attr_value in eic_data.__dict__.items(): 1152 if attr_value is not None: 1153 array = np.array(attr_value) 1154 # Apply data type optimization and compression 1155 if array.dtype == np.int64: 1156 array = array.astype(np.int32) 1157 elif array.dtype == np.float64: 1158 array = array.astype(np.float32) 1159 elif array.dtype.str[0:2] == "<U": 1160 # Convert Unicode strings to UTF-8 encoded strings 1161 string_data = [str(item) for item in array] 1162 string_dtype = h5py.string_dtype(encoding='utf-8') 1163 eic_grp.create_dataset(str(attr_name), data=string_data, dtype=string_dtype, compression="gzip", compression_opts=9, chunks=True) 1164 continue 1165 eic_grp.create_dataset(str(attr_name), data=array, compression="gzip", compression_opts=9, chunks=True) 1166 1167 def _save_mass_features_to_hdf5(self, hdf_handle, group_name = "mass_features", overwrite=False): 1168 """Save the mass features to the HDF5 file. 1169 1170 Parameters 1171 ---------- 1172 hdf_handle : h5py.File 1173 The HDF5 file handle. 1174 group_name : str, optional 1175 The name of the group to save the mass features to. Default is 'mass_features'. 1176 overwrite : bool, optional 1177 Whether to overwrite the group if it exists. Default is False. 1178 """ 1179 # Determine which mass features to save based on group_name 1180 if group_name == "induced_mass_features": 1181 if len(self.mass_spectra.induced_mass_features) == 0: 1182 return # No induced mass features to save 1183 mass_features_dict = self.mass_spectra.induced_mass_features 1184 else: 1185 if len(self.mass_spectra.mass_features) == 0: 1186 return # No mass features to save 1187 mass_features_dict = self.mass_spectra.mass_features 1188 1189 # Add LCMS mass features to hdf5 file 1190 if group_name not in hdf_handle: 1191 mass_features_group = hdf_handle.create_group(group_name) 1192 else: 1193 mass_features_group = hdf_handle.get(group_name) 1194 1195 # Use the static helper method to save the mass features 1196 self._save_mass_features_dict_to_hdf5(mass_features_dict, mass_features_group, overwrite) 1197 1198 def to_hdf(self, overwrite=False, save_parameters=True, parameter_format="toml"): 1199 """Export the data to an HDF5. 1200 1201 Parameters 1202 ---------- 1203 overwrite : bool, optional 1204 Whether to overwrite the output file. Default is False. 1205 save_parameters : bool, optional 1206 Whether to save the parameters as a separate json or toml file. Default is True. 1207 parameter_format : str, optional 1208 The format to save the parameters in. Default is 'toml'. 1209 1210 Raises 1211 ------ 1212 ValueError 1213 If parameter_format is not 'json' or 'toml'. 1214 """ 1215 export_profile_spectra = ( 1216 self.mass_spectra.parameters.lc_ms.export_profile_spectra 1217 ) 1218 1219 # Write the mass spectra data to the hdf5 file 1220 super().to_hdf(overwrite=overwrite, export_raw=export_profile_spectra) 1221 1222 # Write scan info, ms_unprocessed, mass features, eics, and ms2_search results to the hdf5 file 1223 with h5py.File(self.output_file.with_suffix(".hdf5"), "a") as hdf_handle: 1224 # Add scan_info to hdf5 file 1225 if "scan_info" not in hdf_handle or overwrite: 1226 if "scan_info" in hdf_handle and overwrite: 1227 del hdf_handle["scan_info"] 1228 scan_info_group = hdf_handle.create_group("scan_info") 1229 for k, v in self.mass_spectra._scan_info.items(): 1230 array = np.array(list(v.values())) 1231 if array.dtype.str[0:2] == "<U": 1232 array = array.astype("S") 1233 scan_info_group.create_dataset(k, data=array) 1234 1235 # Add ms_unprocessed to hdf5 file 1236 export_unprocessed_ms1 = ( 1237 self.mass_spectra.parameters.lc_ms.export_unprocessed_ms1 1238 ) 1239 if self.mass_spectra._ms_unprocessed and export_unprocessed_ms1: 1240 if "ms_unprocessed" not in hdf_handle or overwrite: 1241 if "ms_unprocessed" in hdf_handle and overwrite: 1242 del hdf_handle["ms_unprocessed"] 1243 ms_unprocessed_group = hdf_handle.create_group("ms_unprocessed") 1244 else: 1245 ms_unprocessed_group = hdf_handle.get("ms_unprocessed") 1246 for k, v in self.mass_spectra._ms_unprocessed.items(): 1247 if str(k) not in ms_unprocessed_group or overwrite: 1248 if str(k) in ms_unprocessed_group and overwrite: 1249 del ms_unprocessed_group[str(k)] 1250 array = np.array(v) 1251 ms_unprocessed_group.create_dataset(str(k), data=array) 1252 1253 # Add LCMS mass features to hdf5 file 1254 self._save_mass_features_to_hdf5(hdf_handle, group_name="mass_features", overwrite=overwrite) 1255 self._save_mass_features_to_hdf5(hdf_handle, group_name="induced_mass_features", overwrite=overwrite) 1256 1257 # Add EIC data to hdf5 file 1258 export_eics = self.mass_spectra.parameters.lc_ms.export_eics 1259 if len(self.mass_spectra.eics) > 0 and export_eics: 1260 if "eics" not in hdf_handle or overwrite: 1261 if "eics" in hdf_handle and overwrite: 1262 del hdf_handle["eics"] 1263 eic_group = hdf_handle.create_group("eics") 1264 else: 1265 eic_group = hdf_handle.get("eics") 1266 1267 # Use the static helper method to save the EICs 1268 self._save_eics_dict_to_hdf5(self.mass_spectra.eics, eic_group, overwrite) 1269 1270 # Add ms2_search results to hdf5 file (parameterized) 1271 if len(self.mass_spectra.spectral_search_results) > 0: 1272 if "spectral_search_results" not in hdf_handle or overwrite: 1273 if "spectral_search_results" in hdf_handle and overwrite: 1274 del hdf_handle["spectral_search_results"] 1275 spectral_search_results = hdf_handle.create_group( 1276 "spectral_search_results" 1277 ) 1278 else: 1279 spectral_search_results = hdf_handle.get("spectral_search_results") 1280 # Create group for each search result by ms2_scan / precursor_mz 1281 for k, v in self.mass_spectra.spectral_search_results.items(): 1282 #TODO KRH: Fix to handle if export_only_relevant and k not in relevant_scan_numbers: continue! 1283 if str(k) not in spectral_search_results or overwrite: 1284 if str(k) in spectral_search_results and overwrite: 1285 del spectral_search_results[str(k)] 1286 spectral_search_results.create_group(str(k)) 1287 for k2, v2 in v.items(): 1288 spectral_search_results[str(k)].create_group(str(k2)) 1289 spectral_search_results[str(k)][str(k2)].attrs[ 1290 "precursor_mz" 1291 ] = v2.precursor_mz 1292 spectral_search_results[str(k)][str(k2)].attrs[ 1293 "query_spectrum_id" 1294 ] = v2.query_spectrum_id 1295 # Loop through each of the attributes and add them as datasets (if array) 1296 for k3, v3 in v2.__dict__.items(): 1297 if v3 is not None and k3 not in [ 1298 "query_spectrum", 1299 "precursor_mz", 1300 "query_spectrum_id", 1301 ]: 1302 if k3 == "query_frag_types" or k3 == "ref_frag_types": 1303 v3 = [", ".join(x) for x in v3] 1304 if all(v3 is not None for v3 in v3): 1305 array = np.array(v3) 1306 if array.dtype.str[0:2] == "<U": 1307 array = array.astype("S") 1308 spectral_search_results[str(k)][str(k2)].create_dataset( 1309 str(k3), data=array 1310 ) 1311 1312 # Save parameters as separate json 1313 if save_parameters: 1314 # Check if parameter_format is valid 1315 if parameter_format not in ["json", "toml"]: 1316 raise ValueError("parameter_format must be 'json' or 'toml'") 1317 1318 if parameter_format == "json": 1319 dump_lcms_settings_json( 1320 filename=self.output_file.with_suffix(".json"), 1321 lcms_obj=self.mass_spectra, 1322 ) 1323 elif parameter_format == "toml": 1324 dump_lcms_settings_toml( 1325 filename=self.output_file.with_suffix(".toml"), 1326 lcms_obj=self.mass_spectra, 1327 )
A class to export high resolution LC-MS data.
This class provides methods to export high resolution LC-MS data to HDF5.
Parameters
- out_file_path (str | Path): The output file path, do not include the file extension.
- lcms_object (LCMSBase): The high resolution lc-ms object.
1034 def __init__(self, out_file_path, mass_spectra): 1035 super().__init__(out_file_path, mass_spectra, output_type="hdf5")
This constructor should always be called with keyword arguments. Arguments are:
group should be None; reserved for future extension when a ThreadGroup class is implemented.
target is the callable object to be invoked by the run() method. Defaults to None, meaning nothing is called.
name is the thread name. By default, a unique name is constructed of the form "Thread-N" where N is a small decimal number.
args is a list or tuple of arguments for the target invocation. Defaults to ().
kwargs is a dictionary of keyword arguments for the target invocation. Defaults to {}.
If a subclass overrides the constructor, it must make sure to invoke the base class constructor (Thread.__init__()) before doing anything else to the thread.
1198 def to_hdf(self, overwrite=False, save_parameters=True, parameter_format="toml"): 1199 """Export the data to an HDF5. 1200 1201 Parameters 1202 ---------- 1203 overwrite : bool, optional 1204 Whether to overwrite the output file. Default is False. 1205 save_parameters : bool, optional 1206 Whether to save the parameters as a separate json or toml file. Default is True. 1207 parameter_format : str, optional 1208 The format to save the parameters in. Default is 'toml'. 1209 1210 Raises 1211 ------ 1212 ValueError 1213 If parameter_format is not 'json' or 'toml'. 1214 """ 1215 export_profile_spectra = ( 1216 self.mass_spectra.parameters.lc_ms.export_profile_spectra 1217 ) 1218 1219 # Write the mass spectra data to the hdf5 file 1220 super().to_hdf(overwrite=overwrite, export_raw=export_profile_spectra) 1221 1222 # Write scan info, ms_unprocessed, mass features, eics, and ms2_search results to the hdf5 file 1223 with h5py.File(self.output_file.with_suffix(".hdf5"), "a") as hdf_handle: 1224 # Add scan_info to hdf5 file 1225 if "scan_info" not in hdf_handle or overwrite: 1226 if "scan_info" in hdf_handle and overwrite: 1227 del hdf_handle["scan_info"] 1228 scan_info_group = hdf_handle.create_group("scan_info") 1229 for k, v in self.mass_spectra._scan_info.items(): 1230 array = np.array(list(v.values())) 1231 if array.dtype.str[0:2] == "<U": 1232 array = array.astype("S") 1233 scan_info_group.create_dataset(k, data=array) 1234 1235 # Add ms_unprocessed to hdf5 file 1236 export_unprocessed_ms1 = ( 1237 self.mass_spectra.parameters.lc_ms.export_unprocessed_ms1 1238 ) 1239 if self.mass_spectra._ms_unprocessed and export_unprocessed_ms1: 1240 if "ms_unprocessed" not in hdf_handle or overwrite: 1241 if "ms_unprocessed" in hdf_handle and overwrite: 1242 del hdf_handle["ms_unprocessed"] 1243 ms_unprocessed_group = hdf_handle.create_group("ms_unprocessed") 1244 else: 1245 ms_unprocessed_group = hdf_handle.get("ms_unprocessed") 1246 for k, v in self.mass_spectra._ms_unprocessed.items(): 1247 if str(k) not in ms_unprocessed_group or overwrite: 1248 if str(k) in ms_unprocessed_group and overwrite: 1249 del ms_unprocessed_group[str(k)] 1250 array = np.array(v) 1251 ms_unprocessed_group.create_dataset(str(k), data=array) 1252 1253 # Add LCMS mass features to hdf5 file 1254 self._save_mass_features_to_hdf5(hdf_handle, group_name="mass_features", overwrite=overwrite) 1255 self._save_mass_features_to_hdf5(hdf_handle, group_name="induced_mass_features", overwrite=overwrite) 1256 1257 # Add EIC data to hdf5 file 1258 export_eics = self.mass_spectra.parameters.lc_ms.export_eics 1259 if len(self.mass_spectra.eics) > 0 and export_eics: 1260 if "eics" not in hdf_handle or overwrite: 1261 if "eics" in hdf_handle and overwrite: 1262 del hdf_handle["eics"] 1263 eic_group = hdf_handle.create_group("eics") 1264 else: 1265 eic_group = hdf_handle.get("eics") 1266 1267 # Use the static helper method to save the EICs 1268 self._save_eics_dict_to_hdf5(self.mass_spectra.eics, eic_group, overwrite) 1269 1270 # Add ms2_search results to hdf5 file (parameterized) 1271 if len(self.mass_spectra.spectral_search_results) > 0: 1272 if "spectral_search_results" not in hdf_handle or overwrite: 1273 if "spectral_search_results" in hdf_handle and overwrite: 1274 del hdf_handle["spectral_search_results"] 1275 spectral_search_results = hdf_handle.create_group( 1276 "spectral_search_results" 1277 ) 1278 else: 1279 spectral_search_results = hdf_handle.get("spectral_search_results") 1280 # Create group for each search result by ms2_scan / precursor_mz 1281 for k, v in self.mass_spectra.spectral_search_results.items(): 1282 #TODO KRH: Fix to handle if export_only_relevant and k not in relevant_scan_numbers: continue! 1283 if str(k) not in spectral_search_results or overwrite: 1284 if str(k) in spectral_search_results and overwrite: 1285 del spectral_search_results[str(k)] 1286 spectral_search_results.create_group(str(k)) 1287 for k2, v2 in v.items(): 1288 spectral_search_results[str(k)].create_group(str(k2)) 1289 spectral_search_results[str(k)][str(k2)].attrs[ 1290 "precursor_mz" 1291 ] = v2.precursor_mz 1292 spectral_search_results[str(k)][str(k2)].attrs[ 1293 "query_spectrum_id" 1294 ] = v2.query_spectrum_id 1295 # Loop through each of the attributes and add them as datasets (if array) 1296 for k3, v3 in v2.__dict__.items(): 1297 if v3 is not None and k3 not in [ 1298 "query_spectrum", 1299 "precursor_mz", 1300 "query_spectrum_id", 1301 ]: 1302 if k3 == "query_frag_types" or k3 == "ref_frag_types": 1303 v3 = [", ".join(x) for x in v3] 1304 if all(v3 is not None for v3 in v3): 1305 array = np.array(v3) 1306 if array.dtype.str[0:2] == "<U": 1307 array = array.astype("S") 1308 spectral_search_results[str(k)][str(k2)].create_dataset( 1309 str(k3), data=array 1310 ) 1311 1312 # Save parameters as separate json 1313 if save_parameters: 1314 # Check if parameter_format is valid 1315 if parameter_format not in ["json", "toml"]: 1316 raise ValueError("parameter_format must be 'json' or 'toml'") 1317 1318 if parameter_format == "json": 1319 dump_lcms_settings_json( 1320 filename=self.output_file.with_suffix(".json"), 1321 lcms_obj=self.mass_spectra, 1322 ) 1323 elif parameter_format == "toml": 1324 dump_lcms_settings_toml( 1325 filename=self.output_file.with_suffix(".toml"), 1326 lcms_obj=self.mass_spectra, 1327 )
Export the data to an HDF5.
Parameters
- overwrite (bool, optional): Whether to overwrite the output file. Default is False.
- save_parameters (bool, optional): Whether to save the parameters as a separate json or toml file. Default is True.
- parameter_format (str, optional): The format to save the parameters in. Default is 'toml'.
Raises
- ValueError: If parameter_format is not 'json' or 'toml'.
Inherited Members
1329class LCMSMetabolomicsExport(LCMSExport): 1330 """A class to export LCMS metabolite data. 1331 1332 This class provides methods to export LCMS metabolite data to various formats and summarize the metabolite report. 1333 1334 Parameters 1335 ---------- 1336 out_file_path : str | Path 1337 The output file path, do not include the file extension. 1338 mass_spectra : object 1339 The high resolution mass spectra object. 1340 """ 1341 1342 def __init__(self, out_file_path, mass_spectra): 1343 super().__init__(out_file_path, mass_spectra) 1344 self.ion_type_dict = ion_type_dict 1345 1346 @staticmethod 1347 def get_ion_formula(neutral_formula, ion_type): 1348 """From a neutral formula and an ion type, return the formula of the ion. 1349 1350 Notes 1351 ----- 1352 This is a static method. 1353 If the neutral_formula is not a string, this method will return None. 1354 1355 Parameters 1356 ---------- 1357 neutral_formula : str 1358 The neutral formula, this should be a string form from the MolecularFormula class 1359 (e.g. 'C2 H4 O2', isotopes OK), or simple string (e.g. 'C2H4O2', no isotope handling in this case). 1360 In the case of a simple string, the atoms are parsed based on the presence of capital letters, 1361 e.g. MgCl2 is parsed as 'Mg Cl2. 1362 ion_type : str 1363 The ion type, e.g. 'protonated', '[M+H]+', '[M+Na]+', etc. 1364 See the self.ion_type_dict for the available ion types. 1365 1366 Returns 1367 ------- 1368 str 1369 The formula of the ion as a string (like 'C2 H4 O2'); or None if the neutral_formula is not a string. 1370 """ 1371 # If neutral_formula is not a string, return None 1372 if not isinstance(neutral_formula, str): 1373 return None 1374 1375 # Check if there are spaces in the formula (these are outputs of the MolecularFormula class and do not need to be processed before being passed to the class) 1376 if re.search(r"\s", neutral_formula): 1377 neutral_formula = MolecularFormula(neutral_formula, ion_charge=0) 1378 else: 1379 form_pre = re.sub(r"([A-Z])", r" \1", neutral_formula)[1:] 1380 elements = [re.findall(r"[A-Z][a-z]*", x) for x in form_pre.split()] 1381 counts = [re.findall(r"\d+", x) for x in form_pre.split()] 1382 neutral_formula = MolecularFormula( 1383 dict( 1384 zip( 1385 [x[0] for x in elements], 1386 [int(x[0]) if x else 1 for x in counts], 1387 ) 1388 ), 1389 ion_charge=0, 1390 ) 1391 neutral_formula_dict = neutral_formula.to_dict().copy() 1392 1393 adduct_add_dict = ion_type_dict[ion_type][0] 1394 for key in adduct_add_dict: 1395 if key in neutral_formula_dict.keys(): 1396 neutral_formula_dict[key] += adduct_add_dict[key] 1397 else: 1398 neutral_formula_dict[key] = adduct_add_dict[key] 1399 1400 adduct_subtract = ion_type_dict[ion_type][1] 1401 for key in adduct_subtract: 1402 neutral_formula_dict[key] -= adduct_subtract[key] 1403 1404 return MolecularFormula(neutral_formula_dict, ion_charge=0).string 1405 1406 @staticmethod 1407 def get_isotope_type(ion_formula): 1408 """From an ion formula, return the 13C isotope type of the ion. 1409 1410 Notes 1411 ----- 1412 This is a static method. 1413 If the ion_formula is not a string, this method will return None. 1414 This is currently only functional for 13C isotopes. 1415 1416 Parameters 1417 ---------- 1418 ion_formula : str 1419 The formula of the ion, expected to be a string like 'C2 H4 O2'. 1420 1421 Returns 1422 ------- 1423 str 1424 The isotope type of the ion, e.g. '13C1', '13C2', etc; or None if the ion_formula does not contain a 13C isotope. 1425 1426 Raises 1427 ------ 1428 ValueError 1429 If the ion_formula is not a string. 1430 """ 1431 if not isinstance(ion_formula, str): 1432 return None 1433 1434 if re.search(r"\s", ion_formula): 1435 ion_formula = MolecularFormula(ion_formula, ion_charge=0) 1436 else: 1437 raise ValueError('ion_formula should be a string like "C2 H4 O2"') 1438 ion_formula_dict = ion_formula.to_dict().copy() 1439 1440 try: 1441 iso_class = "13C" + str(ion_formula_dict.pop("13C")) 1442 except KeyError: 1443 iso_class = None 1444 1445 return iso_class 1446 1447 def report_to_csv(self, molecular_metadata=None): 1448 """Create a report of the mass features and their annotations and save it as a CSV file. 1449 1450 Parameters 1451 ---------- 1452 molecular_metadata : dict, optional 1453 The molecular metadata. Default is None. 1454 """ 1455 report = self.to_report(molecular_metadata=molecular_metadata) 1456 out_file = self.output_file.with_suffix(".csv") 1457 report.to_csv(out_file, index=False) 1458 1459 def clean_ms1_report(self, ms1_summary_full): 1460 """Clean the MS1 report. 1461 1462 Parameters 1463 ---------- 1464 ms1_summary_full : DataFrame 1465 The full MS1 summary DataFrame. 1466 1467 Returns 1468 ------- 1469 DataFrame 1470 The cleaned MS1 summary DataFrame. 1471 """ 1472 ms1_summary_full = ms1_summary_full.reset_index() 1473 cols_to_keep = [ 1474 "mf_id", 1475 "Molecular Formula", 1476 "Ion Type", 1477 "Calculated m/z", 1478 "m/z Error (ppm)", 1479 "m/z Error Score", 1480 "Is Isotopologue", 1481 "Isotopologue Similarity", 1482 "Confidence Score", 1483 ] 1484 ms1_summary = ms1_summary_full[cols_to_keep].copy() 1485 ms1_summary["ion_formula"] = [ 1486 self.get_ion_formula(f, a) 1487 for f, a in zip(ms1_summary["Molecular Formula"], ms1_summary["Ion Type"]) 1488 ] 1489 ms1_summary["isotopologue_type"] = [ 1490 self.get_isotope_type(f) for f in ms1_summary["ion_formula"].tolist() 1491 ] 1492 1493 # Reorder columns 1494 ms1_summary = ms1_summary[ 1495 [ 1496 "mf_id", 1497 "ion_formula", 1498 "isotopologue_type", 1499 "Calculated m/z", 1500 "m/z Error (ppm)", 1501 "m/z Error Score", 1502 "Isotopologue Similarity", 1503 "Confidence Score", 1504 ] 1505 ] 1506 1507 # Set the index to mf_id 1508 ms1_summary = ms1_summary.set_index("mf_id") 1509 1510 return ms1_summary 1511 1512 def summarize_ms2_report(self, ms2_annot_report): 1513 """ 1514 Summarize the MS2 report. 1515 1516 Parameters 1517 ---------- 1518 ms2_annot_report : DataFrame 1519 The MS2 annotation DataFrame with all annotations, output of mass_features_ms2_annot_to_df. 1520 1521 Returns 1522 ------- 1523 """ 1524 1525 def summarize_metabolomics_report(self, ms2_annot_report): 1526 """Summarize the MS2 hits for a metabolomics report 1527 1528 Parameters 1529 ---------- 1530 ms2_annot : DataFrame 1531 The MS2 annotation DataFrame with all annotations. 1532 1533 Returns 1534 ------- 1535 DataFrame 1536 The summarized metabolomics report. 1537 """ 1538 columns_to_drop = [ 1539 "precursor_mz", 1540 "precursor_mz_error_ppm", 1541 "cas", 1542 "data_id", 1543 "iupac_name", 1544 "traditional_name", 1545 "common_name", 1546 "casno", 1547 ] 1548 ms2_annot = ms2_annot_report.drop( 1549 columns=[col for col in columns_to_drop if col in ms2_annot_report.columns] 1550 ) 1551 1552 # Prepare information about the search results, pulling out the best hit for the single report 1553 # Group by mf_id,ref_mol_id grab row with highest entropy similarity 1554 ms2_annot = ms2_annot.reset_index() 1555 # Add column called "n_spectra_contributing" that is the number of unique values in query_spectrum_id per mf_id,ref_mol_id 1556 ms2_annot["n_spectra_contributing"] = ( 1557 ms2_annot.groupby(["mf_id", "ref_mol_id"])["query_spectrum_id"] 1558 .transform("nunique") 1559 ) 1560 # Sort by entropy similarity 1561 ms2_annot = ms2_annot.sort_values( 1562 by=["mf_id", "ref_mol_id", "entropy_similarity"], ascending=[True, True, False] 1563 ) 1564 best_entropy = ms2_annot.drop_duplicates( 1565 subset=["mf_id", "ref_mol_id"], keep="first" 1566 ) 1567 1568 return best_entropy 1569 1570 def clean_ms2_report(self, metabolite_summary): 1571 """Clean the MS2 report. 1572 1573 Parameters 1574 ---------- 1575 metabolite_summary : DataFrame 1576 The full metabolomics summary DataFrame. 1577 1578 Returns 1579 ------- 1580 DataFrame 1581 The cleaned metabolomics summary DataFrame. 1582 """ 1583 metabolite_summary = metabolite_summary.reset_index() 1584 metabolite_summary["ion_formula"] = [ 1585 self.get_ion_formula(f, a) 1586 for f, a in zip(metabolite_summary["formula"], metabolite_summary["ref_ion_type"]) 1587 ] 1588 1589 col_order = [ 1590 "mf_id", 1591 "ion_formula", 1592 "ref_ion_type", 1593 "formula", 1594 "inchikey", 1595 "name", 1596 "inchi", 1597 "chebi", 1598 "smiles", 1599 "kegg", 1600 "cas", 1601 "database_name", 1602 "ref_ms_id", 1603 "entropy_similarity", 1604 "ref_mz_in_query_fract", 1605 "n_spectra_contributing", 1606 ] 1607 1608 # Reorder columns 1609 metabolite_summary = metabolite_summary[ 1610 [col for col in col_order if col in metabolite_summary.columns] 1611 ] 1612 1613 # Convert chebi (if present) to int: 1614 if "chebi" in metabolite_summary.columns: 1615 metabolite_summary["chebi"] = metabolite_summary["chebi"].astype( 1616 "Int64", errors="ignore" 1617 ) 1618 1619 # Set the index to mf_id 1620 metabolite_summary = metabolite_summary.set_index("mf_id") 1621 1622 return metabolite_summary 1623 1624 def combine_reports(self, mf_report, ms1_annot_report, ms2_annot_report): 1625 """Combine the mass feature report with the MS1 and MS2 reports. 1626 1627 Parameters 1628 ---------- 1629 mf_report : DataFrame 1630 The mass feature report DataFrame. 1631 ms1_annot_report : DataFrame 1632 The MS1 annotation report DataFrame. 1633 ms2_annot_report : DataFrame 1634 The MS2 annotation report DataFrame. 1635 """ 1636 # If there is an ms1_annot_report, merge it with the mf_report 1637 if ms1_annot_report is not None and not ms1_annot_report.empty: 1638 # MS1 has been run and has molecular formula information 1639 mf_report = pd.merge( 1640 mf_report, 1641 ms1_annot_report, 1642 how="left", 1643 on=["mf_id", "isotopologue_type"], 1644 ) 1645 if ms2_annot_report is not None: 1646 # If both reports contain 'ion_formula', prefer a merge that respects it. 1647 # Otherwise fall back to merging on 'mf_id' only to remain robust when 1648 # MS1 formula assignment wasn't performed or MS2 summary lacks the field. 1649 if "ion_formula" in mf_report.columns and "ion_formula" in ms2_annot_report.columns: 1650 # pull out the records without ion_formula and merge on mf_id only 1651 mf_no_ion_formula = mf_report[mf_report["ion_formula"].isna()] 1652 mf_no_ion_formula = mf_no_ion_formula.drop(columns=["ion_formula"]) if "ion_formula" in mf_no_ion_formula.columns else mf_no_ion_formula 1653 mf_no_ion_formula = pd.merge( 1654 mf_no_ion_formula, ms2_annot_report, how="left", on=["mf_id"] 1655 ) 1656 1657 # pull out the records with ion_formula and merge on mf_id + ion_formula 1658 mf_with_ion_formula = mf_report[~mf_report["ion_formula"].isna()] 1659 mf_with_ion_formula = pd.merge( 1660 mf_with_ion_formula, 1661 ms2_annot_report, 1662 how="left", 1663 on=["mf_id", "ion_formula"], 1664 ) 1665 1666 # put back together 1667 mf_report = pd.concat([mf_no_ion_formula, mf_with_ion_formula]) 1668 else: 1669 # Fall back to merging on mf_id only (robust when ion_formula missing) 1670 mf_report = pd.merge( 1671 mf_report, ms2_annot_report, how="left", on=["mf_id"] 1672 ) 1673 1674 # Rename colums 1675 rename_dict = { 1676 "mf_id": "Mass Feature ID", 1677 "scan_time": "Retention Time (min)", 1678 "mz": "m/z", 1679 "apex_scan": "Apex Scan Number", 1680 "intensity": "Intensity", 1681 "persistence": "Persistence", 1682 "area": "Area", 1683 "half_height_width": "Half Height Width (min)", 1684 "tailing_factor": "Tailing Factor", 1685 "dispersity_index": "Dispersity Index", 1686 "ms2_spectrum": "MS2 Spectrum", 1687 "monoisotopic_mf_id": "Monoisotopic Mass Feature ID", 1688 "isotopologue_type": "Isotopologue Type", 1689 "mass_spectrum_deconvoluted_parent": "Is Largest Ion after Deconvolution", 1690 "associated_mass_features": "Associated Mass Features after Deconvolution", 1691 "ion_formula": "Ion Formula", 1692 "formula": "Molecular Formula", 1693 "ref_ion_type": "Ion Type", 1694 "annot_level": "Lipid Annotation Level", 1695 "lipid_molecular_species_id": "Lipid Molecular Species", 1696 "lipid_summed_name": "Lipid Species", 1697 "lipid_subclass": "Lipid Subclass", 1698 "lipid_class": "Lipid Class", 1699 "lipid_category": "Lipid Category", 1700 "entropy_similarity": "Entropy Similarity", 1701 "ref_mz_in_query_fract": "Library mzs in Query (fraction)", 1702 "n_spectra_contributing": "Spectra with Annotation (n)", 1703 } 1704 mf_report = mf_report.rename(columns=rename_dict) 1705 mf_report["Sample Name"] = self.mass_spectra.sample_name 1706 mf_report["Polarity"] = self.mass_spectra.polarity 1707 mf_report = mf_report[ 1708 ["Mass Feature ID", "Sample Name", "Polarity"] 1709 + [ 1710 col 1711 for col in mf_report.columns 1712 if col not in ["Mass Feature ID", "Sample Name", "Polarity"] 1713 ] 1714 ] 1715 1716 # Reorder rows by "Mass Feature ID", then "Entropy Similarity" (descending), then "Confidence Score" (descending) 1717 if "Entropy Similarity" in mf_report.columns and "Confidence Score" in mf_report.columns: 1718 mf_report = mf_report.sort_values( 1719 by=["Mass Feature ID", "Entropy Similarity", "Confidence Score"], 1720 ascending=[True, False, False], 1721 ) 1722 elif "Entropy Similarity" in mf_report.columns: 1723 mf_report = mf_report.sort_values( 1724 by=["Mass Feature ID", "Entropy Similarity"], 1725 ascending=[True, False], 1726 ) 1727 elif "Confidence Score" in mf_report.columns: 1728 mf_report = mf_report.sort_values( 1729 by=["Mass Feature ID", "Confidence Score"], 1730 ascending=[True, False], 1731 ) 1732 # If neither "Entropy Similarity" nor "Confidence Score" are in the columns, just sort by "Mass Feature ID" 1733 else: 1734 mf_report = mf_report.sort_values("Mass Feature ID") 1735 1736 # Reset index 1737 mf_report = mf_report.reset_index(drop=True) 1738 1739 return mf_report 1740 1741 def to_report(self, molecular_metadata=None, suppress_warnings=False): 1742 """Create a report of the mass features and their annotations. 1743 1744 Parameters 1745 ---------- 1746 molecular_metadata : dict, optional 1747 The molecular metadata. Default is None. 1748 suppress_warnings : bool, optional 1749 If True, suppresses warnings from mass_features_ms2_annot_to_df. 1750 Default is False. 1751 1752 Returns 1753 ------- 1754 DataFrame 1755 The report as a Pandas DataFrame. 1756 """ 1757 # Get mass feature dataframe 1758 mf_report = self.mass_spectra.mass_features_to_df() 1759 mf_report = mf_report.reset_index(drop=False) 1760 1761 # Get and clean ms1 annotation dataframe 1762 ms1_annot_report = self.mass_spectra.mass_features_ms1_annot_to_df(suppress_warnings=suppress_warnings) 1763 if ms1_annot_report is not None: 1764 ms1_annot_report = ms1_annot_report.copy() 1765 ms1_annot_report = self.clean_ms1_report(ms1_annot_report) 1766 ms1_annot_report = ms1_annot_report.reset_index(drop=False) 1767 else: 1768 ms1_annot_report = None 1769 1770 # Get, summarize, and clean ms2 annotation dataframe 1771 ms2_annot_report = self.mass_spectra.mass_features_ms2_annot_to_df( 1772 molecular_metadata=molecular_metadata, 1773 suppress_warnings=suppress_warnings 1774 ) 1775 if ms2_annot_report is not None and molecular_metadata is not None: 1776 ms2_annot_report = self.summarize_metabolomics_report(ms2_annot_report) 1777 ms2_annot_report = self.clean_ms2_report(ms2_annot_report) 1778 ms2_annot_report = ms2_annot_report.dropna(axis=1, how="all") 1779 ms2_annot_report = ms2_annot_report.reset_index(drop=False) 1780 else: 1781 ms2_annot_report = None 1782 1783 report = self.combine_reports( 1784 mf_report=mf_report, 1785 ms1_annot_report=ms1_annot_report, 1786 ms2_annot_report=ms2_annot_report 1787 ) 1788 1789 return report
A class to export LCMS metabolite data.
This class provides methods to export LCMS metabolite data to various formats and summarize the metabolite report.
Parameters
- out_file_path (str | Path): The output file path, do not include the file extension.
- mass_spectra (object): The high resolution mass spectra object.
1342 def __init__(self, out_file_path, mass_spectra): 1343 super().__init__(out_file_path, mass_spectra) 1344 self.ion_type_dict = ion_type_dict
This constructor should always be called with keyword arguments. Arguments are:
group should be None; reserved for future extension when a ThreadGroup class is implemented.
target is the callable object to be invoked by the run() method. Defaults to None, meaning nothing is called.
name is the thread name. By default, a unique name is constructed of the form "Thread-N" where N is a small decimal number.
args is a list or tuple of arguments for the target invocation. Defaults to ().
kwargs is a dictionary of keyword arguments for the target invocation. Defaults to {}.
If a subclass overrides the constructor, it must make sure to invoke the base class constructor (Thread.__init__()) before doing anything else to the thread.
1346 @staticmethod 1347 def get_ion_formula(neutral_formula, ion_type): 1348 """From a neutral formula and an ion type, return the formula of the ion. 1349 1350 Notes 1351 ----- 1352 This is a static method. 1353 If the neutral_formula is not a string, this method will return None. 1354 1355 Parameters 1356 ---------- 1357 neutral_formula : str 1358 The neutral formula, this should be a string form from the MolecularFormula class 1359 (e.g. 'C2 H4 O2', isotopes OK), or simple string (e.g. 'C2H4O2', no isotope handling in this case). 1360 In the case of a simple string, the atoms are parsed based on the presence of capital letters, 1361 e.g. MgCl2 is parsed as 'Mg Cl2. 1362 ion_type : str 1363 The ion type, e.g. 'protonated', '[M+H]+', '[M+Na]+', etc. 1364 See the self.ion_type_dict for the available ion types. 1365 1366 Returns 1367 ------- 1368 str 1369 The formula of the ion as a string (like 'C2 H4 O2'); or None if the neutral_formula is not a string. 1370 """ 1371 # If neutral_formula is not a string, return None 1372 if not isinstance(neutral_formula, str): 1373 return None 1374 1375 # Check if there are spaces in the formula (these are outputs of the MolecularFormula class and do not need to be processed before being passed to the class) 1376 if re.search(r"\s", neutral_formula): 1377 neutral_formula = MolecularFormula(neutral_formula, ion_charge=0) 1378 else: 1379 form_pre = re.sub(r"([A-Z])", r" \1", neutral_formula)[1:] 1380 elements = [re.findall(r"[A-Z][a-z]*", x) for x in form_pre.split()] 1381 counts = [re.findall(r"\d+", x) for x in form_pre.split()] 1382 neutral_formula = MolecularFormula( 1383 dict( 1384 zip( 1385 [x[0] for x in elements], 1386 [int(x[0]) if x else 1 for x in counts], 1387 ) 1388 ), 1389 ion_charge=0, 1390 ) 1391 neutral_formula_dict = neutral_formula.to_dict().copy() 1392 1393 adduct_add_dict = ion_type_dict[ion_type][0] 1394 for key in adduct_add_dict: 1395 if key in neutral_formula_dict.keys(): 1396 neutral_formula_dict[key] += adduct_add_dict[key] 1397 else: 1398 neutral_formula_dict[key] = adduct_add_dict[key] 1399 1400 adduct_subtract = ion_type_dict[ion_type][1] 1401 for key in adduct_subtract: 1402 neutral_formula_dict[key] -= adduct_subtract[key] 1403 1404 return MolecularFormula(neutral_formula_dict, ion_charge=0).string
From a neutral formula and an ion type, return the formula of the ion.
Notes
This is a static method. If the neutral_formula is not a string, this method will return None.
Parameters
- neutral_formula (str): The neutral formula, this should be a string form from the MolecularFormula class (e.g. 'C2 H4 O2', isotopes OK), or simple string (e.g. 'C2H4O2', no isotope handling in this case). In the case of a simple string, the atoms are parsed based on the presence of capital letters, e.g. MgCl2 is parsed as 'Mg Cl2.
- ion_type (str): The ion type, e.g. 'protonated', '[M+H]+', '[M+Na]+', etc. See the self.ion_type_dict for the available ion types.
Returns
- str: The formula of the ion as a string (like 'C2 H4 O2'); or None if the neutral_formula is not a string.
1406 @staticmethod 1407 def get_isotope_type(ion_formula): 1408 """From an ion formula, return the 13C isotope type of the ion. 1409 1410 Notes 1411 ----- 1412 This is a static method. 1413 If the ion_formula is not a string, this method will return None. 1414 This is currently only functional for 13C isotopes. 1415 1416 Parameters 1417 ---------- 1418 ion_formula : str 1419 The formula of the ion, expected to be a string like 'C2 H4 O2'. 1420 1421 Returns 1422 ------- 1423 str 1424 The isotope type of the ion, e.g. '13C1', '13C2', etc; or None if the ion_formula does not contain a 13C isotope. 1425 1426 Raises 1427 ------ 1428 ValueError 1429 If the ion_formula is not a string. 1430 """ 1431 if not isinstance(ion_formula, str): 1432 return None 1433 1434 if re.search(r"\s", ion_formula): 1435 ion_formula = MolecularFormula(ion_formula, ion_charge=0) 1436 else: 1437 raise ValueError('ion_formula should be a string like "C2 H4 O2"') 1438 ion_formula_dict = ion_formula.to_dict().copy() 1439 1440 try: 1441 iso_class = "13C" + str(ion_formula_dict.pop("13C")) 1442 except KeyError: 1443 iso_class = None 1444 1445 return iso_class
From an ion formula, return the 13C isotope type of the ion.
Notes
This is a static method. If the ion_formula is not a string, this method will return None. This is currently only functional for 13C isotopes.
Parameters
- ion_formula (str): The formula of the ion, expected to be a string like 'C2 H4 O2'.
Returns
- str: The isotope type of the ion, e.g. '13C1', '13C2', etc; or None if the ion_formula does not contain a 13C isotope.
Raises
- ValueError: If the ion_formula is not a string.
1447 def report_to_csv(self, molecular_metadata=None): 1448 """Create a report of the mass features and their annotations and save it as a CSV file. 1449 1450 Parameters 1451 ---------- 1452 molecular_metadata : dict, optional 1453 The molecular metadata. Default is None. 1454 """ 1455 report = self.to_report(molecular_metadata=molecular_metadata) 1456 out_file = self.output_file.with_suffix(".csv") 1457 report.to_csv(out_file, index=False)
Create a report of the mass features and their annotations and save it as a CSV file.
Parameters
- molecular_metadata (dict, optional): The molecular metadata. Default is None.
1459 def clean_ms1_report(self, ms1_summary_full): 1460 """Clean the MS1 report. 1461 1462 Parameters 1463 ---------- 1464 ms1_summary_full : DataFrame 1465 The full MS1 summary DataFrame. 1466 1467 Returns 1468 ------- 1469 DataFrame 1470 The cleaned MS1 summary DataFrame. 1471 """ 1472 ms1_summary_full = ms1_summary_full.reset_index() 1473 cols_to_keep = [ 1474 "mf_id", 1475 "Molecular Formula", 1476 "Ion Type", 1477 "Calculated m/z", 1478 "m/z Error (ppm)", 1479 "m/z Error Score", 1480 "Is Isotopologue", 1481 "Isotopologue Similarity", 1482 "Confidence Score", 1483 ] 1484 ms1_summary = ms1_summary_full[cols_to_keep].copy() 1485 ms1_summary["ion_formula"] = [ 1486 self.get_ion_formula(f, a) 1487 for f, a in zip(ms1_summary["Molecular Formula"], ms1_summary["Ion Type"]) 1488 ] 1489 ms1_summary["isotopologue_type"] = [ 1490 self.get_isotope_type(f) for f in ms1_summary["ion_formula"].tolist() 1491 ] 1492 1493 # Reorder columns 1494 ms1_summary = ms1_summary[ 1495 [ 1496 "mf_id", 1497 "ion_formula", 1498 "isotopologue_type", 1499 "Calculated m/z", 1500 "m/z Error (ppm)", 1501 "m/z Error Score", 1502 "Isotopologue Similarity", 1503 "Confidence Score", 1504 ] 1505 ] 1506 1507 # Set the index to mf_id 1508 ms1_summary = ms1_summary.set_index("mf_id") 1509 1510 return ms1_summary
Clean the MS1 report.
Parameters
- ms1_summary_full (DataFrame): The full MS1 summary DataFrame.
Returns
- DataFrame: The cleaned MS1 summary DataFrame.
1512 def summarize_ms2_report(self, ms2_annot_report): 1513 """ 1514 Summarize the MS2 report. 1515 1516 Parameters 1517 ---------- 1518 ms2_annot_report : DataFrame 1519 The MS2 annotation DataFrame with all annotations, output of mass_features_ms2_annot_to_df. 1520 1521 Returns 1522 ------- 1523 """
Summarize the MS2 report.
Parameters
- ms2_annot_report (DataFrame): The MS2 annotation DataFrame with all annotations, output of mass_features_ms2_annot_to_df.
- Returns
- -------
1525 def summarize_metabolomics_report(self, ms2_annot_report): 1526 """Summarize the MS2 hits for a metabolomics report 1527 1528 Parameters 1529 ---------- 1530 ms2_annot : DataFrame 1531 The MS2 annotation DataFrame with all annotations. 1532 1533 Returns 1534 ------- 1535 DataFrame 1536 The summarized metabolomics report. 1537 """ 1538 columns_to_drop = [ 1539 "precursor_mz", 1540 "precursor_mz_error_ppm", 1541 "cas", 1542 "data_id", 1543 "iupac_name", 1544 "traditional_name", 1545 "common_name", 1546 "casno", 1547 ] 1548 ms2_annot = ms2_annot_report.drop( 1549 columns=[col for col in columns_to_drop if col in ms2_annot_report.columns] 1550 ) 1551 1552 # Prepare information about the search results, pulling out the best hit for the single report 1553 # Group by mf_id,ref_mol_id grab row with highest entropy similarity 1554 ms2_annot = ms2_annot.reset_index() 1555 # Add column called "n_spectra_contributing" that is the number of unique values in query_spectrum_id per mf_id,ref_mol_id 1556 ms2_annot["n_spectra_contributing"] = ( 1557 ms2_annot.groupby(["mf_id", "ref_mol_id"])["query_spectrum_id"] 1558 .transform("nunique") 1559 ) 1560 # Sort by entropy similarity 1561 ms2_annot = ms2_annot.sort_values( 1562 by=["mf_id", "ref_mol_id", "entropy_similarity"], ascending=[True, True, False] 1563 ) 1564 best_entropy = ms2_annot.drop_duplicates( 1565 subset=["mf_id", "ref_mol_id"], keep="first" 1566 ) 1567 1568 return best_entropy
Summarize the MS2 hits for a metabolomics report
Parameters
- ms2_annot (DataFrame): The MS2 annotation DataFrame with all annotations.
Returns
- DataFrame: The summarized metabolomics report.
1570 def clean_ms2_report(self, metabolite_summary): 1571 """Clean the MS2 report. 1572 1573 Parameters 1574 ---------- 1575 metabolite_summary : DataFrame 1576 The full metabolomics summary DataFrame. 1577 1578 Returns 1579 ------- 1580 DataFrame 1581 The cleaned metabolomics summary DataFrame. 1582 """ 1583 metabolite_summary = metabolite_summary.reset_index() 1584 metabolite_summary["ion_formula"] = [ 1585 self.get_ion_formula(f, a) 1586 for f, a in zip(metabolite_summary["formula"], metabolite_summary["ref_ion_type"]) 1587 ] 1588 1589 col_order = [ 1590 "mf_id", 1591 "ion_formula", 1592 "ref_ion_type", 1593 "formula", 1594 "inchikey", 1595 "name", 1596 "inchi", 1597 "chebi", 1598 "smiles", 1599 "kegg", 1600 "cas", 1601 "database_name", 1602 "ref_ms_id", 1603 "entropy_similarity", 1604 "ref_mz_in_query_fract", 1605 "n_spectra_contributing", 1606 ] 1607 1608 # Reorder columns 1609 metabolite_summary = metabolite_summary[ 1610 [col for col in col_order if col in metabolite_summary.columns] 1611 ] 1612 1613 # Convert chebi (if present) to int: 1614 if "chebi" in metabolite_summary.columns: 1615 metabolite_summary["chebi"] = metabolite_summary["chebi"].astype( 1616 "Int64", errors="ignore" 1617 ) 1618 1619 # Set the index to mf_id 1620 metabolite_summary = metabolite_summary.set_index("mf_id") 1621 1622 return metabolite_summary
Clean the MS2 report.
Parameters
- metabolite_summary (DataFrame): The full metabolomics summary DataFrame.
Returns
- DataFrame: The cleaned metabolomics summary DataFrame.
1624 def combine_reports(self, mf_report, ms1_annot_report, ms2_annot_report): 1625 """Combine the mass feature report with the MS1 and MS2 reports. 1626 1627 Parameters 1628 ---------- 1629 mf_report : DataFrame 1630 The mass feature report DataFrame. 1631 ms1_annot_report : DataFrame 1632 The MS1 annotation report DataFrame. 1633 ms2_annot_report : DataFrame 1634 The MS2 annotation report DataFrame. 1635 """ 1636 # If there is an ms1_annot_report, merge it with the mf_report 1637 if ms1_annot_report is not None and not ms1_annot_report.empty: 1638 # MS1 has been run and has molecular formula information 1639 mf_report = pd.merge( 1640 mf_report, 1641 ms1_annot_report, 1642 how="left", 1643 on=["mf_id", "isotopologue_type"], 1644 ) 1645 if ms2_annot_report is not None: 1646 # If both reports contain 'ion_formula', prefer a merge that respects it. 1647 # Otherwise fall back to merging on 'mf_id' only to remain robust when 1648 # MS1 formula assignment wasn't performed or MS2 summary lacks the field. 1649 if "ion_formula" in mf_report.columns and "ion_formula" in ms2_annot_report.columns: 1650 # pull out the records without ion_formula and merge on mf_id only 1651 mf_no_ion_formula = mf_report[mf_report["ion_formula"].isna()] 1652 mf_no_ion_formula = mf_no_ion_formula.drop(columns=["ion_formula"]) if "ion_formula" in mf_no_ion_formula.columns else mf_no_ion_formula 1653 mf_no_ion_formula = pd.merge( 1654 mf_no_ion_formula, ms2_annot_report, how="left", on=["mf_id"] 1655 ) 1656 1657 # pull out the records with ion_formula and merge on mf_id + ion_formula 1658 mf_with_ion_formula = mf_report[~mf_report["ion_formula"].isna()] 1659 mf_with_ion_formula = pd.merge( 1660 mf_with_ion_formula, 1661 ms2_annot_report, 1662 how="left", 1663 on=["mf_id", "ion_formula"], 1664 ) 1665 1666 # put back together 1667 mf_report = pd.concat([mf_no_ion_formula, mf_with_ion_formula]) 1668 else: 1669 # Fall back to merging on mf_id only (robust when ion_formula missing) 1670 mf_report = pd.merge( 1671 mf_report, ms2_annot_report, how="left", on=["mf_id"] 1672 ) 1673 1674 # Rename colums 1675 rename_dict = { 1676 "mf_id": "Mass Feature ID", 1677 "scan_time": "Retention Time (min)", 1678 "mz": "m/z", 1679 "apex_scan": "Apex Scan Number", 1680 "intensity": "Intensity", 1681 "persistence": "Persistence", 1682 "area": "Area", 1683 "half_height_width": "Half Height Width (min)", 1684 "tailing_factor": "Tailing Factor", 1685 "dispersity_index": "Dispersity Index", 1686 "ms2_spectrum": "MS2 Spectrum", 1687 "monoisotopic_mf_id": "Monoisotopic Mass Feature ID", 1688 "isotopologue_type": "Isotopologue Type", 1689 "mass_spectrum_deconvoluted_parent": "Is Largest Ion after Deconvolution", 1690 "associated_mass_features": "Associated Mass Features after Deconvolution", 1691 "ion_formula": "Ion Formula", 1692 "formula": "Molecular Formula", 1693 "ref_ion_type": "Ion Type", 1694 "annot_level": "Lipid Annotation Level", 1695 "lipid_molecular_species_id": "Lipid Molecular Species", 1696 "lipid_summed_name": "Lipid Species", 1697 "lipid_subclass": "Lipid Subclass", 1698 "lipid_class": "Lipid Class", 1699 "lipid_category": "Lipid Category", 1700 "entropy_similarity": "Entropy Similarity", 1701 "ref_mz_in_query_fract": "Library mzs in Query (fraction)", 1702 "n_spectra_contributing": "Spectra with Annotation (n)", 1703 } 1704 mf_report = mf_report.rename(columns=rename_dict) 1705 mf_report["Sample Name"] = self.mass_spectra.sample_name 1706 mf_report["Polarity"] = self.mass_spectra.polarity 1707 mf_report = mf_report[ 1708 ["Mass Feature ID", "Sample Name", "Polarity"] 1709 + [ 1710 col 1711 for col in mf_report.columns 1712 if col not in ["Mass Feature ID", "Sample Name", "Polarity"] 1713 ] 1714 ] 1715 1716 # Reorder rows by "Mass Feature ID", then "Entropy Similarity" (descending), then "Confidence Score" (descending) 1717 if "Entropy Similarity" in mf_report.columns and "Confidence Score" in mf_report.columns: 1718 mf_report = mf_report.sort_values( 1719 by=["Mass Feature ID", "Entropy Similarity", "Confidence Score"], 1720 ascending=[True, False, False], 1721 ) 1722 elif "Entropy Similarity" in mf_report.columns: 1723 mf_report = mf_report.sort_values( 1724 by=["Mass Feature ID", "Entropy Similarity"], 1725 ascending=[True, False], 1726 ) 1727 elif "Confidence Score" in mf_report.columns: 1728 mf_report = mf_report.sort_values( 1729 by=["Mass Feature ID", "Confidence Score"], 1730 ascending=[True, False], 1731 ) 1732 # If neither "Entropy Similarity" nor "Confidence Score" are in the columns, just sort by "Mass Feature ID" 1733 else: 1734 mf_report = mf_report.sort_values("Mass Feature ID") 1735 1736 # Reset index 1737 mf_report = mf_report.reset_index(drop=True) 1738 1739 return mf_report
Combine the mass feature report with the MS1 and MS2 reports.
Parameters
- mf_report (DataFrame): The mass feature report DataFrame.
- ms1_annot_report (DataFrame): The MS1 annotation report DataFrame.
- ms2_annot_report (DataFrame): The MS2 annotation report DataFrame.
1741 def to_report(self, molecular_metadata=None, suppress_warnings=False): 1742 """Create a report of the mass features and their annotations. 1743 1744 Parameters 1745 ---------- 1746 molecular_metadata : dict, optional 1747 The molecular metadata. Default is None. 1748 suppress_warnings : bool, optional 1749 If True, suppresses warnings from mass_features_ms2_annot_to_df. 1750 Default is False. 1751 1752 Returns 1753 ------- 1754 DataFrame 1755 The report as a Pandas DataFrame. 1756 """ 1757 # Get mass feature dataframe 1758 mf_report = self.mass_spectra.mass_features_to_df() 1759 mf_report = mf_report.reset_index(drop=False) 1760 1761 # Get and clean ms1 annotation dataframe 1762 ms1_annot_report = self.mass_spectra.mass_features_ms1_annot_to_df(suppress_warnings=suppress_warnings) 1763 if ms1_annot_report is not None: 1764 ms1_annot_report = ms1_annot_report.copy() 1765 ms1_annot_report = self.clean_ms1_report(ms1_annot_report) 1766 ms1_annot_report = ms1_annot_report.reset_index(drop=False) 1767 else: 1768 ms1_annot_report = None 1769 1770 # Get, summarize, and clean ms2 annotation dataframe 1771 ms2_annot_report = self.mass_spectra.mass_features_ms2_annot_to_df( 1772 molecular_metadata=molecular_metadata, 1773 suppress_warnings=suppress_warnings 1774 ) 1775 if ms2_annot_report is not None and molecular_metadata is not None: 1776 ms2_annot_report = self.summarize_metabolomics_report(ms2_annot_report) 1777 ms2_annot_report = self.clean_ms2_report(ms2_annot_report) 1778 ms2_annot_report = ms2_annot_report.dropna(axis=1, how="all") 1779 ms2_annot_report = ms2_annot_report.reset_index(drop=False) 1780 else: 1781 ms2_annot_report = None 1782 1783 report = self.combine_reports( 1784 mf_report=mf_report, 1785 ms1_annot_report=ms1_annot_report, 1786 ms2_annot_report=ms2_annot_report 1787 ) 1788 1789 return report
Create a report of the mass features and their annotations.
Parameters
- molecular_metadata (dict, optional): The molecular metadata. Default is None.
- suppress_warnings (bool, optional): If True, suppresses warnings from mass_features_ms2_annot_to_df. Default is False.
Returns
- DataFrame: The report as a Pandas DataFrame.
Inherited Members
1790class LipidomicsExport(LCMSMetabolomicsExport): 1791 """A class to export lipidomics data. 1792 1793 This class provides methods to export lipidomics data to various formats and summarize the lipid report. 1794 1795 Parameters 1796 ---------- 1797 out_file_path : str | Path 1798 The output file path, do not include the file extension. 1799 mass_spectra : object 1800 The high resolution mass spectra object. 1801 """ 1802 1803 def __init__(self, out_file_path, mass_spectra): 1804 super().__init__(out_file_path, mass_spectra) 1805 1806 def summarize_lipid_report(self, ms2_annot): 1807 """Summarize the lipid report. 1808 1809 Parameters 1810 ---------- 1811 ms2_annot : DataFrame 1812 The MS2 annotation DataFrame with all annotations. 1813 1814 Returns 1815 ------- 1816 DataFrame 1817 The summarized lipid report. 1818 """ 1819 # Drop unnecessary columns for easier viewing 1820 columns_to_drop = [ 1821 "precursor_mz", 1822 "precursor_mz_error_ppm", 1823 "ref_mol_id", 1824 "ref_precursor_mz", 1825 "cas", 1826 "inchikey", 1827 "inchi", 1828 "chebi", 1829 "smiles", 1830 "kegg", 1831 "data_id", 1832 "iupac_name", 1833 "traditional_name", 1834 "common_name", 1835 "casno", 1836 ] 1837 ms2_annot = ms2_annot.drop( 1838 columns=[col for col in columns_to_drop if col in ms2_annot.columns] 1839 ) 1840 1841 # If ion_types_excluded is not empty, remove those ion types 1842 ion_types_excluded = self.mass_spectra.parameters.mass_spectrum[ 1843 "ms2" 1844 ].molecular_search.ion_types_excluded 1845 if len(ion_types_excluded) > 0: 1846 ms2_annot = ms2_annot[~ms2_annot["ref_ion_type"].isin(ion_types_excluded)] 1847 1848 # If mf_id is not present, check that the index name is mf_id and reset the index 1849 if "mf_id" not in ms2_annot.columns: 1850 if ms2_annot.index.name == "mf_id": 1851 ms2_annot = ms2_annot.reset_index() 1852 else: 1853 raise ValueError("mf_id is not present in the dataframe") 1854 1855 # Attempt to get consensus annotations to the MLF level 1856 mlf_results_all = [] 1857 for mf_id in ms2_annot["mf_id"].unique(): 1858 mlf_results_perid = [] 1859 ms2_annot_mf = ms2_annot[ms2_annot["mf_id"] == mf_id].copy() 1860 ms2_annot_mf["n_spectra_contributing"] = ms2_annot_mf.query_spectrum_id.nunique() 1861 1862 for query_scan in ms2_annot["query_spectrum_id"].unique(): 1863 ms2_annot_sub = ms2_annot_mf[ 1864 ms2_annot_mf["query_spectrum_id"] == query_scan 1865 ].copy() 1866 1867 if ms2_annot_sub["lipid_summed_name"].nunique() == 1: 1868 # If there is only one lipid_summed_name, let's try to get consensus molecular species annotation 1869 if ms2_annot_sub["lipid_summed_name"].nunique() == 1: 1870 ms2_annot_sub["entropy_max"] = ( 1871 ms2_annot_sub["entropy_similarity"] 1872 == ms2_annot_sub["entropy_similarity"].max() 1873 ) 1874 ms2_annot_sub["ref_match_fract_max"] = ( 1875 ms2_annot_sub["ref_mz_in_query_fract"] 1876 == ms2_annot_sub["ref_mz_in_query_fract"].max() 1877 ) 1878 ms2_annot_sub["frag_max"] = ms2_annot_sub[ 1879 "query_frag_types" 1880 ].apply(lambda x: True if "MLF" in x else False) 1881 1882 # New column that looks if there is a consensus between the ranks (one row that is highest in all ranks) 1883 ms2_annot_sub["consensus"] = ms2_annot_sub[ 1884 ["entropy_max", "ref_match_fract_max", "frag_max"] 1885 ].all(axis=1) 1886 1887 # If there is a consensus, take the row with the highest entropy_similarity 1888 if ms2_annot_sub["consensus"].any(): 1889 ms2_annot_sub = ms2_annot_sub[ 1890 ms2_annot_sub["entropy_similarity"] 1891 == ms2_annot_sub["entropy_similarity"].max() 1892 ].head(1) 1893 mlf_results_perid.append(ms2_annot_sub) 1894 if len(mlf_results_perid) == 0: 1895 mlf_results_perid = pd.DataFrame() 1896 else: 1897 mlf_results_perid = pd.concat(mlf_results_perid) 1898 if mlf_results_perid["name"].nunique() == 1: 1899 mlf_results_perid = mlf_results_perid[ 1900 mlf_results_perid["entropy_similarity"] 1901 == mlf_results_perid["entropy_similarity"].max() 1902 ].head(1) 1903 else: 1904 mlf_results_perid = pd.DataFrame() 1905 mlf_results_all.append(mlf_results_perid) 1906 1907 # These are the consensus annotations to the MLF level 1908 if len(mlf_results_all) > 0: 1909 mlf_results_all = pd.concat(mlf_results_all) 1910 mlf_results_all["annot_level"] = mlf_results_all["structure_level"] 1911 else: 1912 # Make an empty dataframe 1913 mlf_results_all = ms2_annot.head(0) 1914 1915 # For remaining mf_ids, try to get a consensus annotation to the species level 1916 species_results_all = [] 1917 # Remove mf_ids that have consensus annotations to the MLF level 1918 ms2_annot_spec = ms2_annot[ 1919 ~ms2_annot["mf_id"].isin(mlf_results_all["mf_id"].unique()) 1920 ] 1921 for mf_id in ms2_annot_spec["mf_id"].unique(): 1922 # Do all the hits have the same lipid_summed_name? 1923 ms2_annot_sub = ms2_annot_spec[ms2_annot_spec["mf_id"] == mf_id].copy() 1924 ms2_annot_sub["n_spectra_contributing"] = len(ms2_annot_sub) 1925 1926 if ms2_annot_sub["lipid_summed_name"].nunique() == 1: 1927 # Grab the highest entropy_similarity result 1928 ms2_annot_sub = ms2_annot_sub[ 1929 ms2_annot_sub["entropy_similarity"] 1930 == ms2_annot_sub["entropy_similarity"].max() 1931 ].head(1) 1932 species_results_all.append(ms2_annot_sub) 1933 1934 # These are the consensus annotations to the species level 1935 if len(species_results_all) > 0: 1936 species_results_all = pd.concat(species_results_all) 1937 species_results_all["annot_level"] = "species" 1938 else: 1939 # Make an empty dataframe 1940 species_results_all = ms2_annot.head(0) 1941 1942 # Deal with the remaining mf_ids that do not have consensus annotations to the species level or MLF level 1943 # Remove mf_ids that have consensus annotations to the species level 1944 ms2_annot_remaining = ms2_annot_spec[ 1945 ~ms2_annot_spec["mf_id"].isin(species_results_all["mf_id"].unique()) 1946 ] 1947 no_consensus = [] 1948 for mf_id in ms2_annot_remaining["mf_id"].unique(): 1949 id_sub = [] 1950 id_no_con = [] 1951 ms2_annot_sub_mf = ms2_annot_remaining[ 1952 ms2_annot_remaining["mf_id"] == mf_id 1953 ].copy() 1954 for query_scan in ms2_annot_sub_mf["query_spectrum_id"].unique(): 1955 ms2_annot_sub = ms2_annot_sub_mf[ 1956 ms2_annot_sub_mf["query_spectrum_id"] == query_scan 1957 ].copy() 1958 1959 # New columns for ranking [HIGHER RANK = BETTER] 1960 ms2_annot_sub["entropy_max"] = ( 1961 ms2_annot_sub["entropy_similarity"] 1962 == ms2_annot_sub["entropy_similarity"].max() 1963 ) 1964 ms2_annot_sub["ref_match_fract_max"] = ( 1965 ms2_annot_sub["ref_mz_in_query_fract"] 1966 == ms2_annot_sub["ref_mz_in_query_fract"].max() 1967 ) 1968 ms2_annot_sub["frag_max"] = ms2_annot_sub["query_frag_types"].apply( 1969 lambda x: True if "MLF" in x else False 1970 ) 1971 1972 # New column that looks if there is a consensus between the ranks (one row that is highest in all ranks) 1973 ms2_annot_sub["consensus"] = ms2_annot_sub[ 1974 ["entropy_max", "ref_match_fract_max", "frag_max"] 1975 ].all(axis=1) 1976 ms2_annot_sub_con = ms2_annot_sub[ms2_annot_sub["consensus"]] 1977 id_sub.append(ms2_annot_sub_con) 1978 id_no_con.append(ms2_annot_sub) 1979 id_sub = pd.concat(id_sub) 1980 id_no_con = pd.concat(id_no_con) 1981 1982 # Scenario 1: Multiple scans are being resolved to different MLFs [could be coelutions and should both be kept and annotated to MS level] 1983 if ( 1984 id_sub["query_frag_types"] 1985 .apply(lambda x: True if "MLF" in x else False) 1986 .all() 1987 and len(id_sub) > 0 1988 ): 1989 idx = id_sub.groupby("name")["entropy_similarity"].idxmax() 1990 id_sub = id_sub.loc[idx] 1991 # Reorder so highest entropy_similarity is first 1992 id_sub = id_sub.sort_values("entropy_similarity", ascending=False) 1993 id_sub["annot_level"] = id_sub["structure_level"] 1994 no_consensus.append(id_sub) 1995 1996 # Scenario 2: Multiple scans are being resolved to different species, keep both and annotate to appropriate level 1997 elif len(id_sub) == 0: 1998 for lipid_summed_name in id_no_con["lipid_summed_name"].unique(): 1999 summed_sub = id_no_con[ 2000 id_no_con["lipid_summed_name"] == lipid_summed_name 2001 ] 2002 # Any consensus to MLF? 2003 if summed_sub["consensus"].any(): 2004 summed_sub = summed_sub[summed_sub["consensus"]] 2005 summed_sub["annot_level"] = summed_sub["structure_level"] 2006 no_consensus.append(summed_sub) 2007 else: 2008 # Grab the highest entropy_similarity, if there are multiple, grab the first one 2009 summed_sub = summed_sub[ 2010 summed_sub["entropy_similarity"] 2011 == summed_sub["entropy_similarity"].max() 2012 ].head(1) 2013 # get first row 2014 summed_sub["annot_level"] = "species" 2015 summed_sub["name"] = "" 2016 no_consensus.append(summed_sub) 2017 else: 2018 raise ValueError("Unexpected scenario for summarizing mf_id: ", mf_id) 2019 2020 if len(no_consensus) > 0: 2021 no_consensus = pd.concat(no_consensus) 2022 else: 2023 no_consensus = ms2_annot.head(0) 2024 2025 # Combine all the consensus annotations and reformat the dataframe for output 2026 species_results_all = species_results_all.drop(columns=["name"]) 2027 species_results_all["lipid_molecular_species_id"] = "" 2028 mlf_results_all["lipid_molecular_species_id"] = mlf_results_all["name"] 2029 no_consensus["lipid_molecular_species_id"] = no_consensus["name"] 2030 consensus_annotations = pd.concat( 2031 [mlf_results_all, species_results_all, no_consensus] 2032 ) 2033 consensus_annotations = consensus_annotations.sort_values( 2034 "mf_id", ascending=True 2035 ) 2036 cols_to_keep = [ 2037 "mf_id", 2038 "ref_ion_type", 2039 "entropy_similarity", 2040 "ref_mz_in_query_fract", 2041 "lipid_molecular_species_id", 2042 "lipid_summed_name", 2043 "lipid_subclass", 2044 "lipid_class", 2045 "lipid_category", 2046 "formula", 2047 "annot_level", 2048 "n_spectra_contributing", 2049 ] 2050 consensus_annotations = consensus_annotations[cols_to_keep] 2051 consensus_annotations = consensus_annotations.set_index("mf_id") 2052 2053 return consensus_annotations 2054 2055 def clean_ms2_report(self, lipid_summary): 2056 """Clean the MS2 report. 2057 2058 Parameters 2059 ---------- 2060 lipid_summary : DataFrame 2061 The full lipid summary DataFrame. 2062 2063 Returns 2064 ------- 2065 DataFrame 2066 The cleaned lipid summary DataFrame. 2067 """ 2068 lipid_summary = lipid_summary.reset_index() 2069 lipid_summary["ion_formula"] = [ 2070 self.get_ion_formula(f, a) 2071 for f, a in zip(lipid_summary["formula"], lipid_summary["ref_ion_type"]) 2072 ] 2073 2074 # Reorder columns 2075 lipid_summary = lipid_summary[ 2076 [ 2077 "mf_id", 2078 "ion_formula", 2079 "ref_ion_type", 2080 "formula", 2081 "annot_level", 2082 "lipid_molecular_species_id", 2083 "lipid_summed_name", 2084 "lipid_subclass", 2085 "lipid_class", 2086 "lipid_category", 2087 "entropy_similarity", 2088 "ref_mz_in_query_fract", 2089 "n_spectra_contributing", 2090 ] 2091 ] 2092 2093 # Set the index to mf_id 2094 lipid_summary = lipid_summary.set_index("mf_id") 2095 2096 return lipid_summary 2097 2098 def to_report(self, molecular_metadata=None): 2099 """Create a report of the mass features and their annotations. 2100 2101 Parameters 2102 ---------- 2103 molecular_metadata : dict, optional 2104 The molecular metadata. Default is None. 2105 2106 Returns 2107 ------- 2108 DataFrame 2109 The report of the mass features and their annotations. 2110 2111 Notes 2112 ----- 2113 The report will contain the mass features and their annotations from MS1 and MS2 (if available). 2114 """ 2115 # Get mass feature dataframe 2116 mf_report = self.mass_spectra.mass_features_to_df() 2117 mf_report = mf_report.reset_index(drop=False) 2118 2119 # Get and clean ms1 annotation dataframe 2120 ms1_annot_report = self.mass_spectra.mass_features_ms1_annot_to_df().copy() 2121 ms1_annot_report = self.clean_ms1_report(ms1_annot_report) 2122 ms1_annot_report = ms1_annot_report.reset_index(drop=False) 2123 2124 # Get, summarize, and clean ms2 annotation dataframe 2125 ms2_annot_report = self.mass_spectra.mass_features_ms2_annot_to_df( 2126 molecular_metadata=molecular_metadata 2127 ) 2128 if ms2_annot_report is not None and molecular_metadata is not None: 2129 ms2_annot_report = self.summarize_lipid_report(ms2_annot_report) 2130 ms2_annot_report = self.clean_ms2_report(ms2_annot_report) 2131 ms2_annot_report = ms2_annot_report.dropna(axis=1, how="all") 2132 ms2_annot_report = ms2_annot_report.reset_index(drop=False) 2133 report = self.combine_reports( 2134 mf_report=mf_report, 2135 ms1_annot_report=ms1_annot_report, 2136 ms2_annot_report=ms2_annot_report 2137 ) 2138 return report
A class to export lipidomics data.
This class provides methods to export lipidomics data to various formats and summarize the lipid report.
Parameters
- out_file_path (str | Path): The output file path, do not include the file extension.
- mass_spectra (object): The high resolution mass spectra object.
1803 def __init__(self, out_file_path, mass_spectra): 1804 super().__init__(out_file_path, mass_spectra)
This constructor should always be called with keyword arguments. Arguments are:
group should be None; reserved for future extension when a ThreadGroup class is implemented.
target is the callable object to be invoked by the run() method. Defaults to None, meaning nothing is called.
name is the thread name. By default, a unique name is constructed of the form "Thread-N" where N is a small decimal number.
args is a list or tuple of arguments for the target invocation. Defaults to ().
kwargs is a dictionary of keyword arguments for the target invocation. Defaults to {}.
If a subclass overrides the constructor, it must make sure to invoke the base class constructor (Thread.__init__()) before doing anything else to the thread.
1806 def summarize_lipid_report(self, ms2_annot): 1807 """Summarize the lipid report. 1808 1809 Parameters 1810 ---------- 1811 ms2_annot : DataFrame 1812 The MS2 annotation DataFrame with all annotations. 1813 1814 Returns 1815 ------- 1816 DataFrame 1817 The summarized lipid report. 1818 """ 1819 # Drop unnecessary columns for easier viewing 1820 columns_to_drop = [ 1821 "precursor_mz", 1822 "precursor_mz_error_ppm", 1823 "ref_mol_id", 1824 "ref_precursor_mz", 1825 "cas", 1826 "inchikey", 1827 "inchi", 1828 "chebi", 1829 "smiles", 1830 "kegg", 1831 "data_id", 1832 "iupac_name", 1833 "traditional_name", 1834 "common_name", 1835 "casno", 1836 ] 1837 ms2_annot = ms2_annot.drop( 1838 columns=[col for col in columns_to_drop if col in ms2_annot.columns] 1839 ) 1840 1841 # If ion_types_excluded is not empty, remove those ion types 1842 ion_types_excluded = self.mass_spectra.parameters.mass_spectrum[ 1843 "ms2" 1844 ].molecular_search.ion_types_excluded 1845 if len(ion_types_excluded) > 0: 1846 ms2_annot = ms2_annot[~ms2_annot["ref_ion_type"].isin(ion_types_excluded)] 1847 1848 # If mf_id is not present, check that the index name is mf_id and reset the index 1849 if "mf_id" not in ms2_annot.columns: 1850 if ms2_annot.index.name == "mf_id": 1851 ms2_annot = ms2_annot.reset_index() 1852 else: 1853 raise ValueError("mf_id is not present in the dataframe") 1854 1855 # Attempt to get consensus annotations to the MLF level 1856 mlf_results_all = [] 1857 for mf_id in ms2_annot["mf_id"].unique(): 1858 mlf_results_perid = [] 1859 ms2_annot_mf = ms2_annot[ms2_annot["mf_id"] == mf_id].copy() 1860 ms2_annot_mf["n_spectra_contributing"] = ms2_annot_mf.query_spectrum_id.nunique() 1861 1862 for query_scan in ms2_annot["query_spectrum_id"].unique(): 1863 ms2_annot_sub = ms2_annot_mf[ 1864 ms2_annot_mf["query_spectrum_id"] == query_scan 1865 ].copy() 1866 1867 if ms2_annot_sub["lipid_summed_name"].nunique() == 1: 1868 # If there is only one lipid_summed_name, let's try to get consensus molecular species annotation 1869 if ms2_annot_sub["lipid_summed_name"].nunique() == 1: 1870 ms2_annot_sub["entropy_max"] = ( 1871 ms2_annot_sub["entropy_similarity"] 1872 == ms2_annot_sub["entropy_similarity"].max() 1873 ) 1874 ms2_annot_sub["ref_match_fract_max"] = ( 1875 ms2_annot_sub["ref_mz_in_query_fract"] 1876 == ms2_annot_sub["ref_mz_in_query_fract"].max() 1877 ) 1878 ms2_annot_sub["frag_max"] = ms2_annot_sub[ 1879 "query_frag_types" 1880 ].apply(lambda x: True if "MLF" in x else False) 1881 1882 # New column that looks if there is a consensus between the ranks (one row that is highest in all ranks) 1883 ms2_annot_sub["consensus"] = ms2_annot_sub[ 1884 ["entropy_max", "ref_match_fract_max", "frag_max"] 1885 ].all(axis=1) 1886 1887 # If there is a consensus, take the row with the highest entropy_similarity 1888 if ms2_annot_sub["consensus"].any(): 1889 ms2_annot_sub = ms2_annot_sub[ 1890 ms2_annot_sub["entropy_similarity"] 1891 == ms2_annot_sub["entropy_similarity"].max() 1892 ].head(1) 1893 mlf_results_perid.append(ms2_annot_sub) 1894 if len(mlf_results_perid) == 0: 1895 mlf_results_perid = pd.DataFrame() 1896 else: 1897 mlf_results_perid = pd.concat(mlf_results_perid) 1898 if mlf_results_perid["name"].nunique() == 1: 1899 mlf_results_perid = mlf_results_perid[ 1900 mlf_results_perid["entropy_similarity"] 1901 == mlf_results_perid["entropy_similarity"].max() 1902 ].head(1) 1903 else: 1904 mlf_results_perid = pd.DataFrame() 1905 mlf_results_all.append(mlf_results_perid) 1906 1907 # These are the consensus annotations to the MLF level 1908 if len(mlf_results_all) > 0: 1909 mlf_results_all = pd.concat(mlf_results_all) 1910 mlf_results_all["annot_level"] = mlf_results_all["structure_level"] 1911 else: 1912 # Make an empty dataframe 1913 mlf_results_all = ms2_annot.head(0) 1914 1915 # For remaining mf_ids, try to get a consensus annotation to the species level 1916 species_results_all = [] 1917 # Remove mf_ids that have consensus annotations to the MLF level 1918 ms2_annot_spec = ms2_annot[ 1919 ~ms2_annot["mf_id"].isin(mlf_results_all["mf_id"].unique()) 1920 ] 1921 for mf_id in ms2_annot_spec["mf_id"].unique(): 1922 # Do all the hits have the same lipid_summed_name? 1923 ms2_annot_sub = ms2_annot_spec[ms2_annot_spec["mf_id"] == mf_id].copy() 1924 ms2_annot_sub["n_spectra_contributing"] = len(ms2_annot_sub) 1925 1926 if ms2_annot_sub["lipid_summed_name"].nunique() == 1: 1927 # Grab the highest entropy_similarity result 1928 ms2_annot_sub = ms2_annot_sub[ 1929 ms2_annot_sub["entropy_similarity"] 1930 == ms2_annot_sub["entropy_similarity"].max() 1931 ].head(1) 1932 species_results_all.append(ms2_annot_sub) 1933 1934 # These are the consensus annotations to the species level 1935 if len(species_results_all) > 0: 1936 species_results_all = pd.concat(species_results_all) 1937 species_results_all["annot_level"] = "species" 1938 else: 1939 # Make an empty dataframe 1940 species_results_all = ms2_annot.head(0) 1941 1942 # Deal with the remaining mf_ids that do not have consensus annotations to the species level or MLF level 1943 # Remove mf_ids that have consensus annotations to the species level 1944 ms2_annot_remaining = ms2_annot_spec[ 1945 ~ms2_annot_spec["mf_id"].isin(species_results_all["mf_id"].unique()) 1946 ] 1947 no_consensus = [] 1948 for mf_id in ms2_annot_remaining["mf_id"].unique(): 1949 id_sub = [] 1950 id_no_con = [] 1951 ms2_annot_sub_mf = ms2_annot_remaining[ 1952 ms2_annot_remaining["mf_id"] == mf_id 1953 ].copy() 1954 for query_scan in ms2_annot_sub_mf["query_spectrum_id"].unique(): 1955 ms2_annot_sub = ms2_annot_sub_mf[ 1956 ms2_annot_sub_mf["query_spectrum_id"] == query_scan 1957 ].copy() 1958 1959 # New columns for ranking [HIGHER RANK = BETTER] 1960 ms2_annot_sub["entropy_max"] = ( 1961 ms2_annot_sub["entropy_similarity"] 1962 == ms2_annot_sub["entropy_similarity"].max() 1963 ) 1964 ms2_annot_sub["ref_match_fract_max"] = ( 1965 ms2_annot_sub["ref_mz_in_query_fract"] 1966 == ms2_annot_sub["ref_mz_in_query_fract"].max() 1967 ) 1968 ms2_annot_sub["frag_max"] = ms2_annot_sub["query_frag_types"].apply( 1969 lambda x: True if "MLF" in x else False 1970 ) 1971 1972 # New column that looks if there is a consensus between the ranks (one row that is highest in all ranks) 1973 ms2_annot_sub["consensus"] = ms2_annot_sub[ 1974 ["entropy_max", "ref_match_fract_max", "frag_max"] 1975 ].all(axis=1) 1976 ms2_annot_sub_con = ms2_annot_sub[ms2_annot_sub["consensus"]] 1977 id_sub.append(ms2_annot_sub_con) 1978 id_no_con.append(ms2_annot_sub) 1979 id_sub = pd.concat(id_sub) 1980 id_no_con = pd.concat(id_no_con) 1981 1982 # Scenario 1: Multiple scans are being resolved to different MLFs [could be coelutions and should both be kept and annotated to MS level] 1983 if ( 1984 id_sub["query_frag_types"] 1985 .apply(lambda x: True if "MLF" in x else False) 1986 .all() 1987 and len(id_sub) > 0 1988 ): 1989 idx = id_sub.groupby("name")["entropy_similarity"].idxmax() 1990 id_sub = id_sub.loc[idx] 1991 # Reorder so highest entropy_similarity is first 1992 id_sub = id_sub.sort_values("entropy_similarity", ascending=False) 1993 id_sub["annot_level"] = id_sub["structure_level"] 1994 no_consensus.append(id_sub) 1995 1996 # Scenario 2: Multiple scans are being resolved to different species, keep both and annotate to appropriate level 1997 elif len(id_sub) == 0: 1998 for lipid_summed_name in id_no_con["lipid_summed_name"].unique(): 1999 summed_sub = id_no_con[ 2000 id_no_con["lipid_summed_name"] == lipid_summed_name 2001 ] 2002 # Any consensus to MLF? 2003 if summed_sub["consensus"].any(): 2004 summed_sub = summed_sub[summed_sub["consensus"]] 2005 summed_sub["annot_level"] = summed_sub["structure_level"] 2006 no_consensus.append(summed_sub) 2007 else: 2008 # Grab the highest entropy_similarity, if there are multiple, grab the first one 2009 summed_sub = summed_sub[ 2010 summed_sub["entropy_similarity"] 2011 == summed_sub["entropy_similarity"].max() 2012 ].head(1) 2013 # get first row 2014 summed_sub["annot_level"] = "species" 2015 summed_sub["name"] = "" 2016 no_consensus.append(summed_sub) 2017 else: 2018 raise ValueError("Unexpected scenario for summarizing mf_id: ", mf_id) 2019 2020 if len(no_consensus) > 0: 2021 no_consensus = pd.concat(no_consensus) 2022 else: 2023 no_consensus = ms2_annot.head(0) 2024 2025 # Combine all the consensus annotations and reformat the dataframe for output 2026 species_results_all = species_results_all.drop(columns=["name"]) 2027 species_results_all["lipid_molecular_species_id"] = "" 2028 mlf_results_all["lipid_molecular_species_id"] = mlf_results_all["name"] 2029 no_consensus["lipid_molecular_species_id"] = no_consensus["name"] 2030 consensus_annotations = pd.concat( 2031 [mlf_results_all, species_results_all, no_consensus] 2032 ) 2033 consensus_annotations = consensus_annotations.sort_values( 2034 "mf_id", ascending=True 2035 ) 2036 cols_to_keep = [ 2037 "mf_id", 2038 "ref_ion_type", 2039 "entropy_similarity", 2040 "ref_mz_in_query_fract", 2041 "lipid_molecular_species_id", 2042 "lipid_summed_name", 2043 "lipid_subclass", 2044 "lipid_class", 2045 "lipid_category", 2046 "formula", 2047 "annot_level", 2048 "n_spectra_contributing", 2049 ] 2050 consensus_annotations = consensus_annotations[cols_to_keep] 2051 consensus_annotations = consensus_annotations.set_index("mf_id") 2052 2053 return consensus_annotations
Summarize the lipid report.
Parameters
- ms2_annot (DataFrame): The MS2 annotation DataFrame with all annotations.
Returns
- DataFrame: The summarized lipid report.
2055 def clean_ms2_report(self, lipid_summary): 2056 """Clean the MS2 report. 2057 2058 Parameters 2059 ---------- 2060 lipid_summary : DataFrame 2061 The full lipid summary DataFrame. 2062 2063 Returns 2064 ------- 2065 DataFrame 2066 The cleaned lipid summary DataFrame. 2067 """ 2068 lipid_summary = lipid_summary.reset_index() 2069 lipid_summary["ion_formula"] = [ 2070 self.get_ion_formula(f, a) 2071 for f, a in zip(lipid_summary["formula"], lipid_summary["ref_ion_type"]) 2072 ] 2073 2074 # Reorder columns 2075 lipid_summary = lipid_summary[ 2076 [ 2077 "mf_id", 2078 "ion_formula", 2079 "ref_ion_type", 2080 "formula", 2081 "annot_level", 2082 "lipid_molecular_species_id", 2083 "lipid_summed_name", 2084 "lipid_subclass", 2085 "lipid_class", 2086 "lipid_category", 2087 "entropy_similarity", 2088 "ref_mz_in_query_fract", 2089 "n_spectra_contributing", 2090 ] 2091 ] 2092 2093 # Set the index to mf_id 2094 lipid_summary = lipid_summary.set_index("mf_id") 2095 2096 return lipid_summary
Clean the MS2 report.
Parameters
- lipid_summary (DataFrame): The full lipid summary DataFrame.
Returns
- DataFrame: The cleaned lipid summary DataFrame.
2098 def to_report(self, molecular_metadata=None): 2099 """Create a report of the mass features and their annotations. 2100 2101 Parameters 2102 ---------- 2103 molecular_metadata : dict, optional 2104 The molecular metadata. Default is None. 2105 2106 Returns 2107 ------- 2108 DataFrame 2109 The report of the mass features and their annotations. 2110 2111 Notes 2112 ----- 2113 The report will contain the mass features and their annotations from MS1 and MS2 (if available). 2114 """ 2115 # Get mass feature dataframe 2116 mf_report = self.mass_spectra.mass_features_to_df() 2117 mf_report = mf_report.reset_index(drop=False) 2118 2119 # Get and clean ms1 annotation dataframe 2120 ms1_annot_report = self.mass_spectra.mass_features_ms1_annot_to_df().copy() 2121 ms1_annot_report = self.clean_ms1_report(ms1_annot_report) 2122 ms1_annot_report = ms1_annot_report.reset_index(drop=False) 2123 2124 # Get, summarize, and clean ms2 annotation dataframe 2125 ms2_annot_report = self.mass_spectra.mass_features_ms2_annot_to_df( 2126 molecular_metadata=molecular_metadata 2127 ) 2128 if ms2_annot_report is not None and molecular_metadata is not None: 2129 ms2_annot_report = self.summarize_lipid_report(ms2_annot_report) 2130 ms2_annot_report = self.clean_ms2_report(ms2_annot_report) 2131 ms2_annot_report = ms2_annot_report.dropna(axis=1, how="all") 2132 ms2_annot_report = ms2_annot_report.reset_index(drop=False) 2133 report = self.combine_reports( 2134 mf_report=mf_report, 2135 ms1_annot_report=ms1_annot_report, 2136 ms2_annot_report=ms2_annot_report 2137 ) 2138 return report
Create a report of the mass features and their annotations.
Parameters
- molecular_metadata (dict, optional): The molecular metadata. Default is None.
Returns
- DataFrame: The report of the mass features and their annotations.
Notes
The report will contain the mass features and their annotations from MS1 and MS2 (if available).
Inherited Members
2141class LCMSCollectionExport(): 2142 """A class to export an LCMS collection to HDF5 format. 2143 2144 This class provides methods to export collection-level data from multi-sample LC-MS 2145 experiments to HDF5 files. It handles the export of metadata, retention time alignments, 2146 cluster assignments, and induced mass features (gap-filled features) across the collection. 2147 2148 The exporter is designed to work with LCMSCollection objects and complements the individual 2149 LCMSExport class by focusing on collection-wide data rather than individual sample data. 2150 2151 Parameters 2152 ---------- 2153 out_file_path : str | Path 2154 The output file path, do not include the file extension. The .hdf5 extension 2155 will be added automatically. 2156 mass_spectra_collection : LCMSCollection 2157 The LCMS collection object containing multiple LCMS samples with processed mass features, 2158 alignments, and clustering information. 2159 2160 Attributes 2161 ---------- 2162 out_file_path : Path 2163 The output file path as a Path object. 2164 mass_spectra_collection : LCMSCollection 2165 The LCMS collection object to be exported. 2166 2167 Methods 2168 ------- 2169 export_to_hdf5(overwrite=False) 2170 Export the LCMS collection to an HDF5 file with collection-level data. 2171 2172 Notes 2173 ----- 2174 This class exports collection-level data including: 2175 - Sample manifest (metadata about all samples in the collection) 2176 - Retention time alignment data (if RT alignment has been performed) 2177 - Cluster assignments (consensus mass feature groupings across samples) 2178 - Induced mass features (gap-filled features saved to individual LCMS object HDF5 files) 2179 2180 Individual sample data (mass spectra, mass features, EICs, etc.) should be exported 2181 separately using the LCMSExport class for each LCMS object in the collection. 2182 2183 Examples 2184 -------- 2185 Export a collection after clustering and gap-filling: 2186 2187 >>> from corems.mass_spectra.output.export import LCMSCollectionExporter 2188 >>> exporter = LCMSCollectionExporter("my_collection", lcms_collection) 2189 >>> exporter.export_to_hdf5(overwrite=True) 2190 2191 The resulting HDF5 file will contain collection-level metadata and can be used 2192 to reconstruct the collection state for further analysis. 2193 2194 See Also 2195 -------- 2196 LCMSExport : Export individual LCMS objects to HDF5 2197 LCMSCollection : The collection object being exported 2198 """ 2199 def __init__(self, out_file_path, mass_spectra_collection): 2200 self.out_file_path = Path(out_file_path) 2201 self.mass_spectra_collection = mass_spectra_collection 2202 2203 def export_to_hdf5( 2204 self, 2205 overwrite = False, 2206 save_parameters=True, 2207 parameter_format="toml", 2208 update_lcms_objects=True): 2209 """Export the LCMS collection to an HDF5 file. 2210 2211 This method saves the collection-level data to an HDF5 file, including: 2212 - Basic metadata (date, folder location, gap-filling status) 2213 - Sample manifest 2214 - Retention time alignments (if available) 2215 - Cluster assignments (if available) 2216 - Induced mass features for each LCMS object (if gap-filling was performed) 2217 2218 Individual LCMS objects in the collection are not exported by this method. 2219 Use LCMSExport for exporting individual LCMS objects. 2220 2221 Parameters 2222 ---------- 2223 overwrite : bool, optional 2224 If True, overwrites the output file if it already exists and replaces 2225 existing groups within the HDF5 file. If False, appends new data to 2226 existing file without overwriting existing groups. Default is False. 2227 save_parameters : bool, optional 2228 If True, saves the collection-level parameters to a separate file in the specified format. 2229 Default is True. 2230 parameter_format : str, optional 2231 The format for saving parameters, either "json" or "toml". Default is "toml". 2232 update_lcms_objects : bool, optional 2233 If True, updates the individual LCMS object HDF5 files with new raw file locations and any additional 2234 information produced during the processing of the collection (e.g. cluster mass feature associations). Default is True. 2235 2236 Notes 2237 ----- 2238 The HDF5 file structure includes: 2239 - Attributes: date_utc, lcms_objects_folder, missing_mass_features_searched, manifest 2240 - Groups: rt_alignments, cluster_assignments (if available) 2241 2242 Induced mass features are saved to the individual LCMS object HDF5 files 2243 within the .corems folder structure, not in the collection-level HDF5 file. 2244 2245 Examples 2246 -------- 2247 >>> exporter = LCMSCollectionExporter("my_collection", lcms_collection) 2248 >>> exporter.export_to_hdf5(overwrite=True) 2249 """ 2250 if overwrite: 2251 if self.out_file_path.with_suffix(".hdf5").exists(): 2252 self.out_file_path.with_suffix(".hdf5").unlink() 2253 2254 with h5py.File(self.out_file_path.with_suffix(".hdf5"), "a") as hdf_handle: 2255 # Add basic attributes to the HDF5 file, always overwrite these 2256 timenow = str( 2257 datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M:%S %Z") 2258 ) 2259 hdf_handle.attrs["date_utc"] = timenow 2260 hdf_handle.attrs["lcms_objects_folder"] = str(self.mass_spectra_collection.collection_parser.folder_location) 2261 hdf_handle.attrs["missing_mass_features_searched"] = self.mass_spectra_collection.missing_mass_features_searched 2262 hdf_handle.attrs["rt_aligned"] = self.mass_spectra_collection.rt_aligned 2263 hdf_handle.attrs["rt_alignment_attempted"] = self.mass_spectra_collection.rt_alignment_attempted 2264 2265 # Add the manifest to the HDF5 file, always overwrite this 2266 hdf_handle.attrs["manifest"] = self._convert_manifest_to_json() 2267 2268 # Save retention time alignments if they exist, only overwrite if specified 2269 self._save_rt_alignments_to_hdf5(hdf_handle, overwrite) 2270 2271 # Save cluster assignments if they exist, only overwrite if specified 2272 self._save_cluster_assignments_to_hdf5(hdf_handle, overwrite) 2273 2274 # Save new raw file locations to each LCMS object's HDF5 file if needed 2275 if hasattr(self.mass_spectra_collection, 'raw_files_relocated') and self.mass_spectra_collection.raw_files_relocated: 2276 self._update_raw_file_locations_in_hdf5() 2277 2278 # Save induced mass features to the collection with associations to each individual, only if lcms_collection.missing_mass_features_searched is True 2279 if self.mass_spectra_collection.missing_mass_features_searched: 2280 self._save_induced_mass_features_to_hdf5(overwrite) 2281 # Save EICs for induced mass features at collection level 2282 self._save_induced_eics_to_hdf5(overwrite) 2283 2284 # Build cluster mass feature map to know which features to update 2285 # This uses the same logic as process_consensus_features to determine loaded features 2286 cluster_mf_map = self._build_cluster_mf_map() 2287 2288 # Save updated mass features for each LCMS object 2289 # This implements selective update: only loaded features are updated, non-cluster features are preserved 2290 if update_lcms_objects: 2291 self._save_lcms_objects_to_hdf5(cluster_mf_map, overwrite) 2292 2293 # Save collection-level parameters as separate file 2294 if save_parameters: 2295 # Check if parameter_format is valid 2296 if parameter_format not in ["json", "toml"]: 2297 raise ValueError("parameter_format must be 'json' or 'toml'") 2298 2299 if parameter_format == "json": 2300 dump_lcms_collection_settings_json( 2301 filename=self.out_file_path.with_suffix(".json"), 2302 lcms_collection=self.mass_spectra_collection, 2303 ) 2304 elif parameter_format == "toml": 2305 dump_lcms_collection_settings_toml( 2306 filename=self.out_file_path.with_suffix(".toml"), 2307 lcms_collection=self.mass_spectra_collection, 2308 ) 2309 2310 def _save_rt_alignments_to_hdf5(self, hdf_handle, overwrite): 2311 """Save retention time alignments to HDF5 file.""" 2312 # If no rt_alignments, return early 2313 if not self.mass_spectra_collection.rt_aligned: 2314 return 2315 2316 # If rt_alignments exist, save them 2317 if self.mass_spectra_collection.rt_aligned: 2318 group_name = "rt_alignments" 2319 # grab dictionary of rt_alignments 2320 rt_alignments = self.mass_spectra_collection.rt_alignments 2321 2322 if rt_alignments: 2323 # Check if group exists and handle overwrite logic 2324 if group_name in hdf_handle: 2325 if not overwrite: 2326 return 2327 del hdf_handle[group_name] 2328 2329 grp = hdf_handle.create_group(group_name) 2330 2331 # Save each alignment as a dataset 2332 for sample_idx, alignment_data in rt_alignments.items(): 2333 grp.create_dataset(str(sample_idx), data=alignment_data) 2334 2335 def _convert_manifest_to_json(self): 2336 """Clean the manifest for export to HDF5.""" 2337 manifest = self.mass_spectra_collection.collection_parser.manifest 2338 2339 # Process the manifest to convert numpy.bool_ or bool values for the 'use_rt_alignment' key 2340 def convert_bool_values(data): 2341 if isinstance(data, dict): 2342 # Process each key-value pair recursively 2343 return {k: (int(v) if k == 'use_rt_alignment' and isinstance(v, (bool, np.bool_)) else convert_bool_values(v)) for k, v in data.items()} 2344 elif isinstance(data, list): 2345 # Recursively process lists 2346 return [convert_bool_values(item) for item in data] 2347 else: 2348 # Return non-dict/list types unchanged 2349 return data 2350 2351 # Clean the whole manifest 2352 cleaned_manifest = convert_bool_values(manifest) 2353 2354 # Serialize the cleaned manifest into JSON format 2355 json_manifest = json.dumps(cleaned_manifest) 2356 return json_manifest 2357 2358 def _save_cluster_assignments_to_hdf5(self, hdf_handle, overwrite): 2359 """Save cluster assignments to HDF5 file.""" 2360 # Check if column "cluster" is present in self.mass_features_dataframe 2361 if "cluster" in self.mass_spectra_collection.mass_features_dataframe.columns: 2362 group_name = "cluster_assignments" 2363 cluster_assignments = self.mass_spectra_collection.mass_features_dataframe[["cluster"]].copy() 2364 2365 # Check if group exists and handle overwrite logic 2366 if group_name in hdf_handle: 2367 if not overwrite: 2368 return 2369 del hdf_handle[group_name] 2370 2371 grp = hdf_handle.create_group(group_name) 2372 2373 # Save the index, converting strings to bytes 2374 grp.create_dataset("index", data=cluster_assignments.index.astype(str).values.astype('S')) 2375 2376 # Save the "cluster" column 2377 grp.create_dataset("cluster", data=cluster_assignments["cluster"].values) 2378 2379 def _build_cluster_mf_map(self): 2380 """Build a mapping of which mass features should be saved for each sample. 2381 2382 This uses the same logic as process_consensus_features to determine which 2383 mass features were loaded and should be updated in HDF5 files. 2384 2385 Returns 2386 ------- 2387 dict 2388 Dictionary mapping sample_id to list of tuples (mf_id, cluster_id). 2389 Only includes samples that have loaded representative features. 2390 Returns empty dict if no clusters exist. 2391 2392 Notes 2393 ----- 2394 This follows the DRY principle by using the same get_sample_mf_map_for_representatives 2395 method used by process_consensus_features and ReadSavedLCMSCollection. 2396 """ 2397 # Check if clusters exist 2398 if "cluster" not in self.mass_spectra_collection.mass_features_dataframe.columns: 2399 return {} 2400 2401 # Check if cluster_summary_dataframe exists (needed by get_sample_mf_map_for_representatives) 2402 if not hasattr(self.mass_spectra_collection, 'cluster_summary_dataframe') or \ 2403 self.mass_spectra_collection.cluster_summary_dataframe is None: 2404 return {} 2405 2406 # Use the same DRY helper method that process_consensus_features uses 2407 # This ensures consistency across the codebase 2408 cluster_mf_map = self.mass_spectra_collection.get_sample_mf_map_for_representatives( 2409 include_cluster_id=True 2410 ) 2411 2412 return cluster_mf_map 2413 2414 def _update_raw_file_locations_in_hdf5(self): 2415 """Update raw file locations in each LCMS object's HDF5 file. 2416 2417 This method updates the 'original_file_location' attribute in each LCMS object's 2418 HDF5 file to reflect the new raw file location after files have been relocated. 2419 """ 2420 for lcms_obj in self.mass_spectra_collection: 2421 # Get the HDF5 file path for this LCMS object 2422 hdf5_path = lcms_obj.file_location.with_suffix('.hdf5') 2423 2424 if hdf5_path.exists(): 2425 with h5py.File(hdf5_path, 'a') as hdf_handle: 2426 # Update the original_file_location attribute 2427 if 'original_file_location' in hdf_handle.attrs: 2428 hdf_handle.attrs['original_file_location'] = str(lcms_obj.raw_file_location) 2429 # If the attribute does not exist, create it 2430 else: 2431 hdf_handle.attrs.create('original_file_location', str(lcms_obj.raw_file_location)) 2432 2433 def _save_induced_mass_features_to_hdf5(self, overwrite): 2434 """Save induced mass features to the collection HDF5 file. 2435 2436 Induced mass features are gap-filled features that only exist at the collection level. 2437 They are saved with full detail (all attributes and datasets) in the collection HDF5 file 2438 and distributed to individual LCMS objects when the collection is loaded. 2439 2440 The induced mass features are stored in the collection's induced_mass_features_dataframe 2441 and are regenerated as LCMSMassFeature objects for saving. 2442 2443 Parameters 2444 ---------- 2445 overwrite : bool 2446 If True, overwrites existing induced mass features group. If False, skips if group exists. 2447 """ 2448 # Check if we have any induced mass features to save 2449 if (self.mass_spectra_collection.induced_mass_features_dataframe is None or 2450 self.mass_spectra_collection.induced_mass_features_dataframe.empty): 2451 return 2452 2453 # Open the collection HDF5 file to save induced mass features 2454 with h5py.File(self.out_file_path.with_suffix(".hdf5"), "a") as hdf_handle: 2455 group_name = "induced_mass_features" 2456 2457 # Check if group exists and handle overwrite logic 2458 if group_name in hdf_handle: 2459 if not overwrite: 2460 return 2461 del hdf_handle[group_name] 2462 2463 # Create top-level group for induced mass features 2464 imf_group = hdf_handle.create_group(group_name) 2465 2466 # Get the induced mass features dataframe 2467 induced_df = self.mass_spectra_collection.induced_mass_features_dataframe 2468 2469 # Get unique sample IDs from the dataframe 2470 sample_ids = induced_df['sample_id'].unique() 2471 2472 # Iterate through each sample and save its induced mass features 2473 for sample_id in sample_ids: 2474 # Filter dataframe to this sample 2475 sample_df = induced_df[induced_df['sample_id'] == sample_id].copy() 2476 2477 if sample_df.empty: 2478 continue 2479 2480 # Regenerate mass features from the dataframe 2481 regenerated_features = self._regenerate_mass_features_from_sample_df( 2482 sample_df, sample_id 2483 ) 2484 2485 if not regenerated_features: 2486 continue 2487 2488 # Create a subgroup for this sample's induced mass features 2489 sample_group = imf_group.create_group(str(sample_id)) 2490 2491 # Use the static helper method from LCMSExport to save the mass features 2492 LCMSExport._save_mass_features_dict_to_hdf5( 2493 regenerated_features, 2494 sample_group, 2495 overwrite=overwrite 2496 ) 2497 2498 def _save_induced_eics_to_hdf5(self, overwrite): 2499 """Save EICs for induced mass features to the collection HDF5 file. 2500 2501 Induced mass features are gap-filled features created during process_consensus_features. 2502 Their associated EICs need to be saved at the collection level so they can be reloaded. 2503 2504 The induced mass features are identified from the collection's induced_mass_features_dataframe, 2505 and their EICs are retrieved from the individual LCMS objects. 2506 2507 Parameters 2508 ---------- 2509 overwrite : bool 2510 If True, overwrites existing induced EICs group. If False, skips if group exists. 2511 """ 2512 # Check if we have any induced mass features to save 2513 if (self.mass_spectra_collection.induced_mass_features_dataframe is None or 2514 self.mass_spectra_collection.induced_mass_features_dataframe.empty): 2515 return 2516 2517 # Open the collection HDF5 file to save induced EICs 2518 with h5py.File(self.out_file_path.with_suffix(".hdf5"), "a") as hdf_handle: 2519 group_name = "induced_eics" 2520 2521 # Check if group exists and handle overwrite logic 2522 if group_name in hdf_handle: 2523 if not overwrite: 2524 return 2525 del hdf_handle[group_name] 2526 2527 # Create top-level group for induced EICs 2528 induced_eics_group = hdf_handle.create_group(group_name) 2529 2530 # Get the induced mass features dataframe 2531 induced_df = self.mass_spectra_collection.induced_mass_features_dataframe 2532 2533 # Get unique sample IDs from the dataframe 2534 sample_ids = induced_df['sample_id'].unique() 2535 2536 # Iterate through each sample and save EICs for its induced mass features 2537 for sample_id in sample_ids: 2538 lcms_obj = self.mass_spectra_collection[sample_id] 2539 2540 # Filter dataframe to this sample 2541 sample_df = induced_df[induced_df['sample_id'] == sample_id].copy() 2542 2543 if sample_df.empty: 2544 continue 2545 2546 # Collect EICs for induced mass features using _eic_mz from dataframe 2547 induced_eics = {} 2548 for _, row in sample_df.iterrows(): 2549 # Get the EIC m/z from the dataframe 2550 eic_mz = row.get('_eic_mz') 2551 2552 if eic_mz is not None and pd.notna(eic_mz): 2553 # Try to get the EIC from the LCMS object 2554 if hasattr(lcms_obj, 'eics') and lcms_obj.eics and eic_mz in lcms_obj.eics: 2555 induced_eics[eic_mz] = lcms_obj.eics[eic_mz] 2556 2557 if not induced_eics: 2558 continue 2559 2560 # Create a subgroup for this sample's induced EICs 2561 sample_group = induced_eics_group.create_group(str(sample_id)) 2562 2563 # Use the static helper method from LCMSExport to save the EICs 2564 LCMSExport._save_eics_dict_to_hdf5(induced_eics, sample_group, overwrite) 2565 2566 def _regenerate_mass_features_from_sample_df(self, sample_df, sample_id): 2567 """Regenerate induced mass features from a sample-specific dataframe. 2568 2569 This method creates LCMSMassFeature objects from rows in the induced_mass_features_dataframe 2570 for a specific sample. The regenerated features are used for saving to HDF5. 2571 2572 Parameters 2573 ---------- 2574 sample_df : pd.DataFrame 2575 DataFrame containing induced mass features for a specific sample. 2576 sample_id : int 2577 The sample ID (index in the collection). 2578 2579 Returns 2580 ------- 2581 dict 2582 Dictionary of regenerated LCMSMassFeature objects keyed by feature ID. 2583 """ 2584 from corems.chroma_peak.factory.chroma_peak_classes import LCMSMassFeature 2585 2586 if sample_df.empty: 2587 return {} 2588 2589 # Get the corresponding LCMS object for proper parent reference 2590 lcms_obj = self.mass_spectra_collection[sample_id] 2591 2592 # Regenerate mass features from the dataframe 2593 regenerated_features = {} 2594 2595 for _, row in sample_df.iterrows(): 2596 # Extract the original ID from mf_id (format: c{cluster}_{index}_i) 2597 # This is the ID used in lcms_obj.induced_mass_features dict 2598 original_id = row['mf_id'] 2599 2600 # Create a new LCMSMassFeature with proper parent reference 2601 # Note: dataframe uses 'scan_time' but __init__ parameter is 'retention_time' 2602 mass_feature = LCMSMassFeature( 2603 lcms_parent=lcms_obj, 2604 mz=row['mz'], 2605 retention_time=row['scan_time'], # Column is 'scan_time' in dataframe 2606 intensity=row['intensity'], 2607 apex_scan=int(row['apex_scan']), 2608 persistence=row.get('persistence', None) if 'persistence' in row else None, 2609 id=original_id # Use the original string ID from gap-filling 2610 ) 2611 2612 # Set additional attributes dynamically from dataframe columns 2613 # Skip columns already handled in __init__ or structural metadata 2614 skip_cols = { 2615 'sample_id', 'mf_id', 'mz', 'scan_time', 'scan_time_aligned', 2616 'intensity', 'apex_scan', 'persistence'} 2617 2618 # Iterate through all columns and set via property setters 2619 for col_name in row.index: 2620 if col_name in skip_cols: 2621 continue 2622 value = row[col_name] 2623 try: 2624 if pd.isna(value): 2625 continue 2626 except (TypeError, ValueError): 2627 pass # value is array-like; not NA, proceed 2628 2629 # Set via property (public interface handles private attributes) 2630 # Don't save empty lists 2631 if isinstance(value, list) and len(value) == 0: 2632 continue 2633 try: 2634 setattr(mass_feature, col_name, value) 2635 except (AttributeError, TypeError): 2636 pass # Skip attributes that don't exist or can't be set 2637 2638 # Set cluster_index if present 2639 if 'cluster' in row and pd.notna(row['cluster']): 2640 mass_feature.cluster_index = int(row['cluster']) 2641 2642 regenerated_features[mass_feature.id] = mass_feature 2643 2644 return regenerated_features 2645 2646 def _save_lcms_objects_to_hdf5(self, cluster_mf_map, overwrite): 2647 """Save updated mass features for each LCMS object. 2648 2649 This method implements a "selective update" strategy for mass features: 2650 - For mass features specified in cluster_mf_map (loaded representatives), we selectively 2651 update them by deleting their old entries and re-saving with new attributes. 2652 - Non-cluster features (not loaded) are never touched/overwritten. 2653 2654 Note: EICs are NOT saved here. Induced feature EICs are saved at the collection level. 2655 2656 Parameters 2657 ---------- 2658 cluster_mf_map : dict 2659 Dictionary mapping sample_id to list of tuples (mf_id, cluster_id). 2660 This explicitly defines which mass features should be updated. 2661 overwrite : bool 2662 If True, allows overwriting of existing data. If False, skips if data exists. 2663 """ 2664 for sample_id, lcms_obj in enumerate(self.mass_spectra_collection): 2665 hdf5_path = lcms_obj.file_location.with_suffix('.hdf5') 2666 2667 if not hdf5_path.exists(): 2668 # If HDF5 doesn't exist, we can't do selective update, raise error 2669 raise FileNotFoundError( 2670 f"HDF5 file for LCMS object {lcms_obj.sample_name} not found at {hdf5_path}" 2671 ) 2672 2673 # Check if this sample has any loaded features in the map 2674 if sample_id not in cluster_mf_map or not cluster_mf_map[sample_id]: 2675 # Nothing loaded for this sample, nothing to update 2676 continue 2677 2678 # Extract mf_ids from the map (cluster_mf_map contains tuples of (mf_id, cluster_id)) 2679 mf_ids_to_update = [mf_id for mf_id, cluster_id in cluster_mf_map[sample_id]] 2680 2681 # Perform selective update of mass features 2682 self._selective_update_mass_features(lcms_obj, hdf5_path, mf_ids_to_update, overwrite) 2683 2684 # Save any new mass spectra that were added during processing 2685 self._save_new_mass_spectra(lcms_obj, hdf5_path, overwrite) 2686 2687 def _save_new_mass_spectra(self, lcms_obj, hdf5_path, overwrite): 2688 """Save new mass spectra that were added during processing. 2689 2690 This method checks what mass spectra are in lcms_obj._ms and saves any 2691 that aren't already in the HDF5 file's mass_spectra group. Uses the 2692 existing add_mass_spectrum_to_hdf5 method for consistency with original 2693 export logic. 2694 2695 Parameters 2696 ---------- 2697 lcms_obj : LCMSBase 2698 The LCMS object with potentially new mass spectra. 2699 hdf5_path : Path 2700 Path to the HDF5 file. 2701 overwrite : bool 2702 If True, allows overwriting existing spectra. 2703 """ 2704 # Check if there are any mass spectra to save 2705 if not hasattr(lcms_obj, '_ms') or not lcms_obj._ms: 2706 return 2707 2708 # Create an LCMS exporter instance for this LCMS object 2709 # This gives us access to add_mass_spectrum_to_hdf5 method inherited from HighResMassSpecExport 2710 # Turn hdf5_path into str without suffix for LCMSExport 2711 hdf5_path_str = str(hdf5_path.with_suffix('')) 2712 exporter = LCMSExport( 2713 out_file_path=hdf5_path_str, 2714 mass_spectra=lcms_obj 2715 ) 2716 2717 # Open HDF5 file and check existing mass spectra 2718 with h5py.File(hdf5_path, 'a') as hdf_handle: 2719 # Create mass_spectra group if it doesn't exist 2720 if 'mass_spectra' not in hdf_handle: 2721 ms_group = hdf_handle.create_group('mass_spectra') 2722 existing_scan_numbers = set() 2723 else: 2724 ms_group = hdf_handle['mass_spectra'] 2725 existing_scan_numbers = set(int(k) for k in ms_group.keys()) 2726 2727 # Find new mass spectra (in _ms but not in HDF5) 2728 new_scan_numbers = set(lcms_obj._ms.keys()) - existing_scan_numbers 2729 2730 if not new_scan_numbers: 2731 return 2732 2733 # Save new mass spectra using existing add_mass_spectrum_to_hdf5 method 2734 export_profile = lcms_obj.parameters.lc_ms.export_profile_spectra 2735 for scan_number in new_scan_numbers: 2736 mass_spec = lcms_obj._ms[scan_number] 2737 scan_group_name = str(scan_number) 2738 2739 # Delete existing group if overwrite is True 2740 if scan_group_name in ms_group and overwrite: 2741 del ms_group[scan_group_name] 2742 elif scan_group_name in ms_group: 2743 continue 2744 2745 # Use the existing method from HighResMassSpecExport 2746 exporter.add_mass_spectrum_to_hdf5( 2747 hdf_handle=hdf_handle, 2748 mass_spectrum=mass_spec, 2749 group_key=scan_group_name, 2750 mass_spectra_group=ms_group, 2751 export_raw=export_profile 2752 ) 2753 2754 def _selective_update_mass_features(self, lcms_obj, hdf5_path, mf_ids_to_update, overwrite): 2755 """Selectively update mass features in HDF5 file. 2756 2757 This method deletes only the mass features specified in mf_ids_to_update, 2758 then re-saves them with their potentially updated attributes. Non-cluster features 2759 in the HDF5 file are left untouched. 2760 2761 Parameters 2762 ---------- 2763 lcms_obj : LCMSBase 2764 The LCMS object with mass features to update. 2765 hdf5_path : Path 2766 Path to the HDF5 file. 2767 mf_ids_to_update : list of int 2768 List of mass feature IDs that should be updated. This explicitly defines 2769 which features were loaded and should be saved. 2770 overwrite : bool 2771 If True, allows overwriting. If False, skips if group exists. 2772 """ 2773 if not mf_ids_to_update: 2774 return 2775 2776 # Open HDF5 file and delete specified feature IDs, then re-save 2777 with h5py.File(hdf5_path, 'a') as hdf_handle: 2778 if 'mass_features' not in hdf_handle: 2779 return 2780 2781 mf_group = hdf_handle['mass_features'] 2782 2783 # Delete features that are being updated 2784 for feature_id in mf_ids_to_update: 2785 feature_id_str = str(feature_id) 2786 if feature_id_str in mf_group: 2787 del mf_group[feature_id_str] 2788 2789 # Re-save updated features (only those that exist in mass_features dict) 2790 updated_features = { 2791 mf.id: mf for mf in lcms_obj.mass_features.values() 2792 if mf.id in mf_ids_to_update 2793 } 2794 2795 if updated_features: 2796 LCMSExport._save_mass_features_dict_to_hdf5( 2797 updated_features, 2798 mf_group, 2799 overwrite=overwrite 2800 )
A class to export an LCMS collection to HDF5 format.
This class provides methods to export collection-level data from multi-sample LC-MS experiments to HDF5 files. It handles the export of metadata, retention time alignments, cluster assignments, and induced mass features (gap-filled features) across the collection.
The exporter is designed to work with LCMSCollection objects and complements the individual LCMSExport class by focusing on collection-wide data rather than individual sample data.
Parameters
- out_file_path (str | Path): The output file path, do not include the file extension. The .hdf5 extension will be added automatically.
- mass_spectra_collection (LCMSCollection): The LCMS collection object containing multiple LCMS samples with processed mass features, alignments, and clustering information.
Attributes
- out_file_path (Path): The output file path as a Path object.
- mass_spectra_collection (LCMSCollection): The LCMS collection object to be exported.
Methods
export_to_hdf5(overwrite=False) Export the LCMS collection to an HDF5 file with collection-level data.
Notes
This class exports collection-level data including:
- Sample manifest (metadata about all samples in the collection)
- Retention time alignment data (if RT alignment has been performed)
- Cluster assignments (consensus mass feature groupings across samples)
- Induced mass features (gap-filled features saved to individual LCMS object HDF5 files)
Individual sample data (mass spectra, mass features, EICs, etc.) should be exported separately using the LCMSExport class for each LCMS object in the collection.
Examples
Export a collection after clustering and gap-filling:
>>> from corems.mass_spectra.output.export import LCMSCollectionExporter
>>> exporter = LCMSCollectionExporter("my_collection", lcms_collection)
>>> exporter.export_to_hdf5(overwrite=True)
The resulting HDF5 file will contain collection-level metadata and can be used to reconstruct the collection state for further analysis.
See Also
LCMSExport: Export individual LCMS objects to HDF5
LCMSCollection: The collection object being exported
2203 def export_to_hdf5( 2204 self, 2205 overwrite = False, 2206 save_parameters=True, 2207 parameter_format="toml", 2208 update_lcms_objects=True): 2209 """Export the LCMS collection to an HDF5 file. 2210 2211 This method saves the collection-level data to an HDF5 file, including: 2212 - Basic metadata (date, folder location, gap-filling status) 2213 - Sample manifest 2214 - Retention time alignments (if available) 2215 - Cluster assignments (if available) 2216 - Induced mass features for each LCMS object (if gap-filling was performed) 2217 2218 Individual LCMS objects in the collection are not exported by this method. 2219 Use LCMSExport for exporting individual LCMS objects. 2220 2221 Parameters 2222 ---------- 2223 overwrite : bool, optional 2224 If True, overwrites the output file if it already exists and replaces 2225 existing groups within the HDF5 file. If False, appends new data to 2226 existing file without overwriting existing groups. Default is False. 2227 save_parameters : bool, optional 2228 If True, saves the collection-level parameters to a separate file in the specified format. 2229 Default is True. 2230 parameter_format : str, optional 2231 The format for saving parameters, either "json" or "toml". Default is "toml". 2232 update_lcms_objects : bool, optional 2233 If True, updates the individual LCMS object HDF5 files with new raw file locations and any additional 2234 information produced during the processing of the collection (e.g. cluster mass feature associations). Default is True. 2235 2236 Notes 2237 ----- 2238 The HDF5 file structure includes: 2239 - Attributes: date_utc, lcms_objects_folder, missing_mass_features_searched, manifest 2240 - Groups: rt_alignments, cluster_assignments (if available) 2241 2242 Induced mass features are saved to the individual LCMS object HDF5 files 2243 within the .corems folder structure, not in the collection-level HDF5 file. 2244 2245 Examples 2246 -------- 2247 >>> exporter = LCMSCollectionExporter("my_collection", lcms_collection) 2248 >>> exporter.export_to_hdf5(overwrite=True) 2249 """ 2250 if overwrite: 2251 if self.out_file_path.with_suffix(".hdf5").exists(): 2252 self.out_file_path.with_suffix(".hdf5").unlink() 2253 2254 with h5py.File(self.out_file_path.with_suffix(".hdf5"), "a") as hdf_handle: 2255 # Add basic attributes to the HDF5 file, always overwrite these 2256 timenow = str( 2257 datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M:%S %Z") 2258 ) 2259 hdf_handle.attrs["date_utc"] = timenow 2260 hdf_handle.attrs["lcms_objects_folder"] = str(self.mass_spectra_collection.collection_parser.folder_location) 2261 hdf_handle.attrs["missing_mass_features_searched"] = self.mass_spectra_collection.missing_mass_features_searched 2262 hdf_handle.attrs["rt_aligned"] = self.mass_spectra_collection.rt_aligned 2263 hdf_handle.attrs["rt_alignment_attempted"] = self.mass_spectra_collection.rt_alignment_attempted 2264 2265 # Add the manifest to the HDF5 file, always overwrite this 2266 hdf_handle.attrs["manifest"] = self._convert_manifest_to_json() 2267 2268 # Save retention time alignments if they exist, only overwrite if specified 2269 self._save_rt_alignments_to_hdf5(hdf_handle, overwrite) 2270 2271 # Save cluster assignments if they exist, only overwrite if specified 2272 self._save_cluster_assignments_to_hdf5(hdf_handle, overwrite) 2273 2274 # Save new raw file locations to each LCMS object's HDF5 file if needed 2275 if hasattr(self.mass_spectra_collection, 'raw_files_relocated') and self.mass_spectra_collection.raw_files_relocated: 2276 self._update_raw_file_locations_in_hdf5() 2277 2278 # Save induced mass features to the collection with associations to each individual, only if lcms_collection.missing_mass_features_searched is True 2279 if self.mass_spectra_collection.missing_mass_features_searched: 2280 self._save_induced_mass_features_to_hdf5(overwrite) 2281 # Save EICs for induced mass features at collection level 2282 self._save_induced_eics_to_hdf5(overwrite) 2283 2284 # Build cluster mass feature map to know which features to update 2285 # This uses the same logic as process_consensus_features to determine loaded features 2286 cluster_mf_map = self._build_cluster_mf_map() 2287 2288 # Save updated mass features for each LCMS object 2289 # This implements selective update: only loaded features are updated, non-cluster features are preserved 2290 if update_lcms_objects: 2291 self._save_lcms_objects_to_hdf5(cluster_mf_map, overwrite) 2292 2293 # Save collection-level parameters as separate file 2294 if save_parameters: 2295 # Check if parameter_format is valid 2296 if parameter_format not in ["json", "toml"]: 2297 raise ValueError("parameter_format must be 'json' or 'toml'") 2298 2299 if parameter_format == "json": 2300 dump_lcms_collection_settings_json( 2301 filename=self.out_file_path.with_suffix(".json"), 2302 lcms_collection=self.mass_spectra_collection, 2303 ) 2304 elif parameter_format == "toml": 2305 dump_lcms_collection_settings_toml( 2306 filename=self.out_file_path.with_suffix(".toml"), 2307 lcms_collection=self.mass_spectra_collection, 2308 )
Export the LCMS collection to an HDF5 file.
This method saves the collection-level data to an HDF5 file, including:
- Basic metadata (date, folder location, gap-filling status)
- Sample manifest
- Retention time alignments (if available)
- Cluster assignments (if available)
- Induced mass features for each LCMS object (if gap-filling was performed)
Individual LCMS objects in the collection are not exported by this method. Use LCMSExport for exporting individual LCMS objects.
Parameters
- overwrite (bool, optional): If True, overwrites the output file if it already exists and replaces existing groups within the HDF5 file. If False, appends new data to existing file without overwriting existing groups. Default is False.
- save_parameters (bool, optional): If True, saves the collection-level parameters to a separate file in the specified format. Default is True.
- parameter_format (str, optional): The format for saving parameters, either "json" or "toml". Default is "toml".
- update_lcms_objects (bool, optional): If True, updates the individual LCMS object HDF5 files with new raw file locations and any additional information produced during the processing of the collection (e.g. cluster mass feature associations). Default is True.
Notes
The HDF5 file structure includes:
- Attributes: date_utc, lcms_objects_folder, missing_mass_features_searched, manifest
- Groups: rt_alignments, cluster_assignments (if available)
Induced mass features are saved to the individual LCMS object HDF5 files within the .corems folder structure, not in the collection-level HDF5 file.
Examples
>>> exporter = LCMSCollectionExporter("my_collection", lcms_collection)
>>> exporter.export_to_hdf5(overwrite=True)