corems.mass_spectra.input.corems_hdf5
1__author__ = "Yuri E. Corilo" 2__date__ = "Oct 29, 2019" 3 4 5from threading import Thread 6import h5py 7import tomlkit 8import json 9import multiprocessing 10from pathlib import Path 11from typing import Union, Tuple, List, Optional 12 13import numpy as np 14import pandas as pd 15import warnings 16 17from corems.chroma_peak.factory.chroma_peak_classes import LCMSMassFeature 18from corems.encapsulation.spawn_pool import spawn_pool 19from corems.encapsulation.input.parameter_from_json import ( 20 load_and_set_json_parameters_lcms, 21 load_and_set_toml_parameters_lcms, 22) 23from corems.mass_spectra.factory.lc_class import ( 24 LCMSBase, 25 MassSpectraBase, 26 LCMSCollection, 27) 28from corems.mass_spectra.factory.chromat_data import EIC_Data 29from corems.mass_spectra.input.parserbase import SpectraParserInterface 30from corems.mass_spectrum.input.coremsHDF5 import ReadCoreMSHDF_MassSpectrum 31from corems.molecular_id.factory.spectrum_search_results import SpectrumSearchResults 32from corems.mass_spectra.input.mzml import MZMLSpectraParser 33 34 35def create_manifest_from_folder( 36 folder_path: Path, 37 output_path: Path = None, 38 batch_time_threshold_hours: float = 12.0, 39 center_name: str = None, 40 overwrite: bool = False, 41) -> Path: 42 """ 43 Create a manifest CSV file for ReadCoreMSHDFMassSpectraCollection from CoreMS HDF5 files. 44 45 Scans a folder for .corems subdirectories and generates a manifest with columns: 46 sample_name, batch, order, center, time. Files are batched by creation time, and 47 one sample is designated as the retention time alignment center. 48 49 Parameters 50 ---------- 51 folder_path : Path 52 Path to folder containing .corems subdirectories with HDF5 files. 53 output_path : Path, optional 54 Output manifest CSV path. Default: folder_path/manifest.csv. 55 batch_time_threshold_hours : float, optional 56 Time gap in hours for batch separation. Default: 12.0. 57 center_name : str, optional 58 Sample name to designate as RT alignment center (must exist in samples). 59 If None, the middle sample (by creation time) is used. 60 overwrite : bool, optional 61 Whether to overwrite existing manifest. Default: False. 62 63 Returns 64 ------- 65 Path 66 Path to created manifest file. 67 68 Raises 69 ------ 70 FileNotFoundError 71 If folder_path doesn't exist or contains no .corems subdirectories. 72 FileExistsError 73 If output file exists and overwrite is False. 74 ValueError 75 If no HDF5 files found, or center_name doesn't match any sample. 76 """ 77 if not folder_path.exists(): 78 raise FileNotFoundError(f"Folder {folder_path} does not exist.") 79 80 # Set default output path if not provided 81 if output_path is None: 82 output_path = folder_path / "manifest.csv" 83 84 # Check if output file exists 85 if output_path.exists() and not overwrite: 86 raise FileExistsError( 87 f"Manifest file {output_path} already exists. " 88 "Set overwrite=True to replace it." 89 ) 90 91 # Find all .corems subdirectories 92 corems_dirs = sorted( 93 [d for d in folder_path.iterdir() if d.is_dir() and d.suffix == ".corems"] 94 ) 95 96 if not corems_dirs: 97 raise FileNotFoundError( 98 f"No .corems subdirectories found in {folder_path}. " 99 "Ensure the folder contains processed CoreMS data." 100 ) 101 102 # Collect sample information 103 sample_data = [] 104 105 for corems_dir in corems_dirs: 106 sample_name = corems_dir.stem # Remove .corems extension 107 hdf5_file = corems_dir / f"{sample_name}.hdf5" 108 109 if not hdf5_file.exists(): 110 print(f"Warning: HDF5 file not found for {sample_name}, skipping.") 111 continue 112 113 # Get creation time using the ReadCoreMSHDFMassSpectra method 114 try: 115 # Use context manager to ensure file is properly closed 116 with ReadCoreMSHDFMassSpectra(str(hdf5_file)) as parser: 117 # Use the get_original_creation_time() method which checks HDF5 attrs first, 118 # then falls back to original parser if needed 119 creation_time = parser.get_original_creation_time() 120 121 # Skip sample if creation time unavailable 122 if creation_time is None: 123 print( 124 f"Warning: Could not get original creation time for {sample_name}, skipping." 125 ) 126 continue 127 128 except Exception as e: 129 print( 130 f"Warning: Error getting creation time for {sample_name}: {e}, skipping." 131 ) 132 continue 133 134 sample_data.append( 135 { 136 "sample_name": sample_name, 137 "creation_time": creation_time, 138 "hdf5_path": hdf5_file, 139 } 140 ) 141 142 if not sample_data: 143 raise ValueError( 144 f"No valid HDF5 files found in {folder_path}. " 145 "Ensure .corems subdirectories contain .hdf5 files." 146 ) 147 148 # Sort by creation time 149 sample_data.sort(key=lambda x: x["creation_time"]) 150 151 # Assign batches based on time threshold 152 batch_assignments = [] 153 current_batch = 1 154 155 for i, sample in enumerate(sample_data): 156 if i == 0: 157 batch_assignments.append(current_batch) 158 else: 159 time_diff = sample["creation_time"] - sample_data[i - 1]["creation_time"] 160 time_diff_hours = time_diff.total_seconds() / 3600 161 162 if time_diff_hours > batch_time_threshold_hours: 163 current_batch += 1 164 165 batch_assignments.append(current_batch) 166 167 # Determine which sample should be the center for retention time alignment 168 sample_names = [s["sample_name"] for s in sample_data] 169 170 if center_name is not None: 171 # Validate that center_name is in the discovered samples 172 if center_name not in sample_names: 173 raise ValueError( 174 f"Specified center_name '{center_name}' not found in discovered samples. " 175 f"Available samples: {', '.join(sample_names)}" 176 ) 177 center_sample = center_name 178 else: 179 # Use the middle sample (by creation time) as center 180 middle_idx = len(sample_data) // 2 181 center_sample = sample_data[middle_idx]["sample_name"] 182 print( 183 f"Auto-selected center sample: {center_sample} (index {middle_idx} of {len(sample_data)}, middle by creation time)" 184 ) 185 186 # Create manifest dataframe with center column as TRUE/FALSE 187 manifest_df = pd.DataFrame( 188 { 189 "sample_name": sample_names, 190 "batch": batch_assignments, 191 "order": list(range(1, len(sample_data) + 1)), 192 "center": [ 193 "TRUE" if name == center_sample else "FALSE" for name in sample_names 194 ], 195 "time": [ 196 s["creation_time"].strftime("%Y-%m-%dT%H:%M:%SZ") for s in sample_data 197 ], 198 } 199 ) 200 201 # Sort manifest by time before saving to ensure proper order 202 manifest_df = manifest_df.sort_values("time").reset_index(drop=True) 203 # Update order column to reflect sorted order 204 manifest_df["order"] = list(range(1, len(manifest_df) + 1)) 205 206 # Save manifest 207 manifest_df.to_csv(output_path, index=False) 208 209 print(f"Manifest created successfully at {output_path}") 210 print(f"Total samples: {len(sample_data)}") 211 print(f"Number of batches: {current_batch}") 212 print( 213 f"Batch assignments: {dict(zip(range(1, current_batch + 1), [batch_assignments.count(b) for b in range(1, current_batch + 1)]))}" 214 ) 215 216 return output_path 217 218 219class ReadCoreMSHDFMassSpectra( 220 SpectraParserInterface, ReadCoreMSHDF_MassSpectrum, Thread 221): 222 """Class to read CoreMS HDF5 files and populate a LCMS or MassSpectraBase object. 223 224 Parameters 225 ---------- 226 file_location : str 227 The location of the HDF5 file to read, including the suffix. 228 229 Attributes 230 ---------- 231 file_location : str 232 The location of the HDF5 file to read. 233 h5pydata : h5py.File 234 The HDF5 file object. 235 scans : list 236 A list of the location of individual mass spectra within the HDF5 file. 237 scan_number_list : list 238 A list of the scan numbers of the mass spectra within the HDF5 file. 239 parameters_location : str 240 The location of the parameters file (json or toml). 241 242 Methods 243 ------- 244 * import_mass_spectra(mass_spectra). 245 Imports all mass spectra from the HDF5 file onto the LCMS or MassSpectraBase object. 246 * get_mass_spectrum_from_scan(scan_number). 247 Return mass spectrum data object from scan number. 248 * load(). 249 Placeholder method to meet the requirements of the SpectraParserInterface. 250 * run(mass_spectra). 251 Runs the importer functions to populate a LCMS or MassSpectraBase object. 252 * import_scan_info(mass_spectra). 253 Imports the scan info from the HDF5 file to populate the _scan_info attribute 254 on the LCMS or MassSpectraBase object 255 * import_ms_unprocessed(mass_spectra). 256 Imports the unprocessed mass spectra from the HDF5 file to populate the 257 _ms_unprocessed attribute on the LCMS or MassSpectraBase object 258 * import_parameters(mass_spectra). 259 Imports the parameters from the HDF5 file to populate the parameters 260 attribute on the LCMS or MassSpectraBase object 261 * import_mass_features(mass_spectra). 262 Imports the mass features from the HDF5 file to populate the mass_features 263 attribute on the LCMS or MassSpectraBase object 264 * import_eics(mass_spectra). 265 Imports the extracted ion chromatograms from the HDF5 file to populate the 266 eics attribute on the LCMS or MassSpectraBase object 267 * import_spectral_search_results(mass_spectra). 268 Imports the spectral search results from the HDF5 file to populate the 269 spectral_search_results attribute on the LCMS or MassSpectraBase object 270 * get_mass_spectra_obj(). 271 Return mass spectra data object, populating the _ms list on the LCMS or 272 MassSpectraBase object from the HDF5 file 273 * get_lcms_obj(). 274 Return LCMSBase object, populating the majority of the attributes on the 275 LCMS object from the HDF5 file 276 277 """ 278 279 def __init__(self, file_location: str): 280 Thread.__init__(self) 281 ReadCoreMSHDF_MassSpectrum.__init__(self, file_location) 282 283 # override the scans attribute on ReadCoreMSHDF_MassSpectrum class to expect a nested location within the HDF5 file 284 self.scans = [ 285 "mass_spectra/" + x for x in list(self.h5pydata["mass_spectra"].keys()) 286 ] 287 self.scan_number_list = sorted( 288 [int(float(i)) for i in list(self.h5pydata["mass_spectra"].keys())] 289 ) 290 291 # set the location of the parameters file (json or toml) 292 add_files = [ 293 x 294 for x in self.file_location.parent.glob( 295 self.file_location.name.replace(".hdf5", ".*") 296 ) 297 if x.suffix != ".hdf5" 298 ] 299 if len([x for x in add_files if x.suffix == ".json"]) > 0: 300 self.parameters_location = [x for x in add_files if x.suffix == ".json"][0] 301 elif len([x for x in add_files if x.suffix == ".toml"]) > 0: 302 self.parameters_location = [x for x in add_files if x.suffix == ".toml"][0] 303 else: 304 self.parameters_location = None 305 306 def __enter__(self): 307 """Context manager entry.""" 308 return self 309 310 def __exit__(self, exc_type, exc_val, exc_tb): 311 """Context manager exit - closes the HDF5 file.""" 312 if hasattr(self, "h5pydata") and self.h5pydata is not None: 313 self.h5pydata.close() 314 return False 315 316 def close(self): 317 """Explicitly close the HDF5 file.""" 318 if hasattr(self, "h5pydata") and self.h5pydata is not None: 319 self.h5pydata.close() 320 321 def get_mass_spectrum_from_scan(self, scan_number): 322 """Return mass spectrum data object from scan number.""" 323 if scan_number in self.scan_number_list: 324 mass_spec = self.get_mass_spectrum(scan_number) 325 return mass_spec 326 else: 327 raise Exception("Scan number not found in HDF5 file.") 328 329 def get_mass_spectra_from_scan_list( 330 self, scan_list, spectrum_mode, auto_process=True 331 ): 332 """Return a list of mass spectrum data objects from a list of scan numbers. 333 334 Parameters 335 ---------- 336 scan_list : list 337 A list of scan numbers to retrieve mass spectra for. 338 spectrum_mode : str 339 The spectrum mode to use when retrieving the mass spectra. 340 Note that this parameter is not used for CoreMS HDF5 files, as the spectra are already processed and only 341 centroided spectra are saved. 342 auto_process : bool 343 If True, automatically process the mass spectra when retrieving them. 344 Note that this parameter is not used for CoreMS HDF5 files, as the spectra are already processed and only 345 centroided spectra are saved. 346 347 Returns 348 ------- 349 list 350 A list of mass spectrum data objects corresponding to the provided scan numbers. 351 """ 352 mass_spectra_list = [] 353 for scan_number in scan_list: 354 if scan_number in self.scan_number_list: 355 mass_spec = self.get_mass_spectrum_from_scan(scan_number) 356 mass_spectra_list.append(mass_spec) 357 else: 358 warnings.warn(f"Scan number {scan_number} not found in HDF5 file.") 359 return mass_spectra_list 360 361 def load(self) -> None: 362 """ """ 363 pass 364 365 def get_ms_raw( 366 self, 367 spectra=None, 368 scan_df=None, 369 time_range: Optional[ 370 Union[Tuple[float, float], List[Tuple[float, float]]] 371 ] = None, 372 ) -> dict: 373 """ """ 374 # Warn if spectra or scan_df are not None that they are not used for CoreMS HDF5 files and should be rerun after instantiation 375 if spectra is not None or scan_df is not None: 376 SyntaxWarning( 377 "get_ms_raw method for CoreMS HDF5 files can only access saved data, consider rerunning after instantiation." 378 ) 379 ms_unprocessed = {} 380 dict_group_load = self.h5pydata["ms_unprocessed"] 381 dict_group_keys = dict_group_load.keys() 382 for k in dict_group_keys: 383 dataset = dict_group_load[k] 384 ms_up_int = dataset[:] 385 column_labels_json = dataset.attrs.get("column_labels") 386 column_labels = ( 387 json.loads(column_labels_json) 388 if column_labels_json 389 else ["scan", "mz", "intensity"] 390 ) 391 ms_unprocessed[int(k)] = pd.DataFrame(ms_up_int, columns=column_labels) 392 return ms_unprocessed 393 394 def get_scan_df( 395 self, 396 time_range: Optional[ 397 Union[Tuple[float, float], List[Tuple[float, float]]] 398 ] = None, 399 ) -> pd.DataFrame: 400 scan_info = {} 401 dict_group_load = self.h5pydata["scan_info"] 402 dict_group_keys = dict_group_load.keys() 403 for k in dict_group_keys: 404 scan_info[k] = dict_group_load[k][:] 405 scan_df = pd.DataFrame(scan_info) 406 scan_df.set_index("scan", inplace=True, drop=False) 407 # HDF5 stores text as fixed-width bytes (numpy "S"). Decode only those 408 # columns. Do not use select_dtypes([object]) + str.decode: under pandas 409 # 3 / infer_string, already-decoded string columns can be selected and 410 # corrupted by a blanket decode. 411 for col in scan_df.columns: 412 series = scan_df[col] 413 sample = series.dropna() 414 if sample.empty: 415 continue 416 first = sample.iloc[0] 417 if isinstance(first, (bytes, bytearray, np.bytes_)): 418 scan_df[col] = series.map( 419 lambda x: ( 420 x.decode("utf-8") 421 if isinstance(x, (bytes, bytearray, np.bytes_)) 422 else x 423 ) 424 ) 425 426 # Apply time range filtering if specified 427 if time_range is not None: 428 time_ranges = self._normalize_time_range(time_range) 429 mask = pd.Series([False] * len(scan_df), index=scan_df.index) 430 for start_time, end_time in time_ranges: 431 mask |= (scan_df.scan_time >= start_time) & ( 432 scan_df.scan_time <= end_time 433 ) 434 scan_df = scan_df[mask] 435 436 return scan_df 437 438 def run(self, mass_spectra, load_raw=True, load_light=False) -> None: 439 """Runs the importer functions to populate a LCMS or MassSpectraBase object. 440 441 Notes 442 ----- 443 The following functions are run in order, if the HDF5 file contains the necessary data: 444 1. import_parameters(), which populates the parameters attribute on the LCMS or MassSpectraBase object. 445 2. import_mass_spectra(), which populates the _ms list on the LCMS or MassSpectraBase object. 446 3. import_scan_info(), which populates the _scan_info on the LCMS or MassSpectraBase object. 447 4. import_ms_unprocessed(), which populates the _ms_unprocessed attribute on the LCMS or MassSpectraBase object. 448 5. import_mass_features(), which populates the mass_features attribute on the LCMS or MassSpectraBase object. 449 6. import_eics(), which populates the eics attribute on the LCMS or MassSpectraBase object. 450 7. import_spectral_search_results(), which populates the spectral_search_results attribute on the LCMS or MassSpectraBase object. 451 452 Parameters 453 ---------- 454 mass_spectra : LCMSBase or MassSpectraBase 455 The LCMS or MassSpectraBase object to populate with mass spectra, generally instantiated with only the file_location, analyzer, and instrument_label attributes. 456 load_raw : bool 457 If True, load raw data (unprocessed) from HDF5 files for overall lcms object and individual mass spectra. Default is True. 458 load_light : bool 459 If True, only load the parameters, mass features, and scan info. Default is False. 460 461 Returns 462 ------- 463 None, but populates several attributes on the LCMS or MassSpectraBase object. 464 465 """ 466 if self.parameters_location is not None: 467 # Populate the parameters attribute on the LCMS object 468 self.import_parameters(mass_spectra) 469 470 if "mass_spectra" in self.h5pydata and not load_light: 471 # Populate the _ms list on the LCMS object 472 self.import_mass_spectra(mass_spectra, load_raw=load_raw) 473 474 if "scan_info" in self.h5pydata: 475 # Populate the _scan_info attribute on the LCMS object 476 self.import_scan_info(mass_spectra) 477 478 if "ms_unprocessed" in self.h5pydata and load_raw and not load_light: 479 # Populate the _ms_unprocessed attribute on the LCMS object 480 self.import_ms_unprocessed(mass_spectra) 481 482 if "mass_features" in self.h5pydata: 483 # Populate the mass_features attribute on the LCMS object 484 self.import_mass_features(mass_spectra) 485 486 if "eics" in self.h5pydata and not load_light: 487 # Populate the eics attribute on the LCMS object 488 self.import_eics(mass_spectra) 489 490 if "spectral_search_results" in self.h5pydata and not load_light: 491 # Populate the spectral_search_results attribute on the LCMS object 492 self.import_spectral_search_results(mass_spectra) 493 494 def import_mass_spectra(self, mass_spectra, load_raw=True) -> None: 495 """Imports all mass spectra from the HDF5 file. 496 497 Parameters 498 ---------- 499 mass_spectra : LCMSBase | MassSpectraBase 500 The MassSpectraBase or LCMSBase object to populate with mass spectra. 501 load_raw : bool 502 If True, load raw data (unprocessed) from HDF5 files for overall lcms object and individual mass spectra. Default 503 504 Returns 505 ------- 506 None, but populates the '_ms' list on the LCMSBase or MassSpectraBase 507 object with mass spectra from the HDF5 file. 508 """ 509 for scan_number in self.scan_number_list: 510 mass_spec = self.get_mass_spectrum(scan_number, load_raw=load_raw) 511 mass_spec.scan_number = scan_number 512 mass_spectra.add_mass_spectrum(mass_spec) 513 514 def import_scan_info(self, mass_spectra) -> None: 515 """Imports the scan info from the HDF5 file. 516 517 Parameters 518 ---------- 519 lcms : LCMSBase | MassSpectraBase 520 The MassSpectraBase or LCMSBase objects 521 522 Returns 523 ------- 524 None, but populates the 'scan_df' attribute on the LCMSBase or MassSpectraBase 525 object with a pandas DataFrame of the 'scan_info' from the HDF5 file. 526 527 """ 528 scan_df = self.get_scan_df() 529 mass_spectra.scan_df = scan_df 530 531 def import_ms_unprocessed(self, mass_spectra) -> None: 532 """Imports the unprocessed mass spectra from the HDF5 file. 533 534 Parameters 535 ---------- 536 lcms : LCMSBase | MassSpectraBase 537 The MassSpectraBase or LCMSBase objects 538 539 Returns 540 ------- 541 None, but populates the '_ms_unprocessed' attribute on the LCMSBase or MassSpectraBase 542 object with a dictionary of the 'ms_unprocessed' from the HDF5 file. 543 544 """ 545 ms_unprocessed = self.get_ms_raw() 546 mass_spectra._ms_unprocessed = ms_unprocessed 547 548 def import_parameters(self, mass_spectra) -> None: 549 """Imports the parameters from the HDF5 file. 550 551 Parameters 552 ---------- 553 mass_spectra : LCMSBase | MassSpectraBase 554 The MassSpectraBase or LCMSBase object to populate with parameters. 555 556 Returns 557 ------- 558 None, but populates the 'parameters' attribute on the LCMS or MassSpectraBase 559 object with a dictionary of the 'parameters' from the HDF5 file. 560 561 """ 562 if ".json" == self.parameters_location.suffix: 563 load_and_set_json_parameters_lcms(mass_spectra, self.parameters_location) 564 if ".toml" == self.parameters_location.suffix: 565 load_and_set_toml_parameters_lcms(mass_spectra, self.parameters_location) 566 else: 567 raise Exception( 568 "Parameters file must be in JSON format, TOML format is not yet supported." 569 ) 570 571 def import_mass_features(self, mass_spectra, mf_ids=None) -> None: 572 """Imports the mass features from the HDF5 file. 573 574 Parameters 575 ---------- 576 mass_spectra : LCMSBase | MassSpectraBase 577 The MassSpectraBase or LCMSBase object to populate with mass features. 578 mf_ids : list, optional 579 A list of mass feature IDs to import. If None, all mass features are imported. 580 581 Returns 582 ------- 583 None, but populates the 'mass_features' attribute on the LCMSBase or MassSpectraBase 584 object with a dictionary of the 'mass_features' from the HDF5 file. 585 586 """ 587 dict_group_load = self.h5pydata["mass_features"] 588 dict_group_keys = dict_group_load.keys() 589 for k in dict_group_keys: 590 if mf_ids is not None and int(k) not in mf_ids: 591 continue 592 # Instantiate the MassFeature object 593 mass_feature = LCMSMassFeature( 594 mass_spectra, 595 mz=dict_group_load[k].attrs["_mz_exp"], 596 retention_time=dict_group_load[k].attrs["_retention_time"], 597 intensity=dict_group_load[k].attrs["_intensity"], 598 apex_scan=dict_group_load[k].attrs["_apex_scan"], 599 persistence=dict_group_load[k].attrs["_persistence"], 600 id=int(k), 601 ) 602 603 # Populate additional attributes on the MassFeature object 604 for key in dict_group_load[k].attrs.keys() - { 605 "_mz_exp", 606 "_mz_cal", 607 "_retention_time", 608 "_intensity", 609 "_apex_scan", 610 "_persistence", 611 }: 612 setattr(mass_feature, key, dict_group_load[k].attrs[key]) 613 614 # Populate attributes on MassFeature object that are lists 615 for key in dict_group_load[k].keys(): 616 setattr(mass_feature, key, dict_group_load[k][key][:]) 617 # Convert _noise_score from array to tuple 618 if key == "_noise_score": 619 mass_feature._noise_score = tuple(mass_feature._noise_score) 620 mass_spectra.mass_features[int(k)] = mass_feature 621 622 # Associate mass features with ms1 and ms2 spectra, if available 623 for mf_id in mass_spectra.mass_features.keys(): 624 if mass_spectra.mass_features[mf_id].apex_scan in mass_spectra._ms.keys(): 625 mass_spectra.mass_features[mf_id].mass_spectrum = mass_spectra._ms[ 626 mass_spectra.mass_features[mf_id].apex_scan 627 ] 628 if mass_spectra.mass_features[mf_id].ms2_scan_numbers is not None: 629 for ms2_scan in mass_spectra.mass_features[mf_id].ms2_scan_numbers: 630 if ms2_scan in mass_spectra._ms.keys(): 631 mass_spectra.mass_features[mf_id].ms2_mass_spectra[ms2_scan] = ( 632 mass_spectra._ms[ms2_scan] 633 ) 634 635 def import_eics(self, mass_spectra, mz_list=None, mz_tolerance=0.0001): 636 """Imports the extracted ion chromatograms from the HDF5 file. 637 638 Parameters 639 ---------- 640 mass_spectra : LCMSBase | MassSpectraBase 641 The MassSpectraBase or LCMSBase object to populate with extracted ion chromatograms. 642 mz_list : list of float, optional 643 List of m/z values to load EICs for. If None, loads all EICs. Default is None. 644 mz_tolerance : float, optional 645 Tolerance in Daltons for matching m/z values when mz_list is provided. 646 Default is 0.0001 Da. 647 648 Returns 649 ------- 650 None, but populates the 'eics' attribute on the LCMSBase or MassSpectraBase 651 object with a dictionary of the 'eics' from the HDF5 file. 652 653 """ 654 if "eics" not in self.h5pydata: 655 # Samples with no exported EICs (e.g. empty / fully gap-filled) have no group 656 return 657 658 dict_group_load = self.h5pydata["eics"] 659 dict_group_keys = dict_group_load.keys() 660 661 # Prefilter dict_group_keys if mz_list is provided to EICs within tolerance 662 if mz_list is not None: 663 if len(mz_list) == 0: 664 return 665 target_mz_array = np.array(sorted(mz_list), dtype=float) 666 mzs = [ 667 float(k) 668 for k in dict_group_keys 669 if np.abs(float(k) - target_mz_array).min() < mz_tolerance 670 ] 671 dict_group_keys = [str(mz) for mz in mzs] 672 673 for k in dict_group_keys: 674 # Check if we should load this EIC (filter by m/z if list provided) 675 eic_mz = dict_group_load[k].attrs["mz"] 676 677 my_eic = EIC_Data( 678 scans=dict_group_load[k]["scans"][:], 679 time=dict_group_load[k]["time"][:], 680 eic=dict_group_load[k]["eic"][:], 681 ) 682 for key in dict_group_load[k].keys(): 683 if key not in ["scans", "time", "eic"]: 684 setattr(my_eic, key, dict_group_load[k][key][:]) 685 # if key is apexes, convert to a tuple of a list 686 if key == "apexes" and len(my_eic.apexes) > 0: 687 my_eic.apexes = [tuple(x) for x in my_eic.apexes] 688 # Add to mass_spectra object 689 mass_spectra.eics[eic_mz] = my_eic 690 691 # Associate EICs with mass features using tolerance-based matching 692 mass_spectra.associate_eics_with_mass_features() 693 694 @staticmethod 695 def _load_eics_from_hdf5_group(eics_group, lcms_obj, mz_filter=None): 696 """Load EICs from an HDF5 group. 697 698 This is a static helper method that can be reused to load EIC data 699 from any HDF5 group in a consistent format. 700 701 Parameters 702 ---------- 703 eics_group : h5py.Group 704 The HDF5 group containing EIC data. 705 lcms_obj : LCMSBase 706 The LCMS object to associate EICs with (for reference, not modified). 707 mz_filter : list, optional 708 List of m/z values to load. If None, loads all EICs. Default is None. 709 Uses tolerance-based matching (0.0001). 710 711 Returns 712 ------- 713 dict 714 Dictionary of EIC_Data objects keyed by m/z value. 715 """ 716 from corems.mass_spectra.factory.chromat_data import EIC_Data 717 718 loaded_eics = {} 719 tolerance = 0.0001 720 721 for eic_key_str in eics_group.keys(): 722 eic_mz = ( 723 float(eic_key_str) 724 if eic_key_str.replace(".", "", 1).replace("-", "", 1).isdigit() 725 else eics_group[eic_key_str].attrs.get("mz") 726 ) 727 728 # If mz_filter is provided, check if this EIC matches any requested m/z 729 if mz_filter is not None: 730 if not any(abs(eic_mz - mz) < tolerance for mz in mz_filter): 731 continue 732 733 eic_data = eics_group[eic_key_str] 734 735 # Create EIC_Data object from datasets 736 eic = EIC_Data( 737 scans=list(eic_data["scans"][:]) if "scans" in eic_data else [], 738 time=list(eic_data["time"][:]) if "time" in eic_data else [], 739 eic=list(eic_data["eic"][:]) if "eic" in eic_data else [], 740 apexes=list(eic_data["apexes"][:]) if "apexes" in eic_data else [], 741 ) 742 743 # Load any additional datasets 744 for key in eic_data.keys(): 745 if key not in ["scans", "time", "eic", "apexes"]: 746 setattr(eic, key, eic_data[key][:]) 747 748 loaded_eics[eic_mz] = eic 749 750 return loaded_eics 751 752 def import_spectral_search_results(self, mass_spectra): 753 """Imports the spectral search results from the HDF5 file. 754 755 Parameters 756 ---------- 757 mass_spectra : LCMSBase | MassSpectraBase 758 The MassSpectraBase or LCMSBase object to populate with spectral search results. 759 760 Returns 761 ------- 762 None, but populates the 'spectral_search_results' attribute on the LCMSBase or MassSpectraBase 763 object with a dictionary of the 'spectral_search_results' from the HDF5 file. 764 765 """ 766 overall_results_dict = {} 767 ms2_results_load = self.h5pydata["spectral_search_results"] 768 for k in ms2_results_load.keys(): 769 overall_results_dict[int(k)] = {} 770 for k2 in ms2_results_load[k].keys(): 771 ms2_search_res = SpectrumSearchResults( 772 query_spectrum=mass_spectra._ms[int(k)], 773 precursor_mz=ms2_results_load[k][k2].attrs["precursor_mz"], 774 spectral_similarity_search_results={}, 775 ) 776 777 for key in ms2_results_load[k][k2].keys() - {"precursor_mz"}: 778 data = list(ms2_results_load[k][k2][key][:]) 779 if data and isinstance(data[0], bytes): 780 data = [x.decode("utf-8") for x in data] 781 setattr(ms2_search_res, key, data) 782 783 overall_results_dict[int(k)][ 784 ms2_results_load[k][k2].attrs["precursor_mz"] 785 ] = ms2_search_res 786 787 # add to mass_spectra 788 mass_spectra.spectral_search_results.update(overall_results_dict) 789 790 # If there are mass features, associate the results with each mass feature 791 if len(mass_spectra.mass_features) > 0: 792 for mass_feature_id, mass_feature in mass_spectra.mass_features.items(): 793 scan_ids = mass_feature.ms2_scan_numbers 794 for ms2_scan_id in scan_ids: 795 precursor_mz = mass_feature.mz 796 try: 797 mass_spectra.spectral_search_results[ms2_scan_id][precursor_mz] 798 except KeyError: 799 pass 800 else: 801 mass_spectra.mass_features[ 802 mass_feature_id 803 ].ms2_similarity_results.append( 804 mass_spectra.spectral_search_results[ms2_scan_id][ 805 precursor_mz 806 ] 807 ) 808 809 def get_mass_spectra_obj( 810 self, 811 load_raw=True, 812 load_light=False, 813 time_range: Optional[ 814 Union[Tuple[float, float], List[Tuple[float, float]]] 815 ] = None, 816 ) -> MassSpectraBase: 817 """ 818 Return mass spectra data object, populating the _ms list on MassSpectraBase object from the HDF5 file. 819 820 Parameters 821 ---------- 822 load_raw : bool 823 If True, load raw data (unprocessed) from HDF5 files for overall spectra object and individual mass spectra. Default is True. 824 load_light : bool 825 If True, only load the parameters, mass features, and scan info. Default is False. 826 time_range : tuple or list of tuples, optional 827 Retention time range(s) to load. Can be: 828 - Single range: (start_time, end_time) in minutes 829 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 830 If None, loads all scans. Note: For HDF5 files, this parameter is accepted for 831 interface consistency but not currently used in filtering. 832 833 """ 834 # Instantiate the LCMS object 835 spectra_obj = MassSpectraBase( 836 file_location=self.file_location, 837 analyzer=self.analyzer, 838 instrument_label=self.instrument_label, 839 sample_name=self.sample_name, 840 ) 841 842 # This will populate the _ms list on the LCMS or MassSpectraBase object 843 self.run(spectra_obj, load_raw=load_raw, load_light=load_light) 844 845 return spectra_obj 846 847 def get_lcms_obj( 848 self, 849 load_raw=True, 850 load_light=False, 851 use_original_parser=True, 852 raw_file_path=None, 853 time_range: Optional[ 854 Union[Tuple[float, float], List[Tuple[float, float]]] 855 ] = None, 856 ) -> LCMSBase: 857 """ 858 Return LCMSBase object, populating attributes on the LCMSBase object from the HDF5 file. 859 860 Parameters 861 ---------- 862 load_raw : bool 863 If True, load raw data (unprocessed) from HDF5 files for overall lcms object and individual mass spectra. Default is True. 864 load_light : bool 865 If True, only load the parameters, mass features, and scan info. Default is False. 866 use_original_parser : bool 867 If True, use the original parser to populate the LCMS object. Default is True. 868 raw_file_path : str 869 The location of the raw file to parse if attempting to use original parser. 870 Default is None, which attempts to get the raw file path from the HDF5 file. 871 If the original file path has moved, this parameter can be used to specify the new location. 872 time_range : tuple or list of tuples, optional 873 Retention time range(s) to load. Can be: 874 - Single range: (start_time, end_time) in minutes 875 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 876 If None, loads all scans. Note: For HDF5 files, this parameter is accepted for 877 interface consistency. If use_original_parser=True, time_range can be passed to the 878 original parser for filtering. 879 """ 880 # Instantiate the LCMS object 881 lcms_obj = LCMSBase( 882 file_location=self.file_location, 883 analyzer=self.analyzer, 884 instrument_label=self.instrument_label, 885 sample_name=self.sample_name, 886 ) 887 888 # This will populate the majority of the attributes on the LCMS object 889 self.run(lcms_obj, load_raw=load_raw, load_light=load_light) 890 891 # Set final attributes of the LCMS object 892 lcms_obj.polarity = self.h5pydata.attrs["polarity"] 893 lcms_obj._scans_number_list = list(lcms_obj.scan_df.scan) 894 lcms_obj._retention_time_list = list(lcms_obj.scan_df.scan_time) 895 lcms_obj._tic_list = list(lcms_obj.scan_df.tic) 896 897 # If use_original_parser is True, instantiate the original parser and populate the LCMS object 898 if use_original_parser: 899 lcms_obj = self.add_original_parser(lcms_obj, raw_file_path=raw_file_path) 900 else: 901 lcms_obj.spectra_parser_class = self.__class__ 902 903 return lcms_obj 904 905 def get_raw_file_location(self): 906 """ 907 Get the raw file location from the HDF5 file attributes. 908 909 Returns 910 ------- 911 str 912 The raw file location. 913 """ 914 if "original_file_location" in self.h5pydata.attrs: 915 return self.h5pydata.attrs["original_file_location"] 916 else: 917 return None 918 919 def add_original_parser(self, mass_spectra, raw_file_path=None): 920 """ 921 Add the original parser to the mass spectra object. 922 923 Parameters 924 ---------- 925 mass_spectra : MassSpectraBase | LCMSBase 926 The MassSpectraBase or LCMSBase object to add the original parser to. 927 raw_file_path : str 928 The location of the raw file to parse. Default is None, which attempts to get the raw file path from the HDF5 file. 929 """ 930 # Get the original parser type 931 og_parser_type = self.h5pydata.attrs["parser_type"] 932 933 # If raw_file_path is None, get it from the HDF5 file attributes 934 if raw_file_path is None: 935 raw_file_path = self.get_raw_file_location() 936 if raw_file_path is None: 937 raise ValueError( 938 "Raw file path not found in HDF5 file attributes, cannot instantiate original parser." 939 ) 940 941 # Set the raw file path on the mass_spectra object so the parser knows where to find the raw file 942 mass_spectra.raw_file_location = raw_file_path 943 944 if og_parser_type == "ImportMassSpectraThermoMSFileReader": 945 # Imported lazily: rawFileReader pulls in pythonnet (`clr`), which 946 # needs a .NET/Mono runtime. Only reopening a Thermo .raw file 947 # needs it; reading an HDF5 file of any other origin must not. 948 from corems.mass_spectra.input.rawFileReader import ( 949 ImportMassSpectraThermoMSFileReader, 950 ) 951 952 # Check that the parser can be instantiated with the raw file path 953 parser_class = ImportMassSpectraThermoMSFileReader 954 elif og_parser_type == "MZMLSpectraParser": 955 # Check that the parser can be instantiated with the raw file path 956 parser_class = MZMLSpectraParser 957 958 # Set the spectra parser class on the mass_spectra object so the spectra_parser property can be used with the original parser 959 mass_spectra.spectra_parser_class = parser_class 960 961 return mass_spectra 962 963 def get_original_creation_time(self): 964 """ 965 Get the creation time of the original raw data file. 966 967 First checks if creation_time is saved in the HDF5 file attributes. 968 If not found, attempts to instantiate the original parser and get the creation time. 969 970 Returns 971 ------- 972 datetime 973 The creation time of the original raw data file, or None if not available. 974 """ 975 # Check if creation_time is saved in HDF5 attributes 976 if "creation_time" in self.h5pydata.attrs: 977 from datetime import datetime 978 979 return datetime.fromisoformat(self.h5pydata.attrs["creation_time"]) 980 981 # Fall back to using original parser to get creation time 982 try: 983 # Get the original parser type and raw file path 984 og_parser_type = self.h5pydata.attrs.get("parser_type") 985 raw_file_path = self.get_raw_file_location() 986 987 if og_parser_type is None or raw_file_path is None: 988 warnings.warn( 989 "Cannot retrieve creation time: parser_type or original_file_location not found in HDF5 attributes." 990 ) 991 return None 992 993 # Check if raw file exists 994 from pathlib import Path 995 996 if not Path(raw_file_path).exists(): 997 warnings.warn( 998 f"Cannot retrieve creation time: original raw file not found at {raw_file_path}" 999 ) 1000 return None 1001 1002 # Instantiate the original parser 1003 if og_parser_type == "ImportMassSpectraThermoMSFileReader": 1004 # Lazy import; see add_original_parser for why. 1005 from corems.mass_spectra.input.rawFileReader import ( 1006 ImportMassSpectraThermoMSFileReader, 1007 ) 1008 1009 parser = ImportMassSpectraThermoMSFileReader(raw_file_path) 1010 elif og_parser_type == "MZMLSpectraParser": 1011 parser = MZMLSpectraParser(raw_file_path) 1012 else: 1013 warnings.warn( 1014 f"Unknown parser type: {og_parser_type}, cannot retrieve creation time." 1015 ) 1016 return None 1017 1018 # Get creation time from parser 1019 return parser.get_creation_time() 1020 1021 except Exception as e: 1022 warnings.warn(f"Failed to retrieve creation time from original parser: {e}") 1023 return None 1024 1025 def get_creation_time(self): 1026 """ 1027 Get the creation time of the original raw data file. 1028 1029 This is an alias for get_original_creation_time() for backward compatibility. 1030 1031 Returns 1032 ------- 1033 datetime 1034 The creation time of the original raw data file, or None if not available. 1035 """ 1036 return self.get_original_creation_time() 1037 1038 def get_instrument_info(self): 1039 """ 1040 Raise a NotImplemented Warning, as instrument info is not available in CoreMS HDF5 files and returning None. 1041 """ 1042 warnings.warn( 1043 "Instrument info is not available in CoreMS HDF5 files, returning None." 1044 "This should be accessed through the original parser.", 1045 ) 1046 return None 1047 1048 def get_scans_in_time_range( 1049 self, 1050 time_range: Union[Tuple[float, float], List[Tuple[float, float]]], 1051 ms_level: Optional[int] = None, 1052 ) -> List[int]: 1053 """Return scan numbers within specified retention time range(s). 1054 1055 Parameters 1056 ---------- 1057 time_range : tuple or list of tuples 1058 Retention time range(s) in minutes. Can be: 1059 - Single range: (start_time, end_time) 1060 - Multiple ranges: [(start1, end1), (start2, end2), ...] 1061 ms_level : int, optional 1062 If specified, only return scans of this MS level (e.g., 1 for MS1, 2 for MS2). 1063 If None, returns scans of all MS levels. 1064 1065 Returns 1066 ------- 1067 list of int 1068 List of scan numbers within the specified time range(s) and MS level. 1069 """ 1070 # Normalize time range to list of tuples 1071 time_ranges = self._normalize_time_range(time_range) 1072 1073 # Get all scan data 1074 scan_df = self.get_scan_df() 1075 1076 # Filter by time range 1077 mask = pd.Series([False] * len(scan_df), index=scan_df.index) 1078 for start_time, end_time in time_ranges: 1079 mask |= (scan_df.scan_time >= start_time) & (scan_df.scan_time <= end_time) 1080 1081 filtered_df = scan_df[mask] 1082 1083 # Filter by MS level if specified 1084 if ms_level is not None: 1085 filtered_df = filtered_df[filtered_df.ms_level == ms_level] 1086 1087 return filtered_df.scan.tolist() 1088 1089 1090class ReadCoreMSHDFMassSpectraCollection: 1091 """Read a collection of CoreMS HDF5 files and populate an LCMSCollection object. 1092 1093 Parameters 1094 ---------- 1095 folder_location : Path 1096 Folder containing .corems subdirectories with HDF5 files. 1097 manifest_file : Path, optional 1098 Manifest CSV with columns: sample_name, order, batch, center, time. 1099 One sample must have center='TRUE' for RT alignment. 1100 If None, checks if auto-generated manifest_auto.csv exists in the folder. If not, 1101 auto-generates from folder contents. Default: None. 1102 cores : int, optional 1103 Number of cores for multiprocessing. Default: 1. 1104 auto_manifest_batch_threshold_hours : float, optional 1105 Time gap (hours) for auto-generated batch separation. Default: 12.0. 1106 auto_manifest_center_name : str, optional 1107 Sample name for RT alignment center when auto-generating. 1108 Must match a discovered sample. If None, uses middle sample. Default: None. 1109 1110 Attributes 1111 ---------- 1112 folder_location : Path 1113 Folder containing CoreMS HDF5 files. 1114 manifest_filepath : Path 1115 Path to manifest file. 1116 manifest : dict 1117 Manifest data indexed by sample_name. 1118 """ 1119 1120 def __init__( 1121 self, 1122 folder_location: Path, 1123 manifest_file: Path = None, 1124 cores: int = 1, 1125 auto_manifest_batch_threshold_hours: float = 12.0, 1126 auto_manifest_center_name: str = None, 1127 ): 1128 # Check for folder location 1129 folder_location = Path(folder_location) 1130 if not folder_location.exists(): 1131 raise FileNotFoundError(f"Folder location {folder_location} not found.") 1132 1133 # Auto-generate manifest if not provided 1134 if manifest_file is None: 1135 # Check if manifest_auto.csv already exists 1136 auto_manifest_path = folder_location / "manifest_auto.csv" 1137 if auto_manifest_path.exists(): 1138 print( 1139 f"No manifest file provided. Using existing manifest_auto.csv from {folder_location}" 1140 ) 1141 manifest_file = auto_manifest_path 1142 else: 1143 print( 1144 f"No manifest file provided. Auto-generating manifest from {folder_location}" 1145 ) 1146 manifest_file = create_manifest_from_folder( 1147 folder_path=folder_location, 1148 output_path=auto_manifest_path, 1149 batch_time_threshold_hours=auto_manifest_batch_threshold_hours, 1150 center_name=auto_manifest_center_name, 1151 overwrite=True, 1152 ) 1153 else: 1154 manifest_file = Path(manifest_file) 1155 if not manifest_file.exists(): 1156 raise FileNotFoundError(f"Manifest file {manifest_file} not found.") 1157 1158 # Check if the manifest file is a CSV 1159 if manifest_file.suffix != ".csv": 1160 raise ValueError("Manifest file must be a CSV.") 1161 1162 self.folder_location = folder_location 1163 self._manifest_dict = None 1164 self._parse_manifest(manifest_file) 1165 self._validate_manifest() 1166 self._validate_parameters() 1167 self._validate_cores(cores) 1168 1169 def _validate_cores(self, cores): 1170 # Check if the cores parameter is an integer greater than 0 and less than the number of cores available 1171 if not isinstance(cores, int) or cores < 1: 1172 raise ValueError("Cores must be an integer greater than 0.") 1173 if cores > multiprocessing.cpu_count(): 1174 raise ValueError( 1175 f"Cores must be less than or equal to the number of cores available ({multiprocessing.cpu_count()})." 1176 ) 1177 self._cores = cores 1178 1179 def _parse_manifest(self, manifest_file): 1180 """Parse the manifest file and set the manifest dictionary.""" 1181 self.manifest_filepath = manifest_file 1182 manifest = pd.read_csv(manifest_file) 1183 # Check if the following columns exisit in the manifest file 1184 if not all( 1185 col in manifest.columns for col in ["sample_name", "order", "batch"] 1186 ): 1187 raise ValueError( 1188 "Manifest file must contain the following columns: 'sample_name', 'order', 'batch'." 1189 ) 1190 # Set index to the 'sample_name' column 1191 manifest.set_index("sample_name", inplace=True) 1192 self._manifest_dict = manifest.to_dict(orient="index") 1193 1194 def _validate_manifest(self): 1195 """Validate the manifest dictionary against the CoreMS folder location.""" 1196 # Check if the folder location contains HDF5 files for each sample 1197 for sample_name in self._manifest_dict.keys(): 1198 corems_dir = self.folder_location / f"{sample_name}.corems" 1199 if not corems_dir.exists(): 1200 raise FileNotFoundError(f"CoreMS folder for {sample_name} not found.") 1201 hdf5_file = corems_dir / f"{sample_name}.hdf5" 1202 if not hdf5_file.exists(): 1203 raise FileNotFoundError(f"HDF5 file for {sample_name} not found.") 1204 1205 # Check that at least one sample has center='TRUE' for retention time alignment 1206 center_values = [ 1207 sample_data.get("center") for sample_data in self._manifest_dict.values() 1208 ] 1209 if not any( 1210 center_val == "TRUE" or center_val == True for center_val in center_values 1211 ): 1212 raise ValueError( 1213 "Manifest must contain at least one sample with center='TRUE' for retention time alignment. " 1214 "None of the samples in the manifest have center='TRUE'." 1215 ) 1216 1217 def _validate_parameters(self): 1218 """Validate that the parameters used for all samples within a batch are the same.""" 1219 # Check if parameters files are saved as JSON or TOML 1220 if self.parameters_files[0].suffix == ".json": 1221 importer = json 1222 suffix = ".json" 1223 1224 elif self.parameters_files[0].suffix == ".toml": 1225 importer = tomlkit 1226 suffix = ".toml" 1227 1228 manfiest_df = self.manifest_dataframe 1229 1230 # Split up samples by batch 1231 batches = manfiest_df["batch"].unique() 1232 1233 for batch in batches: 1234 samples = manfiest_df[manfiest_df["batch"] == batch].index 1235 # check if self.parameters_files end with .json or .toml 1236 batch_param_files = [ 1237 self.folder_location / f"{sample_name}.corems/{sample_name}{suffix}" 1238 for sample_name in self._manifest_dict.keys() 1239 if sample_name in samples 1240 ] 1241 with open( 1242 batch_param_files[0], 1243 "r", 1244 encoding="utf8", 1245 ) as stream: 1246 first_parameters = importer.load(stream) 1247 for parameters_file in batch_param_files[1:]: 1248 with open( 1249 parameters_file, 1250 "r", 1251 encoding="utf8", 1252 ) as stream: 1253 parameters = importer.load(stream) 1254 if parameters != first_parameters: 1255 raise ValueError( 1256 f"Parameters files for samples in batch {batch} are not equal." 1257 ) 1258 1259 def get_lcms_obj( 1260 self, 1261 sample_name: str, 1262 load_raw=False, 1263 load_light=True, 1264 use_original_parser=True, 1265 raw_file_path=None, 1266 ) -> LCMSBase: 1267 """Return a LCMSBase object for a given sample name within the collection. 1268 1269 Parameters 1270 ---------- 1271 sample_name : str 1272 The sample name to retrieve the LCMS object for. 1273 load_raw : bool 1274 If True, load raw data from HDF5 files. Default is False. 1275 load_light : bool 1276 If True, only load the parameters, mass features, and scan info are initially loaded for each lcms object. Default is True. 1277 """ 1278 hdf5_file = self.folder_location / f"{sample_name}.corems/{sample_name}.hdf5" 1279 with ReadCoreMSHDFMassSpectra(hdf5_file) as parser: 1280 lcms_obj = parser.get_lcms_obj( 1281 load_raw=load_raw, 1282 load_light=load_light, 1283 use_original_parser=use_original_parser, 1284 raw_file_path=raw_file_path, 1285 ) 1286 if load_light: 1287 # mass_features_to_df always includes _eic_mz (object value or mz fallback) 1288 mf_df = lcms_obj.mass_features_to_df() 1289 lcms_obj.mass_features = {} 1290 lcms_obj.light_mf_df = mf_df 1291 return lcms_obj 1292 1293 def get_lcms_collection( 1294 self, load_raw=False, load_light=True, use_original_parser=True 1295 ) -> LCMSCollection: 1296 """Return a LCMSCollection object 1297 1298 Parameters 1299 ---------- 1300 load_raw : bool 1301 If True, load raw data from HDF5 files. Default is False. 1302 load_light : bool 1303 If True, only load the parameters, mass features, and scan info are initially loaded for each lcms object. 1304 After concatenating the mass_features, remove the mass_features attribute from the individual LCMS objects for memory efficiency. Default is True. 1305 Default is True. 1306 """ 1307 # Instantiate the LCMSCollection object 1308 lcms_coll = LCMSCollection( 1309 collection_location=self.folder_location, 1310 manifest=self.manifest, 1311 collection_parser=self, 1312 ) 1313 1314 # Set the number of cores on the LCMSCollection object from the ReadCoreMSHDFMassSpectraCollection object 1315 lcms_coll.parameters.lcms_collection.cores = self._cores 1316 1317 # Add LCMS objects to the collection 1318 samples = self._manifest_dict.keys() 1319 1320 # Initialize the LCMS object dictionary 1321 if self._cores > 1: 1322 if self._cores > len(samples): 1323 ncores = len(samples) 1324 else: 1325 ncores = self._cores 1326 # Create a pool of workers (one for each core or sample, whichever is smaller) 1327 args = [ 1328 (sample, load_raw, load_light, use_original_parser) 1329 for sample in samples 1330 ] 1331 with spawn_pool(ncores) as pool: 1332 lcms_objs = pool.starmap(self.get_lcms_obj, args) 1333 for sample_name, lcms_obj in zip(samples, lcms_objs): 1334 lcms_coll._lcms[sample_name] = lcms_obj 1335 1336 elif self._cores == 1: 1337 # Load the LCMS objects sequentially 1338 for sample_name in samples: 1339 lcms_coll._lcms[sample_name] = self.get_lcms_obj( 1340 sample_name, 1341 load_raw=load_raw, 1342 load_light=load_light, 1343 use_original_parser=use_original_parser, 1344 ) 1345 1346 else: 1347 raise ValueError( 1348 "Number of cores must be greater than 0 and set on the ReadCoreMSHDFMassSpectraCollection object." 1349 ) 1350 1351 # Check that all LCMS objects have the same polarity 1352 if len(set([x.polarity for k, x in lcms_coll._lcms.items()])) != 1: 1353 raise ValueError("All samples must have the same polarity.") 1354 1355 # Set ids on the LCMS objects in the manifest 1356 i = 0 1357 for sample in lcms_coll.samples: 1358 lcms_coll._manifest_dict[sample]["collection_id"] = i 1359 i += 1 1360 1361 # Reorder the LCMS objects 1362 lcms_coll._reorder_lcms_objects() 1363 1364 # Collect the mass features from the LCMS objects and combine them into a single dataframe for the collection 1365 lcms_coll._combine_mass_features() 1366 1367 # If load_light, remove the mass_feature attribute from the individual LCMS objects 1368 if load_light: 1369 for sample_name in lcms_coll.samples: 1370 lcms_coll._lcms[sample_name].mass_features = {} 1371 # Remove the light_mf_df attribute from the individual LCMS objects 1372 del lcms_coll._lcms[sample_name].light_mf_df 1373 1374 return lcms_coll 1375 1376 @property 1377 def manifest(self): 1378 return self._manifest_dict 1379 1380 @property 1381 def manifest_dataframe(self): 1382 return pd.DataFrame(self._manifest_dict).T 1383 1384 @property 1385 def hdf5_files(self): 1386 return [ 1387 self.folder_location / f"{sample_name}.corems/{sample_name}.hdf5" 1388 for sample_name in self._manifest_dict.keys() 1389 ] 1390 1391 @property 1392 def parameters_files(self): 1393 # Check if parameters files are saved as JSON or TOML 1394 json_files = [ 1395 self.folder_location / f"{sample_name}.corems/{sample_name}.json" 1396 for sample_name in self._manifest_dict.keys() 1397 ] 1398 toml_files = [ 1399 self.folder_location / f"{sample_name}.corems/{sample_name}.toml" 1400 for sample_name in self._manifest_dict.keys() 1401 ] 1402 if all([x.exists() for x in json_files]): 1403 return json_files 1404 elif all([x.exists() for x in toml_files]): 1405 return toml_files 1406 else: 1407 raise ValueError("Parameters files are not saved for all samples.") 1408 1409 1410class ReadSavedLCMSCollection(ReadCoreMSHDFMassSpectraCollection): 1411 """ 1412 Subclass to read and re-instantiate a LCMSCollection from a saved HDF5 file. 1413 1414 1415 Parameters 1416 ---------- 1417 collection_hdf5_path : str or Path 1418 Path to the saved LCMSCollection HDF5 file. 1419 cores : int, optional 1420 Number of cores for processing. Default is 1. 1421 """ 1422 1423 def __init__(self, collection_hdf5_path: str, cores: int = 1): 1424 # Convert to Path objects 1425 self.collection_hdf5_path = Path(collection_hdf5_path) 1426 1427 # Validate the collection file exists 1428 if not self.collection_hdf5_path.exists(): 1429 raise FileNotFoundError( 1430 f"Collection HDF5 file {self.collection_hdf5_path} not found." 1431 ) 1432 1433 # Validate cores 1434 self._validate_cores(cores) 1435 1436 # Load metadata from saved collection 1437 self._load_collection_metadata() 1438 1439 if not self.folder_location.exists(): 1440 raise FileNotFoundError( 1441 f"Folder location {self.folder_location} not found." 1442 ) 1443 1444 # Load the mass spectra data 1445 self._validate_manifest() 1446 1447 # Set the parameters file location 1448 self.parameters_location = self._get_parameters_location() 1449 1450 def _get_parameters_location(self): 1451 """Find the parameters file (JSON or TOML) associated with the collection HDF5 file.""" 1452 # Check for TOML file first (preferred) 1453 toml_path = self.collection_hdf5_path.with_suffix(".toml") 1454 if toml_path.exists(): 1455 return toml_path 1456 1457 # Check for JSON file 1458 json_path = self.collection_hdf5_path.with_suffix(".json") 1459 if json_path.exists(): 1460 return json_path 1461 1462 # No parameters file found 1463 return None 1464 1465 def _load_collection_metadata(self): 1466 """Load metadata and manifest from the saved collection HDF5 file.""" 1467 with h5py.File(self.collection_hdf5_path, "r") as f: 1468 self.folder_location = Path(f.attrs.get("lcms_objects_folder", "")) 1469 self.missing_mass_features_searched = f.attrs.get( 1470 "missing_mass_features_searched", False 1471 ) 1472 1473 # Call the _load_manifest function to process the manifest 1474 self._manifest_dict = self._load_manifest(f) 1475 1476 def _load_manifest(self, hdf_handle): 1477 """Load and clean the manifest from the HDF5 file.""" 1478 manifest_json = hdf_handle.attrs.get("manifest", "{}") 1479 if isinstance(manifest_json, bytes): 1480 manifest_json = manifest_json.decode("utf-8") 1481 loaded_manifest = json.loads(manifest_json) 1482 1483 # Convert integer values for 'use_rt_alignment' back to booleans 1484 def convert_back_to_bool(data): 1485 if isinstance(data, dict): 1486 # Process each key-value pair recursively 1487 return { 1488 k: ( 1489 bool(v) 1490 if k == "use_rt_alignment" and isinstance(v, int) 1491 else convert_back_to_bool(v) 1492 ) 1493 for k, v in data.items() 1494 } 1495 elif isinstance(data, list): 1496 # Recursively process lists 1497 return [convert_back_to_bool(item) for item in data] 1498 else: 1499 # Return non-dict/list types unchanged 1500 return data 1501 1502 # Clean the loaded manifest 1503 return convert_back_to_bool(loaded_manifest) 1504 1505 def _load_rt_alignments(self, lcms_collection): 1506 """Load retention time alignments from the saved collection HDF5 file.""" 1507 # First Set the rt_aligned flag from the collection-level attribute saved directly 1508 with h5py.File(self.collection_hdf5_path, "r") as f: 1509 lcms_collection.rt_aligned = f.attrs.get("rt_aligned", False) 1510 lcms_collection.rt_alignment_attempted = f.attrs.get( 1511 "rt_alignment_attempted", False 1512 ) 1513 1514 if lcms_collection.rt_aligned: 1515 with h5py.File(self.collection_hdf5_path, "r") as f: 1516 if "rt_alignments" in f: 1517 # Iterate over the group `rt_alignments` containing datasets and add to the corresponding lcms object 1518 rt_alignments_group = f["rt_alignments"] 1519 for sample_idx, lcms_obj in zip( 1520 rt_alignments_group.keys(), lcms_collection 1521 ): 1522 alignment_data = rt_alignments_group[sample_idx][:] 1523 scan_df = lcms_obj.scan_df 1524 scan_df["scan_time_aligned"] = alignment_data 1525 lcms_obj.scan_df = scan_df 1526 elif lcms_collection.rt_alignment_attempted: 1527 # This means it was attempted and not used, so we populate the "scan_time_aligned" 1528 for lcms_obj in lcms_collection: 1529 scan_df = lcms_obj.scan_df 1530 scan_df["scan_time_aligned"] = scan_df["scan_time"] 1531 lcms_obj.scan_df = scan_df 1532 1533 def _load_cluster_assignments(self, lcms_collection): 1534 """Load cluster assignments from the saved collection HDF5 file.""" 1535 with h5py.File(self.collection_hdf5_path, "r") as f: 1536 if "cluster_assignments" in f: 1537 # Access the group containing cluster assignments 1538 cluster_grp = f["cluster_assignments"] 1539 1540 # Reload index and cluster data 1541 index = cluster_grp["index"][:] # Extract index 1542 index = [ 1543 idx.decode("utf-8") for idx in index 1544 ] # Convert byte strings back to regular strings 1545 cluster_data = cluster_grp["cluster"][:] # Extract cluster column 1546 1547 # Reassemble the DataFrame 1548 cluster_df = pd.DataFrame({"cluster": cluster_data}, index=index) 1549 1550 # Assign cluster data back to lcms_collection.mass_features_dataframe 1551 lcms_collection.mass_features_dataframe = ( 1552 lcms_collection.mass_features_dataframe.join(cluster_df, how="left") 1553 ) 1554 1555 # Drop rows with NaN cluster values 1556 lcms_collection.mass_features_dataframe.dropna( 1557 subset=["cluster"], inplace=True 1558 ) 1559 1560 def _load_feature_group_labels(self, lcms_collection): 1561 """Restore ``feature_group_dataframe`` and merge onto feature tables.""" 1562 from corems.mass_spectra.calc.feature_grouping import ( 1563 GROUP_COLUMNS, 1564 empty_group_labels, 1565 merge_feature_group_labels_into_frames, 1566 ) 1567 1568 with h5py.File(self.collection_hdf5_path, "r") as f: 1569 if "feature_group_labels" not in f: 1570 return 1571 grp = f["feature_group_labels"] 1572 if "cluster" not in grp: 1573 raise ValueError( 1574 "feature_group_labels HDF5 group is missing 'cluster' dataset" 1575 ) 1576 clusters = np.asarray(grp["cluster"][:], dtype=np.int64) 1577 labels = empty_group_labels(clusters) 1578 for col in GROUP_COLUMNS: 1579 if col not in grp: 1580 continue 1581 raw = grp[col][:] 1582 if col in ("feature_group_id", "mono_cluster_id"): 1583 vals = [] 1584 for v in raw: 1585 if v is None or (isinstance(v, float) and np.isnan(v)): 1586 vals.append(pd.NA) 1587 else: 1588 vals.append(int(v)) 1589 labels[col] = pd.Series(vals, index=labels.index, dtype="Int64") 1590 else: 1591 decoded = [] 1592 for v in raw: 1593 if isinstance(v, (bytes, bytearray, np.bytes_)): 1594 s = v.decode("utf-8") 1595 else: 1596 s = "" if v is None else str(v) 1597 decoded.append(None if s == "" else s) 1598 labels[col] = decoded 1599 1600 lcms_collection.feature_group_dataframe = labels 1601 mf, induced = merge_feature_group_labels_into_frames( 1602 labels, 1603 lcms_collection.mass_features_dataframe, 1604 getattr(lcms_collection, "induced_mass_features_dataframe", None), 1605 ) 1606 lcms_collection.mass_features_dataframe = mf 1607 if ( 1608 getattr(lcms_collection, "induced_mass_features_dataframe", None) 1609 is not None 1610 ): 1611 lcms_collection.induced_mass_features_dataframe = induced 1612 1613 def get_lcms_collection( 1614 self, 1615 load_raw=False, 1616 load_light=False, 1617 load_representatives=False, 1618 load_eics=False, 1619 load_ms1=False, 1620 load_ms2=False, 1621 ): 1622 """Get the LCMS collection from the saved HDF5 file. 1623 1624 Parameters 1625 ---------- 1626 load_raw : bool, optional 1627 If True, load raw data. Default is False. 1628 load_light : bool, optional 1629 If True, load light data (minimal). Default is False. 1630 load_representatives : bool, optional 1631 If True, load representative mass features from clusters. Default is False. 1632 load_eics : bool, optional 1633 If True, load EIC data for clustered mass features. Default is False. 1634 load_ms1 : bool, optional 1635 If True, load MS1 spectra for loaded mass features. Default is False. 1636 load_ms2 : bool, optional 1637 If True, load MS2 spectra for loaded mass features. Default is False. 1638 1639 Returns 1640 ------- 1641 LCMSCollection 1642 The loaded LCMS collection object. 1643 """ 1644 # First load the LCMSCollection object exactly as in the parent class 1645 lcms_collection = super().get_lcms_collection( 1646 load_raw=load_raw, load_light=load_light 1647 ) 1648 1649 # Set the missing_mass_features_searched flag from saved metadata 1650 lcms_collection.missing_mass_features_searched = ( 1651 self.missing_mass_features_searched 1652 ) 1653 1654 # Load parameters if a parameters file exists 1655 if self.parameters_location: 1656 self._load_parameters(lcms_collection) 1657 1658 # Add retention time alignments if they exist 1659 self._load_rt_alignments(lcms_collection) 1660 1661 # Add cluster assignments if they exist 1662 self._load_cluster_assignments(lcms_collection) 1663 1664 # Load induced mass features if they exist 1665 self._load_induced_mass_features(lcms_collection) 1666 1667 # Load EICs for induced mass features from collection HDF5 1668 if lcms_collection.missing_mass_features_searched and load_eics: 1669 self._load_induced_eics_from_collection(lcms_collection) 1670 1671 # Combine induced mass features into the collection-level dataframe if any were loaded 1672 if lcms_collection.missing_mass_features_searched: 1673 lcms_collection._combine_mass_features(induced_features=True) 1674 1675 # Restore feature-group labels after clusters (+ induced) are in place 1676 self._load_feature_group_labels(lcms_collection) 1677 1678 # Load representative mass features if requested 1679 if load_representatives: 1680 self._load_representative_mass_features(lcms_collection) 1681 1682 # Load MS1 and/or MS2 spectra for loaded mass features if requested 1683 if load_ms1 or load_ms2: 1684 # Reuse the existing ReloadFeaturesOperation from the pipeline system 1685 from corems.mass_spectra.calc.lc_calc_operations import ( 1686 ReloadFeaturesOperation, 1687 ) 1688 1689 operations = [ 1690 ReloadFeaturesOperation( 1691 "reload_spectra", add_ms1=load_ms1, add_ms2=load_ms2 1692 ) 1693 ] 1694 lcms_collection.process_samples_pipeline( 1695 operations, keep_raw_data=False, show_progress=False 1696 ) 1697 1698 # Load EICs for clustered features if requested 1699 if load_eics: 1700 # Reuse the existing LoadEICsOperation from the pipeline system 1701 from corems.mass_spectra.calc.lc_calc_operations import LoadEICsOperation 1702 1703 operations = [LoadEICsOperation("load_eics")] 1704 lcms_collection.process_samples_pipeline( 1705 operations, keep_raw_data=False, show_progress=False 1706 ) 1707 1708 # Associate EICs with mass features (same as in process_consensus_features) 1709 for sample_id in range(len(lcms_collection.samples)): 1710 sample = lcms_collection[sample_id] 1711 if sample.eics: # Only if EICs were loaded 1712 # Associate EICs with regular mass features 1713 sample.associate_eics_with_mass_features(induced=False) 1714 # Associate EICs with induced mass features 1715 sample.associate_eics_with_mass_features(induced=True) 1716 1717 return lcms_collection 1718 1719 def _load_parameters(self, lcms_collection): 1720 """Load collection-level parameters from the saved parameters file.""" 1721 from corems.encapsulation.input.parameter_from_json import ( 1722 load_and_set_json_parameters_lcms_collection, 1723 load_and_set_toml_parameters_lcms_collection, 1724 ) 1725 1726 if self.parameters_location.suffix == ".json": 1727 load_and_set_json_parameters_lcms_collection( 1728 lcms_collection, self.parameters_location 1729 ) 1730 elif self.parameters_location.suffix == ".toml": 1731 load_and_set_toml_parameters_lcms_collection( 1732 lcms_collection, self.parameters_location 1733 ) 1734 else: 1735 warnings.warn( 1736 f"Unknown parameter file format: {self.parameters_location.suffix}. Skipping parameter loading." 1737 ) 1738 1739 def _load_induced_mass_features(self, lcms_collection): 1740 """Load induced mass features from the saved collection HDF5 file. 1741 1742 Induced mass features are gap-filled features that exist at the collection level. 1743 This method loads them from the collection HDF5 file with all their attributes 1744 and datasets, and distributes them to individual LCMS objects. 1745 1746 Parameters 1747 ---------- 1748 lcms_collection : LCMSCollection 1749 The LCMS collection object to populate with induced mass features. 1750 """ 1751 with h5py.File(self.collection_hdf5_path, "r") as f: 1752 if "induced_mass_features" not in f: 1753 return 1754 1755 # Access the top-level induced mass features group 1756 imf_group = f["induced_mass_features"] 1757 1758 # Iterate through each sample's induced mass features 1759 for sample_idx in imf_group.keys(): 1760 lcms_obj = lcms_collection[int(sample_idx)] 1761 sample_group = imf_group[sample_idx] 1762 1763 # Load each mass feature for this sample 1764 for mf_id_str in sample_group.keys(): 1765 mf_group = sample_group[mf_id_str] 1766 1767 # Note: Induced mass feature IDs are strings like 'c2923_0_i', not integers 1768 # Keep them as strings since that's how they're stored 1769 mf_id = mf_id_str 1770 1771 # Instantiate the LCMSMassFeature object with required attributes 1772 mass_feature = LCMSMassFeature( 1773 lcms_obj, 1774 mz=mf_group.attrs["_mz_exp"], 1775 retention_time=mf_group.attrs["_retention_time"], 1776 intensity=mf_group.attrs["_intensity"], 1777 apex_scan=mf_group.attrs["_apex_scan"], 1778 persistence=mf_group.attrs.get("_persistence", 0), 1779 id=mf_id, 1780 ) 1781 1782 # Populate additional attributes from HDF5 attributes 1783 for key in mf_group.attrs.keys() - { 1784 "_mz_exp", 1785 "_mz_cal", 1786 "_retention_time", 1787 "_intensity", 1788 "_apex_scan", 1789 "_persistence", 1790 }: 1791 setattr(mass_feature, key, mf_group.attrs[key]) 1792 1793 # Populate attributes from HDF5 datasets (arrays) 1794 for key in mf_group.keys(): 1795 setattr(mass_feature, key, mf_group[key][:]) 1796 # Convert _noise_score from array to tuple 1797 if key == "_noise_score": 1798 mass_feature._noise_score = tuple(mass_feature._noise_score) 1799 1800 # Add to the LCMS object's induced_mass_features dictionary 1801 lcms_obj.induced_mass_features[mf_id] = mass_feature 1802 1803 def _load_induced_eics_from_collection(self, lcms_collection): 1804 """Load EICs for induced mass features from the collection HDF5 file. 1805 1806 Induced mass features are gap-filled features. Their EICs are saved at the 1807 collection level and need to be loaded and associated with the induced mass features. 1808 1809 Parameters 1810 ---------- 1811 lcms_collection : LCMSCollection 1812 The LCMS collection object with induced mass features to associate EICs with. 1813 """ 1814 with h5py.File(self.collection_hdf5_path, "r") as f: 1815 if "induced_eics" not in f: 1816 return 1817 1818 # Access the top-level induced EICs group 1819 induced_eics_group = f["induced_eics"] 1820 1821 # Iterate through each sample's induced EICs 1822 for sample_idx in induced_eics_group.keys(): 1823 lcms_obj = lcms_collection[int(sample_idx)] 1824 sample_group = induced_eics_group[sample_idx] 1825 1826 # Use the static helper to load EICs 1827 loaded_eics = ReadCoreMSHDFMassSpectra._load_eics_from_hdf5_group( 1828 sample_group, lcms_obj 1829 ) 1830 1831 # Ensure eics dictionary exists (should already be initialized in __init__) 1832 if not hasattr(lcms_obj, "eics") or lcms_obj.eics is None: 1833 lcms_obj.eics = {} 1834 1835 # Add to lcms_obj.eics dictionary 1836 for eic_mz, eic in loaded_eics.items(): 1837 lcms_obj.eics[eic_mz] = eic 1838 1839 # Associate EICs with induced mass features after all samples processed 1840 # This is done outside the loop to handle all samples at once 1841 for lcms_obj in lcms_collection: 1842 if len(lcms_obj.induced_mass_features) > 0: 1843 lcms_obj.associate_eics_with_mass_features(induced=True) 1844 1845 def _load_representative_mass_features(self, lcms_collection): 1846 """Load representative mass features for all clusters from HDF5 files. 1847 1848 This method uses the same logic as process_consensus_features() when loading 1849 representatives, calling get_sample_mf_map_for_representatives() (DRY helper) 1850 to determine which features to load. 1851 1852 Parameters 1853 ---------- 1854 lcms_collection : LCMSCollection 1855 The LCMS collection object to populate with representative mass features. 1856 """ 1857 # Get cluster assignments from the mass_features_dataframe 1858 if "cluster" not in lcms_collection.mass_features_dataframe.columns: 1859 return 1860 1861 # Use DRY helper method to build sample_mf_map with cluster IDs 1862 sample_mf_map = lcms_collection.get_sample_mf_map_for_representatives( 1863 include_cluster_id=True 1864 ) 1865 1866 # Load mass features for each sample 1867 for sample_id, mf_list in sample_mf_map.items(): 1868 lcms_obj = lcms_collection[sample_id] 1869 1870 # Load each mass feature 1871 for mf_id, cluster_id in mf_list: 1872 self._load_single_mass_feature(lcms_obj, mf_id, cluster_id) 1873 1874 def _load_single_mass_feature(self, lcms_obj, feature_id, cluster_index=None): 1875 """Load a single mass feature from an LCMS object's HDF5 file. 1876 1877 Parameters 1878 ---------- 1879 lcms_obj : LCMSBase 1880 The LCMS object to add the mass feature to. 1881 feature_id : int 1882 The ID of the mass feature to load. 1883 cluster_index : int, optional 1884 The cluster index to assign to the loaded mass feature. 1885 """ 1886 hdf5_path = lcms_obj.file_location.with_suffix(".hdf5") 1887 1888 if not hdf5_path.exists(): 1889 return 1890 1891 with h5py.File(hdf5_path, "r") as f: 1892 if "mass_features" not in f: 1893 return 1894 1895 mf_group = f["mass_features"] 1896 feature_id_str = str(feature_id) 1897 1898 if feature_id_str not in mf_group: 1899 return 1900 1901 mf_data = mf_group[feature_id_str] 1902 1903 # Create LCMSMassFeature object 1904 mass_feature = LCMSMassFeature( 1905 lcms_obj, 1906 mz=mf_data.attrs["_mz_exp"], 1907 retention_time=mf_data.attrs["_retention_time"], 1908 intensity=mf_data.attrs["_intensity"], 1909 apex_scan=mf_data.attrs["_apex_scan"], 1910 persistence=mf_data.attrs.get("_persistence", 0), 1911 id=feature_id, 1912 ) 1913 1914 # Set cluster_index if provided 1915 if cluster_index is not None: 1916 mass_feature.cluster_index = cluster_index 1917 1918 # Populate additional attributes from HDF5 attributes 1919 for key in mf_data.attrs.keys() - { 1920 "_mz_exp", 1921 "_mz_cal", 1922 "_retention_time", 1923 "_intensity", 1924 "_apex_scan", 1925 "_persistence", 1926 }: 1927 setattr(mass_feature, key, mf_data.attrs[key]) 1928 1929 # Populate attributes from HDF5 datasets (arrays) 1930 for key in mf_data.keys(): 1931 setattr(mass_feature, key, mf_data[key][:]) 1932 # Convert _noise_score from array to tuple 1933 if key == "_noise_score": 1934 mass_feature._noise_score = tuple(mass_feature._noise_score) 1935 1936 # Add to the LCMS object's mass_features dictionary 1937 lcms_obj.mass_features[feature_id] = mass_feature
36def create_manifest_from_folder( 37 folder_path: Path, 38 output_path: Path = None, 39 batch_time_threshold_hours: float = 12.0, 40 center_name: str = None, 41 overwrite: bool = False, 42) -> Path: 43 """ 44 Create a manifest CSV file for ReadCoreMSHDFMassSpectraCollection from CoreMS HDF5 files. 45 46 Scans a folder for .corems subdirectories and generates a manifest with columns: 47 sample_name, batch, order, center, time. Files are batched by creation time, and 48 one sample is designated as the retention time alignment center. 49 50 Parameters 51 ---------- 52 folder_path : Path 53 Path to folder containing .corems subdirectories with HDF5 files. 54 output_path : Path, optional 55 Output manifest CSV path. Default: folder_path/manifest.csv. 56 batch_time_threshold_hours : float, optional 57 Time gap in hours for batch separation. Default: 12.0. 58 center_name : str, optional 59 Sample name to designate as RT alignment center (must exist in samples). 60 If None, the middle sample (by creation time) is used. 61 overwrite : bool, optional 62 Whether to overwrite existing manifest. Default: False. 63 64 Returns 65 ------- 66 Path 67 Path to created manifest file. 68 69 Raises 70 ------ 71 FileNotFoundError 72 If folder_path doesn't exist or contains no .corems subdirectories. 73 FileExistsError 74 If output file exists and overwrite is False. 75 ValueError 76 If no HDF5 files found, or center_name doesn't match any sample. 77 """ 78 if not folder_path.exists(): 79 raise FileNotFoundError(f"Folder {folder_path} does not exist.") 80 81 # Set default output path if not provided 82 if output_path is None: 83 output_path = folder_path / "manifest.csv" 84 85 # Check if output file exists 86 if output_path.exists() and not overwrite: 87 raise FileExistsError( 88 f"Manifest file {output_path} already exists. " 89 "Set overwrite=True to replace it." 90 ) 91 92 # Find all .corems subdirectories 93 corems_dirs = sorted( 94 [d for d in folder_path.iterdir() if d.is_dir() and d.suffix == ".corems"] 95 ) 96 97 if not corems_dirs: 98 raise FileNotFoundError( 99 f"No .corems subdirectories found in {folder_path}. " 100 "Ensure the folder contains processed CoreMS data." 101 ) 102 103 # Collect sample information 104 sample_data = [] 105 106 for corems_dir in corems_dirs: 107 sample_name = corems_dir.stem # Remove .corems extension 108 hdf5_file = corems_dir / f"{sample_name}.hdf5" 109 110 if not hdf5_file.exists(): 111 print(f"Warning: HDF5 file not found for {sample_name}, skipping.") 112 continue 113 114 # Get creation time using the ReadCoreMSHDFMassSpectra method 115 try: 116 # Use context manager to ensure file is properly closed 117 with ReadCoreMSHDFMassSpectra(str(hdf5_file)) as parser: 118 # Use the get_original_creation_time() method which checks HDF5 attrs first, 119 # then falls back to original parser if needed 120 creation_time = parser.get_original_creation_time() 121 122 # Skip sample if creation time unavailable 123 if creation_time is None: 124 print( 125 f"Warning: Could not get original creation time for {sample_name}, skipping." 126 ) 127 continue 128 129 except Exception as e: 130 print( 131 f"Warning: Error getting creation time for {sample_name}: {e}, skipping." 132 ) 133 continue 134 135 sample_data.append( 136 { 137 "sample_name": sample_name, 138 "creation_time": creation_time, 139 "hdf5_path": hdf5_file, 140 } 141 ) 142 143 if not sample_data: 144 raise ValueError( 145 f"No valid HDF5 files found in {folder_path}. " 146 "Ensure .corems subdirectories contain .hdf5 files." 147 ) 148 149 # Sort by creation time 150 sample_data.sort(key=lambda x: x["creation_time"]) 151 152 # Assign batches based on time threshold 153 batch_assignments = [] 154 current_batch = 1 155 156 for i, sample in enumerate(sample_data): 157 if i == 0: 158 batch_assignments.append(current_batch) 159 else: 160 time_diff = sample["creation_time"] - sample_data[i - 1]["creation_time"] 161 time_diff_hours = time_diff.total_seconds() / 3600 162 163 if time_diff_hours > batch_time_threshold_hours: 164 current_batch += 1 165 166 batch_assignments.append(current_batch) 167 168 # Determine which sample should be the center for retention time alignment 169 sample_names = [s["sample_name"] for s in sample_data] 170 171 if center_name is not None: 172 # Validate that center_name is in the discovered samples 173 if center_name not in sample_names: 174 raise ValueError( 175 f"Specified center_name '{center_name}' not found in discovered samples. " 176 f"Available samples: {', '.join(sample_names)}" 177 ) 178 center_sample = center_name 179 else: 180 # Use the middle sample (by creation time) as center 181 middle_idx = len(sample_data) // 2 182 center_sample = sample_data[middle_idx]["sample_name"] 183 print( 184 f"Auto-selected center sample: {center_sample} (index {middle_idx} of {len(sample_data)}, middle by creation time)" 185 ) 186 187 # Create manifest dataframe with center column as TRUE/FALSE 188 manifest_df = pd.DataFrame( 189 { 190 "sample_name": sample_names, 191 "batch": batch_assignments, 192 "order": list(range(1, len(sample_data) + 1)), 193 "center": [ 194 "TRUE" if name == center_sample else "FALSE" for name in sample_names 195 ], 196 "time": [ 197 s["creation_time"].strftime("%Y-%m-%dT%H:%M:%SZ") for s in sample_data 198 ], 199 } 200 ) 201 202 # Sort manifest by time before saving to ensure proper order 203 manifest_df = manifest_df.sort_values("time").reset_index(drop=True) 204 # Update order column to reflect sorted order 205 manifest_df["order"] = list(range(1, len(manifest_df) + 1)) 206 207 # Save manifest 208 manifest_df.to_csv(output_path, index=False) 209 210 print(f"Manifest created successfully at {output_path}") 211 print(f"Total samples: {len(sample_data)}") 212 print(f"Number of batches: {current_batch}") 213 print( 214 f"Batch assignments: {dict(zip(range(1, current_batch + 1), [batch_assignments.count(b) for b in range(1, current_batch + 1)]))}" 215 ) 216 217 return output_path
Create a manifest CSV file for ReadCoreMSHDFMassSpectraCollection from CoreMS HDF5 files.
Scans a folder for .corems subdirectories and generates a manifest with columns: sample_name, batch, order, center, time. Files are batched by creation time, and one sample is designated as the retention time alignment center.
Parameters
- folder_path (Path): Path to folder containing .corems subdirectories with HDF5 files.
- output_path (Path, optional): Output manifest CSV path. Default: folder_path/manifest.csv.
- batch_time_threshold_hours (float, optional): Time gap in hours for batch separation. Default: 12.0.
- center_name (str, optional): Sample name to designate as RT alignment center (must exist in samples). If None, the middle sample (by creation time) is used.
- overwrite (bool, optional): Whether to overwrite existing manifest. Default: False.
Returns
- Path: Path to created manifest file.
Raises
- FileNotFoundError: If folder_path doesn't exist or contains no .corems subdirectories.
- FileExistsError: If output file exists and overwrite is False.
- ValueError: If no HDF5 files found, or center_name doesn't match any sample.
220class ReadCoreMSHDFMassSpectra( 221 SpectraParserInterface, ReadCoreMSHDF_MassSpectrum, Thread 222): 223 """Class to read CoreMS HDF5 files and populate a LCMS or MassSpectraBase object. 224 225 Parameters 226 ---------- 227 file_location : str 228 The location of the HDF5 file to read, including the suffix. 229 230 Attributes 231 ---------- 232 file_location : str 233 The location of the HDF5 file to read. 234 h5pydata : h5py.File 235 The HDF5 file object. 236 scans : list 237 A list of the location of individual mass spectra within the HDF5 file. 238 scan_number_list : list 239 A list of the scan numbers of the mass spectra within the HDF5 file. 240 parameters_location : str 241 The location of the parameters file (json or toml). 242 243 Methods 244 ------- 245 * import_mass_spectra(mass_spectra). 246 Imports all mass spectra from the HDF5 file onto the LCMS or MassSpectraBase object. 247 * get_mass_spectrum_from_scan(scan_number). 248 Return mass spectrum data object from scan number. 249 * load(). 250 Placeholder method to meet the requirements of the SpectraParserInterface. 251 * run(mass_spectra). 252 Runs the importer functions to populate a LCMS or MassSpectraBase object. 253 * import_scan_info(mass_spectra). 254 Imports the scan info from the HDF5 file to populate the _scan_info attribute 255 on the LCMS or MassSpectraBase object 256 * import_ms_unprocessed(mass_spectra). 257 Imports the unprocessed mass spectra from the HDF5 file to populate the 258 _ms_unprocessed attribute on the LCMS or MassSpectraBase object 259 * import_parameters(mass_spectra). 260 Imports the parameters from the HDF5 file to populate the parameters 261 attribute on the LCMS or MassSpectraBase object 262 * import_mass_features(mass_spectra). 263 Imports the mass features from the HDF5 file to populate the mass_features 264 attribute on the LCMS or MassSpectraBase object 265 * import_eics(mass_spectra). 266 Imports the extracted ion chromatograms from the HDF5 file to populate the 267 eics attribute on the LCMS or MassSpectraBase object 268 * import_spectral_search_results(mass_spectra). 269 Imports the spectral search results from the HDF5 file to populate the 270 spectral_search_results attribute on the LCMS or MassSpectraBase object 271 * get_mass_spectra_obj(). 272 Return mass spectra data object, populating the _ms list on the LCMS or 273 MassSpectraBase object from the HDF5 file 274 * get_lcms_obj(). 275 Return LCMSBase object, populating the majority of the attributes on the 276 LCMS object from the HDF5 file 277 278 """ 279 280 def __init__(self, file_location: str): 281 Thread.__init__(self) 282 ReadCoreMSHDF_MassSpectrum.__init__(self, file_location) 283 284 # override the scans attribute on ReadCoreMSHDF_MassSpectrum class to expect a nested location within the HDF5 file 285 self.scans = [ 286 "mass_spectra/" + x for x in list(self.h5pydata["mass_spectra"].keys()) 287 ] 288 self.scan_number_list = sorted( 289 [int(float(i)) for i in list(self.h5pydata["mass_spectra"].keys())] 290 ) 291 292 # set the location of the parameters file (json or toml) 293 add_files = [ 294 x 295 for x in self.file_location.parent.glob( 296 self.file_location.name.replace(".hdf5", ".*") 297 ) 298 if x.suffix != ".hdf5" 299 ] 300 if len([x for x in add_files if x.suffix == ".json"]) > 0: 301 self.parameters_location = [x for x in add_files if x.suffix == ".json"][0] 302 elif len([x for x in add_files if x.suffix == ".toml"]) > 0: 303 self.parameters_location = [x for x in add_files if x.suffix == ".toml"][0] 304 else: 305 self.parameters_location = None 306 307 def __enter__(self): 308 """Context manager entry.""" 309 return self 310 311 def __exit__(self, exc_type, exc_val, exc_tb): 312 """Context manager exit - closes the HDF5 file.""" 313 if hasattr(self, "h5pydata") and self.h5pydata is not None: 314 self.h5pydata.close() 315 return False 316 317 def close(self): 318 """Explicitly close the HDF5 file.""" 319 if hasattr(self, "h5pydata") and self.h5pydata is not None: 320 self.h5pydata.close() 321 322 def get_mass_spectrum_from_scan(self, scan_number): 323 """Return mass spectrum data object from scan number.""" 324 if scan_number in self.scan_number_list: 325 mass_spec = self.get_mass_spectrum(scan_number) 326 return mass_spec 327 else: 328 raise Exception("Scan number not found in HDF5 file.") 329 330 def get_mass_spectra_from_scan_list( 331 self, scan_list, spectrum_mode, auto_process=True 332 ): 333 """Return a list of mass spectrum data objects from a list of scan numbers. 334 335 Parameters 336 ---------- 337 scan_list : list 338 A list of scan numbers to retrieve mass spectra for. 339 spectrum_mode : str 340 The spectrum mode to use when retrieving the mass spectra. 341 Note that this parameter is not used for CoreMS HDF5 files, as the spectra are already processed and only 342 centroided spectra are saved. 343 auto_process : bool 344 If True, automatically process the mass spectra when retrieving them. 345 Note that this parameter is not used for CoreMS HDF5 files, as the spectra are already processed and only 346 centroided spectra are saved. 347 348 Returns 349 ------- 350 list 351 A list of mass spectrum data objects corresponding to the provided scan numbers. 352 """ 353 mass_spectra_list = [] 354 for scan_number in scan_list: 355 if scan_number in self.scan_number_list: 356 mass_spec = self.get_mass_spectrum_from_scan(scan_number) 357 mass_spectra_list.append(mass_spec) 358 else: 359 warnings.warn(f"Scan number {scan_number} not found in HDF5 file.") 360 return mass_spectra_list 361 362 def load(self) -> None: 363 """ """ 364 pass 365 366 def get_ms_raw( 367 self, 368 spectra=None, 369 scan_df=None, 370 time_range: Optional[ 371 Union[Tuple[float, float], List[Tuple[float, float]]] 372 ] = None, 373 ) -> dict: 374 """ """ 375 # Warn if spectra or scan_df are not None that they are not used for CoreMS HDF5 files and should be rerun after instantiation 376 if spectra is not None or scan_df is not None: 377 SyntaxWarning( 378 "get_ms_raw method for CoreMS HDF5 files can only access saved data, consider rerunning after instantiation." 379 ) 380 ms_unprocessed = {} 381 dict_group_load = self.h5pydata["ms_unprocessed"] 382 dict_group_keys = dict_group_load.keys() 383 for k in dict_group_keys: 384 dataset = dict_group_load[k] 385 ms_up_int = dataset[:] 386 column_labels_json = dataset.attrs.get("column_labels") 387 column_labels = ( 388 json.loads(column_labels_json) 389 if column_labels_json 390 else ["scan", "mz", "intensity"] 391 ) 392 ms_unprocessed[int(k)] = pd.DataFrame(ms_up_int, columns=column_labels) 393 return ms_unprocessed 394 395 def get_scan_df( 396 self, 397 time_range: Optional[ 398 Union[Tuple[float, float], List[Tuple[float, float]]] 399 ] = None, 400 ) -> pd.DataFrame: 401 scan_info = {} 402 dict_group_load = self.h5pydata["scan_info"] 403 dict_group_keys = dict_group_load.keys() 404 for k in dict_group_keys: 405 scan_info[k] = dict_group_load[k][:] 406 scan_df = pd.DataFrame(scan_info) 407 scan_df.set_index("scan", inplace=True, drop=False) 408 # HDF5 stores text as fixed-width bytes (numpy "S"). Decode only those 409 # columns. Do not use select_dtypes([object]) + str.decode: under pandas 410 # 3 / infer_string, already-decoded string columns can be selected and 411 # corrupted by a blanket decode. 412 for col in scan_df.columns: 413 series = scan_df[col] 414 sample = series.dropna() 415 if sample.empty: 416 continue 417 first = sample.iloc[0] 418 if isinstance(first, (bytes, bytearray, np.bytes_)): 419 scan_df[col] = series.map( 420 lambda x: ( 421 x.decode("utf-8") 422 if isinstance(x, (bytes, bytearray, np.bytes_)) 423 else x 424 ) 425 ) 426 427 # Apply time range filtering if specified 428 if time_range is not None: 429 time_ranges = self._normalize_time_range(time_range) 430 mask = pd.Series([False] * len(scan_df), index=scan_df.index) 431 for start_time, end_time in time_ranges: 432 mask |= (scan_df.scan_time >= start_time) & ( 433 scan_df.scan_time <= end_time 434 ) 435 scan_df = scan_df[mask] 436 437 return scan_df 438 439 def run(self, mass_spectra, load_raw=True, load_light=False) -> None: 440 """Runs the importer functions to populate a LCMS or MassSpectraBase object. 441 442 Notes 443 ----- 444 The following functions are run in order, if the HDF5 file contains the necessary data: 445 1. import_parameters(), which populates the parameters attribute on the LCMS or MassSpectraBase object. 446 2. import_mass_spectra(), which populates the _ms list on the LCMS or MassSpectraBase object. 447 3. import_scan_info(), which populates the _scan_info on the LCMS or MassSpectraBase object. 448 4. import_ms_unprocessed(), which populates the _ms_unprocessed attribute on the LCMS or MassSpectraBase object. 449 5. import_mass_features(), which populates the mass_features attribute on the LCMS or MassSpectraBase object. 450 6. import_eics(), which populates the eics attribute on the LCMS or MassSpectraBase object. 451 7. import_spectral_search_results(), which populates the spectral_search_results attribute on the LCMS or MassSpectraBase object. 452 453 Parameters 454 ---------- 455 mass_spectra : LCMSBase or MassSpectraBase 456 The LCMS or MassSpectraBase object to populate with mass spectra, generally instantiated with only the file_location, analyzer, and instrument_label attributes. 457 load_raw : bool 458 If True, load raw data (unprocessed) from HDF5 files for overall lcms object and individual mass spectra. Default is True. 459 load_light : bool 460 If True, only load the parameters, mass features, and scan info. Default is False. 461 462 Returns 463 ------- 464 None, but populates several attributes on the LCMS or MassSpectraBase object. 465 466 """ 467 if self.parameters_location is not None: 468 # Populate the parameters attribute on the LCMS object 469 self.import_parameters(mass_spectra) 470 471 if "mass_spectra" in self.h5pydata and not load_light: 472 # Populate the _ms list on the LCMS object 473 self.import_mass_spectra(mass_spectra, load_raw=load_raw) 474 475 if "scan_info" in self.h5pydata: 476 # Populate the _scan_info attribute on the LCMS object 477 self.import_scan_info(mass_spectra) 478 479 if "ms_unprocessed" in self.h5pydata and load_raw and not load_light: 480 # Populate the _ms_unprocessed attribute on the LCMS object 481 self.import_ms_unprocessed(mass_spectra) 482 483 if "mass_features" in self.h5pydata: 484 # Populate the mass_features attribute on the LCMS object 485 self.import_mass_features(mass_spectra) 486 487 if "eics" in self.h5pydata and not load_light: 488 # Populate the eics attribute on the LCMS object 489 self.import_eics(mass_spectra) 490 491 if "spectral_search_results" in self.h5pydata and not load_light: 492 # Populate the spectral_search_results attribute on the LCMS object 493 self.import_spectral_search_results(mass_spectra) 494 495 def import_mass_spectra(self, mass_spectra, load_raw=True) -> None: 496 """Imports all mass spectra from the HDF5 file. 497 498 Parameters 499 ---------- 500 mass_spectra : LCMSBase | MassSpectraBase 501 The MassSpectraBase or LCMSBase object to populate with mass spectra. 502 load_raw : bool 503 If True, load raw data (unprocessed) from HDF5 files for overall lcms object and individual mass spectra. Default 504 505 Returns 506 ------- 507 None, but populates the '_ms' list on the LCMSBase or MassSpectraBase 508 object with mass spectra from the HDF5 file. 509 """ 510 for scan_number in self.scan_number_list: 511 mass_spec = self.get_mass_spectrum(scan_number, load_raw=load_raw) 512 mass_spec.scan_number = scan_number 513 mass_spectra.add_mass_spectrum(mass_spec) 514 515 def import_scan_info(self, mass_spectra) -> None: 516 """Imports the scan info from the HDF5 file. 517 518 Parameters 519 ---------- 520 lcms : LCMSBase | MassSpectraBase 521 The MassSpectraBase or LCMSBase objects 522 523 Returns 524 ------- 525 None, but populates the 'scan_df' attribute on the LCMSBase or MassSpectraBase 526 object with a pandas DataFrame of the 'scan_info' from the HDF5 file. 527 528 """ 529 scan_df = self.get_scan_df() 530 mass_spectra.scan_df = scan_df 531 532 def import_ms_unprocessed(self, mass_spectra) -> None: 533 """Imports the unprocessed mass spectra from the HDF5 file. 534 535 Parameters 536 ---------- 537 lcms : LCMSBase | MassSpectraBase 538 The MassSpectraBase or LCMSBase objects 539 540 Returns 541 ------- 542 None, but populates the '_ms_unprocessed' attribute on the LCMSBase or MassSpectraBase 543 object with a dictionary of the 'ms_unprocessed' from the HDF5 file. 544 545 """ 546 ms_unprocessed = self.get_ms_raw() 547 mass_spectra._ms_unprocessed = ms_unprocessed 548 549 def import_parameters(self, mass_spectra) -> None: 550 """Imports the parameters from the HDF5 file. 551 552 Parameters 553 ---------- 554 mass_spectra : LCMSBase | MassSpectraBase 555 The MassSpectraBase or LCMSBase object to populate with parameters. 556 557 Returns 558 ------- 559 None, but populates the 'parameters' attribute on the LCMS or MassSpectraBase 560 object with a dictionary of the 'parameters' from the HDF5 file. 561 562 """ 563 if ".json" == self.parameters_location.suffix: 564 load_and_set_json_parameters_lcms(mass_spectra, self.parameters_location) 565 if ".toml" == self.parameters_location.suffix: 566 load_and_set_toml_parameters_lcms(mass_spectra, self.parameters_location) 567 else: 568 raise Exception( 569 "Parameters file must be in JSON format, TOML format is not yet supported." 570 ) 571 572 def import_mass_features(self, mass_spectra, mf_ids=None) -> None: 573 """Imports the mass features from the HDF5 file. 574 575 Parameters 576 ---------- 577 mass_spectra : LCMSBase | MassSpectraBase 578 The MassSpectraBase or LCMSBase object to populate with mass features. 579 mf_ids : list, optional 580 A list of mass feature IDs to import. If None, all mass features are imported. 581 582 Returns 583 ------- 584 None, but populates the 'mass_features' attribute on the LCMSBase or MassSpectraBase 585 object with a dictionary of the 'mass_features' from the HDF5 file. 586 587 """ 588 dict_group_load = self.h5pydata["mass_features"] 589 dict_group_keys = dict_group_load.keys() 590 for k in dict_group_keys: 591 if mf_ids is not None and int(k) not in mf_ids: 592 continue 593 # Instantiate the MassFeature object 594 mass_feature = LCMSMassFeature( 595 mass_spectra, 596 mz=dict_group_load[k].attrs["_mz_exp"], 597 retention_time=dict_group_load[k].attrs["_retention_time"], 598 intensity=dict_group_load[k].attrs["_intensity"], 599 apex_scan=dict_group_load[k].attrs["_apex_scan"], 600 persistence=dict_group_load[k].attrs["_persistence"], 601 id=int(k), 602 ) 603 604 # Populate additional attributes on the MassFeature object 605 for key in dict_group_load[k].attrs.keys() - { 606 "_mz_exp", 607 "_mz_cal", 608 "_retention_time", 609 "_intensity", 610 "_apex_scan", 611 "_persistence", 612 }: 613 setattr(mass_feature, key, dict_group_load[k].attrs[key]) 614 615 # Populate attributes on MassFeature object that are lists 616 for key in dict_group_load[k].keys(): 617 setattr(mass_feature, key, dict_group_load[k][key][:]) 618 # Convert _noise_score from array to tuple 619 if key == "_noise_score": 620 mass_feature._noise_score = tuple(mass_feature._noise_score) 621 mass_spectra.mass_features[int(k)] = mass_feature 622 623 # Associate mass features with ms1 and ms2 spectra, if available 624 for mf_id in mass_spectra.mass_features.keys(): 625 if mass_spectra.mass_features[mf_id].apex_scan in mass_spectra._ms.keys(): 626 mass_spectra.mass_features[mf_id].mass_spectrum = mass_spectra._ms[ 627 mass_spectra.mass_features[mf_id].apex_scan 628 ] 629 if mass_spectra.mass_features[mf_id].ms2_scan_numbers is not None: 630 for ms2_scan in mass_spectra.mass_features[mf_id].ms2_scan_numbers: 631 if ms2_scan in mass_spectra._ms.keys(): 632 mass_spectra.mass_features[mf_id].ms2_mass_spectra[ms2_scan] = ( 633 mass_spectra._ms[ms2_scan] 634 ) 635 636 def import_eics(self, mass_spectra, mz_list=None, mz_tolerance=0.0001): 637 """Imports the extracted ion chromatograms from the HDF5 file. 638 639 Parameters 640 ---------- 641 mass_spectra : LCMSBase | MassSpectraBase 642 The MassSpectraBase or LCMSBase object to populate with extracted ion chromatograms. 643 mz_list : list of float, optional 644 List of m/z values to load EICs for. If None, loads all EICs. Default is None. 645 mz_tolerance : float, optional 646 Tolerance in Daltons for matching m/z values when mz_list is provided. 647 Default is 0.0001 Da. 648 649 Returns 650 ------- 651 None, but populates the 'eics' attribute on the LCMSBase or MassSpectraBase 652 object with a dictionary of the 'eics' from the HDF5 file. 653 654 """ 655 if "eics" not in self.h5pydata: 656 # Samples with no exported EICs (e.g. empty / fully gap-filled) have no group 657 return 658 659 dict_group_load = self.h5pydata["eics"] 660 dict_group_keys = dict_group_load.keys() 661 662 # Prefilter dict_group_keys if mz_list is provided to EICs within tolerance 663 if mz_list is not None: 664 if len(mz_list) == 0: 665 return 666 target_mz_array = np.array(sorted(mz_list), dtype=float) 667 mzs = [ 668 float(k) 669 for k in dict_group_keys 670 if np.abs(float(k) - target_mz_array).min() < mz_tolerance 671 ] 672 dict_group_keys = [str(mz) for mz in mzs] 673 674 for k in dict_group_keys: 675 # Check if we should load this EIC (filter by m/z if list provided) 676 eic_mz = dict_group_load[k].attrs["mz"] 677 678 my_eic = EIC_Data( 679 scans=dict_group_load[k]["scans"][:], 680 time=dict_group_load[k]["time"][:], 681 eic=dict_group_load[k]["eic"][:], 682 ) 683 for key in dict_group_load[k].keys(): 684 if key not in ["scans", "time", "eic"]: 685 setattr(my_eic, key, dict_group_load[k][key][:]) 686 # if key is apexes, convert to a tuple of a list 687 if key == "apexes" and len(my_eic.apexes) > 0: 688 my_eic.apexes = [tuple(x) for x in my_eic.apexes] 689 # Add to mass_spectra object 690 mass_spectra.eics[eic_mz] = my_eic 691 692 # Associate EICs with mass features using tolerance-based matching 693 mass_spectra.associate_eics_with_mass_features() 694 695 @staticmethod 696 def _load_eics_from_hdf5_group(eics_group, lcms_obj, mz_filter=None): 697 """Load EICs from an HDF5 group. 698 699 This is a static helper method that can be reused to load EIC data 700 from any HDF5 group in a consistent format. 701 702 Parameters 703 ---------- 704 eics_group : h5py.Group 705 The HDF5 group containing EIC data. 706 lcms_obj : LCMSBase 707 The LCMS object to associate EICs with (for reference, not modified). 708 mz_filter : list, optional 709 List of m/z values to load. If None, loads all EICs. Default is None. 710 Uses tolerance-based matching (0.0001). 711 712 Returns 713 ------- 714 dict 715 Dictionary of EIC_Data objects keyed by m/z value. 716 """ 717 from corems.mass_spectra.factory.chromat_data import EIC_Data 718 719 loaded_eics = {} 720 tolerance = 0.0001 721 722 for eic_key_str in eics_group.keys(): 723 eic_mz = ( 724 float(eic_key_str) 725 if eic_key_str.replace(".", "", 1).replace("-", "", 1).isdigit() 726 else eics_group[eic_key_str].attrs.get("mz") 727 ) 728 729 # If mz_filter is provided, check if this EIC matches any requested m/z 730 if mz_filter is not None: 731 if not any(abs(eic_mz - mz) < tolerance for mz in mz_filter): 732 continue 733 734 eic_data = eics_group[eic_key_str] 735 736 # Create EIC_Data object from datasets 737 eic = EIC_Data( 738 scans=list(eic_data["scans"][:]) if "scans" in eic_data else [], 739 time=list(eic_data["time"][:]) if "time" in eic_data else [], 740 eic=list(eic_data["eic"][:]) if "eic" in eic_data else [], 741 apexes=list(eic_data["apexes"][:]) if "apexes" in eic_data else [], 742 ) 743 744 # Load any additional datasets 745 for key in eic_data.keys(): 746 if key not in ["scans", "time", "eic", "apexes"]: 747 setattr(eic, key, eic_data[key][:]) 748 749 loaded_eics[eic_mz] = eic 750 751 return loaded_eics 752 753 def import_spectral_search_results(self, mass_spectra): 754 """Imports the spectral search results from the HDF5 file. 755 756 Parameters 757 ---------- 758 mass_spectra : LCMSBase | MassSpectraBase 759 The MassSpectraBase or LCMSBase object to populate with spectral search results. 760 761 Returns 762 ------- 763 None, but populates the 'spectral_search_results' attribute on the LCMSBase or MassSpectraBase 764 object with a dictionary of the 'spectral_search_results' from the HDF5 file. 765 766 """ 767 overall_results_dict = {} 768 ms2_results_load = self.h5pydata["spectral_search_results"] 769 for k in ms2_results_load.keys(): 770 overall_results_dict[int(k)] = {} 771 for k2 in ms2_results_load[k].keys(): 772 ms2_search_res = SpectrumSearchResults( 773 query_spectrum=mass_spectra._ms[int(k)], 774 precursor_mz=ms2_results_load[k][k2].attrs["precursor_mz"], 775 spectral_similarity_search_results={}, 776 ) 777 778 for key in ms2_results_load[k][k2].keys() - {"precursor_mz"}: 779 data = list(ms2_results_load[k][k2][key][:]) 780 if data and isinstance(data[0], bytes): 781 data = [x.decode("utf-8") for x in data] 782 setattr(ms2_search_res, key, data) 783 784 overall_results_dict[int(k)][ 785 ms2_results_load[k][k2].attrs["precursor_mz"] 786 ] = ms2_search_res 787 788 # add to mass_spectra 789 mass_spectra.spectral_search_results.update(overall_results_dict) 790 791 # If there are mass features, associate the results with each mass feature 792 if len(mass_spectra.mass_features) > 0: 793 for mass_feature_id, mass_feature in mass_spectra.mass_features.items(): 794 scan_ids = mass_feature.ms2_scan_numbers 795 for ms2_scan_id in scan_ids: 796 precursor_mz = mass_feature.mz 797 try: 798 mass_spectra.spectral_search_results[ms2_scan_id][precursor_mz] 799 except KeyError: 800 pass 801 else: 802 mass_spectra.mass_features[ 803 mass_feature_id 804 ].ms2_similarity_results.append( 805 mass_spectra.spectral_search_results[ms2_scan_id][ 806 precursor_mz 807 ] 808 ) 809 810 def get_mass_spectra_obj( 811 self, 812 load_raw=True, 813 load_light=False, 814 time_range: Optional[ 815 Union[Tuple[float, float], List[Tuple[float, float]]] 816 ] = None, 817 ) -> MassSpectraBase: 818 """ 819 Return mass spectra data object, populating the _ms list on MassSpectraBase object from the HDF5 file. 820 821 Parameters 822 ---------- 823 load_raw : bool 824 If True, load raw data (unprocessed) from HDF5 files for overall spectra object and individual mass spectra. Default is True. 825 load_light : bool 826 If True, only load the parameters, mass features, and scan info. Default is False. 827 time_range : tuple or list of tuples, optional 828 Retention time range(s) to load. Can be: 829 - Single range: (start_time, end_time) in minutes 830 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 831 If None, loads all scans. Note: For HDF5 files, this parameter is accepted for 832 interface consistency but not currently used in filtering. 833 834 """ 835 # Instantiate the LCMS object 836 spectra_obj = MassSpectraBase( 837 file_location=self.file_location, 838 analyzer=self.analyzer, 839 instrument_label=self.instrument_label, 840 sample_name=self.sample_name, 841 ) 842 843 # This will populate the _ms list on the LCMS or MassSpectraBase object 844 self.run(spectra_obj, load_raw=load_raw, load_light=load_light) 845 846 return spectra_obj 847 848 def get_lcms_obj( 849 self, 850 load_raw=True, 851 load_light=False, 852 use_original_parser=True, 853 raw_file_path=None, 854 time_range: Optional[ 855 Union[Tuple[float, float], List[Tuple[float, float]]] 856 ] = None, 857 ) -> LCMSBase: 858 """ 859 Return LCMSBase object, populating attributes on the LCMSBase object from the HDF5 file. 860 861 Parameters 862 ---------- 863 load_raw : bool 864 If True, load raw data (unprocessed) from HDF5 files for overall lcms object and individual mass spectra. Default is True. 865 load_light : bool 866 If True, only load the parameters, mass features, and scan info. Default is False. 867 use_original_parser : bool 868 If True, use the original parser to populate the LCMS object. Default is True. 869 raw_file_path : str 870 The location of the raw file to parse if attempting to use original parser. 871 Default is None, which attempts to get the raw file path from the HDF5 file. 872 If the original file path has moved, this parameter can be used to specify the new location. 873 time_range : tuple or list of tuples, optional 874 Retention time range(s) to load. Can be: 875 - Single range: (start_time, end_time) in minutes 876 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 877 If None, loads all scans. Note: For HDF5 files, this parameter is accepted for 878 interface consistency. If use_original_parser=True, time_range can be passed to the 879 original parser for filtering. 880 """ 881 # Instantiate the LCMS object 882 lcms_obj = LCMSBase( 883 file_location=self.file_location, 884 analyzer=self.analyzer, 885 instrument_label=self.instrument_label, 886 sample_name=self.sample_name, 887 ) 888 889 # This will populate the majority of the attributes on the LCMS object 890 self.run(lcms_obj, load_raw=load_raw, load_light=load_light) 891 892 # Set final attributes of the LCMS object 893 lcms_obj.polarity = self.h5pydata.attrs["polarity"] 894 lcms_obj._scans_number_list = list(lcms_obj.scan_df.scan) 895 lcms_obj._retention_time_list = list(lcms_obj.scan_df.scan_time) 896 lcms_obj._tic_list = list(lcms_obj.scan_df.tic) 897 898 # If use_original_parser is True, instantiate the original parser and populate the LCMS object 899 if use_original_parser: 900 lcms_obj = self.add_original_parser(lcms_obj, raw_file_path=raw_file_path) 901 else: 902 lcms_obj.spectra_parser_class = self.__class__ 903 904 return lcms_obj 905 906 def get_raw_file_location(self): 907 """ 908 Get the raw file location from the HDF5 file attributes. 909 910 Returns 911 ------- 912 str 913 The raw file location. 914 """ 915 if "original_file_location" in self.h5pydata.attrs: 916 return self.h5pydata.attrs["original_file_location"] 917 else: 918 return None 919 920 def add_original_parser(self, mass_spectra, raw_file_path=None): 921 """ 922 Add the original parser to the mass spectra object. 923 924 Parameters 925 ---------- 926 mass_spectra : MassSpectraBase | LCMSBase 927 The MassSpectraBase or LCMSBase object to add the original parser to. 928 raw_file_path : str 929 The location of the raw file to parse. Default is None, which attempts to get the raw file path from the HDF5 file. 930 """ 931 # Get the original parser type 932 og_parser_type = self.h5pydata.attrs["parser_type"] 933 934 # If raw_file_path is None, get it from the HDF5 file attributes 935 if raw_file_path is None: 936 raw_file_path = self.get_raw_file_location() 937 if raw_file_path is None: 938 raise ValueError( 939 "Raw file path not found in HDF5 file attributes, cannot instantiate original parser." 940 ) 941 942 # Set the raw file path on the mass_spectra object so the parser knows where to find the raw file 943 mass_spectra.raw_file_location = raw_file_path 944 945 if og_parser_type == "ImportMassSpectraThermoMSFileReader": 946 # Imported lazily: rawFileReader pulls in pythonnet (`clr`), which 947 # needs a .NET/Mono runtime. Only reopening a Thermo .raw file 948 # needs it; reading an HDF5 file of any other origin must not. 949 from corems.mass_spectra.input.rawFileReader import ( 950 ImportMassSpectraThermoMSFileReader, 951 ) 952 953 # Check that the parser can be instantiated with the raw file path 954 parser_class = ImportMassSpectraThermoMSFileReader 955 elif og_parser_type == "MZMLSpectraParser": 956 # Check that the parser can be instantiated with the raw file path 957 parser_class = MZMLSpectraParser 958 959 # Set the spectra parser class on the mass_spectra object so the spectra_parser property can be used with the original parser 960 mass_spectra.spectra_parser_class = parser_class 961 962 return mass_spectra 963 964 def get_original_creation_time(self): 965 """ 966 Get the creation time of the original raw data file. 967 968 First checks if creation_time is saved in the HDF5 file attributes. 969 If not found, attempts to instantiate the original parser and get the creation time. 970 971 Returns 972 ------- 973 datetime 974 The creation time of the original raw data file, or None if not available. 975 """ 976 # Check if creation_time is saved in HDF5 attributes 977 if "creation_time" in self.h5pydata.attrs: 978 from datetime import datetime 979 980 return datetime.fromisoformat(self.h5pydata.attrs["creation_time"]) 981 982 # Fall back to using original parser to get creation time 983 try: 984 # Get the original parser type and raw file path 985 og_parser_type = self.h5pydata.attrs.get("parser_type") 986 raw_file_path = self.get_raw_file_location() 987 988 if og_parser_type is None or raw_file_path is None: 989 warnings.warn( 990 "Cannot retrieve creation time: parser_type or original_file_location not found in HDF5 attributes." 991 ) 992 return None 993 994 # Check if raw file exists 995 from pathlib import Path 996 997 if not Path(raw_file_path).exists(): 998 warnings.warn( 999 f"Cannot retrieve creation time: original raw file not found at {raw_file_path}" 1000 ) 1001 return None 1002 1003 # Instantiate the original parser 1004 if og_parser_type == "ImportMassSpectraThermoMSFileReader": 1005 # Lazy import; see add_original_parser for why. 1006 from corems.mass_spectra.input.rawFileReader import ( 1007 ImportMassSpectraThermoMSFileReader, 1008 ) 1009 1010 parser = ImportMassSpectraThermoMSFileReader(raw_file_path) 1011 elif og_parser_type == "MZMLSpectraParser": 1012 parser = MZMLSpectraParser(raw_file_path) 1013 else: 1014 warnings.warn( 1015 f"Unknown parser type: {og_parser_type}, cannot retrieve creation time." 1016 ) 1017 return None 1018 1019 # Get creation time from parser 1020 return parser.get_creation_time() 1021 1022 except Exception as e: 1023 warnings.warn(f"Failed to retrieve creation time from original parser: {e}") 1024 return None 1025 1026 def get_creation_time(self): 1027 """ 1028 Get the creation time of the original raw data file. 1029 1030 This is an alias for get_original_creation_time() for backward compatibility. 1031 1032 Returns 1033 ------- 1034 datetime 1035 The creation time of the original raw data file, or None if not available. 1036 """ 1037 return self.get_original_creation_time() 1038 1039 def get_instrument_info(self): 1040 """ 1041 Raise a NotImplemented Warning, as instrument info is not available in CoreMS HDF5 files and returning None. 1042 """ 1043 warnings.warn( 1044 "Instrument info is not available in CoreMS HDF5 files, returning None." 1045 "This should be accessed through the original parser.", 1046 ) 1047 return None 1048 1049 def get_scans_in_time_range( 1050 self, 1051 time_range: Union[Tuple[float, float], List[Tuple[float, float]]], 1052 ms_level: Optional[int] = None, 1053 ) -> List[int]: 1054 """Return scan numbers within specified retention time range(s). 1055 1056 Parameters 1057 ---------- 1058 time_range : tuple or list of tuples 1059 Retention time range(s) in minutes. Can be: 1060 - Single range: (start_time, end_time) 1061 - Multiple ranges: [(start1, end1), (start2, end2), ...] 1062 ms_level : int, optional 1063 If specified, only return scans of this MS level (e.g., 1 for MS1, 2 for MS2). 1064 If None, returns scans of all MS levels. 1065 1066 Returns 1067 ------- 1068 list of int 1069 List of scan numbers within the specified time range(s) and MS level. 1070 """ 1071 # Normalize time range to list of tuples 1072 time_ranges = self._normalize_time_range(time_range) 1073 1074 # Get all scan data 1075 scan_df = self.get_scan_df() 1076 1077 # Filter by time range 1078 mask = pd.Series([False] * len(scan_df), index=scan_df.index) 1079 for start_time, end_time in time_ranges: 1080 mask |= (scan_df.scan_time >= start_time) & (scan_df.scan_time <= end_time) 1081 1082 filtered_df = scan_df[mask] 1083 1084 # Filter by MS level if specified 1085 if ms_level is not None: 1086 filtered_df = filtered_df[filtered_df.ms_level == ms_level] 1087 1088 return filtered_df.scan.tolist()
Class to read CoreMS HDF5 files and populate a LCMS or MassSpectraBase object.
Parameters
- file_location (str): The location of the HDF5 file to read, including the suffix.
Attributes
- file_location (str): The location of the HDF5 file to read.
- h5pydata (h5py.File): The HDF5 file object.
- scans (list): A list of the location of individual mass spectra within the HDF5 file.
- scan_number_list (list): A list of the scan numbers of the mass spectra within the HDF5 file.
- parameters_location (str): The location of the parameters file (json or toml).
Methods
- import_mass_spectra(mass_spectra). Imports all mass spectra from the HDF5 file onto the LCMS or MassSpectraBase object.
- get_mass_spectrum_from_scan(scan_number). Return mass spectrum data object from scan number.
- load(). Placeholder method to meet the requirements of the SpectraParserInterface.
- run(mass_spectra). Runs the importer functions to populate a LCMS or MassSpectraBase object.
- import_scan_info(mass_spectra). Imports the scan info from the HDF5 file to populate the _scan_info attribute on the LCMS or MassSpectraBase object
- import_ms_unprocessed(mass_spectra). Imports the unprocessed mass spectra from the HDF5 file to populate the _ms_unprocessed attribute on the LCMS or MassSpectraBase object
- import_parameters(mass_spectra). Imports the parameters from the HDF5 file to populate the parameters attribute on the LCMS or MassSpectraBase object
- import_mass_features(mass_spectra). Imports the mass features from the HDF5 file to populate the mass_features attribute on the LCMS or MassSpectraBase object
- import_eics(mass_spectra). Imports the extracted ion chromatograms from the HDF5 file to populate the eics attribute on the LCMS or MassSpectraBase object
- import_spectral_search_results(mass_spectra). Imports the spectral search results from the HDF5 file to populate the spectral_search_results attribute on the LCMS or MassSpectraBase object
- get_mass_spectra_obj(). Return mass spectra data object, populating the _ms list on the LCMS or MassSpectraBase object from the HDF5 file
- get_lcms_obj(). Return LCMSBase object, populating the majority of the attributes on the LCMS object from the HDF5 file
280 def __init__(self, file_location: str): 281 Thread.__init__(self) 282 ReadCoreMSHDF_MassSpectrum.__init__(self, file_location) 283 284 # override the scans attribute on ReadCoreMSHDF_MassSpectrum class to expect a nested location within the HDF5 file 285 self.scans = [ 286 "mass_spectra/" + x for x in list(self.h5pydata["mass_spectra"].keys()) 287 ] 288 self.scan_number_list = sorted( 289 [int(float(i)) for i in list(self.h5pydata["mass_spectra"].keys())] 290 ) 291 292 # set the location of the parameters file (json or toml) 293 add_files = [ 294 x 295 for x in self.file_location.parent.glob( 296 self.file_location.name.replace(".hdf5", ".*") 297 ) 298 if x.suffix != ".hdf5" 299 ] 300 if len([x for x in add_files if x.suffix == ".json"]) > 0: 301 self.parameters_location = [x for x in add_files if x.suffix == ".json"][0] 302 elif len([x for x in add_files if x.suffix == ".toml"]) > 0: 303 self.parameters_location = [x for x in add_files if x.suffix == ".toml"][0] 304 else: 305 self.parameters_location = None
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.
317 def close(self): 318 """Explicitly close the HDF5 file.""" 319 if hasattr(self, "h5pydata") and self.h5pydata is not None: 320 self.h5pydata.close()
Explicitly close the HDF5 file.
322 def get_mass_spectrum_from_scan(self, scan_number): 323 """Return mass spectrum data object from scan number.""" 324 if scan_number in self.scan_number_list: 325 mass_spec = self.get_mass_spectrum(scan_number) 326 return mass_spec 327 else: 328 raise Exception("Scan number not found in HDF5 file.")
Return mass spectrum data object from scan number.
330 def get_mass_spectra_from_scan_list( 331 self, scan_list, spectrum_mode, auto_process=True 332 ): 333 """Return a list of mass spectrum data objects from a list of scan numbers. 334 335 Parameters 336 ---------- 337 scan_list : list 338 A list of scan numbers to retrieve mass spectra for. 339 spectrum_mode : str 340 The spectrum mode to use when retrieving the mass spectra. 341 Note that this parameter is not used for CoreMS HDF5 files, as the spectra are already processed and only 342 centroided spectra are saved. 343 auto_process : bool 344 If True, automatically process the mass spectra when retrieving them. 345 Note that this parameter is not used for CoreMS HDF5 files, as the spectra are already processed and only 346 centroided spectra are saved. 347 348 Returns 349 ------- 350 list 351 A list of mass spectrum data objects corresponding to the provided scan numbers. 352 """ 353 mass_spectra_list = [] 354 for scan_number in scan_list: 355 if scan_number in self.scan_number_list: 356 mass_spec = self.get_mass_spectrum_from_scan(scan_number) 357 mass_spectra_list.append(mass_spec) 358 else: 359 warnings.warn(f"Scan number {scan_number} not found in HDF5 file.") 360 return mass_spectra_list
Return a list of mass spectrum data objects from a list of scan numbers.
Parameters
- scan_list (list): A list of scan numbers to retrieve mass spectra for.
- spectrum_mode (str): The spectrum mode to use when retrieving the mass spectra. Note that this parameter is not used for CoreMS HDF5 files, as the spectra are already processed and only centroided spectra are saved.
- auto_process (bool): If True, automatically process the mass spectra when retrieving them. Note that this parameter is not used for CoreMS HDF5 files, as the spectra are already processed and only centroided spectra are saved.
Returns
- list: A list of mass spectrum data objects corresponding to the provided scan numbers.
366 def get_ms_raw( 367 self, 368 spectra=None, 369 scan_df=None, 370 time_range: Optional[ 371 Union[Tuple[float, float], List[Tuple[float, float]]] 372 ] = None, 373 ) -> dict: 374 """ """ 375 # Warn if spectra or scan_df are not None that they are not used for CoreMS HDF5 files and should be rerun after instantiation 376 if spectra is not None or scan_df is not None: 377 SyntaxWarning( 378 "get_ms_raw method for CoreMS HDF5 files can only access saved data, consider rerunning after instantiation." 379 ) 380 ms_unprocessed = {} 381 dict_group_load = self.h5pydata["ms_unprocessed"] 382 dict_group_keys = dict_group_load.keys() 383 for k in dict_group_keys: 384 dataset = dict_group_load[k] 385 ms_up_int = dataset[:] 386 column_labels_json = dataset.attrs.get("column_labels") 387 column_labels = ( 388 json.loads(column_labels_json) 389 if column_labels_json 390 else ["scan", "mz", "intensity"] 391 ) 392 ms_unprocessed[int(k)] = pd.DataFrame(ms_up_int, columns=column_labels) 393 return ms_unprocessed
395 def get_scan_df( 396 self, 397 time_range: Optional[ 398 Union[Tuple[float, float], List[Tuple[float, float]]] 399 ] = None, 400 ) -> pd.DataFrame: 401 scan_info = {} 402 dict_group_load = self.h5pydata["scan_info"] 403 dict_group_keys = dict_group_load.keys() 404 for k in dict_group_keys: 405 scan_info[k] = dict_group_load[k][:] 406 scan_df = pd.DataFrame(scan_info) 407 scan_df.set_index("scan", inplace=True, drop=False) 408 # HDF5 stores text as fixed-width bytes (numpy "S"). Decode only those 409 # columns. Do not use select_dtypes([object]) + str.decode: under pandas 410 # 3 / infer_string, already-decoded string columns can be selected and 411 # corrupted by a blanket decode. 412 for col in scan_df.columns: 413 series = scan_df[col] 414 sample = series.dropna() 415 if sample.empty: 416 continue 417 first = sample.iloc[0] 418 if isinstance(first, (bytes, bytearray, np.bytes_)): 419 scan_df[col] = series.map( 420 lambda x: ( 421 x.decode("utf-8") 422 if isinstance(x, (bytes, bytearray, np.bytes_)) 423 else x 424 ) 425 ) 426 427 # Apply time range filtering if specified 428 if time_range is not None: 429 time_ranges = self._normalize_time_range(time_range) 430 mask = pd.Series([False] * len(scan_df), index=scan_df.index) 431 for start_time, end_time in time_ranges: 432 mask |= (scan_df.scan_time >= start_time) & ( 433 scan_df.scan_time <= end_time 434 ) 435 scan_df = scan_df[mask] 436 437 return scan_df
Return scan data as a pandas DataFrame.
Parameters
- time_range (tuple or list of tuples, optional):
Retention time range(s) to filter scans. Can be:
- Single range: (start_time, end_time) in minutes
- Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes If None, returns all scans.
Returns
- pd.DataFrame: DataFrame containing scan information, optionally filtered by time range.
439 def run(self, mass_spectra, load_raw=True, load_light=False) -> None: 440 """Runs the importer functions to populate a LCMS or MassSpectraBase object. 441 442 Notes 443 ----- 444 The following functions are run in order, if the HDF5 file contains the necessary data: 445 1. import_parameters(), which populates the parameters attribute on the LCMS or MassSpectraBase object. 446 2. import_mass_spectra(), which populates the _ms list on the LCMS or MassSpectraBase object. 447 3. import_scan_info(), which populates the _scan_info on the LCMS or MassSpectraBase object. 448 4. import_ms_unprocessed(), which populates the _ms_unprocessed attribute on the LCMS or MassSpectraBase object. 449 5. import_mass_features(), which populates the mass_features attribute on the LCMS or MassSpectraBase object. 450 6. import_eics(), which populates the eics attribute on the LCMS or MassSpectraBase object. 451 7. import_spectral_search_results(), which populates the spectral_search_results attribute on the LCMS or MassSpectraBase object. 452 453 Parameters 454 ---------- 455 mass_spectra : LCMSBase or MassSpectraBase 456 The LCMS or MassSpectraBase object to populate with mass spectra, generally instantiated with only the file_location, analyzer, and instrument_label attributes. 457 load_raw : bool 458 If True, load raw data (unprocessed) from HDF5 files for overall lcms object and individual mass spectra. Default is True. 459 load_light : bool 460 If True, only load the parameters, mass features, and scan info. Default is False. 461 462 Returns 463 ------- 464 None, but populates several attributes on the LCMS or MassSpectraBase object. 465 466 """ 467 if self.parameters_location is not None: 468 # Populate the parameters attribute on the LCMS object 469 self.import_parameters(mass_spectra) 470 471 if "mass_spectra" in self.h5pydata and not load_light: 472 # Populate the _ms list on the LCMS object 473 self.import_mass_spectra(mass_spectra, load_raw=load_raw) 474 475 if "scan_info" in self.h5pydata: 476 # Populate the _scan_info attribute on the LCMS object 477 self.import_scan_info(mass_spectra) 478 479 if "ms_unprocessed" in self.h5pydata and load_raw and not load_light: 480 # Populate the _ms_unprocessed attribute on the LCMS object 481 self.import_ms_unprocessed(mass_spectra) 482 483 if "mass_features" in self.h5pydata: 484 # Populate the mass_features attribute on the LCMS object 485 self.import_mass_features(mass_spectra) 486 487 if "eics" in self.h5pydata and not load_light: 488 # Populate the eics attribute on the LCMS object 489 self.import_eics(mass_spectra) 490 491 if "spectral_search_results" in self.h5pydata and not load_light: 492 # Populate the spectral_search_results attribute on the LCMS object 493 self.import_spectral_search_results(mass_spectra)
Runs the importer functions to populate a LCMS or MassSpectraBase object.
Notes
The following functions are run in order, if the HDF5 file contains the necessary data:
- import_parameters(), which populates the parameters attribute on the LCMS or MassSpectraBase object.
- import_mass_spectra(), which populates the _ms list on the LCMS or MassSpectraBase object.
- import_scan_info(), which populates the _scan_info on the LCMS or MassSpectraBase object.
- import_ms_unprocessed(), which populates the _ms_unprocessed attribute on the LCMS or MassSpectraBase object.
- import_mass_features(), which populates the mass_features attribute on the LCMS or MassSpectraBase object.
- import_eics(), which populates the eics attribute on the LCMS or MassSpectraBase object.
- import_spectral_search_results(), which populates the spectral_search_results attribute on the LCMS or MassSpectraBase object.
Parameters
- mass_spectra (LCMSBase or MassSpectraBase): The LCMS or MassSpectraBase object to populate with mass spectra, generally instantiated with only the file_location, analyzer, and instrument_label attributes.
- load_raw (bool): If True, load raw data (unprocessed) from HDF5 files for overall lcms object and individual mass spectra. Default is True.
- load_light (bool): If True, only load the parameters, mass features, and scan info. Default is False.
Returns
- None, but populates several attributes on the LCMS or MassSpectraBase object.
495 def import_mass_spectra(self, mass_spectra, load_raw=True) -> None: 496 """Imports all mass spectra from the HDF5 file. 497 498 Parameters 499 ---------- 500 mass_spectra : LCMSBase | MassSpectraBase 501 The MassSpectraBase or LCMSBase object to populate with mass spectra. 502 load_raw : bool 503 If True, load raw data (unprocessed) from HDF5 files for overall lcms object and individual mass spectra. Default 504 505 Returns 506 ------- 507 None, but populates the '_ms' list on the LCMSBase or MassSpectraBase 508 object with mass spectra from the HDF5 file. 509 """ 510 for scan_number in self.scan_number_list: 511 mass_spec = self.get_mass_spectrum(scan_number, load_raw=load_raw) 512 mass_spec.scan_number = scan_number 513 mass_spectra.add_mass_spectrum(mass_spec)
Imports all mass spectra from the HDF5 file.
Parameters
- mass_spectra (LCMSBase | MassSpectraBase): The MassSpectraBase or LCMSBase object to populate with mass spectra.
- load_raw (bool): If True, load raw data (unprocessed) from HDF5 files for overall lcms object and individual mass spectra. Default
Returns
- None, but populates the '_ms' list on the LCMSBase or MassSpectraBase
- object with mass spectra from the HDF5 file.
515 def import_scan_info(self, mass_spectra) -> None: 516 """Imports the scan info from the HDF5 file. 517 518 Parameters 519 ---------- 520 lcms : LCMSBase | MassSpectraBase 521 The MassSpectraBase or LCMSBase objects 522 523 Returns 524 ------- 525 None, but populates the 'scan_df' attribute on the LCMSBase or MassSpectraBase 526 object with a pandas DataFrame of the 'scan_info' from the HDF5 file. 527 528 """ 529 scan_df = self.get_scan_df() 530 mass_spectra.scan_df = scan_df
Imports the scan info from the HDF5 file.
Parameters
- lcms (LCMSBase | MassSpectraBase): The MassSpectraBase or LCMSBase objects
Returns
- None, but populates the 'scan_df' attribute on the LCMSBase or MassSpectraBase
- object with a pandas DataFrame of the 'scan_info' from the HDF5 file.
532 def import_ms_unprocessed(self, mass_spectra) -> None: 533 """Imports the unprocessed mass spectra from the HDF5 file. 534 535 Parameters 536 ---------- 537 lcms : LCMSBase | MassSpectraBase 538 The MassSpectraBase or LCMSBase objects 539 540 Returns 541 ------- 542 None, but populates the '_ms_unprocessed' attribute on the LCMSBase or MassSpectraBase 543 object with a dictionary of the 'ms_unprocessed' from the HDF5 file. 544 545 """ 546 ms_unprocessed = self.get_ms_raw() 547 mass_spectra._ms_unprocessed = ms_unprocessed
Imports the unprocessed mass spectra from the HDF5 file.
Parameters
- lcms (LCMSBase | MassSpectraBase): The MassSpectraBase or LCMSBase objects
Returns
- None, but populates the '_ms_unprocessed' attribute on the LCMSBase or MassSpectraBase
- object with a dictionary of the 'ms_unprocessed' from the HDF5 file.
549 def import_parameters(self, mass_spectra) -> None: 550 """Imports the parameters from the HDF5 file. 551 552 Parameters 553 ---------- 554 mass_spectra : LCMSBase | MassSpectraBase 555 The MassSpectraBase or LCMSBase object to populate with parameters. 556 557 Returns 558 ------- 559 None, but populates the 'parameters' attribute on the LCMS or MassSpectraBase 560 object with a dictionary of the 'parameters' from the HDF5 file. 561 562 """ 563 if ".json" == self.parameters_location.suffix: 564 load_and_set_json_parameters_lcms(mass_spectra, self.parameters_location) 565 if ".toml" == self.parameters_location.suffix: 566 load_and_set_toml_parameters_lcms(mass_spectra, self.parameters_location) 567 else: 568 raise Exception( 569 "Parameters file must be in JSON format, TOML format is not yet supported." 570 )
Imports the parameters from the HDF5 file.
Parameters
- mass_spectra (LCMSBase | MassSpectraBase): The MassSpectraBase or LCMSBase object to populate with parameters.
Returns
- None, but populates the 'parameters' attribute on the LCMS or MassSpectraBase
- object with a dictionary of the 'parameters' from the HDF5 file.
572 def import_mass_features(self, mass_spectra, mf_ids=None) -> None: 573 """Imports the mass features from the HDF5 file. 574 575 Parameters 576 ---------- 577 mass_spectra : LCMSBase | MassSpectraBase 578 The MassSpectraBase or LCMSBase object to populate with mass features. 579 mf_ids : list, optional 580 A list of mass feature IDs to import. If None, all mass features are imported. 581 582 Returns 583 ------- 584 None, but populates the 'mass_features' attribute on the LCMSBase or MassSpectraBase 585 object with a dictionary of the 'mass_features' from the HDF5 file. 586 587 """ 588 dict_group_load = self.h5pydata["mass_features"] 589 dict_group_keys = dict_group_load.keys() 590 for k in dict_group_keys: 591 if mf_ids is not None and int(k) not in mf_ids: 592 continue 593 # Instantiate the MassFeature object 594 mass_feature = LCMSMassFeature( 595 mass_spectra, 596 mz=dict_group_load[k].attrs["_mz_exp"], 597 retention_time=dict_group_load[k].attrs["_retention_time"], 598 intensity=dict_group_load[k].attrs["_intensity"], 599 apex_scan=dict_group_load[k].attrs["_apex_scan"], 600 persistence=dict_group_load[k].attrs["_persistence"], 601 id=int(k), 602 ) 603 604 # Populate additional attributes on the MassFeature object 605 for key in dict_group_load[k].attrs.keys() - { 606 "_mz_exp", 607 "_mz_cal", 608 "_retention_time", 609 "_intensity", 610 "_apex_scan", 611 "_persistence", 612 }: 613 setattr(mass_feature, key, dict_group_load[k].attrs[key]) 614 615 # Populate attributes on MassFeature object that are lists 616 for key in dict_group_load[k].keys(): 617 setattr(mass_feature, key, dict_group_load[k][key][:]) 618 # Convert _noise_score from array to tuple 619 if key == "_noise_score": 620 mass_feature._noise_score = tuple(mass_feature._noise_score) 621 mass_spectra.mass_features[int(k)] = mass_feature 622 623 # Associate mass features with ms1 and ms2 spectra, if available 624 for mf_id in mass_spectra.mass_features.keys(): 625 if mass_spectra.mass_features[mf_id].apex_scan in mass_spectra._ms.keys(): 626 mass_spectra.mass_features[mf_id].mass_spectrum = mass_spectra._ms[ 627 mass_spectra.mass_features[mf_id].apex_scan 628 ] 629 if mass_spectra.mass_features[mf_id].ms2_scan_numbers is not None: 630 for ms2_scan in mass_spectra.mass_features[mf_id].ms2_scan_numbers: 631 if ms2_scan in mass_spectra._ms.keys(): 632 mass_spectra.mass_features[mf_id].ms2_mass_spectra[ms2_scan] = ( 633 mass_spectra._ms[ms2_scan] 634 )
Imports the mass features from the HDF5 file.
Parameters
- mass_spectra (LCMSBase | MassSpectraBase): The MassSpectraBase or LCMSBase object to populate with mass features.
- mf_ids (list, optional): A list of mass feature IDs to import. If None, all mass features are imported.
Returns
- None, but populates the 'mass_features' attribute on the LCMSBase or MassSpectraBase
- object with a dictionary of the 'mass_features' from the HDF5 file.
636 def import_eics(self, mass_spectra, mz_list=None, mz_tolerance=0.0001): 637 """Imports the extracted ion chromatograms from the HDF5 file. 638 639 Parameters 640 ---------- 641 mass_spectra : LCMSBase | MassSpectraBase 642 The MassSpectraBase or LCMSBase object to populate with extracted ion chromatograms. 643 mz_list : list of float, optional 644 List of m/z values to load EICs for. If None, loads all EICs. Default is None. 645 mz_tolerance : float, optional 646 Tolerance in Daltons for matching m/z values when mz_list is provided. 647 Default is 0.0001 Da. 648 649 Returns 650 ------- 651 None, but populates the 'eics' attribute on the LCMSBase or MassSpectraBase 652 object with a dictionary of the 'eics' from the HDF5 file. 653 654 """ 655 if "eics" not in self.h5pydata: 656 # Samples with no exported EICs (e.g. empty / fully gap-filled) have no group 657 return 658 659 dict_group_load = self.h5pydata["eics"] 660 dict_group_keys = dict_group_load.keys() 661 662 # Prefilter dict_group_keys if mz_list is provided to EICs within tolerance 663 if mz_list is not None: 664 if len(mz_list) == 0: 665 return 666 target_mz_array = np.array(sorted(mz_list), dtype=float) 667 mzs = [ 668 float(k) 669 for k in dict_group_keys 670 if np.abs(float(k) - target_mz_array).min() < mz_tolerance 671 ] 672 dict_group_keys = [str(mz) for mz in mzs] 673 674 for k in dict_group_keys: 675 # Check if we should load this EIC (filter by m/z if list provided) 676 eic_mz = dict_group_load[k].attrs["mz"] 677 678 my_eic = EIC_Data( 679 scans=dict_group_load[k]["scans"][:], 680 time=dict_group_load[k]["time"][:], 681 eic=dict_group_load[k]["eic"][:], 682 ) 683 for key in dict_group_load[k].keys(): 684 if key not in ["scans", "time", "eic"]: 685 setattr(my_eic, key, dict_group_load[k][key][:]) 686 # if key is apexes, convert to a tuple of a list 687 if key == "apexes" and len(my_eic.apexes) > 0: 688 my_eic.apexes = [tuple(x) for x in my_eic.apexes] 689 # Add to mass_spectra object 690 mass_spectra.eics[eic_mz] = my_eic 691 692 # Associate EICs with mass features using tolerance-based matching 693 mass_spectra.associate_eics_with_mass_features()
Imports the extracted ion chromatograms from the HDF5 file.
Parameters
- mass_spectra (LCMSBase | MassSpectraBase): The MassSpectraBase or LCMSBase object to populate with extracted ion chromatograms.
- mz_list (list of float, optional): List of m/z values to load EICs for. If None, loads all EICs. Default is None.
- mz_tolerance (float, optional): Tolerance in Daltons for matching m/z values when mz_list is provided. Default is 0.0001 Da.
Returns
- None, but populates the 'eics' attribute on the LCMSBase or MassSpectraBase
- object with a dictionary of the 'eics' from the HDF5 file.
753 def import_spectral_search_results(self, mass_spectra): 754 """Imports the spectral search results from the HDF5 file. 755 756 Parameters 757 ---------- 758 mass_spectra : LCMSBase | MassSpectraBase 759 The MassSpectraBase or LCMSBase object to populate with spectral search results. 760 761 Returns 762 ------- 763 None, but populates the 'spectral_search_results' attribute on the LCMSBase or MassSpectraBase 764 object with a dictionary of the 'spectral_search_results' from the HDF5 file. 765 766 """ 767 overall_results_dict = {} 768 ms2_results_load = self.h5pydata["spectral_search_results"] 769 for k in ms2_results_load.keys(): 770 overall_results_dict[int(k)] = {} 771 for k2 in ms2_results_load[k].keys(): 772 ms2_search_res = SpectrumSearchResults( 773 query_spectrum=mass_spectra._ms[int(k)], 774 precursor_mz=ms2_results_load[k][k2].attrs["precursor_mz"], 775 spectral_similarity_search_results={}, 776 ) 777 778 for key in ms2_results_load[k][k2].keys() - {"precursor_mz"}: 779 data = list(ms2_results_load[k][k2][key][:]) 780 if data and isinstance(data[0], bytes): 781 data = [x.decode("utf-8") for x in data] 782 setattr(ms2_search_res, key, data) 783 784 overall_results_dict[int(k)][ 785 ms2_results_load[k][k2].attrs["precursor_mz"] 786 ] = ms2_search_res 787 788 # add to mass_spectra 789 mass_spectra.spectral_search_results.update(overall_results_dict) 790 791 # If there are mass features, associate the results with each mass feature 792 if len(mass_spectra.mass_features) > 0: 793 for mass_feature_id, mass_feature in mass_spectra.mass_features.items(): 794 scan_ids = mass_feature.ms2_scan_numbers 795 for ms2_scan_id in scan_ids: 796 precursor_mz = mass_feature.mz 797 try: 798 mass_spectra.spectral_search_results[ms2_scan_id][precursor_mz] 799 except KeyError: 800 pass 801 else: 802 mass_spectra.mass_features[ 803 mass_feature_id 804 ].ms2_similarity_results.append( 805 mass_spectra.spectral_search_results[ms2_scan_id][ 806 precursor_mz 807 ] 808 )
Imports the spectral search results from the HDF5 file.
Parameters
- mass_spectra (LCMSBase | MassSpectraBase): The MassSpectraBase or LCMSBase object to populate with spectral search results.
Returns
- None, but populates the 'spectral_search_results' attribute on the LCMSBase or MassSpectraBase
- object with a dictionary of the 'spectral_search_results' from the HDF5 file.
810 def get_mass_spectra_obj( 811 self, 812 load_raw=True, 813 load_light=False, 814 time_range: Optional[ 815 Union[Tuple[float, float], List[Tuple[float, float]]] 816 ] = None, 817 ) -> MassSpectraBase: 818 """ 819 Return mass spectra data object, populating the _ms list on MassSpectraBase object from the HDF5 file. 820 821 Parameters 822 ---------- 823 load_raw : bool 824 If True, load raw data (unprocessed) from HDF5 files for overall spectra object and individual mass spectra. Default is True. 825 load_light : bool 826 If True, only load the parameters, mass features, and scan info. Default is False. 827 time_range : tuple or list of tuples, optional 828 Retention time range(s) to load. Can be: 829 - Single range: (start_time, end_time) in minutes 830 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 831 If None, loads all scans. Note: For HDF5 files, this parameter is accepted for 832 interface consistency but not currently used in filtering. 833 834 """ 835 # Instantiate the LCMS object 836 spectra_obj = MassSpectraBase( 837 file_location=self.file_location, 838 analyzer=self.analyzer, 839 instrument_label=self.instrument_label, 840 sample_name=self.sample_name, 841 ) 842 843 # This will populate the _ms list on the LCMS or MassSpectraBase object 844 self.run(spectra_obj, load_raw=load_raw, load_light=load_light) 845 846 return spectra_obj
Return mass spectra data object, populating the _ms list on MassSpectraBase object from the HDF5 file.
Parameters
- load_raw (bool): If True, load raw data (unprocessed) from HDF5 files for overall spectra object and individual mass spectra. Default is True.
- load_light (bool): If True, only load the parameters, mass features, and scan info. Default is False.
- time_range (tuple or list of tuples, optional):
Retention time range(s) to load. Can be:
- Single range: (start_time, end_time) in minutes
- Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes If None, loads all scans. Note: For HDF5 files, this parameter is accepted for interface consistency but not currently used in filtering.
848 def get_lcms_obj( 849 self, 850 load_raw=True, 851 load_light=False, 852 use_original_parser=True, 853 raw_file_path=None, 854 time_range: Optional[ 855 Union[Tuple[float, float], List[Tuple[float, float]]] 856 ] = None, 857 ) -> LCMSBase: 858 """ 859 Return LCMSBase object, populating attributes on the LCMSBase object from the HDF5 file. 860 861 Parameters 862 ---------- 863 load_raw : bool 864 If True, load raw data (unprocessed) from HDF5 files for overall lcms object and individual mass spectra. Default is True. 865 load_light : bool 866 If True, only load the parameters, mass features, and scan info. Default is False. 867 use_original_parser : bool 868 If True, use the original parser to populate the LCMS object. Default is True. 869 raw_file_path : str 870 The location of the raw file to parse if attempting to use original parser. 871 Default is None, which attempts to get the raw file path from the HDF5 file. 872 If the original file path has moved, this parameter can be used to specify the new location. 873 time_range : tuple or list of tuples, optional 874 Retention time range(s) to load. Can be: 875 - Single range: (start_time, end_time) in minutes 876 - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes 877 If None, loads all scans. Note: For HDF5 files, this parameter is accepted for 878 interface consistency. If use_original_parser=True, time_range can be passed to the 879 original parser for filtering. 880 """ 881 # Instantiate the LCMS object 882 lcms_obj = LCMSBase( 883 file_location=self.file_location, 884 analyzer=self.analyzer, 885 instrument_label=self.instrument_label, 886 sample_name=self.sample_name, 887 ) 888 889 # This will populate the majority of the attributes on the LCMS object 890 self.run(lcms_obj, load_raw=load_raw, load_light=load_light) 891 892 # Set final attributes of the LCMS object 893 lcms_obj.polarity = self.h5pydata.attrs["polarity"] 894 lcms_obj._scans_number_list = list(lcms_obj.scan_df.scan) 895 lcms_obj._retention_time_list = list(lcms_obj.scan_df.scan_time) 896 lcms_obj._tic_list = list(lcms_obj.scan_df.tic) 897 898 # If use_original_parser is True, instantiate the original parser and populate the LCMS object 899 if use_original_parser: 900 lcms_obj = self.add_original_parser(lcms_obj, raw_file_path=raw_file_path) 901 else: 902 lcms_obj.spectra_parser_class = self.__class__ 903 904 return lcms_obj
Return LCMSBase object, populating attributes on the LCMSBase object from the HDF5 file.
Parameters
- load_raw (bool): If True, load raw data (unprocessed) from HDF5 files for overall lcms object and individual mass spectra. Default is True.
- load_light (bool): If True, only load the parameters, mass features, and scan info. Default is False.
- use_original_parser (bool): If True, use the original parser to populate the LCMS object. Default is True.
- raw_file_path (str): The location of the raw file to parse if attempting to use original parser. Default is None, which attempts to get the raw file path from the HDF5 file. If the original file path has moved, this parameter can be used to specify the new location.
- time_range (tuple or list of tuples, optional):
Retention time range(s) to load. Can be:
- Single range: (start_time, end_time) in minutes
- Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes If None, loads all scans. Note: For HDF5 files, this parameter is accepted for interface consistency. If use_original_parser=True, time_range can be passed to the original parser for filtering.
906 def get_raw_file_location(self): 907 """ 908 Get the raw file location from the HDF5 file attributes. 909 910 Returns 911 ------- 912 str 913 The raw file location. 914 """ 915 if "original_file_location" in self.h5pydata.attrs: 916 return self.h5pydata.attrs["original_file_location"] 917 else: 918 return None
Get the raw file location from the HDF5 file attributes.
Returns
- str: The raw file location.
920 def add_original_parser(self, mass_spectra, raw_file_path=None): 921 """ 922 Add the original parser to the mass spectra object. 923 924 Parameters 925 ---------- 926 mass_spectra : MassSpectraBase | LCMSBase 927 The MassSpectraBase or LCMSBase object to add the original parser to. 928 raw_file_path : str 929 The location of the raw file to parse. Default is None, which attempts to get the raw file path from the HDF5 file. 930 """ 931 # Get the original parser type 932 og_parser_type = self.h5pydata.attrs["parser_type"] 933 934 # If raw_file_path is None, get it from the HDF5 file attributes 935 if raw_file_path is None: 936 raw_file_path = self.get_raw_file_location() 937 if raw_file_path is None: 938 raise ValueError( 939 "Raw file path not found in HDF5 file attributes, cannot instantiate original parser." 940 ) 941 942 # Set the raw file path on the mass_spectra object so the parser knows where to find the raw file 943 mass_spectra.raw_file_location = raw_file_path 944 945 if og_parser_type == "ImportMassSpectraThermoMSFileReader": 946 # Imported lazily: rawFileReader pulls in pythonnet (`clr`), which 947 # needs a .NET/Mono runtime. Only reopening a Thermo .raw file 948 # needs it; reading an HDF5 file of any other origin must not. 949 from corems.mass_spectra.input.rawFileReader import ( 950 ImportMassSpectraThermoMSFileReader, 951 ) 952 953 # Check that the parser can be instantiated with the raw file path 954 parser_class = ImportMassSpectraThermoMSFileReader 955 elif og_parser_type == "MZMLSpectraParser": 956 # Check that the parser can be instantiated with the raw file path 957 parser_class = MZMLSpectraParser 958 959 # Set the spectra parser class on the mass_spectra object so the spectra_parser property can be used with the original parser 960 mass_spectra.spectra_parser_class = parser_class 961 962 return mass_spectra
Add the original parser to the mass spectra object.
Parameters
- mass_spectra (MassSpectraBase | LCMSBase): The MassSpectraBase or LCMSBase object to add the original parser to.
- raw_file_path (str): The location of the raw file to parse. Default is None, which attempts to get the raw file path from the HDF5 file.
964 def get_original_creation_time(self): 965 """ 966 Get the creation time of the original raw data file. 967 968 First checks if creation_time is saved in the HDF5 file attributes. 969 If not found, attempts to instantiate the original parser and get the creation time. 970 971 Returns 972 ------- 973 datetime 974 The creation time of the original raw data file, or None if not available. 975 """ 976 # Check if creation_time is saved in HDF5 attributes 977 if "creation_time" in self.h5pydata.attrs: 978 from datetime import datetime 979 980 return datetime.fromisoformat(self.h5pydata.attrs["creation_time"]) 981 982 # Fall back to using original parser to get creation time 983 try: 984 # Get the original parser type and raw file path 985 og_parser_type = self.h5pydata.attrs.get("parser_type") 986 raw_file_path = self.get_raw_file_location() 987 988 if og_parser_type is None or raw_file_path is None: 989 warnings.warn( 990 "Cannot retrieve creation time: parser_type or original_file_location not found in HDF5 attributes." 991 ) 992 return None 993 994 # Check if raw file exists 995 from pathlib import Path 996 997 if not Path(raw_file_path).exists(): 998 warnings.warn( 999 f"Cannot retrieve creation time: original raw file not found at {raw_file_path}" 1000 ) 1001 return None 1002 1003 # Instantiate the original parser 1004 if og_parser_type == "ImportMassSpectraThermoMSFileReader": 1005 # Lazy import; see add_original_parser for why. 1006 from corems.mass_spectra.input.rawFileReader import ( 1007 ImportMassSpectraThermoMSFileReader, 1008 ) 1009 1010 parser = ImportMassSpectraThermoMSFileReader(raw_file_path) 1011 elif og_parser_type == "MZMLSpectraParser": 1012 parser = MZMLSpectraParser(raw_file_path) 1013 else: 1014 warnings.warn( 1015 f"Unknown parser type: {og_parser_type}, cannot retrieve creation time." 1016 ) 1017 return None 1018 1019 # Get creation time from parser 1020 return parser.get_creation_time() 1021 1022 except Exception as e: 1023 warnings.warn(f"Failed to retrieve creation time from original parser: {e}") 1024 return None
Get the creation time of the original raw data file.
First checks if creation_time is saved in the HDF5 file attributes. If not found, attempts to instantiate the original parser and get the creation time.
Returns
- datetime: The creation time of the original raw data file, or None if not available.
1026 def get_creation_time(self): 1027 """ 1028 Get the creation time of the original raw data file. 1029 1030 This is an alias for get_original_creation_time() for backward compatibility. 1031 1032 Returns 1033 ------- 1034 datetime 1035 The creation time of the original raw data file, or None if not available. 1036 """ 1037 return self.get_original_creation_time()
Get the creation time of the original raw data file.
This is an alias for get_original_creation_time() for backward compatibility.
Returns
- datetime: The creation time of the original raw data file, or None if not available.
1039 def get_instrument_info(self): 1040 """ 1041 Raise a NotImplemented Warning, as instrument info is not available in CoreMS HDF5 files and returning None. 1042 """ 1043 warnings.warn( 1044 "Instrument info is not available in CoreMS HDF5 files, returning None." 1045 "This should be accessed through the original parser.", 1046 ) 1047 return None
Raise a NotImplemented Warning, as instrument info is not available in CoreMS HDF5 files and returning None.
1049 def get_scans_in_time_range( 1050 self, 1051 time_range: Union[Tuple[float, float], List[Tuple[float, float]]], 1052 ms_level: Optional[int] = None, 1053 ) -> List[int]: 1054 """Return scan numbers within specified retention time range(s). 1055 1056 Parameters 1057 ---------- 1058 time_range : tuple or list of tuples 1059 Retention time range(s) in minutes. Can be: 1060 - Single range: (start_time, end_time) 1061 - Multiple ranges: [(start1, end1), (start2, end2), ...] 1062 ms_level : int, optional 1063 If specified, only return scans of this MS level (e.g., 1 for MS1, 2 for MS2). 1064 If None, returns scans of all MS levels. 1065 1066 Returns 1067 ------- 1068 list of int 1069 List of scan numbers within the specified time range(s) and MS level. 1070 """ 1071 # Normalize time range to list of tuples 1072 time_ranges = self._normalize_time_range(time_range) 1073 1074 # Get all scan data 1075 scan_df = self.get_scan_df() 1076 1077 # Filter by time range 1078 mask = pd.Series([False] * len(scan_df), index=scan_df.index) 1079 for start_time, end_time in time_ranges: 1080 mask |= (scan_df.scan_time >= start_time) & (scan_df.scan_time <= end_time) 1081 1082 filtered_df = scan_df[mask] 1083 1084 # Filter by MS level if specified 1085 if ms_level is not None: 1086 filtered_df = filtered_df[filtered_df.ms_level == ms_level] 1087 1088 return filtered_df.scan.tolist()
Return scan numbers within specified retention time range(s).
Parameters
- time_range (tuple or list of tuples):
Retention time range(s) in minutes. Can be:
- Single range: (start_time, end_time)
- Multiple ranges: [(start1, end1), (start2, end2), ...]
- ms_level (int, optional): If specified, only return scans of this MS level (e.g., 1 for MS1, 2 for MS2). If None, returns scans of all MS levels.
Returns
- list of int: List of scan numbers within the specified time range(s) and MS level.
Inherited Members
- corems.mass_spectrum.input.coremsHDF5.ReadCoreMSHDF_MassSpectrum
- h5pydata
- load_raw_data
- load_peak_profile_indices
- get_mass_spectrum
- load_settings
- get_dataframe
- get_time_index_to_pull
- get_high_level_attr_data
- get_scan_group_attr_data
- get_raw_data_attr_data
- get_output_parameters
- corems.mass_spectrum.input.massList.ReadCoremsMasslist
- restore_peak_calibration
- add_molecular_formula
- corems.mass_spectrum.input.baseClass.MassListBaseClass
- file_location
- header_lines
- isCentroid
- isThermoProfile
- headerless
- analyzer
- instrument_label
- sample_name
- parameters
- set_parameter_from_toml
- set_parameter_from_json
- data_type
- delimiter
- encoding_detector
- set_data_type
- clean_data_frame
- check_columns
- read_xml_peaks
- get_xml_polarity
1091class ReadCoreMSHDFMassSpectraCollection: 1092 """Read a collection of CoreMS HDF5 files and populate an LCMSCollection object. 1093 1094 Parameters 1095 ---------- 1096 folder_location : Path 1097 Folder containing .corems subdirectories with HDF5 files. 1098 manifest_file : Path, optional 1099 Manifest CSV with columns: sample_name, order, batch, center, time. 1100 One sample must have center='TRUE' for RT alignment. 1101 If None, checks if auto-generated manifest_auto.csv exists in the folder. If not, 1102 auto-generates from folder contents. Default: None. 1103 cores : int, optional 1104 Number of cores for multiprocessing. Default: 1. 1105 auto_manifest_batch_threshold_hours : float, optional 1106 Time gap (hours) for auto-generated batch separation. Default: 12.0. 1107 auto_manifest_center_name : str, optional 1108 Sample name for RT alignment center when auto-generating. 1109 Must match a discovered sample. If None, uses middle sample. Default: None. 1110 1111 Attributes 1112 ---------- 1113 folder_location : Path 1114 Folder containing CoreMS HDF5 files. 1115 manifest_filepath : Path 1116 Path to manifest file. 1117 manifest : dict 1118 Manifest data indexed by sample_name. 1119 """ 1120 1121 def __init__( 1122 self, 1123 folder_location: Path, 1124 manifest_file: Path = None, 1125 cores: int = 1, 1126 auto_manifest_batch_threshold_hours: float = 12.0, 1127 auto_manifest_center_name: str = None, 1128 ): 1129 # Check for folder location 1130 folder_location = Path(folder_location) 1131 if not folder_location.exists(): 1132 raise FileNotFoundError(f"Folder location {folder_location} not found.") 1133 1134 # Auto-generate manifest if not provided 1135 if manifest_file is None: 1136 # Check if manifest_auto.csv already exists 1137 auto_manifest_path = folder_location / "manifest_auto.csv" 1138 if auto_manifest_path.exists(): 1139 print( 1140 f"No manifest file provided. Using existing manifest_auto.csv from {folder_location}" 1141 ) 1142 manifest_file = auto_manifest_path 1143 else: 1144 print( 1145 f"No manifest file provided. Auto-generating manifest from {folder_location}" 1146 ) 1147 manifest_file = create_manifest_from_folder( 1148 folder_path=folder_location, 1149 output_path=auto_manifest_path, 1150 batch_time_threshold_hours=auto_manifest_batch_threshold_hours, 1151 center_name=auto_manifest_center_name, 1152 overwrite=True, 1153 ) 1154 else: 1155 manifest_file = Path(manifest_file) 1156 if not manifest_file.exists(): 1157 raise FileNotFoundError(f"Manifest file {manifest_file} not found.") 1158 1159 # Check if the manifest file is a CSV 1160 if manifest_file.suffix != ".csv": 1161 raise ValueError("Manifest file must be a CSV.") 1162 1163 self.folder_location = folder_location 1164 self._manifest_dict = None 1165 self._parse_manifest(manifest_file) 1166 self._validate_manifest() 1167 self._validate_parameters() 1168 self._validate_cores(cores) 1169 1170 def _validate_cores(self, cores): 1171 # Check if the cores parameter is an integer greater than 0 and less than the number of cores available 1172 if not isinstance(cores, int) or cores < 1: 1173 raise ValueError("Cores must be an integer greater than 0.") 1174 if cores > multiprocessing.cpu_count(): 1175 raise ValueError( 1176 f"Cores must be less than or equal to the number of cores available ({multiprocessing.cpu_count()})." 1177 ) 1178 self._cores = cores 1179 1180 def _parse_manifest(self, manifest_file): 1181 """Parse the manifest file and set the manifest dictionary.""" 1182 self.manifest_filepath = manifest_file 1183 manifest = pd.read_csv(manifest_file) 1184 # Check if the following columns exisit in the manifest file 1185 if not all( 1186 col in manifest.columns for col in ["sample_name", "order", "batch"] 1187 ): 1188 raise ValueError( 1189 "Manifest file must contain the following columns: 'sample_name', 'order', 'batch'." 1190 ) 1191 # Set index to the 'sample_name' column 1192 manifest.set_index("sample_name", inplace=True) 1193 self._manifest_dict = manifest.to_dict(orient="index") 1194 1195 def _validate_manifest(self): 1196 """Validate the manifest dictionary against the CoreMS folder location.""" 1197 # Check if the folder location contains HDF5 files for each sample 1198 for sample_name in self._manifest_dict.keys(): 1199 corems_dir = self.folder_location / f"{sample_name}.corems" 1200 if not corems_dir.exists(): 1201 raise FileNotFoundError(f"CoreMS folder for {sample_name} not found.") 1202 hdf5_file = corems_dir / f"{sample_name}.hdf5" 1203 if not hdf5_file.exists(): 1204 raise FileNotFoundError(f"HDF5 file for {sample_name} not found.") 1205 1206 # Check that at least one sample has center='TRUE' for retention time alignment 1207 center_values = [ 1208 sample_data.get("center") for sample_data in self._manifest_dict.values() 1209 ] 1210 if not any( 1211 center_val == "TRUE" or center_val == True for center_val in center_values 1212 ): 1213 raise ValueError( 1214 "Manifest must contain at least one sample with center='TRUE' for retention time alignment. " 1215 "None of the samples in the manifest have center='TRUE'." 1216 ) 1217 1218 def _validate_parameters(self): 1219 """Validate that the parameters used for all samples within a batch are the same.""" 1220 # Check if parameters files are saved as JSON or TOML 1221 if self.parameters_files[0].suffix == ".json": 1222 importer = json 1223 suffix = ".json" 1224 1225 elif self.parameters_files[0].suffix == ".toml": 1226 importer = tomlkit 1227 suffix = ".toml" 1228 1229 manfiest_df = self.manifest_dataframe 1230 1231 # Split up samples by batch 1232 batches = manfiest_df["batch"].unique() 1233 1234 for batch in batches: 1235 samples = manfiest_df[manfiest_df["batch"] == batch].index 1236 # check if self.parameters_files end with .json or .toml 1237 batch_param_files = [ 1238 self.folder_location / f"{sample_name}.corems/{sample_name}{suffix}" 1239 for sample_name in self._manifest_dict.keys() 1240 if sample_name in samples 1241 ] 1242 with open( 1243 batch_param_files[0], 1244 "r", 1245 encoding="utf8", 1246 ) as stream: 1247 first_parameters = importer.load(stream) 1248 for parameters_file in batch_param_files[1:]: 1249 with open( 1250 parameters_file, 1251 "r", 1252 encoding="utf8", 1253 ) as stream: 1254 parameters = importer.load(stream) 1255 if parameters != first_parameters: 1256 raise ValueError( 1257 f"Parameters files for samples in batch {batch} are not equal." 1258 ) 1259 1260 def get_lcms_obj( 1261 self, 1262 sample_name: str, 1263 load_raw=False, 1264 load_light=True, 1265 use_original_parser=True, 1266 raw_file_path=None, 1267 ) -> LCMSBase: 1268 """Return a LCMSBase object for a given sample name within the collection. 1269 1270 Parameters 1271 ---------- 1272 sample_name : str 1273 The sample name to retrieve the LCMS object for. 1274 load_raw : bool 1275 If True, load raw data from HDF5 files. Default is False. 1276 load_light : bool 1277 If True, only load the parameters, mass features, and scan info are initially loaded for each lcms object. Default is True. 1278 """ 1279 hdf5_file = self.folder_location / f"{sample_name}.corems/{sample_name}.hdf5" 1280 with ReadCoreMSHDFMassSpectra(hdf5_file) as parser: 1281 lcms_obj = parser.get_lcms_obj( 1282 load_raw=load_raw, 1283 load_light=load_light, 1284 use_original_parser=use_original_parser, 1285 raw_file_path=raw_file_path, 1286 ) 1287 if load_light: 1288 # mass_features_to_df always includes _eic_mz (object value or mz fallback) 1289 mf_df = lcms_obj.mass_features_to_df() 1290 lcms_obj.mass_features = {} 1291 lcms_obj.light_mf_df = mf_df 1292 return lcms_obj 1293 1294 def get_lcms_collection( 1295 self, load_raw=False, load_light=True, use_original_parser=True 1296 ) -> LCMSCollection: 1297 """Return a LCMSCollection object 1298 1299 Parameters 1300 ---------- 1301 load_raw : bool 1302 If True, load raw data from HDF5 files. Default is False. 1303 load_light : bool 1304 If True, only load the parameters, mass features, and scan info are initially loaded for each lcms object. 1305 After concatenating the mass_features, remove the mass_features attribute from the individual LCMS objects for memory efficiency. Default is True. 1306 Default is True. 1307 """ 1308 # Instantiate the LCMSCollection object 1309 lcms_coll = LCMSCollection( 1310 collection_location=self.folder_location, 1311 manifest=self.manifest, 1312 collection_parser=self, 1313 ) 1314 1315 # Set the number of cores on the LCMSCollection object from the ReadCoreMSHDFMassSpectraCollection object 1316 lcms_coll.parameters.lcms_collection.cores = self._cores 1317 1318 # Add LCMS objects to the collection 1319 samples = self._manifest_dict.keys() 1320 1321 # Initialize the LCMS object dictionary 1322 if self._cores > 1: 1323 if self._cores > len(samples): 1324 ncores = len(samples) 1325 else: 1326 ncores = self._cores 1327 # Create a pool of workers (one for each core or sample, whichever is smaller) 1328 args = [ 1329 (sample, load_raw, load_light, use_original_parser) 1330 for sample in samples 1331 ] 1332 with spawn_pool(ncores) as pool: 1333 lcms_objs = pool.starmap(self.get_lcms_obj, args) 1334 for sample_name, lcms_obj in zip(samples, lcms_objs): 1335 lcms_coll._lcms[sample_name] = lcms_obj 1336 1337 elif self._cores == 1: 1338 # Load the LCMS objects sequentially 1339 for sample_name in samples: 1340 lcms_coll._lcms[sample_name] = self.get_lcms_obj( 1341 sample_name, 1342 load_raw=load_raw, 1343 load_light=load_light, 1344 use_original_parser=use_original_parser, 1345 ) 1346 1347 else: 1348 raise ValueError( 1349 "Number of cores must be greater than 0 and set on the ReadCoreMSHDFMassSpectraCollection object." 1350 ) 1351 1352 # Check that all LCMS objects have the same polarity 1353 if len(set([x.polarity for k, x in lcms_coll._lcms.items()])) != 1: 1354 raise ValueError("All samples must have the same polarity.") 1355 1356 # Set ids on the LCMS objects in the manifest 1357 i = 0 1358 for sample in lcms_coll.samples: 1359 lcms_coll._manifest_dict[sample]["collection_id"] = i 1360 i += 1 1361 1362 # Reorder the LCMS objects 1363 lcms_coll._reorder_lcms_objects() 1364 1365 # Collect the mass features from the LCMS objects and combine them into a single dataframe for the collection 1366 lcms_coll._combine_mass_features() 1367 1368 # If load_light, remove the mass_feature attribute from the individual LCMS objects 1369 if load_light: 1370 for sample_name in lcms_coll.samples: 1371 lcms_coll._lcms[sample_name].mass_features = {} 1372 # Remove the light_mf_df attribute from the individual LCMS objects 1373 del lcms_coll._lcms[sample_name].light_mf_df 1374 1375 return lcms_coll 1376 1377 @property 1378 def manifest(self): 1379 return self._manifest_dict 1380 1381 @property 1382 def manifest_dataframe(self): 1383 return pd.DataFrame(self._manifest_dict).T 1384 1385 @property 1386 def hdf5_files(self): 1387 return [ 1388 self.folder_location / f"{sample_name}.corems/{sample_name}.hdf5" 1389 for sample_name in self._manifest_dict.keys() 1390 ] 1391 1392 @property 1393 def parameters_files(self): 1394 # Check if parameters files are saved as JSON or TOML 1395 json_files = [ 1396 self.folder_location / f"{sample_name}.corems/{sample_name}.json" 1397 for sample_name in self._manifest_dict.keys() 1398 ] 1399 toml_files = [ 1400 self.folder_location / f"{sample_name}.corems/{sample_name}.toml" 1401 for sample_name in self._manifest_dict.keys() 1402 ] 1403 if all([x.exists() for x in json_files]): 1404 return json_files 1405 elif all([x.exists() for x in toml_files]): 1406 return toml_files 1407 else: 1408 raise ValueError("Parameters files are not saved for all samples.")
Read a collection of CoreMS HDF5 files and populate an LCMSCollection object.
Parameters
- folder_location (Path): Folder containing .corems subdirectories with HDF5 files.
- manifest_file (Path, optional): Manifest CSV with columns: sample_name, order, batch, center, time. One sample must have center='TRUE' for RT alignment. If None, checks if auto-generated manifest_auto.csv exists in the folder. If not, auto-generates from folder contents. Default: None.
- cores (int, optional): Number of cores for multiprocessing. Default: 1.
- auto_manifest_batch_threshold_hours (float, optional): Time gap (hours) for auto-generated batch separation. Default: 12.0.
- auto_manifest_center_name (str, optional): Sample name for RT alignment center when auto-generating. Must match a discovered sample. If None, uses middle sample. Default: None.
Attributes
- folder_location (Path): Folder containing CoreMS HDF5 files.
- manifest_filepath (Path): Path to manifest file.
- manifest (dict): Manifest data indexed by sample_name.
1121 def __init__( 1122 self, 1123 folder_location: Path, 1124 manifest_file: Path = None, 1125 cores: int = 1, 1126 auto_manifest_batch_threshold_hours: float = 12.0, 1127 auto_manifest_center_name: str = None, 1128 ): 1129 # Check for folder location 1130 folder_location = Path(folder_location) 1131 if not folder_location.exists(): 1132 raise FileNotFoundError(f"Folder location {folder_location} not found.") 1133 1134 # Auto-generate manifest if not provided 1135 if manifest_file is None: 1136 # Check if manifest_auto.csv already exists 1137 auto_manifest_path = folder_location / "manifest_auto.csv" 1138 if auto_manifest_path.exists(): 1139 print( 1140 f"No manifest file provided. Using existing manifest_auto.csv from {folder_location}" 1141 ) 1142 manifest_file = auto_manifest_path 1143 else: 1144 print( 1145 f"No manifest file provided. Auto-generating manifest from {folder_location}" 1146 ) 1147 manifest_file = create_manifest_from_folder( 1148 folder_path=folder_location, 1149 output_path=auto_manifest_path, 1150 batch_time_threshold_hours=auto_manifest_batch_threshold_hours, 1151 center_name=auto_manifest_center_name, 1152 overwrite=True, 1153 ) 1154 else: 1155 manifest_file = Path(manifest_file) 1156 if not manifest_file.exists(): 1157 raise FileNotFoundError(f"Manifest file {manifest_file} not found.") 1158 1159 # Check if the manifest file is a CSV 1160 if manifest_file.suffix != ".csv": 1161 raise ValueError("Manifest file must be a CSV.") 1162 1163 self.folder_location = folder_location 1164 self._manifest_dict = None 1165 self._parse_manifest(manifest_file) 1166 self._validate_manifest() 1167 self._validate_parameters() 1168 self._validate_cores(cores)
1260 def get_lcms_obj( 1261 self, 1262 sample_name: str, 1263 load_raw=False, 1264 load_light=True, 1265 use_original_parser=True, 1266 raw_file_path=None, 1267 ) -> LCMSBase: 1268 """Return a LCMSBase object for a given sample name within the collection. 1269 1270 Parameters 1271 ---------- 1272 sample_name : str 1273 The sample name to retrieve the LCMS object for. 1274 load_raw : bool 1275 If True, load raw data from HDF5 files. Default is False. 1276 load_light : bool 1277 If True, only load the parameters, mass features, and scan info are initially loaded for each lcms object. Default is True. 1278 """ 1279 hdf5_file = self.folder_location / f"{sample_name}.corems/{sample_name}.hdf5" 1280 with ReadCoreMSHDFMassSpectra(hdf5_file) as parser: 1281 lcms_obj = parser.get_lcms_obj( 1282 load_raw=load_raw, 1283 load_light=load_light, 1284 use_original_parser=use_original_parser, 1285 raw_file_path=raw_file_path, 1286 ) 1287 if load_light: 1288 # mass_features_to_df always includes _eic_mz (object value or mz fallback) 1289 mf_df = lcms_obj.mass_features_to_df() 1290 lcms_obj.mass_features = {} 1291 lcms_obj.light_mf_df = mf_df 1292 return lcms_obj
Return a LCMSBase object for a given sample name within the collection.
Parameters
- sample_name (str): The sample name to retrieve the LCMS object for.
- load_raw (bool): If True, load raw data from HDF5 files. Default is False.
- load_light (bool): If True, only load the parameters, mass features, and scan info are initially loaded for each lcms object. Default is True.
1294 def get_lcms_collection( 1295 self, load_raw=False, load_light=True, use_original_parser=True 1296 ) -> LCMSCollection: 1297 """Return a LCMSCollection object 1298 1299 Parameters 1300 ---------- 1301 load_raw : bool 1302 If True, load raw data from HDF5 files. Default is False. 1303 load_light : bool 1304 If True, only load the parameters, mass features, and scan info are initially loaded for each lcms object. 1305 After concatenating the mass_features, remove the mass_features attribute from the individual LCMS objects for memory efficiency. Default is True. 1306 Default is True. 1307 """ 1308 # Instantiate the LCMSCollection object 1309 lcms_coll = LCMSCollection( 1310 collection_location=self.folder_location, 1311 manifest=self.manifest, 1312 collection_parser=self, 1313 ) 1314 1315 # Set the number of cores on the LCMSCollection object from the ReadCoreMSHDFMassSpectraCollection object 1316 lcms_coll.parameters.lcms_collection.cores = self._cores 1317 1318 # Add LCMS objects to the collection 1319 samples = self._manifest_dict.keys() 1320 1321 # Initialize the LCMS object dictionary 1322 if self._cores > 1: 1323 if self._cores > len(samples): 1324 ncores = len(samples) 1325 else: 1326 ncores = self._cores 1327 # Create a pool of workers (one for each core or sample, whichever is smaller) 1328 args = [ 1329 (sample, load_raw, load_light, use_original_parser) 1330 for sample in samples 1331 ] 1332 with spawn_pool(ncores) as pool: 1333 lcms_objs = pool.starmap(self.get_lcms_obj, args) 1334 for sample_name, lcms_obj in zip(samples, lcms_objs): 1335 lcms_coll._lcms[sample_name] = lcms_obj 1336 1337 elif self._cores == 1: 1338 # Load the LCMS objects sequentially 1339 for sample_name in samples: 1340 lcms_coll._lcms[sample_name] = self.get_lcms_obj( 1341 sample_name, 1342 load_raw=load_raw, 1343 load_light=load_light, 1344 use_original_parser=use_original_parser, 1345 ) 1346 1347 else: 1348 raise ValueError( 1349 "Number of cores must be greater than 0 and set on the ReadCoreMSHDFMassSpectraCollection object." 1350 ) 1351 1352 # Check that all LCMS objects have the same polarity 1353 if len(set([x.polarity for k, x in lcms_coll._lcms.items()])) != 1: 1354 raise ValueError("All samples must have the same polarity.") 1355 1356 # Set ids on the LCMS objects in the manifest 1357 i = 0 1358 for sample in lcms_coll.samples: 1359 lcms_coll._manifest_dict[sample]["collection_id"] = i 1360 i += 1 1361 1362 # Reorder the LCMS objects 1363 lcms_coll._reorder_lcms_objects() 1364 1365 # Collect the mass features from the LCMS objects and combine them into a single dataframe for the collection 1366 lcms_coll._combine_mass_features() 1367 1368 # If load_light, remove the mass_feature attribute from the individual LCMS objects 1369 if load_light: 1370 for sample_name in lcms_coll.samples: 1371 lcms_coll._lcms[sample_name].mass_features = {} 1372 # Remove the light_mf_df attribute from the individual LCMS objects 1373 del lcms_coll._lcms[sample_name].light_mf_df 1374 1375 return lcms_coll
Return a LCMSCollection object
Parameters
- load_raw (bool): If True, load raw data from HDF5 files. Default is False.
- load_light (bool): If True, only load the parameters, mass features, and scan info are initially loaded for each lcms object. After concatenating the mass_features, remove the mass_features attribute from the individual LCMS objects for memory efficiency. Default is True. Default is True.
1392 @property 1393 def parameters_files(self): 1394 # Check if parameters files are saved as JSON or TOML 1395 json_files = [ 1396 self.folder_location / f"{sample_name}.corems/{sample_name}.json" 1397 for sample_name in self._manifest_dict.keys() 1398 ] 1399 toml_files = [ 1400 self.folder_location / f"{sample_name}.corems/{sample_name}.toml" 1401 for sample_name in self._manifest_dict.keys() 1402 ] 1403 if all([x.exists() for x in json_files]): 1404 return json_files 1405 elif all([x.exists() for x in toml_files]): 1406 return toml_files 1407 else: 1408 raise ValueError("Parameters files are not saved for all samples.")
1411class ReadSavedLCMSCollection(ReadCoreMSHDFMassSpectraCollection): 1412 """ 1413 Subclass to read and re-instantiate a LCMSCollection from a saved HDF5 file. 1414 1415 1416 Parameters 1417 ---------- 1418 collection_hdf5_path : str or Path 1419 Path to the saved LCMSCollection HDF5 file. 1420 cores : int, optional 1421 Number of cores for processing. Default is 1. 1422 """ 1423 1424 def __init__(self, collection_hdf5_path: str, cores: int = 1): 1425 # Convert to Path objects 1426 self.collection_hdf5_path = Path(collection_hdf5_path) 1427 1428 # Validate the collection file exists 1429 if not self.collection_hdf5_path.exists(): 1430 raise FileNotFoundError( 1431 f"Collection HDF5 file {self.collection_hdf5_path} not found." 1432 ) 1433 1434 # Validate cores 1435 self._validate_cores(cores) 1436 1437 # Load metadata from saved collection 1438 self._load_collection_metadata() 1439 1440 if not self.folder_location.exists(): 1441 raise FileNotFoundError( 1442 f"Folder location {self.folder_location} not found." 1443 ) 1444 1445 # Load the mass spectra data 1446 self._validate_manifest() 1447 1448 # Set the parameters file location 1449 self.parameters_location = self._get_parameters_location() 1450 1451 def _get_parameters_location(self): 1452 """Find the parameters file (JSON or TOML) associated with the collection HDF5 file.""" 1453 # Check for TOML file first (preferred) 1454 toml_path = self.collection_hdf5_path.with_suffix(".toml") 1455 if toml_path.exists(): 1456 return toml_path 1457 1458 # Check for JSON file 1459 json_path = self.collection_hdf5_path.with_suffix(".json") 1460 if json_path.exists(): 1461 return json_path 1462 1463 # No parameters file found 1464 return None 1465 1466 def _load_collection_metadata(self): 1467 """Load metadata and manifest from the saved collection HDF5 file.""" 1468 with h5py.File(self.collection_hdf5_path, "r") as f: 1469 self.folder_location = Path(f.attrs.get("lcms_objects_folder", "")) 1470 self.missing_mass_features_searched = f.attrs.get( 1471 "missing_mass_features_searched", False 1472 ) 1473 1474 # Call the _load_manifest function to process the manifest 1475 self._manifest_dict = self._load_manifest(f) 1476 1477 def _load_manifest(self, hdf_handle): 1478 """Load and clean the manifest from the HDF5 file.""" 1479 manifest_json = hdf_handle.attrs.get("manifest", "{}") 1480 if isinstance(manifest_json, bytes): 1481 manifest_json = manifest_json.decode("utf-8") 1482 loaded_manifest = json.loads(manifest_json) 1483 1484 # Convert integer values for 'use_rt_alignment' back to booleans 1485 def convert_back_to_bool(data): 1486 if isinstance(data, dict): 1487 # Process each key-value pair recursively 1488 return { 1489 k: ( 1490 bool(v) 1491 if k == "use_rt_alignment" and isinstance(v, int) 1492 else convert_back_to_bool(v) 1493 ) 1494 for k, v in data.items() 1495 } 1496 elif isinstance(data, list): 1497 # Recursively process lists 1498 return [convert_back_to_bool(item) for item in data] 1499 else: 1500 # Return non-dict/list types unchanged 1501 return data 1502 1503 # Clean the loaded manifest 1504 return convert_back_to_bool(loaded_manifest) 1505 1506 def _load_rt_alignments(self, lcms_collection): 1507 """Load retention time alignments from the saved collection HDF5 file.""" 1508 # First Set the rt_aligned flag from the collection-level attribute saved directly 1509 with h5py.File(self.collection_hdf5_path, "r") as f: 1510 lcms_collection.rt_aligned = f.attrs.get("rt_aligned", False) 1511 lcms_collection.rt_alignment_attempted = f.attrs.get( 1512 "rt_alignment_attempted", False 1513 ) 1514 1515 if lcms_collection.rt_aligned: 1516 with h5py.File(self.collection_hdf5_path, "r") as f: 1517 if "rt_alignments" in f: 1518 # Iterate over the group `rt_alignments` containing datasets and add to the corresponding lcms object 1519 rt_alignments_group = f["rt_alignments"] 1520 for sample_idx, lcms_obj in zip( 1521 rt_alignments_group.keys(), lcms_collection 1522 ): 1523 alignment_data = rt_alignments_group[sample_idx][:] 1524 scan_df = lcms_obj.scan_df 1525 scan_df["scan_time_aligned"] = alignment_data 1526 lcms_obj.scan_df = scan_df 1527 elif lcms_collection.rt_alignment_attempted: 1528 # This means it was attempted and not used, so we populate the "scan_time_aligned" 1529 for lcms_obj in lcms_collection: 1530 scan_df = lcms_obj.scan_df 1531 scan_df["scan_time_aligned"] = scan_df["scan_time"] 1532 lcms_obj.scan_df = scan_df 1533 1534 def _load_cluster_assignments(self, lcms_collection): 1535 """Load cluster assignments from the saved collection HDF5 file.""" 1536 with h5py.File(self.collection_hdf5_path, "r") as f: 1537 if "cluster_assignments" in f: 1538 # Access the group containing cluster assignments 1539 cluster_grp = f["cluster_assignments"] 1540 1541 # Reload index and cluster data 1542 index = cluster_grp["index"][:] # Extract index 1543 index = [ 1544 idx.decode("utf-8") for idx in index 1545 ] # Convert byte strings back to regular strings 1546 cluster_data = cluster_grp["cluster"][:] # Extract cluster column 1547 1548 # Reassemble the DataFrame 1549 cluster_df = pd.DataFrame({"cluster": cluster_data}, index=index) 1550 1551 # Assign cluster data back to lcms_collection.mass_features_dataframe 1552 lcms_collection.mass_features_dataframe = ( 1553 lcms_collection.mass_features_dataframe.join(cluster_df, how="left") 1554 ) 1555 1556 # Drop rows with NaN cluster values 1557 lcms_collection.mass_features_dataframe.dropna( 1558 subset=["cluster"], inplace=True 1559 ) 1560 1561 def _load_feature_group_labels(self, lcms_collection): 1562 """Restore ``feature_group_dataframe`` and merge onto feature tables.""" 1563 from corems.mass_spectra.calc.feature_grouping import ( 1564 GROUP_COLUMNS, 1565 empty_group_labels, 1566 merge_feature_group_labels_into_frames, 1567 ) 1568 1569 with h5py.File(self.collection_hdf5_path, "r") as f: 1570 if "feature_group_labels" not in f: 1571 return 1572 grp = f["feature_group_labels"] 1573 if "cluster" not in grp: 1574 raise ValueError( 1575 "feature_group_labels HDF5 group is missing 'cluster' dataset" 1576 ) 1577 clusters = np.asarray(grp["cluster"][:], dtype=np.int64) 1578 labels = empty_group_labels(clusters) 1579 for col in GROUP_COLUMNS: 1580 if col not in grp: 1581 continue 1582 raw = grp[col][:] 1583 if col in ("feature_group_id", "mono_cluster_id"): 1584 vals = [] 1585 for v in raw: 1586 if v is None or (isinstance(v, float) and np.isnan(v)): 1587 vals.append(pd.NA) 1588 else: 1589 vals.append(int(v)) 1590 labels[col] = pd.Series(vals, index=labels.index, dtype="Int64") 1591 else: 1592 decoded = [] 1593 for v in raw: 1594 if isinstance(v, (bytes, bytearray, np.bytes_)): 1595 s = v.decode("utf-8") 1596 else: 1597 s = "" if v is None else str(v) 1598 decoded.append(None if s == "" else s) 1599 labels[col] = decoded 1600 1601 lcms_collection.feature_group_dataframe = labels 1602 mf, induced = merge_feature_group_labels_into_frames( 1603 labels, 1604 lcms_collection.mass_features_dataframe, 1605 getattr(lcms_collection, "induced_mass_features_dataframe", None), 1606 ) 1607 lcms_collection.mass_features_dataframe = mf 1608 if ( 1609 getattr(lcms_collection, "induced_mass_features_dataframe", None) 1610 is not None 1611 ): 1612 lcms_collection.induced_mass_features_dataframe = induced 1613 1614 def get_lcms_collection( 1615 self, 1616 load_raw=False, 1617 load_light=False, 1618 load_representatives=False, 1619 load_eics=False, 1620 load_ms1=False, 1621 load_ms2=False, 1622 ): 1623 """Get the LCMS collection from the saved HDF5 file. 1624 1625 Parameters 1626 ---------- 1627 load_raw : bool, optional 1628 If True, load raw data. Default is False. 1629 load_light : bool, optional 1630 If True, load light data (minimal). Default is False. 1631 load_representatives : bool, optional 1632 If True, load representative mass features from clusters. Default is False. 1633 load_eics : bool, optional 1634 If True, load EIC data for clustered mass features. Default is False. 1635 load_ms1 : bool, optional 1636 If True, load MS1 spectra for loaded mass features. Default is False. 1637 load_ms2 : bool, optional 1638 If True, load MS2 spectra for loaded mass features. Default is False. 1639 1640 Returns 1641 ------- 1642 LCMSCollection 1643 The loaded LCMS collection object. 1644 """ 1645 # First load the LCMSCollection object exactly as in the parent class 1646 lcms_collection = super().get_lcms_collection( 1647 load_raw=load_raw, load_light=load_light 1648 ) 1649 1650 # Set the missing_mass_features_searched flag from saved metadata 1651 lcms_collection.missing_mass_features_searched = ( 1652 self.missing_mass_features_searched 1653 ) 1654 1655 # Load parameters if a parameters file exists 1656 if self.parameters_location: 1657 self._load_parameters(lcms_collection) 1658 1659 # Add retention time alignments if they exist 1660 self._load_rt_alignments(lcms_collection) 1661 1662 # Add cluster assignments if they exist 1663 self._load_cluster_assignments(lcms_collection) 1664 1665 # Load induced mass features if they exist 1666 self._load_induced_mass_features(lcms_collection) 1667 1668 # Load EICs for induced mass features from collection HDF5 1669 if lcms_collection.missing_mass_features_searched and load_eics: 1670 self._load_induced_eics_from_collection(lcms_collection) 1671 1672 # Combine induced mass features into the collection-level dataframe if any were loaded 1673 if lcms_collection.missing_mass_features_searched: 1674 lcms_collection._combine_mass_features(induced_features=True) 1675 1676 # Restore feature-group labels after clusters (+ induced) are in place 1677 self._load_feature_group_labels(lcms_collection) 1678 1679 # Load representative mass features if requested 1680 if load_representatives: 1681 self._load_representative_mass_features(lcms_collection) 1682 1683 # Load MS1 and/or MS2 spectra for loaded mass features if requested 1684 if load_ms1 or load_ms2: 1685 # Reuse the existing ReloadFeaturesOperation from the pipeline system 1686 from corems.mass_spectra.calc.lc_calc_operations import ( 1687 ReloadFeaturesOperation, 1688 ) 1689 1690 operations = [ 1691 ReloadFeaturesOperation( 1692 "reload_spectra", add_ms1=load_ms1, add_ms2=load_ms2 1693 ) 1694 ] 1695 lcms_collection.process_samples_pipeline( 1696 operations, keep_raw_data=False, show_progress=False 1697 ) 1698 1699 # Load EICs for clustered features if requested 1700 if load_eics: 1701 # Reuse the existing LoadEICsOperation from the pipeline system 1702 from corems.mass_spectra.calc.lc_calc_operations import LoadEICsOperation 1703 1704 operations = [LoadEICsOperation("load_eics")] 1705 lcms_collection.process_samples_pipeline( 1706 operations, keep_raw_data=False, show_progress=False 1707 ) 1708 1709 # Associate EICs with mass features (same as in process_consensus_features) 1710 for sample_id in range(len(lcms_collection.samples)): 1711 sample = lcms_collection[sample_id] 1712 if sample.eics: # Only if EICs were loaded 1713 # Associate EICs with regular mass features 1714 sample.associate_eics_with_mass_features(induced=False) 1715 # Associate EICs with induced mass features 1716 sample.associate_eics_with_mass_features(induced=True) 1717 1718 return lcms_collection 1719 1720 def _load_parameters(self, lcms_collection): 1721 """Load collection-level parameters from the saved parameters file.""" 1722 from corems.encapsulation.input.parameter_from_json import ( 1723 load_and_set_json_parameters_lcms_collection, 1724 load_and_set_toml_parameters_lcms_collection, 1725 ) 1726 1727 if self.parameters_location.suffix == ".json": 1728 load_and_set_json_parameters_lcms_collection( 1729 lcms_collection, self.parameters_location 1730 ) 1731 elif self.parameters_location.suffix == ".toml": 1732 load_and_set_toml_parameters_lcms_collection( 1733 lcms_collection, self.parameters_location 1734 ) 1735 else: 1736 warnings.warn( 1737 f"Unknown parameter file format: {self.parameters_location.suffix}. Skipping parameter loading." 1738 ) 1739 1740 def _load_induced_mass_features(self, lcms_collection): 1741 """Load induced mass features from the saved collection HDF5 file. 1742 1743 Induced mass features are gap-filled features that exist at the collection level. 1744 This method loads them from the collection HDF5 file with all their attributes 1745 and datasets, and distributes them to individual LCMS objects. 1746 1747 Parameters 1748 ---------- 1749 lcms_collection : LCMSCollection 1750 The LCMS collection object to populate with induced mass features. 1751 """ 1752 with h5py.File(self.collection_hdf5_path, "r") as f: 1753 if "induced_mass_features" not in f: 1754 return 1755 1756 # Access the top-level induced mass features group 1757 imf_group = f["induced_mass_features"] 1758 1759 # Iterate through each sample's induced mass features 1760 for sample_idx in imf_group.keys(): 1761 lcms_obj = lcms_collection[int(sample_idx)] 1762 sample_group = imf_group[sample_idx] 1763 1764 # Load each mass feature for this sample 1765 for mf_id_str in sample_group.keys(): 1766 mf_group = sample_group[mf_id_str] 1767 1768 # Note: Induced mass feature IDs are strings like 'c2923_0_i', not integers 1769 # Keep them as strings since that's how they're stored 1770 mf_id = mf_id_str 1771 1772 # Instantiate the LCMSMassFeature object with required attributes 1773 mass_feature = LCMSMassFeature( 1774 lcms_obj, 1775 mz=mf_group.attrs["_mz_exp"], 1776 retention_time=mf_group.attrs["_retention_time"], 1777 intensity=mf_group.attrs["_intensity"], 1778 apex_scan=mf_group.attrs["_apex_scan"], 1779 persistence=mf_group.attrs.get("_persistence", 0), 1780 id=mf_id, 1781 ) 1782 1783 # Populate additional attributes from HDF5 attributes 1784 for key in mf_group.attrs.keys() - { 1785 "_mz_exp", 1786 "_mz_cal", 1787 "_retention_time", 1788 "_intensity", 1789 "_apex_scan", 1790 "_persistence", 1791 }: 1792 setattr(mass_feature, key, mf_group.attrs[key]) 1793 1794 # Populate attributes from HDF5 datasets (arrays) 1795 for key in mf_group.keys(): 1796 setattr(mass_feature, key, mf_group[key][:]) 1797 # Convert _noise_score from array to tuple 1798 if key == "_noise_score": 1799 mass_feature._noise_score = tuple(mass_feature._noise_score) 1800 1801 # Add to the LCMS object's induced_mass_features dictionary 1802 lcms_obj.induced_mass_features[mf_id] = mass_feature 1803 1804 def _load_induced_eics_from_collection(self, lcms_collection): 1805 """Load EICs for induced mass features from the collection HDF5 file. 1806 1807 Induced mass features are gap-filled features. Their EICs are saved at the 1808 collection level and need to be loaded and associated with the induced mass features. 1809 1810 Parameters 1811 ---------- 1812 lcms_collection : LCMSCollection 1813 The LCMS collection object with induced mass features to associate EICs with. 1814 """ 1815 with h5py.File(self.collection_hdf5_path, "r") as f: 1816 if "induced_eics" not in f: 1817 return 1818 1819 # Access the top-level induced EICs group 1820 induced_eics_group = f["induced_eics"] 1821 1822 # Iterate through each sample's induced EICs 1823 for sample_idx in induced_eics_group.keys(): 1824 lcms_obj = lcms_collection[int(sample_idx)] 1825 sample_group = induced_eics_group[sample_idx] 1826 1827 # Use the static helper to load EICs 1828 loaded_eics = ReadCoreMSHDFMassSpectra._load_eics_from_hdf5_group( 1829 sample_group, lcms_obj 1830 ) 1831 1832 # Ensure eics dictionary exists (should already be initialized in __init__) 1833 if not hasattr(lcms_obj, "eics") or lcms_obj.eics is None: 1834 lcms_obj.eics = {} 1835 1836 # Add to lcms_obj.eics dictionary 1837 for eic_mz, eic in loaded_eics.items(): 1838 lcms_obj.eics[eic_mz] = eic 1839 1840 # Associate EICs with induced mass features after all samples processed 1841 # This is done outside the loop to handle all samples at once 1842 for lcms_obj in lcms_collection: 1843 if len(lcms_obj.induced_mass_features) > 0: 1844 lcms_obj.associate_eics_with_mass_features(induced=True) 1845 1846 def _load_representative_mass_features(self, lcms_collection): 1847 """Load representative mass features for all clusters from HDF5 files. 1848 1849 This method uses the same logic as process_consensus_features() when loading 1850 representatives, calling get_sample_mf_map_for_representatives() (DRY helper) 1851 to determine which features to load. 1852 1853 Parameters 1854 ---------- 1855 lcms_collection : LCMSCollection 1856 The LCMS collection object to populate with representative mass features. 1857 """ 1858 # Get cluster assignments from the mass_features_dataframe 1859 if "cluster" not in lcms_collection.mass_features_dataframe.columns: 1860 return 1861 1862 # Use DRY helper method to build sample_mf_map with cluster IDs 1863 sample_mf_map = lcms_collection.get_sample_mf_map_for_representatives( 1864 include_cluster_id=True 1865 ) 1866 1867 # Load mass features for each sample 1868 for sample_id, mf_list in sample_mf_map.items(): 1869 lcms_obj = lcms_collection[sample_id] 1870 1871 # Load each mass feature 1872 for mf_id, cluster_id in mf_list: 1873 self._load_single_mass_feature(lcms_obj, mf_id, cluster_id) 1874 1875 def _load_single_mass_feature(self, lcms_obj, feature_id, cluster_index=None): 1876 """Load a single mass feature from an LCMS object's HDF5 file. 1877 1878 Parameters 1879 ---------- 1880 lcms_obj : LCMSBase 1881 The LCMS object to add the mass feature to. 1882 feature_id : int 1883 The ID of the mass feature to load. 1884 cluster_index : int, optional 1885 The cluster index to assign to the loaded mass feature. 1886 """ 1887 hdf5_path = lcms_obj.file_location.with_suffix(".hdf5") 1888 1889 if not hdf5_path.exists(): 1890 return 1891 1892 with h5py.File(hdf5_path, "r") as f: 1893 if "mass_features" not in f: 1894 return 1895 1896 mf_group = f["mass_features"] 1897 feature_id_str = str(feature_id) 1898 1899 if feature_id_str not in mf_group: 1900 return 1901 1902 mf_data = mf_group[feature_id_str] 1903 1904 # Create LCMSMassFeature object 1905 mass_feature = LCMSMassFeature( 1906 lcms_obj, 1907 mz=mf_data.attrs["_mz_exp"], 1908 retention_time=mf_data.attrs["_retention_time"], 1909 intensity=mf_data.attrs["_intensity"], 1910 apex_scan=mf_data.attrs["_apex_scan"], 1911 persistence=mf_data.attrs.get("_persistence", 0), 1912 id=feature_id, 1913 ) 1914 1915 # Set cluster_index if provided 1916 if cluster_index is not None: 1917 mass_feature.cluster_index = cluster_index 1918 1919 # Populate additional attributes from HDF5 attributes 1920 for key in mf_data.attrs.keys() - { 1921 "_mz_exp", 1922 "_mz_cal", 1923 "_retention_time", 1924 "_intensity", 1925 "_apex_scan", 1926 "_persistence", 1927 }: 1928 setattr(mass_feature, key, mf_data.attrs[key]) 1929 1930 # Populate attributes from HDF5 datasets (arrays) 1931 for key in mf_data.keys(): 1932 setattr(mass_feature, key, mf_data[key][:]) 1933 # Convert _noise_score from array to tuple 1934 if key == "_noise_score": 1935 mass_feature._noise_score = tuple(mass_feature._noise_score) 1936 1937 # Add to the LCMS object's mass_features dictionary 1938 lcms_obj.mass_features[feature_id] = mass_feature
Subclass to read and re-instantiate a LCMSCollection from a saved HDF5 file.
Parameters
- collection_hdf5_path (str or Path): Path to the saved LCMSCollection HDF5 file.
- cores (int, optional): Number of cores for processing. Default is 1.
1424 def __init__(self, collection_hdf5_path: str, cores: int = 1): 1425 # Convert to Path objects 1426 self.collection_hdf5_path = Path(collection_hdf5_path) 1427 1428 # Validate the collection file exists 1429 if not self.collection_hdf5_path.exists(): 1430 raise FileNotFoundError( 1431 f"Collection HDF5 file {self.collection_hdf5_path} not found." 1432 ) 1433 1434 # Validate cores 1435 self._validate_cores(cores) 1436 1437 # Load metadata from saved collection 1438 self._load_collection_metadata() 1439 1440 if not self.folder_location.exists(): 1441 raise FileNotFoundError( 1442 f"Folder location {self.folder_location} not found." 1443 ) 1444 1445 # Load the mass spectra data 1446 self._validate_manifest() 1447 1448 # Set the parameters file location 1449 self.parameters_location = self._get_parameters_location()
1614 def get_lcms_collection( 1615 self, 1616 load_raw=False, 1617 load_light=False, 1618 load_representatives=False, 1619 load_eics=False, 1620 load_ms1=False, 1621 load_ms2=False, 1622 ): 1623 """Get the LCMS collection from the saved HDF5 file. 1624 1625 Parameters 1626 ---------- 1627 load_raw : bool, optional 1628 If True, load raw data. Default is False. 1629 load_light : bool, optional 1630 If True, load light data (minimal). Default is False. 1631 load_representatives : bool, optional 1632 If True, load representative mass features from clusters. Default is False. 1633 load_eics : bool, optional 1634 If True, load EIC data for clustered mass features. Default is False. 1635 load_ms1 : bool, optional 1636 If True, load MS1 spectra for loaded mass features. Default is False. 1637 load_ms2 : bool, optional 1638 If True, load MS2 spectra for loaded mass features. Default is False. 1639 1640 Returns 1641 ------- 1642 LCMSCollection 1643 The loaded LCMS collection object. 1644 """ 1645 # First load the LCMSCollection object exactly as in the parent class 1646 lcms_collection = super().get_lcms_collection( 1647 load_raw=load_raw, load_light=load_light 1648 ) 1649 1650 # Set the missing_mass_features_searched flag from saved metadata 1651 lcms_collection.missing_mass_features_searched = ( 1652 self.missing_mass_features_searched 1653 ) 1654 1655 # Load parameters if a parameters file exists 1656 if self.parameters_location: 1657 self._load_parameters(lcms_collection) 1658 1659 # Add retention time alignments if they exist 1660 self._load_rt_alignments(lcms_collection) 1661 1662 # Add cluster assignments if they exist 1663 self._load_cluster_assignments(lcms_collection) 1664 1665 # Load induced mass features if they exist 1666 self._load_induced_mass_features(lcms_collection) 1667 1668 # Load EICs for induced mass features from collection HDF5 1669 if lcms_collection.missing_mass_features_searched and load_eics: 1670 self._load_induced_eics_from_collection(lcms_collection) 1671 1672 # Combine induced mass features into the collection-level dataframe if any were loaded 1673 if lcms_collection.missing_mass_features_searched: 1674 lcms_collection._combine_mass_features(induced_features=True) 1675 1676 # Restore feature-group labels after clusters (+ induced) are in place 1677 self._load_feature_group_labels(lcms_collection) 1678 1679 # Load representative mass features if requested 1680 if load_representatives: 1681 self._load_representative_mass_features(lcms_collection) 1682 1683 # Load MS1 and/or MS2 spectra for loaded mass features if requested 1684 if load_ms1 or load_ms2: 1685 # Reuse the existing ReloadFeaturesOperation from the pipeline system 1686 from corems.mass_spectra.calc.lc_calc_operations import ( 1687 ReloadFeaturesOperation, 1688 ) 1689 1690 operations = [ 1691 ReloadFeaturesOperation( 1692 "reload_spectra", add_ms1=load_ms1, add_ms2=load_ms2 1693 ) 1694 ] 1695 lcms_collection.process_samples_pipeline( 1696 operations, keep_raw_data=False, show_progress=False 1697 ) 1698 1699 # Load EICs for clustered features if requested 1700 if load_eics: 1701 # Reuse the existing LoadEICsOperation from the pipeline system 1702 from corems.mass_spectra.calc.lc_calc_operations import LoadEICsOperation 1703 1704 operations = [LoadEICsOperation("load_eics")] 1705 lcms_collection.process_samples_pipeline( 1706 operations, keep_raw_data=False, show_progress=False 1707 ) 1708 1709 # Associate EICs with mass features (same as in process_consensus_features) 1710 for sample_id in range(len(lcms_collection.samples)): 1711 sample = lcms_collection[sample_id] 1712 if sample.eics: # Only if EICs were loaded 1713 # Associate EICs with regular mass features 1714 sample.associate_eics_with_mass_features(induced=False) 1715 # Associate EICs with induced mass features 1716 sample.associate_eics_with_mass_features(induced=True) 1717 1718 return lcms_collection
Get the LCMS collection from the saved HDF5 file.
Parameters
- load_raw (bool, optional): If True, load raw data. Default is False.
- load_light (bool, optional): If True, load light data (minimal). Default is False.
- load_representatives (bool, optional): If True, load representative mass features from clusters. Default is False.
- load_eics (bool, optional): If True, load EIC data for clustered mass features. Default is False.
- load_ms1 (bool, optional): If True, load MS1 spectra for loaded mass features. Default is False.
- load_ms2 (bool, optional): If True, load MS2 spectra for loaded mass features. Default is False.
Returns
- LCMSCollection: The loaded LCMS collection object.