corems.mass_spectra.calc.lc_calc_operations

Sample-level operations for LCMS collection processing pipelines.

This module provides a framework for defining reusable, composable operations that can be executed on individual samples in a parallelized manner.

Classes

SampleOperation Base class for all sample-level operations GapFillOperation Gap-fill missing cluster features for a sample ReloadFeaturesOperation Reload mass features from HDF5 for a sample

   1"""
   2Sample-level operations for LCMS collection processing pipelines.
   3
   4This module provides a framework for defining reusable, composable operations
   5that can be executed on individual samples in a parallelized manner.
   6
   7Classes
   8-------
   9SampleOperation
  10    Base class for all sample-level operations
  11GapFillOperation
  12    Gap-fill missing cluster features for a sample
  13ReloadFeaturesOperation
  14    Reload mass features from HDF5 for a sample
  15
  16"""
  17
  18from abc import ABC, abstractmethod
  19import pandas as pd
  20
  21
  22class SampleOperation(ABC):
  23    """
  24    Base class for operations that can be performed on a sample.
  25    
  26    All sample operations must inherit from this class and implement all
  27    abstract methods. This ensures proper integration with the pipeline framework.
  28    
  29    Parameters
  30    ----------
  31    name : str
  32        Name of the operation (for logging and identification)
  33    **kwargs
  34        Additional keyword arguments stored as operation parameters
  35        
  36    Attributes
  37    ----------
  38    name : str
  39        Operation name
  40    params : dict
  41        Dictionary of operation parameters
  42    description : str
  43        Human-readable description for progress messages (must override in subclasses)
  44    """
  45    
  46    def __init__(self, name, **kwargs):
  47        self.name = name
  48        self.params = kwargs
  49    
  50    @property
  51    @abstractmethod
  52    def description(self):
  53        """
  54        Human-readable description for progress messages.
  55        
  56        This property must be overridden in subclasses to provide a meaningful
  57        description that will be shown in progress bars (e.g., "gap-filling",
  58        "reloading features", etc.).
  59        
  60        Returns
  61        -------
  62        str
  63            Brief description of what this operation does
  64        """
  65        pass
  66    
  67    @abstractmethod
  68    def needs_raw_ms_data(self):
  69        """
  70        Declare whether this operation needs raw MS data loaded.
  71        
  72        Subclasses must implement this method to specify raw data requirements.
  73        The pipeline executor will ensure raw data is loaded before executing
  74        operations that need it, and can clean it up afterwards.
  75        
  76        Returns
  77        -------
  78        tuple of (bool, int or None)
  79            (needs_raw_data, ms_level)
  80            - needs_raw_data: True if operation needs raw MS data
  81            - ms_level: MS level needed (1 for MS1, 2 for MS2, etc.) or None
  82            
  83        Examples
  84        --------
  85        >>> def needs_raw_ms_data(self):
  86        ...     return True, 1  # Needs MS1 data
  87        >>> def needs_raw_ms_data(self):
  88        ...     return False, None  # No raw data needed
  89        """
  90        pass
  91    
  92    @abstractmethod
  93    def can_execute(self, sample, collection):
  94        """
  95        Check if this operation can be executed on the sample.
  96        
  97        Subclasses must implement this method to define prerequisites.
  98        Return True if the operation can execute, False otherwise.
  99        
 100        Parameters
 101        ----------
 102        sample : LCMSBase
 103            The sample to check
 104        collection : LCMSBaseCollection
 105            The collection containing the sample
 106            
 107        Returns
 108        -------
 109        bool
 110            True if operation can execute, False otherwise
 111            
 112        Examples
 113        --------
 114        >>> def can_execute(self, sample, collection):
 115        ...     return True  # Can always execute
 116        >>> def can_execute(self, sample, collection):
 117        ...     return hasattr(sample, 'mass_features') and sample.mass_features
 118        """
 119        pass
 120    
 121    @abstractmethod
 122    def execute(self, sample_id, collection, **runtime_params):
 123        """
 124        Execute the operation on a sample.
 125        
 126        This method must be implemented by subclasses.
 127        
 128        Parameters
 129        ----------
 130        sample_id : int
 131            Sample ID to process
 132        collection : LCMSBaseCollection
 133            The collection containing the sample
 134        **runtime_params
 135            Runtime parameters passed from the pipeline
 136            
 137        Returns
 138        -------
 139        result
 140            Operation result (can be None if operation modifies sample in place)
 141        """
 142        pass
 143    
 144    @abstractmethod
 145    def collect_results(self, sample_id, result, collection):
 146        """
 147        Collect results back into collection after parallel execution.
 148        
 149        Subclasses must implement this method to handle result collection.
 150        If the operation modifies samples in place and doesn't need to collect
 151        results, simply implement as `pass`.
 152        
 153        Parameters
 154        ----------
 155        sample_id : int
 156            Sample ID that was processed
 157        result
 158            Result returned from execute()
 159        collection : LCMSBaseCollection
 160            The collection to update
 161            
 162        Examples
 163        --------
 164        >>> def collect_results(self, sample_id, result, collection):
 165        ...     pass  # Operation modifies sample in place
 166        >>> def collect_results(self, sample_id, result, collection):
 167        ...     collection[sample_id].induced_mass_features = result
 168        """
 169        pass
 170        
 171    def __repr__(self):
 172        return f"{self.__class__.__name__}(name='{self.name}')"
 173
 174
 175class GapFillOperation(SampleOperation):
 176    """
 177    Gap-fill missing cluster features for a sample.
 178    
 179    Searches raw MS1 data to find peaks in expected m/z and retention time
 180    windows for clusters that are present in other samples but missing from
 181    this sample.
 182    
 183    Uses time range filtering for efficient data loading - only loads the 
 184    retention time windows where gaps need to be filled, plus a buffer 
 185    (controlled by eic_buffer_time parameter) for complete EIC extraction. 
 186    Multiple time ranges are automatically merged if they overlap.
 187    
 188    Parameters
 189    ----------
 190    name : str
 191        Operation name
 192    expand_on_miss : bool, optional
 193        If True, expands search window when no peak is found. Default is False.
 194        
 195    Notes
 196    -----
 197    Requires that add_consensus_mass_features() has been run on the collection.
 198    This operation loads raw MS1 data which will be available for subsequent operations.
 199    Time range filtering significantly reduces memory usage and loading time for
 200    large datasets with sparse gaps.
 201    """
 202    
 203    @property
 204    def description(self):
 205        """Human-readable description for progress messages."""
 206        return "gap-filling"
 207    
 208    def needs_raw_ms_data(self):
 209        """This operation needs raw MS1 data."""
 210        return True, 1
 211    
 212    def can_execute(self, sample, collection):
 213        """Check if cluster summary exists."""
 214        return hasattr(collection, 'cluster_summary_dataframe') and \
 215               collection.cluster_summary_dataframe is not None
 216    
 217    def execute(self, sample_id, collection, **runtime_params):
 218        """
 219        Execute gap-filling for a single sample.
 220        
 221        Parameters
 222        ----------
 223        sample_id : int
 224            Sample index to process
 225        collection : LCMSBaseCollection
 226            The collection
 227        **runtime_params
 228            Runtime parameters including:
 229            - missingdf : pd.DataFrame - Cluster information and missing samples (optional)
 230            - cluster_dict : dict - Cluster feature dictionary (optional)
 231            - expand_on_miss : bool - Whether to expand search window on miss (optional)
 232            If these are not provided, returns empty dict (no gaps to fill).
 233            
 234        Returns
 235        -------
 236        dict
 237            Dictionary of induced mass features (empty if no gaps to fill)
 238        """
 239        # Extract gap-fill parameters from runtime_params
 240        # If not present, there are no gaps to fill, so return early
 241        if 'missingdf' not in runtime_params:
 242            return {}
 243        
 244        missingdf = runtime_params['missingdf']
 245        cluster_dict = runtime_params['cluster_dict']
 246        expand_on_miss = runtime_params['expand_on_miss']
 247        
 248        # This is essentially the same logic as _search_for_targeted_mass_features_in_sample
 249        # but extracted into an operation
 250        
 251        # Get clusters missing data for this sample
 252        sampledf = missingdf[
 253            missingdf.missing_samples.apply(lambda x: sample_id in x)
 254        ].reset_index(drop=True).copy()
 255
 256        # Skip if no missing features for this sample
 257        if len(sampledf) == 0:
 258            return {}
 259
 260        # Get buffer time from LCMS parameters for EIC extraction
 261        # This ensures we capture the full chromatographic peak beyond cluster bounds
 262        buffer_rt = collection[sample_id].parameters.lc_ms.eic_buffer_time
 263        
 264        # Calculate time ranges for efficient loading with buffer for EIC extraction
 265        time_ranges = []
 266        
 267        for _, row in sampledf.iterrows():
 268            rt_min = row['scan_time_aligned_min']
 269            rt_max = row['scan_time_aligned_max']
 270            
 271            # If expand_on_miss, also consider the allowed bounds
 272            if expand_on_miss:
 273                rt_min = min(rt_min, row['sta_min_allowed'])
 274                rt_max = max(rt_max, row['sta_max_allowed'])
 275            
 276            # Apply buffer AFTER considering expand_on_miss bounds
 277            # This ensures buffer is added beyond even the expanded search window
 278            time_ranges.append((max(0, rt_min - buffer_rt), rt_max + buffer_rt))
 279        
 280        # Merge overlapping time ranges to reduce number of separate loads
 281        time_ranges = sorted(time_ranges)
 282        merged_ranges = []
 283        if time_ranges:
 284            current_min, current_max = time_ranges[0]
 285            for rt_min, rt_max in time_ranges[1:]:
 286                if rt_min <= current_max:  # Overlapping or adjacent
 287                    current_max = max(current_max, rt_max)
 288                else:
 289                    merged_ranges.append((current_min, current_max))
 290                    current_min, current_max = rt_min, rt_max
 291            merged_ranges.append((current_min, current_max))
 292
 293        # Load raw data for this sample with time range filtering
 294        collection.load_raw_data(sample_id, 1, time_range=merged_ranges)
 295        
 296        # Get MS1 data
 297        ms1df = collection[sample_id]._ms_unprocessed[1].copy()
 298        scan_df = collection[sample_id].scan_df[['scan', 'scan_time_aligned']]
 299        ms1df = pd.merge(ms1df, scan_df, on='scan')
 300
 301        # Pre-extract all values from sampledf
 302        clusters = sampledf.cluster.values
 303        mz_mins = sampledf.mz_min.values
 304        mz_maxs = sampledf.mz_max.values
 305        st_mins = sampledf.scan_time_aligned_min.values
 306        st_maxs = sampledf.scan_time_aligned_max.values
 307        
 308        if expand_on_miss:
 309            mz_mins_allowed = sampledf.mz_min_allowed.values
 310            mz_maxs_allowed = sampledf.mz_max_allowed.values
 311            st_mins_allowed = sampledf.sta_min_allowed.values
 312            st_maxs_allowed = sampledf.sta_max_allowed.values
 313
 314        # Pre-filter ms1df to reduce search space
 315        mz_global_min = mz_mins.min()
 316        mz_global_max = mz_maxs.max()
 317        st_global_min = st_mins.min()
 318        st_global_max = st_maxs.max()
 319        
 320        if expand_on_miss:
 321            mz_global_min = min(mz_global_min, mz_mins_allowed.min())
 322            mz_global_max = max(mz_global_max, mz_maxs_allowed.max())
 323            st_global_min = min(st_global_min, st_mins_allowed.min())
 324            st_global_max = max(st_global_max, st_maxs_allowed.max())
 325        
 326        ms1df_filtered = ms1df[
 327            (ms1df.mz >= mz_global_min) & 
 328            (ms1df.mz <= mz_global_max) &
 329            (ms1df.scan_time_aligned >= st_global_min) &
 330            (ms1df.scan_time_aligned <= st_global_max)
 331        ].copy()
 332
 333        # Generate set_ids for all features
 334        set_ids = [f'c{clusters[i]}_{i}_i' for i in range(len(sampledf))]
 335        
 336        # Use batch method to process all features at once
 337        if expand_on_miss:
 338            # First try with normal bounds
 339            peaks_dict = collection[sample_id].search_for_targeted_mass_features_batch(
 340                ms1df_filtered,
 341                mz_mins,
 342                mz_maxs,
 343                st_mins,
 344                st_maxs,
 345                set_ids,
 346                obj_idx=sample_id,
 347                st_aligned=True
 348            )
 349            
 350            # Retry failed features with expanded bounds
 351            failed_indices = [i for i, sid in enumerate(set_ids) if peaks_dict[sid].apex_scan == -99]
 352            if failed_indices:
 353                failed_ids = [set_ids[i] for i in failed_indices]
 354                retry_peaks = collection[sample_id].search_for_targeted_mass_features_batch(
 355                    ms1df_filtered,
 356                    mz_mins_allowed[failed_indices],
 357                    mz_maxs_allowed[failed_indices],
 358                    st_mins_allowed[failed_indices],
 359                    st_maxs_allowed[failed_indices],
 360                    failed_ids,
 361                    obj_idx=sample_id,
 362                    st_aligned=True
 363                )
 364                peaks_dict.update(retry_peaks)
 365        else:
 366            peaks_dict = collection[sample_id].search_for_targeted_mass_features_batch(
 367                ms1df_filtered,
 368                mz_mins,
 369                mz_maxs,
 370                st_mins,
 371                st_maxs,
 372                set_ids,
 373                obj_idx=sample_id,
 374                st_aligned=True
 375            )
 376        
 377        # Build induced_mass_features dict and update cluster_dict
 378        induced_mass_features = {}
 379        for i in range(len(sampledf)):
 380            peak = peaks_dict[set_ids[i]]
 381            induced_mass_features[peak.id] = peak
 382            cluster_dict[clusters[i]] += [set_ids[i]]
 383        
 384        # Integrate mass features (don't fail on bad integration)
 385        collection[sample_id].induced_mass_features = induced_mass_features
 386        collection[sample_id].integrate_mass_features(drop_if_fail=False, induced_features=True)
 387        
 388        # Add MS1 spectra and peak metrics to successfully detected induced features
 389        # Only process features that were successfully detected (apex_scan != -99)
 390        # This is critical for having m/z values in the pivot table for gap-filled features
 391        successful_induced = {k: v for k, v in induced_mass_features.items() if v.apex_scan != -99}
 392        
 393        if len(successful_induced) > 0:
 394            # Use the already-loaded raw data (use_parser=False) for efficiency
 395            collection[sample_id].add_associated_ms1(
 396                auto_process=True,
 397                use_parser=False,
 398                spectrum_mode=None,
 399                induced_features=True
 400            )
 401        
 402        # Return the induced features (some may have been filtered out)
 403        return collection[sample_id].induced_mass_features
 404        
 405    def collect_results(self, sample_id, result, collection):
 406        """Collect induced mass features back into sample."""
 407        collection[sample_id].induced_mass_features = result
 408
 409
 410class ReloadFeaturesOperation(SampleOperation):
 411    """
 412    Reload mass features from HDF5 and optionally add MS1/MS2 spectra.
 413    
 414    This is useful when the collection was loaded with load_light=True,
 415    which stores mass features only in the collection dataframe and not
 416    as LCMSMassFeature objects in individual samples.
 417    
 418    Parameters
 419    ----------
 420    name : str
 421        Operation name
 422    add_ms1 : bool, optional
 423        If True, adds MS1 spectra to mass features. Automatically uses raw MS1 data
 424        if available (e.g., from gap-filling), otherwise uses parser. Spectrum mode
 425        is auto-detected. Default is False.
 426    add_ms2 : bool, optional
 427        If True, also loads and associates MS2 spectra. Spectrum mode is auto-detected.
 428        Default is False.
 429    auto_process_ms2 : bool, optional
 430        If True and add_ms2=True, auto-processes MS2 spectra. Default is True.
 431    ms2_scan_filter : str or None, optional
 432        Filter string for MS2 scans. Default is None.
 433        
 434    Notes
 435    -----
 436    MS1 spectra association automatically uses raw MS1 data if loaded by a previous
 437    operation (e.g., GapFillOperation). This is efficient when multiple operations
 438    need MS1 data in the same pipeline. All spectrum modes are auto-detected from
 439    the data.
 440    """
 441    
 442    @property
 443    def description(self):
 444        """Human-readable description for progress messages."""
 445        return "reloading features"
 446    
 447    def needs_raw_ms_data(self):
 448        """This operation doesn't need raw data."""
 449        return False, None
 450    
 451    def can_execute(self, sample, collection):
 452        """Check if collection parser is available."""
 453        return hasattr(collection, 'collection_parser') and \
 454               collection.collection_parser is not None
 455    
 456    def execute(self, sample_id, collection, mf_ids_to_load=None, **runtime_params):
 457        """
 458        Execute feature reloading for a single sample.
 459        
 460        Parameters
 461        ----------
 462        sample_id : int
 463            Sample ID to reload features for
 464        collection : LCMSBaseCollection
 465            The collection
 466        mf_ids_to_load : list of str, optional
 467            List of collection-level mf_ids to load
 468        **runtime_params
 469            Additional runtime parameters (ignored)
 470            
 471        Returns
 472        -------
 473        dict
 474            Dictionary of reloaded mass features
 475        """
 476        # Get parameters
 477        add_ms1 = self.params.get('add_ms1', False)
 478        add_ms2 = self.params.get('add_ms2', False)
 479        auto_process_ms2 = self.params.get('auto_process_ms2', True)
 480        ms2_scan_filter = self.params.get('ms2_scan_filter', None)
 481        
 482        sample = collection[sample_id]
 483        sample_name = collection.samples[sample_id]
 484        
 485        # Auto-determine if we should use parser for MS1 (check if raw data is available)
 486        has_raw_ms1 = 1 in sample._ms_unprocessed and not sample._ms_unprocessed[1].empty
 487        use_parser_for_ms1 = not has_raw_ms1  # Use parser only if raw data not available
 488        
 489        # Spectrum modes will be auto-detected (None = auto-detect)
 490        spectrum_mode_ms1 = None
 491        ms2_spectrum_mode = None
 492        
 493        # Check if we have a collection parser
 494        if not hasattr(collection, 'collection_parser') or collection.collection_parser is None:
 495            print(f"Warning: Cannot reload mass features for {sample_name} - no collection_parser available")
 496            return {}
 497        
 498        # Get the HDF5 file for this sample
 499        hdf5_file = collection.collection_parser.folder_location / f"{sample_name}.corems/{sample_name}.hdf5"
 500        
 501        if not hdf5_file.exists():
 502            print(f"Warning: HDF5 file not found for sample {sample_name}: {hdf5_file}")
 503            return {}
 504        
 505        # Import here to avoid circular imports
 506        from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectra
 507        
 508        # If specific mf_ids requested, extract the local mf_ids we need
 509        local_mf_ids_to_load = None
 510        if mf_ids_to_load is not None:
 511            # mf_ids_to_load is already a list of sample-level mf_ids (integers)
 512            # No parsing needed - they come from the mf_id column in the dataframe
 513            if len(mf_ids_to_load) == 0:
 514                # No features to load for this sample - return empty dict
 515                return {}
 516            local_mf_ids_to_load = set(mf_ids_to_load)
 517        
 518        # Reload mass features from HDF5
 519        with ReadCoreMSHDFMassSpectra(hdf5_file) as parser:
 520            parser.import_mass_features(sample, mf_ids=local_mf_ids_to_load)
 521        
 522        # If add_ms1, associate MS1 spectra with the loaded mass features
 523        if add_ms1 and len(sample.mass_features) > 0:
 524            # Check if raw MS1 data is already loaded (e.g., from gap-filling)
 525            has_raw_ms1 = 1 in sample._ms_unprocessed and not sample._ms_unprocessed[1].empty
 526            
 527            if has_raw_ms1 and not use_parser_for_ms1:
 528                # Use already-loaded raw data (more efficient)
 529                sample.add_associated_ms1(
 530                    auto_process=True,
 531                    use_parser=False,
 532                    spectrum_mode=spectrum_mode_ms1
 533                )
 534            else:
 535                # Use parser to get MS1 spectra
 536                sample.add_associated_ms1(
 537                    auto_process=True,
 538                    use_parser=True,
 539                    spectrum_mode=spectrum_mode_ms1
 540                )
 541        
 542        # If add_ms2, associate MS2 spectra with the loaded mass features
 543        if add_ms2 and len(sample.mass_features) > 0:
 544            # Get the IDs of loaded mass features (use what was actually loaded)
 545            mf_ids_for_ms2 = list(sample.mass_features.keys())
 546            
 547            collection._associate_ms2_with_mass_features(
 548                sample, 
 549                mf_ids_for_ms2,
 550                auto_process=auto_process_ms2,
 551                spectrum_mode=ms2_spectrum_mode,
 552                scan_filter=ms2_scan_filter
 553            )
 554        
 555        # Return both mass_features and _ms so they can be collected in multiprocessing
 556        return {'mass_features': sample.mass_features, '_ms': sample._ms}
 557        
 558    def collect_results(self, sample_id, result, collection):
 559        """
 560        Collect reloaded mass features back into sample.
 561        
 562        This operation loads a subset of mass features (e.g., representatives)
 563        into sample.mass_features for processing, while preserving the full
 564        mass_features_dataframe at the collection level. Sets a lock flag to
 565        prevent automatic rebuilding of the collection dataframe from individual
 566        samples. Also collects loaded mass spectra.
 567        
 568        Parameters
 569        ----------
 570        sample_id : int
 571            Sample ID that was processed
 572        result : dict
 573            Dictionary with 'mass_features' and '_ms' from execute()
 574        collection : LCMSBaseCollection
 575            The collection
 576        """
 577        # Update sample.mass_features with loaded features
 578        if isinstance(result, dict) and 'mass_features' in result:
 579            collection[sample_id].mass_features = result['mass_features']
 580            # Also collect the _ms dictionary (MS1 and MS2 spectra)
 581            if '_ms' in result:
 582                collection[sample_id]._ms.update(result['_ms'])
 583        else:
 584            # Backward compatibility - if result is just mass_features dict
 585            collection[sample_id].mass_features = result
 586        
 587        # Lock the collection dataframe to prevent rebuilding from individual samples
 588        # (since we've only loaded a subset, rebuilding would lose data)
 589        collection._mass_features_locked = True
 590
 591
 592class MolecularFormulaSearchOperation(SampleOperation):
 593    """
 594    Perform molecular formula search on mass features using associated MS1 spectra.
 595    
 596    This operation runs molecular formula search on all mass features in a sample
 597    that have associated MS1 spectra. Requires MS1 spectra to be loaded and
 598    processed before execution.
 599    
 600    Parameters
 601    ----------
 602    name : str
 603        Operation name (for logging)
 604    **kwargs
 605        Additional parameters passed to parent class
 606        
 607    Examples
 608    --------
 609    >>> op = MolecularFormulaSearchOperation('mf_search')
 610    >>> # Use in pipeline
 611    >>> results = collection.process_samples_pipeline([op])
 612    
 613    Notes
 614    -----
 615    This operation requires that MS1 spectra have been associated with mass
 616    features (e.g., via ReloadFeaturesOperation with add_ms1=True). The
 617    molecular formula search uses parameters from the collection's 
 618    parameters.mass_spectrum["ms1"].molecular_search settings.
 619    """
 620    
 621    @property
 622    def description(self):
 623        """Human-readable description for progress messages."""
 624        return "molecular formula search"
 625    
 626    def __init__(self, name='molecular_formula_search', **kwargs):
 627        super().__init__(name, **kwargs)
 628    
 629    def needs_raw_ms_data(self):
 630        """
 631        This operation doesn't need raw data - it works on processed MS1 spectra
 632        that are already associated with mass features.
 633        
 634        Returns
 635        -------
 636        tuple
 637            (False, None) - no raw data needed
 638        """
 639        return False, None
 640    
 641    def can_execute(self, sample, collection, **runtime_params):
 642        """
 643        Check if molecular formula search can be executed.
 644        
 645        Requires that the sample has mass features with associated MS1 spectra.
 646        
 647        Parameters
 648        ----------
 649        sample : LCMSObject
 650            The sample object
 651        collection : LCMSCollection
 652            The collection containing the sample
 653        **runtime_params
 654            Runtime parameters (not used)
 655            
 656        Returns
 657        -------
 658        bool
 659            True if sample has mass features with MS1 spectra
 660        """        
 661        # Check if sample has mass features
 662        if not hasattr(sample, 'mass_features') or not sample.mass_features:
 663            return False
 664        
 665        # Check if at least some mass features have MS1 spectra
 666        has_ms1 = any(
 667            hasattr(mf, 'mass_spectrum') and mf.mass_spectrum is not None
 668            for mf in sample.mass_features.values()
 669        )
 670        
 671        return has_ms1
 672    
 673    def execute(self, sample_id, collection, **runtime_params):
 674        """
 675        Execute molecular formula search on a sample.
 676        
 677        Creates a SearchMolecularFormulasLC object and runs mass feature search,
 678        which annotates mass features with molecular formula assignments.
 679        
 680        Parameters
 681        ----------
 682        sample_id : str
 683            Sample identifier
 684        collection : LCMSCollection
 685            The collection containing the sample
 686        **runtime_params
 687            Runtime parameters (not used)
 688            
 689        Returns
 690        -------
 691        int
 692            Number of mass features that were searched
 693        """
 694        from corems.molecular_id.search.molecularFormulaSearch import SearchMolecularFormulasLC
 695        import time
 696        import sqlalchemy.exc
 697        import sqlite3
 698        
 699        sample = collection[sample_id]
 700        
 701        # Verify that mass features exist
 702        if not hasattr(sample, 'mass_features') or not sample.mass_features:
 703            return 0  # No mass features to search
 704        
 705        # Verify that mass features have MS1 spectra associated
 706        if not hasattr(sample, '_ms') or not sample._ms:
 707            raise RuntimeError(
 708                f"Sample {sample_id} does not have MS1 spectra loaded in _ms dictionary. "
 709                "Molecular formula search requires MS1 spectra to be associated with mass features. "
 710                "Ensure add_ms1=True when reloading features."
 711            )
 712        
 713        # Prepare data for bulk molecular formula search
 714        # Group mass features by their apex scan
 715        scan_to_mf = {}
 716        for mf_id, mf in sample.mass_features.items():
 717            apex_scan = mf.apex_scan
 718            if apex_scan not in scan_to_mf:
 719                scan_to_mf[apex_scan] = []
 720            scan_to_mf[apex_scan].append(mf)
 721        
 722        # Build lists of mass spectra and corresponding peaks
 723        mass_spectrum_list = []
 724        ms_peaks_list = []
 725        
 726        for scan_num, mf_list in scan_to_mf.items():
 727            # Get the mass spectrum for this scan
 728            if scan_num not in sample._ms:
 729                continue  # Skip if spectrum not loaded
 730                
 731            mass_spectrum = sample._ms[scan_num]
 732            
 733            # Verify spectrum is processed (has peaks)
 734            if not hasattr(mass_spectrum, '_mspeaks') or not mass_spectrum._mspeaks:
 735                continue  # Skip unprocessed spectra
 736            
 737            # Get the MS1 peaks for each mass feature at this scan
 738            peaks_for_scan = []
 739            for mf in mf_list:
 740                try:
 741                    # Use the ms1_peak property which finds the closest peak
 742                    ms1_peak = mf.ms1_peak
 743                    peaks_for_scan.append(ms1_peak)
 744                except (AttributeError, IndexError):
 745                    # Skip if ms1_peak can't be determined
 746                    continue
 747            
 748            if peaks_for_scan:
 749                mass_spectrum_list.append(mass_spectrum)
 750                ms_peaks_list.append(peaks_for_scan)
 751        
 752        # Run molecular formula search if we have data, with retry logic for database locks
 753        if mass_spectrum_list and ms_peaks_list:
 754            max_retries = 10
 755            retry_delay = 2  # seconds
 756            
 757            for attempt in range(max_retries):
 758                try:
 759                    mol_search = SearchMolecularFormulasLC(sample)
 760                    mol_search.bulk_run_molecular_formula_search(mass_spectrum_list, ms_peaks_list)
 761                    break  # Success, exit retry loop
 762                except (sqlalchemy.exc.OperationalError, sqlite3.OperationalError) as e:
 763                    if attempt < max_retries - 1:
 764                        # Database is locked, retry after delay
 765                        print(f"Sample {sample_id}: Database locked during molecular formula search, retrying in {retry_delay}s (attempt {attempt + 1}/{max_retries})...")
 766                        time.sleep(retry_delay)
 767                    else:
 768                        # Max retries exceeded, re-raise the exception
 769                        raise RuntimeError(
 770                            f"Sample {sample_id}: Molecular formula search failed after {max_retries} attempts due to database lock. "
 771                            "Try reducing parallel cores or increasing database timeout."
 772                        ) from e
 773        
 774        # Return count of features searched
 775        return len(sample.mass_features)
 776    
 777    def collect_results(self, sample_id, result, collection):
 778        """
 779        Collect results (no-op as search modifies mass features in place).
 780        
 781        The molecular formula search modifies mass features in place by adding
 782        molecular formula assignments, so no explicit result collection is needed.
 783        
 784        Parameters
 785        ----------
 786        sample_id : str
 787            Sample identifier
 788        result : int
 789            Number of features searched
 790        collection : LCMSCollection
 791            The collection containing the sample
 792        """
 793        # Search modifies mass features in place, nothing to collect
 794        pass
 795
 796
 797class MS2SpectralSearchOperation(SampleOperation):
 798    """
 799    Perform MS2 spectral search using entropy-based matching.
 800    
 801    This operation performs spectral library search on MS2 spectra associated
 802    with mass features using FlashEntropy for fast similarity scoring. Requires
 803    MS2 spectra to be loaded and processed before execution.
 804    
 805    Parameters
 806    ----------
 807    name : str
 808        Operation name (for logging)
 809    ms2_scan_filter : str or None, optional
 810        Filter string for MS2 scans (e.g., 'hcd'). If None, uses all MS2 scans.
 811        Default is None.
 812    peak_sep_da : float, optional
 813        Peak separation in Daltons for spectral matching. Default is 0.01.
 814    **kwargs
 815        Additional parameters passed to parent class
 816        
 817    Examples
 818    --------
 819    >>> op = MS2SpectralSearchOperation('ms2_search', ms2_scan_filter='hcd')
 820    >>> # Use in pipeline - requires fe_lib in runtime_params
 821    >>> results = collection.process_samples_pipeline([op])
 822    
 823    Notes
 824    -----
 825    This operation requires:
 826    - MS2 spectra to be associated with mass features
 827    - FlashEntropy library (fe_lib) to be provided in runtime_params
 828    - MS2 spectra must be processed (centroided)
 829    
 830    The spectral search modifies mass features in place by adding spectral
 831    match scores and metadata.
 832    """
 833    
 834    @property
 835    def description(self):
 836        """Human-readable description for progress messages."""
 837        return "MS2 spectral search"
 838    
 839    def __init__(self, name='ms2_spectral_search', ms2_scan_filter=None, **kwargs):
 840        super().__init__(name, **kwargs)
 841        self.params['ms2_scan_filter'] = ms2_scan_filter
 842    
 843    def needs_raw_ms_data(self):
 844        """
 845        This operation doesn't need raw data - it works on processed MS2 spectra
 846        that are already associated with mass features.
 847        
 848        Returns
 849        -------
 850        tuple
 851            (False, None) - no raw data needed
 852        """
 853        return False, None
 854    
 855    def can_execute(self, sample, collection, **runtime_params):
 856        """
 857        Check if MS2 spectral search can be executed.
 858        
 859        Requires that the sample has mass features with MS2 spectra associated.
 860        
 861        Parameters
 862        ----------
 863        sample : LCMSObject
 864            The sample object
 865        collection : LCMSCollection
 866            The collection containing the sample
 867        **runtime_params
 868            Runtime parameters (not used)
 869            
 870        Returns
 871        -------
 872        bool
 873            True if sample has mass features with MS2 spectra
 874        """        
 875        # Check if sample has mass features
 876        if not hasattr(sample, 'mass_features') or not sample.mass_features:
 877            return False
 878        
 879        # Check if any mass features have MS2 spectra associated
 880        has_ms2 = any(
 881            hasattr(mf, 'ms2_mass_spectra') and mf.ms2_mass_spectra
 882            for mf in sample.mass_features.values()
 883        )
 884        
 885        return has_ms2
 886    
 887    def execute(self, sample_id, collection, fe_lib=None, molecular_metadata=None, **runtime_params):
 888        """
 889        Execute MS2 spectral search on a sample.
 890        
 891        Performs entropy-based spectral library search on all MS2 spectra
 892        in the sample that match the scan filter criteria.
 893        
 894        Parameters
 895        ----------
 896        sample_id : str
 897            Sample identifier
 898        collection : LCMSCollection
 899            The collection containing the sample
 900        fe_lib : FlashEntropy library
 901            Pre-computed FlashEntropy library for spectral matching
 902        molecular_metadata : pd.DataFrame, optional
 903            Metadata for molecules in the spectral library
 904        **runtime_params
 905            Runtime parameters (not used)
 906            
 907        Returns
 908        -------
 909        int
 910            Number of MS2 spectra searched
 911        """
 912        sample = collection[sample_id]
 913        
 914        # Get parameters
 915        ms2_scan_filter = self.params.get('ms2_scan_filter')
 916        
 917        # Verify that we have a spectral library
 918        if fe_lib is None:
 919            raise ValueError(
 920                f"Sample {sample_id}: MS2 spectral search requires fe_lib (FlashEntropy library) "
 921                "to be provided in runtime parameters. Create the library at the collection level "
 922                "and pass it to the pipeline."
 923            )
 924        
 925        # Extract peak_sep_da from FlashEntropy library configuration
 926        # peak_sep_da should be 2 * max_ms2_tolerance_in_da to match the min_ms2_difference_in_da
 927        # used when creating the library
 928        tolerance_da = fe_lib.entropy_search.max_ms2_tolerance_in_da
 929        if tolerance_da is None:
 930            raise ValueError(
 931                f"Sample {sample_id}: Could not extract max_ms2_tolerance_in_da from FlashEntropy library. "
 932                "Ensure the library was created with this parameter specified."
 933            )
 934        peak_sep_da = 2 * tolerance_da
 935        
 936        # Verify that sample has _ms dictionary
 937        if not hasattr(sample, '_ms') or not sample._ms:
 938            return 0  # No MS2 spectra to search
 939        
 940        # Get MS2 scan numbers based on filter
 941        if ms2_scan_filter is not None:
 942            # Filter by scan text
 943            ms2_scan_df = sample.scan_df[
 944                sample.scan_df.scan_text.str.contains(ms2_scan_filter) &
 945                (sample.scan_df.ms_level == 2)
 946            ]
 947        else:
 948            # All MS2 scans
 949            ms2_scan_df = sample.scan_df[sample.scan_df.ms_level == 2]
 950        
 951        # Get scans that are actually loaded in _ms
 952        ms2_scans_to_search = [
 953            scan for scan in ms2_scan_df.scan.tolist()
 954            if scan in sample._ms.keys()
 955        ]
 956        
 957        if not ms2_scans_to_search:
 958            return 0  # No MS2 spectra to search
 959        
 960        # Perform spectral search using the sample's fe_search method
 961        sample.fe_search(
 962            scan_list=ms2_scans_to_search,
 963            fe_lib=fe_lib,
 964            peak_sep_da=peak_sep_da
 965        )
 966        
 967        # Return the spectral search results for collection
 968        # (needed for multiprocessing - results populated in worker need to be returned)
 969        return sample.spectral_search_results
 970    
 971    def collect_results(self, sample_id, result, collection):
 972        """
 973        Collect spectral search results back into the sample.
 974        
 975        In multiprocessing, the worker's modifications don't persist to the
 976        main process, so we need to explicitly collect and reassign the results.
 977        This also re-associates the results with mass features.
 978        
 979        Parameters
 980        ----------
 981        sample_id : str
 982            Sample identifier
 983        result : dict
 984            Dictionary of spectral search results from execute()
 985        collection : LCMSCollection
 986            The collection containing the sample
 987        """
 988        # Assign the spectral search results back to the sample
 989        if result:
 990            collection[sample_id].spectral_search_results.update(result)
 991            
 992            # Re-associate results with mass features (same logic as fe_search)
 993            sample = collection[sample_id]
 994            if len(sample.mass_features) > 0:
 995                for mass_feature_id, mass_feature in sample.mass_features.items():
 996                    scan_ids = mass_feature.ms2_scan_numbers
 997                    for ms2_scan_id in scan_ids:
 998                        precursor_mz = mass_feature.mz
 999                        try:
1000                            sample.spectral_search_results[ms2_scan_id][precursor_mz]
1001                        except KeyError:
1002                            pass
1003                        else:
1004                            sample.mass_features[
1005                                mass_feature_id
1006                            ].ms2_similarity_results.append(
1007                                sample.spectral_search_results[ms2_scan_id][precursor_mz]
1008                            )
1009
1010
1011class LoadEICsOperation(SampleOperation):
1012    """
1013    Load extracted ion chromatograms (EICs) from HDF5 for regular mass features.
1014    
1015    Loads EICs for regular mass features that belong to consensus clusters from HDF5.
1016    Induced (gap-filled) features already have EICs from integrate_mass_features,
1017    so no additional loading is needed for them.
1018    
1019    This operation enables downstream visualization and analysis of chromatographic
1020    peaks across all samples in a cluster.
1021    
1022    Notes
1023    -----
1024    Requires that mass features have been loaded and cluster_index assigned.
1025    Regular mass feature EICs must have been previously saved to HDF5 with export_eics=True.
1026    Induced mass features already have EICs populated during gap-filling.
1027    """
1028    
1029    @property
1030    def description(self):
1031        """Human-readable description for progress messages."""
1032        return "loading EICs"
1033    
1034    def needs_raw_ms_data(self):
1035        """This operation doesn't need raw data - induced features already have EICs."""
1036        return False, None
1037    
1038    def can_execute(self, sample, collection):
1039        """
1040        Check if EIC loading can be executed.
1041        
1042        This operation can always execute if the sample exists - the actual work
1043        is determined by cluster_mz_dict in runtime_params. If cluster_mz_dict is
1044        empty or None, execute() will simply return 0 (no EICs loaded).
1045        
1046        Returns
1047        -------
1048        bool
1049            True (always executable - runtime_params control actual work)
1050        """
1051        return True
1052    
1053    def execute(self, sample_id, collection, cluster_mz_dict=None, **runtime_params):
1054        """
1055        Load EICs from HDF5 for a single sample.
1056        
1057        Loads EICs for regular mass features that belong to consensus clusters.
1058        Induced (gap-filled) mass features already have EICs from integrate_mass_features,
1059        so no additional loading is needed for them.
1060        
1061        The cluster_mz_dict parameter (passed from collection level) maps sample_id
1062        to a list of m/z values that belong to clusters for that sample.
1063        
1064        Parameters
1065        ----------
1066        sample_id : int
1067            Sample index to process
1068        collection : LCMSBaseCollection
1069            The collection
1070        cluster_mz_dict : dict, optional
1071            Dictionary mapping sample_id to list of m/z values in clusters for that sample.
1072            If None, will not load any EICs. Default is None.
1073        **runtime_params
1074            Additional runtime parameters (ignored)
1075            
1076        Returns
1077        -------
1078        dict
1079            Dictionary of loaded EIC_Data objects, keyed by m/z value
1080        """
1081        from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectra
1082        
1083        sample = collection[sample_id]
1084        
1085        # If no cluster info provided or no m/z values for this sample, return early
1086        if cluster_mz_dict is None or sample_id not in cluster_mz_dict:
1087            return {}
1088        
1089        # Get m/z values for this sample that belong to clusters
1090        sample_cluster_mz = set(cluster_mz_dict[sample_id])
1091        if len(sample_cluster_mz) == 0:
1092            return {}
1093
1094        # Load EICs for each of the sample_cluster_mz
1095        hdf5_path = sample.file_location
1096        if hdf5_path and hdf5_path.exists():
1097            reader = ReadCoreMSHDFMassSpectra(str(hdf5_path))
1098            reader.import_eics(sample, mz_list=list(sample_cluster_mz))
1099            # Return the loaded EICs for multiprocessing collection
1100            # (modifications in worker process don't persist to main process)
1101            return sample.eics.copy()
1102        
1103        return {}
1104    
1105    def collect_results(self, sample_id, result, collection):
1106        """
1107        Collect loaded EICs back into sample.
1108        
1109        In multiprocessing, the worker's modifications don't persist to the
1110        main process, so we need to explicitly collect and reassign the EICs.
1111        This also re-associates EICs with mass features.
1112        
1113        Parameters
1114        ----------
1115        sample_id : int
1116            Sample ID that was processed
1117        result : dict
1118            Dictionary of EIC_Data objects keyed by m/z, returned from execute()
1119        collection : LCMSBaseCollection
1120            The collection
1121        """
1122        if result:
1123            # Update sample.eics with loaded EICs
1124            collection[sample_id].eics.update(result)
1125            # Note: EIC association with mass features happens after pipeline completes
1126            # to avoid multiprocessing issues (modifications in worker processes don't
1127            # persist to main process objects)
class SampleOperation(abc.ABC):
 23class SampleOperation(ABC):
 24    """
 25    Base class for operations that can be performed on a sample.
 26    
 27    All sample operations must inherit from this class and implement all
 28    abstract methods. This ensures proper integration with the pipeline framework.
 29    
 30    Parameters
 31    ----------
 32    name : str
 33        Name of the operation (for logging and identification)
 34    **kwargs
 35        Additional keyword arguments stored as operation parameters
 36        
 37    Attributes
 38    ----------
 39    name : str
 40        Operation name
 41    params : dict
 42        Dictionary of operation parameters
 43    description : str
 44        Human-readable description for progress messages (must override in subclasses)
 45    """
 46    
 47    def __init__(self, name, **kwargs):
 48        self.name = name
 49        self.params = kwargs
 50    
 51    @property
 52    @abstractmethod
 53    def description(self):
 54        """
 55        Human-readable description for progress messages.
 56        
 57        This property must be overridden in subclasses to provide a meaningful
 58        description that will be shown in progress bars (e.g., "gap-filling",
 59        "reloading features", etc.).
 60        
 61        Returns
 62        -------
 63        str
 64            Brief description of what this operation does
 65        """
 66        pass
 67    
 68    @abstractmethod
 69    def needs_raw_ms_data(self):
 70        """
 71        Declare whether this operation needs raw MS data loaded.
 72        
 73        Subclasses must implement this method to specify raw data requirements.
 74        The pipeline executor will ensure raw data is loaded before executing
 75        operations that need it, and can clean it up afterwards.
 76        
 77        Returns
 78        -------
 79        tuple of (bool, int or None)
 80            (needs_raw_data, ms_level)
 81            - needs_raw_data: True if operation needs raw MS data
 82            - ms_level: MS level needed (1 for MS1, 2 for MS2, etc.) or None
 83            
 84        Examples
 85        --------
 86        >>> def needs_raw_ms_data(self):
 87        ...     return True, 1  # Needs MS1 data
 88        >>> def needs_raw_ms_data(self):
 89        ...     return False, None  # No raw data needed
 90        """
 91        pass
 92    
 93    @abstractmethod
 94    def can_execute(self, sample, collection):
 95        """
 96        Check if this operation can be executed on the sample.
 97        
 98        Subclasses must implement this method to define prerequisites.
 99        Return True if the operation can execute, False otherwise.
100        
101        Parameters
102        ----------
103        sample : LCMSBase
104            The sample to check
105        collection : LCMSBaseCollection
106            The collection containing the sample
107            
108        Returns
109        -------
110        bool
111            True if operation can execute, False otherwise
112            
113        Examples
114        --------
115        >>> def can_execute(self, sample, collection):
116        ...     return True  # Can always execute
117        >>> def can_execute(self, sample, collection):
118        ...     return hasattr(sample, 'mass_features') and sample.mass_features
119        """
120        pass
121    
122    @abstractmethod
123    def execute(self, sample_id, collection, **runtime_params):
124        """
125        Execute the operation on a sample.
126        
127        This method must be implemented by subclasses.
128        
129        Parameters
130        ----------
131        sample_id : int
132            Sample ID to process
133        collection : LCMSBaseCollection
134            The collection containing the sample
135        **runtime_params
136            Runtime parameters passed from the pipeline
137            
138        Returns
139        -------
140        result
141            Operation result (can be None if operation modifies sample in place)
142        """
143        pass
144    
145    @abstractmethod
146    def collect_results(self, sample_id, result, collection):
147        """
148        Collect results back into collection after parallel execution.
149        
150        Subclasses must implement this method to handle result collection.
151        If the operation modifies samples in place and doesn't need to collect
152        results, simply implement as `pass`.
153        
154        Parameters
155        ----------
156        sample_id : int
157            Sample ID that was processed
158        result
159            Result returned from execute()
160        collection : LCMSBaseCollection
161            The collection to update
162            
163        Examples
164        --------
165        >>> def collect_results(self, sample_id, result, collection):
166        ...     pass  # Operation modifies sample in place
167        >>> def collect_results(self, sample_id, result, collection):
168        ...     collection[sample_id].induced_mass_features = result
169        """
170        pass
171        
172    def __repr__(self):
173        return f"{self.__class__.__name__}(name='{self.name}')"

Base class for operations that can be performed on a sample.

All sample operations must inherit from this class and implement all abstract methods. This ensures proper integration with the pipeline framework.

Parameters
  • name (str): Name of the operation (for logging and identification)
  • **kwargs: Additional keyword arguments stored as operation parameters
Attributes
  • name (str): Operation name
  • params (dict): Dictionary of operation parameters
  • description (str): Human-readable description for progress messages (must override in subclasses)
name
params
description
51    @property
52    @abstractmethod
53    def description(self):
54        """
55        Human-readable description for progress messages.
56        
57        This property must be overridden in subclasses to provide a meaningful
58        description that will be shown in progress bars (e.g., "gap-filling",
59        "reloading features", etc.).
60        
61        Returns
62        -------
63        str
64            Brief description of what this operation does
65        """
66        pass

Human-readable description for progress messages.

This property must be overridden in subclasses to provide a meaningful description that will be shown in progress bars (e.g., "gap-filling", "reloading features", etc.).

Returns
  • str: Brief description of what this operation does
@abstractmethod
def needs_raw_ms_data(self):
68    @abstractmethod
69    def needs_raw_ms_data(self):
70        """
71        Declare whether this operation needs raw MS data loaded.
72        
73        Subclasses must implement this method to specify raw data requirements.
74        The pipeline executor will ensure raw data is loaded before executing
75        operations that need it, and can clean it up afterwards.
76        
77        Returns
78        -------
79        tuple of (bool, int or None)
80            (needs_raw_data, ms_level)
81            - needs_raw_data: True if operation needs raw MS data
82            - ms_level: MS level needed (1 for MS1, 2 for MS2, etc.) or None
83            
84        Examples
85        --------
86        >>> def needs_raw_ms_data(self):
87        ...     return True, 1  # Needs MS1 data
88        >>> def needs_raw_ms_data(self):
89        ...     return False, None  # No raw data needed
90        """
91        pass

Declare whether this operation needs raw MS data loaded.

Subclasses must implement this method to specify raw data requirements. The pipeline executor will ensure raw data is loaded before executing operations that need it, and can clean it up afterwards.

Returns
  • tuple of (bool, int or None): (needs_raw_data, ms_level)
    • needs_raw_data: True if operation needs raw MS data
    • ms_level: MS level needed (1 for MS1, 2 for MS2, etc.) or None
Examples
>>> def needs_raw_ms_data(self):
...     return True, 1  # Needs MS1 data
>>> def needs_raw_ms_data(self):
...     return False, None  # No raw data needed
@abstractmethod
def can_execute(self, sample, collection):
 93    @abstractmethod
 94    def can_execute(self, sample, collection):
 95        """
 96        Check if this operation can be executed on the sample.
 97        
 98        Subclasses must implement this method to define prerequisites.
 99        Return True if the operation can execute, False otherwise.
100        
101        Parameters
102        ----------
103        sample : LCMSBase
104            The sample to check
105        collection : LCMSBaseCollection
106            The collection containing the sample
107            
108        Returns
109        -------
110        bool
111            True if operation can execute, False otherwise
112            
113        Examples
114        --------
115        >>> def can_execute(self, sample, collection):
116        ...     return True  # Can always execute
117        >>> def can_execute(self, sample, collection):
118        ...     return hasattr(sample, 'mass_features') and sample.mass_features
119        """
120        pass

Check if this operation can be executed on the sample.

Subclasses must implement this method to define prerequisites. Return True if the operation can execute, False otherwise.

Parameters
  • sample (LCMSBase): The sample to check
  • collection (LCMSBaseCollection): The collection containing the sample
Returns
  • bool: True if operation can execute, False otherwise
Examples
>>> def can_execute(self, sample, collection):
...     return True  # Can always execute
>>> def can_execute(self, sample, collection):
...     return hasattr(sample, 'mass_features') and sample.mass_features
@abstractmethod
def execute(self, sample_id, collection, **runtime_params):
122    @abstractmethod
123    def execute(self, sample_id, collection, **runtime_params):
124        """
125        Execute the operation on a sample.
126        
127        This method must be implemented by subclasses.
128        
129        Parameters
130        ----------
131        sample_id : int
132            Sample ID to process
133        collection : LCMSBaseCollection
134            The collection containing the sample
135        **runtime_params
136            Runtime parameters passed from the pipeline
137            
138        Returns
139        -------
140        result
141            Operation result (can be None if operation modifies sample in place)
142        """
143        pass

Execute the operation on a sample.

This method must be implemented by subclasses.

Parameters
  • sample_id (int): Sample ID to process
  • collection (LCMSBaseCollection): The collection containing the sample
  • **runtime_params: Runtime parameters passed from the pipeline
Returns
  • result: Operation result (can be None if operation modifies sample in place)
@abstractmethod
def collect_results(self, sample_id, result, collection):
145    @abstractmethod
146    def collect_results(self, sample_id, result, collection):
147        """
148        Collect results back into collection after parallel execution.
149        
150        Subclasses must implement this method to handle result collection.
151        If the operation modifies samples in place and doesn't need to collect
152        results, simply implement as `pass`.
153        
154        Parameters
155        ----------
156        sample_id : int
157            Sample ID that was processed
158        result
159            Result returned from execute()
160        collection : LCMSBaseCollection
161            The collection to update
162            
163        Examples
164        --------
165        >>> def collect_results(self, sample_id, result, collection):
166        ...     pass  # Operation modifies sample in place
167        >>> def collect_results(self, sample_id, result, collection):
168        ...     collection[sample_id].induced_mass_features = result
169        """
170        pass

Collect results back into collection after parallel execution.

Subclasses must implement this method to handle result collection. If the operation modifies samples in place and doesn't need to collect results, simply implement as pass.

Parameters
  • sample_id (int): Sample ID that was processed
  • result: Result returned from execute()
  • collection (LCMSBaseCollection): The collection to update
Examples
>>> def collect_results(self, sample_id, result, collection):
...     pass  # Operation modifies sample in place
>>> def collect_results(self, sample_id, result, collection):
...     collection[sample_id].induced_mass_features = result
class GapFillOperation(SampleOperation):
176class GapFillOperation(SampleOperation):
177    """
178    Gap-fill missing cluster features for a sample.
179    
180    Searches raw MS1 data to find peaks in expected m/z and retention time
181    windows for clusters that are present in other samples but missing from
182    this sample.
183    
184    Uses time range filtering for efficient data loading - only loads the 
185    retention time windows where gaps need to be filled, plus a buffer 
186    (controlled by eic_buffer_time parameter) for complete EIC extraction. 
187    Multiple time ranges are automatically merged if they overlap.
188    
189    Parameters
190    ----------
191    name : str
192        Operation name
193    expand_on_miss : bool, optional
194        If True, expands search window when no peak is found. Default is False.
195        
196    Notes
197    -----
198    Requires that add_consensus_mass_features() has been run on the collection.
199    This operation loads raw MS1 data which will be available for subsequent operations.
200    Time range filtering significantly reduces memory usage and loading time for
201    large datasets with sparse gaps.
202    """
203    
204    @property
205    def description(self):
206        """Human-readable description for progress messages."""
207        return "gap-filling"
208    
209    def needs_raw_ms_data(self):
210        """This operation needs raw MS1 data."""
211        return True, 1
212    
213    def can_execute(self, sample, collection):
214        """Check if cluster summary exists."""
215        return hasattr(collection, 'cluster_summary_dataframe') and \
216               collection.cluster_summary_dataframe is not None
217    
218    def execute(self, sample_id, collection, **runtime_params):
219        """
220        Execute gap-filling for a single sample.
221        
222        Parameters
223        ----------
224        sample_id : int
225            Sample index to process
226        collection : LCMSBaseCollection
227            The collection
228        **runtime_params
229            Runtime parameters including:
230            - missingdf : pd.DataFrame - Cluster information and missing samples (optional)
231            - cluster_dict : dict - Cluster feature dictionary (optional)
232            - expand_on_miss : bool - Whether to expand search window on miss (optional)
233            If these are not provided, returns empty dict (no gaps to fill).
234            
235        Returns
236        -------
237        dict
238            Dictionary of induced mass features (empty if no gaps to fill)
239        """
240        # Extract gap-fill parameters from runtime_params
241        # If not present, there are no gaps to fill, so return early
242        if 'missingdf' not in runtime_params:
243            return {}
244        
245        missingdf = runtime_params['missingdf']
246        cluster_dict = runtime_params['cluster_dict']
247        expand_on_miss = runtime_params['expand_on_miss']
248        
249        # This is essentially the same logic as _search_for_targeted_mass_features_in_sample
250        # but extracted into an operation
251        
252        # Get clusters missing data for this sample
253        sampledf = missingdf[
254            missingdf.missing_samples.apply(lambda x: sample_id in x)
255        ].reset_index(drop=True).copy()
256
257        # Skip if no missing features for this sample
258        if len(sampledf) == 0:
259            return {}
260
261        # Get buffer time from LCMS parameters for EIC extraction
262        # This ensures we capture the full chromatographic peak beyond cluster bounds
263        buffer_rt = collection[sample_id].parameters.lc_ms.eic_buffer_time
264        
265        # Calculate time ranges for efficient loading with buffer for EIC extraction
266        time_ranges = []
267        
268        for _, row in sampledf.iterrows():
269            rt_min = row['scan_time_aligned_min']
270            rt_max = row['scan_time_aligned_max']
271            
272            # If expand_on_miss, also consider the allowed bounds
273            if expand_on_miss:
274                rt_min = min(rt_min, row['sta_min_allowed'])
275                rt_max = max(rt_max, row['sta_max_allowed'])
276            
277            # Apply buffer AFTER considering expand_on_miss bounds
278            # This ensures buffer is added beyond even the expanded search window
279            time_ranges.append((max(0, rt_min - buffer_rt), rt_max + buffer_rt))
280        
281        # Merge overlapping time ranges to reduce number of separate loads
282        time_ranges = sorted(time_ranges)
283        merged_ranges = []
284        if time_ranges:
285            current_min, current_max = time_ranges[0]
286            for rt_min, rt_max in time_ranges[1:]:
287                if rt_min <= current_max:  # Overlapping or adjacent
288                    current_max = max(current_max, rt_max)
289                else:
290                    merged_ranges.append((current_min, current_max))
291                    current_min, current_max = rt_min, rt_max
292            merged_ranges.append((current_min, current_max))
293
294        # Load raw data for this sample with time range filtering
295        collection.load_raw_data(sample_id, 1, time_range=merged_ranges)
296        
297        # Get MS1 data
298        ms1df = collection[sample_id]._ms_unprocessed[1].copy()
299        scan_df = collection[sample_id].scan_df[['scan', 'scan_time_aligned']]
300        ms1df = pd.merge(ms1df, scan_df, on='scan')
301
302        # Pre-extract all values from sampledf
303        clusters = sampledf.cluster.values
304        mz_mins = sampledf.mz_min.values
305        mz_maxs = sampledf.mz_max.values
306        st_mins = sampledf.scan_time_aligned_min.values
307        st_maxs = sampledf.scan_time_aligned_max.values
308        
309        if expand_on_miss:
310            mz_mins_allowed = sampledf.mz_min_allowed.values
311            mz_maxs_allowed = sampledf.mz_max_allowed.values
312            st_mins_allowed = sampledf.sta_min_allowed.values
313            st_maxs_allowed = sampledf.sta_max_allowed.values
314
315        # Pre-filter ms1df to reduce search space
316        mz_global_min = mz_mins.min()
317        mz_global_max = mz_maxs.max()
318        st_global_min = st_mins.min()
319        st_global_max = st_maxs.max()
320        
321        if expand_on_miss:
322            mz_global_min = min(mz_global_min, mz_mins_allowed.min())
323            mz_global_max = max(mz_global_max, mz_maxs_allowed.max())
324            st_global_min = min(st_global_min, st_mins_allowed.min())
325            st_global_max = max(st_global_max, st_maxs_allowed.max())
326        
327        ms1df_filtered = ms1df[
328            (ms1df.mz >= mz_global_min) & 
329            (ms1df.mz <= mz_global_max) &
330            (ms1df.scan_time_aligned >= st_global_min) &
331            (ms1df.scan_time_aligned <= st_global_max)
332        ].copy()
333
334        # Generate set_ids for all features
335        set_ids = [f'c{clusters[i]}_{i}_i' for i in range(len(sampledf))]
336        
337        # Use batch method to process all features at once
338        if expand_on_miss:
339            # First try with normal bounds
340            peaks_dict = collection[sample_id].search_for_targeted_mass_features_batch(
341                ms1df_filtered,
342                mz_mins,
343                mz_maxs,
344                st_mins,
345                st_maxs,
346                set_ids,
347                obj_idx=sample_id,
348                st_aligned=True
349            )
350            
351            # Retry failed features with expanded bounds
352            failed_indices = [i for i, sid in enumerate(set_ids) if peaks_dict[sid].apex_scan == -99]
353            if failed_indices:
354                failed_ids = [set_ids[i] for i in failed_indices]
355                retry_peaks = collection[sample_id].search_for_targeted_mass_features_batch(
356                    ms1df_filtered,
357                    mz_mins_allowed[failed_indices],
358                    mz_maxs_allowed[failed_indices],
359                    st_mins_allowed[failed_indices],
360                    st_maxs_allowed[failed_indices],
361                    failed_ids,
362                    obj_idx=sample_id,
363                    st_aligned=True
364                )
365                peaks_dict.update(retry_peaks)
366        else:
367            peaks_dict = collection[sample_id].search_for_targeted_mass_features_batch(
368                ms1df_filtered,
369                mz_mins,
370                mz_maxs,
371                st_mins,
372                st_maxs,
373                set_ids,
374                obj_idx=sample_id,
375                st_aligned=True
376            )
377        
378        # Build induced_mass_features dict and update cluster_dict
379        induced_mass_features = {}
380        for i in range(len(sampledf)):
381            peak = peaks_dict[set_ids[i]]
382            induced_mass_features[peak.id] = peak
383            cluster_dict[clusters[i]] += [set_ids[i]]
384        
385        # Integrate mass features (don't fail on bad integration)
386        collection[sample_id].induced_mass_features = induced_mass_features
387        collection[sample_id].integrate_mass_features(drop_if_fail=False, induced_features=True)
388        
389        # Add MS1 spectra and peak metrics to successfully detected induced features
390        # Only process features that were successfully detected (apex_scan != -99)
391        # This is critical for having m/z values in the pivot table for gap-filled features
392        successful_induced = {k: v for k, v in induced_mass_features.items() if v.apex_scan != -99}
393        
394        if len(successful_induced) > 0:
395            # Use the already-loaded raw data (use_parser=False) for efficiency
396            collection[sample_id].add_associated_ms1(
397                auto_process=True,
398                use_parser=False,
399                spectrum_mode=None,
400                induced_features=True
401            )
402        
403        # Return the induced features (some may have been filtered out)
404        return collection[sample_id].induced_mass_features
405        
406    def collect_results(self, sample_id, result, collection):
407        """Collect induced mass features back into sample."""
408        collection[sample_id].induced_mass_features = result

Gap-fill missing cluster features for a sample.

Searches raw MS1 data to find peaks in expected m/z and retention time windows for clusters that are present in other samples but missing from this sample.

Uses time range filtering for efficient data loading - only loads the retention time windows where gaps need to be filled, plus a buffer (controlled by eic_buffer_time parameter) for complete EIC extraction. Multiple time ranges are automatically merged if they overlap.

Parameters
  • name (str): Operation name
  • expand_on_miss (bool, optional): If True, expands search window when no peak is found. Default is False.
Notes

Requires that add_consensus_mass_features() has been run on the collection. This operation loads raw MS1 data which will be available for subsequent operations. Time range filtering significantly reduces memory usage and loading time for large datasets with sparse gaps.

description
204    @property
205    def description(self):
206        """Human-readable description for progress messages."""
207        return "gap-filling"

Human-readable description for progress messages.

def needs_raw_ms_data(self):
209    def needs_raw_ms_data(self):
210        """This operation needs raw MS1 data."""
211        return True, 1

This operation needs raw MS1 data.

def can_execute(self, sample, collection):
213    def can_execute(self, sample, collection):
214        """Check if cluster summary exists."""
215        return hasattr(collection, 'cluster_summary_dataframe') and \
216               collection.cluster_summary_dataframe is not None

Check if cluster summary exists.

def execute(self, sample_id, collection, **runtime_params):
218    def execute(self, sample_id, collection, **runtime_params):
219        """
220        Execute gap-filling for a single sample.
221        
222        Parameters
223        ----------
224        sample_id : int
225            Sample index to process
226        collection : LCMSBaseCollection
227            The collection
228        **runtime_params
229            Runtime parameters including:
230            - missingdf : pd.DataFrame - Cluster information and missing samples (optional)
231            - cluster_dict : dict - Cluster feature dictionary (optional)
232            - expand_on_miss : bool - Whether to expand search window on miss (optional)
233            If these are not provided, returns empty dict (no gaps to fill).
234            
235        Returns
236        -------
237        dict
238            Dictionary of induced mass features (empty if no gaps to fill)
239        """
240        # Extract gap-fill parameters from runtime_params
241        # If not present, there are no gaps to fill, so return early
242        if 'missingdf' not in runtime_params:
243            return {}
244        
245        missingdf = runtime_params['missingdf']
246        cluster_dict = runtime_params['cluster_dict']
247        expand_on_miss = runtime_params['expand_on_miss']
248        
249        # This is essentially the same logic as _search_for_targeted_mass_features_in_sample
250        # but extracted into an operation
251        
252        # Get clusters missing data for this sample
253        sampledf = missingdf[
254            missingdf.missing_samples.apply(lambda x: sample_id in x)
255        ].reset_index(drop=True).copy()
256
257        # Skip if no missing features for this sample
258        if len(sampledf) == 0:
259            return {}
260
261        # Get buffer time from LCMS parameters for EIC extraction
262        # This ensures we capture the full chromatographic peak beyond cluster bounds
263        buffer_rt = collection[sample_id].parameters.lc_ms.eic_buffer_time
264        
265        # Calculate time ranges for efficient loading with buffer for EIC extraction
266        time_ranges = []
267        
268        for _, row in sampledf.iterrows():
269            rt_min = row['scan_time_aligned_min']
270            rt_max = row['scan_time_aligned_max']
271            
272            # If expand_on_miss, also consider the allowed bounds
273            if expand_on_miss:
274                rt_min = min(rt_min, row['sta_min_allowed'])
275                rt_max = max(rt_max, row['sta_max_allowed'])
276            
277            # Apply buffer AFTER considering expand_on_miss bounds
278            # This ensures buffer is added beyond even the expanded search window
279            time_ranges.append((max(0, rt_min - buffer_rt), rt_max + buffer_rt))
280        
281        # Merge overlapping time ranges to reduce number of separate loads
282        time_ranges = sorted(time_ranges)
283        merged_ranges = []
284        if time_ranges:
285            current_min, current_max = time_ranges[0]
286            for rt_min, rt_max in time_ranges[1:]:
287                if rt_min <= current_max:  # Overlapping or adjacent
288                    current_max = max(current_max, rt_max)
289                else:
290                    merged_ranges.append((current_min, current_max))
291                    current_min, current_max = rt_min, rt_max
292            merged_ranges.append((current_min, current_max))
293
294        # Load raw data for this sample with time range filtering
295        collection.load_raw_data(sample_id, 1, time_range=merged_ranges)
296        
297        # Get MS1 data
298        ms1df = collection[sample_id]._ms_unprocessed[1].copy()
299        scan_df = collection[sample_id].scan_df[['scan', 'scan_time_aligned']]
300        ms1df = pd.merge(ms1df, scan_df, on='scan')
301
302        # Pre-extract all values from sampledf
303        clusters = sampledf.cluster.values
304        mz_mins = sampledf.mz_min.values
305        mz_maxs = sampledf.mz_max.values
306        st_mins = sampledf.scan_time_aligned_min.values
307        st_maxs = sampledf.scan_time_aligned_max.values
308        
309        if expand_on_miss:
310            mz_mins_allowed = sampledf.mz_min_allowed.values
311            mz_maxs_allowed = sampledf.mz_max_allowed.values
312            st_mins_allowed = sampledf.sta_min_allowed.values
313            st_maxs_allowed = sampledf.sta_max_allowed.values
314
315        # Pre-filter ms1df to reduce search space
316        mz_global_min = mz_mins.min()
317        mz_global_max = mz_maxs.max()
318        st_global_min = st_mins.min()
319        st_global_max = st_maxs.max()
320        
321        if expand_on_miss:
322            mz_global_min = min(mz_global_min, mz_mins_allowed.min())
323            mz_global_max = max(mz_global_max, mz_maxs_allowed.max())
324            st_global_min = min(st_global_min, st_mins_allowed.min())
325            st_global_max = max(st_global_max, st_maxs_allowed.max())
326        
327        ms1df_filtered = ms1df[
328            (ms1df.mz >= mz_global_min) & 
329            (ms1df.mz <= mz_global_max) &
330            (ms1df.scan_time_aligned >= st_global_min) &
331            (ms1df.scan_time_aligned <= st_global_max)
332        ].copy()
333
334        # Generate set_ids for all features
335        set_ids = [f'c{clusters[i]}_{i}_i' for i in range(len(sampledf))]
336        
337        # Use batch method to process all features at once
338        if expand_on_miss:
339            # First try with normal bounds
340            peaks_dict = collection[sample_id].search_for_targeted_mass_features_batch(
341                ms1df_filtered,
342                mz_mins,
343                mz_maxs,
344                st_mins,
345                st_maxs,
346                set_ids,
347                obj_idx=sample_id,
348                st_aligned=True
349            )
350            
351            # Retry failed features with expanded bounds
352            failed_indices = [i for i, sid in enumerate(set_ids) if peaks_dict[sid].apex_scan == -99]
353            if failed_indices:
354                failed_ids = [set_ids[i] for i in failed_indices]
355                retry_peaks = collection[sample_id].search_for_targeted_mass_features_batch(
356                    ms1df_filtered,
357                    mz_mins_allowed[failed_indices],
358                    mz_maxs_allowed[failed_indices],
359                    st_mins_allowed[failed_indices],
360                    st_maxs_allowed[failed_indices],
361                    failed_ids,
362                    obj_idx=sample_id,
363                    st_aligned=True
364                )
365                peaks_dict.update(retry_peaks)
366        else:
367            peaks_dict = collection[sample_id].search_for_targeted_mass_features_batch(
368                ms1df_filtered,
369                mz_mins,
370                mz_maxs,
371                st_mins,
372                st_maxs,
373                set_ids,
374                obj_idx=sample_id,
375                st_aligned=True
376            )
377        
378        # Build induced_mass_features dict and update cluster_dict
379        induced_mass_features = {}
380        for i in range(len(sampledf)):
381            peak = peaks_dict[set_ids[i]]
382            induced_mass_features[peak.id] = peak
383            cluster_dict[clusters[i]] += [set_ids[i]]
384        
385        # Integrate mass features (don't fail on bad integration)
386        collection[sample_id].induced_mass_features = induced_mass_features
387        collection[sample_id].integrate_mass_features(drop_if_fail=False, induced_features=True)
388        
389        # Add MS1 spectra and peak metrics to successfully detected induced features
390        # Only process features that were successfully detected (apex_scan != -99)
391        # This is critical for having m/z values in the pivot table for gap-filled features
392        successful_induced = {k: v for k, v in induced_mass_features.items() if v.apex_scan != -99}
393        
394        if len(successful_induced) > 0:
395            # Use the already-loaded raw data (use_parser=False) for efficiency
396            collection[sample_id].add_associated_ms1(
397                auto_process=True,
398                use_parser=False,
399                spectrum_mode=None,
400                induced_features=True
401            )
402        
403        # Return the induced features (some may have been filtered out)
404        return collection[sample_id].induced_mass_features

Execute gap-filling for a single sample.

Parameters
  • sample_id (int): Sample index to process
  • collection (LCMSBaseCollection): The collection
  • **runtime_params: Runtime parameters including:
    • missingdf : pd.DataFrame - Cluster information and missing samples (optional)
    • cluster_dict : dict - Cluster feature dictionary (optional)
    • expand_on_miss : bool - Whether to expand search window on miss (optional) If these are not provided, returns empty dict (no gaps to fill).
Returns
  • dict: Dictionary of induced mass features (empty if no gaps to fill)
def collect_results(self, sample_id, result, collection):
406    def collect_results(self, sample_id, result, collection):
407        """Collect induced mass features back into sample."""
408        collection[sample_id].induced_mass_features = result

Collect induced mass features back into sample.

class ReloadFeaturesOperation(SampleOperation):
411class ReloadFeaturesOperation(SampleOperation):
412    """
413    Reload mass features from HDF5 and optionally add MS1/MS2 spectra.
414    
415    This is useful when the collection was loaded with load_light=True,
416    which stores mass features only in the collection dataframe and not
417    as LCMSMassFeature objects in individual samples.
418    
419    Parameters
420    ----------
421    name : str
422        Operation name
423    add_ms1 : bool, optional
424        If True, adds MS1 spectra to mass features. Automatically uses raw MS1 data
425        if available (e.g., from gap-filling), otherwise uses parser. Spectrum mode
426        is auto-detected. Default is False.
427    add_ms2 : bool, optional
428        If True, also loads and associates MS2 spectra. Spectrum mode is auto-detected.
429        Default is False.
430    auto_process_ms2 : bool, optional
431        If True and add_ms2=True, auto-processes MS2 spectra. Default is True.
432    ms2_scan_filter : str or None, optional
433        Filter string for MS2 scans. Default is None.
434        
435    Notes
436    -----
437    MS1 spectra association automatically uses raw MS1 data if loaded by a previous
438    operation (e.g., GapFillOperation). This is efficient when multiple operations
439    need MS1 data in the same pipeline. All spectrum modes are auto-detected from
440    the data.
441    """
442    
443    @property
444    def description(self):
445        """Human-readable description for progress messages."""
446        return "reloading features"
447    
448    def needs_raw_ms_data(self):
449        """This operation doesn't need raw data."""
450        return False, None
451    
452    def can_execute(self, sample, collection):
453        """Check if collection parser is available."""
454        return hasattr(collection, 'collection_parser') and \
455               collection.collection_parser is not None
456    
457    def execute(self, sample_id, collection, mf_ids_to_load=None, **runtime_params):
458        """
459        Execute feature reloading for a single sample.
460        
461        Parameters
462        ----------
463        sample_id : int
464            Sample ID to reload features for
465        collection : LCMSBaseCollection
466            The collection
467        mf_ids_to_load : list of str, optional
468            List of collection-level mf_ids to load
469        **runtime_params
470            Additional runtime parameters (ignored)
471            
472        Returns
473        -------
474        dict
475            Dictionary of reloaded mass features
476        """
477        # Get parameters
478        add_ms1 = self.params.get('add_ms1', False)
479        add_ms2 = self.params.get('add_ms2', False)
480        auto_process_ms2 = self.params.get('auto_process_ms2', True)
481        ms2_scan_filter = self.params.get('ms2_scan_filter', None)
482        
483        sample = collection[sample_id]
484        sample_name = collection.samples[sample_id]
485        
486        # Auto-determine if we should use parser for MS1 (check if raw data is available)
487        has_raw_ms1 = 1 in sample._ms_unprocessed and not sample._ms_unprocessed[1].empty
488        use_parser_for_ms1 = not has_raw_ms1  # Use parser only if raw data not available
489        
490        # Spectrum modes will be auto-detected (None = auto-detect)
491        spectrum_mode_ms1 = None
492        ms2_spectrum_mode = None
493        
494        # Check if we have a collection parser
495        if not hasattr(collection, 'collection_parser') or collection.collection_parser is None:
496            print(f"Warning: Cannot reload mass features for {sample_name} - no collection_parser available")
497            return {}
498        
499        # Get the HDF5 file for this sample
500        hdf5_file = collection.collection_parser.folder_location / f"{sample_name}.corems/{sample_name}.hdf5"
501        
502        if not hdf5_file.exists():
503            print(f"Warning: HDF5 file not found for sample {sample_name}: {hdf5_file}")
504            return {}
505        
506        # Import here to avoid circular imports
507        from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectra
508        
509        # If specific mf_ids requested, extract the local mf_ids we need
510        local_mf_ids_to_load = None
511        if mf_ids_to_load is not None:
512            # mf_ids_to_load is already a list of sample-level mf_ids (integers)
513            # No parsing needed - they come from the mf_id column in the dataframe
514            if len(mf_ids_to_load) == 0:
515                # No features to load for this sample - return empty dict
516                return {}
517            local_mf_ids_to_load = set(mf_ids_to_load)
518        
519        # Reload mass features from HDF5
520        with ReadCoreMSHDFMassSpectra(hdf5_file) as parser:
521            parser.import_mass_features(sample, mf_ids=local_mf_ids_to_load)
522        
523        # If add_ms1, associate MS1 spectra with the loaded mass features
524        if add_ms1 and len(sample.mass_features) > 0:
525            # Check if raw MS1 data is already loaded (e.g., from gap-filling)
526            has_raw_ms1 = 1 in sample._ms_unprocessed and not sample._ms_unprocessed[1].empty
527            
528            if has_raw_ms1 and not use_parser_for_ms1:
529                # Use already-loaded raw data (more efficient)
530                sample.add_associated_ms1(
531                    auto_process=True,
532                    use_parser=False,
533                    spectrum_mode=spectrum_mode_ms1
534                )
535            else:
536                # Use parser to get MS1 spectra
537                sample.add_associated_ms1(
538                    auto_process=True,
539                    use_parser=True,
540                    spectrum_mode=spectrum_mode_ms1
541                )
542        
543        # If add_ms2, associate MS2 spectra with the loaded mass features
544        if add_ms2 and len(sample.mass_features) > 0:
545            # Get the IDs of loaded mass features (use what was actually loaded)
546            mf_ids_for_ms2 = list(sample.mass_features.keys())
547            
548            collection._associate_ms2_with_mass_features(
549                sample, 
550                mf_ids_for_ms2,
551                auto_process=auto_process_ms2,
552                spectrum_mode=ms2_spectrum_mode,
553                scan_filter=ms2_scan_filter
554            )
555        
556        # Return both mass_features and _ms so they can be collected in multiprocessing
557        return {'mass_features': sample.mass_features, '_ms': sample._ms}
558        
559    def collect_results(self, sample_id, result, collection):
560        """
561        Collect reloaded mass features back into sample.
562        
563        This operation loads a subset of mass features (e.g., representatives)
564        into sample.mass_features for processing, while preserving the full
565        mass_features_dataframe at the collection level. Sets a lock flag to
566        prevent automatic rebuilding of the collection dataframe from individual
567        samples. Also collects loaded mass spectra.
568        
569        Parameters
570        ----------
571        sample_id : int
572            Sample ID that was processed
573        result : dict
574            Dictionary with 'mass_features' and '_ms' from execute()
575        collection : LCMSBaseCollection
576            The collection
577        """
578        # Update sample.mass_features with loaded features
579        if isinstance(result, dict) and 'mass_features' in result:
580            collection[sample_id].mass_features = result['mass_features']
581            # Also collect the _ms dictionary (MS1 and MS2 spectra)
582            if '_ms' in result:
583                collection[sample_id]._ms.update(result['_ms'])
584        else:
585            # Backward compatibility - if result is just mass_features dict
586            collection[sample_id].mass_features = result
587        
588        # Lock the collection dataframe to prevent rebuilding from individual samples
589        # (since we've only loaded a subset, rebuilding would lose data)
590        collection._mass_features_locked = True

Reload mass features from HDF5 and optionally add MS1/MS2 spectra.

This is useful when the collection was loaded with load_light=True, which stores mass features only in the collection dataframe and not as LCMSMassFeature objects in individual samples.

Parameters
  • name (str): Operation name
  • add_ms1 (bool, optional): If True, adds MS1 spectra to mass features. Automatically uses raw MS1 data if available (e.g., from gap-filling), otherwise uses parser. Spectrum mode is auto-detected. Default is False.
  • add_ms2 (bool, optional): If True, also loads and associates MS2 spectra. Spectrum mode is auto-detected. Default is False.
  • auto_process_ms2 (bool, optional): If True and add_ms2=True, auto-processes MS2 spectra. Default is True.
  • ms2_scan_filter (str or None, optional): Filter string for MS2 scans. Default is None.
Notes

MS1 spectra association automatically uses raw MS1 data if loaded by a previous operation (e.g., GapFillOperation). This is efficient when multiple operations need MS1 data in the same pipeline. All spectrum modes are auto-detected from the data.

description
443    @property
444    def description(self):
445        """Human-readable description for progress messages."""
446        return "reloading features"

Human-readable description for progress messages.

def needs_raw_ms_data(self):
448    def needs_raw_ms_data(self):
449        """This operation doesn't need raw data."""
450        return False, None

This operation doesn't need raw data.

def can_execute(self, sample, collection):
452    def can_execute(self, sample, collection):
453        """Check if collection parser is available."""
454        return hasattr(collection, 'collection_parser') and \
455               collection.collection_parser is not None

Check if collection parser is available.

def execute(self, sample_id, collection, mf_ids_to_load=None, **runtime_params):
457    def execute(self, sample_id, collection, mf_ids_to_load=None, **runtime_params):
458        """
459        Execute feature reloading for a single sample.
460        
461        Parameters
462        ----------
463        sample_id : int
464            Sample ID to reload features for
465        collection : LCMSBaseCollection
466            The collection
467        mf_ids_to_load : list of str, optional
468            List of collection-level mf_ids to load
469        **runtime_params
470            Additional runtime parameters (ignored)
471            
472        Returns
473        -------
474        dict
475            Dictionary of reloaded mass features
476        """
477        # Get parameters
478        add_ms1 = self.params.get('add_ms1', False)
479        add_ms2 = self.params.get('add_ms2', False)
480        auto_process_ms2 = self.params.get('auto_process_ms2', True)
481        ms2_scan_filter = self.params.get('ms2_scan_filter', None)
482        
483        sample = collection[sample_id]
484        sample_name = collection.samples[sample_id]
485        
486        # Auto-determine if we should use parser for MS1 (check if raw data is available)
487        has_raw_ms1 = 1 in sample._ms_unprocessed and not sample._ms_unprocessed[1].empty
488        use_parser_for_ms1 = not has_raw_ms1  # Use parser only if raw data not available
489        
490        # Spectrum modes will be auto-detected (None = auto-detect)
491        spectrum_mode_ms1 = None
492        ms2_spectrum_mode = None
493        
494        # Check if we have a collection parser
495        if not hasattr(collection, 'collection_parser') or collection.collection_parser is None:
496            print(f"Warning: Cannot reload mass features for {sample_name} - no collection_parser available")
497            return {}
498        
499        # Get the HDF5 file for this sample
500        hdf5_file = collection.collection_parser.folder_location / f"{sample_name}.corems/{sample_name}.hdf5"
501        
502        if not hdf5_file.exists():
503            print(f"Warning: HDF5 file not found for sample {sample_name}: {hdf5_file}")
504            return {}
505        
506        # Import here to avoid circular imports
507        from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectra
508        
509        # If specific mf_ids requested, extract the local mf_ids we need
510        local_mf_ids_to_load = None
511        if mf_ids_to_load is not None:
512            # mf_ids_to_load is already a list of sample-level mf_ids (integers)
513            # No parsing needed - they come from the mf_id column in the dataframe
514            if len(mf_ids_to_load) == 0:
515                # No features to load for this sample - return empty dict
516                return {}
517            local_mf_ids_to_load = set(mf_ids_to_load)
518        
519        # Reload mass features from HDF5
520        with ReadCoreMSHDFMassSpectra(hdf5_file) as parser:
521            parser.import_mass_features(sample, mf_ids=local_mf_ids_to_load)
522        
523        # If add_ms1, associate MS1 spectra with the loaded mass features
524        if add_ms1 and len(sample.mass_features) > 0:
525            # Check if raw MS1 data is already loaded (e.g., from gap-filling)
526            has_raw_ms1 = 1 in sample._ms_unprocessed and not sample._ms_unprocessed[1].empty
527            
528            if has_raw_ms1 and not use_parser_for_ms1:
529                # Use already-loaded raw data (more efficient)
530                sample.add_associated_ms1(
531                    auto_process=True,
532                    use_parser=False,
533                    spectrum_mode=spectrum_mode_ms1
534                )
535            else:
536                # Use parser to get MS1 spectra
537                sample.add_associated_ms1(
538                    auto_process=True,
539                    use_parser=True,
540                    spectrum_mode=spectrum_mode_ms1
541                )
542        
543        # If add_ms2, associate MS2 spectra with the loaded mass features
544        if add_ms2 and len(sample.mass_features) > 0:
545            # Get the IDs of loaded mass features (use what was actually loaded)
546            mf_ids_for_ms2 = list(sample.mass_features.keys())
547            
548            collection._associate_ms2_with_mass_features(
549                sample, 
550                mf_ids_for_ms2,
551                auto_process=auto_process_ms2,
552                spectrum_mode=ms2_spectrum_mode,
553                scan_filter=ms2_scan_filter
554            )
555        
556        # Return both mass_features and _ms so they can be collected in multiprocessing
557        return {'mass_features': sample.mass_features, '_ms': sample._ms}

Execute feature reloading for a single sample.

Parameters
  • sample_id (int): Sample ID to reload features for
  • collection (LCMSBaseCollection): The collection
  • mf_ids_to_load (list of str, optional): List of collection-level mf_ids to load
  • **runtime_params: Additional runtime parameters (ignored)
Returns
  • dict: Dictionary of reloaded mass features
def collect_results(self, sample_id, result, collection):
559    def collect_results(self, sample_id, result, collection):
560        """
561        Collect reloaded mass features back into sample.
562        
563        This operation loads a subset of mass features (e.g., representatives)
564        into sample.mass_features for processing, while preserving the full
565        mass_features_dataframe at the collection level. Sets a lock flag to
566        prevent automatic rebuilding of the collection dataframe from individual
567        samples. Also collects loaded mass spectra.
568        
569        Parameters
570        ----------
571        sample_id : int
572            Sample ID that was processed
573        result : dict
574            Dictionary with 'mass_features' and '_ms' from execute()
575        collection : LCMSBaseCollection
576            The collection
577        """
578        # Update sample.mass_features with loaded features
579        if isinstance(result, dict) and 'mass_features' in result:
580            collection[sample_id].mass_features = result['mass_features']
581            # Also collect the _ms dictionary (MS1 and MS2 spectra)
582            if '_ms' in result:
583                collection[sample_id]._ms.update(result['_ms'])
584        else:
585            # Backward compatibility - if result is just mass_features dict
586            collection[sample_id].mass_features = result
587        
588        # Lock the collection dataframe to prevent rebuilding from individual samples
589        # (since we've only loaded a subset, rebuilding would lose data)
590        collection._mass_features_locked = True

Collect reloaded mass features back into sample.

This operation loads a subset of mass features (e.g., representatives) into sample.mass_features for processing, while preserving the full mass_features_dataframe at the collection level. Sets a lock flag to prevent automatic rebuilding of the collection dataframe from individual samples. Also collects loaded mass spectra.

Parameters
  • sample_id (int): Sample ID that was processed
  • result (dict): Dictionary with 'mass_features' and '_ms' from execute()
  • collection (LCMSBaseCollection): The collection
class MolecularFormulaSearchOperation(SampleOperation):
593class MolecularFormulaSearchOperation(SampleOperation):
594    """
595    Perform molecular formula search on mass features using associated MS1 spectra.
596    
597    This operation runs molecular formula search on all mass features in a sample
598    that have associated MS1 spectra. Requires MS1 spectra to be loaded and
599    processed before execution.
600    
601    Parameters
602    ----------
603    name : str
604        Operation name (for logging)
605    **kwargs
606        Additional parameters passed to parent class
607        
608    Examples
609    --------
610    >>> op = MolecularFormulaSearchOperation('mf_search')
611    >>> # Use in pipeline
612    >>> results = collection.process_samples_pipeline([op])
613    
614    Notes
615    -----
616    This operation requires that MS1 spectra have been associated with mass
617    features (e.g., via ReloadFeaturesOperation with add_ms1=True). The
618    molecular formula search uses parameters from the collection's 
619    parameters.mass_spectrum["ms1"].molecular_search settings.
620    """
621    
622    @property
623    def description(self):
624        """Human-readable description for progress messages."""
625        return "molecular formula search"
626    
627    def __init__(self, name='molecular_formula_search', **kwargs):
628        super().__init__(name, **kwargs)
629    
630    def needs_raw_ms_data(self):
631        """
632        This operation doesn't need raw data - it works on processed MS1 spectra
633        that are already associated with mass features.
634        
635        Returns
636        -------
637        tuple
638            (False, None) - no raw data needed
639        """
640        return False, None
641    
642    def can_execute(self, sample, collection, **runtime_params):
643        """
644        Check if molecular formula search can be executed.
645        
646        Requires that the sample has mass features with associated MS1 spectra.
647        
648        Parameters
649        ----------
650        sample : LCMSObject
651            The sample object
652        collection : LCMSCollection
653            The collection containing the sample
654        **runtime_params
655            Runtime parameters (not used)
656            
657        Returns
658        -------
659        bool
660            True if sample has mass features with MS1 spectra
661        """        
662        # Check if sample has mass features
663        if not hasattr(sample, 'mass_features') or not sample.mass_features:
664            return False
665        
666        # Check if at least some mass features have MS1 spectra
667        has_ms1 = any(
668            hasattr(mf, 'mass_spectrum') and mf.mass_spectrum is not None
669            for mf in sample.mass_features.values()
670        )
671        
672        return has_ms1
673    
674    def execute(self, sample_id, collection, **runtime_params):
675        """
676        Execute molecular formula search on a sample.
677        
678        Creates a SearchMolecularFormulasLC object and runs mass feature search,
679        which annotates mass features with molecular formula assignments.
680        
681        Parameters
682        ----------
683        sample_id : str
684            Sample identifier
685        collection : LCMSCollection
686            The collection containing the sample
687        **runtime_params
688            Runtime parameters (not used)
689            
690        Returns
691        -------
692        int
693            Number of mass features that were searched
694        """
695        from corems.molecular_id.search.molecularFormulaSearch import SearchMolecularFormulasLC
696        import time
697        import sqlalchemy.exc
698        import sqlite3
699        
700        sample = collection[sample_id]
701        
702        # Verify that mass features exist
703        if not hasattr(sample, 'mass_features') or not sample.mass_features:
704            return 0  # No mass features to search
705        
706        # Verify that mass features have MS1 spectra associated
707        if not hasattr(sample, '_ms') or not sample._ms:
708            raise RuntimeError(
709                f"Sample {sample_id} does not have MS1 spectra loaded in _ms dictionary. "
710                "Molecular formula search requires MS1 spectra to be associated with mass features. "
711                "Ensure add_ms1=True when reloading features."
712            )
713        
714        # Prepare data for bulk molecular formula search
715        # Group mass features by their apex scan
716        scan_to_mf = {}
717        for mf_id, mf in sample.mass_features.items():
718            apex_scan = mf.apex_scan
719            if apex_scan not in scan_to_mf:
720                scan_to_mf[apex_scan] = []
721            scan_to_mf[apex_scan].append(mf)
722        
723        # Build lists of mass spectra and corresponding peaks
724        mass_spectrum_list = []
725        ms_peaks_list = []
726        
727        for scan_num, mf_list in scan_to_mf.items():
728            # Get the mass spectrum for this scan
729            if scan_num not in sample._ms:
730                continue  # Skip if spectrum not loaded
731                
732            mass_spectrum = sample._ms[scan_num]
733            
734            # Verify spectrum is processed (has peaks)
735            if not hasattr(mass_spectrum, '_mspeaks') or not mass_spectrum._mspeaks:
736                continue  # Skip unprocessed spectra
737            
738            # Get the MS1 peaks for each mass feature at this scan
739            peaks_for_scan = []
740            for mf in mf_list:
741                try:
742                    # Use the ms1_peak property which finds the closest peak
743                    ms1_peak = mf.ms1_peak
744                    peaks_for_scan.append(ms1_peak)
745                except (AttributeError, IndexError):
746                    # Skip if ms1_peak can't be determined
747                    continue
748            
749            if peaks_for_scan:
750                mass_spectrum_list.append(mass_spectrum)
751                ms_peaks_list.append(peaks_for_scan)
752        
753        # Run molecular formula search if we have data, with retry logic for database locks
754        if mass_spectrum_list and ms_peaks_list:
755            max_retries = 10
756            retry_delay = 2  # seconds
757            
758            for attempt in range(max_retries):
759                try:
760                    mol_search = SearchMolecularFormulasLC(sample)
761                    mol_search.bulk_run_molecular_formula_search(mass_spectrum_list, ms_peaks_list)
762                    break  # Success, exit retry loop
763                except (sqlalchemy.exc.OperationalError, sqlite3.OperationalError) as e:
764                    if attempt < max_retries - 1:
765                        # Database is locked, retry after delay
766                        print(f"Sample {sample_id}: Database locked during molecular formula search, retrying in {retry_delay}s (attempt {attempt + 1}/{max_retries})...")
767                        time.sleep(retry_delay)
768                    else:
769                        # Max retries exceeded, re-raise the exception
770                        raise RuntimeError(
771                            f"Sample {sample_id}: Molecular formula search failed after {max_retries} attempts due to database lock. "
772                            "Try reducing parallel cores or increasing database timeout."
773                        ) from e
774        
775        # Return count of features searched
776        return len(sample.mass_features)
777    
778    def collect_results(self, sample_id, result, collection):
779        """
780        Collect results (no-op as search modifies mass features in place).
781        
782        The molecular formula search modifies mass features in place by adding
783        molecular formula assignments, so no explicit result collection is needed.
784        
785        Parameters
786        ----------
787        sample_id : str
788            Sample identifier
789        result : int
790            Number of features searched
791        collection : LCMSCollection
792            The collection containing the sample
793        """
794        # Search modifies mass features in place, nothing to collect
795        pass

Perform molecular formula search on mass features using associated MS1 spectra.

This operation runs molecular formula search on all mass features in a sample that have associated MS1 spectra. Requires MS1 spectra to be loaded and processed before execution.

Parameters
  • name (str): Operation name (for logging)
  • **kwargs: Additional parameters passed to parent class
Examples
>>> op = MolecularFormulaSearchOperation('mf_search')
>>> # Use in pipeline
>>> results = collection.process_samples_pipeline([op])
Notes

This operation requires that MS1 spectra have been associated with mass features (e.g., via ReloadFeaturesOperation with add_ms1=True). The molecular formula search uses parameters from the collection's parameters.mass_spectrum["ms1"].molecular_search settings.

MolecularFormulaSearchOperation(name='molecular_formula_search', **kwargs)
627    def __init__(self, name='molecular_formula_search', **kwargs):
628        super().__init__(name, **kwargs)
description
622    @property
623    def description(self):
624        """Human-readable description for progress messages."""
625        return "molecular formula search"

Human-readable description for progress messages.

def needs_raw_ms_data(self):
630    def needs_raw_ms_data(self):
631        """
632        This operation doesn't need raw data - it works on processed MS1 spectra
633        that are already associated with mass features.
634        
635        Returns
636        -------
637        tuple
638            (False, None) - no raw data needed
639        """
640        return False, None

This operation doesn't need raw data - it works on processed MS1 spectra that are already associated with mass features.

Returns
  • tuple: (False, None) - no raw data needed
def can_execute(self, sample, collection, **runtime_params):
642    def can_execute(self, sample, collection, **runtime_params):
643        """
644        Check if molecular formula search can be executed.
645        
646        Requires that the sample has mass features with associated MS1 spectra.
647        
648        Parameters
649        ----------
650        sample : LCMSObject
651            The sample object
652        collection : LCMSCollection
653            The collection containing the sample
654        **runtime_params
655            Runtime parameters (not used)
656            
657        Returns
658        -------
659        bool
660            True if sample has mass features with MS1 spectra
661        """        
662        # Check if sample has mass features
663        if not hasattr(sample, 'mass_features') or not sample.mass_features:
664            return False
665        
666        # Check if at least some mass features have MS1 spectra
667        has_ms1 = any(
668            hasattr(mf, 'mass_spectrum') and mf.mass_spectrum is not None
669            for mf in sample.mass_features.values()
670        )
671        
672        return has_ms1

Check if molecular formula search can be executed.

Requires that the sample has mass features with associated MS1 spectra.

Parameters
  • sample (LCMSObject): The sample object
  • collection (LCMSCollection): The collection containing the sample
  • **runtime_params: Runtime parameters (not used)
Returns
  • bool: True if sample has mass features with MS1 spectra
def execute(self, sample_id, collection, **runtime_params):
674    def execute(self, sample_id, collection, **runtime_params):
675        """
676        Execute molecular formula search on a sample.
677        
678        Creates a SearchMolecularFormulasLC object and runs mass feature search,
679        which annotates mass features with molecular formula assignments.
680        
681        Parameters
682        ----------
683        sample_id : str
684            Sample identifier
685        collection : LCMSCollection
686            The collection containing the sample
687        **runtime_params
688            Runtime parameters (not used)
689            
690        Returns
691        -------
692        int
693            Number of mass features that were searched
694        """
695        from corems.molecular_id.search.molecularFormulaSearch import SearchMolecularFormulasLC
696        import time
697        import sqlalchemy.exc
698        import sqlite3
699        
700        sample = collection[sample_id]
701        
702        # Verify that mass features exist
703        if not hasattr(sample, 'mass_features') or not sample.mass_features:
704            return 0  # No mass features to search
705        
706        # Verify that mass features have MS1 spectra associated
707        if not hasattr(sample, '_ms') or not sample._ms:
708            raise RuntimeError(
709                f"Sample {sample_id} does not have MS1 spectra loaded in _ms dictionary. "
710                "Molecular formula search requires MS1 spectra to be associated with mass features. "
711                "Ensure add_ms1=True when reloading features."
712            )
713        
714        # Prepare data for bulk molecular formula search
715        # Group mass features by their apex scan
716        scan_to_mf = {}
717        for mf_id, mf in sample.mass_features.items():
718            apex_scan = mf.apex_scan
719            if apex_scan not in scan_to_mf:
720                scan_to_mf[apex_scan] = []
721            scan_to_mf[apex_scan].append(mf)
722        
723        # Build lists of mass spectra and corresponding peaks
724        mass_spectrum_list = []
725        ms_peaks_list = []
726        
727        for scan_num, mf_list in scan_to_mf.items():
728            # Get the mass spectrum for this scan
729            if scan_num not in sample._ms:
730                continue  # Skip if spectrum not loaded
731                
732            mass_spectrum = sample._ms[scan_num]
733            
734            # Verify spectrum is processed (has peaks)
735            if not hasattr(mass_spectrum, '_mspeaks') or not mass_spectrum._mspeaks:
736                continue  # Skip unprocessed spectra
737            
738            # Get the MS1 peaks for each mass feature at this scan
739            peaks_for_scan = []
740            for mf in mf_list:
741                try:
742                    # Use the ms1_peak property which finds the closest peak
743                    ms1_peak = mf.ms1_peak
744                    peaks_for_scan.append(ms1_peak)
745                except (AttributeError, IndexError):
746                    # Skip if ms1_peak can't be determined
747                    continue
748            
749            if peaks_for_scan:
750                mass_spectrum_list.append(mass_spectrum)
751                ms_peaks_list.append(peaks_for_scan)
752        
753        # Run molecular formula search if we have data, with retry logic for database locks
754        if mass_spectrum_list and ms_peaks_list:
755            max_retries = 10
756            retry_delay = 2  # seconds
757            
758            for attempt in range(max_retries):
759                try:
760                    mol_search = SearchMolecularFormulasLC(sample)
761                    mol_search.bulk_run_molecular_formula_search(mass_spectrum_list, ms_peaks_list)
762                    break  # Success, exit retry loop
763                except (sqlalchemy.exc.OperationalError, sqlite3.OperationalError) as e:
764                    if attempt < max_retries - 1:
765                        # Database is locked, retry after delay
766                        print(f"Sample {sample_id}: Database locked during molecular formula search, retrying in {retry_delay}s (attempt {attempt + 1}/{max_retries})...")
767                        time.sleep(retry_delay)
768                    else:
769                        # Max retries exceeded, re-raise the exception
770                        raise RuntimeError(
771                            f"Sample {sample_id}: Molecular formula search failed after {max_retries} attempts due to database lock. "
772                            "Try reducing parallel cores or increasing database timeout."
773                        ) from e
774        
775        # Return count of features searched
776        return len(sample.mass_features)

Execute molecular formula search on a sample.

Creates a SearchMolecularFormulasLC object and runs mass feature search, which annotates mass features with molecular formula assignments.

Parameters
  • sample_id (str): Sample identifier
  • collection (LCMSCollection): The collection containing the sample
  • **runtime_params: Runtime parameters (not used)
Returns
  • int: Number of mass features that were searched
def collect_results(self, sample_id, result, collection):
778    def collect_results(self, sample_id, result, collection):
779        """
780        Collect results (no-op as search modifies mass features in place).
781        
782        The molecular formula search modifies mass features in place by adding
783        molecular formula assignments, so no explicit result collection is needed.
784        
785        Parameters
786        ----------
787        sample_id : str
788            Sample identifier
789        result : int
790            Number of features searched
791        collection : LCMSCollection
792            The collection containing the sample
793        """
794        # Search modifies mass features in place, nothing to collect
795        pass

Collect results (no-op as search modifies mass features in place).

The molecular formula search modifies mass features in place by adding molecular formula assignments, so no explicit result collection is needed.

Parameters
  • sample_id (str): Sample identifier
  • result (int): Number of features searched
  • collection (LCMSCollection): The collection containing the sample
Inherited Members
SampleOperation
name
params
class MS2SpectralSearchOperation(SampleOperation):
 798class MS2SpectralSearchOperation(SampleOperation):
 799    """
 800    Perform MS2 spectral search using entropy-based matching.
 801    
 802    This operation performs spectral library search on MS2 spectra associated
 803    with mass features using FlashEntropy for fast similarity scoring. Requires
 804    MS2 spectra to be loaded and processed before execution.
 805    
 806    Parameters
 807    ----------
 808    name : str
 809        Operation name (for logging)
 810    ms2_scan_filter : str or None, optional
 811        Filter string for MS2 scans (e.g., 'hcd'). If None, uses all MS2 scans.
 812        Default is None.
 813    peak_sep_da : float, optional
 814        Peak separation in Daltons for spectral matching. Default is 0.01.
 815    **kwargs
 816        Additional parameters passed to parent class
 817        
 818    Examples
 819    --------
 820    >>> op = MS2SpectralSearchOperation('ms2_search', ms2_scan_filter='hcd')
 821    >>> # Use in pipeline - requires fe_lib in runtime_params
 822    >>> results = collection.process_samples_pipeline([op])
 823    
 824    Notes
 825    -----
 826    This operation requires:
 827    - MS2 spectra to be associated with mass features
 828    - FlashEntropy library (fe_lib) to be provided in runtime_params
 829    - MS2 spectra must be processed (centroided)
 830    
 831    The spectral search modifies mass features in place by adding spectral
 832    match scores and metadata.
 833    """
 834    
 835    @property
 836    def description(self):
 837        """Human-readable description for progress messages."""
 838        return "MS2 spectral search"
 839    
 840    def __init__(self, name='ms2_spectral_search', ms2_scan_filter=None, **kwargs):
 841        super().__init__(name, **kwargs)
 842        self.params['ms2_scan_filter'] = ms2_scan_filter
 843    
 844    def needs_raw_ms_data(self):
 845        """
 846        This operation doesn't need raw data - it works on processed MS2 spectra
 847        that are already associated with mass features.
 848        
 849        Returns
 850        -------
 851        tuple
 852            (False, None) - no raw data needed
 853        """
 854        return False, None
 855    
 856    def can_execute(self, sample, collection, **runtime_params):
 857        """
 858        Check if MS2 spectral search can be executed.
 859        
 860        Requires that the sample has mass features with MS2 spectra associated.
 861        
 862        Parameters
 863        ----------
 864        sample : LCMSObject
 865            The sample object
 866        collection : LCMSCollection
 867            The collection containing the sample
 868        **runtime_params
 869            Runtime parameters (not used)
 870            
 871        Returns
 872        -------
 873        bool
 874            True if sample has mass features with MS2 spectra
 875        """        
 876        # Check if sample has mass features
 877        if not hasattr(sample, 'mass_features') or not sample.mass_features:
 878            return False
 879        
 880        # Check if any mass features have MS2 spectra associated
 881        has_ms2 = any(
 882            hasattr(mf, 'ms2_mass_spectra') and mf.ms2_mass_spectra
 883            for mf in sample.mass_features.values()
 884        )
 885        
 886        return has_ms2
 887    
 888    def execute(self, sample_id, collection, fe_lib=None, molecular_metadata=None, **runtime_params):
 889        """
 890        Execute MS2 spectral search on a sample.
 891        
 892        Performs entropy-based spectral library search on all MS2 spectra
 893        in the sample that match the scan filter criteria.
 894        
 895        Parameters
 896        ----------
 897        sample_id : str
 898            Sample identifier
 899        collection : LCMSCollection
 900            The collection containing the sample
 901        fe_lib : FlashEntropy library
 902            Pre-computed FlashEntropy library for spectral matching
 903        molecular_metadata : pd.DataFrame, optional
 904            Metadata for molecules in the spectral library
 905        **runtime_params
 906            Runtime parameters (not used)
 907            
 908        Returns
 909        -------
 910        int
 911            Number of MS2 spectra searched
 912        """
 913        sample = collection[sample_id]
 914        
 915        # Get parameters
 916        ms2_scan_filter = self.params.get('ms2_scan_filter')
 917        
 918        # Verify that we have a spectral library
 919        if fe_lib is None:
 920            raise ValueError(
 921                f"Sample {sample_id}: MS2 spectral search requires fe_lib (FlashEntropy library) "
 922                "to be provided in runtime parameters. Create the library at the collection level "
 923                "and pass it to the pipeline."
 924            )
 925        
 926        # Extract peak_sep_da from FlashEntropy library configuration
 927        # peak_sep_da should be 2 * max_ms2_tolerance_in_da to match the min_ms2_difference_in_da
 928        # used when creating the library
 929        tolerance_da = fe_lib.entropy_search.max_ms2_tolerance_in_da
 930        if tolerance_da is None:
 931            raise ValueError(
 932                f"Sample {sample_id}: Could not extract max_ms2_tolerance_in_da from FlashEntropy library. "
 933                "Ensure the library was created with this parameter specified."
 934            )
 935        peak_sep_da = 2 * tolerance_da
 936        
 937        # Verify that sample has _ms dictionary
 938        if not hasattr(sample, '_ms') or not sample._ms:
 939            return 0  # No MS2 spectra to search
 940        
 941        # Get MS2 scan numbers based on filter
 942        if ms2_scan_filter is not None:
 943            # Filter by scan text
 944            ms2_scan_df = sample.scan_df[
 945                sample.scan_df.scan_text.str.contains(ms2_scan_filter) &
 946                (sample.scan_df.ms_level == 2)
 947            ]
 948        else:
 949            # All MS2 scans
 950            ms2_scan_df = sample.scan_df[sample.scan_df.ms_level == 2]
 951        
 952        # Get scans that are actually loaded in _ms
 953        ms2_scans_to_search = [
 954            scan for scan in ms2_scan_df.scan.tolist()
 955            if scan in sample._ms.keys()
 956        ]
 957        
 958        if not ms2_scans_to_search:
 959            return 0  # No MS2 spectra to search
 960        
 961        # Perform spectral search using the sample's fe_search method
 962        sample.fe_search(
 963            scan_list=ms2_scans_to_search,
 964            fe_lib=fe_lib,
 965            peak_sep_da=peak_sep_da
 966        )
 967        
 968        # Return the spectral search results for collection
 969        # (needed for multiprocessing - results populated in worker need to be returned)
 970        return sample.spectral_search_results
 971    
 972    def collect_results(self, sample_id, result, collection):
 973        """
 974        Collect spectral search results back into the sample.
 975        
 976        In multiprocessing, the worker's modifications don't persist to the
 977        main process, so we need to explicitly collect and reassign the results.
 978        This also re-associates the results with mass features.
 979        
 980        Parameters
 981        ----------
 982        sample_id : str
 983            Sample identifier
 984        result : dict
 985            Dictionary of spectral search results from execute()
 986        collection : LCMSCollection
 987            The collection containing the sample
 988        """
 989        # Assign the spectral search results back to the sample
 990        if result:
 991            collection[sample_id].spectral_search_results.update(result)
 992            
 993            # Re-associate results with mass features (same logic as fe_search)
 994            sample = collection[sample_id]
 995            if len(sample.mass_features) > 0:
 996                for mass_feature_id, mass_feature in sample.mass_features.items():
 997                    scan_ids = mass_feature.ms2_scan_numbers
 998                    for ms2_scan_id in scan_ids:
 999                        precursor_mz = mass_feature.mz
1000                        try:
1001                            sample.spectral_search_results[ms2_scan_id][precursor_mz]
1002                        except KeyError:
1003                            pass
1004                        else:
1005                            sample.mass_features[
1006                                mass_feature_id
1007                            ].ms2_similarity_results.append(
1008                                sample.spectral_search_results[ms2_scan_id][precursor_mz]
1009                            )

Perform MS2 spectral search using entropy-based matching.

This operation performs spectral library search on MS2 spectra associated with mass features using FlashEntropy for fast similarity scoring. Requires MS2 spectra to be loaded and processed before execution.

Parameters
  • name (str): Operation name (for logging)
  • ms2_scan_filter (str or None, optional): Filter string for MS2 scans (e.g., 'hcd'). If None, uses all MS2 scans. Default is None.
  • peak_sep_da (float, optional): Peak separation in Daltons for spectral matching. Default is 0.01.
  • **kwargs: Additional parameters passed to parent class
Examples
>>> op = MS2SpectralSearchOperation('ms2_search', ms2_scan_filter='hcd')
>>> # Use in pipeline - requires fe_lib in runtime_params
>>> results = collection.process_samples_pipeline([op])
Notes

This operation requires:

  • MS2 spectra to be associated with mass features
  • FlashEntropy library (fe_lib) to be provided in runtime_params
  • MS2 spectra must be processed (centroided)

The spectral search modifies mass features in place by adding spectral match scores and metadata.

MS2SpectralSearchOperation(name='ms2_spectral_search', ms2_scan_filter=None, **kwargs)
840    def __init__(self, name='ms2_spectral_search', ms2_scan_filter=None, **kwargs):
841        super().__init__(name, **kwargs)
842        self.params['ms2_scan_filter'] = ms2_scan_filter
description
835    @property
836    def description(self):
837        """Human-readable description for progress messages."""
838        return "MS2 spectral search"

Human-readable description for progress messages.

def needs_raw_ms_data(self):
844    def needs_raw_ms_data(self):
845        """
846        This operation doesn't need raw data - it works on processed MS2 spectra
847        that are already associated with mass features.
848        
849        Returns
850        -------
851        tuple
852            (False, None) - no raw data needed
853        """
854        return False, None

This operation doesn't need raw data - it works on processed MS2 spectra that are already associated with mass features.

Returns
  • tuple: (False, None) - no raw data needed
def can_execute(self, sample, collection, **runtime_params):
856    def can_execute(self, sample, collection, **runtime_params):
857        """
858        Check if MS2 spectral search can be executed.
859        
860        Requires that the sample has mass features with MS2 spectra associated.
861        
862        Parameters
863        ----------
864        sample : LCMSObject
865            The sample object
866        collection : LCMSCollection
867            The collection containing the sample
868        **runtime_params
869            Runtime parameters (not used)
870            
871        Returns
872        -------
873        bool
874            True if sample has mass features with MS2 spectra
875        """        
876        # Check if sample has mass features
877        if not hasattr(sample, 'mass_features') or not sample.mass_features:
878            return False
879        
880        # Check if any mass features have MS2 spectra associated
881        has_ms2 = any(
882            hasattr(mf, 'ms2_mass_spectra') and mf.ms2_mass_spectra
883            for mf in sample.mass_features.values()
884        )
885        
886        return has_ms2

Check if MS2 spectral search can be executed.

Requires that the sample has mass features with MS2 spectra associated.

Parameters
  • sample (LCMSObject): The sample object
  • collection (LCMSCollection): The collection containing the sample
  • **runtime_params: Runtime parameters (not used)
Returns
  • bool: True if sample has mass features with MS2 spectra
def execute( self, sample_id, collection, fe_lib=None, molecular_metadata=None, **runtime_params):
888    def execute(self, sample_id, collection, fe_lib=None, molecular_metadata=None, **runtime_params):
889        """
890        Execute MS2 spectral search on a sample.
891        
892        Performs entropy-based spectral library search on all MS2 spectra
893        in the sample that match the scan filter criteria.
894        
895        Parameters
896        ----------
897        sample_id : str
898            Sample identifier
899        collection : LCMSCollection
900            The collection containing the sample
901        fe_lib : FlashEntropy library
902            Pre-computed FlashEntropy library for spectral matching
903        molecular_metadata : pd.DataFrame, optional
904            Metadata for molecules in the spectral library
905        **runtime_params
906            Runtime parameters (not used)
907            
908        Returns
909        -------
910        int
911            Number of MS2 spectra searched
912        """
913        sample = collection[sample_id]
914        
915        # Get parameters
916        ms2_scan_filter = self.params.get('ms2_scan_filter')
917        
918        # Verify that we have a spectral library
919        if fe_lib is None:
920            raise ValueError(
921                f"Sample {sample_id}: MS2 spectral search requires fe_lib (FlashEntropy library) "
922                "to be provided in runtime parameters. Create the library at the collection level "
923                "and pass it to the pipeline."
924            )
925        
926        # Extract peak_sep_da from FlashEntropy library configuration
927        # peak_sep_da should be 2 * max_ms2_tolerance_in_da to match the min_ms2_difference_in_da
928        # used when creating the library
929        tolerance_da = fe_lib.entropy_search.max_ms2_tolerance_in_da
930        if tolerance_da is None:
931            raise ValueError(
932                f"Sample {sample_id}: Could not extract max_ms2_tolerance_in_da from FlashEntropy library. "
933                "Ensure the library was created with this parameter specified."
934            )
935        peak_sep_da = 2 * tolerance_da
936        
937        # Verify that sample has _ms dictionary
938        if not hasattr(sample, '_ms') or not sample._ms:
939            return 0  # No MS2 spectra to search
940        
941        # Get MS2 scan numbers based on filter
942        if ms2_scan_filter is not None:
943            # Filter by scan text
944            ms2_scan_df = sample.scan_df[
945                sample.scan_df.scan_text.str.contains(ms2_scan_filter) &
946                (sample.scan_df.ms_level == 2)
947            ]
948        else:
949            # All MS2 scans
950            ms2_scan_df = sample.scan_df[sample.scan_df.ms_level == 2]
951        
952        # Get scans that are actually loaded in _ms
953        ms2_scans_to_search = [
954            scan for scan in ms2_scan_df.scan.tolist()
955            if scan in sample._ms.keys()
956        ]
957        
958        if not ms2_scans_to_search:
959            return 0  # No MS2 spectra to search
960        
961        # Perform spectral search using the sample's fe_search method
962        sample.fe_search(
963            scan_list=ms2_scans_to_search,
964            fe_lib=fe_lib,
965            peak_sep_da=peak_sep_da
966        )
967        
968        # Return the spectral search results for collection
969        # (needed for multiprocessing - results populated in worker need to be returned)
970        return sample.spectral_search_results

Execute MS2 spectral search on a sample.

Performs entropy-based spectral library search on all MS2 spectra in the sample that match the scan filter criteria.

Parameters
  • sample_id (str): Sample identifier
  • collection (LCMSCollection): The collection containing the sample
  • fe_lib (FlashEntropy library): Pre-computed FlashEntropy library for spectral matching
  • molecular_metadata (pd.DataFrame, optional): Metadata for molecules in the spectral library
  • **runtime_params: Runtime parameters (not used)
Returns
  • int: Number of MS2 spectra searched
def collect_results(self, sample_id, result, collection):
 972    def collect_results(self, sample_id, result, collection):
 973        """
 974        Collect spectral search results back into the sample.
 975        
 976        In multiprocessing, the worker's modifications don't persist to the
 977        main process, so we need to explicitly collect and reassign the results.
 978        This also re-associates the results with mass features.
 979        
 980        Parameters
 981        ----------
 982        sample_id : str
 983            Sample identifier
 984        result : dict
 985            Dictionary of spectral search results from execute()
 986        collection : LCMSCollection
 987            The collection containing the sample
 988        """
 989        # Assign the spectral search results back to the sample
 990        if result:
 991            collection[sample_id].spectral_search_results.update(result)
 992            
 993            # Re-associate results with mass features (same logic as fe_search)
 994            sample = collection[sample_id]
 995            if len(sample.mass_features) > 0:
 996                for mass_feature_id, mass_feature in sample.mass_features.items():
 997                    scan_ids = mass_feature.ms2_scan_numbers
 998                    for ms2_scan_id in scan_ids:
 999                        precursor_mz = mass_feature.mz
1000                        try:
1001                            sample.spectral_search_results[ms2_scan_id][precursor_mz]
1002                        except KeyError:
1003                            pass
1004                        else:
1005                            sample.mass_features[
1006                                mass_feature_id
1007                            ].ms2_similarity_results.append(
1008                                sample.spectral_search_results[ms2_scan_id][precursor_mz]
1009                            )

Collect spectral search results back into the sample.

In multiprocessing, the worker's modifications don't persist to the main process, so we need to explicitly collect and reassign the results. This also re-associates the results with mass features.

Parameters
  • sample_id (str): Sample identifier
  • result (dict): Dictionary of spectral search results from execute()
  • collection (LCMSCollection): The collection containing the sample
Inherited Members
SampleOperation
name
params
class LoadEICsOperation(SampleOperation):
1012class LoadEICsOperation(SampleOperation):
1013    """
1014    Load extracted ion chromatograms (EICs) from HDF5 for regular mass features.
1015    
1016    Loads EICs for regular mass features that belong to consensus clusters from HDF5.
1017    Induced (gap-filled) features already have EICs from integrate_mass_features,
1018    so no additional loading is needed for them.
1019    
1020    This operation enables downstream visualization and analysis of chromatographic
1021    peaks across all samples in a cluster.
1022    
1023    Notes
1024    -----
1025    Requires that mass features have been loaded and cluster_index assigned.
1026    Regular mass feature EICs must have been previously saved to HDF5 with export_eics=True.
1027    Induced mass features already have EICs populated during gap-filling.
1028    """
1029    
1030    @property
1031    def description(self):
1032        """Human-readable description for progress messages."""
1033        return "loading EICs"
1034    
1035    def needs_raw_ms_data(self):
1036        """This operation doesn't need raw data - induced features already have EICs."""
1037        return False, None
1038    
1039    def can_execute(self, sample, collection):
1040        """
1041        Check if EIC loading can be executed.
1042        
1043        This operation can always execute if the sample exists - the actual work
1044        is determined by cluster_mz_dict in runtime_params. If cluster_mz_dict is
1045        empty or None, execute() will simply return 0 (no EICs loaded).
1046        
1047        Returns
1048        -------
1049        bool
1050            True (always executable - runtime_params control actual work)
1051        """
1052        return True
1053    
1054    def execute(self, sample_id, collection, cluster_mz_dict=None, **runtime_params):
1055        """
1056        Load EICs from HDF5 for a single sample.
1057        
1058        Loads EICs for regular mass features that belong to consensus clusters.
1059        Induced (gap-filled) mass features already have EICs from integrate_mass_features,
1060        so no additional loading is needed for them.
1061        
1062        The cluster_mz_dict parameter (passed from collection level) maps sample_id
1063        to a list of m/z values that belong to clusters for that sample.
1064        
1065        Parameters
1066        ----------
1067        sample_id : int
1068            Sample index to process
1069        collection : LCMSBaseCollection
1070            The collection
1071        cluster_mz_dict : dict, optional
1072            Dictionary mapping sample_id to list of m/z values in clusters for that sample.
1073            If None, will not load any EICs. Default is None.
1074        **runtime_params
1075            Additional runtime parameters (ignored)
1076            
1077        Returns
1078        -------
1079        dict
1080            Dictionary of loaded EIC_Data objects, keyed by m/z value
1081        """
1082        from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectra
1083        
1084        sample = collection[sample_id]
1085        
1086        # If no cluster info provided or no m/z values for this sample, return early
1087        if cluster_mz_dict is None or sample_id not in cluster_mz_dict:
1088            return {}
1089        
1090        # Get m/z values for this sample that belong to clusters
1091        sample_cluster_mz = set(cluster_mz_dict[sample_id])
1092        if len(sample_cluster_mz) == 0:
1093            return {}
1094
1095        # Load EICs for each of the sample_cluster_mz
1096        hdf5_path = sample.file_location
1097        if hdf5_path and hdf5_path.exists():
1098            reader = ReadCoreMSHDFMassSpectra(str(hdf5_path))
1099            reader.import_eics(sample, mz_list=list(sample_cluster_mz))
1100            # Return the loaded EICs for multiprocessing collection
1101            # (modifications in worker process don't persist to main process)
1102            return sample.eics.copy()
1103        
1104        return {}
1105    
1106    def collect_results(self, sample_id, result, collection):
1107        """
1108        Collect loaded EICs back into sample.
1109        
1110        In multiprocessing, the worker's modifications don't persist to the
1111        main process, so we need to explicitly collect and reassign the EICs.
1112        This also re-associates EICs with mass features.
1113        
1114        Parameters
1115        ----------
1116        sample_id : int
1117            Sample ID that was processed
1118        result : dict
1119            Dictionary of EIC_Data objects keyed by m/z, returned from execute()
1120        collection : LCMSBaseCollection
1121            The collection
1122        """
1123        if result:
1124            # Update sample.eics with loaded EICs
1125            collection[sample_id].eics.update(result)
1126            # Note: EIC association with mass features happens after pipeline completes
1127            # to avoid multiprocessing issues (modifications in worker processes don't
1128            # persist to main process objects)

Load extracted ion chromatograms (EICs) from HDF5 for regular mass features.

Loads EICs for regular mass features that belong to consensus clusters from HDF5. Induced (gap-filled) features already have EICs from integrate_mass_features, so no additional loading is needed for them.

This operation enables downstream visualization and analysis of chromatographic peaks across all samples in a cluster.

Notes

Requires that mass features have been loaded and cluster_index assigned. Regular mass feature EICs must have been previously saved to HDF5 with export_eics=True. Induced mass features already have EICs populated during gap-filling.

description
1030    @property
1031    def description(self):
1032        """Human-readable description for progress messages."""
1033        return "loading EICs"

Human-readable description for progress messages.

def needs_raw_ms_data(self):
1035    def needs_raw_ms_data(self):
1036        """This operation doesn't need raw data - induced features already have EICs."""
1037        return False, None

This operation doesn't need raw data - induced features already have EICs.

def can_execute(self, sample, collection):
1039    def can_execute(self, sample, collection):
1040        """
1041        Check if EIC loading can be executed.
1042        
1043        This operation can always execute if the sample exists - the actual work
1044        is determined by cluster_mz_dict in runtime_params. If cluster_mz_dict is
1045        empty or None, execute() will simply return 0 (no EICs loaded).
1046        
1047        Returns
1048        -------
1049        bool
1050            True (always executable - runtime_params control actual work)
1051        """
1052        return True

Check if EIC loading can be executed.

This operation can always execute if the sample exists - the actual work is determined by cluster_mz_dict in runtime_params. If cluster_mz_dict is empty or None, execute() will simply return 0 (no EICs loaded).

Returns
  • bool: True (always executable - runtime_params control actual work)
def execute(self, sample_id, collection, cluster_mz_dict=None, **runtime_params):
1054    def execute(self, sample_id, collection, cluster_mz_dict=None, **runtime_params):
1055        """
1056        Load EICs from HDF5 for a single sample.
1057        
1058        Loads EICs for regular mass features that belong to consensus clusters.
1059        Induced (gap-filled) mass features already have EICs from integrate_mass_features,
1060        so no additional loading is needed for them.
1061        
1062        The cluster_mz_dict parameter (passed from collection level) maps sample_id
1063        to a list of m/z values that belong to clusters for that sample.
1064        
1065        Parameters
1066        ----------
1067        sample_id : int
1068            Sample index to process
1069        collection : LCMSBaseCollection
1070            The collection
1071        cluster_mz_dict : dict, optional
1072            Dictionary mapping sample_id to list of m/z values in clusters for that sample.
1073            If None, will not load any EICs. Default is None.
1074        **runtime_params
1075            Additional runtime parameters (ignored)
1076            
1077        Returns
1078        -------
1079        dict
1080            Dictionary of loaded EIC_Data objects, keyed by m/z value
1081        """
1082        from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectra
1083        
1084        sample = collection[sample_id]
1085        
1086        # If no cluster info provided or no m/z values for this sample, return early
1087        if cluster_mz_dict is None or sample_id not in cluster_mz_dict:
1088            return {}
1089        
1090        # Get m/z values for this sample that belong to clusters
1091        sample_cluster_mz = set(cluster_mz_dict[sample_id])
1092        if len(sample_cluster_mz) == 0:
1093            return {}
1094
1095        # Load EICs for each of the sample_cluster_mz
1096        hdf5_path = sample.file_location
1097        if hdf5_path and hdf5_path.exists():
1098            reader = ReadCoreMSHDFMassSpectra(str(hdf5_path))
1099            reader.import_eics(sample, mz_list=list(sample_cluster_mz))
1100            # Return the loaded EICs for multiprocessing collection
1101            # (modifications in worker process don't persist to main process)
1102            return sample.eics.copy()
1103        
1104        return {}

Load EICs from HDF5 for a single sample.

Loads EICs for regular mass features that belong to consensus clusters. Induced (gap-filled) mass features already have EICs from integrate_mass_features, so no additional loading is needed for them.

The cluster_mz_dict parameter (passed from collection level) maps sample_id to a list of m/z values that belong to clusters for that sample.

Parameters
  • sample_id (int): Sample index to process
  • collection (LCMSBaseCollection): The collection
  • cluster_mz_dict (dict, optional): Dictionary mapping sample_id to list of m/z values in clusters for that sample. If None, will not load any EICs. Default is None.
  • **runtime_params: Additional runtime parameters (ignored)
Returns
  • dict: Dictionary of loaded EIC_Data objects, keyed by m/z value
def collect_results(self, sample_id, result, collection):
1106    def collect_results(self, sample_id, result, collection):
1107        """
1108        Collect loaded EICs back into sample.
1109        
1110        In multiprocessing, the worker's modifications don't persist to the
1111        main process, so we need to explicitly collect and reassign the EICs.
1112        This also re-associates EICs with mass features.
1113        
1114        Parameters
1115        ----------
1116        sample_id : int
1117            Sample ID that was processed
1118        result : dict
1119            Dictionary of EIC_Data objects keyed by m/z, returned from execute()
1120        collection : LCMSBaseCollection
1121            The collection
1122        """
1123        if result:
1124            # Update sample.eics with loaded EICs
1125            collection[sample_id].eics.update(result)
1126            # Note: EIC association with mass features happens after pipeline completes
1127            # to avoid multiprocessing issues (modifications in worker processes don't
1128            # persist to main process objects)

Collect loaded EICs back into sample.

In multiprocessing, the worker's modifications don't persist to the main process, so we need to explicitly collect and reassign the EICs. This also re-associates EICs with mass features.

Parameters
  • sample_id (int): Sample ID that was processed
  • result (dict): Dictionary of EIC_Data objects keyed by m/z, returned from execute()
  • collection (LCMSBaseCollection): The collection