From e8dd955ec035cd0e10a61cf73775e4a8a448230b Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 12 Aug 2024 12:07:07 -0700 Subject: [PATCH 001/158] Start of LC Collection classes --- corems/mass_spectra/factory/lc_collection.py | 10 +++++++++ corems/mass_spectra/input/corems_hdf5.py | 22 ++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 corems/mass_spectra/factory/lc_collection.py diff --git a/corems/mass_spectra/factory/lc_collection.py b/corems/mass_spectra/factory/lc_collection.py new file mode 100644 index 000000000..791ad950e --- /dev/null +++ b/corems/mass_spectra/factory/lc_collection.py @@ -0,0 +1,10 @@ +class MassSpectraCollectionBase: + """Base class for a collection of MassSpectra objects. + + Attributes + ---------- + _mass_spectra : list + A list of MassSpectraBase objects. + """ + def __init__(self): + self._mass_spectra = [] \ No newline at end of file diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 8b30d7a71..363f73ee8 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -444,3 +444,25 @@ def get_lcms_obj(self) -> LCMSBase: lcms_obj._tic_list = list(lcms_obj.scan_df.tic) return lcms_obj + +class ReadCoreMSHDFMassSpectraCollection( + ReadCoreMSHDFMassSpectra +): + def __init__( + self, + folder_location: str, + manifest_file: str + ): + self.manifest = self._parse_manifest(manifest_file) + + def _parse_manifest(self, manifest_file): + manifest = pd.read_csv(manifest_file) + self._manifest_dict = manifest.to_dict(orient='records') + + @property + def manifest(self): + return self._manifest_dict + + + + \ No newline at end of file From 6b264e93fe1a706a31c144e0f38e885f2dc905df Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 12 Aug 2024 15:09:21 -0700 Subject: [PATCH 002/158] Improve handling of molecular formula parsing --- corems/mass_spectra/output/export.py | 3 -- corems/mass_spectrum/input/coremsHDF5.py | 21 ++++++-------- corems/mass_spectrum/input/massList.py | 28 ++++++++++++------- .../factory/MolecularFormulaFactory.py | 26 ++++++++++++----- .../nmdc/lipidomics/lipidomics_collection.py | 11 ++++++++ .../nmdc/lipidomics/lipidomics_workflow.py | 6 ++-- 6 files changed, 60 insertions(+), 35 deletions(-) create mode 100644 support_code/nmdc/lipidomics/lipidomics_collection.py diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index 332b95f7b..9e3ea11a3 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -1284,9 +1284,6 @@ def get_ion_formula(neutral_formula, ion_type): # If neutral_formula is not a string, return None if not isinstance(neutral_formula, str): return None - - if "Cl" in neutral_formula: - print("Cl in neutral formula") # Check if there are spaces in the formula (these are outputs of the MolecularFormula class and do not need to be processed before being passed to the class) if re.search(r"\s", neutral_formula): diff --git a/corems/mass_spectrum/input/coremsHDF5.py b/corems/mass_spectrum/input/coremsHDF5.py index 54f3de720..26816e9ba 100644 --- a/corems/mass_spectrum/input/coremsHDF5.py +++ b/corems/mass_spectrum/input/coremsHDF5.py @@ -123,17 +123,6 @@ def get_mass_spectrum( else: scan_index = self.scans.index(str(scan_number)) dataframe = self.get_dataframe(scan_index, time_index=time_index) - if dataframe["Molecular Formula"].any() and not dataframe["C"].any(): - #TODO KRH: figure out why this is necessary - cols = dataframe.columns.tolist() - cols = cols[cols.index("Molecular Formula") + 1 :] - for index, row in dataframe.iterrows(): - if row["Molecular Formula"] is not None: - og_formula = row["Molecular Formula"] - for col in cols: - if "col" in og_formula: - # get the digit after the element ("col") in the molecular formula and set it to the dataframe - row[col] = int(og_formula.split(col)[1].split(" ")[0]) if not set( ["H/C", "O/C", "Heteroatom Class", "Ion Type", "Is Isotopologue"] @@ -257,7 +246,15 @@ def get_dataframe(self, scan_index=0, time_index=-1): list_dict.append(data_dict) - return DataFrame(list_dict) + # Reorder the columns from low to high "Index" to match the order of the dataframe + df = DataFrame(list_dict) + # set the "Index" column to int so it sorts correctly + df["Index"] = df["Index"].astype(int) + df = df.sort_values(by="Index") + # Reset index to match the "Index" column + df = df.set_index("Index", drop=False) + + return df def get_time_index_to_pull(self, scan_label, time_index): """ diff --git a/corems/mass_spectrum/input/massList.py b/corems/mass_spectrum/input/massList.py index be3f2bf4f..d4608d4fa 100644 --- a/corems/mass_spectrum/input/massList.py +++ b/corems/mass_spectrum/input/massList.py @@ -77,6 +77,8 @@ def add_molecular_formula(self, mass_spec_obj, dataframe): # check if is coreMS file if 'Is Isotopologue' in dataframe: + # Reindex dataframe to row index to avoid issues with duplicated indexes (e.g. when multiple formula map to single mz_exp) + dataframe = dataframe.reset_index(drop=True) mz_exp_df = dataframe[Labels.mz].astype(float) formula_df = dataframe[dataframe.columns.intersection(Atoms.atoms_order)].copy() @@ -105,8 +107,11 @@ def add_molecular_formula(self, mass_spec_obj, dataframe): counts = list(formula_df.iloc[df_index].astype(int)) formula_dict = dict(zip(atoms, counts)) - if sum(counts) > 0: + # Drop any atoms with 0 counts + formula_dict = {atom: formula_dict[atom] for atom in formula_dict if formula_dict[atom] > 0} + + if sum(counts) > 0: ion_type = str(Labels.ion_type_translate.get(ion_type_df[df_index])) if adduct_df is not None: adduct_atom = str(adduct_df[df_index]) @@ -134,8 +139,10 @@ def add_molecular_formula(self, mass_spec_obj, dataframe): else: # remove any numbers from the atom name to cast as a mono-isotopic atom atom_mono = atom.strip('0123456789') - if atom_mono in Atoms.isotopes.keys(): + if atom_mono in Atoms.isotopes.keys() and atom_mono in formula_list_parent.keys(): formula_list_parent[atom_mono] = formula_list_parent[atom_mono]+formula_dict[atom] + elif atom_mono in Atoms.isotopes.keys(): + formula_list_parent[atom_mono] = formula_dict[atom] else: print(f"Atom {atom} not in Atoms.atoms_order") mono_index = int(dataframe.iloc[df_index]['Mono Isotopic Index']) @@ -150,21 +157,22 @@ def add_molecular_formula(self, mass_spec_obj, dataframe): # Next, generate isotopologues from the parent isos = list( mono_mfobj.isotopologues( - min_abundance = mass_spec_obj[df_index].abundance*0.1, + min_abundance = mass_spec_obj[df_index].abundance*0.001, current_mono_abundance = mass_spec_obj[mono_index].abundance, dynamic_range = mass_spec_obj.dynamic_range ) ) # Finally, find the isotopologue that matches the formula_dict - matched_isos = isos + matched_isos = [] for iso in isos: - if set(iso.atoms) == set(formula_dict.keys()): - # Check the values of the atoms match - if all([iso[atom] == formula_dict[atom] for atom in formula_dict]): - matched_isos = [iso] - if len(matched_isos) > 1: - raise ValueError("More than one isotopologue matched the formula_dict: {matched_isos}") + while len(matched_isos) < 1: + # Check the atoms match + if set(iso.atoms) == set(formula_dict.keys()): + # Check the values of the atoms match + if all([iso[atom] == formula_dict[atom] for atom in formula_dict]): + matched_isos.append(iso) + if len(matched_isos) == 0: raise ValueError("No isotopologue matched the formula_dict") mfobj = matched_isos[0] diff --git a/corems/molecular_formula/factory/MolecularFormulaFactory.py b/corems/molecular_formula/factory/MolecularFormulaFactory.py index 92d87853b..6d680a698 100644 --- a/corems/molecular_formula/factory/MolecularFormulaFactory.py +++ b/corems/molecular_formula/factory/MolecularFormulaFactory.py @@ -206,13 +206,25 @@ def _from_list(self, molecular_formula_list, ion_type, adduct_atom): def _from_str(self, molecular_formula_str, ion_type, adduct_atom): # string has to be in the format #'C10 H21 13C1 Cl1 37Cl1 etc' - molecular_formula_list = molecular_formula_str.split(' ') - final_formula = [] - for i in molecular_formula_list: - atoms_count = self.split(Atoms.atoms_order, i) - final_formula.extend(atoms_count) - print(final_formula) - self._from_list(final_formula, ion_type, adduct_atom) + # Check if there are spaces in the string + if ' ' not in molecular_formula_str: + raise ValueError("The molecular formula string should have spaces, input: %s" % molecular_formula_str) + + # Split the string by spaces + # Grab the text before a digit for each element after splitting on spaces (atoms) + elements = [re.sub(r'\d+$', '', x) for x in molecular_formula_str.split()] + # Grab the digits at the end of each element after splitting on spaces (counts) + counts = [re.findall(r'\d+$', x)[0] for x in molecular_formula_str.split()] + # Check that the number of elements and counts are the same + if len(elements) != len(counts): + raise ValueError("The number of elements and counts do not match, input: %s" % molecular_formula_str) + + # Create a dictionary from the elements and counts and add it to the molecular formula + dict_ = dict(zip(elements, counts)) + # Cast counts to integers + dict_ = {key: int(val) for key, val in dict_.items()} + self._from_dict(dict_, ion_type, adduct_atom) + def split(self, delimiters, string, maxsplit=0): #pragma: no cover """Splits the molecular formula string. diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py new file mode 100644 index 000000000..ed2693461 --- /dev/null +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -0,0 +1,11 @@ +from pathlib import Path +from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectra + + +if __name__ == "__main__": + + out_path = Path("tmp_data/NMDC_processed_0812/EMSL_49991_Brodie_123_Lipids_Neg_12Aug19_Lola-WCSH417820") + out_path_hdf5 = str(out_path) + ".corems/" + out_path.stem + ".hdf5" + parser = ReadCoreMSHDFMassSpectra(out_path_hdf5) + myLCMSobj = parser.get_lcms_obj() + myLCMSobj.mass_features_to_df() \ No newline at end of file diff --git a/support_code/nmdc/lipidomics/lipidomics_workflow.py b/support_code/nmdc/lipidomics/lipidomics_workflow.py index 98a5f87c5..4880a603f 100644 --- a/support_code/nmdc/lipidomics/lipidomics_workflow.py +++ b/support_code/nmdc/lipidomics/lipidomics_workflow.py @@ -521,9 +521,9 @@ def run_lipid_workflow(file_dir, out_dir, params_toml, verbose, cores=1): if __name__ == "__main__": # Set input variables to run cores = 1 - file_dir = Path("tmp_data/thermo_raw_NMDC_mini") - out_dir = Path("tmp_data/NMDC_processed_0807b") - params_toml = Path("tmp_data/thermo_raw_NMDC_mini/nmdc_lipid_params.toml") + file_dir = Path("tmp_data/thermo_raw_collection") + out_dir = Path("tmp_data/NMDC_processed_collection_0812") + params_toml = Path("tmp_data/thermo_raw_collection/nmdc_lipid_params.toml") verbose = True From 08447b6fbef2bd0985f5c283dcfd0be0c11bbee3 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 14 Aug 2024 12:27:35 -0700 Subject: [PATCH 003/158] Add validation steps to lcms collection parser --- corems/mass_spectra/input/corems_hdf5.py | 97 +++++++++++++++++-- .../nmdc/lipidomics/lipidomics_collection.py | 15 +-- 2 files changed, 98 insertions(+), 14 deletions(-) diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 363f73ee8..f18f55be4 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -3,6 +3,8 @@ from threading import Thread +import toml +import json import pandas as pd @@ -445,24 +447,105 @@ def get_lcms_obj(self) -> LCMSBase: return lcms_obj -class ReadCoreMSHDFMassSpectraCollection( - ReadCoreMSHDFMassSpectra -): +class ReadCoreMSHDFMassSpectraCollection(): def __init__( self, folder_location: str, manifest_file: str - ): - self.manifest = self._parse_manifest(manifest_file) - + ): + # Check for folder location and manifest file + if not folder_location.exists(): + raise FileNotFoundError(f"Folder location {folder_location} not found.") + if not manifest_file.exists(): + raise FileNotFoundError(f"Manifest file {manifest_file} not found.") + + # Check if the manifest file is a CSV + if manifest_file.suffix != ".csv": + raise ValueError("Manifest file must be a CSV.") + + self.folder_location = folder_location + self._parse_manifest(manifest_file) + self._validate_manifest() + self._validate_parameters() + def _parse_manifest(self, manifest_file): + """Parse the manifest file and set the manifest dictionary.""" manifest = pd.read_csv(manifest_file) - self._manifest_dict = manifest.to_dict(orient='records') + # Check if the following columns exisit in the manifest file + if not all(col in manifest.columns for col in ['sample_name', 'order', 'batch']): + raise ValueError("Manifest file must contain the following columns: 'sample_name', 'order', 'batch'.") + # Set index to the 'sample_name' column + manifest.set_index('sample_name', inplace=True) + self._manifest_dict = manifest.to_dict(orient='index') + + def _validate_manifest(self): + """Validate the manifest dictionary against the CoreMS folder location.""" + # Check if the folder location contains HDF5 files for each sample + for sample_name in self._manifest_dict.keys(): + corems_dir = self.folder_location / f"{sample_name}.corems" + if not corems_dir.exists(): + raise FileNotFoundError(f"CoreMS folder for {sample_name} not found.") + hdf5_file = corems_dir / f"{sample_name}.hdf5" + if not hdf5_file.exists(): + raise FileNotFoundError(f"HDF5 file for {sample_name} not found.") + + def _validate_parameters(self): + """Validate that the parameters used for all samples within a batch are the same.""" + # Check if parameters files are saved as JSON or TOML + if self.parameters_files[0].suffix == ".json": + importer = json + suffix = ".json" + + elif self.parameters_files[0].suffix == ".toml": + importer = toml + suffix = ".toml" + + manfiest_df = self.manifest_dataframe + + # Split up samples by batch + batches = manfiest_df['batch'].unique() + + for batch in batches: + samples = manfiest_df[manfiest_df['batch'] == batch].index + # check if self.parameters_files end with .json or .toml + batch_param_files = [self.folder_location / f"{sample_name}.corems/{sample_name}{suffix}" for sample_name in self._manifest_dict.keys() if sample_name in samples] + with open(batch_param_files[0], 'r', encoding='utf8',) as stream: + first_parameters = importer.load(stream) + for parameters_file in batch_param_files[1:]: + with open(parameters_file, 'r', encoding='utf8',) as stream: + parameters = importer.load(stream) + if parameters != first_parameters: + raise ValueError(f"Parameters files for samples in batch {batch} are not equal.") + + + + # Load the first parameters file and check if is all equal to the others + @property def manifest(self): return self._manifest_dict + @property + def manifest_dataframe(self): + return pd.DataFrame(self._manifest_dict).T + + @property + def hdf5_files(self): + return [self.folder_location / f"{sample_name}.corems/{sample_name}.hdf5" for sample_name in self._manifest_dict.keys()] + + @property + def parameters_files(self): + # Check if parameters files are saved as JSON or TOML + json_files = [self.folder_location / f"{sample_name}.corems/{sample_name}.json" for sample_name in self._manifest_dict.keys()] + toml_files = [self.folder_location / f"{sample_name}.corems/{sample_name}.toml" for sample_name in self._manifest_dict.keys()] + if all([x.exists() for x in json_files]): + return json_files + elif all([x.exists() for x in toml_files]): + return toml_files + else: + raise ValueError("Parameters files are not saved for all samples.") + \ No newline at end of file diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index ed2693461..ae40b6c07 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -1,11 +1,12 @@ from pathlib import Path -from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectra +from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectraCollection if __name__ == "__main__": - - out_path = Path("tmp_data/NMDC_processed_0812/EMSL_49991_Brodie_123_Lipids_Neg_12Aug19_Lola-WCSH417820") - out_path_hdf5 = str(out_path) + ".corems/" + out_path.stem + ".hdf5" - parser = ReadCoreMSHDFMassSpectra(out_path_hdf5) - myLCMSobj = parser.get_lcms_obj() - myLCMSobj.mass_features_to_df() \ No newline at end of file + collection_path = Path("tmp_data/NMDC_processed_collection_0813") + manifest_file = collection_path / "manifest.csv" + parser = ReadCoreMSHDFMassSpectraCollection( + folder_location = collection_path, + manifest_file = manifest_file + ) + print("Here") \ No newline at end of file From 150ad6063d5d4f532ffc067aa329ab7d92db5119 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 14 Aug 2024 14:10:27 -0700 Subject: [PATCH 004/158] Add LCMSCollection class --- corems/mass_spectra/factory/lc_class.py | 43 ++++++ corems/mass_spectra/input/corems_hdf5.py | 145 ++++++++++++------ .../nmdc/lipidomics/lipidomics_collection.py | 2 + .../nmdc/lipidomics/lipidomics_workflow.py | 2 +- 4 files changed, 140 insertions(+), 52 deletions(-) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index d94f83411..7533ce5ef 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1067,3 +1067,46 @@ def tic(self, tic_list): A list of TIC values for the dataset. """ self._tic_list = np.array(tic_list) + +class LCMSCollection: + """A class representing a collection of liquid chromatography-mass spectrometry (LC-MS) runs. + + Parameters + ----------- + + Attributes + ----------- + + Methods + -------- + """ + + def __init__( + self, + collection_location, + manifest, + collection_parser=None + ): + self.collection_location = collection_location + self._manifest_dict = manifest + self.collection_parser = collection_parser + self._lcms = {} + + @property + def samples(self): + return set(self._lcms.keys()) + + @property + def ordered_samples(self): + manifest_df = self.manifest_dataframe + # order by batch, then by order + manifest_df = manifest_df.sort_values(by=['batch', 'order']) + return manifest_df.index.tolist() + + @property + def manifest(self): + return self._manifest_dict + + @property + def manifest_dataframe(self): + return pd.DataFrame(self._manifest_dict).T diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index f18f55be4..5331a6b2a 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -13,7 +13,7 @@ load_and_set_json_parameters_lcms, load_and_set_toml_parameters_lcms, ) -from corems.mass_spectra.factory.lc_class import LCMSBase, MassSpectraBase +from corems.mass_spectra.factory.lc_class import LCMSBase, MassSpectraBase, LCMSCollection from corems.mass_spectra.factory.LC_Temp import EIC_Data from corems.mass_spectra.input.parserbase import SpectraParserInterface from corems.mass_spectrum.input.coremsHDF5 import ReadCoreMSHDF_MassSpectrum @@ -119,7 +119,7 @@ def load(self) -> None: """ """ pass - def run(self, mass_spectra) -> None: + def run(self, mass_spectra, load_raw=True) -> None: """Runs the importer functions to populate a LCMS or MassSpectraBase object. Notes @@ -137,7 +137,8 @@ def run(self, mass_spectra) -> None: ---------- mass_spectra : LCMSBase or MassSpectraBase The LCMS or MassSpectraBase object to populate with mass spectra, generally instantiated with only the file_location, analyzer, and instrument_label attributes. - + load_raw : bool + If True, load raw data (unprocessed) from HDF5 files for overall lcms object and individual mass spectra. Default is True. Returns ------- None, but populates several attributes on the LCMS or MassSpectraBase object. @@ -149,13 +150,13 @@ def run(self, mass_spectra) -> None: if "mass_spectra" in self.h5pydata: # Populate the _ms list on the LCMS object - self.import_mass_spectra(mass_spectra) + self.import_mass_spectra(mass_spectra, load_raw=load_raw) if "scan_info" in self.h5pydata: # Populate the _scan_info attribute on the LCMS object self.import_scan_info(mass_spectra) - if "ms_unprocessed" in self.h5pydata: + if "ms_unprocessed" in self.h5pydata and load_raw: # Populate the _ms_unprocessed attribute on the LCMS object self.import_ms_unprocessed(mass_spectra) @@ -171,21 +172,23 @@ def run(self, mass_spectra) -> None: # Populate the spectral_search_results attribute on the LCMS object self.import_spectral_search_results(mass_spectra) - def import_mass_spectra(self, mass_spectra) -> None: + def import_mass_spectra(self, mass_spectra, load_raw=True) -> None: """Imports all mass spectra from the HDF5 file. Parameters ---------- mass_spectra : LCMSBase | MassSpectraBase The MassSpectraBase or LCMSBase object to populate with mass spectra. + load_raw : bool + If True, load raw data (unprocessed) from HDF5 files for overall lcms object and individual mass spectra. Default Returns ------- - None, but populates the '_ms' list on the LCMSBase or MassSpectraBase + None, but populates the '_ms' list on the LCMSBase or MassSpectraBase object with mass spectra from the HDF5 file. """ for scan_number in self.scan_number_list: - mass_spec = self.get_mass_spectrum(scan_number) + mass_spec = self.get_mass_spectrum(scan_number, load_raw=load_raw) mass_spec.scan_number = scan_number mass_spectra.add_mass_spectrum(mass_spec) @@ -199,7 +202,7 @@ def import_scan_info(self, mass_spectra) -> None: Returns ------- - None, but populates the 'scan_df' attribute on the LCMSBase or MassSpectraBase + None, but populates the 'scan_df' attribute on the LCMSBase or MassSpectraBase object with a pandas DataFrame of the 'scan_info' from the HDF5 file. """ @@ -226,7 +229,7 @@ def import_ms_unprocessed(self, mass_spectra) -> None: Returns ------- - None, but populates the '_ms_unprocessed' attribute on the LCMSBase or MassSpectraBase + None, but populates the '_ms_unprocessed' attribute on the LCMSBase or MassSpectraBase object with a dictionary of the 'ms_unprocessed' from the HDF5 file. """ @@ -250,7 +253,7 @@ def import_parameters(self, mass_spectra) -> None: Returns ------- - None, but populates the 'parameters' attribute on the LCMS or MassSpectraBase + None, but populates the 'parameters' attribute on the LCMS or MassSpectraBase object with a dictionary of the 'parameters' from the HDF5 file. """ @@ -273,7 +276,7 @@ def import_mass_features(self, mass_spectra) -> None: Returns ------- - None, but populates the 'mass_features' attribute on the LCMSBase or MassSpectraBase + None, but populates the 'mass_features' attribute on the LCMSBase or MassSpectraBase object with a dictionary of the 'mass_features' from the HDF5 file. """ @@ -331,7 +334,7 @@ def import_eics(self, mass_spectra): Returns ------- - None, but populates the 'eics' attribute on the LCMSBase or MassSpectraBase + None, but populates the 'eics' attribute on the LCMSBase or MassSpectraBase object with a dictionary of the 'eics' from the HDF5 file. """ @@ -364,7 +367,7 @@ def import_spectral_search_results(self, mass_spectra): Returns ------- - None, but populates the 'spectral_search_results' attribute on the LCMSBase or MassSpectraBase + None, but populates the 'spectral_search_results' attribute on the LCMSBase or MassSpectraBase object with a dictionary of the 'spectral_search_results' from the HDF5 file. """ @@ -424,7 +427,7 @@ def get_mass_spectra_obj(self) -> MassSpectraBase: return spectra_obj - def get_lcms_obj(self) -> LCMSBase: + def get_lcms_obj(self, load_raw=True) -> LCMSBase: """ Return LCMSBase object, populating attributes on the LCMSBase object from the HDF5 file. """ @@ -437,7 +440,7 @@ def get_lcms_obj(self) -> LCMSBase: ) # This will populate the majority of the attributes on the LCMS object - self.run(lcms_obj) + self.run(lcms_obj, load_raw=load_raw) # Set final attributes of the LCMS object lcms_obj.polarity = self.h5pydata.attrs["polarity"] @@ -446,24 +449,21 @@ def get_lcms_obj(self) -> LCMSBase: lcms_obj._tic_list = list(lcms_obj.scan_df.tic) return lcms_obj - -class ReadCoreMSHDFMassSpectraCollection(): - def __init__( - self, - folder_location: str, - manifest_file: str - ): + + +class ReadCoreMSHDFMassSpectraCollection: + def __init__(self, folder_location: str, manifest_file: str): # Check for folder location and manifest file if not folder_location.exists(): raise FileNotFoundError(f"Folder location {folder_location} not found.") if not manifest_file.exists(): raise FileNotFoundError(f"Manifest file {manifest_file} not found.") - + # Check if the manifest file is a CSV if manifest_file.suffix != ".csv": raise ValueError("Manifest file must be a CSV.") - - self.folder_location = folder_location + + self.folder_location = folder_location self._parse_manifest(manifest_file) self._validate_manifest() self._validate_parameters() @@ -472,12 +472,16 @@ def _parse_manifest(self, manifest_file): """Parse the manifest file and set the manifest dictionary.""" manifest = pd.read_csv(manifest_file) # Check if the following columns exisit in the manifest file - if not all(col in manifest.columns for col in ['sample_name', 'order', 'batch']): - raise ValueError("Manifest file must contain the following columns: 'sample_name', 'order', 'batch'.") + if not all( + col in manifest.columns for col in ["sample_name", "order", "batch"] + ): + raise ValueError( + "Manifest file must contain the following columns: 'sample_name', 'order', 'batch'." + ) # Set index to the 'sample_name' column - manifest.set_index('sample_name', inplace=True) - self._manifest_dict = manifest.to_dict(orient='index') - + manifest.set_index("sample_name", inplace=True) + self._manifest_dict = manifest.to_dict(orient="index") + def _validate_manifest(self): """Validate the manifest dictionary against the CoreMS folder location.""" # Check if the folder location contains HDF5 files for each sample @@ -488,14 +492,14 @@ def _validate_manifest(self): hdf5_file = corems_dir / f"{sample_name}.hdf5" if not hdf5_file.exists(): raise FileNotFoundError(f"HDF5 file for {sample_name} not found.") - + def _validate_parameters(self): """Validate that the parameters used for all samples within a batch are the same.""" # Check if parameters files are saved as JSON or TOML if self.parameters_files[0].suffix == ".json": importer = json suffix = ".json" - + elif self.parameters_files[0].suffix == ".toml": importer = toml suffix = ".toml" @@ -503,49 +507,88 @@ def _validate_parameters(self): manfiest_df = self.manifest_dataframe # Split up samples by batch - batches = manfiest_df['batch'].unique() + batches = manfiest_df["batch"].unique() for batch in batches: - samples = manfiest_df[manfiest_df['batch'] == batch].index + samples = manfiest_df[manfiest_df["batch"] == batch].index # check if self.parameters_files end with .json or .toml - batch_param_files = [self.folder_location / f"{sample_name}.corems/{sample_name}{suffix}" for sample_name in self._manifest_dict.keys() if sample_name in samples] - with open(batch_param_files[0], 'r', encoding='utf8',) as stream: + batch_param_files = [ + self.folder_location / f"{sample_name}.corems/{sample_name}{suffix}" + for sample_name in self._manifest_dict.keys() + if sample_name in samples + ] + with open( + batch_param_files[0], + "r", + encoding="utf8", + ) as stream: first_parameters = importer.load(stream) for parameters_file in batch_param_files[1:]: - with open(parameters_file, 'r', encoding='utf8',) as stream: + with open( + parameters_file, + "r", + encoding="utf8", + ) as stream: parameters = importer.load(stream) if parameters != first_parameters: - raise ValueError(f"Parameters files for samples in batch {batch} are not equal.") - + raise ValueError( + f"Parameters files for samples in batch {batch} are not equal." + ) + def get_lcms_collection(self, load_raw=False) -> LCMSCollection: + """Return a LCMSCollection object - # Load the first parameters file and check if is all equal to the others + Parameters + ---------- + load_raw : bool + If True, load raw data from HDF5 files. Default is False. + """ + # Instantiate the LCMSCollection object + lcms_coll = LCMSCollection( + collection_location=self.folder_location, + manifest=self.manifest, + collection_parser=self + ) + # Add LCMS objects to the collection + samples = self._manifest_dict.keys() + + for sample_name in samples: + hdf5_file = self.folder_location / f"{sample_name}.corems/{sample_name}.hdf5" + parser = ReadCoreMSHDFMassSpectra(hdf5_file) + lcms_coll._lcms[sample_name] = parser.get_lcms_obj(load_raw=load_raw) + + return lcms_coll @property def manifest(self): return self._manifest_dict - + @property def manifest_dataframe(self): return pd.DataFrame(self._manifest_dict).T - + @property def hdf5_files(self): - return [self.folder_location / f"{sample_name}.corems/{sample_name}.hdf5" for sample_name in self._manifest_dict.keys()] - + return [ + self.folder_location / f"{sample_name}.corems/{sample_name}.hdf5" + for sample_name in self._manifest_dict.keys() + ] + @property def parameters_files(self): # Check if parameters files are saved as JSON or TOML - json_files = [self.folder_location / f"{sample_name}.corems/{sample_name}.json" for sample_name in self._manifest_dict.keys()] - toml_files = [self.folder_location / f"{sample_name}.corems/{sample_name}.toml" for sample_name in self._manifest_dict.keys()] + json_files = [ + self.folder_location / f"{sample_name}.corems/{sample_name}.json" + for sample_name in self._manifest_dict.keys() + ] + toml_files = [ + self.folder_location / f"{sample_name}.corems/{sample_name}.toml" + for sample_name in self._manifest_dict.keys() + ] if all([x.exists() for x in json_files]): return json_files elif all([x.exists() for x in toml_files]): return toml_files else: raise ValueError("Parameters files are not saved for all samples.") - - - - \ No newline at end of file diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index ae40b6c07..5c004953f 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -9,4 +9,6 @@ folder_location = collection_path, manifest_file = manifest_file ) + lcms_collection = parser.get_lcms_collection(load_raw=False) + lcms_collection._lcms[lcms_collection.ordered_samples[0]].mass_features_to_df() print("Here") \ No newline at end of file diff --git a/support_code/nmdc/lipidomics/lipidomics_workflow.py b/support_code/nmdc/lipidomics/lipidomics_workflow.py index d86677dda..1f48da276 100644 --- a/support_code/nmdc/lipidomics/lipidomics_workflow.py +++ b/support_code/nmdc/lipidomics/lipidomics_workflow.py @@ -571,7 +571,7 @@ def run_lipid_workflow( # Set input variables to run cores = 1 file_dir = Path("tmp_data/thermo_raw_collection") - out_dir = Path("tmp_data/NMDC_processed_collection_0812") + out_dir = Path("tmp_data/NMDC_processed_collection_0813") params_toml = Path("tmp_data/thermo_raw_collection/nmdc_lipid_params.toml") verbose = True From 8e0de4dcd6630fcd940785b770a8ddc1cee390ba Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 14 Aug 2024 17:21:47 -0700 Subject: [PATCH 005/158] Add calculations for lcms collections to find consensus mass features --- corems/mass_spectra/calc/lc_calc.py | 134 ++++++++++++++++++ corems/mass_spectra/factory/lc_class.py | 5 +- corems/mass_spectra/input/corems_hdf5.py | 12 +- .../nmdc/lipidomics/lipidomics_collection.py | 1 + 4 files changed, 149 insertions(+), 3 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index e97484b2f..21a0a6f28 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -3,6 +3,7 @@ from ripser import ripser from scipy import sparse from scipy.spatial import KDTree +from ms_entropy import FlashEntropySearch from corems.chroma_peak.factory.chroma_peak_classes import LCMSMassFeature from corems.mass_spectra.calc import SignalProcessing as sp @@ -1404,3 +1405,136 @@ def cluster_mass_features( } else: return cluster_daughters + +class LCMSCollectionCalculations: + """Methods for performing calculations related to LCMS collections. + + Notes + ----- + This class is intended as a mixin for the LCMSCollection class. + """ + def clean_sparse_matrix(self, sparse_matrix): + """Clean a sparse matrix by removing duplicates and sorting. + + Parameters + ---------- + sparse_matrix : :obj:`~numpy.array` + A sparse matrix to clean. + + Returns + ------- + :obj:`~numpy.array` + A cleaned sparse matrix. + """ + for match in sparse_matrix: + match.sort() + sparse_matrix.sort() + dereplicated_sparse_matrix = np.unique(sparse_matrix, axis=0) + return dereplicated_sparse_matrix + + + def get_ms1_sparse_matrix(self): + """Get a sparse matrix of MS1 matches. + + Parameters + ---------- + mode : str, optional + The mode to use for matching. Default is "open". Other options are "identity". + + Returns + ------- + :obj:`~numpy.array` + A sparse matrix of MS1 matches, with each row containing the ids of the mass features that match. + """ + # Grab all deconvoluted ms1s from all mass features + ms1_decon_fe_lib = [] + max_mz = 0 + for key, lcms_obj in self._lcms.items(): + if lcms_obj.mass_features is None: + raise ValueError( + "No mass features found in LCMSBase object, run find_mass_features() first" + ) + for mf_id, mass_feature in lcms_obj.mass_features.items(): + lib_entry = { + "id": str(self.manifest[key]['collection_id']) + "_" + str(mass_feature.id), #TODO KRH: create id for each lcms_obj that is not its name + "precursor_mz": mass_feature.mz, + "peaks": np.column_stack((mass_feature.mass_spectrum.mz_exp, mass_feature.mass_spectrum.abundance)) + } + ms1_decon_fe_lib.append(lib_entry) + if mass_feature.mz > max_mz: + max_mz = mass_feature.mz + + # Build flash entropy search index + fes = FlashEntropySearch( + max_ms2_tolerance_in_da = 0.001 #TODO KRH: source this from a parameter + ) + fes.build_index( + all_spectra_list = ms1_decon_fe_lib, + max_indexed_mz = max_mz+5, + precursor_ions_removal_da = None, + noise_threshold=0, + min_ms2_difference_in_da = 0.001, #TODO KRH: source this from a parameter + max_peak_num = 0, + clean_spectra = True + ) + + results_sparse_open = [] + results_sparse_identity = [] + min_match_score = 0.1 #TODO KRH: source this from a parameter + + for spec in fes: + search_results = fes.search( + precursor_mz=spec['precursor_mz'], + peaks=spec['peaks'], + ms1_tolerance_in_da=0.001, #TODO KRH: source this from a parameter + ms2_tolerance_in_da=0.001, #TODO KRH: source this from a parameter [note this isn't really MS2] + method={"open", "identity"}, + precursor_ions_removal_da=None, + noise_threshold=0, + target="cpu", + ) + + match_inds_open = np.where(search_results["open_search"] > min_match_score)[0] + if len(match_inds_open) > 0: + ref_ms_ids = [fes[x]["id"] for x in match_inds_open] + # drop ref_ms_ids if it matches spec["id"] + ref_ms_ids = [x for x in ref_ms_ids if x != spec["id"]] + if len(ref_ms_ids) > 0: + for ref_ms_id in ref_ms_ids: + results_sparse_open.append([spec["id"], ref_ms_id]) + match_inds_identity = np.where(search_results["identity_search"] > min_match_score)[0] + if len(match_inds_identity) > 0: + ref_ms_ids = [fes[x]["id"] for x in match_inds_identity] + # drop ref_ms_ids if it matches spec["id"] + ref_ms_ids = [x for x in ref_ms_ids if x != spec["id"]] + if len(ref_ms_ids) > 0: + for ref_ms_id in ref_ms_ids: + results_sparse_identity.append([spec["id"], ref_ms_id]) + + results_sparse_open = self.clean_sparse_matrix(results_sparse_open) + results_sparse_identity = self.clean_sparse_matrix(results_sparse_identity) + + return results_sparse_open, results_sparse_identity + + + def add_consensus_mass_features(self): + """Add consensus mass features to the LCMSCollection object. + + Parameters + ---------- + None + + Returns + ------- + None, but assigns the consensus_mass_features attribute to the LCMSCollection object. + + Raises + ------ + ValueError + If no mass features are found in any of the LCMSBase objects in the LCMSCollection object. + """ + # Get a series of sparse matrices of matches between lcms objects' mass features + ms1_open_sm, ms1_identity_sm = self.get_ms1_sparse_matrix() + + + diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 7533ce5ef..dcdbaaa63 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -5,7 +5,7 @@ import pandas as pd from corems.encapsulation.factory.parameters import LCMSParameters -from corems.mass_spectra.calc.lc_calc import LCCalculations, PHCalculations +from corems.mass_spectra.calc.lc_calc import LCCalculations, PHCalculations, LCMSCollectionCalculations from corems.molecular_id.search.lcms_spectral_search import LCMSSpectralSearch from corems.mass_spectrum.input.numpyArray import ms_from_array_profile @@ -1068,7 +1068,7 @@ def tic(self, tic_list): """ self._tic_list = np.array(tic_list) -class LCMSCollection: +class LCMSCollection(LCMSCollectionCalculations): """A class representing a collection of liquid chromatography-mass spectrometry (LC-MS) runs. Parameters @@ -1091,6 +1091,7 @@ def __init__( self._manifest_dict = manifest self.collection_parser = collection_parser self._lcms = {} + self.consensus_mass_features = {} @property def samples(self): diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 5331a6b2a..5dc38b985 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -533,7 +533,7 @@ def _validate_parameters(self): if parameters != first_parameters: raise ValueError( f"Parameters files for samples in batch {batch} are not equal." - ) + ) def get_lcms_collection(self, load_raw=False) -> LCMSCollection: """Return a LCMSCollection object @@ -557,6 +557,16 @@ def get_lcms_collection(self, load_raw=False) -> LCMSCollection: hdf5_file = self.folder_location / f"{sample_name}.corems/{sample_name}.hdf5" parser = ReadCoreMSHDFMassSpectra(hdf5_file) lcms_coll._lcms[sample_name] = parser.get_lcms_obj(load_raw=load_raw) + + # Check that all LCMS objects have the same polarity + if len(set([x.polarity for k, x in lcms_coll._lcms.items()])) != 1: + raise ValueError("All samples must have the same polarity.") + + # Set ids on the LCMS objects + i = 1 + for sample in lcms_coll.ordered_samples: + lcms_coll._manifest_dict[sample]["collection_id"] = i + i += 1 return lcms_coll diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 5c004953f..46a8862e2 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -11,4 +11,5 @@ ) lcms_collection = parser.get_lcms_collection(load_raw=False) lcms_collection._lcms[lcms_collection.ordered_samples[0]].mass_features_to_df() + lcms_collection.add_consensus_mass_features() print("Here") \ No newline at end of file From 5fead06fe90fca44271698164150ba9265ecaa23 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 15 Aug 2024 14:14:49 -0700 Subject: [PATCH 006/158] Start retention time alignment functionality --- corems/chroma_peak/calc/ChromaPeakCalc.py | 6 +- corems/mass_spectra/calc/lc_calc.py | 112 ++++++++++++------ corems/mass_spectra/factory/lc_class.py | 7 ++ corems/mass_spectra/input/corems_hdf5.py | 2 +- .../nmdc/lipidomics/lipidomics_collection.py | 2 +- .../nmdc/lipidomics/lipidomics_workflow.py | 8 +- 6 files changed, 91 insertions(+), 46 deletions(-) diff --git a/corems/chroma_peak/calc/ChromaPeakCalc.py b/corems/chroma_peak/calc/ChromaPeakCalc.py index 40c8b3247..ac9a4c55e 100644 --- a/corems/chroma_peak/calc/ChromaPeakCalc.py +++ b/corems/chroma_peak/calc/ChromaPeakCalc.py @@ -169,9 +169,9 @@ def calc_fraction_height_width(self, fraction: float): max_index = np.where(self._eic_data.scans == self.apex_scan)[0][0] left_index = max_index right_index = max_index - while eic[left_index] > eic[max_index] * fraction: + while eic[left_index] > eic[max_index] * fraction and left_index > 0: left_index -= 1 - while eic[right_index] > eic[max_index] * fraction: + while eic[right_index] > eic[max_index] * fraction and right_index < len(eic) - 1: right_index += 1 # Get the retention times of the indexes just below the half height @@ -241,7 +241,7 @@ def calc_tailing_factor(self, accept_estimated: bool = False): eic = self._eic_data.eic_smoothed max_index = np.where(self._eic_data.scans == self.apex_scan)[0][0] left_index = max_index - while eic[left_index] > eic[max_index] * 0.05: + while eic[left_index] > eic[max_index] * 0.05 and left_index > 0: left_index -= 1 left_half_time_min = ( diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 21a0a6f28..a6623f027 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -691,6 +691,11 @@ def deconvolute_ms1_mass_features(self): # Loop through each mass feature for mf_id, mass_feature in self.mass_features.items(): + + # Check that the mass_feature.mz attribute == the mz of the mass feature in the mass_feature_df + if mass_feature.mz != mass_feature.ms1_peak.mz_exp: + continue + # Get the left and right limits of the EIC of the mass feature l_scan, _, r_scan = mass_feature._eic_data.apexes[0] @@ -1432,8 +1437,8 @@ def clean_sparse_matrix(self, sparse_matrix): dereplicated_sparse_matrix = np.unique(sparse_matrix, axis=0) return dereplicated_sparse_matrix - - def get_ms1_sparse_matrix(self): + + def get_sparse_matrix(self, anchors_only=True, ms_level=1): """Get a sparse matrix of MS1 matches. Parameters @@ -1447,7 +1452,7 @@ def get_ms1_sparse_matrix(self): A sparse matrix of MS1 matches, with each row containing the ids of the mass features that match. """ # Grab all deconvoluted ms1s from all mass features - ms1_decon_fe_lib = [] + ms_lib = [] max_mz = 0 for key, lcms_obj in self._lcms.items(): if lcms_obj.mass_features is None: @@ -1455,21 +1460,31 @@ def get_ms1_sparse_matrix(self): "No mass features found in LCMSBase object, run find_mass_features() first" ) for mf_id, mass_feature in lcms_obj.mass_features.items(): - lib_entry = { - "id": str(self.manifest[key]['collection_id']) + "_" + str(mass_feature.id), #TODO KRH: create id for each lcms_obj that is not its name - "precursor_mz": mass_feature.mz, - "peaks": np.column_stack((mass_feature.mass_spectrum.mz_exp, mass_feature.mass_spectrum.abundance)) - } - ms1_decon_fe_lib.append(lib_entry) - if mass_feature.mz > max_mz: - max_mz = mass_feature.mz + if anchors_only and not mass_feature.anchor_feature: + continue + if ms_level == 1: + ms_list = [mass_feature.mass_spectrum_deconvoluted] + elif ms_level == 2: + ms_list = [x for k, x in mass_feature.ms2_mass_spectra.items()] + if ms_list is None: + continue + for ms in ms_list: + lib_entry = { + "id": str(self.manifest[key]['collection_id']) + "_" + str(mass_feature.id) + "_" + str(ms.scan_number), #lcmsID_mfID_scanNumber [unique ID for the mass spectrum associated with the mass feature] + "precursor_mz": mass_feature.mz, + "peaks": np.column_stack((ms.mz_exp, ms.abundance)), + "mf_id": str(self.manifest[key]['collection_id']) + "_" + str(mass_feature.id) #lcmsID_mfID [unique ID for the mass feature] + } + ms_lib.append(lib_entry) + if mass_feature.mz > max_mz: + max_mz = mass_feature.mz # Build flash entropy search index fes = FlashEntropySearch( max_ms2_tolerance_in_da = 0.001 #TODO KRH: source this from a parameter ) fes.build_index( - all_spectra_list = ms1_decon_fe_lib, + all_spectra_list = ms_lib, max_indexed_mz = max_mz+5, precursor_ions_removal_da = None, noise_threshold=0, @@ -1478,9 +1493,8 @@ def get_ms1_sparse_matrix(self): clean_spectra = True ) - results_sparse_open = [] results_sparse_identity = [] - min_match_score = 0.1 #TODO KRH: source this from a parameter + min_match_score = 0.8 #TODO KRH: source this from a parameter for spec in fes: search_results = fes.search( @@ -1488,37 +1502,27 @@ def get_ms1_sparse_matrix(self): peaks=spec['peaks'], ms1_tolerance_in_da=0.001, #TODO KRH: source this from a parameter ms2_tolerance_in_da=0.001, #TODO KRH: source this from a parameter [note this isn't really MS2] - method={"open", "identity"}, + method={"identity"}, precursor_ions_removal_da=None, noise_threshold=0, target="cpu", ) - - match_inds_open = np.where(search_results["open_search"] > min_match_score)[0] - if len(match_inds_open) > 0: - ref_ms_ids = [fes[x]["id"] for x in match_inds_open] - # drop ref_ms_ids if it matches spec["id"] - ref_ms_ids = [x for x in ref_ms_ids if x != spec["id"]] - if len(ref_ms_ids) > 0: - for ref_ms_id in ref_ms_ids: - results_sparse_open.append([spec["id"], ref_ms_id]) match_inds_identity = np.where(search_results["identity_search"] > min_match_score)[0] if len(match_inds_identity) > 0: - ref_ms_ids = [fes[x]["id"] for x in match_inds_identity] + ref_ms_ids = [fes[x]["mf_id"] for x in match_inds_identity] # drop ref_ms_ids if it matches spec["id"] - ref_ms_ids = [x for x in ref_ms_ids if x != spec["id"]] + ref_ms_ids = [x for x in ref_ms_ids if x != spec["mf_id"]] if len(ref_ms_ids) > 0: for ref_ms_id in ref_ms_ids: - results_sparse_identity.append([spec["id"], ref_ms_id]) + results_sparse_identity.append([spec["mf_id"], ref_ms_id]) - results_sparse_open = self.clean_sparse_matrix(results_sparse_open) results_sparse_identity = self.clean_sparse_matrix(results_sparse_identity) - return results_sparse_open, results_sparse_identity + return results_sparse_identity - def add_consensus_mass_features(self): - """Add consensus mass features to the LCMSCollection object. + def set_anchor_features(self): + """Set anchor features within the mass features of the LCMS objects in the collection. Parameters ---------- @@ -1526,15 +1530,49 @@ def add_consensus_mass_features(self): Returns ------- - None, but assigns the consensus_mass_features attribute to the LCMSCollection object. - - Raises - ------ - ValueError - If no mass features are found in any of the LCMSBase objects in the LCMSCollection object. + None, but assigns the anchor_feature attribute to the mass features in the mass_features attribute of the LCMSBase objects in the collection. + """ + for key, lcms_obj in self._lcms.items(): + if lcms_obj.mass_features is None: + raise ValueError( + "No mass features found in LCMSBase object, run find_mass_features() first" + ) + mf_df = lcms_obj.mass_features_to_df() + mf_df = mf_df[mf_df["mass_spectrum_deconvoluted_parent"]] + mf_df = mf_df[~mf_df["ms2_spectrum"].isnull()] + for mf_id, mass_feature in lcms_obj.mass_features.items(): + if mf_id in mf_df.index: + mass_feature.anchor_feature = True + else: + mass_feature.anchor_feature = False + + def align_lcms_objects(self): + """Align LCMS objects in the collection.""" + # Set anchors + self.set_anchor_features() + ms1_sparse = self.get_sparse_matrix(anchors_only=True, ms_level=1) + ms2_sparse = self.get_sparse_matrix(anchors_only=True, ms_level=2) + print("here") + + def add_consensus_mass_features(self): + """ """ # Get a series of sparse matrices of matches between lcms objects' mass features + # Get the easy ones first + self.get_distance_matrices() + + # Get the ms1 sparse matrices ms1_open_sm, ms1_identity_sm = self.get_ms1_sparse_matrix() + + # Next get the rt sparse matrices + + # Next get the mz sparse matrices + + # Next get the ms2 sparse matrices + + # Next get the peak shape sparse matrices + + diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index dcdbaaa63..8fa7f4dff 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1093,6 +1093,11 @@ def __init__( self._lcms = {} self.consensus_mass_features = {} + def __getitem__(self, index): + samp_name = self.ordered_samples[index] + self._lcms[samp_name] + return self._lcms[samp_name] + @property def samples(self): return set(self._lcms.keys()) @@ -1111,3 +1116,5 @@ def manifest(self): @property def manifest_dataframe(self): return pd.DataFrame(self._manifest_dict).T + + diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 5dc38b985..a6d5fff03 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -563,7 +563,7 @@ def get_lcms_collection(self, load_raw=False) -> LCMSCollection: raise ValueError("All samples must have the same polarity.") # Set ids on the LCMS objects - i = 1 + i = 0 for sample in lcms_coll.ordered_samples: lcms_coll._manifest_dict[sample]["collection_id"] = i i += 1 diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 46a8862e2..20d8303a6 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -11,5 +11,5 @@ ) lcms_collection = parser.get_lcms_collection(load_raw=False) lcms_collection._lcms[lcms_collection.ordered_samples[0]].mass_features_to_df() - lcms_collection.add_consensus_mass_features() + lcms_collection.align_lcms_objects() print("Here") \ No newline at end of file diff --git a/support_code/nmdc/lipidomics/lipidomics_workflow.py b/support_code/nmdc/lipidomics/lipidomics_workflow.py index 1f48da276..8b09ccbfb 100644 --- a/support_code/nmdc/lipidomics/lipidomics_workflow.py +++ b/support_code/nmdc/lipidomics/lipidomics_workflow.py @@ -528,7 +528,7 @@ def run_lipid_workflow( files_list = list(file_dir.glob("*.raw")) out_paths_list = [out_dir / f.stem for f in files_list] - # Run signal processing, get associated ms1, add associated ms2, do ms1 molecular search, and export temp results + # Run signal processing, get associated ms1, add associated ms2, and export temp results if cores == 1 or len(files_list) == 1: mz_dicts = [] for file_in, file_out in list(zip(files_list, out_paths_list)): @@ -549,7 +549,7 @@ def run_lipid_workflow( mz_dicts = pool.starmap(run_lipid_sp_ms1, args) pool.close() pool.join() - + """ # Prepare ms2 spectral search space metadata = prep_metadata(mz_dicts, out_dir) @@ -563,7 +563,7 @@ def run_lipid_workflow( mz_dicts = pool.starmap(run_lipid_ms2, args) pool.close() pool.join() - + """ print("Finished processing, data are written in " + str(out_dir)) @@ -571,7 +571,7 @@ def run_lipid_workflow( # Set input variables to run cores = 1 file_dir = Path("tmp_data/thermo_raw_collection") - out_dir = Path("tmp_data/NMDC_processed_collection_0813") + out_dir = Path("tmp_data/NMDC_processed_collection_0815") params_toml = Path("tmp_data/thermo_raw_collection/nmdc_lipid_params.toml") verbose = True From 04d3a5bab43286b9b29ad704c9e4fde0f6733075 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 15 Aug 2024 17:10:11 -0700 Subject: [PATCH 007/158] Add work towards aligning mass features --- corems/mass_spectra/factory/lc_class.py | 25 +++++++++++++++---- corems/mass_spectra/input/corems_hdf5.py | 5 +++- .../nmdc/lipidomics/lipidomics_collection.py | 2 +- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 8fa7f4dff..9067fbe16 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1093,17 +1093,32 @@ def __init__( self._lcms = {} self.consensus_mass_features = {} + def _reorder_lcms_objects(self): + """ + Reorders the LCMS objects in the collection based on the order in the manifest. + """ + ordered_samples = self.samples + self._lcms = {k: self._lcms[k] for k in ordered_samples} + def __getitem__(self, index): - samp_name = self.ordered_samples[index] + samp_name = self.samples[index] self._lcms[samp_name] return self._lcms[samp_name] + + def mass_features_to_df(self): + """Returns a pandas dataframe summarizing all the mass features in the collection.""" + mf_df_list = [] + for lcms_obj, sample in zip(self._lcms, self.samples): + mf_df = lcms_obj.mass_features_to_df() + # Remove index + mf_df = mf_df.reset_index(drop=False) + # Add sample name and sample id to the dataframe + mf_df["sample_name"] = lcms_obj.sample_name + mf_df["sample_id"] = self.manifest[sample]["collection_id"] + mf_df["coll_mf_id"] = mf_df["sample_id"].astype(str) + "_" + mf_df["mf_id"].astype(str) @property def samples(self): - return set(self._lcms.keys()) - - @property - def ordered_samples(self): manifest_df = self.manifest_dataframe # order by batch, then by order manifest_df = manifest_df.sort_values(by=['batch', 'order']) diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index a6d5fff03..600747d8b 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -564,10 +564,13 @@ def get_lcms_collection(self, load_raw=False) -> LCMSCollection: # Set ids on the LCMS objects i = 0 - for sample in lcms_coll.ordered_samples: + for sample in lcms_coll.samples: lcms_coll._manifest_dict[sample]["collection_id"] = i i += 1 + # Reorder the LCMS objects + lcms_coll._reorder_lcms_objects() + return lcms_coll @property diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 20d8303a6..5ef4f5961 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -10,6 +10,6 @@ manifest_file = manifest_file ) lcms_collection = parser.get_lcms_collection(load_raw=False) - lcms_collection._lcms[lcms_collection.ordered_samples[0]].mass_features_to_df() + lcms_collection[0].mass_features_to_df() lcms_collection.align_lcms_objects() print("Here") \ No newline at end of file From 60202c25bfb92b088a86a5647fa6183ef0d9b6cf Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 19 Aug 2024 11:42:09 -0700 Subject: [PATCH 008/158] Add lcms collection attributes, plotting functions --- corems/mass_spectra/calc/lc_calc.py | 109 +++++++++++++++++- corems/mass_spectra/factory/lc_class.py | 29 +++++ corems/mass_spectrum/calc/PeakPicking.py | 13 +-- .../nmdc/lipidomics/lipidomics_collection.py | 4 +- .../nmdc/lipidomics/lipidomics_workflow.py | 2 +- 5 files changed, 139 insertions(+), 18 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index a6623f027..bc8073deb 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1539,20 +1539,119 @@ def set_anchor_features(self): ) mf_df = lcms_obj.mass_features_to_df() mf_df = mf_df[mf_df["mass_spectrum_deconvoluted_parent"]] - mf_df = mf_df[~mf_df["ms2_spectrum"].isnull()] for mf_id, mass_feature in lcms_obj.mass_features.items(): if mf_id in mf_df.index: mass_feature.anchor_feature = True else: mass_feature.anchor_feature = False + def get_anchor_feature_ids(self, lcms_obj): + """Returns the ids of the anchor features in the mass features of an LCMSBase object.""" + anchor_feature_ids = [] + for mf_id, mass_feature in lcms_obj.mass_features.items(): + if mass_feature.anchor_feature: + anchor_feature_ids.append(mf_id) + return anchor_feature_ids + + def match_mfs(self, lc_obj, mf1_sub): + if a is None or b is None: + return None, None + + # Safely cast to list + dims = deimos.utils.safelist(dims) + tol = deimos.utils.safelist(tol) + relative = deimos.utils.safelist(relative) + + # Check dims + deimos.utils.check_length([dims, tol, relative]) + + # Compute inter-feature distances + idx = [] + for i, f in enumerate(dims): + # vectors + v1 = a[f].values.reshape(-1, 1) + v2 = b[f].values.reshape(-1, 1) + + # Distances + d = scipy.spatial.distance.cdist(v1, v2) + + if relative[i] is True: + # Divisor + basis = np.repeat(v1, v2.shape[0], axis=1) + fix = np.repeat(v2, v1.shape[0], axis=1).T + basis = np.where(basis == 0, fix, basis) + + # Divide + d = np.divide(d, basis, out=np.zeros_like(basis), where=basis != 0) + + # Check tol + idx.append(d <= tol[i]) + + # Stack truth arrays + idx = np.prod(np.dstack(idx), axis=-1, dtype=bool) + + # Compute normalized 3d distance + v1 = a[dims].values / tol + v2 = b[dims].values / tol + dist3d = scipy.spatial.distance.cdist(v1, v2, 'cityblock') + dist3d = np.multiply(dist3d, idx) + + # Normalize to 0-1 + mx = dist3d.max() + if mx > 0: + dist3d = dist3d / dist3d.max() + + # Intensities + intensity = np.repeat(a['intensity'].values.reshape(-1, 1), + b.shape[0], axis=1) + intensity = np.multiply(intensity, idx) + + # Max over dims + maxcols = np.max(intensity, axis=0, keepdims=True) + + # Zero out nonmax over dims + intensity[intensity != maxcols] = 0 + + # Break ties by distance + intensity = intensity - dist3d + + # Max over clusters + maxrows = np.max(intensity, axis=1, keepdims=True) + + # Where max and nonzero + ii, jj = np.where((intensity == maxrows) & (intensity > 0)) + + # Reorder + a = a.iloc[ii] + b = b.iloc[jj] + + if len(a.index) < 1 or len(b.index) < 1: + return None, None + + return a, b + def align_lcms_objects(self): """Align LCMS objects in the collection.""" - # Set anchors + + # Set anchors on all LCMS objects' mass features self.set_anchor_features() - ms1_sparse = self.get_sparse_matrix(anchors_only=True, ms_level=1) - ms2_sparse = self.get_sparse_matrix(anchors_only=True, ms_level=2) - print("here") + + # Prepare the center LCMS object + center_obj_id = 0 #KRH TODO: make this part of the manifest (QC sample or the center in the order) + center_lcms_obj = self[center_obj_id] + mf1 = center_lcms_obj.mass_features_to_df() + anchors = self.get_anchor_feature_ids(center_lcms_obj) + mf1_sub = mf1[mf1.index.isin(anchors)] + + # Loop through the other LCMS objects in the collection (going forward) + i = center_obj_id + 1 + while i < (len(self)-1): + anchors_i = self.get_anchor_feature_ids(lc_obj) + mf_i = lc_obj.mass_features_to_df() + mf_i_sub = mf_i[mf_i.index.isin(anchors_i)] + matches = self.match_mfs(lc_obj, mf_i_sub) #match the mass features in the LCMS object to the anchor mass features in the center LCMS object. + self.fit_rts(lc_obj, mf1_sub, matches) #fit the retention times of the LCMS object to the center LCMS object using the matched + def add_consensus_mass_features(self): """ diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 9067fbe16..5a9a4ece2 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1105,6 +1105,9 @@ def __getitem__(self, index): self._lcms[samp_name] return self._lcms[samp_name] + def __len__(self): + return len(self.samples) + def mass_features_to_df(self): """Returns a pandas dataframe summarizing all the mass features in the collection.""" mf_df_list = [] @@ -1117,6 +1120,32 @@ def mass_features_to_df(self): mf_df["sample_id"] = self.manifest[sample]["collection_id"] mf_df["coll_mf_id"] = mf_df["sample_id"].astype(str) + "_" + mf_df["mf_id"].astype(str) + def plot_tics(self, ms_level=1, corrected_rt=False): + """Plots the TICs for all the LCMS objects in the collection. + + Parameters + ----------- + ms_level : int, optional + The MS level to plot the TICs for. Defaults to 1. + corrected_rt : bool, optional + If True, plots the corrected retention time. Defaults to False. + """ + import matplotlib.pyplot as plt + fig, ax = plt.subplots() + for lcms_obj in self: + scan_df = lcms_obj.scan_df + scan_df = scan_df[scan_df.ms_level == ms_level] + ax.plot(scan_df.scan_time, scan_df.tic, label=lcms_obj.sample_name) + ax.set_xlabel("Retention Time (min)") + ax.set_ylabel("TIC") + ax.legend() + # Add overall title + if corrected_rt: + plt.suptitle("TICs for LCMS Collection (Corrected RT)") + else: + plt.suptitle("TICs for LCMS Collection") + plt.show() + @property def samples(self): manifest_df = self.manifest_dataframe diff --git a/corems/mass_spectrum/calc/PeakPicking.py b/corems/mass_spectrum/calc/PeakPicking.py index da0e37a52..fb0b243c4 100644 --- a/corems/mass_spectrum/calc/PeakPicking.py +++ b/corems/mass_spectrum/calc/PeakPicking.py @@ -266,11 +266,7 @@ def calculate_resolving_power(self, intes, massa, current_index): try: peak_height_minus = intes[index_minus] except IndexError: - print('Res. calc. warning - peak index minus adjacent to spectrum edge \n \ - Zeroing the first 5 data points of abundance. Peaks at spectrum edge may be incorrectly reported') - intes[:5] = 0 - peak_height_minus = target_peak_height - index_minus -= 1 + return nan #print "massa", "," , "intes", "," , massa[index_minus], "," , peak_height_minus x = [ massa[index_minus], massa[index_minus+1]] y = [ intes[index_minus], intes[index_minus+1]] @@ -294,11 +290,8 @@ def calculate_resolving_power(self, intes, massa, current_index): try: peak_height_plus = intes[index_plus] except IndexError: - print('Res. calc. warning - peak index plus adjacent to spectrum edge \n \ - Zeroing the last 5 data points of abundance. Peaks at spectrum edge may be incorrectly reported') - intes[-5:] = 0 - peak_height_plus = target_peak_height - index_plus += 1 + return nan + #print "massa", "," , "intes", "," , massa[index_plus], "," , peak_height_plus x = [massa[index_plus], massa[index_plus - 1]] diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 5ef4f5961..5d971a9d6 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -3,13 +3,13 @@ if __name__ == "__main__": - collection_path = Path("tmp_data/NMDC_processed_collection_0813") + collection_path = Path("tmp_data/NMDC_processed_collection_0819") manifest_file = collection_path / "manifest.csv" parser = ReadCoreMSHDFMassSpectraCollection( folder_location = collection_path, manifest_file = manifest_file ) lcms_collection = parser.get_lcms_collection(load_raw=False) - lcms_collection[0].mass_features_to_df() + #lcms_collection.plot_tics() lcms_collection.align_lcms_objects() print("Here") \ No newline at end of file diff --git a/support_code/nmdc/lipidomics/lipidomics_workflow.py b/support_code/nmdc/lipidomics/lipidomics_workflow.py index 8b09ccbfb..9eb77e4d6 100644 --- a/support_code/nmdc/lipidomics/lipidomics_workflow.py +++ b/support_code/nmdc/lipidomics/lipidomics_workflow.py @@ -571,7 +571,7 @@ def run_lipid_workflow( # Set input variables to run cores = 1 file_dir = Path("tmp_data/thermo_raw_collection") - out_dir = Path("tmp_data/NMDC_processed_collection_0815") + out_dir = Path("tmp_data/NMDC_processed_collection_0819") params_toml = Path("tmp_data/thermo_raw_collection/nmdc_lipid_params.toml") verbose = True From 020501ba600e8f9b682464eb02f63262aa3405d7 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 19 Aug 2024 16:46:40 -0700 Subject: [PATCH 009/158] Add functionality for alignment within an LCMS collection --- corems/mass_spectra/calc/lc_calc.py | 195 +++++++++++------- corems/mass_spectra/factory/lc_class.py | 55 ++++- corems/mass_spectra/input/corems_hdf5.py | 60 ++++-- .../nmdc/lipidomics/lipidomics_collection.py | 8 +- 4 files changed, 222 insertions(+), 96 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index bc8073deb..93531ed54 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1,9 +1,12 @@ import numpy as np import pandas as pd from ripser import ripser +import scipy from scipy import sparse from scipy.spatial import KDTree from ms_entropy import FlashEntropySearch +from sklearn.cluster import AgglomerativeClustering +from sklearn.svm import SVR from corems.chroma_peak.factory.chroma_peak_classes import LCMSMassFeature from corems.mass_spectra.calc import SignalProcessing as sp @@ -1519,58 +1522,22 @@ def get_sparse_matrix(self, anchors_only=True, ms_level=1): results_sparse_identity = self.clean_sparse_matrix(results_sparse_identity) return results_sparse_identity - - - def set_anchor_features(self): - """Set anchor features within the mass features of the LCMS objects in the collection. - - Parameters - ---------- - None - - Returns - ------- - None, but assigns the anchor_feature attribute to the mass features in the mass_features attribute of the LCMSBase objects in the collection. - """ - for key, lcms_obj in self._lcms.items(): - if lcms_obj.mass_features is None: - raise ValueError( - "No mass features found in LCMSBase object, run find_mass_features() first" - ) - mf_df = lcms_obj.mass_features_to_df() - mf_df = mf_df[mf_df["mass_spectrum_deconvoluted_parent"]] - for mf_id, mass_feature in lcms_obj.mass_features.items(): - if mf_id in mf_df.index: - mass_feature.anchor_feature = True - else: - mass_feature.anchor_feature = False - - def get_anchor_feature_ids(self, lcms_obj): - """Returns the ids of the anchor features in the mass features of an LCMSBase object.""" - anchor_feature_ids = [] - for mf_id, mass_feature in lcms_obj.mass_features.items(): - if mass_feature.anchor_feature: - anchor_feature_ids.append(mf_id) - return anchor_feature_ids - - def match_mfs(self, lc_obj, mf1_sub): - if a is None or b is None: + + def match_mfs(self, mf_c, mf_i): + if mf_c is None or mf_i is None: return None, None # Safely cast to list - dims = deimos.utils.safelist(dims) - tol = deimos.utils.safelist(tol) - relative = deimos.utils.safelist(relative) - - # Check dims - deimos.utils.check_length([dims, tol, relative]) + dims = ["mz", "scan_time"] + relative = [False, False] + tol = [0.0001, 1] #TODO KRH: source this from a parameter, make mz relative # Compute inter-feature distances idx = [] for i, f in enumerate(dims): # vectors - v1 = a[f].values.reshape(-1, 1) - v2 = b[f].values.reshape(-1, 1) + v1 = mf_c[f].values.reshape(-1, 1) + v2 = mf_i[f].values.reshape(-1, 1) # Distances d = scipy.spatial.distance.cdist(v1, v2) @@ -1591,8 +1558,8 @@ def match_mfs(self, lc_obj, mf1_sub): idx = np.prod(np.dstack(idx), axis=-1, dtype=bool) # Compute normalized 3d distance - v1 = a[dims].values / tol - v2 = b[dims].values / tol + v1 = mf_c[dims].values / tol + v2 = mf_i[dims].values / tol dist3d = scipy.spatial.distance.cdist(v1, v2, 'cityblock') dist3d = np.multiply(dist3d, idx) @@ -1602,8 +1569,8 @@ def match_mfs(self, lc_obj, mf1_sub): dist3d = dist3d / dist3d.max() # Intensities - intensity = np.repeat(a['intensity'].values.reshape(-1, 1), - b.shape[0], axis=1) + intensity = np.repeat(mf_c['intensity'].values.reshape(-1, 1), + mf_i.shape[0], axis=1) intensity = np.multiply(intensity, idx) # Max over dims @@ -1622,35 +1589,123 @@ def match_mfs(self, lc_obj, mf1_sub): ii, jj = np.where((intensity == maxrows) & (intensity > 0)) # Reorder - a = a.iloc[ii] - b = b.iloc[jj] + mf_c = mf_c.iloc[ii] + mf_i = mf_i.iloc[jj] - if len(a.index) < 1 or len(b.index) < 1: + if len(mf_c.index) < 1 or len(mf_i.index) < 1: return None, None - return a, b + return mf_c, mf_i + + def fit_rts(self, a, b, align='scan_time', **kwargs): + ''' + Fit a support vector regressor to matched features. + + Parameters + ---------- + a : :obj:`~pandas.DataFrame` + First set of input feature coordinates and intensities. + b : :obj:`~pandas.DataFrame` + Second set of input feature coordinates and intensities. + align : str + Dimension to align. + kwargs + Keyword arguments for support vector regressor + (:class:`sklearn.svm.SVR`). + + Returns + ------- + :obj:`~scipy.interpolate.interp1d` + Interpolated fit of the SVR result. + + ''' + + # Uniqueify + x = a[align].values + y = b[align].values + arr = np.vstack((x, y)).T + arr = np.unique(arr, axis=0) + + # Check kwargs + if 'kernel' in kwargs: + kernel = kwargs.get('kernel') + else: + kernel = 'linear' + + # Construct interpolation axis + newx = np.linspace(arr[:, 0].min(), arr[:, 0].max(), 1000) + + # Linear kernel + if kernel == 'linear': + reg = scipy.stats.linregress(x, y) + newy = reg.slope * newx + reg.intercept + + # Other kernels + else: + # Fit + svr = SVR(**kwargs) + svr.fit(arr[:, 0].reshape(-1, 1), arr[:, 1]) + + # Predict + newy = svr.predict(newx.reshape(-1, 1)) + + return scipy.interpolate.interp1d(newx, newy, + kind='linear', fill_value='extrapolate') def align_lcms_objects(self): """Align LCMS objects in the collection.""" - - # Set anchors on all LCMS objects' mass features - self.set_anchor_features() - + #TODO KRH: add the ability to do this by batch and then connect the batches # Prepare the center LCMS object - center_obj_id = 0 #KRH TODO: make this part of the manifest (QC sample or the center in the order) - center_lcms_obj = self[center_obj_id] - mf1 = center_lcms_obj.mass_features_to_df() - anchors = self.get_anchor_feature_ids(center_lcms_obj) - mf1_sub = mf1[mf1.index.isin(anchors)] - - # Loop through the other LCMS objects in the collection (going forward) - i = center_obj_id + 1 - while i < (len(self)-1): - anchors_i = self.get_anchor_feature_ids(lc_obj) - mf_i = lc_obj.mass_features_to_df() - mf_i_sub = mf_i[mf_i.index.isin(anchors_i)] - matches = self.match_mfs(lc_obj, mf_i_sub) #match the mass features in the LCMS object to the anchor mass features in the center LCMS object. - self.fit_rts(lc_obj, mf1_sub, matches) #fit the retention times of the LCMS object to the center LCMS object using the matched + center_obj_id = 0 #KRH TODO: make this part of the manifest or infer during intialization (QC sample or the center in the order) + mf_df_c = self[center_obj_id].mass_features_to_df().copy() + # Drop mass featuers with nan in mass_spectrum_deconvoluted_parent + mf_df_c = mf_df_c.dropna(subset=["mass_spectrum_deconvoluted_parent"]) + mf_df_c = mf_df_c[mf_df_c["mass_spectrum_deconvoluted_parent"]] + center_scan_df = self[center_obj_id].scan_df.copy() + center_scan_df['scan_time_aligned'] = center_scan_df['scan_time'] + self[center_obj_id].scan_df = center_scan_df + + index_steps = (1, -1) + # Run this twice, once going forward (+1 indexing) and once going backward (-1 indexing) + for index_step in index_steps: + # Loop through the other LCMS objects in the collection (going forward) + i = center_obj_id + index_step + if i < len(self) and i >= 0: + # Grab the first LCMS object after the center object + lc_obj = self[i] + mf_df_i = lc_obj.mass_features_to_df().copy() + mf_df_i['scan_time_og'] = mf_df_i['scan_time'] + + while mf_df_i is not None: + mf_df_i = mf_df_i.dropna(subset=["mass_spectrum_deconvoluted_parent"]) + mf_df_i = mf_df_i[mf_df_i["mass_spectrum_deconvoluted_parent"]] + + # Match the mass features in the LCMS object to the anchor mass features in the center LCMS object. + matches_c, matches_i = self.match_mfs(mf_df_c, mf_df_i) + if matches_c is not None: + # Reset the scan_time to the original scan_time + matches_i = matches_i.copy() + matches_i['scan_time'] = matches_i['scan_time_og'] + + # Fit the retention times of the LCMS object to the center LCMS object using the matched mass features + spl = self.fit_rts(matches_c, matches_i, kernel='rbf', C=1000) + # Set new retention times on scan_df for lc_obj + new_times = spl(lc_obj.scan_df['scan_time']) + new_scan_info = lc_obj.scan_df.copy() + new_scan_info['scan_time_aligned'] = new_times + lc_obj.scan_df = new_scan_info + i += index_step + if i >= len(self) or i == 0: + mf_df_i = None + else: + # Grab the next LCMS object and use the previous spline fitting to get a better starting point + lc_obj = self[i] + mf_df_i = lc_obj.mass_features_to_df().copy() + mf_df_i['scan_time_og'] = mf_df_i['scan_time'] + # Set scan_time to previous sample's predicted scan_time to find closer matches + mf_df_i['scan_time'] = spl(mf_df_i['scan_time']) + else: + raise ValueError(f'No matches found between the center object and {self.samples[i]}') def add_consensus_mass_features(self): diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 5a9a4ece2..f92df5ad6 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -690,7 +690,8 @@ def mass_spectrum_to_string( if len(self.mass_features[mf_id].ms2_scan_numbers) > 0: # Add MS2 spectra info best_ms2_spectrum = self.mass_features[mf_id].best_ms2 - dict_mf["ms2_spectrum"] = mass_spectrum_to_string(best_ms2_spectrum) + if best_ms2_spectrum is not None: + dict_mf["ms2_spectrum"] = mass_spectrum_to_string(best_ms2_spectrum) if len(self.mass_features[mf_id].associated_mass_features_deconvoluted) > 0: dict_mf["associated_mass_features"] = ", ".join( map( @@ -1091,6 +1092,7 @@ def __init__( self._manifest_dict = manifest self.collection_parser = collection_parser self._lcms = {} + self._combined_mass_features = None self.consensus_mass_features = {} def _reorder_lcms_objects(self): @@ -1108,17 +1110,49 @@ def __getitem__(self, index): def __len__(self): return len(self.samples) - def mass_features_to_df(self): - """Returns a pandas dataframe summarizing all the mass features in the collection.""" + def combine_mass_features(self): + """ + Concatenates the mass features from all the LCMS objects in the collection. + """ mf_df_list = [] - for lcms_obj, sample in zip(self._lcms, self.samples): + for lcms_obj in self: mf_df = lcms_obj.mass_features_to_df() # Remove index mf_df = mf_df.reset_index(drop=False) # Add sample name and sample id to the dataframe mf_df["sample_name"] = lcms_obj.sample_name - mf_df["sample_id"] = self.manifest[sample]["collection_id"] + mf_df["sample_id"] = self.manifest[lcms_obj.sample_name]["collection_id"] mf_df["coll_mf_id"] = mf_df["sample_id"].astype(str) + "_" + mf_df["mf_id"].astype(str) + + # Check if scan_df has scan_time_aligned and add to mf_df if so + if "scan_time_aligned" in lcms_obj.scan_df.columns: + scan_df = lcms_obj.scan_df[["scan", "scan_time_aligned"]].copy() + scan_df = scan_df.rename(columns={"scan": "apex_scan"}) + mf_df = mf_df.merge(scan_df, left_on="apex_scan", right_index=True) + mf_df_list.append(mf_df) + combined_mass_features = pd.concat(mf_df_list) + # Move coll_mf_id, sample_name, and sample_id to front + cols = combined_mass_features.columns.tolist() + top_cols = ["coll_mf_id", "sample_name", "sample_id", "mz", "scan_time_aligned", "a test"] + cols = [x for x in top_cols + [col for col in cols if col not in top_cols] if x in cols] + combined_mass_features = combined_mass_features[cols] + self._combined_mass_features = combined_mass_features.to_dict() + + def mass_features_to_df(self): + """Returns a pandas dataframe summarizing all the mass features in the collection.""" + # Check if combined_mass_features is set, set if not + if self._combined_mass_features is None: + self.combine_mass_features() + + # Check if scan_time_aligned is in combined_mass_features, try to add if not + elif self._combined_mass_features is not None and "scan_time_aligned" not in self._combined_mass_features: + if all([True for x in self if "scan_time_aligned" in x.scan_df.columns]): + self.combine_mass_features() + + df = pd.DataFrame(self._combined_mass_features) + return df + + def plot_tics(self, ms_level=1, corrected_rt=False): """Plots the TICs for all the LCMS objects in the collection. @@ -1135,13 +1169,20 @@ def plot_tics(self, ms_level=1, corrected_rt=False): for lcms_obj in self: scan_df = lcms_obj.scan_df scan_df = scan_df[scan_df.ms_level == ms_level] - ax.plot(scan_df.scan_time, scan_df.tic, label=lcms_obj.sample_name) + if corrected_rt: + # Check that scan_time_aligned is key in scan_df + if "scan_time_aligned" not in scan_df.columns: + raise ValueError(f"scan_time_aligned not found in scan_df for {lcms_obj.sample_name}") + else: + ax.plot(scan_df.scan_time_aligned, scan_df.tic, label=lcms_obj.sample_name) + else: + ax.plot(scan_df.scan_time, scan_df.tic, label=lcms_obj.sample_name) ax.set_xlabel("Retention Time (min)") ax.set_ylabel("TIC") ax.legend() # Add overall title if corrected_rt: - plt.suptitle("TICs for LCMS Collection (Corrected RT)") + plt.suptitle("TICs for LCMS Collection (Aligned RT)") else: plt.suptitle("TICs for LCMS Collection") plt.show() diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index a484a191c..bbf9cddf0 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -119,7 +119,7 @@ def load(self) -> None: """ """ pass - def run(self, mass_spectra, load_raw=True) -> None: + def run(self, mass_spectra, load_raw=True, load_light=False) -> None: """Runs the importer functions to populate a LCMS or MassSpectraBase object. Notes @@ -139,6 +139,9 @@ def run(self, mass_spectra, load_raw=True) -> None: The LCMS or MassSpectraBase object to populate with mass spectra, generally instantiated with only the file_location, analyzer, and instrument_label attributes. load_raw : bool If True, load raw data (unprocessed) from HDF5 files for overall lcms object and individual mass spectra. Default is True. + load_light : bool + If True, only load the parameters, mass features, and scan info. Default is False. + Returns ------- None, but populates several attributes on the LCMS or MassSpectraBase object. @@ -148,7 +151,7 @@ def run(self, mass_spectra, load_raw=True) -> None: # Populate the parameters attribute on the LCMS object self.import_parameters(mass_spectra) - if "mass_spectra" in self.h5pydata: + if "mass_spectra" in self.h5pydata and not load_light: # Populate the _ms list on the LCMS object self.import_mass_spectra(mass_spectra, load_raw=load_raw) @@ -156,7 +159,7 @@ def run(self, mass_spectra, load_raw=True) -> None: # Populate the _scan_info attribute on the LCMS object self.import_scan_info(mass_spectra) - if "ms_unprocessed" in self.h5pydata and load_raw: + if "ms_unprocessed" in self.h5pydata and load_raw and not load_light: # Populate the _ms_unprocessed attribute on the LCMS object self.import_ms_unprocessed(mass_spectra) @@ -164,11 +167,11 @@ def run(self, mass_spectra, load_raw=True) -> None: # Populate the mass_features attribute on the LCMS object self.import_mass_features(mass_spectra) - if "eics" in self.h5pydata: + if "eics" in self.h5pydata and not load_light: # Populate the eics attribute on the LCMS object self.import_eics(mass_spectra) - if "spectral_search_results" in self.h5pydata: + if "spectral_search_results" in self.h5pydata and not load_light: # Populate the spectral_search_results attribute on the LCMS object self.import_spectral_search_results(mass_spectra) @@ -410,7 +413,7 @@ def import_spectral_search_results(self, mass_spectra): ] ) - def get_mass_spectra_obj(self, load_raw=True) -> MassSpectraBase: + def get_mass_spectra_obj(self, load_raw=True, load_light=False) -> MassSpectraBase: """ Return mass spectra data object, populating the _ms list on MassSpectraBase object from the HDF5 file. @@ -418,6 +421,8 @@ def get_mass_spectra_obj(self, load_raw=True) -> MassSpectraBase: ---------- load_raw : bool If True, load raw data (unprocessed) from HDF5 files for overall spectra object and individual mass spectra. Default is True. + load_light : bool + If True, only load the parameters, mass features, and scan info. Default is False. """ # Instantiate the LCMS object @@ -429,11 +434,11 @@ def get_mass_spectra_obj(self, load_raw=True) -> MassSpectraBase: ) # This will populate the _ms list on the LCMS or MassSpectraBase object - self.run(spectra_obj, load_raw=load_raw) + self.run(spectra_obj, load_raw=load_raw, load_light=load_light) return spectra_obj - def get_lcms_obj(self, load_raw=True) -> LCMSBase: + def get_lcms_obj(self, load_raw=True, load_light=False) -> LCMSBase: """ Return LCMSBase object, populating attributes on the LCMSBase object from the HDF5 file. @@ -441,6 +446,10 @@ def get_lcms_obj(self, load_raw=True) -> LCMSBase: ---------- load_raw : bool If True, load raw data (unprocessed) from HDF5 files for overall lcms object and individual mass spectra. Default is True. + load_light : bool + If True, only load the parameters, mass features, and scan info. Default is False. + return_generator : bool + If True, return a generator object. Default is False. """ # Instantiate the LCMS object lcms_obj = LCMSBase( @@ -451,7 +460,7 @@ def get_lcms_obj(self, load_raw=True) -> LCMSBase: ) # This will populate the majority of the attributes on the LCMS object - self.run(lcms_obj, load_raw=load_raw) + self.run(lcms_obj, load_raw=load_raw, load_light=load_light) # Set final attributes of the LCMS object lcms_obj.polarity = self.h5pydata.attrs["polarity"] @@ -544,15 +553,33 @@ def _validate_parameters(self): if parameters != first_parameters: raise ValueError( f"Parameters files for samples in batch {batch} are not equal." - ) - - def get_lcms_collection(self, load_raw=False) -> LCMSCollection: + ) + + def get_lcms_obj(self, sample_name: str, load_raw=False, load_light=True) -> LCMSBase: + """Return a LCMSBase object for a given sample name within the collection. + + Parameters + ---------- + sample_name : str + The sample name to retrieve the LCMS object for. + load_raw : bool + If True, load raw data from HDF5 files. Default is False. + load_light : bool + If True, only load the parameters, mass features, and scan info are initially loaded for each lcms object. Default is True. + """ + hdf5_file = self.folder_location / f"{sample_name}.corems/{sample_name}.hdf5" + parser = ReadCoreMSHDFMassSpectra(hdf5_file) + return parser.get_lcms_obj(load_raw=load_raw, load_light=load_light) + + def get_lcms_collection(self, load_raw = False, load_light = True) -> LCMSCollection: """Return a LCMSCollection object Parameters ---------- load_raw : bool - If True, load raw data from HDF5 files. Default is False. + If True, load raw data from HDF5 files. Default is False. + load_light : bool + If True, only load the parameters, mass features, and scan info are initially loaded for each lcms object. Default is True. """ # Instantiate the LCMSCollection object lcms_coll = LCMSCollection( @@ -564,16 +591,15 @@ def get_lcms_collection(self, load_raw=False) -> LCMSCollection: # Add LCMS objects to the collection samples = self._manifest_dict.keys() + # Initialize the LCMS object dictionary for sample_name in samples: - hdf5_file = self.folder_location / f"{sample_name}.corems/{sample_name}.hdf5" - parser = ReadCoreMSHDFMassSpectra(hdf5_file) - lcms_coll._lcms[sample_name] = parser.get_lcms_obj(load_raw=load_raw) + lcms_coll._lcms[sample_name] = self.get_lcms_obj(sample_name, load_raw=load_raw, load_light=load_light) # Check that all LCMS objects have the same polarity if len(set([x.polarity for k, x in lcms_coll._lcms.items()])) != 1: raise ValueError("All samples must have the same polarity.") - # Set ids on the LCMS objects + # Set ids on the LCMS objects in the manifest i = 0 for sample in lcms_coll.samples: lcms_coll._manifest_dict[sample]["collection_id"] = i diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 5d971a9d6..116adcaed 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -9,7 +9,11 @@ folder_location = collection_path, manifest_file = manifest_file ) - lcms_collection = parser.get_lcms_collection(load_raw=False) + lcms_collection = parser.get_lcms_collection(load_raw=False, load_light=True) #lcms_collection.plot_tics() lcms_collection.align_lcms_objects() - print("Here") \ No newline at end of file + #lcms_collection.plot_tics(corrected_rt=True) + mass_feature_df = lcms_collection.mass_features_to_df() + print("Here") + + #lcms_collection. \ No newline at end of file From 8753e4f36928259b050f4fd09eb168114d265051 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 20 Aug 2024 07:26:23 -0700 Subject: [PATCH 010/158] Continue to add functionality for getting consensus mass features --- corems/mass_spectra/calc/lc_calc.py | 153 ++++++++++++++++-- corems/mass_spectra/factory/lc_class.py | 64 ++++---- .../nmdc/lipidomics/lipidomics_collection.py | 5 +- 3 files changed, 175 insertions(+), 47 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 93531ed54..f2a98f439 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -4,6 +4,7 @@ import scipy from scipy import sparse from scipy.spatial import KDTree +from scipy.spatial.distance import cdist from ms_entropy import FlashEntropySearch from sklearn.cluster import AgglomerativeClustering from sklearn.svm import SVR @@ -1415,7 +1416,7 @@ def cluster_mass_features( return cluster_daughters class LCMSCollectionCalculations: - """Methods for performing calculations related to LCMS collections. + """Methods for performing calculations related to LCMSCollection objects. Notes ----- @@ -1440,7 +1441,6 @@ def clean_sparse_matrix(self, sparse_matrix): dereplicated_sparse_matrix = np.unique(sparse_matrix, axis=0) return dereplicated_sparse_matrix - def get_sparse_matrix(self, anchors_only=True, ms_level=1): """Get a sparse matrix of MS1 matches. @@ -1524,6 +1524,27 @@ def get_sparse_matrix(self, anchors_only=True, ms_level=1): return results_sparse_identity def match_mfs(self, mf_c, mf_i): + """Match mass features between two LCMS objects. + + Parameters + ---------- + mf_c : :obj:`~pandas.DataFrame` + The mass features to match against. + mf_i : :obj:`~pandas.DataFrame` + The mass features to match. + + Returns + ------- + :obj:`~pandas.DataFrame` + The matched mass features from mf_c. + :obj:`~pandas.DataFrame` + The matched mass features from mf_i. + + Notes + ----- + This function has been adapted from the original implementation in the Deimos package: + https://github.com/pnnl/deimos + """ if mf_c is None or mf_i is None: return None, None @@ -1598,7 +1619,7 @@ def match_mfs(self, mf_c, mf_i): return mf_c, mf_i def fit_rts(self, a, b, align='scan_time', **kwargs): - ''' + """ Fit a support vector regressor to matched features. Parameters @@ -1618,7 +1639,12 @@ def fit_rts(self, a, b, align='scan_time', **kwargs): :obj:`~scipy.interpolate.interp1d` Interpolated fit of the SVR result. - ''' + Notes + ----- + This function has been adapted from the original implementation in the Deimos package: + https://github.com/pnnl/deimos + + """ # Uniqueify x = a[align].values @@ -1653,7 +1679,23 @@ def fit_rts(self, a, b, align='scan_time', **kwargs): kind='linear', fill_value='extrapolate') def align_lcms_objects(self): - """Align LCMS objects in the collection.""" + """ + Align LCMS objects in the collection. + + Aligns the LCMS objects in the collection by aligning the retention times of the mass features in the LCMS objects. + First, the mass features in the center LCMS object are matched to the mass features in the other LCMS objects, + starting with the LCMS object immediately following the center LCMS object. The retention times of the LCMS objects + are then fit to the center LCMS object using the matched mass features. + + Returns + ------- + None, but aligns the LCMS objects in the collection and sets the scan_time_aligned column in the scan_df attribute of each LCMS object. + + Notes + ----- + This function has been adapted from the original implementation in the Deimos package: + https://github.com/pnnl/deimos + """ #TODO KRH: add the ability to do this by batch and then connect the batches # Prepare the center LCMS object center_obj_id = 0 #KRH TODO: make this part of the manifest or infer during intialization (QC sample or the center in the order) @@ -1707,24 +1749,101 @@ def align_lcms_objects(self): else: raise ValueError(f'No matches found between the center object and {self.samples[i]}') - def add_consensus_mass_features(self): - """ - """ - # Get a series of sparse matrices of matches between lcms objects' mass features - # Get the easy ones first - self.get_distance_matrices() + # Check if the LCMS objects are aligned + if not all([True for x in self if "scan_time_aligned" in x.scan_df.columns]): + raise ValueError("LCMS objects are not aligned, run align_lcms_objects() first") + + # Get the combined mass features from all LCMS objects + combined_mfs = self.mass_features_to_df().copy() + self.agglomerative_clustering(combined_mfs) + + def agglomerative_clustering(self, features): + ''' + Cluster features within provided linkage tolerances. Recursively merges + the pair of clusters that minimally increases a given linkage distance. + See :class:`sklearn.cluster.AgglomerativeClustering`. + + Parameters + ---------- + features : :obj:`~pandas.DataFrame` or :obj:`~dask.dataframe.DataFrame` + Input feature coordinates and intensities per sample. + dims : str or list + Dimensions considered in clustering. + tol : float or list + Tolerance in each dimension to define maximum cluster linkage + distance. + relative : bool or list + Whether to use relative or absolute tolerances per dimension. + + Returns + ------- + features : :obj:`~pandas.DataFrame` + Features concatenated over samples with cluster labels. + + ''' + + if features is None: + return None + + # Safely cast to list + dims = ["mz", "scan_time"] + relative = [False, False] + tol = [0.0001, 1] #TODO KRH: source this from a parameter, make mz relative + + # Copy input + features = features.copy() + + # Make connectivity matrix for masking within sample mass features + if 'sample_idx' not in features.columns: + cmat = None + else: + vals = features['sample_id'].values.reshape(-1, 1) + cmat = cdist(vals, vals, metric=lambda x, y: x != y).astype(bool) + + # Compute inter-feature distances + distances = [] + for i, d in enumerate(dims): + # Vectors + v1 = features[d].values.reshape(-1, 1) + + # Distances + dist = scipy.spatial.distance.cdist(v1, v1) + + if relative[i] is True: + # Divisor + basis = np.repeat(v1, v1.shape[0], axis=1) + fix = np.repeat(v1, v1.shape[0], axis=1).T + basis = np.where(basis == 0, fix, basis) + + # Divide + dist = np.divide(dist, basis, out=np.zeros_like( + basis), where=basis != 0) + + # Check tol + distances.append(dist / tol[i]) + + # Stack distances + distances = np.dstack(distances) - # Get the ms1 sparse matrices - ms1_open_sm, ms1_identity_sm = self.get_ms1_sparse_matrix() + # Max distance + distances = np.max(distances, axis=-1) - # Next get the rt sparse matrices + # Perform clustering + try: + clustering = AgglomerativeClustering(n_clusters=None, + linkage='complete', + metric='precomputed', + distance_threshold=1, + connectivity=cmat).fit(distances) + features['cluster'] = clustering.labels_ - # Next get the mz sparse matrices + # All data points are singleton clusters + except: + features['cluster'] = np.arange(len(features.index)) - # Next get the ms2 sparse matrices + return features - # Next get the peak shape sparse matrices diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index f92df5ad6..3a67450af 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1110,9 +1110,13 @@ def __getitem__(self, index): def __len__(self): return len(self.samples) - def combine_mass_features(self): + def _combine_mass_features(self): """ Concatenates the mass features from all the LCMS objects in the collection. + + Returns + -------- + None, sets the _combined_mass_features attribute. """ mf_df_list = [] for lcms_obj in self: @@ -1142,49 +1146,53 @@ def mass_features_to_df(self): """Returns a pandas dataframe summarizing all the mass features in the collection.""" # Check if combined_mass_features is set, set if not if self._combined_mass_features is None: - self.combine_mass_features() + self._combine_mass_features() # Check if scan_time_aligned is in combined_mass_features, try to add if not elif self._combined_mass_features is not None and "scan_time_aligned" not in self._combined_mass_features: if all([True for x in self if "scan_time_aligned" in x.scan_df.columns]): - self.combine_mass_features() - + self._combine_mass_features() + df = pd.DataFrame(self._combined_mass_features) return df - - - def plot_tics(self, ms_level=1, corrected_rt=False): + def plot_tics(self, ms_level=1, type = "raw"): """Plots the TICs for all the LCMS objects in the collection. Parameters ----------- ms_level : int, optional The MS level to plot the TICs for. Defaults to 1. - corrected_rt : bool, optional - If True, plots the corrected retention time. Defaults to False. + type : str, optional + The type of TIC to plot, either "raw" or "corrected" or "both". Defaults to "raw". """ import matplotlib.pyplot as plt - fig, ax = plt.subplots() - for lcms_obj in self: - scan_df = lcms_obj.scan_df - scan_df = scan_df[scan_df.ms_level == ms_level] - if corrected_rt: - # Check that scan_time_aligned is key in scan_df - if "scan_time_aligned" not in scan_df.columns: - raise ValueError(f"scan_time_aligned not found in scan_df for {lcms_obj.sample_name}") - else: - ax.plot(scan_df.scan_time_aligned, scan_df.tic, label=lcms_obj.sample_name) - else: - ax.plot(scan_df.scan_time, scan_df.tic, label=lcms_obj.sample_name) - ax.set_xlabel("Retention Time (min)") - ax.set_ylabel("TIC") - ax.legend() - # Add overall title - if corrected_rt: - plt.suptitle("TICs for LCMS Collection (Aligned RT)") + to_plot = [] + if type == "both": + to_plot = ["raw", "corrected"] else: - plt.suptitle("TICs for LCMS Collection") + to_plot = [type] + + fig, axs = plt.subplots( + len(to_plot), 1, figsize=(10, 5 * len(to_plot)), sharex=True, squeeze=False + ) + + for i, plot_type in enumerate(to_plot): + ax = axs[i, 0] + for lcms_obj in self: + scan_df = lcms_obj.scan_df + scan_df = scan_df[scan_df.ms_level == ms_level] + if plot_type == "corrected": + # Check that scan_time_aligned is key in scan_df + if "scan_time_aligned" not in scan_df.columns: + raise ValueError(f"scan_time_aligned not found in scan_df for {lcms_obj.sample_name}") + else: + ax.plot(scan_df.scan_time_aligned, scan_df.tic, label=lcms_obj.sample_name) + elif plot_type == "raw": + ax.plot(scan_df.scan_time, scan_df.tic, label=lcms_obj.sample_name) + ax.set_xlabel("Retention Time (min," + f" {plot_type})" ) + ax.set_ylabel("TIC") + ax.legend() plt.show() @property diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 116adcaed..04e48bbd2 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -12,8 +12,9 @@ lcms_collection = parser.get_lcms_collection(load_raw=False, load_light=True) #lcms_collection.plot_tics() lcms_collection.align_lcms_objects() - #lcms_collection.plot_tics(corrected_rt=True) - mass_feature_df = lcms_collection.mass_features_to_df() + #lcms_collection.plot_tics(type="both") + #mass_feature_df = lcms_collection.mass_features_to_df() + lcms_collection.add_consensus_mass_features() print("Here") #lcms_collection. \ No newline at end of file From c4f3d0874465d09ec634fd2d38896a5955d6a37d Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 20 Aug 2024 15:11:31 -0700 Subject: [PATCH 011/158] Start building consensus mass feature functionality --- corems/mass_spectra/calc/lc_calc.py | 241 +++++++++++------- corems/mass_spectra/factory/lc_class.py | 37 ++- .../nmdc/lipidomics/lipidomics_collection.py | 3 +- .../nmdc/lipidomics/lipidomics_workflow.py | 4 +- 4 files changed, 192 insertions(+), 93 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index f2a98f439..f1a941338 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1551,7 +1551,7 @@ def match_mfs(self, mf_c, mf_i): # Safely cast to list dims = ["mz", "scan_time"] relative = [False, False] - tol = [0.0001, 1] #TODO KRH: source this from a parameter, make mz relative + tol = [0.001, 1.5] #TODO KRH: source this from a parameter, make mz relative # Compute inter-feature distances idx = [] @@ -1585,29 +1585,25 @@ def match_mfs(self, mf_c, mf_i): dist3d = np.multiply(dist3d, idx) # Normalize to 0-1 - mx = dist3d.max() + mx = dist3d.max() if mx > 0: + # Lower distance is better dist3d = dist3d / dist3d.max() - # Intensities - intensity = np.repeat(mf_c['intensity'].values.reshape(-1, 1), - mf_i.shape[0], axis=1) - intensity = np.multiply(intensity, idx) + # Turn zeros to inf (no match) + dist3d[dist3d == 0] = np.inf - # Max over dims - maxcols = np.max(intensity, axis=0, keepdims=True) + # Min over dims + mincols = np.min(dist3d, axis=0, keepdims=True) - # Zero out nonmax over dims - intensity[intensity != maxcols] = 0 + # Zero out mincols over dims + dist3d[dist3d != mincols] = np.inf - # Break ties by distance - intensity = intensity - dist3d - - # Max over clusters - maxrows = np.max(intensity, axis=1, keepdims=True) + # Min over clusters + minrows = np.min(dist3d, axis=1, keepdims=True) # Where max and nonzero - ii, jj = np.where((intensity == maxrows) & (intensity > 0)) + ii, jj = np.where((dist3d == minrows) & (dist3d < np.inf)) # Reorder mf_c = mf_c.iloc[ii] @@ -1625,9 +1621,9 @@ def fit_rts(self, a, b, align='scan_time', **kwargs): Parameters ---------- a : :obj:`~pandas.DataFrame` - First set of input feature coordinates and intensities. + First set of input feature coordinates and intensities; the center object and the object to align to. b : :obj:`~pandas.DataFrame` - Second set of input feature coordinates and intensities. + Second set of input feature coordinates and intensities; the object to align to the center object. align : str Dimension to align. kwargs @@ -1636,8 +1632,8 @@ def fit_rts(self, a, b, align='scan_time', **kwargs): Returns ------- - :obj:`~scipy.interpolate.interp1d` - Interpolated fit of the SVR result. + :obj:`~function` + An interpolation function where one can input a retention time and get the predicted retention time. Notes ----- @@ -1670,14 +1666,53 @@ def fit_rts(self, a, b, align='scan_time', **kwargs): else: # Fit svr = SVR(**kwargs) - svr.fit(arr[:, 0].reshape(-1, 1), arr[:, 1]) + svr.fit(arr[:, 1].reshape(-1, 1), arr[:, 0]) # Predict newy = svr.predict(newx.reshape(-1, 1)) - - return scipy.interpolate.interp1d(newx, newy, - kind='linear', fill_value='extrapolate') + + # Pad x and y_pred with zeros to force interpolation to start at 0 + newx = np.concatenate(([0], newx)) + newy = np.concatenate(([0], newy)) + + # Pad x and y_pred with max time to force interpolation to end at max time to force interpolation to match at end max time + max_time = self[0].scan_df['scan_time'].max() + newx = np.concatenate((newx, [max_time])) + newy = np.concatenate((newy, [max_time])) + + # Return an interpolation function for the x and y_pred + def interp(x): + pred_y = np.interp(x, newx, newy) + return pred_y + + return interp + def get_anchor_mass_features(self, mf_df): + """ + Get the anchor mass features from a DataFrame of mass features. + + Parameters + ---------- + mf_df : :obj:`~pandas.DataFrame` + The mass features to filter to just the anchor mass features. + + Returns + ------- + :obj:`~pandas.DataFrame` + The anchor mass features dataframe. + """ + #TODO KRH: add error handling and the ability to implement other anchoring techniques through parameters + mf_df = mf_df.copy() + + # Drop features that are not mass_spectrum_deconvoluted_parent or are NA as mass_spectrum_deconvoluted_parent + mf_df = mf_df.dropna(subset=["mass_spectrum_deconvoluted_parent"]) + mf_df = mf_df[mf_df["mass_spectrum_deconvoluted_parent"]] + + # Drop features that have NA as dispersity_index or half_height_width (generally bad shape) + mf_df = mf_df.dropna(subset=["dispersity_index", "half_height_width", "mass_spectrum_deconvoluted_parent"]) + + return mf_df + def align_lcms_objects(self): """ Align LCMS objects in the collection. @@ -1686,6 +1721,8 @@ def align_lcms_objects(self): First, the mass features in the center LCMS object are matched to the mass features in the other LCMS objects, starting with the LCMS object immediately following the center LCMS object. The retention times of the LCMS objects are then fit to the center LCMS object using the matched mass features. + + Currently, this function only aligns LCMS objects within each batch, but not between batches. Returns ------- @@ -1698,65 +1735,74 @@ def align_lcms_objects(self): """ #TODO KRH: add the ability to do this by batch and then connect the batches # Prepare the center LCMS object - center_obj_id = 0 #KRH TODO: make this part of the manifest or infer during intialization (QC sample or the center in the order) - mf_df_c = self[center_obj_id].mass_features_to_df().copy() - # Drop mass featuers with nan in mass_spectrum_deconvoluted_parent - mf_df_c = mf_df_c.dropna(subset=["mass_spectrum_deconvoluted_parent"]) - mf_df_c = mf_df_c[mf_df_c["mass_spectrum_deconvoluted_parent"]] - center_scan_df = self[center_obj_id].scan_df.copy() - center_scan_df['scan_time_aligned'] = center_scan_df['scan_time'] - self[center_obj_id].scan_df = center_scan_df - - index_steps = (1, -1) - # Run this twice, once going forward (+1 indexing) and once going backward (-1 indexing) - for index_step in index_steps: - # Loop through the other LCMS objects in the collection (going forward) - i = center_obj_id + index_step - if i < len(self) and i >= 0: - # Grab the first LCMS object after the center object - lc_obj = self[i] - mf_df_i = lc_obj.mass_features_to_df().copy() - mf_df_i['scan_time_og'] = mf_df_i['scan_time'] - - while mf_df_i is not None: - mf_df_i = mf_df_i.dropna(subset=["mass_spectrum_deconvoluted_parent"]) - mf_df_i = mf_df_i[mf_df_i["mass_spectrum_deconvoluted_parent"]] - - # Match the mass features in the LCMS object to the anchor mass features in the center LCMS object. - matches_c, matches_i = self.match_mfs(mf_df_c, mf_df_i) - if matches_c is not None: - # Reset the scan_time to the original scan_time - matches_i = matches_i.copy() - matches_i['scan_time'] = matches_i['scan_time_og'] - - # Fit the retention times of the LCMS object to the center LCMS object using the matched mass features - spl = self.fit_rts(matches_c, matches_i, kernel='rbf', C=1000) - # Set new retention times on scan_df for lc_obj - new_times = spl(lc_obj.scan_df['scan_time']) - new_scan_info = lc_obj.scan_df.copy() - new_scan_info['scan_time_aligned'] = new_times - lc_obj.scan_df = new_scan_info - i += index_step - if i >= len(self) or i == 0: - mf_df_i = None + center_obj_ids = self.manifest_dataframe[self.manifest_dataframe['center']].collection_id.values + for center_obj_id in center_obj_ids: + mf_df_c = self[center_obj_id].mass_features_to_df().copy() + mf_df_c = mf_df_c.reset_index(drop=False) + # Drop mass featuers with nan in mass_spectrum_deconvoluted_parent + mf_df_c = self.get_anchor_mass_features(mf_df_c) + center_scan_df = self[center_obj_id].scan_df.copy() + center_scan_df['scan_time_aligned'] = center_scan_df['scan_time'] + self[center_obj_id].scan_df = center_scan_df + + index_steps = (1, -1) + # Run this twice, once going forward (+1 indexing) and once going backward (-1 indexing) + for index_step in index_steps: + # Loop through the other LCMS objects in the collection (going forward) + i = center_obj_id + index_step + if i < len(self) and i >= 0: + # Grab the first LCMS object after the center object + lc_obj = self[i] + mf_df_i = lc_obj.mass_features_to_df().copy() + # Remove index from mass features + mf_df_i = mf_df_i.reset_index(drop=False) + mf_df_i['scan_time_og'] = mf_df_i['scan_time'] + + while mf_df_i is not None: + mf_df_i = self.get_anchor_mass_features(mf_df_i) + + # Match the mass features in the LCMS object to the anchor mass features in the center LCMS object. + matches_c, matches_i = self.match_mfs(mf_df_c, mf_df_i) + if matches_c is not None: + # Reset the scan_time to the original scan_time + matches_i = matches_i.copy() + matches_i['scan_time'] = matches_i['scan_time_og'] + + # Fit the retention times of the LCMS object to the center LCMS object using the matched mass features + spl = self.fit_rts(matches_c, matches_i, kernel='rbf', C=1000) + # Set new retention times on scan_df for lc_obj + matches_i['scan_time_fit'] = spl(matches_i['scan_time']) + new_times = spl(lc_obj.scan_df['scan_time']) + new_scan_info = lc_obj.scan_df.copy() + new_scan_info['scan_time_aligned'] = new_times + lc_obj.scan_df = new_scan_info + i += index_step + if i >= len(self) or i < 0: + mf_df_i = None + else: + # Grab the next LCMS object and use the previous spline fitting to get a better starting point + lc_obj = self[i] + mf_df_i = lc_obj.mass_features_to_df().copy() + mf_df_i['scan_time_og'] = mf_df_i['scan_time'] + mf_df_i = mf_df_i.reset_index(drop=False) + # Set scan_time to previous sample's predicted scan_time to find closer matches + mf_df_i['scan_time'] = spl(mf_df_i['scan_time']) else: - # Grab the next LCMS object and use the previous spline fitting to get a better starting point - lc_obj = self[i] - mf_df_i = lc_obj.mass_features_to_df().copy() - mf_df_i['scan_time_og'] = mf_df_i['scan_time'] - # Set scan_time to previous sample's predicted scan_time to find closer matches - mf_df_i['scan_time'] = spl(mf_df_i['scan_time']) - else: - raise ValueError(f'No matches found between the center object and {self.samples[i]}') + raise ValueError(f'No matches found between the center object and {self.samples[i]}') def add_consensus_mass_features(self): - # Check if the LCMS objects are aligned - if not all([True for x in self if "scan_time_aligned" in x.scan_df.columns]): - raise ValueError("LCMS objects are not aligned, run align_lcms_objects() first") - # Get the combined mass features from all LCMS objects - combined_mfs = self.mass_features_to_df().copy() - self.agglomerative_clustering(combined_mfs) + combined_mfs = self.mass_features_dataframe + + # Check if the mass features have been aligned + if 'scan_time_aligned' not in combined_mfs.columns: + raise ValueError('Mass features have not been aligned, run align_lcms_objects() first') + + # Drop mass features with nan in mass_spectrum_deconvoluted_parent and if they are not mass_spectrum_deconvoluted_parent + combined_mfs = combined_mfs.dropna(subset=["mass_spectrum_deconvoluted_parent"]) + combined_mfs = combined_mfs[combined_mfs["mass_spectrum_deconvoluted_parent"]] + + test = self.agglomerative_clustering(combined_mfs) def agglomerative_clustering(self, features): ''' @@ -1789,17 +1835,19 @@ def agglomerative_clustering(self, features): # Safely cast to list dims = ["mz", "scan_time"] relative = [False, False] - tol = [0.0001, 1] #TODO KRH: source this from a parameter, make mz relative + tol = [0.001, 0.3] #TODO KRH: source this from a parameter, make mz relative # Copy input features = features.copy() # Make connectivity matrix for masking within sample mass features - if 'sample_idx' not in features.columns: + if 'sample_id' not in features.columns: cmat = None else: vals = features['sample_id'].values.reshape(-1, 1) - cmat = cdist(vals, vals, metric=lambda x, y: x != y).astype(bool) + cmat = scipy.spatial.distance.cdist(vals, vals) + # Convert to binary (0 if same sample, 1 if different) + cmat = np.where(cmat == 0, np.nan, 1) # Compute inter-feature distances distances = [] @@ -1820,22 +1868,37 @@ def agglomerative_clustering(self, features): dist = np.divide(dist, basis, out=np.zeros_like( basis), where=basis != 0) - # Check tol - distances.append(dist / tol[i]) + # Check tolerance, convert to binary for masking + idx = dist <= tol[i] + idx = np.where(idx, 1, np.nan) + + # Multiply by distance + dist = np.multiply(dist, idx) + + # Normalize to the maximum distance that is not nan + dist = dist / np.nanmax(dist) - # Stack distances + distances.append(dist) + + # Mutliply distances distances = np.dstack(distances) - # Max distance - distances = np.max(distances, axis=-1) + # Mean distance + distances = np.prod(distances, axis=-1) + + # Multiply by connectivity matrix for more masking + if cmat is not None: + distances = np.multiply(distances, cmat) + + # Recast nan to 1 (maximum distance, no linkage) + distances[np.isnan(distances)] = 1 # Perform clustering try: clustering = AgglomerativeClustering(n_clusters=None, linkage='complete', metric='precomputed', - distance_threshold=1, - connectivity=cmat).fit(distances) + distance_threshold=1).fit(distances) features['cluster'] = clustering.labels_ # All data points are singleton clusters diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index a81adda65..e3a480b90 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1144,10 +1144,24 @@ def _combine_mass_features(self): top_cols = ["coll_mf_id", "sample_name", "sample_id", "mz", "scan_time_aligned", "a test"] cols = [x for x in top_cols + [col for col in cols if col not in top_cols] if x in cols] combined_mass_features = combined_mass_features[cols] + # Make coll_mf_id the index + combined_mass_features = combined_mass_features.set_index("coll_mf_id") self._combined_mass_features = combined_mass_features.to_dict() def mass_features_to_df(self): - """Returns a pandas dataframe summarizing all the mass features in the collection.""" + """Returns a pandas dataframe summarizing all the mass features in the collection. + + Returns + -------- + pandas.DataFrame + A pandas dataframe of mass features in the collection. + + Notes + ------ + If _combined_mass_features is not set, calls _combine_mass_features to set it. + If scan_time_aligned is not in the _combined_mass_features, tries to add it. + + """ # Check if combined_mass_features is set, set if not if self._combined_mass_features is None: self._combine_mass_features() @@ -1199,6 +1213,27 @@ def plot_tics(self, ms_level=1, type = "raw"): ax.legend() plt.show() + def plot_alignments(self): + """Plots the alignment of the LCMS objects in the collection.""" + import matplotlib.pyplot as plt + fig, ax = plt.subplots(figsize=(10, 5)) + for lcms_obj in self: + scan_df = lcms_obj.scan_df + if "scan_time_aligned" not in scan_df.columns: + raise ValueError(f"scan_time_aligned not found in scan_df for {lcms_obj.sample_name}") + scan_df['time_diff'] = scan_df.scan_time - scan_df.scan_time_aligned + ax.plot(scan_df.scan_time_aligned, scan_df.time_diff, label=lcms_obj.sample_name) + ax.set_xlabel("Aligned Retention Time (min)") + ax.set_ylabel("Time Difference (min)") + ax.legend() + plt.show() + + @property + def mass_features_dataframe(self): + if self._combined_mass_features is None: + self._combine_mass_features() + return self.mass_features_to_df() + @property def samples(self): manifest_df = self.manifest_dataframe diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 04e48bbd2..2f8cbe274 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -3,7 +3,7 @@ if __name__ == "__main__": - collection_path = Path("tmp_data/NMDC_processed_collection_0819") + collection_path = Path("tmp_data/NMDC_processed_collection_0820") manifest_file = collection_path / "manifest.csv" parser = ReadCoreMSHDFMassSpectraCollection( folder_location = collection_path, @@ -13,6 +13,7 @@ #lcms_collection.plot_tics() lcms_collection.align_lcms_objects() #lcms_collection.plot_tics(type="both") + #lcms_collection.plot_alignments() #mass_feature_df = lcms_collection.mass_features_to_df() lcms_collection.add_consensus_mass_features() print("Here") diff --git a/support_code/nmdc/lipidomics/lipidomics_workflow.py b/support_code/nmdc/lipidomics/lipidomics_workflow.py index 9eb77e4d6..052732a42 100644 --- a/support_code/nmdc/lipidomics/lipidomics_workflow.py +++ b/support_code/nmdc/lipidomics/lipidomics_workflow.py @@ -569,9 +569,9 @@ def run_lipid_workflow( if __name__ == "__main__": # Set input variables to run - cores = 1 + cores = 7 file_dir = Path("tmp_data/thermo_raw_collection") - out_dir = Path("tmp_data/NMDC_processed_collection_0819") + out_dir = Path("tmp_data/NMDC_processed_collection_0820") params_toml = Path("tmp_data/thermo_raw_collection/nmdc_lipid_params.toml") verbose = True From e33d330db286f63623d978766cf688f4657b98e6 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 26 Aug 2024 16:27:26 -0700 Subject: [PATCH 012/158] Begin adding more dimensions to agglomerative_clustering, move to KDtree for speed --- corems/mass_spectra/calc/lc_calc.py | 121 ++++++++++++------ .../nmdc/lipidomics/lipidomics_collection.py | 1 - 2 files changed, 81 insertions(+), 41 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index f1a941338..15e59da18 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1548,9 +1548,16 @@ def match_mfs(self, mf_c, mf_i): if mf_c is None or mf_i is None: return None, None - # Safely cast to list - dims = ["mz", "scan_time"] - relative = [False, False] + # Prepare dataframes + mf_c = mf_c.copy() + mf_c['id_i'] = 0 + mf_i = mf_i.copy() + mf_i['id_i'] = 1 + mf_ci = pd.concat([mf_c, mf_i], axis=0) + + # Set dimensions for matching + dims = ["mz", "scan_time"] #TODO KRH: source this from a parameter, make mz relative + relative = [False, False] #TODO KRH: source this from a parameter, make mz relative tol = [0.001, 1.5] #TODO KRH: source this from a parameter, make mz relative # Compute inter-feature distances @@ -1799,8 +1806,8 @@ def add_consensus_mass_features(self): raise ValueError('Mass features have not been aligned, run align_lcms_objects() first') # Drop mass features with nan in mass_spectrum_deconvoluted_parent and if they are not mass_spectrum_deconvoluted_parent - combined_mfs = combined_mfs.dropna(subset=["mass_spectrum_deconvoluted_parent"]) - combined_mfs = combined_mfs[combined_mfs["mass_spectrum_deconvoluted_parent"]] + #combined_mfs = combined_mfs.dropna(subset=["mass_spectrum_deconvoluted_parent"]) + #combined_mfs = combined_mfs[combined_mfs["mass_spectrum_deconvoluted_parent"]] test = self.agglomerative_clustering(combined_mfs) @@ -1833,12 +1840,20 @@ def agglomerative_clustering(self, features): return None # Safely cast to list - dims = ["mz", "scan_time"] - relative = [False, False] - tol = [0.001, 0.3] #TODO KRH: source this from a parameter, make mz relative + # TODO KRH: source this from a parameter + # TODO KRH: make mz relative + dims = ["mz", "scan_time_aligned", "tailing_factor", "dispersity_index"] + relative = [False, False, False, False] + tol = [0.001, 0.2, 0.4, 0.1] + dist_weight = [1, 1, 0.2, 0.5] + na_fill = [None, None, 1, np.nanmedian(features.dispersity_index)] + + # Check that the dimensions and tolerances are the same length + if len(dims) != len(tol) or len(dims) != len(relative) or len(dims) != len(na_fill) or len(dims) != len(dist_weight): + raise ValueError("The dimensions, tolerances, relative, dist_weight, and na_allow lists must be the same length") # Copy input - features = features.copy() + features = features.copy().reset_index(drop=False) # Make connectivity matrix for masking within sample mass features if 'sample_id' not in features.columns: @@ -1847,51 +1862,76 @@ def agglomerative_clustering(self, features): vals = features['sample_id'].values.reshape(-1, 1) cmat = scipy.spatial.distance.cdist(vals, vals) # Convert to binary (0 if same sample, 1 if different) - cmat = np.where(cmat == 0, np.nan, 1) + cmat = np.where(cmat == 0, 0, 1) + # Convert to coorindate matrix for sparse operations later + cmat = sparse.coo_matrix(cmat) - # Compute inter-feature distances - distances = [] - for i, d in enumerate(dims): - # Vectors - v1 = features[d].values.reshape(-1, 1) + # Compute inter-feature distances using sparse matrix approach + distances = None + for i in range(len(dims)): + # Construct k-d tree + values = features[dims[i]].values - # Distances - dist = scipy.spatial.distance.cdist(v1, v1) + # Fill NAs if applicable + if na_fill[i] is not None: + values = np.where(np.isnan(values), na_fill[i], values) + + tree = KDTree(values.reshape(-1, 1)) + max_tol = tol[i] if relative[i] is True: - # Divisor - basis = np.repeat(v1, v1.shape[0], axis=1) - fix = np.repeat(v1, v1.shape[0], axis=1).T - basis = np.where(basis == 0, fix, basis) + # Maximum absolute tolerance + max_tol = tol[i] * values.max() - # Divide - dist = np.divide(dist, basis, out=np.zeros_like( - basis), where=basis != 0) + # Compute sparse distance matrix + # the larger the max_tol, the slower this operation is + sdm = tree.sparse_distance_matrix(tree, max_tol, output_type="coo_matrix") - # Check tolerance, convert to binary for masking - idx = dist <= tol[i] - idx = np.where(idx, 1, np.nan) + # Only consider forward case, exclude diagonal + sdm = sparse.triu(sdm, k=1) + + # Filter relative distances + if relative[i] is True: + # Compute relative distances + rel_dists = sdm.data / values[sdm.row] # or col? + + # Indices of relative distances less than tolerance + idx = rel_dists <= tol[i] - # Multiply by distance - dist = np.multiply(dist, idx) + # Reconstruct sparse distance matrix + sdm = sparse.coo_matrix( + (rel_dists[idx], (sdm.row[idx], sdm.col[idx])), + shape=(len(values), len(values)), + ) - # Normalize to the maximum distance that is not nan - dist = dist / np.nanmax(dist) + # Stack distances for dimensions where na_allow is False + if distances is None: + sdm.data = sdm.data*dist_weight[i] + distances = sdm + else: + # Prepare sdm to match shape of existing distances + distances_truth = distances.copy() + distances_truth.data = np.ones_like(distances_truth.data) + sdm = distances_truth.multiply(sdm) + sdm.data = sdm.data*dist_weight[i] - distances.append(dist) + sdm_truth = sdm.copy() + sdm_truth.data = np.ones_like(sdm_truth.data) - # Mutliply distances - distances = np.dstack(distances) + # remove the distances that are not sdm + distances = distances.multiply(sdm_truth) - # Mean distance - distances = np.prod(distances, axis=-1) + # Add the new distances + distances = distances + sdm # Multiply by connectivity matrix for more masking - if cmat is not None: - distances = np.multiply(distances, cmat) + distances = distances.multiply(cmat) - # Recast nan to 1 (maximum distance, no linkage) - distances[np.isnan(distances)] = 1 + # Convert to full matrix + distances = distances.todense() + # Cast all 0s to 1s for a distance matrix + distances[distances == 0] = 1 + distances = np.asarray(distances) # Perform clustering try: @@ -1905,6 +1945,7 @@ def agglomerative_clustering(self, features): except: features['cluster'] = np.arange(len(features.index)) + features_sub = features[['cluster', 'mz', 'scan_time_aligned', 'sample_id', 'tailing_factor', 'dispersity_index', 'half_height_width', 'mass_spectrum_deconvoluted_parent']] return features diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 2f8cbe274..689bd9691 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -10,7 +10,6 @@ manifest_file = manifest_file ) lcms_collection = parser.get_lcms_collection(load_raw=False, load_light=True) - #lcms_collection.plot_tics() lcms_collection.align_lcms_objects() #lcms_collection.plot_tics(type="both") #lcms_collection.plot_alignments() From 5d3e9fc5af679d71bad6b629ae64ea0a1f99b772 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 12 Sep 2024 11:01:01 -0700 Subject: [PATCH 013/158] Add memory saving and multiprocess loading --- corems/chroma_peak/calc/subset.py | 406 ++++++++++++++++++ corems/mass_spectra/calc/lc_calc.py | 87 ++-- corems/mass_spectra/factory/lc_class.py | 139 ++++-- corems/mass_spectra/input/corems_hdf5.py | 57 ++- .../nmdc/lipidomics/lipidomics_collection.py | 38 +- .../nmdc/lipidomics/lipidomics_workflow.py | 31 +- 6 files changed, 673 insertions(+), 85 deletions(-) create mode 100644 corems/chroma_peak/calc/subset.py diff --git a/corems/chroma_peak/calc/subset.py b/corems/chroma_peak/calc/subset.py new file mode 100644 index 000000000..e207e2f72 --- /dev/null +++ b/corems/chroma_peak/calc/subset.py @@ -0,0 +1,406 @@ +# This file contains functions for subsetting dataframes that contain mass feature data. +# This is based on the deimos package, found here: https://github.com/pnnl/deimos/blob/master/deimos/subset.py with some modifications. + +import multiprocessing as mp +from functools import partial + +import dask.dataframe as dd +import numpy as np +import pandas as pd + + +class Partitions: + ''' + Generator object that will lazily build and return each partition. + + Attributes + ---------- + features : :obj:`~pandas.DataFrame` + Input feature coordinates and intensities. + split_on : str + Dimension to partition the data. + size : int + Target partition size. + overlap : float + Amount of overlap between partitions to ameliorate edge effects. + + ''' + + def __init__(self, features, split_on='mz', size=1000, overlap=0.05): + ''' + Initialize :obj:`~deimos.subset.Partitions` instance. + + Parameters + ---------- + features : :obj:`~pandas.DataFrame` + Input feature coordinates and intensities. + split_on : str + Dimension to partition the data. + size : int + Target partition size. + overlap : float + Amount of overlap between partitions to ameliorate edge effects. + + ''' + + self.features = features + self.split_on = split_on + self.size = size + self.overlap = overlap + + self._compute_splits() + + def _compute_splits(self): + ''' + Determines data splits for partitioning. + + ''' + + # Unique to split on + idx = np.unique(self.features[self.split_on].values) + + # Number of partitions + partitions = np.ceil(len(idx) / self.size) + + # Determine partition bounds + bounds = [[x.min(), x.max()] for x in np.array_split(idx, partitions)] + for i in range(1, len(bounds)): + bounds[i][0] = bounds[i - 1][1] - self.overlap + + if (self.overlap > 0) & (len(bounds) > 1): + # Functional bounds + fbounds = [] + for i in range(len(bounds)): + a, b = bounds[i] + + # First partition + if i < 1: + b = b - self.overlap / 2 + + # Middle partitions + elif i < len(bounds) - 1: + a = a + self.overlap / 2 + b = b - self.overlap / 2 + + # Last partition + else: + a = a + self.overlap / 2 + + fbounds.append([a, b]) + else: + fbounds = bounds + + self.bounds = bounds + self.fbounds = fbounds + + def __iter__(self): + ''' + Yields each partition. + + Yields + ------ + :obj:`~pandas.DataFrame` + Partition of feature coordinates and intensities. + + ''' + + for a, b in self.bounds: + yield slice(self.features, by=self.split_on, low=a, high=b) + + def map(self, func, processes=1, **kwargs): + ''' + Maps `func` to each partition, then returns the combined result, + accounting for overlap regions. + + Parameters + ---------- + func : function + Function to apply to partitions. + processes : int + Number of parallel processes. If less than 2, a serial mapping is + applied. + kwargs + Keyword arguments passed to `func`. + + Returns + ------- + :obj:`~pandas.DataFrame` + Combined result of `func` applied to partitions. + + ''' + + # Serial + if processes < 2: + result = [func(x, **kwargs) for x in self] + + # Parallel + else: + with mp.Pool(processes=processes) as p: + result = list(p.imap(partial(func, **kwargs), self)) + + # Reconcile overlap + result = [slice(result[i], by=self.split_on, low=a, high=b) + for i, (a, b) in enumerate(self.fbounds)] + + # Combine partitions + return pd.concat(result).reset_index(drop=True) + + def zipmap(self, func, b, processes=1, **kwargs): + ''' + Maps `func` to each partition pair resulting from the zip operation of + `self` and `b`, then returns the combined result, accounting for + overlap regions. + + Parameters + ---------- + func : function + Function to apply to zipped partitions. Must accept and return two + :obj:`~pandas.DataFrame` instances. + b : :obj:`~pandas.DataFrame` + Input feature coordinates and intensities. + processes : int + Number of parallel processes. If less than 2, a serial mapping is + applied. + kwargs + Keyword arguments passed to `func`. + + Returns + ------- + a, b : :obj:`~pandas.DataFrame` + Result of `func` applied to paired partitions. + + ''' + + # Partition other dataset + partitions = (slice(b, by=self.split_on, low=a, high=b_) + for a, b_ in self.bounds) + + # Serial + if processes < 2: + result = [func(a, b_, **kwargs) for a, b_ in zip(self, partitions)] + + # Parallel + else: + with mp.Pool(processes=processes) as p: + result = list(p.starmap(partial(func, **kwargs), + zip(self, partitions))) + + result = {'a': [x[0] for x in result], 'b': [x[1] for x in result]} + + # Reconcile overlap + tmp = [slice(result['a'][i], by=self.split_on, low=a, high=b_, + return_index=True) + for i, (a, b_) in enumerate(self.fbounds)] + + result['a'] = [x[0] for x in tmp] + idx = [x[1] for x in tmp] + result['b'] = [p.iloc[i, :] if i is not None else None for p, + i in zip(result['b'], idx)] + + # Combine partitions + result['a'] = pd.concat(result['a']) + result['b'] = pd.concat(result['b']) + + return result['a'], result['b'] + + +class MultiSamplePartitions: + ''' + Generator object that will lazily build and return each partition constructed + from multiple samples. + + Attributes + ---------- + features : :obj:`~pandas.DataFrame` or :obj:`~dask.dataframe.DataFrame` + Input feature coordinates and intensities. + split_on : str + Dimension to partition the data. + size : int + Target partition size. + tol : float + Largest allowed distance between unique `split_on` observations. + + ''' + + def __init__(self, features, split_on='mz', size=500, tol=25E-6): + ''' + Initialize :obj:`~deimos.subset.Partitions` instance. + + Parameters + ---------- + features : :obj:`~pandas.DataFrame` or :obj:`~dask.dataframe.DataFrame` + Input feature coordinates and intensities. + split_on : str + Dimension to partition the data. + size : int + Target partition size. + tol : float + Largest allowed distance between unique `split_on` observations. + + ''' + + self.features = features + self.split_on = split_on + self.size = size + self.tol = tol + + if isinstance(features, dd.DataFrame): + self.dask = True + else: + self.dask = False + + self._compute_splits() + + def _compute_splits(self): + ''' + Determines data splits for partitioning. + + ''' + + self.counter = 0 + + if self.dask: + idx = self.features.groupby( + by=self.split_on).size().compute().sort_index() + else: + idx = self.features.groupby(by=self.split_on).size().sort_index() + + counts = idx.values + idx = idx.index + + dxs = np.diff(idx) / idx[:-1] + + bins = [] + current_count = counts[0] + current_bin = [idx[0]] + self._counts = [] + + for i, dx in zip(range(1, len(idx)), dxs): + if (current_count + counts[i] <= self.size) or (dx <= self.tol): + current_bin.append(idx[i]) + current_count += counts[i] + + else: + bins.append(np.array(current_bin)) + self._counts.append(current_count) + + current_bin = [idx[i]] + current_count = counts[i] + + # Add last unadded bin + bins.append(np.array(current_bin)) + self._counts.append(current_count) + + self.bounds = np.array([[x.min(), x.max()] for x in bins]) + + def __iter__(self): + return self + + def __next__(self): + if self.counter < len(self.bounds): + q = '({} >= {}) & ({} <= {})'.format(self.split_on, + self.bounds[self.counter][0], + self.split_on, + self.bounds[self.counter][1]) + + if self.dask: + subset = self.features.query(q).compute() + else: + subset = self.features.query(q) + + self.counter += 1 + if len(subset.index) > 1: + return subset + else: + return None + + raise StopIteration + + def map(self, func, processes=1, **kwargs): + ''' + Maps `func` to each partition, then returns the combined result. + + Parameters + ---------- + func : function + Function to apply to partitions. + processes : int + Number of parallel processes. If less than 2, a serial mapping is + applied. + kwargs + Keyword arguments passed to `func`. + + Returns + ------- + :obj:`~pandas.DataFrame` + Combined result of `func` applied to partitions. + + ''' + + # Serial + if processes < 2: + result = [func(x, **kwargs) for x in self] + + # Parallel + else: + with mp.Pool(processes=processes) as p: + result = list(p.imap(partial(func, **kwargs), self)) + + # Add partition index + for i in range(len(result)): + if result[i] is not None: + result[i]['partition_idx'] = i + + # Combine partitions + return pd.concat(result, ignore_index=True) + + +def partition(features, split_on='mz', size=1000, overlap=0.05): + ''' + Partitions data along a given dimension. + + Parameters + ---------- + features : :obj:`~pandas.DataFrame` + Input feature coordinates and intensities. + split_on : str + Dimension to partition the data. + size : int + Target partition size. + overlap : float + Amount of overlap between partitions to ameliorate edge effects. + + Returns + ------- + :obj:`~deimos.subset.Partitions` + A generator object that will lazily build and return each partition. + + ''' + + return Partitions(features, split_on, size, overlap) + + +def multi_sample_partition(features, split_on='mz', size=500, tol=25E-6): + ''' + Partitions data along a given dimension. For use with features across + multiple samples, e.g. in alignment. + + Parameters + ---------- + features : :obj:`~pandas.DataFrame` or :obj:`~dask.dataframe.DataFrame` + Input feature coordinates and intensities. + split_on : str + Dimension to partition the data. + size : int + Target partition size. + tol : float + Largest allowed distance between unique `split_on` observations. + + Returns + ------- + :obj:`~deimos.subset.Partitions` + A generator object that will lazily build and return each partition. + + ''' + + return MultiSamplePartitions(features, split_on, size, tol) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 15e59da18..351d46471 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1553,12 +1553,11 @@ def match_mfs(self, mf_c, mf_i): mf_c['id_i'] = 0 mf_i = mf_i.copy() mf_i['id_i'] = 1 - mf_ci = pd.concat([mf_c, mf_i], axis=0) # Set dimensions for matching dims = ["mz", "scan_time"] #TODO KRH: source this from a parameter, make mz relative relative = [False, False] #TODO KRH: source this from a parameter, make mz relative - tol = [0.001, 1.5] #TODO KRH: source this from a parameter, make mz relative + tol = [0.0005, 0.1] #TODO KRH: source this from a parameter, make mz relative # Compute inter-feature distances idx = [] @@ -1708,19 +1707,16 @@ def get_anchor_mass_features(self, mf_df): :obj:`~pandas.DataFrame` The anchor mass features dataframe. """ - #TODO KRH: add error handling and the ability to implement other anchoring techniques through parameters + #TODO KRH: add anchoring_technique parameter to source this from a parameter mf_df = mf_df.copy() # Drop features that are not mass_spectrum_deconvoluted_parent or are NA as mass_spectrum_deconvoluted_parent mf_df = mf_df.dropna(subset=["mass_spectrum_deconvoluted_parent"]) mf_df = mf_df[mf_df["mass_spectrum_deconvoluted_parent"]] - # Drop features that have NA as dispersity_index or half_height_width (generally bad shape) - mf_df = mf_df.dropna(subset=["dispersity_index", "half_height_width", "mass_spectrum_deconvoluted_parent"]) - return mf_df - def align_lcms_objects(self): + def align_lcms_objects(self, overwrite=False): """ Align LCMS objects in the collection. @@ -1743,11 +1739,20 @@ def align_lcms_objects(self): #TODO KRH: add the ability to do this by batch and then connect the batches # Prepare the center LCMS object center_obj_ids = self.manifest_dataframe[self.manifest_dataframe['center']].collection_id.values + + full_mf_df = self.mass_features_dataframe + # re-index to sample_name for faster lookups + full_mf_df = full_mf_df.reset_index().set_index('sample_name') + + if 'scan_time_aligned' in full_mf_df.columns and not overwrite: + raise ValueError('Mass features have already been aligned') + for center_obj_id in center_obj_ids: - mf_df_c = self[center_obj_id].mass_features_to_df().copy() - mf_df_c = mf_df_c.reset_index(drop=False) - # Drop mass featuers with nan in mass_spectrum_deconvoluted_parent + # Get the anchor mass features from the center LCMS object + mf_df_c = full_mf_df.loc[self.samples[center_obj_id]] mf_df_c = self.get_anchor_mass_features(mf_df_c) + + # Set scan_time_aligned to scan_time for the center LCMS object center_scan_df = self[center_obj_id].scan_df.copy() center_scan_df['scan_time_aligned'] = center_scan_df['scan_time'] self[center_obj_id].scan_df = center_scan_df @@ -1759,10 +1764,7 @@ def align_lcms_objects(self): i = center_obj_id + index_step if i < len(self) and i >= 0: # Grab the first LCMS object after the center object - lc_obj = self[i] - mf_df_i = lc_obj.mass_features_to_df().copy() - # Remove index from mass features - mf_df_i = mf_df_i.reset_index(drop=False) + mf_df_i = full_mf_df.loc[self.samples[i]].copy() mf_df_i['scan_time_og'] = mf_df_i['scan_time'] while mf_df_i is not None: @@ -1779,21 +1781,20 @@ def align_lcms_objects(self): spl = self.fit_rts(matches_c, matches_i, kernel='rbf', C=1000) # Set new retention times on scan_df for lc_obj matches_i['scan_time_fit'] = spl(matches_i['scan_time']) - new_times = spl(lc_obj.scan_df['scan_time']) - new_scan_info = lc_obj.scan_df.copy() + new_times = spl(self[i].scan_df['scan_time']) + new_scan_info = self[i].scan_df.copy() new_scan_info['scan_time_aligned'] = new_times - lc_obj.scan_df = new_scan_info + self[i].scan_df = new_scan_info i += index_step if i >= len(self) or i < 0: mf_df_i = None else: # Grab the next LCMS object and use the previous spline fitting to get a better starting point - lc_obj = self[i] - mf_df_i = lc_obj.mass_features_to_df().copy() + mf_df_i = full_mf_df.loc[self.samples[i]].copy() mf_df_i['scan_time_og'] = mf_df_i['scan_time'] mf_df_i = mf_df_i.reset_index(drop=False) # Set scan_time to previous sample's predicted scan_time to find closer matches - mf_df_i['scan_time'] = spl(mf_df_i['scan_time']) + mf_df_i['scan_time'] = spl(mf_df_i['scan_time']) #might need to add a copy step here else: raise ValueError(f'No matches found between the center object and {self.samples[i]}') @@ -1804,14 +1805,43 @@ def add_consensus_mass_features(self): # Check if the mass features have been aligned if 'scan_time_aligned' not in combined_mfs.columns: raise ValueError('Mass features have not been aligned, run align_lcms_objects() first') + + # Partition the mass features by mz so we can parallelize the matching before clustering + from corems.chroma_peak.calc import subset as corems_subset + lazy_partitions = corems_subset.multi_sample_partition(combined_mfs, split_on = 'mz') + + # Make distance matrix for each partition + distance_matrix = lazy_partitions.map(self.create_distance_matrix) + # Drop mass features with nan in mass_spectrum_deconvoluted_parent and if they are not mass_spectrum_deconvoluted_parent #combined_mfs = combined_mfs.dropna(subset=["mass_spectrum_deconvoluted_parent"]) #combined_mfs = combined_mfs[combined_mfs["mass_spectrum_deconvoluted_parent"]] + + # Partition the mass features by mz so we can parallelize the matching before clustering + #combined_mfs = combined_mfs.sort_values(by='mz') + #combined_mfs = combined_mfs.reset_index(drop=True) + test = self.agglomerative_clustering(combined_mfs) - def agglomerative_clustering(self, features): + def partition_mass_features(self, combined_mfs): + # Partition the mass features by mz so we can parallelize the matching before clustering + combined_mfs = combined_mfs.sort_values(by='mz') + combined_mfs = combined_mfs.reset_index(drop=True) + + # Get the unique mz values + mz_vals = combined_mfs['mz'].unique() + + # Partition the mass features by mz + partitions = [] + for mz in mz_vals: + partition = combined_mfs[combined_mfs['mz'] == mz] + partitions.append(partition) + + yield partitions + + def create_distance_matrix(self, features): ''' Cluster features within provided linkage tolerances. Recursively merges the pair of clusters that minimally increases a given linkage distance. @@ -1842,11 +1872,11 @@ def agglomerative_clustering(self, features): # Safely cast to list # TODO KRH: source this from a parameter # TODO KRH: make mz relative - dims = ["mz", "scan_time_aligned", "tailing_factor", "dispersity_index"] - relative = [False, False, False, False] - tol = [0.001, 0.2, 0.4, 0.1] - dist_weight = [1, 1, 0.2, 0.5] - na_fill = [None, None, 1, np.nanmedian(features.dispersity_index)] + dims = ["mz", "scan_time_aligned"] + relative = [False, False] + tol = [0.001, 0.2] + dist_weight = [1, 1] + na_fill = [None, None] # Check that the dimensions and tolerances are the same length if len(dims) != len(tol) or len(dims) != len(relative) or len(dims) != len(na_fill) or len(dims) != len(dist_weight): @@ -1933,6 +1963,9 @@ def agglomerative_clustering(self, features): distances[distances == 0] = 1 distances = np.asarray(distances) + return distances + + ''' # Perform clustering try: clustering = AgglomerativeClustering(n_clusters=None, @@ -1947,7 +1980,7 @@ def agglomerative_clustering(self, features): features_sub = features[['cluster', 'mz', 'scan_time_aligned', 'sample_id', 'tailing_factor', 'dispersity_index', 'half_height_width', 'mass_spectrum_deconvoluted_parent']] return features - + ''' diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index e3a480b90..725075b19 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -3,6 +3,9 @@ import numpy as np import pandas as pd +import multiprocessing + +import matplotlib.pyplot as plt from corems.encapsulation.factory.parameters import LCMSParameters from corems.mass_spectra.calc.lc_calc import LCCalculations, PHCalculations, LCMSCollectionCalculations @@ -1075,6 +1078,7 @@ def tic(self, tic_list): class LCMSCollection(LCMSCollectionCalculations): """A class representing a collection of liquid chromatography-mass spectrometry (LC-MS) runs. + These runs can be from the same or different samples, but must be from the same instrument and have the same parameters. Parameters ----------- @@ -1099,6 +1103,11 @@ def __init__( self._combined_mass_features = None self.consensus_mass_features = {} + # TODO KRH: parameters that should be sourced from somewhere else and better stored + self.parameter_dict = {} + self.parameter_dict["cores"] = self.collection_parser._cores + self.parameter_dict["df_type"] = "dask" + def _reorder_lcms_objects(self): """ Reorders the LCMS objects in the collection based on the order in the manifest. @@ -1114,6 +1123,30 @@ def __getitem__(self, index): def __len__(self): return len(self.samples) + def _prepare_lcms_mass_features_for_combination(self, lcms_obj): + """ + Prepares the mass features in the LCMS objects in the collection for combination. + """ + # Check if lcms_obj has attribute light_mf_df + if hasattr(lcms_obj, "light_mf_df"): + mf_df = lcms_obj.light_mf_df + else: + mf_df = lcms_obj.mass_features_to_df() + # Remove index + mf_df = mf_df.reset_index(drop=False) + # Add sample name and sample id to the dataframe + mf_df["sample_name"] = lcms_obj.sample_name + mf_df["sample_id"] = self.manifest[lcms_obj.sample_name]["collection_id"] + mf_df["coll_mf_id"] = mf_df["sample_id"].astype(str) + "_" + mf_df["mf_id"].astype(str) + + # Check if scan_df has scan_time_aligned and add to mf_df if so + if "scan_time_aligned" in lcms_obj.scan_df.columns: + scan_df = lcms_obj.scan_df[["scan", "scan_time_aligned"]].copy() + scan_df = scan_df.rename(columns={"scan": "apex_scan"}) + mf_df = mf_df.merge(scan_df, left_on="apex_scan", right_index=True) + + return mf_df + def _combine_mass_features(self): """ Concatenates the mass features from all the LCMS objects in the collection. @@ -1122,34 +1155,35 @@ def _combine_mass_features(self): -------- None, sets the _combined_mass_features attribute. """ - mf_df_list = [] - for lcms_obj in self: - mf_df = lcms_obj.mass_features_to_df() - # Remove index - mf_df = mf_df.reset_index(drop=False) - # Add sample name and sample id to the dataframe - mf_df["sample_name"] = lcms_obj.sample_name - mf_df["sample_id"] = self.manifest[lcms_obj.sample_name]["collection_id"] - mf_df["coll_mf_id"] = mf_df["sample_id"].astype(str) + "_" + mf_df["mf_id"].astype(str) - - # Check if scan_df has scan_time_aligned and add to mf_df if so - if "scan_time_aligned" in lcms_obj.scan_df.columns: - scan_df = lcms_obj.scan_df[["scan", "scan_time_aligned"]].copy() - scan_df = scan_df.rename(columns={"scan": "apex_scan"}) - mf_df = mf_df.merge(scan_df, left_on="apex_scan", right_index=True) - mf_df_list.append(mf_df) + + if self.parameter_dict["cores"] == 1: + # Prepare mass features for combination sequentially + mf_df_list = [] + for lcms_obj in self: + mf_df = self._prepare_lcms_mass_features_for_combination(lcms_obj) + mf_df_list.append(mf_df) + + if self.parameter_dict["cores"] > 1: + # Parallelize the mass feature preparation + if self.parameter_dict["cores"] > len(self): + ncores = len(self) + else: + ncores = self.parameter_dict["cores"] + pool = multiprocessing.Pool(ncores) + mf_df_list = pool.starmap(self._prepare_lcms_mass_features_for_combination, [(lcms_obj,) for lcms_obj in self]) + combined_mass_features = pd.concat(mf_df_list) # Move coll_mf_id, sample_name, and sample_id to front cols = combined_mass_features.columns.tolist() - top_cols = ["coll_mf_id", "sample_name", "sample_id", "mz", "scan_time_aligned", "a test"] + top_cols = ["coll_mf_id", "sample_name", "sample_id", "mz", "scan_time_aligned"] cols = [x for x in top_cols + [col for col in cols if col not in top_cols] if x in cols] combined_mass_features = combined_mass_features[cols] # Make coll_mf_id the index combined_mass_features = combined_mass_features.set_index("coll_mf_id") - self._combined_mass_features = combined_mass_features.to_dict() + self._combined_mass_features = combined_mass_features - def mass_features_to_df(self): - """Returns a pandas dataframe summarizing all the mass features in the collection. + def _check_mass_features_df(self): + """Checks if the mass features dataframe has expected columns. If not, adds them. Returns -------- @@ -1158,23 +1192,21 @@ def mass_features_to_df(self): Notes ------ - If _combined_mass_features is not set, calls _combine_mass_features to set it. If scan_time_aligned is not in the _combined_mass_features, tries to add it. - """ - # Check if combined_mass_features is set, set if not - if self._combined_mass_features is None: - self._combine_mass_features() - + """ # Check if scan_time_aligned is in combined_mass_features, try to add if not - elif self._combined_mass_features is not None and "scan_time_aligned" not in self._combined_mass_features: - if all([True for x in self if "scan_time_aligned" in x.scan_df.columns]): - self._combine_mass_features() - - df = pd.DataFrame(self._combined_mass_features) - return df - - def plot_tics(self, ms_level=1, type = "raw"): + if self._combined_mass_features is not None and "scan_time_aligned" not in self._combined_mass_features.columns: + lcms_aligned = [True for x in self if "scan_time_aligned" in x.scan_df.columns] + if len(lcms_aligned) == len(self): + # Add scan_time_aligned to combined_mass_features dataframe + scan_time_aligned_list = [] + for lcms_obj in self: + scan_time_aligned_list.append(lcms_obj.scan_df[["scan", "scan_time_aligned"]]) + scan_time_aligned_df = pd.concat(scan_time_aligned_list) + self._combined_mass_features = self._combined_mass_features.merge(scan_time_aligned_df, left_on="apex_scan", right_on="scan") + + def plot_tics(self, ms_level=1, type = "raw", plot_legend=False): """Plots the TICs for all the LCMS objects in the collection. Parameters @@ -1183,8 +1215,9 @@ def plot_tics(self, ms_level=1, type = "raw"): The MS level to plot the TICs for. Defaults to 1. type : str, optional The type of TIC to plot, either "raw" or "corrected" or "both". Defaults to "raw". + plot_legend : bool, optional + If True, plots a legend on the TIC plot that labels each sample. Defaults to False. """ - import matplotlib.pyplot as plt to_plot = [] if type == "both": to_plot = ["raw", "corrected"] @@ -1197,7 +1230,13 @@ def plot_tics(self, ms_level=1, type = "raw"): for i, plot_type in enumerate(to_plot): ax = axs[i, 0] + colors = iter(plt.cm.rainbow(np.linspace(0, 1, len(self)))) for lcms_obj in self: + c = next(colors) + # check if lcms_obj is the center of the collection + self.manifest_dataframe[self.manifest_dataframe['center']].collection_id.values + + scan_df = lcms_obj.scan_df scan_df = scan_df[scan_df.ms_level == ms_level] if plot_type == "corrected": @@ -1205,34 +1244,46 @@ def plot_tics(self, ms_level=1, type = "raw"): if "scan_time_aligned" not in scan_df.columns: raise ValueError(f"scan_time_aligned not found in scan_df for {lcms_obj.sample_name}") else: - ax.plot(scan_df.scan_time_aligned, scan_df.tic, label=lcms_obj.sample_name) + ax.plot(scan_df.scan_time_aligned, scan_df.tic, label=lcms_obj.sample_name, c=c, linewidth=0.3) elif plot_type == "raw": - ax.plot(scan_df.scan_time, scan_df.tic, label=lcms_obj.sample_name) + ax.plot(scan_df.scan_time, scan_df.tic, label=lcms_obj.sample_name, c=c, linewidth=0.3) ax.set_xlabel("Retention Time (min," + f" {plot_type})" ) ax.set_ylabel("TIC") - ax.legend() + if plot_legend: + ax.legend() plt.show() - def plot_alignments(self): - """Plots the alignment of the LCMS objects in the collection.""" - import matplotlib.pyplot as plt + def plot_alignments(self, plot_legend=False): + """Plots the alignment of the LCMS objects in the collection. + + Parameters + ----------- + plot_legend : bool, optional + If True, plots a legend on the alignment plot that labels each sample. Defaults to False. + """ fig, ax = plt.subplots(figsize=(10, 5)) + colors = iter(plt.cm.rainbow(np.linspace(0, 1, len(self)))) + for lcms_obj in self: + c = next(colors) scan_df = lcms_obj.scan_df if "scan_time_aligned" not in scan_df.columns: raise ValueError(f"scan_time_aligned not found in scan_df for {lcms_obj.sample_name}") scan_df['time_diff'] = scan_df.scan_time - scan_df.scan_time_aligned - ax.plot(scan_df.scan_time_aligned, scan_df.time_diff, label=lcms_obj.sample_name) + ax.plot(scan_df.scan_time_aligned, scan_df.time_diff, label=lcms_obj.sample_name, c=c, linewidth=0.3) + ax.set_xlabel("Aligned Retention Time (min)") ax.set_ylabel("Time Difference (min)") - ax.legend() + if plot_legend: + ax.legend() plt.show() @property def mass_features_dataframe(self): if self._combined_mass_features is None: self._combine_mass_features() - return self.mass_features_to_df() + self._check_mass_features_df() + return self._combined_mass_features @property def samples(self): diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index bbf9cddf0..57689ebdb 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -5,6 +5,7 @@ from threading import Thread import toml import json +import multiprocessing import pandas as pd @@ -472,7 +473,7 @@ def get_lcms_obj(self, load_raw=True, load_light=False) -> LCMSBase: class ReadCoreMSHDFMassSpectraCollection: - def __init__(self, folder_location: str, manifest_file: str): + def __init__(self, folder_location: str, manifest_file: str, cores: int = 1): # Check for folder location and manifest file if not folder_location.exists(): raise FileNotFoundError(f"Folder location {folder_location} not found.") @@ -487,6 +488,17 @@ def __init__(self, folder_location: str, manifest_file: str): self._parse_manifest(manifest_file) self._validate_manifest() self._validate_parameters() + self._validate_cores(cores) + + def _validate_cores(self, cores): + # Check if the cores parameter is an integer greater than 0 and less than the number of cores available + if not isinstance(cores, int) or cores < 1: + raise ValueError("Cores must be an integer greater than 0.") + if cores > multiprocessing.cpu_count(): + raise ValueError( + f"Cores must be less than or equal to the number of cores available ({multiprocessing.cpu_count()})." + ) + self._cores = cores def _parse_manifest(self, manifest_file): """Parse the manifest file and set the manifest dictionary.""" @@ -569,7 +581,12 @@ def get_lcms_obj(self, sample_name: str, load_raw=False, load_light=True) -> LCM """ hdf5_file = self.folder_location / f"{sample_name}.corems/{sample_name}.hdf5" parser = ReadCoreMSHDFMassSpectra(hdf5_file) - return parser.get_lcms_obj(load_raw=load_raw, load_light=load_light) + lcms_obj = parser.get_lcms_obj(load_raw=load_raw, load_light=load_light) + if load_light: + mf_df = lcms_obj.mass_features_to_df() + lcms_obj.mass_features = {} + lcms_obj.light_mf_df = mf_df + return lcms_obj def get_lcms_collection(self, load_raw = False, load_light = True) -> LCMSCollection: """Return a LCMSCollection object @@ -579,7 +596,9 @@ def get_lcms_collection(self, load_raw = False, load_light = True) -> LCMSCollec load_raw : bool If True, load raw data from HDF5 files. Default is False. load_light : bool - If True, only load the parameters, mass features, and scan info are initially loaded for each lcms object. Default is True. + If True, only load the parameters, mass features, and scan info are initially loaded for each lcms object. + After concatenating the mass_features, remove the mass_features attribute from the individual LCMS objects for memory efficiency. Default is True. + Default is True. """ # Instantiate the LCMSCollection object lcms_coll = LCMSCollection( @@ -592,8 +611,26 @@ def get_lcms_collection(self, load_raw = False, load_light = True) -> LCMSCollec samples = self._manifest_dict.keys() # Initialize the LCMS object dictionary - for sample_name in samples: - lcms_coll._lcms[sample_name] = self.get_lcms_obj(sample_name, load_raw=load_raw, load_light=load_light) + if self._cores > 1: + if self._cores > len(samples): + ncores = len(samples) + else: + ncores = self._cores + # Create a pool of workers (one for each core or sample, whichever is smaller) + pool = multiprocessing.Pool(ncores) + # Load the LCMS objects in parallel + args = [(sample, load_raw, load_light) for sample in samples] + lcms_objs = pool.starmap(self.get_lcms_obj, args) + for sample_name, lcms_obj in zip(samples, lcms_objs): + lcms_coll._lcms[sample_name] = lcms_obj + + elif self._cores == 1: + # Load the LCMS objects sequentially + for sample_name in samples: + lcms_coll._lcms[sample_name] = self.get_lcms_obj(sample_name, load_raw=load_raw, load_light=load_light) + + else: + raise ValueError("Number of cores must be greater than 0 and set on the ReadCoreMSHDFMassSpectraCollection object.") # Check that all LCMS objects have the same polarity if len(set([x.polarity for k, x in lcms_coll._lcms.items()])) != 1: @@ -608,6 +645,16 @@ def get_lcms_collection(self, load_raw = False, load_light = True) -> LCMSCollec # Reorder the LCMS objects lcms_coll._reorder_lcms_objects() + # Collect the mass features from the LCMS objects and combine them into a single dataframe for the collection + lcms_coll._combine_mass_features() + + # If load_light, remove the mass_feature attribute from the individual LCMS objects + if load_light: + for sample_name in lcms_coll.samples: + lcms_coll._lcms[sample_name].mass_features = {} + lcms_coll._lcms[sample_name].light_mf_df = None + + return lcms_coll @property diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 689bd9691..54eb32b92 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -1,20 +1,48 @@ from pathlib import Path +import time from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectraCollection if __name__ == "__main__": - collection_path = Path("tmp_data/NMDC_processed_collection_0820") - manifest_file = collection_path / "manifest.csv" + collection_path = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/processed_files") + manifest_file = collection_path / "manifest_small.csv" + + ''' + # Read in manifest file + import pandas as pd + manifest_df = pd.read_csv(manifest_file) + # Remove any rows with missing file paths + for index, row in manifest_df.iterrows(): + sample_folder = Path(row["sample_name"] + ".corems") + # Check if sample_folder exists in collection_path + if not (collection_path / sample_folder).exists(): + manifest_df.drop(index, inplace=True) + + manifest_df.to_csv(manifest_file, index=False) + ''' + ncores = 10 parser = ReadCoreMSHDFMassSpectraCollection( folder_location = collection_path, - manifest_file = manifest_file + manifest_file = manifest_file, + cores = ncores ) + + print("Loading LCMS collection with", len(parser.manifest), "samples using", ncores, " cores") + start_time = time.time() lcms_collection = parser.get_lcms_collection(load_raw=False, load_light=True) + print("Time to load LCMS collection ", time.time() - start_time, "seconds -", len(lcms_collection), " LCMS runs and ", ncores, " cores") #10s for 7 samples, 10 cores; + + print("Aligning LCMS collection") + start_time = time.time() lcms_collection.align_lcms_objects() + print("Time to align LCMS collection: ", time.time() - start_time, "seconds") # 1.5s for 7 samples, 15s for 70 samples + + lcms_collection.mass_features_dataframe #lcms_collection.plot_tics(type="both") #lcms_collection.plot_alignments() #mass_feature_df = lcms_collection.mass_features_to_df() - lcms_collection.add_consensus_mass_features() - print("Here") + #print("Adding consensus mass features") + #lcms_collection.add_consensus_mass_features() + #print("Here") #lcms_collection. \ No newline at end of file diff --git a/support_code/nmdc/lipidomics/lipidomics_workflow.py b/support_code/nmdc/lipidomics/lipidomics_workflow.py index 052732a42..b96e66438 100644 --- a/support_code/nmdc/lipidomics/lipidomics_workflow.py +++ b/support_code/nmdc/lipidomics/lipidomics_workflow.py @@ -262,14 +262,28 @@ def add_mass_features(myLCMSobj, verbose): ------- None, processes the LCMS object """ + if verbose: + print("Finding mass features") myLCMSobj.find_mass_features(verbose=verbose) + if verbose: + print("Adding associated ms1 spectra") myLCMSobj.add_associated_ms1( auto_process=True, use_parser=False, spectrum_mode="profile" ) + if verbose: + print("Integrating mass features") myLCMSobj.integrate_mass_features(drop_if_fail=True) + if verbose: + print("Annotating c13 mass features") myLCMSobj.find_c13_mass_features(verbose=verbose) + if verbose: + print("Deconvoluting mass features") myLCMSobj.deconvolute_ms1_mass_features() + if verbose: + print("Adding peak metrics") myLCMSobj.add_peak_metrics() + if verbose: + print("Adding associated ms2 spectra") myLCMSobj.add_associated_ms2_dda(spectrum_mode="centroid") @@ -532,6 +546,12 @@ def run_lipid_workflow( if cores == 1 or len(files_list) == 1: mz_dicts = [] for file_in, file_out in list(zip(files_list, out_paths_list)): + # Check if folder already exists + if Path(str(file_out) + ".corems").exists(): + print("File already exists, skipping: ", file_out) + continue + if verbose: + print("Processing file: ", file_in) mz_dict = run_lipid_sp_ms1( str(file_in), str(file_out), @@ -569,10 +589,13 @@ def run_lipid_workflow( if __name__ == "__main__": # Set input variables to run - cores = 7 - file_dir = Path("tmp_data/thermo_raw_collection") - out_dir = Path("tmp_data/NMDC_processed_collection_0820") - params_toml = Path("tmp_data/thermo_raw_collection/nmdc_lipid_params.toml") + cores = 1 + #file_dir = Path("tmp_data/thermo_raw_collection") + file_dir = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/raw_files") + #out_dir = Path("tmp_data/NMDC_processed_collection_0820") + out_dir = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/processed_files") + #params_toml = Path("tmp_data/thermo_raw_collection/nmdc_lipid_params.toml") + params_toml = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/lipid_workflow_params.toml") verbose = True From f483f346b4f81283972e200e31ca094c6e329153 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 12 Sep 2024 13:16:32 -0700 Subject: [PATCH 014/158] Add partitioning to consensus mass feature step --- corems/mass_spectra/calc/lc_calc.py | 46 +++++++++---------- corems/mass_spectra/factory/lc_class.py | 15 ++++-- corems/mass_spectra/input/corems_hdf5.py | 3 +- .../nmdc/lipidomics/lipidomics_collection.py | 16 +++++-- 4 files changed, 45 insertions(+), 35 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 351d46471..48204cc66 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1808,10 +1808,11 @@ def add_consensus_mass_features(self): # Partition the mass features by mz so we can parallelize the matching before clustering from corems.chroma_peak.calc import subset as corems_subset - lazy_partitions = corems_subset.multi_sample_partition(combined_mfs, split_on = 'mz') + lazy_partitions = corems_subset.multi_sample_partition(combined_mfs, split_on = 'mz', size=1000, tol=0.001) # Make distance matrix for each partition - distance_matrix = lazy_partitions.map(self.create_distance_matrix) + distance_matrix = lazy_partitions.map(self.create_distance_matrix, processes=self.parameter_dict["cores"]) + self.distance_matrix = distance_matrix # Drop mass features with nan in mass_spectrum_deconvoluted_parent and if they are not mass_spectrum_deconvoluted_parent @@ -1823,23 +1824,7 @@ def add_consensus_mass_features(self): #combined_mfs = combined_mfs.reset_index(drop=True) - test = self.agglomerative_clustering(combined_mfs) - - def partition_mass_features(self, combined_mfs): - # Partition the mass features by mz so we can parallelize the matching before clustering - combined_mfs = combined_mfs.sort_values(by='mz') - combined_mfs = combined_mfs.reset_index(drop=True) - - # Get the unique mz values - mz_vals = combined_mfs['mz'].unique() - - # Partition the mass features by mz - partitions = [] - for mz in mz_vals: - partition = combined_mfs[combined_mfs['mz'] == mz] - partitions.append(partition) - - yield partitions + #test = self.agglomerative_clustering(combined_mfs) def create_distance_matrix(self, features): ''' @@ -1923,7 +1908,7 @@ def create_distance_matrix(self, features): # Filter relative distances if relative[i] is True: # Compute relative distances - rel_dists = sdm.data / values[sdm.row] # or col? + rel_dists = sdm.data / values[sdm.row] # Indices of relative distances less than tolerance idx = rel_dists <= tol[i] @@ -1957,15 +1942,26 @@ def create_distance_matrix(self, features): # Multiply by connectivity matrix for more masking distances = distances.multiply(cmat) - # Convert to full matrix + # Extract indices of within-tolerance points + distances = distances.tocoo() + pairs = np.stack((distances.row, distances.col, distances.data), axis=1) + + # Convert to coll_mf_ids rather than indices + mfid1 = features['coll_mf_id'].values[pairs[:, 0].astype(int)] + mfid2 = features['coll_mf_id'].values[pairs[:, 1].astype(int)] + pairs = np.column_stack((mfid1, mfid2, pairs[:, 2])) + + # Convert to DataFrame + pairs = pd.DataFrame(pairs, columns=['coll_mf_id1', 'coll_mf_id2', 'distance']) + + return pairs + + ''' + # Convert to full matrix distances = distances.todense() # Cast all 0s to 1s for a distance matrix distances[distances == 0] = 1 distances = np.asarray(distances) - - return distances - - ''' # Perform clustering try: clustering = AgglomerativeClustering(n_clusters=None, diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 725075b19..92e687738 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1197,14 +1197,23 @@ def _check_mass_features_df(self): """ # Check if scan_time_aligned is in combined_mass_features, try to add if not if self._combined_mass_features is not None and "scan_time_aligned" not in self._combined_mass_features.columns: + cmb_mf = self._combined_mass_features.copy() + cmb_mf = cmb_mf.reset_index(drop=False) lcms_aligned = [True for x in self if "scan_time_aligned" in x.scan_df.columns] if len(lcms_aligned) == len(self): # Add scan_time_aligned to combined_mass_features dataframe scan_time_aligned_list = [] for lcms_obj in self: - scan_time_aligned_list.append(lcms_obj.scan_df[["scan", "scan_time_aligned"]]) + scan_time_df_i = lcms_obj.scan_df[["scan", "scan_time_aligned"]] + scan_time_df_i["sample_name"] = lcms_obj.sample_name + scan_time_aligned_list.append(scan_time_df_i) scan_time_aligned_df = pd.concat(scan_time_aligned_list) - self._combined_mass_features = self._combined_mass_features.merge(scan_time_aligned_df, left_on="apex_scan", right_on="scan") + # Rename scan to apex_scan + scan_time_aligned_df = scan_time_aligned_df.rename(columns={"scan": "apex_scan"}) + cmb_mf_merged = cmb_mf.merge(scan_time_aligned_df, on=["apex_scan", "sample_name"]) + cmb_mf_mergd = cmb_mf_merged.set_index("coll_mf_id") + # Merge scan_time_aligned_df with combined_mass_features on apex_scan and sample_name + self._combined_mass_features = cmb_mf_mergd def plot_tics(self, ms_level=1, type = "raw", plot_legend=False): """Plots the TICs for all the LCMS objects in the collection. @@ -1280,8 +1289,6 @@ def plot_alignments(self, plot_legend=False): @property def mass_features_dataframe(self): - if self._combined_mass_features is None: - self._combine_mass_features() self._check_mass_features_df() return self._combined_mass_features diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 57689ebdb..e5ae2e06c 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -652,7 +652,8 @@ def get_lcms_collection(self, load_raw = False, load_light = True) -> LCMSCollec if load_light: for sample_name in lcms_coll.samples: lcms_coll._lcms[sample_name].mass_features = {} - lcms_coll._lcms[sample_name].light_mf_df = None + # Remove the light_mf_df attribute from the individual LCMS objects + del lcms_coll._lcms[sample_name].light_mf_df return lcms_coll diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 54eb32b92..584149616 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -5,7 +5,7 @@ if __name__ == "__main__": collection_path = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/processed_files") - manifest_file = collection_path / "manifest_small.csv" + manifest_file = collection_path / "manifest.csv" ''' # Read in manifest file @@ -31,18 +31,24 @@ start_time = time.time() lcms_collection = parser.get_lcms_collection(load_raw=False, load_light=True) print("Time to load LCMS collection ", time.time() - start_time, "seconds -", len(lcms_collection), " LCMS runs and ", ncores, " cores") #10s for 7 samples, 10 cores; - + lcms_collection.mass_features_dataframe + + print("Aligning LCMS collection") start_time = time.time() lcms_collection.align_lcms_objects() print("Time to align LCMS collection: ", time.time() - start_time, "seconds") # 1.5s for 7 samples, 15s for 70 samples - lcms_collection.mass_features_dataframe + assert lcms_collection.mass_features_dataframe.index.name == "coll_mf_id" + + #lcms_collection.plot_tics(type="both") #lcms_collection.plot_alignments() #mass_feature_df = lcms_collection.mass_features_to_df() #print("Adding consensus mass features") - #lcms_collection.add_consensus_mass_features() - #print("Here") + start_time = time.time() + lcms_collection.add_consensus_mass_features() + print("Time to calculate distance matrices: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") + print("Here") #lcms_collection. \ No newline at end of file From bdcd4f8016c168f06c24f31b6bff11d48988fa59 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 17 Sep 2024 09:14:42 -0700 Subject: [PATCH 015/158] Add calculations for getting consensus mass features --- corems/chroma_peak/calc/subset.py | 5 + corems/mass_spectra/calc/lc_calc.py | 145 ++++-------------- corems/mass_spectra/factory/lc_class.py | 14 ++ .../nmdc/lipidomics/lipidomics_collection.py | 11 +- .../lipidomics_collection_explore.py | 23 +++ 5 files changed, 78 insertions(+), 120 deletions(-) create mode 100644 support_code/nmdc/lipidomics/lipidomics_collection_explore.py diff --git a/corems/chroma_peak/calc/subset.py b/corems/chroma_peak/calc/subset.py index e207e2f72..89cd64961 100644 --- a/corems/chroma_peak/calc/subset.py +++ b/corems/chroma_peak/calc/subset.py @@ -219,6 +219,8 @@ class MultiSamplePartitions: Target partition size. tol : float Largest allowed distance between unique `split_on` observations. + n_partitions : int + Number of partitions in the data. ''' @@ -293,6 +295,9 @@ def _compute_splits(self): self.bounds = np.array([[x.min(), x.max()] for x in bins]) + # Number of partitions in the data + self.n_partitions = len(bins) + def __iter__(self): return self diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 48204cc66..e05bfa503 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1440,89 +1440,7 @@ def clean_sparse_matrix(self, sparse_matrix): sparse_matrix.sort() dereplicated_sparse_matrix = np.unique(sparse_matrix, axis=0) return dereplicated_sparse_matrix - - def get_sparse_matrix(self, anchors_only=True, ms_level=1): - """Get a sparse matrix of MS1 matches. - - Parameters - ---------- - mode : str, optional - The mode to use for matching. Default is "open". Other options are "identity". - - Returns - ------- - :obj:`~numpy.array` - A sparse matrix of MS1 matches, with each row containing the ids of the mass features that match. - """ - # Grab all deconvoluted ms1s from all mass features - ms_lib = [] - max_mz = 0 - for key, lcms_obj in self._lcms.items(): - if lcms_obj.mass_features is None: - raise ValueError( - "No mass features found in LCMSBase object, run find_mass_features() first" - ) - for mf_id, mass_feature in lcms_obj.mass_features.items(): - if anchors_only and not mass_feature.anchor_feature: - continue - if ms_level == 1: - ms_list = [mass_feature.mass_spectrum_deconvoluted] - elif ms_level == 2: - ms_list = [x for k, x in mass_feature.ms2_mass_spectra.items()] - if ms_list is None: - continue - for ms in ms_list: - lib_entry = { - "id": str(self.manifest[key]['collection_id']) + "_" + str(mass_feature.id) + "_" + str(ms.scan_number), #lcmsID_mfID_scanNumber [unique ID for the mass spectrum associated with the mass feature] - "precursor_mz": mass_feature.mz, - "peaks": np.column_stack((ms.mz_exp, ms.abundance)), - "mf_id": str(self.manifest[key]['collection_id']) + "_" + str(mass_feature.id) #lcmsID_mfID [unique ID for the mass feature] - } - ms_lib.append(lib_entry) - if mass_feature.mz > max_mz: - max_mz = mass_feature.mz - - # Build flash entropy search index - fes = FlashEntropySearch( - max_ms2_tolerance_in_da = 0.001 #TODO KRH: source this from a parameter - ) - fes.build_index( - all_spectra_list = ms_lib, - max_indexed_mz = max_mz+5, - precursor_ions_removal_da = None, - noise_threshold=0, - min_ms2_difference_in_da = 0.001, #TODO KRH: source this from a parameter - max_peak_num = 0, - clean_spectra = True - ) - - results_sparse_identity = [] - min_match_score = 0.8 #TODO KRH: source this from a parameter - - for spec in fes: - search_results = fes.search( - precursor_mz=spec['precursor_mz'], - peaks=spec['peaks'], - ms1_tolerance_in_da=0.001, #TODO KRH: source this from a parameter - ms2_tolerance_in_da=0.001, #TODO KRH: source this from a parameter [note this isn't really MS2] - method={"identity"}, - precursor_ions_removal_da=None, - noise_threshold=0, - target="cpu", - ) - match_inds_identity = np.where(search_results["identity_search"] > min_match_score)[0] - if len(match_inds_identity) > 0: - ref_ms_ids = [fes[x]["mf_id"] for x in match_inds_identity] - # drop ref_ms_ids if it matches spec["id"] - ref_ms_ids = [x for x in ref_ms_ids if x != spec["mf_id"]] - if len(ref_ms_ids) > 0: - for ref_ms_id in ref_ms_ids: - results_sparse_identity.append([spec["mf_id"], ref_ms_id]) - - results_sparse_identity = self.clean_sparse_matrix(results_sparse_identity) - - return results_sparse_identity - + def match_mfs(self, mf_c, mf_i): """Match mass features between two LCMS objects. @@ -1808,25 +1726,35 @@ def add_consensus_mass_features(self): # Partition the mass features by mz so we can parallelize the matching before clustering from corems.chroma_peak.calc import subset as corems_subset - lazy_partitions = corems_subset.multi_sample_partition(combined_mfs, split_on = 'mz', size=1000, tol=0.001) - # Make distance matrix for each partition - distance_matrix = lazy_partitions.map(self.create_distance_matrix, processes=self.parameter_dict["cores"]) - self.distance_matrix = distance_matrix + # TODO KRH: source partition size and tol from parameters + lazy_partitions = corems_subset.multi_sample_partition(combined_mfs, split_on = 'mz', size=10000, tol=0.001) - - # Drop mass features with nan in mass_spectrum_deconvoluted_parent and if they are not mass_spectrum_deconvoluted_parent - #combined_mfs = combined_mfs.dropna(subset=["mass_spectrum_deconvoluted_parent"]) - #combined_mfs = combined_mfs[combined_mfs["mass_spectrum_deconvoluted_parent"]] - - # Partition the mass features by mz so we can parallelize the matching before clustering - #combined_mfs = combined_mfs.sort_values(by='mz') - #combined_mfs = combined_mfs.reset_index(drop=True) + # Make distance matrix for each partition and cluster + if self.parameter_dict["cores"] > lazy_partitions.n_partitions: + cores_to_use = lazy_partitions.n_partitions + else: + cores_to_use = self.parameter_dict["cores"] + mfs_with_clusters = lazy_partitions.map(self.cluster_mass_features, processes=cores_to_use) + + if len(mfs_with_clusters.partition_idx.unique()) > 1: + # Clean up the cluster ids + new_cluster_ids = mfs_with_clusters[['cluster', 'partition_idx']].drop_duplicates().reset_index(drop=True) + new_cluster_ids['cluster_unqiue'] = new_cluster_ids.index + mfs_with_clusters = mfs_with_clusters.merge(new_cluster_ids, on=['cluster', 'partition_idx']) + mfs_with_clusters['cluster'] = mfs_with_clusters['cluster_unqiue'] + mfs_with_clusters = mfs_with_clusters.drop(columns=['cluster_unqiue']) + + mfs_with_clusters = mfs_with_clusters.drop(columns=['partition_idx']) + + # Set coll_mf_id as the index + mfs_with_clusters = mfs_with_clusters.set_index('coll_mf_id') + # Set the mass_features_dataframe property with the mfs_with_clusters attribtute + self.mass_features_dataframe = mfs_with_clusters - #test = self.agglomerative_clustering(combined_mfs) - def create_distance_matrix(self, features): + def cluster_mass_features(self, features): ''' Cluster features within provided linkage tolerances. Recursively merges the pair of clusters that minimally increases a given linkage distance. @@ -1854,7 +1782,7 @@ def create_distance_matrix(self, features): if features is None: return None - # Safely cast to list + # Define how to calculate the distance between features # TODO KRH: source this from a parameter # TODO KRH: make mz relative dims = ["mz", "scan_time_aligned"] @@ -1942,23 +1870,12 @@ def create_distance_matrix(self, features): # Multiply by connectivity matrix for more masking distances = distances.multiply(cmat) - # Extract indices of within-tolerance points - distances = distances.tocoo() - pairs = np.stack((distances.row, distances.col, distances.data), axis=1) - - # Convert to coll_mf_ids rather than indices - mfid1 = features['coll_mf_id'].values[pairs[:, 0].astype(int)] - mfid2 = features['coll_mf_id'].values[pairs[:, 1].astype(int)] - pairs = np.column_stack((mfid1, mfid2, pairs[:, 2])) + # Set attribute holding distance matrix + self._sparse_distance_matrix = distances - # Convert to DataFrame - pairs = pd.DataFrame(pairs, columns=['coll_mf_id1', 'coll_mf_id2', 'distance']) - - return pairs - - ''' - # Convert to full matrix + # Convert to full matrix distances = distances.todense() + # Cast all 0s to 1s for a distance matrix distances[distances == 0] = 1 distances = np.asarray(distances) @@ -1974,9 +1891,7 @@ def create_distance_matrix(self, features): except: features['cluster'] = np.arange(len(features.index)) - features_sub = features[['cluster', 'mz', 'scan_time_aligned', 'sample_id', 'tailing_factor', 'dispersity_index', 'half_height_width', 'mass_spectrum_deconvoluted_parent']] return features - ''' diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 92e687738..c54126dcf 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1292,6 +1292,20 @@ def mass_features_dataframe(self): self._check_mass_features_df() return self._combined_mass_features + @mass_features_dataframe.setter + def mass_features_dataframe(self, df): + # Check that the dataframe has the expected columns + expected_cols = ["sample_name", "sample_id", "mz", "scan_time"] + if not all([col in df.columns for col in expected_cols]): + raise ValueError(f"Expected columns not found in dataframe: {expected_cols}") + + # Check that coll_mf_id is the index and it is unique + if df.index.name != "coll_mf_id": + raise ValueError("coll_mf_id must be the index of the dataframe") + if not df.index.is_unique: + raise ValueError("coll_mf_id must be unique") + self._combined_mass_features = df + @property def samples(self): manifest_df = self.manifest_dataframe diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 584149616..2c8c14d0e 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -30,17 +30,16 @@ print("Loading LCMS collection with", len(parser.manifest), "samples using", ncores, " cores") start_time = time.time() lcms_collection = parser.get_lcms_collection(load_raw=False, load_light=True) - print("Time to load LCMS collection ", time.time() - start_time, "seconds -", len(lcms_collection), " LCMS runs and ", ncores, " cores") #10s for 7 samples, 10 cores; + print("Time to load LCMS collection ", time.time() - start_time, "seconds -", len(lcms_collection), " LCMS runs and ", ncores, " cores") + #10s for 7 samples, 10 cores; 162s for 70 samples, 10 cores lcms_collection.mass_features_dataframe print("Aligning LCMS collection") start_time = time.time() lcms_collection.align_lcms_objects() - print("Time to align LCMS collection: ", time.time() - start_time, "seconds") # 1.5s for 7 samples, 15s for 70 samples - lcms_collection.mass_features_dataframe - assert lcms_collection.mass_features_dataframe.index.name == "coll_mf_id" - + print("Time to align LCMS collection: ", time.time() - start_time, "seconds") + #1.5s for 7 samples; 15s for 70 samples #lcms_collection.plot_tics(type="both") #lcms_collection.plot_alignments() @@ -49,6 +48,8 @@ start_time = time.time() lcms_collection.add_consensus_mass_features() print("Time to calculate distance matrices: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") + # 33 seconds for 22K mass features (7 samples) - 10 cores; 290 seconds for 250K mass features (70 samples) - 10 cores + lcms_collection.mass_features_dataframe.to_csv(collection_path / "collection_mass_features_ward.csv", index=True) print("Here") #lcms_collection. \ No newline at end of file diff --git a/support_code/nmdc/lipidomics/lipidomics_collection_explore.py b/support_code/nmdc/lipidomics/lipidomics_collection_explore.py new file mode 100644 index 000000000..f1c5e7509 --- /dev/null +++ b/support_code/nmdc/lipidomics/lipidomics_collection_explore.py @@ -0,0 +1,23 @@ +import pandas as pd +from pathlib import Path + +collection_path = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/processed_files") +collection_mass_features = pd.read_csv(collection_path / "collection_mass_features_ward.csv") + +# Pivot the mass features dataframe, sample_id as columns and cluster_id as index +mass_feature_pivot = collection_mass_features.pivot(index='cluster', columns='sample_id', values='intensity') + +# Get average mz and rt for each cluster +cluster_avg_mz_rt = collection_mass_features.groupby('cluster').agg({'mz': 'mean', 'scan_time_aligned': 'mean', 'sample_id': 'nunique', 'intensity': 'mean'}).reset_index() + +# Add average mz and rt to mass_feature_pivot +mass_feature_pivot = mass_feature_pivot.join(cluster_avg_mz_rt, on='cluster') + +# Rearrange columns +mass_feature_pivot = mass_feature_pivot[['mz', 'scan_time_aligned', 'sample_id', 'intensity'] + list(mass_feature_pivot.columns[2:])] + +# Save mass_feature_pivot to csv +mass_feature_pivot.to_csv(collection_path / "collection_mass_features_pivot_ward.csv", index=True) + +print("here") + From aab03066d4c6dc333c963806d3a2c8e9af10fd93 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 17 Sep 2024 14:22:34 -0700 Subject: [PATCH 016/158] Start to add functionality for accept reject rt alignment --- corems/mass_spectra/calc/lc_calc.py | 54 +++++++++++++++---- .../nmdc/lipidomics/lipidomics_collection.py | 2 +- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index e05bfa503..8a5a29aad 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1690,19 +1690,52 @@ def align_lcms_objects(self, overwrite=False): # Match the mass features in the LCMS object to the anchor mass features in the center LCMS object. matches_c, matches_i = self.match_mfs(mf_df_c, mf_df_i) + if matches_c is not None: + # Hold out a subset of matches_c and matches_i for spline fitting + matches_c.reset_index(drop=False, inplace=True) + matches_i.reset_index(drop=False, inplace=True) + + #TODO KRH: source holdout fraction from a parameter + #TODO KRH: source random seed from a parameter for reproducibility + idx_holdout = np.random.choice(matches_c.index, size=int(len(matches_c) * 0.3), replace=False) + matches_c_holdout = matches_c.loc[idx_holdout].copy() + matches_i_holdout = matches_i.loc[idx_holdout].copy() + + # Remove the holdout matches from the matches_c and matches_i DataFrames and reset the index + matches_c = matches_c.drop(index=idx_holdout).set_index('sample_name') + matches_i = matches_i.drop(index=idx_holdout).set_index('sample_name') + # Reset the scan_time to the original scan_time matches_i = matches_i.copy() matches_i['scan_time'] = matches_i['scan_time_og'] # Fit the retention times of the LCMS object to the center LCMS object using the matched mass features spl = self.fit_rts(matches_c, matches_i, kernel='rbf', C=1000) - # Set new retention times on scan_df for lc_obj - matches_i['scan_time_fit'] = spl(matches_i['scan_time']) - new_times = spl(self[i].scan_df['scan_time']) - new_scan_info = self[i].scan_df.copy() - new_scan_info['scan_time_aligned'] = new_times - self[i].scan_df = new_scan_info + + # Check if the spline fitting improved the alignment for the holdout matches + matches_i_holdout['scan_time_fit'] = spl(matches_i_holdout['scan_time']) + og_diff = np.abs(matches_i_holdout['scan_time'] - matches_c_holdout['scan_time']) + fit_diff = np.abs(matches_i_holdout['scan_time_fit'] - matches_c_holdout['scan_time']) + fraction_improved = np.sum(fit_diff < og_diff) / len(og_diff) + + #TODO KRH: source improvement threshold from a parameter + use_spline_alignment = fraction_improved > 0.5 + #TODO KRH: use_spline_alignment in the LCMS collection object + + if use_spline_alignment: + # Set new retention times on scan_df for lc_obj using the spline fitting + matches_i['scan_time_fit'] = spl(matches_i['scan_time']) + new_times = spl(self[i].scan_df['scan_time']) + new_scan_info = self[i].scan_df.copy() + new_scan_info['scan_time_aligned'] = new_times + self[i].scan_df = new_scan_info + else: + # Set aligned retention times on scan_df for lc_obj using the original retention times + new_scan_info = self[i].scan_df.copy() + new_scan_info['scan_time_aligned'] = new_scan_info['scan_time'] + self[i].scan_df = new_scan_info + i += index_step if i >= len(self) or i < 0: mf_df_i = None @@ -1711,8 +1744,9 @@ def align_lcms_objects(self, overwrite=False): mf_df_i = full_mf_df.loc[self.samples[i]].copy() mf_df_i['scan_time_og'] = mf_df_i['scan_time'] mf_df_i = mf_df_i.reset_index(drop=False) - # Set scan_time to previous sample's predicted scan_time to find closer matches - mf_df_i['scan_time'] = spl(mf_df_i['scan_time']) #might need to add a copy step here + if use_spline_alignment: + # Set scan_time to previous sample's predicted scan_time to find closer matches + mf_df_i['scan_time'] = spl(mf_df_i['scan_time']) #might need to add a copy step here else: raise ValueError(f'No matches found between the center object and {self.samples[i]}') @@ -1730,15 +1764,15 @@ def add_consensus_mass_features(self): # TODO KRH: source partition size and tol from parameters lazy_partitions = corems_subset.multi_sample_partition(combined_mfs, split_on = 'mz', size=10000, tol=0.001) - # Make distance matrix for each partition and cluster + # Cluster the mass features within each partition if self.parameter_dict["cores"] > lazy_partitions.n_partitions: cores_to_use = lazy_partitions.n_partitions else: cores_to_use = self.parameter_dict["cores"] mfs_with_clusters = lazy_partitions.map(self.cluster_mass_features, processes=cores_to_use) + # Combine the mass features with clusters into a single dataframe and clean up the cluster ids if len(mfs_with_clusters.partition_idx.unique()) > 1: - # Clean up the cluster ids new_cluster_ids = mfs_with_clusters[['cluster', 'partition_idx']].drop_duplicates().reset_index(drop=True) new_cluster_ids['cluster_unqiue'] = new_cluster_ids.index mfs_with_clusters = mfs_with_clusters.merge(new_cluster_ids, on=['cluster', 'partition_idx']) diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 2c8c14d0e..b120a075e 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -5,7 +5,7 @@ if __name__ == "__main__": collection_path = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/processed_files") - manifest_file = collection_path / "manifest.csv" + manifest_file = collection_path / "manifest_small.csv" ''' # Read in manifest file From 3e10066a87162c97e5d9ecaed27220a655c2d1dc Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 17 Sep 2024 14:25:44 -0700 Subject: [PATCH 017/158] Start to add functionality for accept reject rt alignment --- corems/mass_spectra/calc/lc_calc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 8a5a29aad..944e1d71c 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1746,7 +1746,7 @@ def align_lcms_objects(self, overwrite=False): mf_df_i = mf_df_i.reset_index(drop=False) if use_spline_alignment: # Set scan_time to previous sample's predicted scan_time to find closer matches - mf_df_i['scan_time'] = spl(mf_df_i['scan_time']) #might need to add a copy step here + mf_df_i['scan_time'] = spl(mf_df_i['scan_time']) else: raise ValueError(f'No matches found between the center object and {self.samples[i]}') From ab96d02cf05c511e627aef3493edfb833fd6f51c Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 17 Sep 2024 17:54:35 -0700 Subject: [PATCH 018/158] Add small mods on workflow --- support_code/nmdc/lipidomics/lipidomics_workflow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/support_code/nmdc/lipidomics/lipidomics_workflow.py b/support_code/nmdc/lipidomics/lipidomics_workflow.py index a19dd8a9b..99ccd75c7 100644 --- a/support_code/nmdc/lipidomics/lipidomics_workflow.py +++ b/support_code/nmdc/lipidomics/lipidomics_workflow.py @@ -588,11 +588,11 @@ def run_lipid_workflow( if __name__ == "__main__": # Set input variables to run - cores = 1 + cores = 6 #file_dir = Path("tmp_data/thermo_raw_collection") file_dir = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/raw_files") #out_dir = Path("tmp_data/NMDC_processed_collection_0820") - out_dir = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/processed_files") + out_dir = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/processed_files2") #params_toml = Path("tmp_data/thermo_raw_collection/nmdc_lipid_params.toml") params_toml = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/lipid_workflow_params.toml") From e5240a62b5bf49a327b5d3a05e509c049eb47372 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 18 Sep 2024 09:08:38 -0700 Subject: [PATCH 019/158] Add remove unprocessed data from parser --- support_code/nmdc/lipidomics/lipidomics_workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/support_code/nmdc/lipidomics/lipidomics_workflow.py b/support_code/nmdc/lipidomics/lipidomics_workflow.py index 52554bd99..4d66233ce 100644 --- a/support_code/nmdc/lipidomics/lipidomics_workflow.py +++ b/support_code/nmdc/lipidomics/lipidomics_workflow.py @@ -282,6 +282,7 @@ def add_mass_features(myLCMSobj, verbose): if verbose: print("Adding peak metrics") myLCMSobj.add_peak_metrics() + myLCMSobj.remove_unprocessed_data() if verbose: print("Adding associated ms2 spectra") myLCMSobj.add_associated_ms2_dda(spectrum_mode="centroid") @@ -471,7 +472,6 @@ def run_lipid_sp_ms1( myLCMSobj = instantiate_lcms_obj(file_in, verbose) set_params_on_lcms_obj(myLCMSobj, params_toml, verbose) add_mass_features(myLCMSobj, verbose) - myLCMSobj.remove_unprocessed_data() if ms1_molecular_search: molecular_formula_search(myLCMSobj) export_results(myLCMSobj, out_path=out_path, final=False) From a94ae134b11f4efc917a58c75a4ce0c080f7b277 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 18 Sep 2024 18:25:39 -0700 Subject: [PATCH 020/158] Start to add parameters for LCMSCollections class --- corems/encapsulation/factory/parameters.py | 5 ++- .../factory/processingSetting.py | 4 +++ corems/mass_spectra/factory/lc_class.py | 35 +++++++++++++------ corems/mass_spectra/input/corems_hdf5.py | 16 +++++---- .../nmdc/lipidomics/lipidomics_collection.py | 2 +- .../nmdc/lipidomics/lipidomics_workflow.py | 2 +- 6 files changed, 45 insertions(+), 19 deletions(-) diff --git a/corems/encapsulation/factory/parameters.py b/corems/encapsulation/factory/parameters.py index 1ecbcbc84..47686dbcb 100644 --- a/corems/encapsulation/factory/parameters.py +++ b/corems/encapsulation/factory/parameters.py @@ -1,4 +1,4 @@ -from corems.encapsulation.factory.processingSetting import LiquidChromatographSetting, MolecularFormulaSearchSettings, TransientSetting, MassSpecPeakSetting, MassSpectrumSetting +from corems.encapsulation.factory.processingSetting import LiquidChromatographSetting, MolecularFormulaSearchSettings, TransientSetting, MassSpecPeakSetting, MassSpectrumSetting, LCMSCollectionSettings from corems.encapsulation.factory.processingSetting import CompoundSearchSettings, GasChromatographSetting from corems.encapsulation.factory.processingSetting import DataInputSetting @@ -70,6 +70,9 @@ class LCMSParameters: ms1_molecular_search = MolecularFormulaSearchSettings() ms2_molecular_search = MolecularFormulaSearchSettings() + +class LCMSCollectionParameters: + lcms_collection = LCMSCollectionSettings() def default_parameters(file_location): # pragma: no cover """Generate parameters dictionary with the default parameters for data processing diff --git a/corems/encapsulation/factory/processingSetting.py b/corems/encapsulation/factory/processingSetting.py index 7ec7ca052..8acb5fbed 100644 --- a/corems/encapsulation/factory/processingSetting.py +++ b/corems/encapsulation/factory/processingSetting.py @@ -886,3 +886,7 @@ def __post_init__(self): #will get the first number of all possible covalances, which should be the most commum self.used_atom_valences[atom] = covalence[0] +@dataclasses.dataclass +class LCMSCollectionSettings: + cores = 1 + \ No newline at end of file diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index dedfdada9..15387f3d9 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -8,7 +8,7 @@ import matplotlib.pyplot as plt -from corems.encapsulation.factory.parameters import LCMSParameters +from corems.encapsulation.factory.parameters import LCMSParameters, LCMSCollectionParameters from corems.mass_spectra.calc.lc_calc import LCCalculations, PHCalculations, LCMSCollectionCalculations from corems.molecular_id.search.lcms_spectral_search import LCMSSpectralSearch from corems.mass_spectrum.input.numpyArray import ms_from_array_profile @@ -1133,11 +1133,7 @@ def __init__( self._lcms = {} self._combined_mass_features = None self.consensus_mass_features = {} - - # TODO KRH: parameters that should be sourced from somewhere else and better stored - self.parameter_dict = {} - self.parameter_dict["cores"] = self.collection_parser._cores - self.parameter_dict["df_type"] = "dask" + self._parameters = LCMSCollectionParameters() def _reorder_lcms_objects(self): """ @@ -1187,19 +1183,19 @@ def _combine_mass_features(self): None, sets the _combined_mass_features attribute. """ - if self.parameter_dict["cores"] == 1: + if self.parameters.lcms_collection.cores == 1: # Prepare mass features for combination sequentially mf_df_list = [] for lcms_obj in self: mf_df = self._prepare_lcms_mass_features_for_combination(lcms_obj) mf_df_list.append(mf_df) - if self.parameter_dict["cores"] > 1: + if self.parameters.lcms_collection.cores > 1: # Parallelize the mass feature preparation - if self.parameter_dict["cores"] > len(self): + if self.parameters.lcms_collection.cores > len(self): ncores = len(self) else: - ncores = self.parameter_dict["cores"] + ncores = self.parameters.lcms_collection.cores pool = multiprocessing.Pool(ncores) mf_df_list = pool.starmap(self._prepare_lcms_mass_features_for_combination, [(lcms_obj,) for lcms_obj in self]) @@ -1318,6 +1314,25 @@ def plot_alignments(self, plot_legend=False): ax.legend() plt.show() + @property + def parameters(self): + """ + LCMSCollectionParameters : The parameters used for the LCMS collection. + """ + return self._parameters + + @parameters.setter + def parameters(self, paramsinstance): + """ + Sets the parameters used for the LCMS analysis collection. + + Parameters + ----------- + paramsinstance : LCMSCollectionParameters + The parameters used for the LC-MS analysis. + """ + self._parameters = paramsinstance + @property def mass_features_dataframe(self): self._check_mass_features_df() diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index cac65ffe8..355627d42 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -623,7 +623,7 @@ def _validate_parameters(self): f"Parameters files for samples in batch {batch} are not equal." ) - def get_lcms_obj(self, sample_name: str, load_raw=False, load_light=True) -> LCMSBase: + def get_lcms_obj(self, sample_name: str, load_raw=False, load_light=True, use_original_parser=True, raw_file_path=None) -> LCMSBase: """Return a LCMSBase object for a given sample name within the collection. Parameters @@ -637,7 +637,7 @@ def get_lcms_obj(self, sample_name: str, load_raw=False, load_light=True) -> LCM """ hdf5_file = self.folder_location / f"{sample_name}.corems/{sample_name}.hdf5" parser = ReadCoreMSHDFMassSpectra(hdf5_file) - lcms_obj = parser.get_lcms_obj(load_raw=load_raw, load_light=load_light) + lcms_obj = parser.get_lcms_obj(load_raw=load_raw, load_light=load_light, use_original_parser=use_original_parser, raw_file_path=raw_file_path) if load_light: mf_df = lcms_obj.mass_features_to_df() lcms_obj.mass_features = {} @@ -663,6 +663,9 @@ def get_lcms_collection(self, load_raw = False, load_light = True) -> LCMSCollec collection_parser=self ) + # Set the number of cores on the LCMSCollection object from the ReadCoreMSHDFMassSpectraCollection object + lcms_coll.parameters.lcms_collection.cores = self._cores + # Add LCMS objects to the collection samples = self._manifest_dict.keys() @@ -674,16 +677,17 @@ def get_lcms_collection(self, load_raw = False, load_light = True) -> LCMSCollec ncores = self._cores # Create a pool of workers (one for each core or sample, whichever is smaller) pool = multiprocessing.Pool(ncores) - # Load the LCMS objects in parallel - args = [(sample, load_raw, load_light) for sample in samples] + # Load the LCMS objects in parallel - do not instantiate the original parser by default + use_original_parser = False + args = [(sample, load_raw, load_light, use_original_parser) for sample in samples] lcms_objs = pool.starmap(self.get_lcms_obj, args) for sample_name, lcms_obj in zip(samples, lcms_objs): lcms_coll._lcms[sample_name] = lcms_obj elif self._cores == 1: - # Load the LCMS objects sequentially + # Load the LCMS objects sequentially - do not instantiate the original parser by default for sample_name in samples: - lcms_coll._lcms[sample_name] = self.get_lcms_obj(sample_name, load_raw=load_raw, load_light=load_light) + lcms_coll._lcms[sample_name] = self.get_lcms_obj(sample_name, load_raw=load_raw, load_light=load_light, use_original_parser=False) else: raise ValueError("Number of cores must be greater than 0 and set on the ReadCoreMSHDFMassSpectraCollection object.") diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index b120a075e..1664621eb 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -4,7 +4,7 @@ if __name__ == "__main__": - collection_path = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/processed_files") + collection_path = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/processed_files3") manifest_file = collection_path / "manifest_small.csv" ''' diff --git a/support_code/nmdc/lipidomics/lipidomics_workflow.py b/support_code/nmdc/lipidomics/lipidomics_workflow.py index 4d66233ce..56ca3a579 100644 --- a/support_code/nmdc/lipidomics/lipidomics_workflow.py +++ b/support_code/nmdc/lipidomics/lipidomics_workflow.py @@ -593,7 +593,7 @@ def run_lipid_workflow( #file_dir = Path("tmp_data/thermo_raw_collection") file_dir = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/raw_files") #out_dir = Path("tmp_data/NMDC_processed_collection_0820") - out_dir = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/processed_files2") + out_dir = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/processed_files3") #params_toml = Path("tmp_data/thermo_raw_collection/nmdc_lipid_params.toml") params_toml = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/lipid_workflow_params.toml") From 24424e821d2ee10881faae3b780d7a94649f4b63 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 18 Sep 2024 18:32:45 -0700 Subject: [PATCH 021/158] Add anchoring technique to lcms collection alignment --- .../encapsulation/factory/processingSetting.py | 3 +++ corems/mass_spectra/calc/lc_calc.py | 17 +++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/corems/encapsulation/factory/processingSetting.py b/corems/encapsulation/factory/processingSetting.py index 8acb5fbed..fa5debdec 100644 --- a/corems/encapsulation/factory/processingSetting.py +++ b/corems/encapsulation/factory/processingSetting.py @@ -889,4 +889,7 @@ def __post_init__(self): @dataclasses.dataclass class LCMSCollectionSettings: cores = 1 + mass_feature_anchor_technique = ["deconvoluted_mass_spectra"] + mass_feature_anchor_techniques_available = ("deconvoluted_mass_spectra", "absolute_intensity") + mass_feature_anchor_aboslute_intensity_threshold = 10000 \ No newline at end of file diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 944e1d71c..f372336d3 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1473,8 +1473,8 @@ def match_mfs(self, mf_c, mf_i): mf_i['id_i'] = 1 # Set dimensions for matching - dims = ["mz", "scan_time"] #TODO KRH: source this from a parameter, make mz relative - relative = [False, False] #TODO KRH: source this from a parameter, make mz relative + dims = ["mz", "scan_time"] + relative = [False, False] tol = [0.0005, 0.1] #TODO KRH: source this from a parameter, make mz relative # Compute inter-feature distances @@ -1625,12 +1625,17 @@ def get_anchor_mass_features(self, mf_df): :obj:`~pandas.DataFrame` The anchor mass features dataframe. """ - #TODO KRH: add anchoring_technique parameter to source this from a parameter mf_df = mf_df.copy() - # Drop features that are not mass_spectrum_deconvoluted_parent or are NA as mass_spectrum_deconvoluted_parent - mf_df = mf_df.dropna(subset=["mass_spectrum_deconvoluted_parent"]) - mf_df = mf_df[mf_df["mass_spectrum_deconvoluted_parent"]] + if "deconvoluted_mass_spectra" in self.parameters.lcms_collection.mass_feature_anchor_technique: + # Drop features that are not mass_spectrum_deconvoluted_parent or are NA as mass_spectrum_deconvoluted_parent + mf_df = mf_df.dropna(subset=["mass_spectrum_deconvoluted_parent"]) + mf_df = mf_df[mf_df["mass_spectrum_deconvoluted_parent"]] + + if "absolute_intensity" in self.parameters.lcms_collection.mass_feature_anchor_technique: + # Drop features that have an absolute_intensity lower than the threshold + threshold = self.parameters.lcms_collection.mass_feature_anchor_aboslute_intensity_threshold + mf_df = mf_df[mf_df["absolute_intensity"] > threshold] return mf_df From 7ad7a3915f813e3d174ab673773b433595c1ef5a Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 18 Sep 2024 18:43:00 -0700 Subject: [PATCH 022/158] Add alignment acceptable parameters to lcms collection --- .../factory/processingSetting.py | 8 ++++++++ corems/mass_spectra/calc/lc_calc.py | 20 ++++++++++--------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/corems/encapsulation/factory/processingSetting.py b/corems/encapsulation/factory/processingSetting.py index fa5debdec..180a3f6d9 100644 --- a/corems/encapsulation/factory/processingSetting.py +++ b/corems/encapsulation/factory/processingSetting.py @@ -888,8 +888,16 @@ def __post_init__(self): @dataclasses.dataclass class LCMSCollectionSettings: + + # Settings for general processing cores = 1 + + # Settings for doing mass feature alignment mass_feature_anchor_technique = ["deconvoluted_mass_spectra"] mass_feature_anchor_techniques_available = ("deconvoluted_mass_spectra", "absolute_intensity") mass_feature_anchor_aboslute_intensity_threshold = 10000 + alignment_hold_out_fraction = 0.3 + alignment_acceptance_techinque = "fraction_improved" + alignment_acceptance_techinques_available = ("fraction_improved") + alignment_fraction_improved_threshold = 0.3 \ No newline at end of file diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index f372336d3..5cd2df60c 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1701,9 +1701,9 @@ def align_lcms_objects(self, overwrite=False): matches_c.reset_index(drop=False, inplace=True) matches_i.reset_index(drop=False, inplace=True) - #TODO KRH: source holdout fraction from a parameter - #TODO KRH: source random seed from a parameter for reproducibility - idx_holdout = np.random.choice(matches_c.index, size=int(len(matches_c) * 0.3), replace=False) + hold_out_fraction = self.parameters.lcms_collection.alignment_hold_out_fraction + #TODO KRH: pull idx_holdout not randomly, but by scan_time, grabbing a reproducible subset of the full range of scan_times + idx_holdout = np.random.choice(matches_c.index, size=int(len(matches_c) * hold_out_fraction), replace=False) matches_c_holdout = matches_c.loc[idx_holdout].copy() matches_i_holdout = matches_i.loc[idx_holdout].copy() @@ -1722,11 +1722,13 @@ def align_lcms_objects(self, overwrite=False): matches_i_holdout['scan_time_fit'] = spl(matches_i_holdout['scan_time']) og_diff = np.abs(matches_i_holdout['scan_time'] - matches_c_holdout['scan_time']) fit_diff = np.abs(matches_i_holdout['scan_time_fit'] - matches_c_holdout['scan_time']) - fraction_improved = np.sum(fit_diff < og_diff) / len(og_diff) - #TODO KRH: source improvement threshold from a parameter - use_spline_alignment = fraction_improved > 0.5 - #TODO KRH: use_spline_alignment in the LCMS collection object + if self.parameters.lcms_collection.alignment_acceptance_techinque == "fraction_improved": + fraction_improved = np.sum(fit_diff < og_diff) / len(og_diff) + use_spline_alignment = fraction_improved > self.parameters.lcms_collection.alignment_fraction_improved_threshold + #TODO KRH: add use_spline_alignment attribute to in the LCMS collection object somewhere + else: + raise ValueError(f'Alignment acceptance technique {self.parameters.lcms_collection.alignment_acceptance_techinque} not recognized') if use_spline_alignment: # Set new retention times on scan_df for lc_obj using the spline fitting @@ -1770,10 +1772,10 @@ def add_consensus_mass_features(self): lazy_partitions = corems_subset.multi_sample_partition(combined_mfs, split_on = 'mz', size=10000, tol=0.001) # Cluster the mass features within each partition - if self.parameter_dict["cores"] > lazy_partitions.n_partitions: + if self.parameters.lcms_collection.cores > lazy_partitions.n_partitions: cores_to_use = lazy_partitions.n_partitions else: - cores_to_use = self.parameter_dict["cores"] + cores_to_use = self.parameters.lcms_collection.cores mfs_with_clusters = lazy_partitions.map(self.cluster_mass_features, processes=cores_to_use) # Combine the mass features with clusters into a single dataframe and clean up the cluster ids From d30ce86772af54400a7b9405419af2b513a23e47 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 20 Sep 2024 08:58:17 -0700 Subject: [PATCH 023/158] Add documentation for lcms collection settings --- .../factory/processingSetting.py | 93 +++++++++++++++++-- corems/mass_spectra/calc/lc_calc.py | 44 +++++---- 2 files changed, 109 insertions(+), 28 deletions(-) diff --git a/corems/encapsulation/factory/processingSetting.py b/corems/encapsulation/factory/processingSetting.py index 180a3f6d9..3215fa30e 100644 --- a/corems/encapsulation/factory/processingSetting.py +++ b/corems/encapsulation/factory/processingSetting.py @@ -888,16 +888,91 @@ def __post_init__(self): @dataclasses.dataclass class LCMSCollectionSettings: - + """Settings for LCMS collection class + + Attributes + ---------- + cores : int, optional + Number of cores to use for processing. Default is 1. + mass_feature_anchor_technique: list, optional + List of mass feature anchor techniques for retention time alignment. + Default is ['deconvoluted_mass_spectra']. + mass_feature_anchor_techniques_available: tuple, optional + Tuple of available mass feature anchor techniques for retention time alignment. + Default is ('deconvoluted_mass_spectra', 'absolute_intensity'). + mass_feature_anchor_aboslute_intensity_threshold: int, optional + Absolute intensity threshold for mass feature anchor for retention time alignment. + Default is 10000. + alignment_hold_out_fraction: float, optional + Hold out fraction for testing retention time alignment. + Default is 0.3. + alignment_acceptance_techinque: list, optional + List of alignment acceptance techniques for retention time alignment. + Default is ['fraction_improved', 'mean_squared_error_improved']. + alignment_acceptance_techinques_available: tuple, optional + Tuple of available alignment acceptance techniques for retention time alignment. + Default is ('fraction_improved', 'mean_squared_error_improved'). + alignment_acceptance_fraction_improved_threshold: float, optional + Threshold for the improved fraction of the hold out mass features for accepting retention time alignment. + Default is 0.5. + alignment_mz_tol_ppm: int, optional + m/z tolerance in ppm for retention time alignment, in ppm. Default is 5. + alignment_rt_tol: float, optional + Retention time tolerance for retention time alignment, in minutes. Default is 0.3. + consensus_mz_tol_ppm: int, optional + m/z tolerance in ppm for consensus mass feature alignment. Default is 5. + The recommendation is that this value should be the same as alignment_mz_tol_ppm. + consensus_rt_tol: float, optional + Retention time tolerance for consensus mass feature alignment, in minutes. Default is 0.2. + """ # Settings for general processing cores = 1 # Settings for doing mass feature alignment - mass_feature_anchor_technique = ["deconvoluted_mass_spectra"] - mass_feature_anchor_techniques_available = ("deconvoluted_mass_spectra", "absolute_intensity") - mass_feature_anchor_aboslute_intensity_threshold = 10000 - alignment_hold_out_fraction = 0.3 - alignment_acceptance_techinque = "fraction_improved" - alignment_acceptance_techinques_available = ("fraction_improved") - alignment_fraction_improved_threshold = 0.3 - \ No newline at end of file + _mass_feature_anchor_technique: list = dataclasses.field(default_factory=lambda: ["deconvoluted_mass_spectra"]) + mass_feature_anchor_techniques_available: tuple = ("deconvoluted_mass_spectra", "absolute_intensity") + mass_feature_anchor_aboslute_intensity_threshold: int = 10000 + alignment_hold_out_fraction: float = 0.3 + _alignment_acceptance_techinque: list = dataclasses.field(default_factory=lambda: ["fraction_improved", "mean_squared_error_improved"]) + alignment_acceptance_techinques_available: tuple = ("fraction_improved", "mean_squared_error_improved") + alignment_acceptance_fraction_improved_threshold: float = 0.5 + alignment_mz_tol_ppm: int = 5 + alignment_rt_tol: float = 0.3 + + # Consensus mass feature settings + consensus_mz_tol_ppm = alignment_mz_tol_ppm + consensus_rt_tol = 0.2 + + def __post_init__(self): + self.consensus_mz_tol_ppm = self.alignment_mz_tol_ppm + self._validate_alignment_acceptance_techinque(self.alignment_acceptance_techinque) + self._validate_mass_feature_anchor_technique(self.mass_feature_anchor_technique) + + def _validate_alignment_acceptance_techinque(self, techniques): + for technique in techniques: + if technique not in self.alignment_acceptance_techinques_available: + raise ValueError(f"Alignment acceptance technique '{technique}' is not available. Alignment acceptance technique must be passed as a list. Available techniques: {self.alignment_acceptance_techinques_available}") + + def _validate_mass_feature_anchor_technique(self, techniques): + for technique in techniques: + if technique not in self.mass_feature_anchor_techniques_available: + raise ValueError(f"Mass feature anchor technique '{technique}' is not available. Alignment acceptance technique must be passed as a list. Available techniques: {self.mass_feature_anchor_techniques_available}") + + @property + def alignment_acceptance_techinque(self): + return self._alignment_acceptance_techinque + + @alignment_acceptance_techinque.setter + def alignment_acceptance_techinque(self, value): + self._validate_alignment_acceptance_techinque(value) + self._alignment_acceptance_techinque = value + + @property + def mass_feature_anchor_technique(self): + return self._mass_feature_anchor_technique + + @mass_feature_anchor_technique.setter + def mass_feature_anchor_technique(self, value): + self._validate_mass_feature_anchor_technique(value) + self._mass_feature_anchor_technique = value + diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 5cd2df60c..f21bbf3d8 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1474,8 +1474,10 @@ def match_mfs(self, mf_c, mf_i): # Set dimensions for matching dims = ["mz", "scan_time"] - relative = [False, False] - tol = [0.0005, 0.1] #TODO KRH: source this from a parameter, make mz relative + relative = [True, False] + mz_tol = self.parameters.lcms_collection.alignment_mz_tol_ppm * 1E-6 + rt_tol = self.parameters.lcms_collection.alignment_rt_tol + tol = [mz_tol, rt_tol] # Compute inter-feature distances idx = [] @@ -1701,9 +1703,14 @@ def align_lcms_objects(self, overwrite=False): matches_c.reset_index(drop=False, inplace=True) matches_i.reset_index(drop=False, inplace=True) + # Rearrange matches_c and matches_i to be in the order of the scan_time of matches_c + matches_c = matches_c.sort_values(by='scan_time') + matches_i = matches_i.iloc[matches_c.index.values] + hold_out_fraction = self.parameters.lcms_collection.alignment_hold_out_fraction - #TODO KRH: pull idx_holdout not randomly, but by scan_time, grabbing a reproducible subset of the full range of scan_times - idx_holdout = np.random.choice(matches_c.index, size=int(len(matches_c) * hold_out_fraction), replace=False) + # starting with an array of length len(matches_c), select equally spaced indices to hold out + idx_holdout = matches_c.index.values[np.arange(0, len(matches_c), int(1/hold_out_fraction))] + matches_c_holdout = matches_c.loc[idx_holdout].copy() matches_i_holdout = matches_i.loc[idx_holdout].copy() @@ -1723,12 +1730,17 @@ def align_lcms_objects(self, overwrite=False): og_diff = np.abs(matches_i_holdout['scan_time'] - matches_c_holdout['scan_time']) fit_diff = np.abs(matches_i_holdout['scan_time_fit'] - matches_c_holdout['scan_time']) - if self.parameters.lcms_collection.alignment_acceptance_techinque == "fraction_improved": + if "fraction_improved" in self.parameters.lcms_collection.alignment_acceptance_techinque: fraction_improved = np.sum(fit_diff < og_diff) / len(og_diff) - use_spline_alignment = fraction_improved > self.parameters.lcms_collection.alignment_fraction_improved_threshold - #TODO KRH: add use_spline_alignment attribute to in the LCMS collection object somewhere - else: - raise ValueError(f'Alignment acceptance technique {self.parameters.lcms_collection.alignment_acceptance_techinque} not recognized') + use_spline_alignment = fraction_improved > self.parameters.lcms_collection.alignment_acceptance_fraction_improved_threshold + if "mean_squared_error_improved" in self.parameters.lcms_collection.alignment_acceptance_techinque: + mse_og = np.mean(og_diff**2) + mse = np.mean(fit_diff**2) + use_spline_alignment = mse < mse_og + + # Record if we used alignment for this sample + sample_name = self.samples[i] + self._manifest_dict[sample_name]['use_rt_alignment'] = use_spline_alignment if use_spline_alignment: # Set new retention times on scan_df for lc_obj using the spline fitting @@ -1824,16 +1836,14 @@ def cluster_mass_features(self, features): return None # Define how to calculate the distance between features - # TODO KRH: source this from a parameter - # TODO KRH: make mz relative dims = ["mz", "scan_time_aligned"] - relative = [False, False] - tol = [0.001, 0.2] + relative = [True, False] + mz_tol_relative = self.parameters.lcms_collection.consensus_mz_tol_ppm*1e-6 + tol = [mz_tol_relative, self.parameters.lcms_collection.consensus_rt_tol] dist_weight = [1, 1] - na_fill = [None, None] # Check that the dimensions and tolerances are the same length - if len(dims) != len(tol) or len(dims) != len(relative) or len(dims) != len(na_fill) or len(dims) != len(dist_weight): + if len(dims) != len(tol) or len(dims) != len(relative) or len(dims) != len(dist_weight): raise ValueError("The dimensions, tolerances, relative, dist_weight, and na_allow lists must be the same length") # Copy input @@ -1856,10 +1866,6 @@ def cluster_mass_features(self, features): # Construct k-d tree values = features[dims[i]].values - # Fill NAs if applicable - if na_fill[i] is not None: - values = np.where(np.isnan(values), na_fill[i], values) - tree = KDTree(values.reshape(-1, 1)) max_tol = tol[i] From 8c6acb4b3ef2b70cbc885f7ecd7e913c595cd1cf Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 20 Sep 2024 12:21:31 -0700 Subject: [PATCH 024/158] Add option for dropping isotopologues from downstream for lcms collection --- .../factory/processingSetting.py | 6 ++++++ corems/mass_spectra/factory/lc_class.py | 21 +++++++++++++++++++ .../nmdc/lipidomics/lipidomics_collection.py | 3 +++ .../nmdc/lipidomics/lipidomics_workflow.py | 2 +- 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/corems/encapsulation/factory/processingSetting.py b/corems/encapsulation/factory/processingSetting.py index 3215fa30e..df1be9396 100644 --- a/corems/encapsulation/factory/processingSetting.py +++ b/corems/encapsulation/factory/processingSetting.py @@ -894,6 +894,11 @@ class LCMSCollectionSettings: ---------- cores : int, optional Number of cores to use for processing. Default is 1. + drop_isotopologues : bool, optional + If True, drop isotopologues from all analyses. + Note that this will keep mass features identified as monoisotopes or largest ion in deconvoluted mass spectrum. + It will also keep mass features not identified as isotopologues or monoisotopes. + Default is True. mass_feature_anchor_technique: list, optional List of mass feature anchor techniques for retention time alignment. Default is ['deconvoluted_mass_spectra']. @@ -927,6 +932,7 @@ class LCMSCollectionSettings: """ # Settings for general processing cores = 1 + drop_isotopologues: bool = False # Settings for doing mass feature alignment _mass_feature_anchor_technique: list = dataclasses.field(default_factory=lambda: ["deconvoluted_mass_spectra"]) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 15387f3d9..aa9dcd239 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1134,6 +1134,7 @@ def __init__( self._combined_mass_features = None self.consensus_mass_features = {} self._parameters = LCMSCollectionParameters() + self.isotopes_dropped = False def _reorder_lcms_objects(self): """ @@ -1222,6 +1223,10 @@ def _check_mass_features_df(self): If scan_time_aligned is not in the _combined_mass_features, tries to add it. """ + # Check if parameters are set to drop isotopologues and drop if so + if self.parameters.lcms_collection.drop_isotopologues: + if not self.isotopes_dropped: + self._drop_isotopologues() # Check if scan_time_aligned is in combined_mass_features, try to add if not if self._combined_mass_features is not None and "scan_time_aligned" not in self._combined_mass_features.columns: cmb_mf = self._combined_mass_features.copy() @@ -1314,6 +1319,22 @@ def plot_alignments(self, plot_legend=False): ax.legend() plt.show() + def _drop_isotopologues(self): + """Drops isotopologues from the mass features in combined_mass_features dataframe.""" + cmb_mf_df = self._combined_mass_features + + # Keep monos or if no monos + cmb_monos = cmb_mf_df[cmb_mf_df.monoisotopic_mf_id == cmb_mf_df.mf_id] + cmb_nomonos = cmb_mf_df[cmb_mf_df.monoisotopic_mf_id.isnull()] + # Keep deconvoluted parent or if no deconvoluted parent + cmb_decon_parent = cmb_mf_df[cmb_mf_df.mass_spectrum_deconvoluted_parent | cmb_mf_df.monoisotopic_mf_id.isnull()] + + cmb_mf_df2 = pd.concat([cmb_monos, cmb_nomonos, cmb_decon_parent]) + cmb_mf_df2 = cmb_mf_df2[~cmb_mf_df2.index.duplicated(keep='first')] + + self.isotopes_dropped = True + self._combined_mass_features = cmb_mf_df2 + @property def parameters(self): """ diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 1664621eb..2d77a3246 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -33,6 +33,9 @@ print("Time to load LCMS collection ", time.time() - start_time, "seconds -", len(lcms_collection), " LCMS runs and ", ncores, " cores") #10s for 7 samples, 10 cores; 162s for 70 samples, 10 cores lcms_collection.mass_features_dataframe + lcms_collection.parameters.lcms_collection.drop_isotopologues = True + lcms_collection.mass_features_dataframe + print("Calculating distance matrices") print("Aligning LCMS collection") diff --git a/support_code/nmdc/lipidomics/lipidomics_workflow.py b/support_code/nmdc/lipidomics/lipidomics_workflow.py index 56ca3a579..428dda62d 100644 --- a/support_code/nmdc/lipidomics/lipidomics_workflow.py +++ b/support_code/nmdc/lipidomics/lipidomics_workflow.py @@ -593,7 +593,7 @@ def run_lipid_workflow( #file_dir = Path("tmp_data/thermo_raw_collection") file_dir = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/raw_files") #out_dir = Path("tmp_data/NMDC_processed_collection_0820") - out_dir = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/processed_files3") + out_dir = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/processed_files4") #params_toml = Path("tmp_data/thermo_raw_collection/nmdc_lipid_params.toml") params_toml = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/lipid_workflow_params.toml") From 7c6fff71206578f2c9d387a7d13ee017ba8ca740 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 23 Sep 2024 10:50:06 -0700 Subject: [PATCH 025/158] Change mass feature clustering to roll up approach --- corems/mass_spectra/calc/lc_calc.py | 90 ++++++++++++++++--- .../nmdc/lipidomics/lipidomics_collection.py | 32 +++++-- 2 files changed, 105 insertions(+), 17 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index eb9f4e826..950d854d6 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1785,7 +1785,7 @@ def add_consensus_mass_features(self): from corems.chroma_peak.calc import subset as corems_subset # TODO KRH: source partition size and tol from parameters - lazy_partitions = corems_subset.multi_sample_partition(combined_mfs, split_on = 'mz', size=10000, tol=0.001) + lazy_partitions = corems_subset.multi_sample_partition(combined_mfs, split_on = 'mz', size=10000, tol=0.01) # Cluster the mass features within each partition if self.parameters.lcms_collection.cores > lazy_partitions.n_partitions: @@ -1794,19 +1794,26 @@ def add_consensus_mass_features(self): cores_to_use = self.parameters.lcms_collection.cores mfs_with_clusters = lazy_partitions.map(self.cluster_mass_features, processes=cores_to_use) - # Combine the mass features with clusters into a single dataframe and clean up the cluster ids - if len(mfs_with_clusters.partition_idx.unique()) > 1: - new_cluster_ids = mfs_with_clusters[['cluster', 'partition_idx']].drop_duplicates().reset_index(drop=True) - new_cluster_ids['cluster_unqiue'] = new_cluster_ids.index - mfs_with_clusters = mfs_with_clusters.merge(new_cluster_ids, on=['cluster', 'partition_idx']) - mfs_with_clusters['cluster'] = mfs_with_clusters['cluster_unqiue'] - mfs_with_clusters = mfs_with_clusters.drop(columns=['cluster_unqiue']) + # Clean up cluster id names + new_cluster_ids = mfs_with_clusters[['cluster', 'partition_idx']].drop_duplicates().reset_index(drop=True) + new_cluster_ids['cluster_unqiue'] = new_cluster_ids.index + mfs_with_clusters = mfs_with_clusters.merge(new_cluster_ids, on=['cluster', 'partition_idx']) + mfs_with_clusters['cluster'] = mfs_with_clusters['cluster_unqiue'] + mfs_with_clusters = mfs_with_clusters.drop(columns=['cluster_unqiue']) - mfs_with_clusters = mfs_with_clusters.drop(columns=['partition_idx']) + # Deal with duplicated mass features <=> clusters [these arise when a mass feature is a daughter of multiple parents....need to break ties somehow] + + # Tag mass features that are duplicated in multiple clusters + mfs_with_clusters['duplicated'] = mfs_with_clusters.duplicated(subset=['coll_mf_id'], keep=False) # Set coll_mf_id as the index mfs_with_clusters = mfs_with_clusters.set_index('coll_mf_id') + # Find any non-unique index values + if mfs_with_clusters.index.duplicated().any(): + dup_index = mfs_with_clusters[mfs_with_clusters.index.duplicated()] + dup_index.sort_index(inplace=True) + # Set the mass_features_dataframe property with the mfs_with_clusters attribtute self.mass_features_dataframe = mfs_with_clusters @@ -1851,7 +1858,8 @@ def cluster_mass_features(self, features): raise ValueError("The dimensions, tolerances, relative, dist_weight, and na_allow lists must be the same length") # Copy input - features = features.copy().reset_index(drop=False) + features = features.copy().sort_values(by='intensity', ascending=False).reset_index(drop=False) + #TODO KRH: order by central sample, and then by intensity? # Make connectivity matrix for masking within sample mass features if 'sample_id' not in features.columns: @@ -1924,6 +1932,66 @@ def cluster_mass_features(self, features): # Set attribute holding distance matrix self._sparse_distance_matrix = distances + # Roll up features + # Extract indices of within-tolerance points + distances = distances.tocoo() + pairs = np.stack((distances.row, distances.col), axis=1) + pairs_df = pd.DataFrame(pairs, columns=["parent", "child"]) + pairs_df = pairs_df.set_index("parent") + + to_drop = [] + final_pairs_df = [] + while not pairs_df.empty: + # Find root_parents and their children + root_parents = np.setdiff1d(np.unique(pairs_df.index.values), np.unique(pairs_df.child.values)) + children_of_roots = pairs_df.loc[root_parents, "child"].unique() + to_drop = np.append(to_drop, children_of_roots) + + # Add the root_parents and children_of_roots to the final_pairs_df + final_pairs_df.append(pairs_df.loc[root_parents]) + + # Remove root_children as possible parents from pairs_df for next iteration + pairs_df = pairs_df.drop( + index=children_of_roots, errors="ignore" + ) + pairs_df = pairs_df.reset_index().set_index("child") + # Remove root_children as possible children from pairs_df for next iteration + pairs_df = pairs_df.drop(index=children_of_roots) + + # Prepare for next iteration + pairs_df = pairs_df.reset_index().set_index("parent") + + # Clean up the final_pairs_df + final_pairs_df = pd.concat(final_pairs_df) + + ## For each parent, add itself as a child to final_pairs_df + add_df = pd.DataFrame(np.stack((np.unique(final_pairs_df.index), np.unique(final_pairs_df.index)), axis=1), columns=["parent", "child"]) + add_df = add_df.set_index("parent") + final_pairs_df = pd.concat([final_pairs_df, add_df]) + final_pairs_df = final_pairs_df.sort_index() + + # Make a dataframe with unique parents (index of final_pairs_df) and cluster id + cluster_df = pd.DataFrame(np.arange(len(np.unique(final_pairs_df.index))), index=np.unique(final_pairs_df.index), columns=["cluster"]) + cluster_df.index.name = "parent" + + # Add the cluster id to the final_pairs_df + mf_cluster_df = final_pairs_df.merge(cluster_df, left_index=True, right_index=True).reset_index(drop=True) + # reset the index so it's the child and take off the name + mf_cluster_df = mf_cluster_df.set_index("child") + + # Add the cluster id to the features + features_final = features.merge(mf_cluster_df, left_index=True, right_index=True, how="left") + + # Add a tag if the features is the parent + features_final['is_largest'] = np.where(features_final.index.isin(np.unique(final_pairs_df.index)), True, False) + + # If a feature is not part of a cluster, assign it to its own cluster + features_final['cluster'] = features_final['cluster'].fillna(features_final.coll_mf_id) + + return features_final + + + """ # Convert to full matrix distances = distances.todense() @@ -1943,7 +2011,7 @@ def cluster_mass_features(self, features): features['cluster'] = np.arange(len(features.index)) return features - + """ diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 2d77a3246..ea7b1a90d 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -4,7 +4,7 @@ if __name__ == "__main__": - collection_path = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/processed_files3") + collection_path = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/processed_files4") manifest_file = collection_path / "manifest_small.csv" ''' @@ -32,9 +32,9 @@ lcms_collection = parser.get_lcms_collection(load_raw=False, load_light=True) print("Time to load LCMS collection ", time.time() - start_time, "seconds -", len(lcms_collection), " LCMS runs and ", ncores, " cores") #10s for 7 samples, 10 cores; 162s for 70 samples, 10 cores - lcms_collection.mass_features_dataframe lcms_collection.parameters.lcms_collection.drop_isotopologues = True - lcms_collection.mass_features_dataframe + print("Number of total mass features: ", len(lcms_collection.mass_features_dataframe)) + print("Calculating distance matrices") @@ -52,7 +52,27 @@ lcms_collection.add_consensus_mass_features() print("Time to calculate distance matrices: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") # 33 seconds for 22K mass features (7 samples) - 10 cores; 290 seconds for 250K mass features (70 samples) - 10 cores - lcms_collection.mass_features_dataframe.to_csv(collection_path / "collection_mass_features_ward.csv", index=True) - print("Here") + + + # Prepare mass features dataframe for export for examination + collection_mass_features = lcms_collection.mass_features_dataframe + + # Pivot the mass features dataframe, sample_id as columns and cluster_id as index, then remove the index name + mass_feature_pivot = collection_mass_features.pivot(index='cluster', columns='sample_name', values='area').reset_index() + mass_feature_pivot.columns.name = None + + # Get average mz and rt for each cluster + cluster_avg_mz_rt = collection_mass_features.groupby('cluster').agg({'mz': 'median', 'scan_time_aligned': 'median', 'sample_id': 'nunique', 'area': 'max'}).reset_index() + # Rename sample_id to n_samples + cluster_avg_mz_rt.rename(columns={'sample_id': 'n_samples', 'area': 'max_area'}, inplace=True) + + # Add average mz and rt to mass_feature_pivot + mass_feature_pivot = cluster_avg_mz_rt.merge(mass_feature_pivot, on='cluster', how='left') + + # Reorder by mz and reset index + mass_feature_pivot.sort_values('mz', inplace=True) + + # Save mass_feature_pivot to csv + mass_feature_pivot.to_csv(collection_path / "collection_mass_features_pivot_rollup_area.csv", index=True) - #lcms_collection. \ No newline at end of file + print("here") \ No newline at end of file From bf2afdc02c0e15b777893471240fd689edec0598 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 24 Sep 2024 17:07:48 -0700 Subject: [PATCH 026/158] Add functionality for multi-batch alignment to lcms collection --- corems/mass_spectra/calc/lc_calc.py | 129 ++++++++++++------ .../nmdc/lipidomics/lipidomics_collection.py | 23 ++-- .../nmdc/lipidomics/lipidomics_workflow.py | 6 +- 3 files changed, 106 insertions(+), 52 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 74912e714..01c5ee12f 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1672,6 +1672,52 @@ def get_anchor_mass_features(self, mf_df): return mf_df + def attempt_alignment(self, matches_c, matches_i): + """ + Check if alignment is needed for the LCMS objects in the collection. + """ + + # Hold out a subset of matches_c and matches_i for spline fitting + matches_c.reset_index(drop=False, inplace=True) + matches_i.reset_index(drop=False, inplace=True) + + # Rearrange matches_c and matches_i to be in the order of the scan_time of matches_c + matches_c = matches_c.sort_values(by='scan_time') + matches_i = matches_i.iloc[matches_c.index.values] + + hold_out_fraction = self.parameters.lcms_collection.alignment_hold_out_fraction + # starting with an array of length len(matches_c), select equally spaced indices to hold out + idx_holdout = matches_c.index.values[np.arange(0, len(matches_c), int(1/hold_out_fraction))] + + matches_c_holdout = matches_c.loc[idx_holdout].copy() + matches_i_holdout = matches_i.loc[idx_holdout].copy() + + # Remove the holdout matches from the matches_c and matches_i DataFrames and reset the index + matches_c = matches_c.drop(index=idx_holdout).set_index('sample_name') + matches_i = matches_i.drop(index=idx_holdout).set_index('sample_name') + + # Reset the scan_time to the original scan_time + matches_i = matches_i.copy() + matches_i['scan_time'] = matches_i['scan_time_og'] + + # Fit the retention times of the LCMS object to the center LCMS object using the matched mass features + spl = self.fit_rts(matches_c, matches_i, kernel='rbf', C=1000) + + # Check if the spline fitting improved the alignment for the holdout matches + matches_i_holdout['scan_time_fit'] = spl(matches_i_holdout['scan_time']) + og_diff = np.abs(matches_i_holdout['scan_time'] - matches_c_holdout['scan_time']) + fit_diff = np.abs(matches_i_holdout['scan_time_fit'] - matches_c_holdout['scan_time']) + + if "fraction_improved" in self.parameters.lcms_collection.alignment_acceptance_techinque: + fraction_improved = np.sum(fit_diff < og_diff) / len(og_diff) + use_spline_alignment = fraction_improved > self.parameters.lcms_collection.alignment_acceptance_fraction_improved_threshold + if "mean_squared_error_improved" in self.parameters.lcms_collection.alignment_acceptance_techinque: + mse_og = np.mean(og_diff**2) + mse = np.mean(fit_diff**2) + use_spline_alignment = mse < mse_og + + return use_spline_alignment, spl + def align_lcms_objects(self, overwrite=False): """ Align LCMS objects in the collection. @@ -1702,11 +1748,13 @@ def align_lcms_objects(self, overwrite=False): if 'scan_time_aligned' in full_mf_df.columns and not overwrite: raise ValueError('Mass features have already been aligned') - + + anchor_mf_dfs = [] for center_obj_id in center_obj_ids: # Get the anchor mass features from the center LCMS object mf_df_c = full_mf_df.loc[self.samples[center_obj_id]] mf_df_c = self.get_anchor_mass_features(mf_df_c) + anchor_mf_dfs.append(mf_df_c) # Set scan_time_aligned to scan_time for the center LCMS object center_scan_df = self[center_obj_id].scan_df.copy() @@ -1730,44 +1778,7 @@ def align_lcms_objects(self, overwrite=False): matches_c, matches_i = self.match_mfs(mf_df_c, mf_df_i) if matches_c is not None: - # Hold out a subset of matches_c and matches_i for spline fitting - matches_c.reset_index(drop=False, inplace=True) - matches_i.reset_index(drop=False, inplace=True) - - # Rearrange matches_c and matches_i to be in the order of the scan_time of matches_c - matches_c = matches_c.sort_values(by='scan_time') - matches_i = matches_i.iloc[matches_c.index.values] - - hold_out_fraction = self.parameters.lcms_collection.alignment_hold_out_fraction - # starting with an array of length len(matches_c), select equally spaced indices to hold out - idx_holdout = matches_c.index.values[np.arange(0, len(matches_c), int(1/hold_out_fraction))] - - matches_c_holdout = matches_c.loc[idx_holdout].copy() - matches_i_holdout = matches_i.loc[idx_holdout].copy() - - # Remove the holdout matches from the matches_c and matches_i DataFrames and reset the index - matches_c = matches_c.drop(index=idx_holdout).set_index('sample_name') - matches_i = matches_i.drop(index=idx_holdout).set_index('sample_name') - - # Reset the scan_time to the original scan_time - matches_i = matches_i.copy() - matches_i['scan_time'] = matches_i['scan_time_og'] - - # Fit the retention times of the LCMS object to the center LCMS object using the matched mass features - spl = self.fit_rts(matches_c, matches_i, kernel='rbf', C=1000) - - # Check if the spline fitting improved the alignment for the holdout matches - matches_i_holdout['scan_time_fit'] = spl(matches_i_holdout['scan_time']) - og_diff = np.abs(matches_i_holdout['scan_time'] - matches_c_holdout['scan_time']) - fit_diff = np.abs(matches_i_holdout['scan_time_fit'] - matches_c_holdout['scan_time']) - - if "fraction_improved" in self.parameters.lcms_collection.alignment_acceptance_techinque: - fraction_improved = np.sum(fit_diff < og_diff) / len(og_diff) - use_spline_alignment = fraction_improved > self.parameters.lcms_collection.alignment_acceptance_fraction_improved_threshold - if "mean_squared_error_improved" in self.parameters.lcms_collection.alignment_acceptance_techinque: - mse_og = np.mean(og_diff**2) - mse = np.mean(fit_diff**2) - use_spline_alignment = mse < mse_og + use_spline_alignment, spl = self.attempt_alignment(matches_c, matches_i) # Record if we used alignment for this sample sample_name = self.samples[i] @@ -1800,6 +1811,46 @@ def align_lcms_objects(self, overwrite=False): else: raise ValueError(f'No matches found between the center object and {self.samples[i]}') + # Now align each batch using the center objects as anchors with the other batches + mf_df_c = anchor_mf_dfs[0] + for i in center_obj_ids[1:]: + mf_df_i = full_mf_df.loc[self.samples[i]].copy() + mf_df_i['scan_time_og'] = mf_df_i['scan_time'] + mf_df_i = self.get_anchor_mass_features(mf_df_i) + + matches_c, matches_i = self.match_mfs(mf_df_c, mf_df_i) + if matches_c is not None: + use_spline_alignment, spl = self.attempt_alignment(matches_c, matches_i) + + # Record if we used alignment for this sample + sample_name = self.samples[i] + self._manifest_dict[sample_name]['use_rt_alignment'] = use_spline_alignment + + if use_spline_alignment: + # Set new retention times on all this object's + new_times = spl(self[i].scan_df['scan_time']) + new_scan_info = self[i].scan_df.copy() + new_scan_info['scan_time_aligned'] = new_times + self[i].scan_df = new_scan_info + + # Get the batch that this object belongs to + batch = self.manifest[self.samples[i]]['batch'] + + for j in range(len(self)): + if self.manifest[self.samples[j]]['batch'] == batch: + if j != i: + sample_name = self.samples[j] + self._manifest_dict[sample_name]['use_rt_alignment'] = use_spline_alignment + new_scan_info = self[j].scan_df.copy() + new_scan_info['scan_time_aligned'] = spl(self[j].scan_df['scan_time_aligned']) + self[j].scan_df = new_scan_info + + # Set final mass_features_dataframe with the aligned scan_time + center_sample_name = self.samples[center_obj_ids[0]] + self._manifest_dict[center_sample_name]['use_rt_alignment'] = False + new_scan_info = self[center_obj_ids[0]].scan_df.copy() + new_scan_info['scan_time_aligned'] = new_scan_info['scan_time'] + def add_consensus_mass_features(self): # Get the combined mass features from all LCMS objects combined_mfs = self.mass_features_dataframe diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index ea7b1a90d..1656badb6 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -4,22 +4,28 @@ if __name__ == "__main__": - collection_path = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/processed_files4") - manifest_file = collection_path / "manifest_small.csv" - - ''' + collection_path = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/UDN_neg/processed_data") + manifest_file = collection_path / "manifest_working.csv" + """ # Read in manifest file import pandas as pd manifest_df = pd.read_csv(manifest_file) + missing_files = [] # Remove any rows with missing file paths for index, row in manifest_df.iterrows(): sample_folder = Path(row["sample_name"] + ".corems") # Check if sample_folder exists in collection_path if not (collection_path / sample_folder).exists(): + missing_files.append(row["sample_name"]) manifest_df.drop(index, inplace=True) - manifest_df.to_csv(manifest_file, index=False) - ''' + + manifest_df.to_csv(collection_path / "manifest_working.csv", index=False) + # save missing files to a text file + with open(collection_path / "missing_files.txt", "w") as f: + for item in missing_files: + f.write("%s\n" % item) + """ ncores = 10 parser = ReadCoreMSHDFMassSpectraCollection( folder_location = collection_path, @@ -34,9 +40,6 @@ #10s for 7 samples, 10 cores; 162s for 70 samples, 10 cores lcms_collection.parameters.lcms_collection.drop_isotopologues = True print("Number of total mass features: ", len(lcms_collection.mass_features_dataframe)) - - print("Calculating distance matrices") - print("Aligning LCMS collection") start_time = time.time() @@ -45,7 +48,7 @@ #1.5s for 7 samples; 15s for 70 samples #lcms_collection.plot_tics(type="both") - #lcms_collection.plot_alignments() + lcms_collection.plot_alignments() #mass_feature_df = lcms_collection.mass_features_to_df() #print("Adding consensus mass features") start_time = time.time() diff --git a/support_code/nmdc/lipidomics/lipidomics_workflow.py b/support_code/nmdc/lipidomics/lipidomics_workflow.py index d02215415..ddd74a237 100644 --- a/support_code/nmdc/lipidomics/lipidomics_workflow.py +++ b/support_code/nmdc/lipidomics/lipidomics_workflow.py @@ -590,11 +590,11 @@ def run_lipid_workflow( if __name__ == "__main__": # Set input variables to run - cores = 6 + cores = 1 #file_dir = Path("tmp_data/thermo_raw_collection") - file_dir = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/raw_files") + file_dir = Path("/Users/heal742/Library/CloudStorage/OneDrive-PNNL/Documents/_DMS_data/UDN_Neg") #out_dir = Path("tmp_data/NMDC_processed_collection_0820") - out_dir = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/processed_files4") + out_dir = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/UDN_neg/processed_data") #params_toml = Path("tmp_data/thermo_raw_collection/nmdc_lipid_params.toml") params_toml = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/lipid_workflow_params.toml") From 8536c348adbfa510fe4df89edca1a409a6f6f463 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 26 Sep 2024 08:59:01 -0700 Subject: [PATCH 027/158] Make partioning relative or not --- corems/chroma_peak/calc/subset.py | 250 ++---------------- .../factory/processingSetting.py | 11 +- corems/mass_spectra/calc/lc_calc.py | 45 ++-- corems/mass_spectra/factory/lc_class.py | 19 +- .../nmdc/lipidomics/lipidomics_collection.py | 21 +- 5 files changed, 91 insertions(+), 255 deletions(-) diff --git a/corems/chroma_peak/calc/subset.py b/corems/chroma_peak/calc/subset.py index 89cd64961..4b4fa1943 100644 --- a/corems/chroma_peak/calc/subset.py +++ b/corems/chroma_peak/calc/subset.py @@ -8,202 +8,6 @@ import numpy as np import pandas as pd - -class Partitions: - ''' - Generator object that will lazily build and return each partition. - - Attributes - ---------- - features : :obj:`~pandas.DataFrame` - Input feature coordinates and intensities. - split_on : str - Dimension to partition the data. - size : int - Target partition size. - overlap : float - Amount of overlap between partitions to ameliorate edge effects. - - ''' - - def __init__(self, features, split_on='mz', size=1000, overlap=0.05): - ''' - Initialize :obj:`~deimos.subset.Partitions` instance. - - Parameters - ---------- - features : :obj:`~pandas.DataFrame` - Input feature coordinates and intensities. - split_on : str - Dimension to partition the data. - size : int - Target partition size. - overlap : float - Amount of overlap between partitions to ameliorate edge effects. - - ''' - - self.features = features - self.split_on = split_on - self.size = size - self.overlap = overlap - - self._compute_splits() - - def _compute_splits(self): - ''' - Determines data splits for partitioning. - - ''' - - # Unique to split on - idx = np.unique(self.features[self.split_on].values) - - # Number of partitions - partitions = np.ceil(len(idx) / self.size) - - # Determine partition bounds - bounds = [[x.min(), x.max()] for x in np.array_split(idx, partitions)] - for i in range(1, len(bounds)): - bounds[i][0] = bounds[i - 1][1] - self.overlap - - if (self.overlap > 0) & (len(bounds) > 1): - # Functional bounds - fbounds = [] - for i in range(len(bounds)): - a, b = bounds[i] - - # First partition - if i < 1: - b = b - self.overlap / 2 - - # Middle partitions - elif i < len(bounds) - 1: - a = a + self.overlap / 2 - b = b - self.overlap / 2 - - # Last partition - else: - a = a + self.overlap / 2 - - fbounds.append([a, b]) - else: - fbounds = bounds - - self.bounds = bounds - self.fbounds = fbounds - - def __iter__(self): - ''' - Yields each partition. - - Yields - ------ - :obj:`~pandas.DataFrame` - Partition of feature coordinates and intensities. - - ''' - - for a, b in self.bounds: - yield slice(self.features, by=self.split_on, low=a, high=b) - - def map(self, func, processes=1, **kwargs): - ''' - Maps `func` to each partition, then returns the combined result, - accounting for overlap regions. - - Parameters - ---------- - func : function - Function to apply to partitions. - processes : int - Number of parallel processes. If less than 2, a serial mapping is - applied. - kwargs - Keyword arguments passed to `func`. - - Returns - ------- - :obj:`~pandas.DataFrame` - Combined result of `func` applied to partitions. - - ''' - - # Serial - if processes < 2: - result = [func(x, **kwargs) for x in self] - - # Parallel - else: - with mp.Pool(processes=processes) as p: - result = list(p.imap(partial(func, **kwargs), self)) - - # Reconcile overlap - result = [slice(result[i], by=self.split_on, low=a, high=b) - for i, (a, b) in enumerate(self.fbounds)] - - # Combine partitions - return pd.concat(result).reset_index(drop=True) - - def zipmap(self, func, b, processes=1, **kwargs): - ''' - Maps `func` to each partition pair resulting from the zip operation of - `self` and `b`, then returns the combined result, accounting for - overlap regions. - - Parameters - ---------- - func : function - Function to apply to zipped partitions. Must accept and return two - :obj:`~pandas.DataFrame` instances. - b : :obj:`~pandas.DataFrame` - Input feature coordinates and intensities. - processes : int - Number of parallel processes. If less than 2, a serial mapping is - applied. - kwargs - Keyword arguments passed to `func`. - - Returns - ------- - a, b : :obj:`~pandas.DataFrame` - Result of `func` applied to paired partitions. - - ''' - - # Partition other dataset - partitions = (slice(b, by=self.split_on, low=a, high=b_) - for a, b_ in self.bounds) - - # Serial - if processes < 2: - result = [func(a, b_, **kwargs) for a, b_ in zip(self, partitions)] - - # Parallel - else: - with mp.Pool(processes=processes) as p: - result = list(p.starmap(partial(func, **kwargs), - zip(self, partitions))) - - result = {'a': [x[0] for x in result], 'b': [x[1] for x in result]} - - # Reconcile overlap - tmp = [slice(result['a'][i], by=self.split_on, low=a, high=b_, - return_index=True) - for i, (a, b_) in enumerate(self.fbounds)] - - result['a'] = [x[0] for x in tmp] - idx = [x[1] for x in tmp] - result['b'] = [p.iloc[i, :] if i is not None else None for p, - i in zip(result['b'], idx)] - - # Combine partitions - result['a'] = pd.concat(result['a']) - result['b'] = pd.concat(result['b']) - - return result['a'], result['b'] - - class MultiSamplePartitions: ''' Generator object that will lazily build and return each partition constructed @@ -224,7 +28,12 @@ class MultiSamplePartitions: ''' - def __init__(self, features, split_on='mz', size=500, tol=25E-6): + def __init__(self, + features, + split_on: str = 'mz', + size: int = 500, + tol: float = 25E-6, + relative: bool = False): ''' Initialize :obj:`~deimos.subset.Partitions` instance. @@ -240,11 +49,20 @@ def __init__(self, features, split_on='mz', size=500, tol=25E-6): Largest allowed distance between unique `split_on` observations. ''' + if not isinstance(split_on, str): + raise TypeError(f"Expected 'split_on' to be a string, got {type(split_on).__name__}") + if not isinstance(size, int): + raise TypeError(f"Expected 'size' to be an integer, got {type(size).__name__}") + if not isinstance(tol, float): + raise TypeError(f"Expected 'tol' to be a float, got {type(tol).__name__}") + if not isinstance(relative, bool): + raise TypeError(f"Expected 'relative' to be a boolean, got {type(relative).__name__}") self.features = features self.split_on = split_on self.size = size self.tol = tol + self.relative = relative if isinstance(features, dd.DataFrame): self.dask = True @@ -270,8 +88,12 @@ def _compute_splits(self): counts = idx.values idx = idx.index - dxs = np.diff(idx) / idx[:-1] + if self.relative: + dxs = np.diff(idx) / idx[:-1] + else: + dxs = np.diff(idx) + # if relative, convert tol to absolute bins = [] current_count = counts[0] current_bin = [idx[0]] @@ -359,33 +181,7 @@ def map(self, func, processes=1, **kwargs): # Combine partitions return pd.concat(result, ignore_index=True) - -def partition(features, split_on='mz', size=1000, overlap=0.05): - ''' - Partitions data along a given dimension. - - Parameters - ---------- - features : :obj:`~pandas.DataFrame` - Input feature coordinates and intensities. - split_on : str - Dimension to partition the data. - size : int - Target partition size. - overlap : float - Amount of overlap between partitions to ameliorate edge effects. - - Returns - ------- - :obj:`~deimos.subset.Partitions` - A generator object that will lazily build and return each partition. - - ''' - - return Partitions(features, split_on, size, overlap) - - -def multi_sample_partition(features, split_on='mz', size=500, tol=25E-6): +def multi_sample_partition(features, split_on='mz', size=500, tol=25E-6, relative=True): ''' Partitions data along a given dimension. For use with features across multiple samples, e.g. in alignment. @@ -400,6 +196,8 @@ def multi_sample_partition(features, split_on='mz', size=500, tol=25E-6): Target partition size. tol : float Largest allowed distance between unique `split_on` observations. + relative : bool + If `True`, the `tol` parameter is interpreted as a relative tolerance. Returns ------- @@ -408,4 +206,4 @@ def multi_sample_partition(features, split_on='mz', size=500, tol=25E-6): ''' - return MultiSamplePartitions(features, split_on, size, tol) + return MultiSamplePartitions(features, split_on, size, tol, relative) diff --git a/corems/encapsulation/factory/processingSetting.py b/corems/encapsulation/factory/processingSetting.py index df1be9396..900017492 100644 --- a/corems/encapsulation/factory/processingSetting.py +++ b/corems/encapsulation/factory/processingSetting.py @@ -896,7 +896,7 @@ class LCMSCollectionSettings: Number of cores to use for processing. Default is 1. drop_isotopologues : bool, optional If True, drop isotopologues from all analyses. - Note that this will keep mass features identified as monoisotopes or largest ion in deconvoluted mass spectrum. + Note that this will keep mass features identified as monoisotopes and any largest ion in deconvoluted mass spectrum. It will also keep mass features not identified as isotopologues or monoisotopes. Default is True. mass_feature_anchor_technique: list, optional @@ -929,6 +929,8 @@ class LCMSCollectionSettings: The recommendation is that this value should be the same as alignment_mz_tol_ppm. consensus_rt_tol: float, optional Retention time tolerance for consensus mass feature alignment, in minutes. Default is 0.2. + consensus_partition_size: int, optional + Partition size for consensus mass feature alignment. Default is 5000. """ # Settings for general processing cores = 1 @@ -943,11 +945,14 @@ class LCMSCollectionSettings: alignment_acceptance_techinques_available: tuple = ("fraction_improved", "mean_squared_error_improved") alignment_acceptance_fraction_improved_threshold: float = 0.5 alignment_mz_tol_ppm: int = 5 - alignment_rt_tol: float = 0.3 + alignment_rt_tol: float = 0.4 # Consensus mass feature settings consensus_mz_tol_ppm = alignment_mz_tol_ppm - consensus_rt_tol = 0.2 + consensus_rt_tol = 0.3 + consensus_partition_size = 10000 + filter_consensus_mass_features = True + consensus_min_sample_fraction = 0.2 def __post_init__(self): self.consensus_mz_tol_ppm = self.alignment_mz_tol_ppm diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 01c5ee12f..beca7f323 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1,14 +1,13 @@ import numpy as np import pandas as pd +import warnings from ripser import ripser import scipy from scipy import sparse from scipy.spatial import KDTree -from scipy.spatial.distance import cdist -from ms_entropy import FlashEntropySearch -from sklearn.cluster import AgglomerativeClustering from sklearn.svm import SVR + from corems.chroma_peak.factory.chroma_peak_classes import LCMSMassFeature from corems.mass_spectra.calc import SignalProcessing as sp from corems.mass_spectra.factory.LC_Temp import EIC_Data @@ -1862,8 +1861,14 @@ def add_consensus_mass_features(self): # Partition the mass features by mz so we can parallelize the matching before clustering from corems.chroma_peak.calc import subset as corems_subset - # TODO KRH: source partition size and tol from parameters - lazy_partitions = corems_subset.multi_sample_partition(combined_mfs, split_on = 'mz', size=10000, tol=0.01) + # get max mz from combined_mfs and calculate tolerance from ppm + mz_tol = self.parameters.lcms_collection.consensus_mz_tol_ppm * 1e-6 + n_partition_size = self.parameters.lcms_collection.consensus_partition_size + lazy_partitions = corems_subset.multi_sample_partition(combined_mfs, split_on = 'mz', size=n_partition_size, tol=mz_tol, relative=True) + + # If any of lazy_partitions._counts is 2xn_partition_size, issue a warning + if np.array(lazy_partitions._counts).max() > 2*n_partition_size: + warnings.warn('Some partitions are larger than 2x the goal partition size. Consider increasing the partition or decreasing the mz_tol.') # Cluster the mass features within each partition if self.parameters.lcms_collection.cores > lazy_partitions.n_partitions: @@ -1879,22 +1884,16 @@ def add_consensus_mass_features(self): mfs_with_clusters['cluster'] = mfs_with_clusters['cluster_unqiue'] mfs_with_clusters = mfs_with_clusters.drop(columns=['cluster_unqiue']) - # Deal with duplicated mass features <=> clusters [these arise when a mass feature is a daughter of multiple parents....need to break ties somehow] + # Warn if any mass features are in multiple clusters + if mfs_with_clusters.duplicated(subset='coll_mf_id', keep=False).any(): + warnings.warn('Some mass features are in multiple clusters! This may indicate that the mz_tol or rt_tol is too large.') - # Tag mass features that are duplicated in multiple clusters - mfs_with_clusters['duplicated'] = mfs_with_clusters.duplicated(subset=['coll_mf_id'], keep=False) - - # Set coll_mf_id as the index + # Set the index back to coll_mf_id mfs_with_clusters = mfs_with_clusters.set_index('coll_mf_id') - # Find any non-unique index values - if mfs_with_clusters.index.duplicated().any(): - dup_index = mfs_with_clusters[mfs_with_clusters.index.duplicated()] - dup_index.sort_index(inplace=True) - - # Set the mass_features_dataframe property with the mfs_with_clusters attribtute self.mass_features_dataframe = mfs_with_clusters + #TODO KRH: drop consensus mass features that were not observed in previously set fraction of samples def cluster_mass_features(self, features): ''' @@ -1923,6 +1922,8 @@ def cluster_mass_features(self, features): if features is None: return None + else: + features = features.copy() # Define how to calculate the distance between features dims = ["mz", "scan_time_aligned"] @@ -1935,8 +1936,11 @@ def cluster_mass_features(self, features): if len(dims) != len(tol) or len(dims) != len(relative) or len(dims) != len(dist_weight): raise ValueError("The dimensions, tolerances, relative, dist_weight, and na_allow lists must be the same length") - # Copy input - features = features.copy().sort_values(by='intensity', ascending=False).reset_index(drop=False) + center_obj_ids = self.manifest_dataframe[self.manifest_dataframe['center']].collection_id.values + features['center_obj'] = features['sample_id'] == center_obj_ids[0] + + # First sort by center_obj and then by intensity + features = features.sort_values(by=['center_obj', 'sample_id', 'intensity'], ascending=[False, True, False]).reset_index(drop=False) #TODO KRH: order by central sample, and then by intensity? # Make connectivity matrix for masking within sample mass features @@ -2022,11 +2026,14 @@ def cluster_mass_features(self, features): while not pairs_df.empty: # Find root_parents and their children root_parents = np.setdiff1d(np.unique(pairs_df.index.values), np.unique(pairs_df.child.values)) + # Check if there are any repeat children of roots children_of_roots = pairs_df.loc[root_parents, "child"].unique() to_drop = np.append(to_drop, children_of_roots) # Add the root_parents and children_of_roots to the final_pairs_df - final_pairs_df.append(pairs_df.loc[root_parents]) + df_to_add = pairs_df.loc[root_parents] + df_to_add = df_to_add.drop_duplicates(subset='child', keep='first') + final_pairs_df.append(df_to_add) # Remove root_children as possible parents from pairs_df for next iteration pairs_df = pairs_df.drop( diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index f76f9628f..7e5a8f33e 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1114,7 +1114,8 @@ def tic(self, tic_list): class LCMSCollection(LCMSCollectionCalculations): """A class representing a collection of liquid chromatography-mass spectrometry (LC-MS) runs. - These runs can be from the same or different samples, but must be from the same instrument and have the same parameters. + These runs can be from the same or different samples, but must be from the same instrument and have the same parameters + for the initial processing steps. The LCMS objects are stored in an ordered dictionary with the sample name as the key. Parameters ----------- @@ -1392,5 +1393,21 @@ def manifest(self): @property def manifest_dataframe(self): return pd.DataFrame(self._manifest_dict).T + + @property + def consensus_mass_feature_dataframe(self): + df = self.mass_features_dataframe + #TODO KRH: build this out + + # Check if mass features are clustered + if 'cluster' not in df.columns: + return None + else: + # Group by cluster and summarize median mz, median scan_time, min max and median intensity, and count + cluster_summary = df.groupby('cluster').agg({'mz': 'median', 'scan_time': 'median', 'intensity': ['min', 'max', 'median'], 'mf_id': 'count'}) + + # Clean up column names + cluster_summary.columns = ['_'.join(col).strip() for col in cluster_summary.columns.values] + diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 1656badb6..1fdf7ada2 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -5,7 +5,8 @@ if __name__ == "__main__": collection_path = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/UDN_neg/processed_data") - manifest_file = collection_path / "manifest_working.csv" + manifest_file = collection_path / "manifest_small_test.csv" + """ # Read in manifest file import pandas as pd @@ -26,6 +27,7 @@ for item in missing_files: f.write("%s\n" % item) """ + ncores = 10 parser = ReadCoreMSHDFMassSpectraCollection( folder_location = collection_path, @@ -48,18 +50,20 @@ #1.5s for 7 samples; 15s for 70 samples #lcms_collection.plot_tics(type="both") - lcms_collection.plot_alignments() + #lcms_collection.plot_alignments() #mass_feature_df = lcms_collection.mass_features_to_df() #print("Adding consensus mass features") start_time = time.time() lcms_collection.add_consensus_mass_features() - print("Time to calculate distance matrices: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") - # 33 seconds for 22K mass features (7 samples) - 10 cores; 290 seconds for 250K mass features (70 samples) - 10 cores - + print("Time to roll up consensus mass features: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") # Prepare mass features dataframe for export for examination collection_mass_features = lcms_collection.mass_features_dataframe + # Save mass features to csv + collection_mass_features.to_csv(collection_path / "collection_mass_features.csv", index=False) + + """ # Pivot the mass features dataframe, sample_id as columns and cluster_id as index, then remove the index name mass_feature_pivot = collection_mass_features.pivot(index='cluster', columns='sample_name', values='area').reset_index() mass_feature_pivot.columns.name = None @@ -78,4 +82,9 @@ # Save mass_feature_pivot to csv mass_feature_pivot.to_csv(collection_path / "collection_mass_features_pivot_rollup_area.csv", index=True) - print("here") \ No newline at end of file + #TODO KRH: Add code to load and save information about chromatographic settings + #TODO KRH: Add code to save and load collection to HDF5 file + #TODO KRH: Add code to plot a consensus mass feature + + print("here") + """ \ No newline at end of file From 419eaa302644adc1103becf008292abbc91aefe8 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 26 Sep 2024 12:36:34 -0700 Subject: [PATCH 028/158] Add method for summarizing clusters --- corems/mass_spectra/calc/lc_calc.py | 39 ++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index beca7f323..a0a7cc133 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1884,8 +1884,19 @@ def add_consensus_mass_features(self): mfs_with_clusters['cluster'] = mfs_with_clusters['cluster_unqiue'] mfs_with_clusters = mfs_with_clusters.drop(columns=['cluster_unqiue']) + # Deal with duplicates + cluster_summary = self.summarize_clusters(mfs_with_clusters) + # Scenario 1: Two mass features from single sample are in the same cluster + # Solution: First see if there is a different cluster that one of the mass features can be moved to (based on mz and rt) + # If not, then choose the mass feature with the highest intensity as the representative mass feature and move the other mass feature to a new cluster (its own cluster) + + # Scenario 2: A single mass feature belongs to multiple clusters + # Solution: Choose the cluster with the closest median rt to the mass feature and assign the mass feature to that cluster + + # Warn if any mass features are in multiple clusters if mfs_with_clusters.duplicated(subset='coll_mf_id', keep=False).any(): + warnings.warn('Some mass features are in multiple clusters! This may indicate that the mz_tol or rt_tol is too large.') # Set the index back to coll_mf_id @@ -1895,6 +1906,32 @@ def add_consensus_mass_features(self): #TODO KRH: drop consensus mass features that were not observed in previously set fraction of samples + def summarize_clusters(self, features): + """ + Summarize the clusters of mass features by median attributes + """ + # First check if there are minimum columsn in the features dataframe + if len(features.columns) < 1: + return None + + summary_df = features.groupby('cluster').agg({ + 'mz': 'median', + 'scan_time_aligned': 'median', + 'half_height_width': 'median', + 'tailing_factor': 'median', + 'dispersity_index': 'median', + 'sample_id': ['nunique', 'count'], + 'intensity': ['max', 'median'], + 'persistence': ['max', 'median'] + }).reset_index() + + # Fix the column names + summary_df.columns = ['_'.join(col).strip() for col in summary_df.columns.values if col != 'cluster'] + summary_df = summary_df.rename(columns={'cluster_': 'cluster'}) + summary_df = summary_df.reset_index(drop=True) + + return summary_df + def cluster_mass_features(self, features): ''' Cluster features within provided linkage tolerances. Recursively merges @@ -2032,7 +2069,7 @@ def cluster_mass_features(self, features): # Add the root_parents and children_of_roots to the final_pairs_df df_to_add = pairs_df.loc[root_parents] - df_to_add = df_to_add.drop_duplicates(subset='child', keep='first') + #df_to_add = df_to_add.drop_duplicates(subset='child', keep='first') final_pairs_df.append(df_to_add) # Remove root_children as possible parents from pairs_df for next iteration From 7315895cb13f299996cdef41ca89a401230d88c1 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 27 Sep 2024 10:04:11 -0700 Subject: [PATCH 029/158] Add ability to add basic chromotography to lcmscollection --- corems/mass_spectra/factory/lc_class.py | 51 ++++++++++++ corems/mass_spectra/input/corems_hdf5.py | 79 ++++++++++++++++++- .../nmdc/lipidomics/lipidomics_collection.py | 4 +- 3 files changed, 130 insertions(+), 4 deletions(-) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 7e5a8f33e..83e483849 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1125,6 +1125,10 @@ class LCMSCollection(LCMSCollectionCalculations): Methods -------- + + Notes + ------ + This class is not intended to be instantiated directly, but rather instantiated using a parser object and then interacted with. """ def __init__( @@ -1136,6 +1140,9 @@ def __init__( self.collection_location = collection_location self._manifest_dict = manifest self.collection_parser = collection_parser + + # These attributes are generally set by the parser during instantiation of this class + self._chromatography_df = None self._lcms = {} self._combined_mass_features = None self.consensus_mass_features = {} @@ -1181,6 +1188,41 @@ def _prepare_lcms_mass_features_for_combination(self, lcms_obj): return mf_df + def _convert_solvent_A(self, x): + """ + Converts the solvent A fraction based on the scan time. + + Parameters + ----------- + x : float or numpy.ndarray + The scan time in minutes or an array of scan times in minutes. + + Returns + -------- + float or numpy.ndarray or None + The solvent A fraction at the scan time or an array of solvent A fractions at the scan times (if the function is set) + + Notes + ------ + This function is set by the _interpolate_chromatography_data method and is used to convert the solvent A fraction based on the scan time (in minutes) + """ + pass + + def _interpolate_chromatography_data(self): + """ + Interpolates the chromatography data for each LCMS object in the collection and defines the _convert_solvent_A function. + """ + chromatogram_df = self._chromatography_df.copy() + xp = chromatogram_df.time_min.values + yp = chromatogram_df.A.values + + # Define a function to interpolate the chromatogram data + def interp(x): + pred_y = np.interp(x, xp, yp) + return pred_y + + self._convert_solvent_A = interp + def _combine_mass_features(self): """ Concatenates the mass features from all the LCMS objects in the collection. @@ -1207,13 +1249,22 @@ def _combine_mass_features(self): mf_df_list = pool.starmap(self._prepare_lcms_mass_features_for_combination, [(lcms_obj,) for lcms_obj in self]) combined_mass_features = pd.concat(mf_df_list) + # Move coll_mf_id, sample_name, and sample_id to front cols = combined_mass_features.columns.tolist() top_cols = ["coll_mf_id", "sample_name", "sample_id", "mz", "scan_time_aligned"] cols = [x for x in top_cols + [col for col in cols if col not in top_cols] if x in cols] combined_mass_features = combined_mass_features[cols] + # Make coll_mf_id the index combined_mass_features = combined_mass_features.set_index("coll_mf_id") + + # If _chromatogram_df is set, combine it with the mass features dataframe to add A fraction for each mass feature + if self._chromatography_df is not None: + + # Add solvent A fraction to the combined mass features dataframe + combined_mass_features['solvent_A_frac'] = self._convert_solvent_A(combined_mass_features.scan_time.values) + self._combined_mass_features = combined_mass_features def _check_mass_features_df(self): diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index c18d746ae..a61184616 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -533,7 +533,36 @@ def add_original_parser(self, mass_spectra, raw_file_path=None): class ReadCoreMSHDFMassSpectraCollection: - def __init__(self, folder_location: str, manifest_file: str, cores: int = 1): + """Class to read a collection of CoreMS HDF5 files and populate a LCMSCollection object. + + Parameters + ---------- + folder_location : str + The location of the folder containing the CoreMS HDF5 files. + manifest_file : str + The location of the manifest file containing the sample names, order, and batch. + This must be a csv with the following columns: 'sample_name', 'order', 'batch'. + Other fields can be included in the manifest file, but these are required. + chromatography_file: str, optional + The location of the chromatography file containing at a minimum time_min and A (fraction of solvent A). + Default is None. + cores : int + The number of cores to use for multiprocessing. Default is 1. + + Attributes + ---------- + folder_location : str + The location of the folder containing the CoreMS HDF5 files. + manifest_filepath : str + The location of the manifest file containing the sample names, order, and batch. + """ + def __init__( + self, + folder_location: str, + manifest_file: str, + chromatography_file: str = None, + cores: int = 1 + ): # Check for folder location and manifest file if not folder_location.exists(): raise FileNotFoundError(f"Folder location {folder_location} not found.") @@ -545,11 +574,41 @@ def __init__(self, folder_location: str, manifest_file: str, cores: int = 1): raise ValueError("Manifest file must be a CSV.") self.folder_location = folder_location + self._chromatography_df = None + self._manifest_dict = None self._parse_manifest(manifest_file) self._validate_manifest() self._validate_parameters() self._validate_cores(cores) - + if chromatography_file is not None: + self._validate_chromatography_file(chromatography_file) + + def _validate_chromatography_file(self, chromatography_file): + # Check if the chromatography file exists + if not chromatography_file.exists(): + raise FileNotFoundError(f"Chromatography file {chromatography_file} not found.") + # Read in the chromatography file, if the first line starts with #, skip it + with open(chromatography_file, "r") as f: + first_line = f.readline() + if first_line.startswith('\ufeff"#'): + chromat_df = pd.read_csv(chromatography_file, skiprows=1) + else: + chromat_df = pd.read_csv(chromatography_file) + + # Check if the following columns exist in the chromatography file + if not all(col in chromat_df.columns for col in ["time_min", "A"]): + raise ValueError("Chromatography file must contain the following columns: 'time_min', 'A'.") + + # Check if the time_min column is in ascending order + if not chromat_df["time_min"].is_monotonic_increasing: + raise ValueError("Chromatography file must be in ascending order by 'time_min'.") + + # Check if the A column is between 0 and 1 + if not chromat_df["A"].between(0, 1).all(): + raise ValueError("Chromatography file 'A' column must be between 0 and 1.") + + self._chromatography_df = chromat_df + def _validate_cores(self, cores): # Check if the cores parameter is an integer greater than 0 and less than the number of cores available if not isinstance(cores, int) or cores < 1: @@ -562,6 +621,7 @@ def _validate_cores(self, cores): def _parse_manifest(self, manifest_file): """Parse the manifest file and set the manifest dictionary.""" + self.manifest_filepath = manifest_file manifest = pd.read_csv(manifest_file) # Check if the following columns exisit in the manifest file if not all( @@ -669,6 +729,7 @@ def get_lcms_collection(self, load_raw = False, load_light = True) -> LCMSCollec # Set the number of cores on the LCMSCollection object from the ReadCoreMSHDFMassSpectraCollection object lcms_coll.parameters.lcms_collection.cores = self._cores + lcms_coll._chromatography_df = self._chromatography_df # Add LCMS objects to the collection samples = self._manifest_dict.keys() @@ -708,9 +769,17 @@ def get_lcms_collection(self, load_raw = False, load_light = True) -> LCMSCollec # Reorder the LCMS objects lcms_coll._reorder_lcms_objects() - + # Collect the mass features from the LCMS objects and combine them into a single dataframe for the collection lcms_coll._combine_mass_features() + + # If chromatography file is provided, interpolate the chromatography data + if self._chromatography_df is not None: + lcms_coll._interpolate_chromatography_data() + # Add the chromatography data to the combined mass features dataframe + combined_mf_df = lcms_coll.mass_features_dataframe + combined_mf_df['fraction_A'] = lcms_coll._convert_solvent_A(combined_mf_df.scan_time.values) + lcms_coll.mass_features_dataframe = combined_mf_df # If load_light, remove the mass_feature attribute from the individual LCMS objects if load_light: @@ -730,6 +799,10 @@ def manifest(self): def manifest_dataframe(self): return pd.DataFrame(self._manifest_dict).T + @property + def chromatography_df(self): + return self._chromatography_df + @property def hdf5_files(self): return [ diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 1fdf7ada2..42944f7db 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -5,7 +5,8 @@ if __name__ == "__main__": collection_path = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/UDN_neg/processed_data") - manifest_file = collection_path / "manifest_small_test.csv" + manifest_file = collection_path / "manifest_very_small.csv" + chromatography_file = collection_path / "long_lipid_gradient_chroma.csv" """ # Read in manifest file @@ -32,6 +33,7 @@ parser = ReadCoreMSHDFMassSpectraCollection( folder_location = collection_path, manifest_file = manifest_file, + chromatography_file=chromatography_file, cores = ncores ) From 4d1002ff59bbf65f93a207c7ead7c14091c4ca3d Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 30 Sep 2024 13:59:53 -0700 Subject: [PATCH 030/158] Add functionality for merging appropriate consensus mass features after agglom clustering --- corems/mass_spectra/calc/lc_calc.py | 292 ++++++++++++++++++++--- corems/mass_spectra/factory/lc_class.py | 27 +-- corems/mass_spectra/input/corems_hdf5.py | 1 - 3 files changed, 263 insertions(+), 57 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 023acf7d0..46ab7071a 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -6,6 +6,7 @@ from scipy import sparse from scipy.spatial import KDTree from sklearn.svm import SVR +from sklearn.cluster import AgglomerativeClustering from corems.chroma_peak.factory.chroma_peak_classes import LCMSMassFeature @@ -1851,8 +1852,9 @@ def align_lcms_objects(self, overwrite=False): new_scan_info['scan_time_aligned'] = new_scan_info['scan_time'] def add_consensus_mass_features(self): - # Get the combined mass features from all LCMS objects - combined_mfs = self.mass_features_dataframe + # Get the combined mass features from all LCMS objects, keep the original index as a separate column + combined_mfs = self.mass_features_dataframe.copy() + combined_mfs['coll_mf_id'] = combined_mfs.index # Check if the mass features have been aligned if 'scan_time_aligned' not in combined_mfs.columns: @@ -1875,18 +1877,104 @@ def add_consensus_mass_features(self): cores_to_use = lazy_partitions.n_partitions else: cores_to_use = self.parameters.lcms_collection.cores - mfs_with_clusters = lazy_partitions.map(self.cluster_mass_features, processes=cores_to_use) + #mfs_with_clusters = lazy_partitions.map(self.cluster_mass_features, processes=cores_to_use) + mfs_with_clusters = lazy_partitions.map(self.cluster_mass_features_agg_cluster, processes=cores_to_use) - # Clean up cluster id names + # Clean up cluster id names after partitioning new_cluster_ids = mfs_with_clusters[['cluster', 'partition_idx']].drop_duplicates().reset_index(drop=True) new_cluster_ids['cluster_unqiue'] = new_cluster_ids.index mfs_with_clusters = mfs_with_clusters.merge(new_cluster_ids, on=['cluster', 'partition_idx']) mfs_with_clusters['cluster'] = mfs_with_clusters['cluster_unqiue'] mfs_with_clusters = mfs_with_clusters.drop(columns=['cluster_unqiue']) + # Embed a new cluster id into the mass features dataframe and set as index + mfs_with_clusters['idx'] = mfs_with_clusters.index + + # Check if any clusters can be merged into a single cluster + eval_dict = self.evaluate_clusters_for_repeats(mfs_with_clusters) + + # Merge clusters identified in eval_dict + while len(eval_dict['merge_these_clusters']) > 0: + list_of_clusters_to_merge = [[x[0], x[1]] for x in eval_dict['merge_these_clusters']] + # Convert to a dataframe with columns "new_cluster" and "cluster" + df = pd.DataFrame(np.array(list_of_clusters_to_merge), columns=["new_cluster", "cluster"]) + # Drop duplicates of "child" clusters + df = df.drop_duplicates("cluster", keep="first") + df = df.drop_duplicates("new_cluster", keep="first") + mfs_with_clusters = mfs_with_clusters.merge(df, on="cluster", how="left") + mfs_with_clusters["cluster"] = mfs_with_clusters["new_cluster"].fillna(mfs_with_clusters["cluster"]) + mfs_with_clusters = mfs_with_clusters.drop(columns=["new_cluster"]) + + # Re-evaluate clusters for repeats + eval_dict = self.evaluate_clusters_for_repeats(mfs_with_clusters) + + #TODO KRH: Deal with isomers better? Pool them together and then split them out using samples with 2 as the template? + + self.mass_features_dataframe = mfs_with_clusters + + """ # Deal with duplicates + to_reassign = [] + + # Find the overlap of clusters that have multiple mass features and mass features that are in multiple clusters + mfs_with_clusters["mf_in_dupe_cluster"] = mfs_with_clusters.duplicated(subset='coll_mf_id', keep=False) + cluster_summary = self.summarize_clusters(mfs_with_clusters) - # Scenario 1: Two mass features from single sample are in the same cluster + clusters_with_dupes = cluster_summary[cluster_summary['sample_id_nunique'] < cluster_summary['sample_id_count']] + + mfs_with_clusters["cluster_with_dupe_mf"] = mfs_with_clusters['cluster'].isin(clusters_with_dupes['cluster']) + + # Scenario 1: One mass feature is assigned to multiple clusters + # Solution, choose the cluster with the closest median rt to the mass feature and assign the mass feature to that cluster + if len(clusters_with_dupes) > 0: + mfs_with_clusters_dupes = mfs_with_clusters[mfs_with_clusters['mf_in_dupe_cluster']] + for mf in mfs_with_clusters_dupes['coll_mf_id'].values: + # Pull out the cluster it is assigned to + distances = [] + + + # Scenario 1: Two mass features from a single sample are assigned into a single cluster and neither is in a different cluster + # Choose one to stay in the cluster and the other to marked for reassignment + if len(clusters_with_dupes) > 0: + for cluster in clusters_with_dupes['cluster'].values: + cluster_summary_sub = cluster_summary[cluster_summary['cluster'] == cluster] + sub_df = mfs_with_clusters[mfs_with_clusters['cluster'] == cluster].copy() + # Mark if sample_id is duplicated + sub_df['sample_id_duplicated'] = sub_df.duplicated(subset='sample_id', keep=False) + # Get the mass features that are duplicated in this cluster and are not in any other cluster + #TODO KRH: This is still pulling mass features that are in other clusters (7_0 is the example) + duplicated_sample_ids = sub_df[sub_df['sample_id_duplicated'] & ~sub_df['mf_in_dupe_cluster']]['sample_id'].unique() + for sample_id in duplicated_sample_ids: + # Print length of mfs_with_clusters + print("Length of mfs_with_clusters: ", len(mfs_with_clusters)) + # Pull mass features + sample_mfs = sub_df[sub_df['sample_id'] == sample_id][['idx', 'coll_mf_id', 'mz', 'scan_time_aligned', 'intensity', 'persistence', 'mass_spectrum_deconvoluted_parent']] + # Compare mz to the median mz of the cluster + sample_mfs['ppm_diff'] = np.abs(sample_mfs['mz'] - cluster_summary_sub['mz_median'].values[0])/cluster_summary_sub['mz_median'].values[0]*1e6 + sample_mfs['ppm_diff_rank'] = sample_mfs['ppm_diff'].rank() + sample_mfs['time_diff'] = np.abs(sample_mfs['scan_time_aligned'] - cluster_summary_sub['scan_time_aligned_median'].values[0]) + sample_mfs['time_diff_rank'] = sample_mfs['time_diff'].rank() + + # Check if there is a clear winner (lowest ppm_diff_rank and time_diff_rank) + sample_mfs['rank_sum'] = sample_mfs['ppm_diff_rank'] + sample_mfs['time_diff_rank'] + best_mf = sample_mfs[sample_mfs['rank_sum'] == sample_mfs['rank_sum'].min()] + if len(best_mf) == 1: + orphan_idx = sample_mfs[sample_mfs['rank_sum'] != sample_mfs['rank_sum'].min()]['idx'].values + orphan_mfs = mfs_with_clusters.loc[orphan_idx] + # Drop orphan_mf records from mfs_with_clusters but add them to clusters_to_reassign + to_reassign.append(orphan_mfs) + mfs_with_clusters = mfs_with_clusters.drop(index=orphan_idx) + else: + print("here") + # Check if these coll_mf_id are in a different cluster too + check_cluster = mfs_with_clusters[mfs_with_clusters['coll_mf_id'].isin(best_mf['coll_mf_id']) & (mfs_with_clusters['cluster'] != cluster)] + + # Check if there are still duplicates + cluster_summary = self.summarize_clusters(mfs_with_clusters) + clusters_with_dupes = cluster_summary[cluster_summary['sample_id_nunique'] < cluster_summary['sample_id_count']] + + + print("here") # Solution: First see if there is a different cluster that one of the mass features can be moved to (based on mz and rt) # If not, then choose the mass feature with the highest intensity as the representative mass feature and move the other mass feature to a new cluster (its own cluster) @@ -1905,7 +1993,7 @@ def add_consensus_mass_features(self): self.mass_features_dataframe = mfs_with_clusters #TODO KRH: drop consensus mass features that were not observed in previously set fraction of samples - + """ def summarize_clusters(self, features): """ Summarize the clusters of mass features by median attributes @@ -1932,31 +2020,7 @@ def summarize_clusters(self, features): return summary_df - def cluster_mass_features(self, features): - ''' - Cluster features within provided linkage tolerances. Recursively merges - the pair of clusters that minimally increases a given linkage distance. - See :class:`sklearn.cluster.AgglomerativeClustering`. - - Parameters - ---------- - features : :obj:`~pandas.DataFrame` or :obj:`~dask.dataframe.DataFrame` - Input feature coordinates and intensities per sample. - dims : str or list - Dimensions considered in clustering. - tol : float or list - Tolerance in each dimension to define maximum cluster linkage - distance. - relative : bool or list - Whether to use relative or absolute tolerances per dimension. - - Returns - ------- - features : :obj:`~pandas.DataFrame` - Features concatenated over samples with cluster labels. - - ''' - + def add_sparse_distance_matrix(self, features): if features is None: return None else: @@ -1973,12 +2037,6 @@ def cluster_mass_features(self, features): if len(dims) != len(tol) or len(dims) != len(relative) or len(dims) != len(dist_weight): raise ValueError("The dimensions, tolerances, relative, dist_weight, and na_allow lists must be the same length") - center_obj_ids = self.manifest_dataframe[self.manifest_dataframe['center']].collection_id.values - features['center_obj'] = features['sample_id'] == center_obj_ids[0] - - # First sort by center_obj and then by intensity - features = features.sort_values(by=['center_obj', 'sample_id', 'intensity'], ascending=[False, True, False]).reset_index(drop=False) - #TODO KRH: order by central sample, and then by intensity? # Make connectivity matrix for masking within sample mass features if 'sample_id' not in features.columns: @@ -2050,6 +2108,37 @@ def cluster_mass_features(self, features): # Set attribute holding distance matrix self._sparse_distance_matrix = distances + + def cluster_mass_features(self, features): + ''' + Cluster features within provided linkage tolerances. Recursively merges + the pair of clusters that minimally increases a given linkage distance. + See :class:`sklearn.cluster.AgglomerativeClustering`. + + Parameters + ---------- + features : :obj:`~pandas.DataFrame` or :obj:`~dask.dataframe.DataFrame` + Input feature coordinates and intensities per sample. + dims : str or list + Dimensions considered in clustering. + tol : float or list + Tolerance in each dimension to define maximum cluster linkage + distance. + relative : bool or list + Whether to use relative or absolute tolerances per dimension. + + Returns + ------- + features : :obj:`~pandas.DataFrame` + Features concatenated over samples with cluster labels. + + ''' + if features is None: + return None + else: + self.add_sparse_distance_matrix(features) + + distances = self._sparse_distance_matrix # Roll up features # Extract indices of within-tolerance points @@ -2111,6 +2200,135 @@ def cluster_mass_features(self, features): features_final['cluster'] = features_final['cluster'].fillna(features_final.coll_mf_id) return features_final + + def evaluate_clusters_for_repeats(self, features): + summary_df = self.summarize_clusters(features) + summary_df = summary_df.copy() + + # Arrange by decreasing median intensity + summary_df = summary_df.sort_values(by='intensity_median', ascending=False).reset_index(drop=True) + + # Find clusters that are within the mz_tol and rt_tol of each other (on the medians) + # Create a distance matrix + # Define how to calculate the distance between features + dims = ["mz_median", "scan_time_aligned_median"] + relative = [True, False] + mz_tol_relative = self.parameters.lcms_collection.consensus_mz_tol_ppm*1e-6 + tol = [mz_tol_relative, self.parameters.lcms_collection.consensus_rt_tol] + + # Compute inter-feature distances + distances = None + for i in range(len(dims)): + # Construct k-d tree + values = summary_df[dims[i]].values + tree = KDTree(values.reshape(-1, 1)) + + max_tol = tol[i] + if relative[i] is True: + # Maximum absolute tolerance + max_tol = tol[i] * values.max() + + # Compute sparse distance matrix + # the larger the max_tol, the slower this operation is + sdm = tree.sparse_distance_matrix(tree, max_tol, output_type="coo_matrix") + + # Only consider forward case, exclude diagonal + sdm = sparse.triu(sdm, k=1) + + # Filter relative distances + if relative[i] is True: + # Compute relative distances + rel_dists = sdm.data / values[sdm.row] # or col? + + # Indices of relative distances less than tolerance + idx = rel_dists <= tol[i] + + # Reconstruct sparse distance matrix + sdm = sparse.coo_matrix( + (rel_dists[idx], (sdm.row[idx], sdm.col[idx])), + shape=(len(values), len(values)), + ) + + # Cast as binary matrix + sdm.data = np.ones_like(sdm.data) + + # Stack distances + if distances is None: + distances = sdm + else: + distances = distances.multiply(sdm) + + # Roll up features + # Extract indices of within-tolerance points + distances = distances.tocoo() + pairs = np.stack((distances.row, distances.col), axis=1) # These are the index values of the clusters, not the cluster ids + # Conver to cluster ids + pairs_df = pd.DataFrame(pairs, columns=["parent", "child"]) + pairs_df["parent"] = summary_df.loc[pairs[:,0]]["cluster"].values + pairs_df["child"] = summary_df.loc[pairs[:,1]]["cluster"].values + pairs_df = pairs_df.set_index("parent") + + merge_these_clusters = [] + possible_overlaps = [] + root_parents = np.setdiff1d(np.unique(pairs_df.index.values), np.unique(pairs_df.child.values)) + for parent in root_parents: + parent_features = features[features['cluster'] == parent] + children = pairs_df.loc[[parent], "child"].tolist() + for child in children: + overlap = self.check_merge(parent_features, child, features) + if len(overlap) == 0: + merge_these_clusters.append((parent, child, len(overlap))) + else: + possible_overlaps.append((parent, child, len(overlap))) + + result_dict = {} + result_dict['merge_these_clusters'] = merge_these_clusters + result_dict['possible_overlaps'] = possible_overlaps + + return result_dict + + + def check_merge(self, parent_features, child, features): + # Grab the features of the parent and children + child_features = features[features['cluster'] == child] + + # Check if there is an overlap between mf_coll_id in the parent and child clusters + overlap = np.intersect1d(parent_features['sample_id'].values, child_features['sample_id'].values) + + return overlap + + def cluster_mass_features_agg_cluster(self, features): + if features is None: + return None + + features = features.copy() + + self.add_sparse_distance_matrix(features) + + distances = self._sparse_distance_matrix + + # Convert to full matrix + distances = distances.todense() + + # Cast all 0s to 1s for a distance matrix + distances[distances == 0] = 1 + distances = np.asarray(distances) + + # Perform clustering + try: + clustering = AgglomerativeClustering(n_clusters=None, + linkage='complete', + # using complete linkage will prevent one sample from being assigned to multiple clusters + metric='precomputed', + distance_threshold=1).fit(distances) + features['cluster'] = clustering.labels_ + + # All data points are singleton clusters + except: + features['cluster'] = np.arange(len(features.index)) + + return features + """ diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 83e483849..867418478 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1201,28 +1201,17 @@ def _convert_solvent_A(self, x): -------- float or numpy.ndarray or None The solvent A fraction at the scan time or an array of solvent A fractions at the scan times (if the function is set) + """ + if self._chromatography_df is not None: + xp = self._chromatography_df.time_min.values + yp = self._chromatography_df.A.values - Notes - ------ - This function is set by the _interpolate_chromatography_data method and is used to convert the solvent A fraction based on the scan time (in minutes) - """ - pass - - def _interpolate_chromatography_data(self): - """ - Interpolates the chromatography data for each LCMS object in the collection and defines the _convert_solvent_A function. - """ - chromatogram_df = self._chromatography_df.copy() - xp = chromatogram_df.time_min.values - yp = chromatogram_df.A.values - - # Define a function to interpolate the chromatogram data - def interp(x): + # Define a function to interpolate the chromatogram data pred_y = np.interp(x, xp, yp) return pred_y - - self._convert_solvent_A = interp - + else: + return None + def _combine_mass_features(self): """ Concatenates the mass features from all the LCMS objects in the collection. diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index a61184616..b3cddd7b0 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -775,7 +775,6 @@ def get_lcms_collection(self, load_raw = False, load_light = True) -> LCMSCollec # If chromatography file is provided, interpolate the chromatography data if self._chromatography_df is not None: - lcms_coll._interpolate_chromatography_data() # Add the chromatography data to the combined mass features dataframe combined_mf_df = lcms_coll.mass_features_dataframe combined_mf_df['fraction_A'] = lcms_coll._convert_solvent_A(combined_mf_df.scan_time.values) From 6cdb2ca917444f97661456beee7b0076b9ab9e59 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 30 Sep 2024 15:19:41 -0700 Subject: [PATCH 031/158] Clean up lc_calc module --- corems/mass_spectra/calc/lc_calc.py | 604 ++++++++++++---------------- 1 file changed, 247 insertions(+), 357 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 46ab7071a..5dc74d706 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -2,7 +2,7 @@ import pandas as pd import warnings from ripser import ripser -import scipy +import scipy from scipy import sparse from scipy.spatial import KDTree from sklearn.svm import SVR @@ -378,7 +378,9 @@ def find_mass_features(self, ms_level=1, verbose=True, grid=True): else: raise ValueError("Peak picking method not implemented") - def integrate_mass_features(self, drop_if_fail=True, drop_duplicates=True, ms_level=1): + def integrate_mass_features( + self, drop_if_fail=True, drop_duplicates=True, ms_level=1 + ): """Integrate mass features and extract EICs. Populates the _eics attribute on the LCMSBase object for each unique mz in the mass_features dataframe and adds data (start_scan, final_scan, area) to the mass_features attribute. @@ -389,7 +391,7 @@ def integrate_mass_features(self, drop_if_fail=True, drop_duplicates=True, ms_le Whether to drop mass features if the EIC limit calculations fail. Default is True. drop_duplicates : bool, optional - Whether to mass features that appear to be duplicates + Whether to mass features that appear to be duplicates (i.e., mz is similar to another mass feature and limits of the EIC are similar or encapsulating). Default is True. ms_level : int, optional @@ -504,7 +506,7 @@ def integrate_mass_features(self, drop_if_fail=True, drop_duplicates=True, ms_le else: if drop_if_fail is True: self.mass_features.pop(idx) - + if drop_duplicates: # Prepare mass feature dataframe mf_df = self.mass_features_to_df().copy() @@ -516,17 +518,22 @@ def integrate_mass_features(self, drop_if_fail=True, drop_duplicates=True, ms_le apex_scan = mass_feature.apex_scan mf_df["mz_diff_ppm"] = np.abs(mf_df["mz"] - mz) / mz * 10**6 - mf_df_sub = mf_df[mf_df["mz_diff_ppm"] < self.parameters.lc_ms.mass_feature_cluster_mz_tolerance_rel * 10**6].copy() + mf_df_sub = mf_df[ + mf_df["mz_diff_ppm"] + < self.parameters.lc_ms.mass_feature_cluster_mz_tolerance_rel + * 10**6 + ].copy() # For all mass features within the clustering tolerance, check if the start and end times are within the start and end times of the mass feature for idx2, mass_feature2 in mf_df_sub.iterrows(): if idx2 != idx: - if mass_feature2.start_scan >= mass_feature.start_scan and mass_feature2.final_scan <= mass_feature.final_scan: + if ( + mass_feature2.start_scan >= mass_feature.start_scan + and mass_feature2.final_scan <= mass_feature.final_scan + ): if idx2 in self.mass_features.keys(): self.mass_features.pop(idx2) - - def find_c13_mass_features(self, verbose=True): """Mark likely C13 isotopes and connect to monoisoitopic mass features. @@ -722,7 +729,6 @@ def deconvolute_ms1_mass_features(self): # Loop through each mass feature for mf_id, mass_feature in self.mass_features.items(): - # Check that the mass_feature.mz attribute == the mz of the mass feature in the mass_feature_df if mass_feature.mz != mass_feature.ms1_peak.mz_exp: continue @@ -1406,14 +1412,14 @@ def cluster_mass_features( to_drop = [] while not pairs_df.empty: # Find root_parents and their children - root_parents = np.setdiff1d(np.unique(pairs_df.index.values), np.unique(pairs_df.child.values)) + root_parents = np.setdiff1d( + np.unique(pairs_df.index.values), np.unique(pairs_df.child.values) + ) children_of_roots = pairs_df.loc[root_parents, "child"].unique() to_drop = np.append(to_drop, children_of_roots) # Remove root_children as possible parents from pairs_df for next iteration - pairs_df = pairs_df.drop( - index=children_of_roots, errors="ignore" - ) + pairs_df = pairs_df.drop(index=children_of_roots, errors="ignore") pairs_df = pairs_df.reset_index().set_index("child") # Remove root_children as possible children from pairs_df for next iteration pairs_df = pairs_df.drop(index=children_of_roots) @@ -1445,7 +1451,8 @@ def cluster_mass_features( } else: return cluster_daughters - + + class LCMSCollectionCalculations: """Methods for performing calculations related to LCMSCollection objects. @@ -1453,6 +1460,7 @@ class LCMSCollectionCalculations: ----- This class is intended as a mixin for the LCMSCollection class. """ + def clean_sparse_matrix(self, sparse_matrix): """Clean a sparse matrix by removing duplicates and sorting. @@ -1471,7 +1479,7 @@ def clean_sparse_matrix(self, sparse_matrix): sparse_matrix.sort() dereplicated_sparse_matrix = np.unique(sparse_matrix, axis=0) return dereplicated_sparse_matrix - + def match_mfs(self, mf_c, mf_i): """Match mass features between two LCMS objects. @@ -1499,16 +1507,16 @@ def match_mfs(self, mf_c, mf_i): # Prepare dataframes mf_c = mf_c.copy() - mf_c['id_i'] = 0 + mf_c["id_i"] = 0 mf_i = mf_i.copy() - mf_i['id_i'] = 1 + mf_i["id_i"] = 1 # Set dimensions for matching - dims = ["mz", "scan_time"] - relative = [True, False] - mz_tol = self.parameters.lcms_collection.alignment_mz_tol_ppm * 1E-6 + dims = ["mz", "scan_time"] + relative = [True, False] + mz_tol = self.parameters.lcms_collection.alignment_mz_tol_ppm * 1e-6 rt_tol = self.parameters.lcms_collection.alignment_rt_tol - tol = [mz_tol, rt_tol] + tol = [mz_tol, rt_tol] # Compute inter-feature distances idx = [] @@ -1538,11 +1546,11 @@ def match_mfs(self, mf_c, mf_i): # Compute normalized 3d distance v1 = mf_c[dims].values / tol v2 = mf_i[dims].values / tol - dist3d = scipy.spatial.distance.cdist(v1, v2, 'cityblock') + dist3d = scipy.spatial.distance.cdist(v1, v2, "cityblock") dist3d = np.multiply(dist3d, idx) # Normalize to 0-1 - mx = dist3d.max() + mx = dist3d.max() if mx > 0: # Lower distance is better dist3d = dist3d / dist3d.max() @@ -1570,8 +1578,8 @@ def match_mfs(self, mf_c, mf_i): return None, None return mf_c, mf_i - - def fit_rts(self, a, b, align='scan_time', **kwargs): + + def fit_rts(self, a, b, align="scan_time", **kwargs): """ Fit a support vector regressor to matched features. @@ -1606,16 +1614,16 @@ def fit_rts(self, a, b, align='scan_time', **kwargs): arr = np.unique(arr, axis=0) # Check kwargs - if 'kernel' in kwargs: - kernel = kwargs.get('kernel') + if "kernel" in kwargs: + kernel = kwargs.get("kernel") else: - kernel = 'linear' + kernel = "linear" # Construct interpolation axis newx = np.linspace(arr[:, 0].min(), arr[:, 0].max(), 1000) # Linear kernel - if kernel == 'linear': + if kernel == "linear": reg = scipy.stats.linregress(x, y) newy = reg.slope * newx + reg.intercept @@ -1627,13 +1635,13 @@ def fit_rts(self, a, b, align='scan_time', **kwargs): # Predict newy = svr.predict(newx.reshape(-1, 1)) - + # Pad x and y_pred with zeros to force interpolation to start at 0 newx = np.concatenate(([0], newx)) newy = np.concatenate(([0], newy)) # Pad x and y_pred with max time to force interpolation to end at max time to force interpolation to match at end max time - max_time = self[0].scan_df['scan_time'].max() + max_time = self[0].scan_df["scan_time"].max() newx = np.concatenate((newx, [max_time])) newy = np.concatenate((newy, [max_time])) @@ -1641,9 +1649,9 @@ def fit_rts(self, a, b, align='scan_time', **kwargs): def interp(x): pred_y = np.interp(x, newx, newy) return pred_y - + return interp - + def get_anchor_mass_features(self, mf_df): """ Get the anchor mass features from a DataFrame of mass features. @@ -1660,12 +1668,18 @@ def get_anchor_mass_features(self, mf_df): """ mf_df = mf_df.copy() - if "deconvoluted_mass_spectra" in self.parameters.lcms_collection.mass_feature_anchor_technique: + if ( + "deconvoluted_mass_spectra" + in self.parameters.lcms_collection.mass_feature_anchor_technique + ): # Drop features that are not mass_spectrum_deconvoluted_parent or are NA as mass_spectrum_deconvoluted_parent mf_df = mf_df.dropna(subset=["mass_spectrum_deconvoluted_parent"]) mf_df = mf_df[mf_df["mass_spectrum_deconvoluted_parent"]] - if "absolute_intensity" in self.parameters.lcms_collection.mass_feature_anchor_technique: + if ( + "absolute_intensity" + in self.parameters.lcms_collection.mass_feature_anchor_technique + ): # Drop features that have an absolute_intensity lower than the threshold threshold = self.parameters.lcms_collection.mass_feature_anchor_aboslute_intensity_threshold mf_df = mf_df[mf_df["absolute_intensity"] > threshold] @@ -1676,59 +1690,74 @@ def attempt_alignment(self, matches_c, matches_i): """ Check if alignment is needed for the LCMS objects in the collection. """ - + # Hold out a subset of matches_c and matches_i for spline fitting matches_c.reset_index(drop=False, inplace=True) matches_i.reset_index(drop=False, inplace=True) # Rearrange matches_c and matches_i to be in the order of the scan_time of matches_c - matches_c = matches_c.sort_values(by='scan_time') + matches_c = matches_c.sort_values(by="scan_time") matches_i = matches_i.iloc[matches_c.index.values] hold_out_fraction = self.parameters.lcms_collection.alignment_hold_out_fraction # starting with an array of length len(matches_c), select equally spaced indices to hold out - idx_holdout = matches_c.index.values[np.arange(0, len(matches_c), int(1/hold_out_fraction))] + idx_holdout = matches_c.index.values[ + np.arange(0, len(matches_c), int(1 / hold_out_fraction)) + ] matches_c_holdout = matches_c.loc[idx_holdout].copy() matches_i_holdout = matches_i.loc[idx_holdout].copy() # Remove the holdout matches from the matches_c and matches_i DataFrames and reset the index - matches_c = matches_c.drop(index=idx_holdout).set_index('sample_name') - matches_i = matches_i.drop(index=idx_holdout).set_index('sample_name') + matches_c = matches_c.drop(index=idx_holdout).set_index("sample_name") + matches_i = matches_i.drop(index=idx_holdout).set_index("sample_name") # Reset the scan_time to the original scan_time matches_i = matches_i.copy() - matches_i['scan_time'] = matches_i['scan_time_og'] + matches_i["scan_time"] = matches_i["scan_time_og"] # Fit the retention times of the LCMS object to the center LCMS object using the matched mass features - spl = self.fit_rts(matches_c, matches_i, kernel='rbf', C=1000) + spl = self.fit_rts(matches_c, matches_i, kernel="rbf", C=1000) # Check if the spline fitting improved the alignment for the holdout matches - matches_i_holdout['scan_time_fit'] = spl(matches_i_holdout['scan_time']) - og_diff = np.abs(matches_i_holdout['scan_time'] - matches_c_holdout['scan_time']) - fit_diff = np.abs(matches_i_holdout['scan_time_fit'] - matches_c_holdout['scan_time']) + matches_i_holdout["scan_time_fit"] = spl(matches_i_holdout["scan_time"]) + og_diff = np.abs( + matches_i_holdout["scan_time"] - matches_c_holdout["scan_time"] + ) + fit_diff = np.abs( + matches_i_holdout["scan_time_fit"] - matches_c_holdout["scan_time"] + ) - if "fraction_improved" in self.parameters.lcms_collection.alignment_acceptance_techinque: + if ( + "fraction_improved" + in self.parameters.lcms_collection.alignment_acceptance_techinque + ): fraction_improved = np.sum(fit_diff < og_diff) / len(og_diff) - use_spline_alignment = fraction_improved > self.parameters.lcms_collection.alignment_acceptance_fraction_improved_threshold - if "mean_squared_error_improved" in self.parameters.lcms_collection.alignment_acceptance_techinque: + use_spline_alignment = ( + fraction_improved + > self.parameters.lcms_collection.alignment_acceptance_fraction_improved_threshold + ) + if ( + "mean_squared_error_improved" + in self.parameters.lcms_collection.alignment_acceptance_techinque + ): mse_og = np.mean(og_diff**2) mse = np.mean(fit_diff**2) use_spline_alignment = mse < mse_og return use_spline_alignment, spl - + def align_lcms_objects(self, overwrite=False): """ Align LCMS objects in the collection. - + Aligns the LCMS objects in the collection by aligning the retention times of the mass features in the LCMS objects. First, the mass features in the center LCMS object are matched to the mass features in the other LCMS objects, starting with the LCMS object immediately following the center LCMS object. The retention times of the LCMS objects are then fit to the center LCMS object using the matched mass features. Currently, this function only aligns LCMS objects within each batch, but not between batches. - + Returns ------- None, but aligns the LCMS objects in the collection and sets the scan_time_aligned column in the scan_df attribute of each LCMS object. @@ -1738,17 +1767,18 @@ def align_lcms_objects(self, overwrite=False): This function has been adapted from the original implementation in the Deimos package: https://github.com/pnnl/deimos """ - #TODO KRH: add the ability to do this by batch and then connect the batches # Prepare the center LCMS object - center_obj_ids = self.manifest_dataframe[self.manifest_dataframe['center']].collection_id.values + center_obj_ids = self.manifest_dataframe[ + self.manifest_dataframe["center"] + ].collection_id.values full_mf_df = self.mass_features_dataframe # re-index to sample_name for faster lookups - full_mf_df = full_mf_df.reset_index().set_index('sample_name') + full_mf_df = full_mf_df.reset_index().set_index("sample_name") + + if "scan_time_aligned" in full_mf_df.columns and not overwrite: + raise ValueError("Mass features have already been aligned") - if 'scan_time_aligned' in full_mf_df.columns and not overwrite: - raise ValueError('Mass features have already been aligned') - anchor_mf_dfs = [] for center_obj_id in center_obj_ids: # Get the anchor mass features from the center LCMS object @@ -1758,7 +1788,7 @@ def align_lcms_objects(self, overwrite=False): # Set scan_time_aligned to scan_time for the center LCMS object center_scan_df = self[center_obj_id].scan_df.copy() - center_scan_df['scan_time_aligned'] = center_scan_df['scan_time'] + center_scan_df["scan_time_aligned"] = center_scan_df["scan_time"] self[center_obj_id].scan_df = center_scan_df index_steps = (1, -1) @@ -1769,32 +1799,38 @@ def align_lcms_objects(self, overwrite=False): if i < len(self) and i >= 0: # Grab the first LCMS object after the center object mf_df_i = full_mf_df.loc[self.samples[i]].copy() - mf_df_i['scan_time_og'] = mf_df_i['scan_time'] + mf_df_i["scan_time_og"] = mf_df_i["scan_time"] while mf_df_i is not None: mf_df_i = self.get_anchor_mass_features(mf_df_i) # Match the mass features in the LCMS object to the anchor mass features in the center LCMS object. - matches_c, matches_i = self.match_mfs(mf_df_c, mf_df_i) + matches_c, matches_i = self.match_mfs(mf_df_c, mf_df_i) if matches_c is not None: - use_spline_alignment, spl = self.attempt_alignment(matches_c, matches_i) - + use_spline_alignment, spl = self.attempt_alignment( + matches_c, matches_i + ) + # Record if we used alignment for this sample sample_name = self.samples[i] - self._manifest_dict[sample_name]['use_rt_alignment'] = use_spline_alignment + self._manifest_dict[sample_name]["use_rt_alignment"] = ( + use_spline_alignment + ) if use_spline_alignment: # Set new retention times on scan_df for lc_obj using the spline fitting - matches_i['scan_time_fit'] = spl(matches_i['scan_time']) - new_times = spl(self[i].scan_df['scan_time']) + matches_i["scan_time_fit"] = spl(matches_i["scan_time"]) + new_times = spl(self[i].scan_df["scan_time"]) new_scan_info = self[i].scan_df.copy() - new_scan_info['scan_time_aligned'] = new_times + new_scan_info["scan_time_aligned"] = new_times self[i].scan_df = new_scan_info else: # Set aligned retention times on scan_df for lc_obj using the original retention times new_scan_info = self[i].scan_df.copy() - new_scan_info['scan_time_aligned'] = new_scan_info['scan_time'] + new_scan_info["scan_time_aligned"] = new_scan_info[ + "scan_time" + ] self[i].scan_df = new_scan_info i += index_step @@ -1803,62 +1839,72 @@ def align_lcms_objects(self, overwrite=False): else: # Grab the next LCMS object and use the previous spline fitting to get a better starting point mf_df_i = full_mf_df.loc[self.samples[i]].copy() - mf_df_i['scan_time_og'] = mf_df_i['scan_time'] + mf_df_i["scan_time_og"] = mf_df_i["scan_time"] mf_df_i = mf_df_i.reset_index(drop=False) if use_spline_alignment: # Set scan_time to previous sample's predicted scan_time to find closer matches - mf_df_i['scan_time'] = spl(mf_df_i['scan_time']) + mf_df_i["scan_time"] = spl(mf_df_i["scan_time"]) else: - raise ValueError(f'No matches found between the center object and {self.samples[i]}') + raise ValueError( + f"No matches found between the center object and {self.samples[i]}" + ) # Now align each batch using the center objects as anchors with the other batches mf_df_c = anchor_mf_dfs[0] for i in center_obj_ids[1:]: mf_df_i = full_mf_df.loc[self.samples[i]].copy() - mf_df_i['scan_time_og'] = mf_df_i['scan_time'] + mf_df_i["scan_time_og"] = mf_df_i["scan_time"] mf_df_i = self.get_anchor_mass_features(mf_df_i) - + matches_c, matches_i = self.match_mfs(mf_df_c, mf_df_i) if matches_c is not None: use_spline_alignment, spl = self.attempt_alignment(matches_c, matches_i) # Record if we used alignment for this sample sample_name = self.samples[i] - self._manifest_dict[sample_name]['use_rt_alignment'] = use_spline_alignment + self._manifest_dict[sample_name]["use_rt_alignment"] = ( + use_spline_alignment + ) if use_spline_alignment: - # Set new retention times on all this object's - new_times = spl(self[i].scan_df['scan_time']) + # Set new retention times on all this object's + new_times = spl(self[i].scan_df["scan_time"]) new_scan_info = self[i].scan_df.copy() - new_scan_info['scan_time_aligned'] = new_times + new_scan_info["scan_time_aligned"] = new_times self[i].scan_df = new_scan_info # Get the batch that this object belongs to - batch = self.manifest[self.samples[i]]['batch'] + batch = self.manifest[self.samples[i]]["batch"] for j in range(len(self)): - if self.manifest[self.samples[j]]['batch'] == batch: + if self.manifest[self.samples[j]]["batch"] == batch: if j != i: sample_name = self.samples[j] - self._manifest_dict[sample_name]['use_rt_alignment'] = use_spline_alignment + self._manifest_dict[sample_name]["use_rt_alignment"] = ( + use_spline_alignment + ) new_scan_info = self[j].scan_df.copy() - new_scan_info['scan_time_aligned'] = spl(self[j].scan_df['scan_time_aligned']) + new_scan_info["scan_time_aligned"] = spl( + self[j].scan_df["scan_time_aligned"] + ) self[j].scan_df = new_scan_info - + # Set final mass_features_dataframe with the aligned scan_time center_sample_name = self.samples[center_obj_ids[0]] - self._manifest_dict[center_sample_name]['use_rt_alignment'] = False + self._manifest_dict[center_sample_name]["use_rt_alignment"] = False new_scan_info = self[center_obj_ids[0]].scan_df.copy() - new_scan_info['scan_time_aligned'] = new_scan_info['scan_time'] + new_scan_info["scan_time_aligned"] = new_scan_info["scan_time"] def add_consensus_mass_features(self): # Get the combined mass features from all LCMS objects, keep the original index as a separate column combined_mfs = self.mass_features_dataframe.copy() - combined_mfs['coll_mf_id'] = combined_mfs.index + combined_mfs["coll_mf_id"] = combined_mfs.index # Check if the mass features have been aligned - if 'scan_time_aligned' not in combined_mfs.columns: - raise ValueError('Mass features have not been aligned, run align_lcms_objects() first') + if "scan_time_aligned" not in combined_mfs.columns: + raise ValueError( + "Mass features have not been aligned, run align_lcms_objects() first" + ) # Partition the mass features by mz so we can parallelize the matching before clustering from corems.chroma_peak.calc import subset as corems_subset @@ -1866,134 +1912,74 @@ def add_consensus_mass_features(self): # get max mz from combined_mfs and calculate tolerance from ppm mz_tol = self.parameters.lcms_collection.consensus_mz_tol_ppm * 1e-6 n_partition_size = self.parameters.lcms_collection.consensus_partition_size - lazy_partitions = corems_subset.multi_sample_partition(combined_mfs, split_on = 'mz', size=n_partition_size, tol=mz_tol, relative=True) + lazy_partitions = corems_subset.multi_sample_partition( + combined_mfs, + split_on="mz", + size=n_partition_size, + tol=mz_tol, + relative=True, + ) # If any of lazy_partitions._counts is 2xn_partition_size, issue a warning - if np.array(lazy_partitions._counts).max() > 2*n_partition_size: - warnings.warn('Some partitions are larger than 2x the goal partition size. Consider increasing the partition or decreasing the mz_tol.') + if np.array(lazy_partitions._counts).max() > 2 * n_partition_size: + warnings.warn( + "Some partitions are larger than 2x the goal partition size. Consider increasing the partition or decreasing the mz_tol." + ) # Cluster the mass features within each partition if self.parameters.lcms_collection.cores > lazy_partitions.n_partitions: cores_to_use = lazy_partitions.n_partitions else: cores_to_use = self.parameters.lcms_collection.cores - #mfs_with_clusters = lazy_partitions.map(self.cluster_mass_features, processes=cores_to_use) - mfs_with_clusters = lazy_partitions.map(self.cluster_mass_features_agg_cluster, processes=cores_to_use) + # mfs_with_clusters = lazy_partitions.map(self.cluster_mass_features, processes=cores_to_use) + mfs_with_clusters = lazy_partitions.map( + self.cluster_mass_features_agg_cluster, processes=cores_to_use + ) # Clean up cluster id names after partitioning - new_cluster_ids = mfs_with_clusters[['cluster', 'partition_idx']].drop_duplicates().reset_index(drop=True) - new_cluster_ids['cluster_unqiue'] = new_cluster_ids.index - mfs_with_clusters = mfs_with_clusters.merge(new_cluster_ids, on=['cluster', 'partition_idx']) - mfs_with_clusters['cluster'] = mfs_with_clusters['cluster_unqiue'] - mfs_with_clusters = mfs_with_clusters.drop(columns=['cluster_unqiue']) + new_cluster_ids = ( + mfs_with_clusters[["cluster", "partition_idx"]] + .drop_duplicates() + .reset_index(drop=True) + ) + new_cluster_ids["cluster_unqiue"] = new_cluster_ids.index + mfs_with_clusters = mfs_with_clusters.merge( + new_cluster_ids, on=["cluster", "partition_idx"] + ) + mfs_with_clusters["cluster"] = mfs_with_clusters["cluster_unqiue"] + mfs_with_clusters = mfs_with_clusters.drop(columns=["cluster_unqiue"]) # Embed a new cluster id into the mass features dataframe and set as index - mfs_with_clusters['idx'] = mfs_with_clusters.index + mfs_with_clusters["idx"] = mfs_with_clusters.index # Check if any clusters can be merged into a single cluster eval_dict = self.evaluate_clusters_for_repeats(mfs_with_clusters) # Merge clusters identified in eval_dict - while len(eval_dict['merge_these_clusters']) > 0: - list_of_clusters_to_merge = [[x[0], x[1]] for x in eval_dict['merge_these_clusters']] + while len(eval_dict["merge_these_clusters"]) > 0: + list_of_clusters_to_merge = [ + [x[0], x[1]] for x in eval_dict["merge_these_clusters"] + ] # Convert to a dataframe with columns "new_cluster" and "cluster" - df = pd.DataFrame(np.array(list_of_clusters_to_merge), columns=["new_cluster", "cluster"]) + df = pd.DataFrame( + np.array(list_of_clusters_to_merge), columns=["new_cluster", "cluster"] + ) # Drop duplicates of "child" clusters df = df.drop_duplicates("cluster", keep="first") df = df.drop_duplicates("new_cluster", keep="first") mfs_with_clusters = mfs_with_clusters.merge(df, on="cluster", how="left") - mfs_with_clusters["cluster"] = mfs_with_clusters["new_cluster"].fillna(mfs_with_clusters["cluster"]) + mfs_with_clusters["cluster"] = mfs_with_clusters["new_cluster"].fillna( + mfs_with_clusters["cluster"] + ) mfs_with_clusters = mfs_with_clusters.drop(columns=["new_cluster"]) # Re-evaluate clusters for repeats eval_dict = self.evaluate_clusters_for_repeats(mfs_with_clusters) - #TODO KRH: Deal with isomers better? Pool them together and then split them out using samples with 2 as the template? + # TODO KRH: Deal with isomers better? Pool them together and then split them out using samples with 2 as the template? self.mass_features_dataframe = mfs_with_clusters - """ - # Deal with duplicates - to_reassign = [] - - # Find the overlap of clusters that have multiple mass features and mass features that are in multiple clusters - mfs_with_clusters["mf_in_dupe_cluster"] = mfs_with_clusters.duplicated(subset='coll_mf_id', keep=False) - - cluster_summary = self.summarize_clusters(mfs_with_clusters) - clusters_with_dupes = cluster_summary[cluster_summary['sample_id_nunique'] < cluster_summary['sample_id_count']] - - mfs_with_clusters["cluster_with_dupe_mf"] = mfs_with_clusters['cluster'].isin(clusters_with_dupes['cluster']) - - # Scenario 1: One mass feature is assigned to multiple clusters - # Solution, choose the cluster with the closest median rt to the mass feature and assign the mass feature to that cluster - if len(clusters_with_dupes) > 0: - mfs_with_clusters_dupes = mfs_with_clusters[mfs_with_clusters['mf_in_dupe_cluster']] - for mf in mfs_with_clusters_dupes['coll_mf_id'].values: - # Pull out the cluster it is assigned to - distances = [] - - - # Scenario 1: Two mass features from a single sample are assigned into a single cluster and neither is in a different cluster - # Choose one to stay in the cluster and the other to marked for reassignment - if len(clusters_with_dupes) > 0: - for cluster in clusters_with_dupes['cluster'].values: - cluster_summary_sub = cluster_summary[cluster_summary['cluster'] == cluster] - sub_df = mfs_with_clusters[mfs_with_clusters['cluster'] == cluster].copy() - # Mark if sample_id is duplicated - sub_df['sample_id_duplicated'] = sub_df.duplicated(subset='sample_id', keep=False) - # Get the mass features that are duplicated in this cluster and are not in any other cluster - #TODO KRH: This is still pulling mass features that are in other clusters (7_0 is the example) - duplicated_sample_ids = sub_df[sub_df['sample_id_duplicated'] & ~sub_df['mf_in_dupe_cluster']]['sample_id'].unique() - for sample_id in duplicated_sample_ids: - # Print length of mfs_with_clusters - print("Length of mfs_with_clusters: ", len(mfs_with_clusters)) - # Pull mass features - sample_mfs = sub_df[sub_df['sample_id'] == sample_id][['idx', 'coll_mf_id', 'mz', 'scan_time_aligned', 'intensity', 'persistence', 'mass_spectrum_deconvoluted_parent']] - # Compare mz to the median mz of the cluster - sample_mfs['ppm_diff'] = np.abs(sample_mfs['mz'] - cluster_summary_sub['mz_median'].values[0])/cluster_summary_sub['mz_median'].values[0]*1e6 - sample_mfs['ppm_diff_rank'] = sample_mfs['ppm_diff'].rank() - sample_mfs['time_diff'] = np.abs(sample_mfs['scan_time_aligned'] - cluster_summary_sub['scan_time_aligned_median'].values[0]) - sample_mfs['time_diff_rank'] = sample_mfs['time_diff'].rank() - - # Check if there is a clear winner (lowest ppm_diff_rank and time_diff_rank) - sample_mfs['rank_sum'] = sample_mfs['ppm_diff_rank'] + sample_mfs['time_diff_rank'] - best_mf = sample_mfs[sample_mfs['rank_sum'] == sample_mfs['rank_sum'].min()] - if len(best_mf) == 1: - orphan_idx = sample_mfs[sample_mfs['rank_sum'] != sample_mfs['rank_sum'].min()]['idx'].values - orphan_mfs = mfs_with_clusters.loc[orphan_idx] - # Drop orphan_mf records from mfs_with_clusters but add them to clusters_to_reassign - to_reassign.append(orphan_mfs) - mfs_with_clusters = mfs_with_clusters.drop(index=orphan_idx) - else: - print("here") - # Check if these coll_mf_id are in a different cluster too - check_cluster = mfs_with_clusters[mfs_with_clusters['coll_mf_id'].isin(best_mf['coll_mf_id']) & (mfs_with_clusters['cluster'] != cluster)] - - # Check if there are still duplicates - cluster_summary = self.summarize_clusters(mfs_with_clusters) - clusters_with_dupes = cluster_summary[cluster_summary['sample_id_nunique'] < cluster_summary['sample_id_count']] - - - print("here") - # Solution: First see if there is a different cluster that one of the mass features can be moved to (based on mz and rt) - # If not, then choose the mass feature with the highest intensity as the representative mass feature and move the other mass feature to a new cluster (its own cluster) - - # Scenario 2: A single mass feature belongs to multiple clusters - # Solution: Choose the cluster with the closest median rt to the mass feature and assign the mass feature to that cluster - - - # Warn if any mass features are in multiple clusters - if mfs_with_clusters.duplicated(subset='coll_mf_id', keep=False).any(): - - warnings.warn('Some mass features are in multiple clusters! This may indicate that the mz_tol or rt_tol is too large.') - - # Set the index back to coll_mf_id - mfs_with_clusters = mfs_with_clusters.set_index('coll_mf_id') - - self.mass_features_dataframe = mfs_with_clusters - - #TODO KRH: drop consensus mass features that were not observed in previously set fraction of samples - """ def summarize_clusters(self, features): """ Summarize the clusters of mass features by median attributes @@ -2002,24 +1988,34 @@ def summarize_clusters(self, features): if len(features.columns) < 1: return None - summary_df = features.groupby('cluster').agg({ - 'mz': 'median', - 'scan_time_aligned': 'median', - 'half_height_width': 'median', - 'tailing_factor': 'median', - 'dispersity_index': 'median', - 'sample_id': ['nunique', 'count'], - 'intensity': ['max', 'median'], - 'persistence': ['max', 'median'] - }).reset_index() + summary_df = ( + features.groupby("cluster") + .agg( + { + "mz": "median", + "scan_time_aligned": "median", + "half_height_width": "median", + "tailing_factor": "median", + "dispersity_index": "median", + "sample_id": ["nunique", "count"], + "intensity": ["max", "median"], + "persistence": ["max", "median"], + } + ) + .reset_index() + ) # Fix the column names - summary_df.columns = ['_'.join(col).strip() for col in summary_df.columns.values if col != 'cluster'] - summary_df = summary_df.rename(columns={'cluster_': 'cluster'}) + summary_df.columns = [ + "_".join(col).strip() + for col in summary_df.columns.values + if col != "cluster" + ] + summary_df = summary_df.rename(columns={"cluster_": "cluster"}) summary_df = summary_df.reset_index(drop=True) return summary_df - + def add_sparse_distance_matrix(self, features): if features is None: return None @@ -2027,22 +2023,27 @@ def add_sparse_distance_matrix(self, features): features = features.copy() # Define how to calculate the distance between features - dims = ["mz", "scan_time_aligned"] - relative = [True, False] - mz_tol_relative = self.parameters.lcms_collection.consensus_mz_tol_ppm*1e-6 - tol = [mz_tol_relative, self.parameters.lcms_collection.consensus_rt_tol] - dist_weight = [1, 1] + dims = ["mz", "scan_time_aligned"] + relative = [True, False] + mz_tol_relative = self.parameters.lcms_collection.consensus_mz_tol_ppm * 1e-6 + tol = [mz_tol_relative, self.parameters.lcms_collection.consensus_rt_tol] + dist_weight = [1, 1] # Check that the dimensions and tolerances are the same length - if len(dims) != len(tol) or len(dims) != len(relative) or len(dims) != len(dist_weight): - raise ValueError("The dimensions, tolerances, relative, dist_weight, and na_allow lists must be the same length") - + if ( + len(dims) != len(tol) + or len(dims) != len(relative) + or len(dims) != len(dist_weight) + ): + raise ValueError( + "The dimensions, tolerances, relative, dist_weight, and na_allow lists must be the same length" + ) # Make connectivity matrix for masking within sample mass features - if 'sample_id' not in features.columns: + if "sample_id" not in features.columns: cmat = None else: - vals = features['sample_id'].values.reshape(-1, 1) + vals = features["sample_id"].values.reshape(-1, 1) cmat = scipy.spatial.distance.cdist(vals, vals) # Convert to binary (0 if same sample, 1 if different) cmat = np.where(cmat == 0, 0, 1) @@ -2085,14 +2086,14 @@ def add_sparse_distance_matrix(self, features): # Stack distances for dimensions where na_allow is False if distances is None: - sdm.data = sdm.data*dist_weight[i] + sdm.data = sdm.data * dist_weight[i] distances = sdm else: # Prepare sdm to match shape of existing distances distances_truth = distances.copy() distances_truth.data = np.ones_like(distances_truth.data) sdm = distances_truth.multiply(sdm) - sdm.data = sdm.data*dist_weight[i] + sdm.data = sdm.data * dist_weight[i] sdm_truth = sdm.copy() sdm_truth.data = np.ones_like(sdm_truth.data) @@ -2108,113 +2109,23 @@ def add_sparse_distance_matrix(self, features): # Set attribute holding distance matrix self._sparse_distance_matrix = distances - - def cluster_mass_features(self, features): - ''' - Cluster features within provided linkage tolerances. Recursively merges - the pair of clusters that minimally increases a given linkage distance. - See :class:`sklearn.cluster.AgglomerativeClustering`. - Parameters - ---------- - features : :obj:`~pandas.DataFrame` or :obj:`~dask.dataframe.DataFrame` - Input feature coordinates and intensities per sample. - dims : str or list - Dimensions considered in clustering. - tol : float or list - Tolerance in each dimension to define maximum cluster linkage - distance. - relative : bool or list - Whether to use relative or absolute tolerances per dimension. - - Returns - ------- - features : :obj:`~pandas.DataFrame` - Features concatenated over samples with cluster labels. - - ''' - if features is None: - return None - else: - self.add_sparse_distance_matrix(features) - - distances = self._sparse_distance_matrix - - # Roll up features - # Extract indices of within-tolerance points - distances = distances.tocoo() - pairs = np.stack((distances.row, distances.col), axis=1) - pairs_df = pd.DataFrame(pairs, columns=["parent", "child"]) - pairs_df = pairs_df.set_index("parent") - - to_drop = [] - final_pairs_df = [] - while not pairs_df.empty: - # Find root_parents and their children - root_parents = np.setdiff1d(np.unique(pairs_df.index.values), np.unique(pairs_df.child.values)) - # Check if there are any repeat children of roots - children_of_roots = pairs_df.loc[root_parents, "child"].unique() - to_drop = np.append(to_drop, children_of_roots) - - # Add the root_parents and children_of_roots to the final_pairs_df - df_to_add = pairs_df.loc[root_parents] - #df_to_add = df_to_add.drop_duplicates(subset='child', keep='first') - final_pairs_df.append(df_to_add) - - # Remove root_children as possible parents from pairs_df for next iteration - pairs_df = pairs_df.drop( - index=children_of_roots, errors="ignore" - ) - pairs_df = pairs_df.reset_index().set_index("child") - # Remove root_children as possible children from pairs_df for next iteration - pairs_df = pairs_df.drop(index=children_of_roots) - - # Prepare for next iteration - pairs_df = pairs_df.reset_index().set_index("parent") - - # Clean up the final_pairs_df - final_pairs_df = pd.concat(final_pairs_df) - - ## For each parent, add itself as a child to final_pairs_df - add_df = pd.DataFrame(np.stack((np.unique(final_pairs_df.index), np.unique(final_pairs_df.index)), axis=1), columns=["parent", "child"]) - add_df = add_df.set_index("parent") - final_pairs_df = pd.concat([final_pairs_df, add_df]) - final_pairs_df = final_pairs_df.sort_index() - - # Make a dataframe with unique parents (index of final_pairs_df) and cluster id - cluster_df = pd.DataFrame(np.arange(len(np.unique(final_pairs_df.index))), index=np.unique(final_pairs_df.index), columns=["cluster"]) - cluster_df.index.name = "parent" - - # Add the cluster id to the final_pairs_df - mf_cluster_df = final_pairs_df.merge(cluster_df, left_index=True, right_index=True).reset_index(drop=True) - # reset the index so it's the child and take off the name - mf_cluster_df = mf_cluster_df.set_index("child") - - # Add the cluster id to the features - features_final = features.merge(mf_cluster_df, left_index=True, right_index=True, how="left") - - # Add a tag if the features is the parent - features_final['is_largest'] = np.where(features_final.index.isin(np.unique(final_pairs_df.index)), True, False) - - # If a feature is not part of a cluster, assign it to its own cluster - features_final['cluster'] = features_final['cluster'].fillna(features_final.coll_mf_id) - - return features_final - def evaluate_clusters_for_repeats(self, features): summary_df = self.summarize_clusters(features) summary_df = summary_df.copy() # Arrange by decreasing median intensity - summary_df = summary_df.sort_values(by='intensity_median', ascending=False).reset_index(drop=True) + summary_df = summary_df.sort_values( + by="intensity_median", ascending=False + ).reset_index(drop=True) # Find clusters that are within the mz_tol and rt_tol of each other (on the medians) # Create a distance matrix # Define how to calculate the distance between features - dims = ["mz_median", "scan_time_aligned_median"] - relative = [True, False] - mz_tol_relative = self.parameters.lcms_collection.consensus_mz_tol_ppm*1e-6 - tol = [mz_tol_relative, self.parameters.lcms_collection.consensus_rt_tol] + dims = ["mz_median", "scan_time_aligned_median"] + relative = [True, False] + mz_tol_relative = self.parameters.lcms_collection.consensus_mz_tol_ppm * 1e-6 + tol = [mz_tol_relative, self.parameters.lcms_collection.consensus_rt_tol] # Compute inter-feature distances distances = None @@ -2257,22 +2168,26 @@ def evaluate_clusters_for_repeats(self, features): distances = sdm else: distances = distances.multiply(sdm) - + # Roll up features # Extract indices of within-tolerance points distances = distances.tocoo() - pairs = np.stack((distances.row, distances.col), axis=1) # These are the index values of the clusters, not the cluster ids - # Conver to cluster ids + pairs = np.stack( + (distances.row, distances.col), axis=1 + ) # These are the index values of the clusters, not the cluster ids + # Conver to cluster ids pairs_df = pd.DataFrame(pairs, columns=["parent", "child"]) - pairs_df["parent"] = summary_df.loc[pairs[:,0]]["cluster"].values - pairs_df["child"] = summary_df.loc[pairs[:,1]]["cluster"].values + pairs_df["parent"] = summary_df.loc[pairs[:, 0]]["cluster"].values + pairs_df["child"] = summary_df.loc[pairs[:, 1]]["cluster"].values pairs_df = pairs_df.set_index("parent") merge_these_clusters = [] possible_overlaps = [] - root_parents = np.setdiff1d(np.unique(pairs_df.index.values), np.unique(pairs_df.child.values)) + root_parents = np.setdiff1d( + np.unique(pairs_df.index.values), np.unique(pairs_df.child.values) + ) for parent in root_parents: - parent_features = features[features['cluster'] == parent] + parent_features = features[features["cluster"] == parent] children = pairs_df.loc[[parent], "child"].tolist() for child in children: overlap = self.check_merge(parent_features, child, features) @@ -2280,79 +2195,54 @@ def evaluate_clusters_for_repeats(self, features): merge_these_clusters.append((parent, child, len(overlap))) else: possible_overlaps.append((parent, child, len(overlap))) - + result_dict = {} - result_dict['merge_these_clusters'] = merge_these_clusters - result_dict['possible_overlaps'] = possible_overlaps + result_dict["merge_these_clusters"] = merge_these_clusters + result_dict["possible_overlaps"] = possible_overlaps return result_dict - - + def check_merge(self, parent_features, child, features): # Grab the features of the parent and children - child_features = features[features['cluster'] == child] + child_features = features[features["cluster"] == child] # Check if there is an overlap between mf_coll_id in the parent and child clusters - overlap = np.intersect1d(parent_features['sample_id'].values, child_features['sample_id'].values) + overlap = np.intersect1d( + parent_features["sample_id"].values, child_features["sample_id"].values + ) return overlap def cluster_mass_features_agg_cluster(self, features): if features is None: return None - + features = features.copy() self.add_sparse_distance_matrix(features) distances = self._sparse_distance_matrix - - # Convert to full matrix - distances = distances.todense() - - # Cast all 0s to 1s for a distance matrix - distances[distances == 0] = 1 - distances = np.asarray(distances) - - # Perform clustering - try: - clustering = AgglomerativeClustering(n_clusters=None, - linkage='complete', - # using complete linkage will prevent one sample from being assigned to multiple clusters - metric='precomputed', - distance_threshold=1).fit(distances) - features['cluster'] = clustering.labels_ - - # All data points are singleton clusters - except: - features['cluster'] = np.arange(len(features.index)) - - return features - - - """ # Convert to full matrix distances = distances.todense() # Cast all 0s to 1s for a distance matrix distances[distances == 0] = 1 distances = np.asarray(distances) + # Perform clustering try: - clustering = AgglomerativeClustering(n_clusters=None, - linkage='complete', - metric='precomputed', - distance_threshold=1).fit(distances) - features['cluster'] = clustering.labels_ + clustering = AgglomerativeClustering( + n_clusters=None, + linkage="complete", + # using complete linkage will prevent one sample from being assigned to multiple clusters + metric="precomputed", + distance_threshold=1, + ).fit(distances) + features["cluster"] = clustering.labels_ # All data points are singleton clusters except: - features['cluster'] = np.arange(len(features.index)) + features["cluster"] = np.arange(len(features.index)) return features - """ - - - - From 72f3b0a2c8598c56b8aaf19cfff8c5e7ffeeebca Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 13 Feb 2025 08:41:44 -0800 Subject: [PATCH 032/158] Clean up collection branch for distribution --- corems/mass_spectra/calc/lc_calc.py | 2 - .../nmdc/lipidomics/lipidomics_collection.py | 77 ++++++------------- .../nmdc/lipidomics/lipidomics_workflow.py | 3 + 3 files changed, 26 insertions(+), 56 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index e4e52abaf..f4149ba43 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1729,8 +1729,6 @@ def align_lcms_objects(self, overwrite=False): starting with the LCMS object immediately following the center LCMS object. The retention times of the LCMS objects are then fit to the center LCMS object using the matched mass features. - Currently, this function only aligns LCMS objects within each batch, but not between batches. - Returns ------- None, but aligns the LCMS objects in the collection and sets the scan_time_aligned column in the scan_df attribute of each LCMS object. diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 42944f7db..6f8a64830 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -5,31 +5,17 @@ if __name__ == "__main__": collection_path = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/UDN_neg/processed_data") + # Will need a bunch of already processed data for this to work (KRH to raw data, process using the lipidomics_workflow script, no need to do MS2 matching) manifest_file = collection_path / "manifest_very_small.csv" + # This file will need to be created by the user or helper script? For now, we have a small and large manifest file for testing (KRH to provide an example) chromatography_file = collection_path / "long_lipid_gradient_chroma.csv" + # This file will need to be created by the user or helper script? + # For now, setting to None should also work, it isn't used at the moment (or KRH to provide an example if setting to None doesn't work) - """ - # Read in manifest file - import pandas as pd - manifest_df = pd.read_csv(manifest_file) - missing_files = [] - # Remove any rows with missing file paths - for index, row in manifest_df.iterrows(): - sample_folder = Path(row["sample_name"] + ".corems") - # Check if sample_folder exists in collection_path - if not (collection_path / sample_folder).exists(): - missing_files.append(row["sample_name"]) - manifest_df.drop(index, inplace=True) - - - manifest_df.to_csv(collection_path / "manifest_working.csv", index=False) - # save missing files to a text file - with open(collection_path / "missing_files.txt", "w") as f: - for item in missing_files: - f.write("%s\n" % item) - """ - + # Set the number of cores to use for loading the data (the parser is parallelized) ncores = 10 + + # Instantiate the parser parser = ReadCoreMSHDFMassSpectraCollection( folder_location = collection_path, manifest_file = manifest_file, @@ -38,55 +24,38 @@ ) print("Loading LCMS collection with", len(parser.manifest), "samples using", ncores, " cores") + + # Load the LCMS collection (minimally load the data) start_time = time.time() lcms_collection = parser.get_lcms_collection(load_raw=False, load_light=True) print("Time to load LCMS collection ", time.time() - start_time, "seconds -", len(lcms_collection), " LCMS runs and ", ncores, " cores") #10s for 7 samples, 10 cores; 162s for 70 samples, 10 cores + + # Honestly, I can't quite remember what this does - I think it is to remove isotopologues from the mass features for future steps lcms_collection.parameters.lcms_collection.drop_isotopologues = True print("Number of total mass features: ", len(lcms_collection.mass_features_dataframe)) + # Align the LCMS runs between each other print("Aligning LCMS collection") start_time = time.time() lcms_collection.align_lcms_objects() print("Time to align LCMS collection: ", time.time() - start_time, "seconds") #1.5s for 7 samples; 15s for 70 samples - #lcms_collection.plot_tics(type="both") - #lcms_collection.plot_alignments() - #mass_feature_df = lcms_collection.mass_features_to_df() - #print("Adding consensus mass features") + # Make some plots + lcms_collection.plot_tics(type="both") + lcms_collection.plot_alignments() + # TODO: Think about other plots that would be useful to have here + + # Consolidate the mass features from the LCMS runs into a single dataframe + mass_feature_df = lcms_collection.mass_features_to_df() + + # Make consensus mass features from the consolidated mass features start_time = time.time() lcms_collection.add_consensus_mass_features() + # THIS FUNCTION NEEDS WORK AND NEEDS MECHANISM TO EVALUATE THE RESULTS (plots? reports?) print("Time to roll up consensus mass features: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") - - # Prepare mass features dataframe for export for examination - collection_mass_features = lcms_collection.mass_features_dataframe - - # Save mass features to csv - collection_mass_features.to_csv(collection_path / "collection_mass_features.csv", index=False) - """ - # Pivot the mass features dataframe, sample_id as columns and cluster_id as index, then remove the index name - mass_feature_pivot = collection_mass_features.pivot(index='cluster', columns='sample_name', values='area').reset_index() - mass_feature_pivot.columns.name = None - - # Get average mz and rt for each cluster - cluster_avg_mz_rt = collection_mass_features.groupby('cluster').agg({'mz': 'median', 'scan_time_aligned': 'median', 'sample_id': 'nunique', 'area': 'max'}).reset_index() - # Rename sample_id to n_samples - cluster_avg_mz_rt.rename(columns={'sample_id': 'n_samples', 'area': 'max_area'}, inplace=True) - - # Add average mz and rt to mass_feature_pivot - mass_feature_pivot = cluster_avg_mz_rt.merge(mass_feature_pivot, on='cluster', how='left') - - # Reorder by mz and reset index - mass_feature_pivot.sort_values('mz', inplace=True) - - # Save mass_feature_pivot to csv - mass_feature_pivot.to_csv(collection_path / "collection_mass_features_pivot_rollup_area.csv", index=True) - #TODO KRH: Add code to load and save information about chromatographic settings #TODO KRH: Add code to save and load collection to HDF5 file - #TODO KRH: Add code to plot a consensus mass feature - - print("here") - """ \ No newline at end of file + #TODO KRH: Add code to plot a consensus mass feature \ No newline at end of file diff --git a/support_code/nmdc/lipidomics/lipidomics_workflow.py b/support_code/nmdc/lipidomics/lipidomics_workflow.py index b26a1db3a..753599eee 100644 --- a/support_code/nmdc/lipidomics/lipidomics_workflow.py +++ b/support_code/nmdc/lipidomics/lipidomics_workflow.py @@ -592,6 +592,8 @@ def run_lipid_workflow( pool.close() pool.join() + # Skip ms2 spectral search for now, will add back once we have an improved MetabRefLCInterface method + """ # Prepare ms2 spectral search space metadata = prep_metadata(mz_dicts, out_dir) @@ -607,6 +609,7 @@ def run_lipid_workflow( mz_dicts = pool.starmap(run_lipid_ms2, args) pool.close() pool.join() + """ print("Finished processing, data are written in " + str(out_dir)) From 83e3133da808b60bef202b830005a6b9eb0b4491 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 27 Feb 2025 11:22:21 -0800 Subject: [PATCH 033/158] Prepare collection for distribution --- .../nmdc/lipidomics/lipidomics_collection.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 6f8a64830..61e74f1de 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -4,14 +4,13 @@ if __name__ == "__main__": + # Set the path to the collection of LCMS runs (previously processed) collection_path = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/UDN_neg/processed_data") - # Will need a bunch of already processed data for this to work (KRH to raw data, process using the lipidomics_workflow script, no need to do MS2 matching) + # Path to manifest file manifest_file = collection_path / "manifest_very_small.csv" - # This file will need to be created by the user or helper script? For now, we have a small and large manifest file for testing (KRH to provide an example) + # This file will need to be created by the user or helper script? chromatography_file = collection_path / "long_lipid_gradient_chroma.csv" - # This file will need to be created by the user or helper script? - # For now, setting to None should also work, it isn't used at the moment (or KRH to provide an example if setting to None doesn't work) - + # Set the number of cores to use for loading the data (the parser is parallelized) ncores = 10 @@ -22,7 +21,6 @@ chromatography_file=chromatography_file, cores = ncores ) - print("Loading LCMS collection with", len(parser.manifest), "samples using", ncores, " cores") # Load the LCMS collection (minimally load the data) @@ -45,7 +43,7 @@ # Make some plots lcms_collection.plot_tics(type="both") lcms_collection.plot_alignments() - # TODO: Think about other plots that would be useful to have here + # TODO: Think about other plots that would be useful to have here for assessing the quality of the data and alignment # Consolidate the mass features from the LCMS runs into a single dataframe mass_feature_df = lcms_collection.mass_features_to_df() @@ -56,6 +54,6 @@ # THIS FUNCTION NEEDS WORK AND NEEDS MECHANISM TO EVALUATE THE RESULTS (plots? reports?) print("Time to roll up consensus mass features: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") - #TODO KRH: Add code to load and save information about chromatographic settings - #TODO KRH: Add code to save and load collection to HDF5 file - #TODO KRH: Add code to plot a consensus mass feature \ No newline at end of file + #TODO: Add code to load and save information about chromatographic settings + #TODO: Add code to save and load collection to HDF5 file + #TODO: Add code to plot a consensus mass feature \ No newline at end of file From cd98ecb238b72e94070f7ca61cbbffcb69f29877 Mon Sep 17 00:00:00 2001 From: "Ciesielski, Danielle K" Date: Thu, 13 Mar 2025 12:03:33 -0700 Subject: [PATCH 034/158] remove dask, remove extra dataframe step, block runtime warnings, set coll_mf_id column as index in add_consensus_mass_features() --- corems/chroma_peak/calc/subset.py | 23 ++++--------------- corems/mass_spectra/calc/lc_calc.py | 3 +++ .../nmdc/lipidomics/lipidomics_collection.py | 5 +--- 3 files changed, 9 insertions(+), 22 deletions(-) diff --git a/corems/chroma_peak/calc/subset.py b/corems/chroma_peak/calc/subset.py index 4b4fa1943..45158e3f7 100644 --- a/corems/chroma_peak/calc/subset.py +++ b/corems/chroma_peak/calc/subset.py @@ -4,7 +4,6 @@ import multiprocessing as mp from functools import partial -import dask.dataframe as dd import numpy as np import pandas as pd @@ -15,7 +14,7 @@ class MultiSamplePartitions: Attributes ---------- - features : :obj:`~pandas.DataFrame` or :obj:`~dask.dataframe.DataFrame` + features : :obj:`~pandas.DataFrame` Input feature coordinates and intensities. split_on : str Dimension to partition the data. @@ -39,7 +38,7 @@ def __init__(self, Parameters ---------- - features : :obj:`~pandas.DataFrame` or :obj:`~dask.dataframe.DataFrame` + features : :obj:`~pandas.DataFrame` Input feature coordinates and intensities. split_on : str Dimension to partition the data. @@ -64,11 +63,6 @@ def __init__(self, self.tol = tol self.relative = relative - if isinstance(features, dd.DataFrame): - self.dask = True - else: - self.dask = False - self._compute_splits() def _compute_splits(self): @@ -79,11 +73,7 @@ def _compute_splits(self): self.counter = 0 - if self.dask: - idx = self.features.groupby( - by=self.split_on).size().compute().sort_index() - else: - idx = self.features.groupby(by=self.split_on).size().sort_index() + idx = self.features.groupby(by=self.split_on).size().sort_index() counts = idx.values idx = idx.index @@ -130,10 +120,7 @@ def __next__(self): self.split_on, self.bounds[self.counter][1]) - if self.dask: - subset = self.features.query(q).compute() - else: - subset = self.features.query(q) + subset = self.features.query(q) self.counter += 1 if len(subset.index) > 1: @@ -188,7 +175,7 @@ def multi_sample_partition(features, split_on='mz', size=500, tol=25E-6, relativ Parameters ---------- - features : :obj:`~pandas.DataFrame` or :obj:`~dask.dataframe.DataFrame` + features : :obj:`~pandas.DataFrame` Input feature coordinates and intensities. split_on : str Dimension to partition the data. diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index f4149ba43..b4efa10b3 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -14,6 +14,7 @@ from corems.mass_spectra.factory.chromat_data import EIC_Data from corems.mass_spectrum.input.numpyArray import ms_from_array_profile +warnings.filterwarnings("ignore", category=RuntimeWarning) def find_closest(A, target): """Find the index of closest value in A to each value in target. @@ -1948,6 +1949,8 @@ def add_consensus_mass_features(self): eval_dict = self.evaluate_clusters_for_repeats(mfs_with_clusters) # TODO KRH: Deal with isomers better? Pool them together and then split them out using samples with 2 as the template? + + mfs_with_clusters.set_index('coll_mf_id', inplace = True) self.mass_features_dataframe = mfs_with_clusters diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 61e74f1de..1051e8b69 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -29,7 +29,7 @@ print("Time to load LCMS collection ", time.time() - start_time, "seconds -", len(lcms_collection), " LCMS runs and ", ncores, " cores") #10s for 7 samples, 10 cores; 162s for 70 samples, 10 cores - # Honestly, I can't quite remember what this does - I think it is to remove isotopologues from the mass features for future steps + # Set flag to call _drop_isotopologue() when running _check_mass_features_df() lcms_collection.parameters.lcms_collection.drop_isotopologues = True print("Number of total mass features: ", len(lcms_collection.mass_features_dataframe)) @@ -45,9 +45,6 @@ lcms_collection.plot_alignments() # TODO: Think about other plots that would be useful to have here for assessing the quality of the data and alignment - # Consolidate the mass features from the LCMS runs into a single dataframe - mass_feature_df = lcms_collection.mass_features_to_df() - # Make consensus mass features from the consolidated mass features start_time = time.time() lcms_collection.add_consensus_mass_features() From b04dba7a02f88e22f1fe30c91ef1552c6fc015a4 Mon Sep 17 00:00:00 2001 From: "Ciesielski, Danielle K" Date: Mon, 14 Apr 2025 10:54:16 -0700 Subject: [PATCH 035/158] added 2 basic plots, needs complex plot --- corems/mass_spectra/calc/lc_calc.py | 57 +++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index b4efa10b3..1d51f5dc1 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -7,7 +7,7 @@ from scipy.spatial import KDTree from sklearn.svm import SVR from sklearn.cluster import AgglomerativeClustering - +import matplotlib.pyplot as plt from corems.chroma_peak.factory.chroma_peak_classes import LCMSMassFeature from corems.mass_spectra.calc import SignalProcessing as sp @@ -1953,12 +1953,13 @@ def add_consensus_mass_features(self): mfs_with_clusters.set_index('coll_mf_id', inplace = True) self.mass_features_dataframe = mfs_with_clusters + self.summarize_clusters(mfs_with_clusters) def summarize_clusters(self, features): """ Summarize the clusters of mass features by median attributes """ - # First check if there are minimum columsn in the features dataframe + # First check if there are minimum columns in the features dataframe if len(features.columns) < 1: return None @@ -1987,9 +1988,57 @@ def summarize_clusters(self, features): ] summary_df = summary_df.rename(columns={"cluster_": "cluster"}) summary_df = summary_df.reset_index(drop=True) + self.cluster_summary_dataframe = summary_df - return summary_df + def plot_mass_feature_per_cluster(self, return_fig = False): + """ + Plot the number of mass features in a cluster against how many clusters + contain that number of mass features + """ + if not hasattr(self, 'cluster_summary_dataframe'): + raise ValueError( + 'cluster_summary_dataframe is not set, must run add_consensus_mass_features() first' + ) + else: + sum_data = self.cluster_summary_dataframe + fig, ax = plt.subplots() + sum_data.sample_id_count.value_counts().sort_index().plot(ax = ax, kind = 'bar') + plt.xlabel('Number of mass features in a cluster') + plt.ylabel('Number of clusters with this many mass features') + if return_fig: + plt.close(fig) + return fig + else: + plt.show() + + def plot_mass_features_across_samples(self, alpha = 0.75, s = 0.005, return_fig = False): + """ + Generate Scan Time vs m/z plot of all the mass features across all + samples in collection where intensity of color on the plot indicates + density of mass features, NOT INTENSITY + """ + df = self.mass_features_dataframe.copy() + fig = plt.figure() + plt.scatter( + df.scan_time_aligned, + df.mz, + c = 'tab:gray', + alpha = alpha, + s = s + ) + plt.xlabel('Scan time') + plt.ylabel('m/z') + plt.ylim(0, np.ceil(np.max(df.mz))) + plt.xlim(0, np.ceil(np.max(df.scan_time))) + plt.title('All mass features, all samples') + + if return_fig: + plt.close(fig) + return fig + else: + plt.show() + def add_sparse_distance_matrix(self, features): if features is None: return None @@ -2086,7 +2135,7 @@ def add_sparse_distance_matrix(self, features): def evaluate_clusters_for_repeats(self, features): summary_df = self.summarize_clusters(features) - summary_df = summary_df.copy() + summary_df = self.cluster_summary_dataframe.copy() # Arrange by decreasing median intensity summary_df = summary_df.sort_values( From d3f13031847014768918d6cebb9ab9c24a787ddb Mon Sep 17 00:00:00 2001 From: "Ciesielski, Danielle K" Date: Mon, 14 Apr 2025 11:00:02 -0700 Subject: [PATCH 036/158] temp working files for complex plot --- .../lipidomics/lipidomics_collection_nb.ipynb | 302 ++++++++++++++++++ .../lipidomics_collection_working.py | 96 ++++++ .../nmdc/lipidomics/lipidomics_workflow.py | 6 +- 3 files changed, 401 insertions(+), 3 deletions(-) create mode 100644 support_code/nmdc/lipidomics/lipidomics_collection_nb.ipynb create mode 100644 support_code/nmdc/lipidomics/lipidomics_collection_working.py diff --git a/support_code/nmdc/lipidomics/lipidomics_collection_nb.ipynb b/support_code/nmdc/lipidomics/lipidomics_collection_nb.ipynb new file mode 100644 index 000000000..71405307e --- /dev/null +++ b/support_code/nmdc/lipidomics/lipidomics_collection_nb.ipynb @@ -0,0 +1,302 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "6b2609cf-1a43-47c1-9685-255054bc5393", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading LCMS collection with 22 samples using 8 cores\n", + "Time to load LCMS collection 181.71068596839905 seconds - 22 LCMS runs and 8 cores\n", + "Number of total mass features: 151007\n", + "Aligning LCMS collection\n", + "Time to align LCMS collection: 12.283224821090698 seconds\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA04AAANQCAYAAAAffD9qAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuNSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/xnp5ZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOz9eZTlV33fe7/3bzhjjT2PEhISYhACWQYjfG3AAWMW4YEV24lJnohk2X6CF9x4SOwsxXlyl+048nC5xNfGYF8/tmywLMdgIMaDkMGSbEuABGohITS31GNVd03n1Jl+w977+eNUV1d1zV3VXVWnP6+1iqU6U+1TVP/O7/P77v3dxnvvERERERERkSUFmz0AERERERGRrU7BSUREREREZAUKTiIiIiIiIitQcBIREREREVmBgpOIiIiIiMgKFJxERERERERWoOAkIiIiIiKyAgUnERERERGRFSg4iYiIiIiIrEDBSUREREREZAVXdHB64IEHeM973sOBAwcwxvC5z31uza9xzz338KY3vYn+/n52797ND/7gD/Liiy9u+FhFRERERGTzXNHBqdls8rrXvY6PfexjF/X8o0eP8t73vpfv+77v48iRI9xzzz2MjY3xz/7ZP9vgkYqIiIiIyGYy3nu/2YPYCowxfPazn+V973vf7G1JkvDzP//z/Mmf/AlTU1PceOON/Oqv/ipvfetbAfj0pz/N+9//fpIkIQi6GfQv/uIveO9730uSJMRxvAnvRERERERENtoVXXFayYc//GEeeugh7r77br75zW/ywz/8w/zAD/wAzz77LAC33HILQRDwB3/wB1hrqdVqfPKTn+Ttb3+7QpOIiIiISA9RxWnGhRWnY8eOce2113Ls2DEOHDgw+7i3v/3tvPGNb+S///f/DsD999/PP//n/5zx8XGstdx666381V/9FUNDQ5vwLkRERERE5FJQxWkJjz/+ONZaXvGKV9DX1zf7df/99/P8888DMDIywo//+I/zgQ98gIcffpj777+fQqHAD/3QD6E8KiIiIiLSO6LNHsBW1Wg0CMOQr3/964RhOO++vr4+AD72sY8xODjIr/3ar83e96lPfYrDhw/z1a9+lTe96U2XdcwiIiIiInJpKDgt4eabb8Zay5kzZ/ie7/meRR/TarVmm0Kccy5kOecu+RhFREREROTyuKKn6jUaDY4cOcKRI0eAbnvxI0eOcOzYMV7xilfwr/7Vv+K2227jz//8zzl69Chf+9rXuOOOO/jLv/xLAN797nfz8MMP84u/+Is8++yzfOMb3+Df/tt/y9VXX83NN9+8ie9MREREREQ20hXdHOK+++7jbW9724LbP/CBD3DnnXeSZRn/7b/9N/7oj/6IkydPsmvXLt70pjfxC7/wC7z2ta8F4O677+bXfu3XeOaZZ6hUKtx666386q/+Kq985Ssv99sREREREZFL5IoOTiIiIiIiIqtxRU/VExERERERWQ0FJxERERERkRVccV31nHOcOnWK/v5+jDGbPRwREREREdkk3nump6c5cODAgm7ZF7rigtOpU6c4fPjwZg9DRERERES2iOPHj3Po0KFlH3PFBaf+/n6g+8sZGBjY5NGIiIiIiMhmqdfrHD58eDYjLOeKC07npucNDAwoOImIiIiIyKqW8Kg5hIiIiIiIyAoUnERERERERFag4CQiIiIiIrICBScREREREZEVKDiJiIiIiIisQMFJRERERERkBZsanD7+8Y9z0003zbYGv/XWW/nrv/7rJR9/5513YoyZ91UqlS7jiEVERERE5Eq0qfs4HTp0iF/5lV/h+uuvx3vPH/7hH/Le976XRx99lNe85jWLPmdgYICnn3569vvV9FwXERERERFZj00NTu95z3vmff/Lv/zLfPzjH+crX/nKksHJGMO+ffsux/BERERERESALbTGyVrL3XffTbPZ5NZbb13ycY1Gg6uvvprDhw/z3ve+l29961vLvm6SJNTr9XlfIiIiIiIia7Hpwenxxx+nr6+PYrHIBz/4QT772c/y6le/etHH3nDDDfz+7/8+n//85/nUpz6Fc443v/nNnDhxYsnXv+OOOxgcHJz9Onz48KV6KyIiIiIi0qOM995v5gDSNOXYsWPUajU+/elP83u/93vcf//9S4anubIs41WvehXvf//7+aVf+qVFH5MkCUmSzH5fr9c5fPgwtVqNgYGBDXsfIiIiIiKyvdTrdQYHB1eVDTZ1jRNAoVDguuuuA+CWW27h4Ycf5jd+4zf4nd/5nRWfG8cxN998M88999ySjykWixSLxQ0bryzvj45kvOv6iN1VNe0QERERkd6x6VP1LuScm1chWo61lscff5z9+/df4lHJaj1yynK25TZ7GCIiIiIiG2pTK063334773rXu7jqqquYnp7mrrvu4r777uOee+4B4LbbbuPgwYPccccdAPziL/4ib3rTm7juuuuYmpri13/913nppZf4sR/7sc18GzLDeU9owCk3iYiIiEiP2dTgdObMGW677TZOnz7N4OAgN910E/fccw/veMc7ADh27BhBcL4oNjk5yY//+I8zMjLC8PAwt9xyCw8++OCq1kPJpZdaqBYMdlNXzYmIiIiIbLxNbw5xua1lAZiszVTH89tfy/iB60K+40C42cMREREREVnWWrLBllvjJNtXJ/dUY1RxEhEREZGeo+AkGybJoaKpeiIiIiLSgxScZMN0croVJzWHEBEREZEeo+AkG6aTeyqxwV5Zy+ZERERE5Aqg4CQbpjtVT+3IRURERKT3KDjJhulYTzU25ApOIiIiItJjFJxkw3RyqMTgNFNPRERERHqMgpNsmDSHaqyueiIiIiLSexScZMN0ck+loK56IiIiItJ7FJxkw3Sn6qniJCIiIiK9R8FJNozzEAdgtchJRERERHqMgpNsGGMgDNQcQkRERER6j4KTbBjvITSoHbmIiIiI9BwFJ9lQqjiJiIiISC9ScJINY0y34qTmECIiIiLSaxScZEMZY/AKTiIiIiLSYxScZMOcC0zGbO44REREREQ2moKTbDhVnERERESk1yg4iYiIiIiIrEDBSTaE8352ip6m6omIiIhIr1Fwkg0x2YbhUjcxaaqeiIiIiPQaBSfZEKMNx94+lZpEREREpDcpOMmGGG169lS7wUlT9URERESk1yg4yYbo5FCJu/+tqXoiIiIi0msUnGRDWNfd/FZEREREpBcpOMmG8ECornoiIiIi0qMUnGRDWAfBTGDSVD0RERER6TUKTrIhnPezwUlEREREpNcoOMmGcP58xUlT9URERESk1yg4yYZwHsKZvyZN1RMRERGRXqPgJBvCzqk4iYiIiIj0GgUn2RBeU/VEREREpIcpOMmGsP58O3IRERERkV6j4CQbwnttgCsiIiIivUvBSUREREREZAWbGpw+/vGPc9NNNzEwMMDAwAC33norf/3Xf73sc/7sz/6MV77ylZRKJV772tfyV3/1V5dptCIiIiIicqXa1OB06NAhfuVXfoWvf/3rPPLII3zf930f733ve/nWt7616OMffPBB3v/+9/OjP/qjPProo7zvfe/jfe97H0888cRlHrmIiIiIiFxJjPdba9edHTt28Ou//uv86I/+6IL7/sW/+Bc0m02+8IUvzN72pje9ide//vV84hOfWNXr1+t1BgcHqdVqDAwMbNi4r3R/dCTjttfHAHzysYx//bp4k0ckIiIiIrK8tWSDLbPGyVrL3XffTbPZ5NZbb130MQ899BBvf/vb5932zne+k4ceemjJ102ShHq9Pu9LRERERERkLTY9OD3++OP09fVRLBb54Ac/yGc/+1le/epXL/rYkZER9u7dO++2vXv3MjIysuTr33HHHQwODs5+HT58eEPHLyIiIiIivW/Tg9MNN9zAkSNH+OpXv8pP/MRP8IEPfIAnn3xyw17/9ttvp1arzX4dP358w15bRERERESuDNFmD6BQKHDdddcBcMstt/Dwww/zG7/xG/zO7/zOgsfu27eP0dHRebeNjo6yb9++JV+/WCxSLBY3dtAiIiIiInJF2fSK04WccyRJsuh9t956K1/60pfm3XbvvfcuuSZKRERERERkI2xqxen222/nXe96F1dddRXT09Pcdddd3Hfffdxzzz0A3HbbbRw8eJA77rgDgJ/8yZ/kLW95Cx/5yEd497vfzd13380jjzzC7/7u727m2xARERERkR63qcHpzJkz3HbbbZw+fZrBwUFuuukm7rnnHt7xjncAcOzYMYLgfFHszW9+M3fddRf/5b/8F/7zf/7PXH/99Xzuc5/jxhtv3Ky3ICIiIiIiV4Att4/TpaZ9nC4N7eMkIiIiItvNttzHSUREREREZKtScJINd2XVMEVERETkSqDgJBvOmM0egYiIiIjIxlJwEhERERERWYGCk4iIiIiIyAoUnERERERERFag4CQiIiIiIrICBScREREREZEVKDjJhlM7chERERHpNQpOIiIiIiIiK1Bwkg2nfZxEREREpNcoOImIiIiIiKxAwUlERERERGQFCk4iIiIiIiIrUHASERERERFZgYKTiIiIiIjIChScZMMZwGkzJxERERHpIQpOsuECA9Zt9ihERERERDaOgpNsuMCAU8FJRERERHqIgpNsuDBQcBIRERGR3qLgJBsuMGAVnERERESkhyg4yYYLjcGtsMbpfz2V00iVrkRERERke1Bwkg0XGFipN8SZpufpMXWQEBHpVe22Lo6JSG9RcJINt5queoFBwUlEpIf97RcVnESktyg4yYbrNodY/gMzCiCxl2lAIiJy2WXpZo9ARGRjKTjJhgtX2RwiMJd+LCIisjmybLNHICKysRScZEOYOSFotRvgKjeJiPQuBScR6TUKTrLhAgMrzNQDQLPfRUR6V6qpeiLSYxScZMOFgfZxEhG50mmNk4j0GgUn2XCBMasKTpqqJyLSu1JN1RORHqPgJBsuNKy4Aa6IiPS2XMFJRHqMgpNsuMCA0xonEZErWprqKC8ivUXBSTZcsMp25CIi0ru0xklEeo2Ck2y41WyAC1rjJCLSy7TGSUR6jYKTbLjV7uMkIiK9S/s4iUiv2dTgdMcdd/CGN7yB/v5+9uzZw/ve9z6efvrpZZ9z5513YoyZ91UqlS7TiGU1VjNVz6jcJCLS09Jks0cgIrKxNjU43X///XzoQx/iK1/5Cvfeey9ZlvH93//9NJvNZZ83MDDA6dOnZ79eeumlyzRiWY1wFRvgrmaDXBER2b5UcRKRXhNt5g//m7/5m3nf33nnnezZs4evf/3rfO/3fu+SzzPGsG/fvks9PLlIQQA23+xRiIjIZnHOayGriPScLbXGqVarAbBjx45lH9doNLj66qs5fPgw733ve/nWt7615GOTJKFer8/7kksrXOUGuABepScRkZ5jbbdRkIhIL9kyhzXnHD/1Uz/Fd3/3d3PjjTcu+bgbbriB3//93+fzn/88n/rUp3DO8eY3v5kTJ04s+vg77riDwcHB2a/Dhw9fqrcgM1a7AW6otuUiIj3JWgjDzR6FiMjG2jLB6UMf+hBPPPEEd99997KPu/XWW7ntttt4/etfz1ve8hb+/M//nN27d/M7v/M7iz7+9ttvp1arzX4dP378Ugxf5jCr3AA3DiFX9z0RkZ5jLYSbuhhARGTjbYnD2oc//GG+8IUv8MADD3Do0KE1PTeOY26++Waee+65Re8vFosUi8WNGKasUhiAXWEKnjEQh4bUQmlL/BWKiMhGUcVJRHrRplacvPd8+MMf5rOf/Sxf/vKXueaaa9b8GtZaHn/8cfbv338JRigXI1xFxcl7iAPI7OUZk4iIXD5Oa5xEpAdt6rX+D33oQ9x11118/vOfp7+/n5GREQAGBwcpl8sA3HbbbRw8eJA77rgDgF/8xV/kTW96E9dddx1TU1P8+q//Oi+99BI/9mM/tmnvQ+Zb7Qa4cQiZ86j1kohIb1HFSUR60aYGp49//OMAvPWtb513+x/8wR/wb/7NvwHg2LFjBMH5y1aTk5P8+I//OCMjIwwPD3PLLbfw4IMP8upXv/pyDVtWEKx2jVMAuSpOIiI9x1rYce0xcn+QyBQ2ezgiIhtiU4PTalpR33ffffO+/+hHP8pHP/rRSzQi2QirmaoHM2uc1BxCRKTnWAuVwTrO7wUFJxHpEZqBLBtibgYOgtXt46Q1TiIivclaCAsWj66OiUjvUHCSdfPeE8xZprRSxck6TxTMrHHSRk4iIj3HWojiHOcVnESkdyg4ybo5P7+9w0rNITLXDU1x0P1vERHpLdZCGFus07QCEekdCk6ybp7uvkzndJtDLF1JSi1EgSEOjabqiYj0IGshKuQ4p6tjItI7FJxk3ZyfH5zCYPmpepntVptUcRIR6U3dilOuipOI9BQFJ1k371mwxmm5pUuZ8xRCrXESEelVbqbilK+mxaqIyDah4CTrduEWtiuucbIza5xCVZxERHpRbiEMnabqiUhP2dR9nKQ3XFhxWmkD3MxBHBjiQGucRER6kbOQdwJyTdUTkR6iipOs22Jd9VZc46SKk4hIz7IWOuMBTlP1RKSHKDjJul3YVc8YM+/7C2XWn28OoTVOIiI9x1rImwHWquIkIr1DwUnWrdtVb35SWqYb+fl9nFRxEhHpSdaCSwNaNR3kRaR3KDjJ+l2wxmklqYU4NDMVp0s3LBER2RzWeqI4oHFGswpEpHcoOMm6OeavcVpJ7rrT9Iwx6CNVRKT3WAuFckBjTFfHRKR3KDjJul3YVQ9YcY1TIby0YxIRkc1jLURRSNbRVD0R6R0KTrJuizVNWm6NUzqzxgnWVqkSEZHtwVoICPBG8wpEpHcoOMm6eda2ximzEK3lCSIisq2cD06aqicivUPBSdbNe7/s1LwLZRYK+ssTEelZzoHxAd5oqp6I9I5oswcg299ia5yWkzk/O1VPRER6kcPbEAIFJxHpHbruL+u2WFe95ZtDoOAkItLLjOPJwXzZ9a4iItuNgpOsm/cLg9JyH5bWQ6glTiIivcs4OrpAJiI9RsFJ1s35tXfHM2tZFCUiItuL8XTCxbuuiohsVwpOsm5r7aonIiI9zjjagcFrm3MR6SEKTrJui03VW66gpDnvIiI9znhyA7kuqolID1FwknVbrKuewpGIyJXMkxlDvtnDEBHZQApOsm5rbTar5U0iIj3OOLw35EZX0USkdyg4ybp1K05KQyIi0mWMJ3QBeQjObvZoREQ2hoKTrNtiXfWUo0RErmSOyAW40JO1NnssIiIbQ8FJNsTFrnHSJA4RkR5kPJELyEOP00InEekRCk6ybm6RrnrLUeMIEZEeZxyxD7AGBScR6RkKTrJui7UjXy3N6BMR6UHnKk6BKk4i0jsUnGTdHGsLQFr/JCLS6zyRM+QBeDWHEJEeoeAk67bYPk6rDUeatSci0nu8sRRciFXFSUR6iIKTrJv3fkHFSeuYRESuXJn3lF2IC7XGSUR6h4KTrJtnYcVptTRrT0Sk9+Q4Cr57iqHgJCK9QsFJ1k1d9UREZK4cRwEDRhvgikjv2NTgdMcdd/CGN7yB/v5+9uzZw/ve9z6efvrpFZ/3Z3/2Z7zyla+kVCrx2te+lr/6q7+6DKOVpSzWVW+5IDX3vq2eoV4c8ZytbfVRiohsLZlxxATd4KSKk4j0iE0NTvfffz8f+tCH+MpXvsK9995LlmV8//d/P81mc8nnPPjgg7z//e/nR3/0R3n00Ud53/vex/ve9z6eeOKJyzhymWuxrnq9UlV66Yzn5FiPvBkRkcskx1H0AcaoOYSI9I5oM3/43/zN38z7/s4772TPnj18/etf53u/93sXfc5v/MZv8AM/8AP87M/+LAC/9Eu/xL333stv/dZv8YlPfOKSj1kW8h7CNUTwuaHKMNNcYov2KG+0PVqJJSKyNjmOuJJhjFFwEpGesaXWONVqNQB27Nix5GMeeugh3v72t8+77Z3vfCcPPfTQoo9PkoR6vT7vSzaW9xcfLeIQ0i08/73VgVpTFScRkbWwxjF1cBzQPk4i0ju2THByzvFTP/VTfPd3fzc33njjko8bGRlh7969827bu3cvIyMjiz7+jjvuYHBwcPbr8OHDGzpuWXtXvbnFpd0Vw9nW1g0mxkCabfYoRES2F4cnLSdgnCpOItIztkxw+tCHPsQTTzzB3XffvaGve/vtt1Or1Wa/jh8/vqGvL92uehdbcjo0GHCivnWDk4iIrF0pmiaKaxBqjZOI9I5NXeN0zoc//GG+8IUv8MADD3Do0KFlH7tv3z5GR0fn3TY6Osq+ffsWfXyxWKRYLG7YWGUh7yG4yDVKhwYM3xq1cCjc4FGJiMhmicIm/a7JRNin4CQiPWNTK07eez784Q/z2c9+li9/+ctcc801Kz7n1ltv5Utf+tK82+69915uvfXWSzVMWcFiXfVWq69gaGoqnIhIbzEOF7UgcNrHSUR6xqZWnD70oQ9x11138fnPf57+/v7ZdUqDg4OUy2UAbrvtNg4ePMgdd9wBwE/+5E/ylre8hY985CO8+93v5u677+aRRx7hd3/3dzftfVzx/NrWOImISG8rhzX6O00mi7kqTiLSMza14vTxj3+cWq3GW9/6Vvbv3z/79ad/+qezjzl27BinT5+e/f7Nb34zd911F7/7u7/L6173Oj796U/zuc99btmGEnJpuXV01YOtvwnuVh+fiMhWUwoaVLNUa5xEpKdsasXJr2KX1Pvuu2/BbT/8wz/MD//wD1+CEcnF8MzvlLdWW71YtdXHJyKy1YTGEjog8Fjr2EK9qERELpqOZLJuzq8tOK0iL4uIyDYWGkvkDcZYvDZyEpEeoeAk6+bxPbnGyXu/rkqaiMiVKjA548F+DpkRrLpDiEiPUHCSdfNrXOO0XcJIkkEx3uxRiIhsP8ZYCj7AmgCn4CQiPULBSdbN92hXvXYCpcJmj0JEZPsJcRRdgDcG69UdQkR6g4KTrNtauuo577dNs4U0h2K8XUYrIrJ1GCxFb/CBx2mNk4j0CAUnWbe1dNWzbvtUp+ZO1VtNB0gREekKcJSsxRHgjYKTiPQGBSdZt7V01bMeom3yV5dmnkIMcQS5PvdFRFbPwIGJ5/Cg4CQiPWPVp7CTk5P85m/+JvV6fcF9tVptyfuk93lWX0WyDsJtEpzOVZyiEDJ97ouIrJoPoJh3cAQQ6gAqIr1h1aewv/Vbv8UDDzzAwMDAgvsGBwf5+7//e37zN39zQwcn28NauupZB9EFKcuY7tqnrSbJoBB1K06Z1jaLiKyawRPbDAf4QMFJRHrDqoPTZz7zGT74wQ8uef+/+3f/jk9/+tMbMijZXtbSVS/3EF7w2MIWreicaw4RBUZT9URE1ijKE7wxGFWcRKRHrDo4Pf/881x//fVL3n/99dfz/PPPb8igZHtxHlZbc7LOL5iqVwgNyRb8XE0yZtc4qeIkIrJ6w2Nj3eDkDV7BSUR6xKqDUxiGnDp1asn7T506RRBsk8UrsqHWssYpX2SNUyGEdAt+rqaZn13jpIqTiMjq9U3XCZt1HAYfZJs9HBGRDbHqpHPzzTfzuc99bsn7P/vZz3LzzTdvxJhkm/Fr7aq3yFS9JN+aa5yi2KmrnojIGtkoxiQpeLCqOIlIj4hW+8APf/jD/MiP/AiHDh3iJ37iJwjDEABrLb/927/NRz/6Ue66665LNlDZutbbVW/rVpzgq9FxdoZXkVnP6ltgiIgIQUjgPS7aggd4EZGLsOrg9IM/+IP83M/9HP/+3/97fv7nf55rr70WgBdeeIFGo8HP/uzP8kM/9EOXbKCydbk1dNXLHYQXlKeKkdmSwckDdZOwV2ucRETWyONNSOwczrjNHoyIyIZYdXAC+OVf/mXe+9738sd//Mc899xzeO95y1vewr/8l/+SN77xjZdqjLLFrWmqnlu4AW4xhNRuval6AHVS4hDa6WaPRERk+zDe48OIatTAsjWP7yIia7Wm4ATwxje+USFJ5nHeL6g4GQPee8wFicr6xafqdbZoRWeapNscYouOT0RkKwq8Iw0MscvR4VNEesWqg9M3v/nNVT3upptuuujByPa02BqngMUbQVjnF1ScCqGhnmzNK5JtcsLQk1mtbxIRWS3jPXkYElnHFpyJLSJyUVYdnF7/+tdjjMH7pU9wjTFYq0PklWaxqXpBsPi0vHyRzXILW3iqXpEQG1tyu+birIjIFcvgaNmY0Fo6mz0YEZENsuqzwaNHj17Kccg2NltxGhuDXbsACM25jXHnsw5KF/zVdduRX/JhXpQKMTbMFJxERNYg8J7UFYidpak1TiLSI1Z9NviHf/iH/Mf/+B+pVCqXcjyyDc121fvpn4Zf+zXYv3/J4NTdAHd7dNUDKBORxznZFtxnSkRkqwq8p01M6JzWOIlIz1j1Bri/8Au/QKPRuJRjkW1qdqrezTfDzCbJxnTXOF3ILbUB7hacqufxVG1KFuRkWzTYiYhsRaUnT8FYg9BarXESkZ6x6uC03NomubLNVpyKRXDd/TrCwCxTcZp/WzmCxhZs9+2Npy85QRok6qonIrIG4VSL4rGzRM6iXZxEpFesOjgBC1pLi5wTGLqlpxmh6a5nupB1nvCCP6MwMOfy1pbiAktkm6SmpYqTiMgaGOfwSU7gnPZxEpGesaYV7694xStWDE8TExPrGpBsP86DSRMoFCDLgG6QWm3FCaBagGbqqRa2Tjh3oSM0MTnJou9FREQWZzD4mfMFiyf3CZEpbvKoRETWZ03B6Rd+4RcYHBy8VGORbcoDZmwMdu+GU6eAc8HJAws3wL2wRTnAUMkwvcWCk49a9J9uMXlNsmCDXxERWY7vhicM1sDxzqNcU37TZg9KRGRd1hScfuRHfoQ9e/ZcqrHINjYbnE6fBmb2cVqiHfmFXfVgZi+nLbaOKChMsPurT1Or7CIpvBJQR0kRkVXxHht58OACR9vWNntEIiLrtuo1TlrfJMs6e7YbnGaEhkXXLVnPgjVOAHFoyLbYOicTtymenaT/icfI4y3YvUJEZIsydKc74z3OOFpuarOHJCKybuqqJxtjchKGhma/DZdoR77UGqet1pI8yTxh3Kb8xDMU6zXyUN0hRETWwgYBeANRBn6LXRkTEbkIq56q57Zi2zPZOlotqFZn5uhZgmDx5hDW+UXXOMUhW6pz3eQ0FEs54dgUUaOFU3ASEVkTbwzGe5yBYtC32cMREVm3NbUjF1lSuw3lcrfqNDVFwOL7OC01Va8QQrqFsslkwxMXPGQ5YTvFKjiJiKyJzUNM7sgxCk4i0hMUnGRjOAdhCMPDMDlJGCy1j9PiXfUKgdlSFaeJaYhjj+lkRO0ONtxinStERLYob3MwhiyPCJyj1hykFAxs9rBERNZNwUk2Vl8fNJvEAWSLlJxyN7NZ7gW6Faets8ZpctoTxh6yjDDP8YGmqoqIrEqe4wODNRHGOWpJP4GJ8FrnJCLbnIKTbKxSCTodKgVoZYs/ZLEOjXEI6Rb6TO2kYAKPj0LCrVQKExHZ4rxNITDkRBjr8GFOaCIcOpaKyPam4CQbq1SCdptqbGimq68gFUKzpfZx8gDe4eKYINtCAxMR2eJ81q04JVGBMM0xcU5AhPU6lorI9rapwemBBx7gPe95DwcOHMAYw+c+97llH3/fffdhjFnwNTIycnkGLCsrl6HToVowNBepOC21HVgcsvX2ccpzXCHGbKVEJyKyxbm0W3HqhEVMmmOinNDEOL/ENIQNVs91TiAil8amBqdms8nrXvc6Pvaxj63peU8//TSnT5+e/dqzZ88lGqGs2rl9vmam6lVjaGULK05LbQdW3GJrnADCNMMVi6o4iYisgc8thAHtuESQWoIoJzARlstzLD2dPHlZfo6IXHlWvY/TpfCud72Ld73rXWt+3p49exias9mqbCEzU/VKEbTXcHFxq+3jZMOU0lSDvK8Pk1mM5uaLiKyKy1J8YMijCJM3CGam6rnLNFVvOj9zWX6OiFx5tuUap9e//vXs37+fd7zjHfzjP/7jso9NkoR6vT7vSy6Bc3PwZipOxhjWUj8KzOL7Pm0WFydUx+qkw8OY1BKYyzPFRERku/NZhg8M1aGEMM2J4qzbHOIyBCfvPZZMHfxE5JLYVsFp//79fOITn+Azn/kMn/nMZzh8+DBvfetb+cY3vrHkc+644w4GBwdnvw4fPnwZR3wFuWCqHsASy5m2BR+mlCfqdHbtIsgtYaDgJCKyGj7LIAjw1YAos4Txual6l75yn/sOlWCY1Lcv+c8SkSvPpk7VW6sbbriBG264Yfb7N7/5zTz//PN89KMf5ZOf/OSiz7n99tv5mZ/5mdnv6/W6wtOlFARLL2Ri6eYQW42PMgoTdRrDO+EoRL6z2UMSEdkWfJpCEGCqAcFEThiem6p36S9AJa5Jf7SH1DUpBtVL/vNE5MqyrSpOi3njG9/Ic889t+T9xWKRgYGBeV9yCawyES2TqbYUHyQUR8ZoHTiEiwoUk9pmD0lEZFvwaYILDNXnRgjznDC0GBfhLkNziNQ36Q93k7rmJf9ZInLl2fbB6ciRI+zfv3+zhyG9JswwZ8eIr74WH0WYbHKzRyQisi3YtIOPAoYfO0aQdYMT+eXZxylxTfqiPSS+dcl/lohceTZ1ql6j0ZhXLTp69ChHjhxhx44dXHXVVdx+++2cPHmSP/qjPwLgf/yP/8E111zDa17zGjqdDr/3e7/Hl7/8Zb74xS9u1luQc6yF556G625Y+bHbQZhh223GhgyDsSPLx7DOEwbbZK6hiMhmSTv4MCCspZA7osBCFuEKlz44WZ9RMJXL1sFPRK4smxqcHnnkEd72trfNfn9uLdIHPvAB7rzzTk6fPs2xY8dm70/TlP/wH/4DJ0+epFKpcNNNN/G3f/u3815DNkl9Cu79y3nBaZvMylsgzTxhlGFaHU4N7eDaQpVdky2SzFMpKjiJiCzHZx18EFCYahHmljCw+DzAFy7Pp4LZLotpRWTb2dTg9Na3vhW/zKKXO++8c973P/dzP8fP/dzPXeJRyUWZGIe4N67wtVOICjmB9TSjAs1Cicp0g3buqRQ3e3SypBceg74dsEfNX0Q2k89TfBRQnpoG6wgDi1VjUhHpAdt+jZNsPmMt1KZgz77NHsqGaHUgDFMAEmNIS2Uq0w2aW2mHXllofAQ6WhAusunSBMKAwnQLYx0hDqfgJCI9QMFJ1s3kWXfTprgw//bNGc66tRJPMWvgo5igk1I4PUGp3aZptaHiljY9gS5ri2wBeYoPDTYwkFtCcv3TFJGeoOAk6xbkGYTh+Ru2S8/xJXQyqNQnsIUilbOTlEYmKXQ6NHN98m9pjUnQ/0cil8yJzmOrepzPU5yDpFgG6wm8VcVJRHqCgpOsW2BziKLuV5ou+9jtsGY3SWHg2Ak6u4cpjzcITERhuknDaarelpYlCk4il4j1ObX89Koe6/OMwvg0rlokSDJCvCpOItITFJxk3YI8gyCA3Xvh7Oiy6Wg7FKOSDCpnx0mHBrjm2cfpCw1Bo63gtNVFsabqiVwiLTux6sf6LCGYTvDVAqQ5mFwVJxHpCQpOsm4mzyEMYN8BGF3dFcmtLMmgPD5JMtRP39mzFPbspNBo0XS90TWwZ4UxWP1/JHIpNOw41XDHqh7r85ygldIeHMBklpDlu+p573G+R9aQ2gzGn93sUYjIJaLgJOtmbA5BCHv3d4PTRZaVtkoxKsk8caOJG6hQmqpjdgwQphlNVZy2tjDSVD2RSyT3CZFZ5X4MNiNoJJTIMElOyPJrnFpukvHs6MYMdLPZBEZWtxZMRLYfBSdZtyDPIAphxy6YGNseC5mWkaRgOh2oFDAOgiggTHOamga2tWmqnsgls5ajus8zTCvFRAFYS+SXX+OUuQ7W98i/XWdhevvPvBCRxSk4yboFNocw5NnT8PSx89MttkoFaa3SHMJ2h2znTqyJCWam6CX0yAd7L/K+G5xyTdUTuXRWd1T3eUbQyTGh6XbVc8vv45T5No4e+bfrLaTaT06kVyk4ybqZPIcg4GtPeVrJ+Q9WA7g1TNvbSnUqY3MYGCYLLBbAgw+X7xgomyhLoFhVxUlkK7A5JstJq1UC6wi9W/afZu47WN8jwcnZ7tT1XlmzJSLzKDjJup2bqleI598eBbAd94w1BsgtlKvkODIcGPBGa5y2rCyBYml7tG0U2WZynxKaAqu+vGVzsJ60XIbMEji/bMXJ+nxLXThbF2ehPATJ9GaPREQuAQUnWTdjLTkBcTj/9igAu8bzWL9FTnyNc5hCGQJPbgzGe1Bw2rqyBOJVLlwXkTXp2DqlYGD1T7AZeMhLZcLcEaxQcerqkejkLUQVVb9FepSCk6xbkGc00pCDu8y8xhBhAPkaKk5RANkWqFB53/2fLC4QAGlfCZPmYLbA4GRxaQJxabNHIdKTcp8Qr7ajHjNTnfFklTJBZglXWOO0fVfELsLb7rFIG1eJ9CQFJ1m3wObUOgFX7ZkJTTNVoygwa5qqV44NnS0yzT1wjnZcIMCQ9VcwmcP3yhz8XpQlUFDFSa4cmeswll6eFt4OS2Ci1T8ht+DBlsqQ21VWnHqEcxCVwGpNrEgvUnCSdTN5RisN2D04//bQrK3iVI6gk22NK4/GOTpBQBAGZFWDd54462z2sGQpaUcVJ7miWJ+SuMZl+VnO5xi6c7FXNZ3a5eA8WbmEyS2h89jMYzC9s9HtUnwOUVnBSaRHKTjJuoVpB1soEgTz56h3m0OsPgiVImhvgaKOmWk/7usdXH+JTrkFDgqd+iaPTJakNU5yhbHkl62Ft8MSmhBDgGcVwcd7wJCXKxjbXePUSSEw0aJj9hs8Va89uaEvtzbOzlScrpQSm8iVRcFJ1i1ME2zUPWnNC2XIuh8YYQD5Gj4PS1tkql6YNnBRSDjRxA+UoDqAxzAwMbbZQ5OlZB1N1ZMrivP5Zds01nmLIZwJPpbTyZPLPt7j8EBa7gPrCJyjEzhCU1gw5un8DJVwx4aO95GPb+jLrY13EKviJNKrFJxk3cK0jZ05abWFCj5LwPs1tyPvVpw2f6pemE3hw5DC2RrZjn7swBAuiBicHF/9izxyP7z49KUbpMyn5hByhbE+x12mdZeOnMBEBIQ4nzOSPLXCEyyYble9wENoHc04JzYlMtea99Cp/BQ7oqs2dLzTpzb05dbG5d2Kk5pDiPQkBSdZtzDpYAsVAIJqhTy1YO2au+qVo61RcTLZFC4KKJ05S9gHlAtgDP2Ta5j/8dRjsJagJeujqXpyhXFk2Ms1Vc9bAkIC0w1OmW8vX+3yDowhK1XBOSJrmawkFIIKqW/Pe6gBjNnYVuTTJzf05dbGO61xEulhCk6ybmGa4GYqTtUdfbRaDvKcyKyxq94WaQ5R6JzFVopEZ8foj5oMuBF8GFKpT63+RbyHRu2SjVEuoKl6coXpbhp7+T7CjTEEJiL1LYaiAzTtMheGfHeqXlaqgPcY65noS4lNmcy1l37eBqlvZnDSGieRnqbgJOvWbQ7RrTjt2l+l3sghy9a+xmmLNIeIGmewlSLxxBitvTvwYQlfiKg2GqSrCXbeQ9+ggtPlZC2Ea2iXLLLNOZ8TrqVF+AYICOm4OsPx4RWCk8UbyMpVmJmqN1lNu1P1/KXtTtqqedKGZ9P2UvcWoqKm6on0KAUnWbcwTchnrvbv3Fuh2cy7Fae1dtXbIs0h4tYoeV+VUmuKrK9KHlbwUUCUpbRXM/siTaDa1z2ZFxG5BCw5AZc3OIUmom3rlIPhFabqdbvq2XI/xnnCLKdeypaYknf+tpZdfzu8E0/BVNhdarQpnIUg6qk9fUXkPAUnWbcgS2YrTlF/H9hucFrrGqdSBO0tMFUvbo3h+ysELmUqLFAjxkQQpQnpaj6Mm3WoDlzycYrIlcv5fG2b0m6AcxWnYlBZNhcYb8EYfKUf4pC41qZTWiponX+lF9oPrnuM3neXGG1awcdbCMK5eVBEeoiCk6xblCa4Yrn7TblC4OzMGqe1ddWL1xi0LgXvPUHWwZRKRD7ldF7mVFqFwBOlGdlqglNjGvr6L/lYReTKttpz81VtWrsKcVAmdW0is0IHS+fxGEypgqsWKZ+cwEbLHzydtxsyTpdCaagbnDbqfa9tABaMTq1EepX+dcu6mTybbQ5BpUpgs5mKk1lTENrozkoXI8kgyhNMsUhIRjuscDbaR2QcYZovrDi98M2FL3KxFacv/PFFjVlEriznA8HKx8zMdXi88RekF7QBvxjFoI/X9r8bY8xMa/ElGj14BwGEpQq+GBNPtfCFpeY5d9/DUHyAwejAuseYJ1AcBJeFeDZhurS3PPq8Tq1EepX+dcu6GWfJoyJ1n/B83AT8+TVOcy74bcrVvzVqJxDlKRQLBC4nNRUmzTCB8RjrSPI5H8Q2h6e+tvBFLjY4/eM93fVRIiLLOJ48il/lIprMtxmOD9NYrpnDRYiD8rzW4mPpC7P/bbzDYQiLRVwUEDYSwmj5uXMD0T4CE65rTN57bArlIXCpwbEJUxic5WvPoDVOIj1KwUnWLbAWF8Y0yThDEx+E57vqzfncch6CzS8qLaudQph1IHPYgRK4Mi0/CAEY52jPfUOdJqSLXMW92Kl6+w7DM49f/OAFtkDVUuRSS+w0iZte1WOdz+kLd9HegMYLc8WmSO67F3q8d5xNnz9/p+8e7ONCAV+MiRodwmipitP8hLGeC2weh00CqjshawV4vxlzvz1n6zq1EulV+tct62Zcjo8KJORMk3bT0ewap/MfgtZDuMX/4lodT5RntMamYV8fgeujUiziA0NgHW0354O4VV+8QtRuQKVv7T/8mhvgxacvfvBXtJm/s21Q1RTZCKvtQOewhKaw4dWXyJTIXff413GNeW3GjXN4YygGAa5UJGp2iFdY4wTd5hNuHdPrPA6fB/TvhE5tk6bqAWM1r+YQIj1qi5/GynZgnMVFMQmWFhk+DBftqmcdRFv8L66VQICHegc3VCbM+9hdLuEDg3GWztwW4+0GZIsEp4vZU8i5bicmt8ndMbYtnaXIlaMU9s9We1ZifU5AuOH/QmJTnA1LHVejGFRn7zPO4YKAUmhw5ZiwmRB5y/jM8XN+Ven8yEITY/1q9nxYnPcOn4f074bOdLA5U/WA8fr2mJouImu3xU9jZTsIbI6NirRdg2I+M1VvkTVOuYNw7qf3FgwJ7Y4n8I6g0SEfKlOy/ewoliAwGOtozR1zexrOdRNcr+Y09A10p5ptwd+LiGwdhpBquHNVj3XYmbblGxudwjlT9dquRik4v67TeIcPAkrGQFQgsI7YWh5rd4jmPK/Lz3nNwvL7Q63AzVScBvdAu7ZZU/UgCCDXNn4iPUnBSdbNeIcLI5rJcxRsbeZTY2FXPeshOrfIaXwc/tt/gy9+cXMGvYSk2cYEEDYT8sESnZN97IwLEAQY50jcnE/DVgPKy6xlCkPIV3kS0KhB3yBcfT289Mz63oSI9Lx9xVdhCHB++TP07n5P62u6sJjABJwLPc7nhHP2lOpWnAylwEAhJnCWgrOcaOdEpkC+SFXpuUf8kvet1rmpeoP7z1WcNie9lPd1qG323hoickkoOMm6GWshjLEmJPTgzZyK09zmEGfGus0h6nX41V+FnTthZASeeGLTxn6hrD49WxbLCxHj3y4zmBa6U/Wso8mcINReITgVy5B0lr5/rukpGBiCQ9fAqZcuevwicmUYjPYTmgjnV94fKeBSb5RrmFfR8h4XhZRDA3ERk1vCwGET5lWcuhWh7vMe+uy5itN6pupZXBYwsBeSRrhpFad4X5tavppN/0Rku1FwknUz3kEYMLPlIf5cxclAfq45xIMPUv4//jORt/D44zA11b39X/9r+Pznt8yi/my61l2IFQTkQQj1kHgqxBVjTO5ozb2CmacQxUu/WKkCnVXunTJdg/4hKJTUklxEViUwMZYVghOXpuK0mHPrembXOBkDhQKBtcShpTU1PzhZupWqdsNz9hjrrjg5HC4PKA8ZbBLgN2GNk/NQ7HN01rL7u4hsGwpOsm7OQ1zIwBQIMdgogCybv8bpkUdIXnMTL/vjj8Ef/iHccEM3LBkD114LY2Ob+h7OMVmNAIMJDHkQUA0gHTO4UozJLW2zyquIf/ZJKJbWUHGamapXVHBaty0SwkUutZBoxTVBzucE9tI3T5lbLTLe4UxAKTCYuIjJHVFgmR47F46S82MjYuR52HlwA5pD4PA2ICqD66yvQ9/FyjL47kc/Qzq5unbxIrK9bGpweuCBB3jPe97DgQMHMMbwuc99bsXn3HfffXzHd3wHxWKR6667jjvvvPOSj1OW5z0USk1MUKJAiI08frF9nMKQ0Nnu+iZjoFTq3jE8DLXa5gz+AmFWAwMeQxZE7NtpqJ/tdoYKrCMLVvmh/shXyJsJdNorPxagWedvH4Psqacgu/gThyteGIHTqmy5MgSrmKrn8QS/9usEoxu7Ae6F5u7rZJzHBiEFYzDFIsHMVL3GJETBnIqTz8FFnHgK9l27/uYQ3luwIQk51rDi+q9LIXPw2ifuh1Ojl/1n95LENTZ7CCKL2tTg1Gw2ed3rXsfHPvaxVT3+6NGjvPvd7+Ztb3sbR44c4ad+6qf4sR/7Me65555LPFJZjvdQKLZmg5OLDS6ds8ZpdBT27iU5cJjymVPwx3/cDUtXXdV9gcHBLROcgnyK0IAxhjwMGahA0gRfivHOY/zqKki+VOYffuP06qfqeU/r60c4/SdfWMfohShefUMOkW0uNDGWVfy9P/IIweSlPcaGpsB41l2faZzHhQGRMZhSHybPCXBkbQg5H44cOWdfiBg9CsXq+vdxOtdV77M8TWdXsilT9bLM01+fglNbYxbFdjWSfHuzhyCyqEu9YnRZ73rXu3jXu9616sd/4hOf4JprruEjH/kIAK961av4h3/4Bz760Y/yzne+81INU1bgPYSFJpgSMU3SCGyaUzAG5+mGouFhkr6DBDsf71aa3vhGGBrqvsDgYLdJxFaQ14gAH3i8iTAzXQBtuYDJHcbOrSB5MBdce2g3oVhmvPByBvIRXOfaVV+dODz1DOPBEFdtxPu4UoUR2AzYoDbxIlvM3P2BQhORuuUvzpjJOtMHr8aP1/DeYS48Zm2Q2JQ41vk6B0uvxXiHDUJiY3DFCsb77m22e1Fqbje+rB3xve+Hb//D7Du86DF4HM4FlAiZDsymVJxsmnY/K05OXPaf3UuyVV6kFLncttUap4ceeoi3v/3t82575zvfyUMPPbRJIxKYCU5RAiamQIAreFw2Z/pIqwXlMs2rX87UD/7L7m2veQ0cPNj97y1UcXJ0CIwhNw5Pgcx4mnhsqYhxntBecDA3Zv7UsKlxGN5FfTJm+IAhr69yqh4ze5/EBe3jtB5RAdTNSnpYt9lD95pnQNSd7raMYGyCT/ddy/SZ+kVVc+yy7cwNbqZzXTXcyeHSzd1g5zw27AYnU6h0j53eceFQrc/JmhGlKsTFc/uJL70eq5afXnas3ju8gQoRNvT4TVjjlKcZgXG4JJ+5iCMXI3Or/+wUuZy2VXAaGRlh7969827bu3cv9Xqddnvxf2RJklCv1+d9ycZyMDu9LSbEReDSOZ+Q7TZUKqRBjD98eOELXBCcNnPH9Ty0hLnFFiO8j3mxP+dxk5NVSpjMEuYXNG4olCCdE6Ymz8LwLvIM4v4ieX2VU/UAjKE9tLfbrl0uThh1ux2K9CjrM8KZySLdZgrLn5xnzQ6Fqw6RjTUuqgKT+w6xKXUvgF2gEFTJfPd2Y0x3zRU5xjvyMCQGglIFgFLeweXzj+2OnHQmOJX6oLPCspbx9MVl73c4XAglYtwmVZzMseOUTo0St1qQreH4L/Oo4iRb1bYKThfjjjvuYHBwcPbr8GIn7rI+HvqyEUqNEUIMPjbzK07tNpTLZBbixS5cVqvQbAJQiiDZxLX9DkvYTskGKzgXMlJy1IwjKVe6C5zzCw7m8QVd8CbHYGgnAGG5gG0scfCfeb9zeQx5sarmEOsRxbrKKz3N+ozQdLdB6AaVFbrqNdoMHdpHUmvh5rQub9rVNYvIXEJkivArv7KgGl40FRLX5Nz0upAY6/NucwgTYowhLJTw3tPfauIuWHN0bqpeXIJSFToLD4vzx+KXDyLeW2zo+UYrx8We3F7+DxPfbBI22t2Wshd+XsiqZV4VJ9matlVw2rdvH6Oj8zvVjI6OMjAwQLm8+JqG22+/nVqtNvt1/PjxyzHUK4oHWuFZBp57mPjUaQhDXDJ3o9hucModFIJFpmGY87eVIkNnE2damSzBWEc+WCb3EXvcMQLfpF2tYqwlnltdwnQ3uU3nHOCnxqFYxhXKRMWAvL3ESc2//tfzT0LyHBdGxH0V0pbakV+0MNZUPelp1uezwSlcxVQ9c2aca7/1IJkL5lVgTnQem51mt5zZitP4OLw0f3PuQlAldefTTndD3gzjPTacqYoN7AAHA60mtjg/yJzrqmeMWVXFaaX1XNZZamXH41MOU4BOexOCU7tNkOXdzkhrrDh57+f9Pq9U3nvydXRXFLmUtlVwuvXWW/nSl74077Z7772XW2+9dcnnFItFBgYG5n3JxvIeAm/Z/e2TlP/xHyEMFq04ucY08QpzzksxtLPNm6oX5CkmzbEDFXJb4LrmY7zFfpF2qbvAuXTBJdG0EEEy58PROWg0yEtDREVDnizyXo4e7Z7cz51e2m6RVofpHy7TaajidNGiSBUn6WnWz1SAYKbRw/LHy+Lj36bcrOG9mVdx6rjpFSs40J0yFZkSlEqc+tT8Tmfd4HT+NWY35HW+u4E4EA7uxHvoq0+za/KZec93ZJB3A1ZxFRWndIUqRJ47pgYtz6fd4JSlmxOccGAuJjhhGUmeukQj2z4cOQGXZ9NmkbXa1ODUaDQ4cuQIR44cAbrtxo8cOcKxY8eAbrXotttum338Bz/4QV544QV+7ud+jqeeeorf/u3f5n/+z//JT//0T2/G8GWG9xDi6PMxfmAQEy8enIb+4a8pnXh+8ReZqTqVIjat4uScJ0pTSHOCgSLtrIyJQ6gW6VDEG0PpgvbiL8VnFrYcr02RlYcIByrY1iIhaHQUXvWqedP1snqTWjbE4K6KgtN6hJqqJ70t9ymhKaz68eGZCfzQEN6E8ypOsSmtaq+c3CdEQRF74GWMfv7F+a9topl26Ob89z7DOE8WdAORGRjCG8PQCyPc+Nhf49z5oGd9Drb7uFK1u/XDUrpViOWnvuWZ4+TejNwGEJrNWe7YbOHiCOMtNNe2XtX6nHwdGwD3iu501E1t+iyypE0NTo888gg333wzN998MwA/8zM/w80338x//a//FYDTp0/PhiiAa665hr/8y7/k3nvv5XWvex0f+chH+L3f+z21It9kc693GgKMcbhszpW+VgsqFcKpcYpjp5Z9rXJkaOebU3FqdTyFJMGnGXHV0GhVCQoRhTK0XAkCQ+ncAmnvcd7RLLr5l0mNwU9NkJeHCXfvxNYXORNIU9ixAxrnT1qyeouXXhhmYEeFTlMfnBdNXfWkx1lSojUEJ59bomIBfDC7Wa73nmLQt6rg1G1GEdOuB9hk4dQ+5+1sl7+QcxvyerKZqXr0D+MLIUPPnWZ46hRJq/s5cW6a4Ln26KU+aC8zHI/FrFCFyHNHXnKkucFGHrsJh4K4MYEpB/hCDBNn1/RcSza7OfCVzPqcGjqOy9a0qZH+rW9967Id1O68885Fn/Poo49ewlHJmnkI8hwfRoREBEGnu/HtOXkOcYzLLYUzp+Dz/7N7+3v/+ZzX6P4dlCNINul42Xr+KJ2+AWwUEAeOeKJDcM3LMGefJ89LYKB8blpenmGjgDOFYMH8kmSkRrD3KoI9g/jGIsEpSWDnznnBKW+0aNshOs0yrqPgtCZzjyGRuupJb8vd2ipOPs2ICjHeG+zMVOncd6iGO2YaO8yXuibTdoyd8dWztxljODN9lvDAMC6HYM6Zg/UZJfqBmal6rtld43SuYtA/iI8jykfP0h44TLsO0c7SgupRqbr8Gqfu2q7lT1ls7ugvTXPQOzqFXZtyKAjbDXwhxBYLMLW2vZysz7CqOOHIyUw34BuzdHt6kc2wrdY4yRZlLOH0NMmOfgICAmM5NxtjbizO4hJhaxomx6E2BeNnYXLmg2WmUUIpMizVT+FSc9/8GrX9+3BhSOQtQSfl69/YzXTdY10BAkOQzQwubZMVIk6GzF/j5D3tY+OceeMkT+2rYxqLnAmcqzjNmaqXTzcZuGoHIycrBLmmmq2JsxDOXIkOYzblMrPIZbLW9R/eQxQFM1P1uv82Om6aUjC46ONbdoqR5NsLLmoWp75F/yvqTFww23ruvlLdqXs5xs+pOFX7IQ4onJykubef9jTEQWlBu+kwMstuYefICU1h2RbjeW4pFdq8PJxgomg3ZdZu0G7hKjEUApheW6MH5/MFnQevRNZnJCbYlH24RFai4CTrZnBE001GXZXAQ2BynPUz952Xe0P01OPw+jfAD/+/4fd+E/72L7t39vdDvd5tDrFJU/WSdk4pTbBRSOgtYZKyd8cOxptlwiwEY4jOTQNLE/I4pG4KC07Uk8mUymBIODRMkC5Rcbpgqp7rpAzuqzBpRjBOJ/5rYudcAg9jUPCUnmYuuAq//BV5D0QGIJjdALfj6pTCfqxPF6ypSVyD/nDPzNqlGdZSSscZHjjBia/Mf/3IFOd0+YuxPgEP9ly4MwaKMeFki7wEzbqjFPTTcdNretfW5xRMZdl9q6xzRMWUawpTTJXtphwKwlYbO1whjID6ylMh57I+I96sK4dbiPM5bROQ67NQtiAFJ1k3E1jieoujY/0EUYQJHC6fc6Vo5sql9XSrKa95HZQr8J9+sVt9AbjlFvjGNza0HfmJ456jL6w+hHUyqLQa2DjEALk19JuIdtBHlBowENqZ8aZtOoWAugvnTxWjO08/C87wyGhCkC7SBWqRNU7OeeLI4A88B0ZXHNfE2u7Gt6CueiIXcA7iADzn1zglrknRVDlYvIkz6fxOd5nvUA4Hyd35tTbHf+eTlKemwCR0pua/fjHom92QNzBRd42OBxecr4q5apGwmZAMV2ifrlEKBujYGmvhfEYhqCzbPMFZiOKMXVGLRtFtSnAyWUq2c4DI2DVXnGxnmr2f+vwlGtn2YV1KGoSkaL2XbD0KTrIu3TnIlvxsizG/ExPFmMDhF9t40HvMz/w8xPHC+171KnjyScoRdDaoHfnTT3mOH1v9a7VyR+wszgTgwbiIYmzwUT9R6sAYwnOfxGmH6chg/QVrDYwhS2GqldPft0Tr3HMVpzlT9ayF0tBpduTPYTQ9YW1cfj44aR8n6WG1/PQity5zjMsyXGCIwhBj/WxwAo8xwczUt4X/XiJTnNekYPKhRyB15EGIueCsoS/cRRxUAAhMSMdOAx439/SiGJMN99HZ1UfjxASBCfHnpqTNDP/st5d/K5acyVp52TVAzsJVjRPsmxqlXXAbdg0lt6v/HAmcxQ32YwKDnVpbVc0cfYZgE1qobzXFP74Lbwqk2stJtiAFJ1kX57tT9fLUc+rlIc4HmKC7IBlmPgfnTis5/LLFXyiKutNBNrAdeac9r6izotTklNM2SaEIBryDwSHD8M5+fCvHBxDZ88GpEUHEwhDojQMfYJY6C1ik4pRbz85r/pL9kyfwS3UT+r//79W/mSuJzeHc1e1I7cild51Knphd99Gc8kyNLn9C75sN8igmHhwiMAF5a/4BcamF9xcGJ1sukTpLbuge3+f82P5oD5VwaPb7tqsBhnzO6YUrhHT27IDBItn02IKfVzsOD/1fUDu24C6Od7rNoKzP+ftHS3RcY8l1Ts7CdWPHGJqaJo82rnJ/932rfy1jLVNxldBZ0jVuLRG89ALpdddBemVXWoJnn8OYgipOsiUpOMm65A5M4Mk9pDtCmi3T3QD3wlW+jWlssbzi64WBYQ0X9zaO96RRzuDYGWp79+CCAGdhcBB2DPd3W/2ZgChLu4um0zatOKZkzPylvN7j4pzARnTXHizyZtIUyuVumWmGtYaCt2Slffilmhv8xV/AxNq6NF0R5k3Vi9VVT3qW9RktOwXAw38Jj9+3wuNbU+SFEuHgIHEUkNdWsyGrJzKF88HJe4YGuv34Jl1Gobr8vq4dVwfvseGcDnjFmJE33URxMKLZHjn/k2YS2MgRuPn/mzB5dOHr1fLuFhatNGNsvMzR2mMLphee4yzkhYhinkPo2YglMnmW4esnV/14k1uyXWXiJCVZ6+yJLCG//no4+vQaR9lbgmPHaZqQRBUn2YIUnGRdrO8uOXbAVcUqjxzNugWmmelSs9czH/gSJ1/31uVfbOdOGFt4NfLix+ZX/8HVblLrjxgYq1E/sBdrQqyFgUHYNTyAyVN8FBKlCc3MQdqhFccMhSGdc5dfvQdjsHFOkMcYG3WrT4u54Eqvc55qsoP+4RsJ8mTBuikAvuM74P771/AbuELMnaoXhCzbmktkG6sEw7NrfFy+8jWCfHqcsaQE1T6iYoSdbnY3nV22oYSZqTjNXCTKMkxkcBhamac87Gkvc/0mMiVwjjQqzt7mB6qE7ZR4R4FGo3uMD0xMlufERZg+67lv59+TV+efKHvvadlJAMZqGZyt0qpHpL5N5hZuhuss9I2OsX/8bHc/wQ2Y9dZuNDicrWELFOfo7OkjShOSNZ73ewPZ1dfgX3hybU/sJd4TTEyQEJKqNbtsQQpOsi7d/Zq6jRNKp/fSHuh+mPkLm0OcGaG9Y//yL3bTTfD44xs2toePe545u8rgNDnG2M4q1fFppg/uwmPIXcDgEOwbLpBm4OOAOE2oJflMcCowHBRon7usmXSgUMRGGY2piLQdYSMD2coHf+88rjhNed8hjMkX7wx31VVwcvVXPq8Yc6fqifSwQlCmEg7hvZ9tJBmYaCYMLWSbk4wlJZ44W8Fbg6s3Gc9eZGf8smV/zrmKkyUjSjwu7DZ7aMURUbWxbHDqC3eB8yRzghM7hym36kTDBVx7HIBKMEQjnaJUhYlykxua00zfOD7vtVLfJDbdmQqJzYmzMp2jr+Fg8SZG06cWvl8LN/z5PxCMtSgFGW4DChbtRpuqG1/5gTOMc7SHB4lth2yZfSoXf7IhKvXhFmsqdKVoNbHlErkLyFBwkq1HwUnWxXq6Fy8jxzefK1Ae7u7h5C9coL9YQwhgquHP7xfy8pfDCy9syLjy3BGHa1gvVZugPtRP1GiRDFfAGRquSqUCO/sMqY27FSdrmciybnAKY3aGRVrnTlo6LfDQHiyS1IvkrTJ5MYDWCgutvMd5Tx4lPNufEIZuYdhKEigUFlSqhG5wCpffGFOkNxheVvoukiYUKxCXwOQlMr/4iXbemMAXK/zJ18pkucE1WiRumnK4+B5O1mcEJsLMdIDIXIc4hdAntAslXKHIVOnsssFpb/EGcJ40Kp1/3T27KTdbhHjyoHtxrRIOY3NHsQrHDp7l+HQbV8nm7R/VslOUw0G896R5xtt3fYmpF6+a2Qh34bEwGhvD9ReJvnKcSpBsSMWp02oRrWGtjXeeoDxEYCy5XXv1OzLF2bbxV6R6jfbBPUTTqZpDyJak4CTrYq3HAEHgad9wmnKfJe2AW0Vns4m6555vlzlzYiZYlMvQ3pgrbcdPw66Zc4MLN3JcVH2SVrUfrCUIwPoAZ/swxhDHButCCAzG5YznGdiMPAoZDAq0zwWndhOXZDSHythmH3mrQlY00JzTWam5yBlHluIwFMOYPC5hAt/tbDHX2Bjs2nVxv4xe5+z84KRwKT3MGEN9HAZ3QWUAXKtE5hY/btrmFKZY5cd+aADvHH66xXLT9FLXomAqM995ct8hTiDyLdrFMi4qMBmMdYPTElNiB6P93eAUz5mqN7SDOE0I2x183D0ex6aMSfooVTwjfQ32hjm26OdNP+y4GtVwF46cTuI5VDlOZ5l9/sx0E1suEEx12GnrLLNX7qql7TYuWHl9LtDtKOQgKg4ThI78IpJbaAo9H5we8ac445do1V6bonl4L3G9s2CTZJGtQMFJ1sUmKT4IKaYdvqP4GAPlaWxm8CtsoPE/77d88euOf/L9B3j+sVMbPq4nX/Rcuy8gis5vFbWs6Snaff14ZwmMJ7cQmurs3Z4QH4eEScbUnK5tg0GRjp85gWi3yJuWdKBI0VVJpyvditPc4PTYXy382VmKNYYgDLna7MUXgOSCE6GzZ2H37tX/Aq4kmqonVwDnHWYm9NTPQv9McMob5aVPMJvTRLGjfygEC346oxwMLPkzMt+mMCckTNuzRKnHu4S+UkYeF2mlo93g9JFfWmKgDjykhfOvY0p94ByVTocgdHQaHmMM5bNvYNeXP0Exr7OjUKI+aEmaAW7mmGp9RjGokvuUTht2FkZI3TIXwtIUYx1BYtmVTW/IzgRZu8XkZHnxdacXshnGeeLyACYy+GwNFZOZ31vxwYdxS3VW7REnmOYMSwSn6RrTV+1jYLJFroqTbEEKTrIuLs3woaGYpVzXOk5SjHC5md8Ses4Hzqnx7n/Xm556y7PrFQdIT87fm2QjCgbHznhetr/bZK2zmotWnTa2EIGzRDiyHMrFvvNvIQhx5QJBklGz56dtDJgCic+777HdIG1a0v6ISliBVj95yUBrTnCqnV74AZwmEAZgDBVK+DhYGJzOnIE9e1RNWcyFU/XWuq5AZIvz3jOWPU9kulWc+hgMzASntD6/4nQmfYZa1r0YZVodhgs1Bp79GOVsHF8vMhwfXvLnzK04neveV05jLI56cYCBuIOfGsM3W/iHH1r8RfIcnCcvnN/jLij24UxAX7vF4aPP0PzEXQBkjYjCxDH2n3iWRlbCF6FVC2ZbrkN3vZX1KUniCCZasER1DSBod8B5wsQznE2TbMBm4s2JNqfP7lq+leAMl2fgPVF1AEKzcMr6cjotfBgR//XfzLRb6k3Oe3ZRobbU9MfaFPWr9jA42cQut7GXyCZRcJJ1sUmKCQMC5+hPHaYYYl13ShvQPYnNMih257v/8ZcsWe7ZMWD4/7w7gr37KU+OzHtNv9wVxVXqdKBxPKUYQ6Ox8us576m26xBFlG1Knhp27+yfvd8EBlctYjKPa4zjZ678FoOYvBBB2oHpGlkbsoGYiikTZhVsIYDmnDVOrSmwF5TAsgQfgiEkJsQFwcKpeotUnFLXpm1ra/q99KQLp+qJ9JjJ/ASnkidmg9P0BPQNQ3kAOlOleRWn1LWYmmnh7W13GwUG9lCljm3lhObC9aZmdjpz6luzm9nmPmFf4ZWYpIPzjsmhPcRpTjA1gTn1DMdH9pDd8X8uvFBhc4z3ZIXza5yioX0Y5yi1O3zhf//fGX+pO2U5aUKwbx+7jz/H/adjKAXUJ8ycTXkNoSmQ+xTTaFL+f/6GiKklf09h1ukuvC0W2NmYJDfr/yyZGknwfXugs/TPPaeT5njniSoDEAVr21Ou1cAHhuDUqSX3qeoFGZYi4dKRqF5j6uAeOk/WyXURTLYgBSdZF5dmGBw+MAw/fYy8GGLIZ8NB1GpAFMLgEB548iXPc6c81+6fqZz09VNM6mTn5q0PDRE31h8G0gTOZA/hRhzjUys/vpPCYGsKH8eUszZZGvCqG88HpyAAVy2BdQyefgq7Yzf4mIiYrFiAThPqk6Qtj+0rYogIibGFEFufU3EK427IumCwLjaEJsIYg49CfOuCaQy1WndTqTmadpx6Pj90XpHUHGJ7WmE6r5zXtGe5uvQGikG3Cu4dhJGhMgDtesCF+8UVgyqpa+G8wXSbnhIEhmyRTfJic765hPN2pvECXF95C4EJodPBW8vk7oNErYxCbYLRrz5Jp7CD/OjJhXvLWQvek8fnp+rFu67BFyN2PXWM/EAfJzvdYNBpQjBYpVKfZLqWE5cM0zUzb43PuYrT3ue/SXB8lFIwueTvKcwSApuT7uxn/6kzZMH6A4i14MIi2JUbRKSdbsWpUB7ERAaz1J58i2k1MB5M/yC+h/eiS7EUWGZqdbvFZF8/wXiHp59ScJKtR8FJ1sV2Ugw5PgoonTxNq9JPwbQJZvrAlibPQqUM/d159bsH4YuPOF55eCY4GcOeIcOzJ2cOkIcOUTmzvpbbufWkCQwOjLCr+Qzjq8hhzcwx1KjhSgVil5HbgD2Hzy9uNiHk1SImtxx66dske/cBBWJistJMcEoT8laGK3enqBRCg4sj8sac6lFpAPILglNjGluJKJju82y5SPvMBYP2vpve5khcg9SvZkPLHqc1TtvTvX+02SPYNgwhw/Eh+qL5DWLK/dCec13Ge48HdsRXMZo+A4npVj4wBB6yOTPAzh73TJz2DET7qOejC35mHMxUjJIOpCljB6+h1EooT01ycPAkhw6eJSkNwuQFQcZa8Mybqlccvorpq/Yx9NxJ4jilFnQHkqWQtD12T4Vbn7iPUtHSaRuct2SuQ2gKhHQrTntefBIaHa565svdLqOLCLMO3gTYHTsYODtOGm7MlLfMF/D5ysGp08nAeaJCBR+GBDZn1a39Wg28N3DTLd0ZBj0qxS0fnLynk8OeHSGRrofJFqTgJOvi0gzwRM7SKVZpRFWiKMHYFLynODUOhbi7k6xPue6QozL8PKXC+bU6u4cNT5+YCU4HD1IdXV9wOnEWik3P4S89wrVjX2KyvvJVq1qes6M+iSsWCLwjy2P6zi9xIgggq1YwmeXgqWdJhofBx4SE3YpT0uoe8DM7OxUmDrobGs67eFgeOF9xOjcNoTGNrXarVwB5f5XmiYUnMvOe87W/w02MsvxGllcIddXbnk6/0A29sqQTnceWvO/v/THGTWvmkND9m899QmyKFIIqsSkRJRX2n3oWbyIC/LypT6efhZNPQSkYoO2WvrrkWy1MktHed5AotYStDn37GkBG2soWqTh113xmc/ZxCqMi09VB4nZKodSelyVqjZzKjgjKAdYkpGmA9SlNO05fuHO24tQ3cRoCw6HHP0022v2ZF3ZMDdIUHxrs8DB9Z2t0ihs05S0ukDVWrgK1at31XVFcwocBPgDaSzRBWPDkBsY5gltuJTy9xPG/B6xYcTKG3MHOYUNfX7dzr8hWouAk6+LTFGMcUZZRu+ZGotFpwtDhQ6DT6Vac4hAGhjDhS7z85Ud50y0vznuNOOB896ODB6mus+L00hlPqdVhKE0Ybh9jcnrl59RtzvDEJLZcIsSRZAXC8PwJeBh60sE+wk5KbgLaJiMwRYwxZMVSt+IEtF1Gge5JfBwCc4OT993glF1QcWpOk5WLxDMfJnn/AOnYEh+cQdC9ovvCU4RTk4pNMDNVTxWnbSdtQ331G4teiabtmUVvd4Gj/bUHqc9uENo9ucx9OrsOal/xlfgESnkLGhnGuHnHi8YkTI5025svdhw5e8zzv/6HJ51uY6yFaj+xD+jPGlQP1EjbZRJfma04eQ/H/pE5FafivNdrVQYI0oxS3O5u75DnBGmL8TxkV3OKdhCQBS1ajTJtV6dpx6mGOwlNTO5TCkkLP1Chf+Q0k09NEZoYy/zpnmGe4KIQW61SaLTplNcfnAwQlgq06ytXnJr17rE9MhGEMSb085sDuRye/PNFG9j45jTeOqaqhzCd1e8btd1kWCJviAjI/VIVQc+RsRBjE83olS1HwUnWxSUpAY4gtxzJvouho+OY0OGKETSbFCfHup88A4OcPdEhHnyKYlhc+ELnPkgqFcILO8qt0eS050D6DcojZ+ifGqW2iuYQDZ8zPDqG3TlEiCXNCvPuLxhDc/cOwlbCsX3XUnMd+k13Ksvc4NQxllIQUSxCHIEPDHl75sM7a0N1x/nmEEHQbUFbmyTpL88Gp2xoJ256iakaAwMwPQ0TZwlqagwBzEzVU1e9bad/B0wtHgwEnM/p2Pqi9yXFlJs/8imS8fkXWKxPCc35Y5drJ/hiRDDZxOCZuxYq7cydRbYwOj36RXjLv4SXHu1097ALiwSFkJ1ME5pJrC+ThpXZilM6Dc/8BbNrnFw0/zjv+iuQOuIgY6K8F06fptAcY6JcpZQnNCkQ+DqdTpHENfA4AhPObMbriZI2yf4d1G98JfVnpohMgdzPDxhhluADgw8iAu9Jiw57EZvQzuWB8oChVVs5zCSNJhiICPHBTMWpOafilDa6r3j6Gwuea1tThLnhngf6gJkLZD0oxdJInqTkA9pLtF13wJGhw/SfOkWa6nguW4uCk6yLz7rNIVwYMl5+NYNjNWLybmvvVou43cTlKfT102l7CmFIORya/yI7dlFpjpHPlOTXe96bWzj8whd4sPidlJpTNFdqR55ntEJPtdEmHeon8pY0nf+hH2NISxU8nm/ceCs112FH0H1MVjofnLIwJ3IhAwOGasWQFYvY9swls04DSn3np5Kd2/B3aoK0v0w0M8UvH96Jb00tPtaBAajX4fQo8beeuZhfT+9xVhWn7Whoj4LTMlLXxphg0Q28O5WEwtAuePzxebfnPpmtOAHYdood6OfEiUmcCQndBZfvlyhZO+splGBwjyHtQAFLVEnJyjGn8wFKz7xA81VvwhcKMDUFQP20pznK+Xbk8fyLT76vjE8dRXJGS4fg+HGi9hStvpA0jHl1Y5LDjRdxxW7A83NCnscTpgn2upfRfs3VtI9NEpkiuevQcecrOkGWQhhgkwZBbmnHhk5rfQEkCHOGX/UNWquYqpe3m3gCQgIoFPDh/O0optsv4ne+AloLK60uaRBlnhdOV/BxtHBLih6RYjnRmcblljaLlJO8xxt4/cBxCvUGSQ83ypDtScFJ1sUmCYGxuCjin95yAOt8t8teKYZmkzAA68AbQyeBlxXeyoKJIddez8uT5zl2pvtBGaWddV8l3HH8CH/0r/4Z3rqVS/1TE0wN9VGZatDZPUyII0/nt+wtGoP1AS4O6K9P0vSO4SDidDpKHhfwnW6ThjTKifKYnfExdldGsHER25o58CdNmLM3FH190Gxi63V8X4Gw0L3P7NqN6SwxL34mOJ05USccn1j8MVcaddXbnvqGoTG12aPYMiZOeZ59eE5FyDcpmMpMGJofQmifIPqet1B46tmZG86tcUrnPdZ1UlxgeMl7ksAQ+aWrJnMD2vgp2DWz3ZPzUPIpb3jxi9hKH2GS42stwgMJpWCsWzUH/jE4jhvOwFmMBxvPP4YGcYSxjnKeMnZwL/nzxwjTadKKIS3FXNUXcdOxp3BFS8tOUQmGZ5+b+xST5TT39VNoNQibUwxE+zmVfItnmn+HnWlfblyOAaZ3DhBPtmhGIa3W+qa9VftOMzQ6saoA5lt1fGg4SQNXLnXPsOZ0SG20X6IVLv6BZH1K6EJOnwm7F4I6vdn4J8XRth0ms3TpipN3vLw8SdDsKDjJlqPgJOvisw4Ghw1DBq+1eBdiDNhCCM0mEQ7nPK2GJzGwszrTTnfuVdSXXctVred5dqZBRO373kXnzz67rnGZsuWVr36J1AJ+hZLT1Bi1wQHiiQaN3UMY7wkvqDhVYkPqI1wUs2P8LE2Xs+v5x5i4/48phOH5lQYmJ7JlhuzT7AqeJysWsO2ZD+6kAXPnrvf1QaOB7SSYQoSd2U+luHsvYbLEh+bgIExM0LY1SjOfx4tdkb6iXNhVT80htr4shbigaZVzHH8SHvtbaE/P7KnkWhSC6kxwKs17bN+xR5m46bvotLOZyNR9jr0gOPl2G18MGLL9tANLaLNFf+exKZHPOU6OvgB7r2HmNcFklqu+9CB2117yq3dz7N230nrpIUp+pPt6E+OMd0aJXpHMVJwcPp4/5iAo0i5VedVTT9E43E/n2BhRMk1eDHH9EQPfepaDjz6Fiy0D0T52xFfNPjckAuuov+wA5WYd35jC+JC9hVfwisrbGM9eAMA4S/XoGdpX7SYaa9AOQ1qr2gF9adXpM7zsK/evaiN105zCBwHfZhxKJTDdjdFn30eWMR00Fn2u9TnYmEIMLooW7uXXI1Ise+47Qn1qavGKE1DNGjTMML7RorOWTYRFLgMFJ1kXlzYJfU5aKPCn+XGMCzB4sv4SjIwQBgbr4annoBJAHJmFi3pLZSqNceozWSF4zatJjx676DFF9QmiIcdbTj9Ha7BNsbjCAvSpMaYHhwjSlHaxgDdQ9fP/aZSLJfI8gMBQbnWokxI9/1Vc/SRlY7oH9zDEG4tJixRiT7XQJo0r5Oc+cZMG3P3/O7/MoFqFRoO8nRHFMH7sa3ztwRb9/TsJ/BIfFv39cOo4E8UdTDznCE1hwQLpK442wN1+2tNQGbgkL+2957nW31+S176Uamfh+jdAc6r7feKaFIIKme8Qm/kXcqqnj/EXxcOc7bh5F07yC9Y4+aRFFsfsLpfJoxAbGGidvyhTLEOn4YmDMqk/f6I+fgJ2Hpz5xljiWoNTr7ue7PBBDj75LCcqB4mjGmZnCb7xVfiHL7Pj8eew+zv4LAEPpjC/ShaYIq2hIfa+eJLqy3Oe+xJESZ20AC4MCEYm2PGVZ0gjz77iK2fWNnX1RXsgd0xec5BCs8X+69o88pcwGB+gGPSRuu578t4SJhnt6hBhIyELDJ11NlrYdfZ5So2p83sNLsNPT+OikDO0oVwlNH7e7zvKMvI4hqjUXfM6h/UpeR5z8BBkJu7Z4JRh2f+VJ3ETY0tWnHbmE+y7+0GydsK0ttyQLUbBSdbFZ20C48kLMYeDCsZ1ryInuwbh4Ydxg0PkHkZHPcW4e1JTMGUyd8GHwnfeys7nHwZgoGholfq6TRAuwo5nv0Hr8G6O+gg7WGa3e275J0yO0xgYAmtpFIt46xlm/ol4pVImtxE4R+yg7TPyfBpjoBQEJPUp2LGHwOSk7RKFyFEqWNpmAO9mrjB2mnDmdPdqO8xO1ctaGXHsmersx9aO0FcexJglpoX09cHkONYUabYgXux3eaVxTvs4bTfNOlT6uXDj1o0wmR/j6Wxqw1/3UnMW+nZAa6YfhMcRmSKpaxIF56s3znkCZ0l3v8DI9ddSOfXt2fvONVQ4x3TauEKB8r4d5KUK3uUw3r2QFDUmuH70Lzj1dycomDKpa85Oo/YegpmuolE1BeeZuOFaWr5N4WydV7Weol0tYg/tozOdk58+wZnGaWpDLXyS4J0nKMyvOBXiCll/mfJEA7O7SW6hHDbJ4pS04Wm+8y3QzmiWMrJk/t/FjugQPodk/34Cm1OstKif7a7FMjMV5padwpsc4zzFegfw5JGhk60uODXtBH6RLm9BkhIkFmuWnz6epZ6yq+PCkDoJptJHYPJ57chNnkBUhtIgJPMbfzgck7bGgVtfJImrMLU9O05m3tJZ6sIf3X/xYbNDPL1EcDKGvcePceCxI1Tak9RRcJKtRcFJ1idpYQzkQYQBfDhM3E4g8JDndG58PdbC2LijWDBkCcRBhdRdsIbndbcweOJJvPcMFA0jb/on8KUvXdSQhp9/mPFXHCYv9uH39bHr1FPLP6HTwha7i7EahRJBbolKg/Me0lctdrcnKcZUsgbV48dJqyVCQgp4bH0Kdh8gCCxuOic6e4p49CVafhjjZ4JTsw7f/f1wdmZB/LmKUyulUIC27adYyImCQQKWCE7VKu7MGVy5gg+7m1RmK01F7HXez5+ep+lfW1+rPlNx2vhplS07xVFf4bTdfmsjKoPngxNAZAq03RSxKc/elnXABp5rG99k5JZXM3z0K0tvh9Vu0qlWKL/8BoLQQ5bA+Djee/K/vYvh73s17k//jDio0Hb1edWq2TGUW4Bh8sDVTI2fpr1vD1PFIlM79tIuOCY7O2ifOclgMskLe5okY90LOYULdi/tq1ZpV/spTTYJCpO0mlAqO4q0CEfqtA71Y4OAbE9t7uy2mTed4r1jp21ggCRpc83ru9MbP/VRh/dwJn2WvF3GFWOKU9OAwRtPuoqNawHG0hfI/cK/mbjdpPDSSXDLX8hr16FEEx+FlIigr48AP2+NEwDG4KMyZAvXsXZsh6F9Hab7d8KZkVWNe6s5TYPHWGYfqqTF9I4hwuYkbokLJ31npyj6hMrZSZpc4RcGZctRcJL1SZuYaGZqAVBgkDDNIHDwn/4T6Q2vxnqotTrEcYlOA4pBlfTC8nsc0xdbxuswUISx3VfB8eNrHo73HnP6aUavOsRrv3GE/qzB8MkXln+SMVRNC2xOGpTIrSEanB+cCtUinbyA7S9Rnp7k6ke+ydj3vIEiJSKT4GpTsGsvQeQoZS2C6WlIMzLXj/FtyDNoTMM1r4J05oO8r1tVy9sZhdiRU2bfbsdz3y5hzMIPlNynUKmQnhwh37Gb1KniNFfiPM11NhWRy+RccLoE69ES73h5WOGRdPFW3luW6Rbh5genIm1bI56zximpZ2RM8cbTJyiXOhTDNlkrwPmFF1uCTpOsv0ohnaDkUqK0DePjnD7jOVXrEN5wHT6MiU2Jtp1a2IQCiItNvIGJcCd9PuPh7/4BngxeBuUCEzaHErRHXuR7Tj5Nbsdpj7TBGArB/P9vhyoDtEt9BJln3/QpEgulCsSmTXF0mtPX7sQGIYXqNJ0LM0qnQ+A9A8kEhIYkSdhxACZHPNNHvsjJZ1137A5sOcY162AMxTAl9U0mspWnfme+s6C9OUCUdCh8+0XKo8t/jrRqUHLT5FHEHqq4/jKBdwum3MWmzEQtpDU+Pzh57/BZzNAwNPp3wej2DE4dLA9P1znbXDwUVZ57honXXEU8vfS/z0KzRTBcoHpmkvRKn4ouW46Ck6yLSZr4MMCZkL5OCx8ME2YWg4OBAeI4wHrIo2nCqEp7GmJTmZ2TPteeYXj2pGegZKgn/qIqB+OjbcK0RhiEFKfPku/tZ/DsiRUbKAwkkziXk5SK5DagODx//UWxUqSTxfhqkWJ9isnh/UyZDqWBvURTI9hOG4IIWy5QyFtQ6u+uqrYlfJBDqwHtFgwMnX/RoSGo1ciTjFKYk9gh9t3+f3Dsxe7dF54IHW1/BW8M+VSNbHgvGAdJURWnGV+uJXytoQ/ZbaF1bqrexpv0OfuiKo7tt4loZbBbuTinu1dRylPHPf/1D3NGJz3h//krVHZmNPN9DNkJXMmTtWOsX/i3b2xKmRRjE4q+gwk8/vRpnvjaNP0HdjA1s8ddYEIS1yAyRVp1T/lc88+RU3R29OGDABfv4uwN13B8Tz9Tk0N0rMOWHMFQgZHvvIHxdB83NZ9l/GgLb6B0YXAqD5CbgMB7BvNpDr7SUywZikGHvudP8vipm0jiIv12nPac4PRII8W3WuAdYamMCQxp2mFgJ4y+lPH93/4IrZqhP9wNpHjT3VjVRyF7OhNkhbNMZStvqp4vEpyc9RjvwOb0nVh+yndrGsK0iS3G7KVKNlDpPrd9PjglCdh2P88+Yzn7/PzPQIcjz0KGSxUau3fD2e0ZnOp5zgtjhhP1xS9ihbVxGrsGKY5NsOhUXe8xuSOshhSmmov+XYtsJgUnWReTtiE0xM5z7UtfxQ4OE2YZzJz0FwKw1nNq9wSpH6A9DYEJ5u3Rcc5g1XDyjKUYQicHdu+G0WVK/ot48S++wvTV+3jZS89RKofkgyWKaZ3GMkUZ7xyDIyP4SokyLXIbUt4zv+JU6i+RZjFUS7SG+xl99TXUwpiRN76Joa98DQ+4+jRZpUzJjkFcgbBAYEu4MJsJTu3uJdZg5p9dpQLN7lz/yFgOnrofPzzArqf+Ae/NgkDUtjUS18A16mSDhwgjQ2s8XvQq6YU6tr7oFele8q1WRj136qq3HSRtKJQvybTKSZezJ+yDRaZdbVXed/ctagQnZ5dAAoSmQCGo8PhRzwfeEXD8rCctDtHfqXPsH7/NYFanU3BkjQL2wvfrPYHNiEIwV99Mweb4Ykw6WePUY6d5zZsP8PRxj5+ZLnlt+Vaq4U5Gj8Lea2de4+tf4ZlDO/GFiLCY4Pbs5+Ujz0AWEhAxumOYcIejND3OE8l+CpN1Ou0OYBYEp6hUxcYxPnHsak9R6y+SnE0YTiYp1dq89sH/RbpziKuef4baTGbInOfPx9ukzWa3erPjKkIDeZYRxobmUy8wfOxZeOll9Ed7MaaDMwbrI3yhwMH6KK4ySWjmt0ZfjCFYcCxNWp4gT/FhQDSzr9JEtvhMiNa0pZBPYwsFrnnuKJ3BfkJvcXPbkU/DNx7qp2VyOpMXVJwAmxgGi2Vae4dgYmzFMS8lcx0mspcu+vnrMWUz9kclznQWDzxhq4F7doRoapLmIh9JzlviiSb0FynZZLbV/GIy11n2fpFLQcFJ1sWkbUwQEHlPX3OS1r4hwtzOng8VjCMnIK60+EYes9T2RADBy6+ncOxZJs/Nb3/b2+Dv/m7VY3noSUfthVHykmHfyAkmbn4HYWSIfMpYbennZViGXjyN3buD/qSGzQ3FwfkVp7hSJMliKIR4l2GikE7cxyPVFNPpXg1tnR4nq1bYdfJB+K5/CmGRfix5IYTmTMXJxFCcmQ4zc4JvvSXKc4Zrz+G+9w30H3u8G5wuqMqVgn6adhzXbOIGDmPLfbRfWt2H66nkCdpumV9CDygaQ91qfdP24M9fQNjg8JR6T19QxmyjK9WtVgde8wDPTH8VH54PQAVTYTg6jPdwaLdh7NlR8tzR3DfMi3E/lTOTTJdSkukLOpU+8wwkHYzPyQpF6N9FMUsJQkPbQjAywoGb9zE65TF4vPeUwgHioMTICzPBKenw0ukzRMbjihFhcYrdhIxefzO33vY97IwzpvsKvHDgPZSPneLGs8/zhl/6AzpBEwJD8YLgRKlCHvZTO3SYV3/tMcb2DTF1os7uxgQ2C9iTjpMPD3D48aeone4+pWYdpxJHZ7KDwRPtfR1h4LBp93dkjj1NbfAQe7/xt9g0gDzBlmKyQh+uUmTvyAi0BgnNyl03I1NcEJyaTU+YpN1j+Mzxeix9ftHnN9IaBVvHxyH7H/wSrWp3jZOfU3Gy1pNMBlBOMBdcyPI4cIaBb38VW4xwNiftXNy/jcQ1aNnJi3rueo1nOSkRY0tsoBg2mwx98yUOPfgtTi2yN5bNm4TjLbKdVWKXky9xwa/bPfMBmnZ7NtGQ7UvBSdbFdFJMAFEAcWmI3eHDBNbhZ05gi50Gabkf46G/FFCrL/NBcNN38O7yo/zdkZkS/759MLL66QpHRzwvL0ywe/wEtdddTXXfd+KLMbHtcHapnztdo16sMHRslHzvTirJNCkxpXKI9/DYJ7tdY02xgs9DglIBl+ScSg/w8v/2uxw42eLM//Z6yHPaJ49hS314Y/iD1GLDmMG4TR7F3Xkc7TZgmGkvODsES04hTYmSDuzeQ2jbOBdSy2feu+tWUcrhIG1Xw2UZgzuHyfuGSF9ceQrKuZ/RtlOr/l1uR6XAkDgFp+1h5qQ6LrDyDtVrF5kihu1TcWo02ySlGqdGBqDUmGkrHhMHJXbEVxMEUIwN5ae/SXN4F2HR813PPkblW8dpRgmdqXh+Y4OPfxzu+hOMt+TFMhQqhNYShJ628/TVRylddYAkg7ga0hw7f9U+aUK5z8Cv/wInUsue6SlcKSYwlhvjEmM3v4Vm2CAuD1D105w4uItJV2LoxCin3nwTlcILeAMD0QXBqVDBhmXccIXKyCS1PUMk4y9irCUpDNE5cBgfFhmsTVKfOfRN5p563dM804IA/tHuwQ9VKNRq4D1xa5Tjr3sH19e/xIOfgbg9TTpUZfTG7wQcu0ZGMSdevar/DyJTJLsgONVrLUySYeMQH3RP1pt28Y3HO65OlCT4wBBZS16uElg3r9JvHbwh/yRhsnB9j8MTZwmF8VM4AtLM8sCfrGroC6S+vWlTuBu5p+0Dpt3i/66DVpP6jh0QBjxTWzidz9k24VSLZO8gYZaTL9LpELrt24tBv6aqy2Wn4CTrYvIcE0AYGuLD/xu2DIFznDvWFVs1GuV+8HDTcMi3Guc+ROaf4Frv8NU+wk7r3Eb0Mz/AdIPDKq5KxyG8bE+3qJP2V9gzsJ+8WKJIm5HxJZoGnDzKmd0HGTw9RnZgD6FPyfMi5TLUj0PegW9+Eij2UXYWVy0RJi0OPfwsbuggr/6Lr5AND/LU/+uH6Jw+AaUKtdjz6mKRx9OUvqhNGsbQnO6+j3YHyuVudysA7/FB3l0XZsoEA8MEQQrGkOczHwjtNr5cxhDgsXjnGBgokQ3vwb/0wqLTHufy3lMK+ums0BVqrpbPGPfbp+mE9/58fzZ11ds+CqVum7gNFpsiZhtN1Wu2Ooy/cAu10ZvwxenZzW8Bak0Y6v4nfSefpxYVGBw9w7HX30DlpdOkgSW/cI3Ty18OJ090jwwz+ykZZzCBp505Ks2zsGsXBihetZv6UxdUrpM2na89RBK2GRqdJu0vg69QMoZbilWyzgR2x0GCWka1/1ucNX2YoT0UrxokPvY0GMNQvLDilBb7aLRCKllCbd8QvnOWcLRB68CrmL76OrAx5U6bxkwRYSp3tF4IaE80IAz40zMZ6cE9lBo1mJ4m7oN4qJ/YNrE5RO0GnZ39+Je/DB/HDI6M40dfhiHErTClKzDRgsdMjDQgs7g4BmPppCl2ianRvjgNnQwfBgQHX0GQ5d3N4Oe0PDTOUaFOqb7wgqDPMkIcJknIYkuW5kys7rrYAgu61m4kt0IDnryOdR4XLP77NmmKC0Nwhlq+8LWszyg3OyQ7+ohsTr7E8bzjGvRFu9QcSS47BSdZF29dtxFEXCAYuorp3Ye6J7C5ZTI7QXnqOCOmnwi4ZtBwOl38oHs/x7ifY+en75zzxjfCT/80/OmfLjuOyWnPYB+keUJgLHkYMZh1SPsHiW1CvbnEwfXEUU7u2Uff6DidA7uJfE6eFClXYOQxePk7IJkGghBjInylRPhSjWuGijzyT96PaXgc0Omrkp09xYBPmewf5jsaz7G79iSFyJHFMUzPTJNrNGDnThifmYuSZ/higOk4TGk3RIbIpNhikex4u3sy1Gphy93OV5EpgckZGi7R2ncN4fgJmJlqs5SWm6QS7lgxYM11yp7mJbbP1L7Uc35qkAm6m+LImmzKeoG4COnGBicDhNus4tRO23SSCq/YN0gjapC4BkVTAeBszbN7yNBpeoyztKZGSEsFHr/xZsoTk7R3DBGPt0jcNAHh+QsHzSZ5tYKLu8EpxGCMYzrzhHgIu/s9la/bQ/O5s93nNEYpmTO4+3+Xfzj0CoKkzsDZMdrDffSxl45v85pikYbNKe87RGmywauyx3nkO97Iqf2vpOA9dqrb0W44vuBYXiwT+wI1W6GcpYztqxDndYonJjjxHe8hvPE7ySpVip2EZLr7HkYbDnPW0J6o4cOA/UGMv/oGjG+Rj46y4xpD/2BMlubs2A9xq0lz1zBxuUo+WKVvdArbYebC0YU9zs87f+Fl/jGydrYOnZx8sELoO0xPtebtqTWPcZiOJQS+eeAmwskaGEM2Z61PX/00Z0rfy/WfvXPh09ttgihgpA9afS38S6eW3gh9BblPup8VG61eg9//2LIP6UtPs9seJ48WH7v3Du9NtwPiIl1QXasOueX4rgME1pEvMYsg8Q2q4c5VrfEV2UgKTrIuLvfdTRNnPpz39+/Fh4YoSzjbfI6+B77AI8M3EgUhLzv+BIWxJ4GZaRGue8J0r3+BKjERQfcDt2p59NmZA+p3fRf8038KY8uv5Xn2pOeG3Sn5lz6HrZSwPib92v9FoVgmKBiCxhJNJlrTjMYRcZYzNTBI5C02K1EsQu0lGLwayjsgbUIYBdhKkad++Aepv+2NvLy/wuPfGoZGi3Z/BabOMpy3mO7biX/+AYJ2Rin25GGMa7UA3w1Ou/fAxMx4kg6uFGASS1wdxgSOYiElGxyg880JEtfobpJbMhSCCnsK1+NsyNDOImb/QWhMMRDuZdou3USjlp1iKDqwph1zJtKnmXLbp51z23nK54JTFLP0xjaylInsGA179vL+0Lh0vj3/BgpNBEtM8bncMtdZcnrXOZ20QxCU2DNQICVl2p6hGu4E4OwU7B40fOE3YeQ5j2tNkMcFXlabIEgzGvuGKYxN0rDj3SrVxATs2AGvvoH2VXvJKhGP+ucJAjAYRpqePpPB6ZPsfvofqV6/m87R7t5y7sV/4Pr4UzTOtnn2re+meWA/hfoZGnuG2B3so0mTfWFI2zl27L6KUrvDZFziyVv+N8Z/4J/R327jbA6BYfDC4BSEVLOAtFjAeE9SaUHBEp+pM/6GlxHsibEDVaJ2SnNmvdapCceusxGdibPYQoR9wvCp2nUUIsujD50kKnh2154jiStcc3iCuN1mau9uhqMK2WAf5ck6nXZ3/VZnmeOZJVu0gYS33SpSZ6ifMMxpjoxRMBXcEn9b3oLH8Jl2RrvVwYSGtDPnsZ02f/eVFOfsgos7ptMhiAyuPEDgSxSPfIvdxYvvrGfWdMRf6NtPLhJYvvkNqE0t+zznHGXfxIZLHIPzlL52k7RQZrB+Zv5FP+8Jxk+TFGJOV/dgAkM4vXjgTVyDUnBpOnOKLEfBSdbF5g6MJygUASiXhiEKsaUC0TeOYL/nuxjPcqIkZOzB53jZ+GNMjXqq4U6adgLnPWVi3kj3xN4duoq37z7R3VTXziwgf8c7Vpx+dXLcc7D5LLXde5m49gCVPONbN7yCXa0atq9IeezFJZ87bR0FZ5ko9BN7i3WV2d3ojYG+vdA80x1K3l9hz/gUU9R4hRvm6eS76f/q09iCwWTTDFlLWqxikpSSt9ioAwayZGb8jQbs2gu1mbkoaYIrBgSZJfQRQRxQKOTYgUGSF8e6wWl0lHRPH4WgSmhi8qRIozpN6zUxpjnJQLSPer50cFrqpGA5meuQrWFq36Yyhqb1VOYGp1zBaa1S3yC5bP+feyZzRz0snJ+2umGvfHnU8/8/e/8dZelVnnnDv/3kk2Pl1F2dk9TdygEJhCQMGJNswDYm2MbveGzP2HwTnMaeMQ7vzHjZxjOOGAY8YAyYZIKEEsqp1TlWV1fOdU6dnJ64vz9OdVIHJJAAv65rrVqr6px9Tj157/u+r/u6FplqvvAdx5W8WYpXUGI7C8d10XSdaEggrRqaMBCiPT3ny5JsArL90LtBYnVWiNUrbAwqFJNponYBs7SIE9QxlQjMzUF/P6QTuNkETtQkjIkXkvimyf6dd9GdO4n/9S8ymH+eETeLurIMpUlq6hZWrDdTX1rh9ffehfH2NyMqVSodaTpUa9WwVBIWCiUzQSTwecS8A80zsHoWiNdLqL6LlJDSLl1ehFwFryMGTsD6+hhBSkP6Ej/RRBc2bjyGZjtY22qcfk6SK0uynStUl9qBU23zEmPh7Rgy4LmlMwjfI54bpbxxF4lvfx6t0SLX1U2nauIkwhi1Vrvu2IzRCiqUvYXLHv+alyN3In2RhxaAotbA8ah1ZtAVj+bcEmE1hXsZGrMiXWQg8BSVNydP4NVLCEXgOe1nUSB9Wp7gbsZZ3L4DMz9HcEE1RTRbBGaY2YWA3oOjtHo7iNZnvqOVxquF5565DJNhcgw6u6/6OU9KNKEjxRXkyJtNEs0qtXCG/rnT2BeavXse0nVAUVjRMsiIQWTu8nzFQL78eW0Na3glsBY4reF7gvQCFECYbVqAZaYQKpQTnSTufwZ703psv8HdT/4DQbVGV3yFsQMB4aklGkGRBi5RDIQQxDCpD6+HMyPEw4LFqydpL4LngTp1iopp4nbGiPpN1mXuRhMSLxYivHwZJSTXAd2g7npo0qegxdHwUYKLKQ6RTqgvgS4gMHT0houvlGksmbR6d2CdXCCggcQlrGgomgBFwxrLEX3wm8AqG0kCtRp214WBUwvfUBFSgOuBFULpjhOIMHppuR04zc5S67EIKymgTUVatKcxEjUC215dYF1tcl0NAl8Cz/8sfMXECP6FNN1KSSOQhNXVwEnVwP+Xo6j2wwMF5/vYL3C47vJkU7ziVL2zUAD/VVx0lty5l6TW1qaIXX07XBt0UxALQWmhnx5jx7n3HA+CFlgRSTSkIIIaLaGjxwRT2QHSxQXM4jwCFVOJ4E/Ocnq2F8olNN0jks/RTyeVtIKmw1JVIfeB/8bheZNyNMborI9RX4LcKapyE30rnyY/VmVDPEJIEWitGivZDtKqijQTTKx8C1Xv5LBiEJIetV1J+sIrqDGJ5rqEag28dISYeul+aoGC6E8hWwG9cyNE3VZb6rweIbx0CLenG0VKzOw0s6egWg6IvXUeu7qCb+kopstiNoNUBOHiJIGqY7byeG4RHAfV81lJ9JFBxwmH0GwHsztg9piGG7SYbR2+ZJvq/goFd5rF/V3USxe/Z2g5pBtQ7exBx6M+v4zqpnCDRvvZfAHirTlcdHxFEJh9tKpVpCLwnPazyJcuSMFs3yye2cCaP4N9oXBq08YzI0xnXDqfPU7xmi2kS8cv2abvF+p1qFag7C2QO6skKOWldPoXQUgbU71KX7LjYBJQNbronp2kyQVzkt0C38M3NBp6giBmEVuY/w5butbTuobvL9YCpzV8T5CuREiJHmpXnEwzAYpKIdRB9OQ8bjKEKfOsXzpGxphCwcdoPIbyt/+dQLpUsInT/mwai5WeFCzMkYrBTO6CB+Kq59FVtoTKN/+BwvbNqFGBGo6R1DqQhomfChOfvUzgdOoQbL6WQNiofoCvGfiAioHvgrK6Jop0titOFgJfCpRAY6PM8tvaMvMbPTw7jCnK2CGTZnoThqWjaCG0UgNjeREZgFO32xNOrcbDnQZBZVUq1rEJVBW5eisKIZDXb0Ofr6DXlvGkg7MwQdCVOVcFAwgX51hXHMVXfHz3yhOHL7123wMQUuM0XyL9Tgod5QqqSD+MaAYvqjitBU4vG99v96uiFzCL/qqIQwDoQtB4leh6UgYoLyFoWh3Ndzq6riMxTQhbkJ8ZvOheB5gbgYHUAuZgF4bToGmG0FVYNnuI1goIt0pKH0AXIYqPj3DwWC9IiSY9snMz5GcOUk+mMIVDOL9McvH/MPG66ymHu4hXZuhQngdFRc6PsJD+GYRXRPUaNEsS1W4wn+0hrSgMpe4isnSYIDZMFYGhCBxsXuMcoJDMsSIFTn8H9s5e9MsssA0dnEwGaUt6FydpvXEri3s3c+3CoyhuA6XLQggQ7jgALdtlix0lES0RGBq9cwvc2BkGTSFVKhALz+KqNunaMRhej1ptobqCr31G4OsqioRGJGBxHGp+DkVcGs2tOJN0m1tpVaFVE+dEdHwvQG+VkEJQyA6iBS4rS4s88a0kjmwy3dyPXL2+3KBFvLaEI0wQUJap1WyeQuC1KypBYBNdnsXTBclDZ9CPH8C+YEpTmjbFjODaxUUCx6G4rpdEaT+ll2dl+IrB92Al366sts5aWQhBuRXQuMqcE1JqhDQbg8vf10qtASGFheFd9J0+Q/NCGX3HQQYevqah6VH8uEVi8Qd0ANawhivghyJw+ou/+AvWrVuHZVncdNNNPP/881cc+8lPfhIhxEU/lvUqNEGu4aXBbWeLNCMEgFBUAlVBU5q0brgHT9oM1sZpdnTScAYYCe8mNDaFN5RGjh5ltvkCCdr9USksCooDrkMyCnP5CxY911wDhw61f58dh/mLzf00WUSWiqjVBkZMRYkkUISCH4mhhFS04mXKVxOnYHgrUq2hBgElu4SNhVRjlCYgub497GzgZAiBj4IT7eDwIZOfmkxyxqhTr0axggqljg4WIn2kDAl6DEVotDb3EZ7N02zZkEhCsUAxEqbWXJ2INAXF9wguWITpA1lkXWDYRRp+gZqzTKe1tf2mbEs8GI0yGcUAy6FwefYJwHn6DhBSki9ZkrzNj/8XkskTol1xOhs4qfqrInG9hlcSgqIXYJnWqyIOAWAgaLxKps91f4WImr6oV/NycIMWqjCu+l1Nv4yDTzKqIIRA1S6g9q5iYQy6Tt9HZEMPutOiZkYRQiHih5Gui6N59JjbEJNT5KtJsptDBFIiREB5YAhv/BCtjh404fFjk19ncfcu5jNhGkNRemcfJ+/vbSvhrEzy1XwPrmbQmF+kMCFQPI8lvWt1vlXxB+9lyBIseR5mIoLeyJF0G+S9MI3AIJwU+GED/TJG1EoyjaOoVLQM60YnUCIa9e4M3ePP0erYjt8RBaFgNSfo2Qj1RpPd2ihho46QsGfqMD8yqBIYBn35BWqqSjUbb9tf9MRRyk1uf+ZRjh4CqWoIKZlzXWQAChpRNXtuW87SmxWhEpZhtkTuwz61k0X7JACTEwUirYBAU9kX3oQSeMRqeU4cCWH7dVpBDXtVva4VVIjOjOFIjUBRKHkxROAjVQXprlacvDqJkydpbLyHZl8PgbRprRatZBCgtBzcMITtOoHbZDJ1E3p9nMr30Hb4vdD8EknIr7TnAlOJ0fKrSODBMZ/J4pUTEhKJqkosWb/k/0spUWtNmtEwS6lNZBaWLq44OTaB9PENDUOkCFIRYvNrgdMafrjwAw+cPve5z/HhD3+Y3/3d3+XAgQNce+21vOENb2B5efmKn4nH4ywsLJz7mZr6wThkrwFwXISqnAucAAJdJRqUWf7Z36JZlXQXp1netJuYrDCe3k3q5Lfx3/w2ek6WoOqiOTa+Jwmj08CFrl66qwvUL1z77t0LBw60f9//BBzff+4tKSXrD30WZcseFsQQkVYTM9xurPYjcWTcwqqW2j1TF0LKtmxucQ5Mg6iTwwkiBFqClVHIbGoP0yzwbDAAqQrGOjazffoo5oEw6U0upbpFEKg0PYtKvUhnUGIu3c9KJI7eaxGZzLGSSLXV9I4dZtuRcapem6MRKALV85CrVSEiaYygjqOm0OwKChopfQBTiXLyaYlsNvDMEEqpQviRI8TmzrA0LlHEi3xczp6eoH5O1vilSpL70gWhIhD4PyQN9leFlDR9SUgVbY9KTVsLnL4rfH9rTr6EiBWiab86FSdDKK9axaniLxPTulZ7Na9swLniTpLV1wFXXsTmnDFa9QiJCOC56BaX9Nr4LmiPP0hi/gTRUonlaBbXyDBgriARNIzVxeeBfcSXDjCQmqeUAwUfoRl4QtDsHUIRPsUf/xDHOjYx2V0iG3kedX6JgHbyseIGPHXvCLX4BtwnPkVpTG8bz5rnm/DT0e2E1BIVP0DvyRCeySNUE2wBiknYbeAYBqHLBE5qzxDhlQLlxCDVbYPosyWq/UOofhzfSrDSuQNFF8Qrs2y7TdA3XMPUSqTsJsIN2CEnkZEadiRMz8IUQVBGTadwUKE6jQzpJMsldNXDNyxEELBgtqOT4fCtiAuWPGONp7CDevu1RplwMAdSEFU7qHkrzC7PEw08fE3lSGyIQFHoa04zNm1SbdoYikUzKAHQDMqET44QnZtHqioeJkixSmtrB+9uvYAyXeLej/46xT07Qbq0qu3rM/BaBAhc16Wkp9HrDexdG9GrZUpXXgpdFarQCPjuez1TKSisBIAgrQ9RaI5TdFS2psYpXeWWDQJYWrCIKHWaL/r3HgFWo4auKbxj+ZPojvOiipON9HwCTUMP0hDSMGuvorT6GtbwXeAHHjj9yZ/8CR/60If44Ac/yPbt2/nrv/5rwuEwn/jEJ674GSEE3d3d5366urq+j1u8hrOQUqK4LigCQ4+ce93TNGJemcIKPPtVMO0m1XUD5LuG6DaTlI0IWue1lAa2ETqZ46lvT/DAJx2WplYfoF1Zrv/4f6Dj5EPn/5mqguvCb/82fPMhePCRc29VGpBcOEMo0sHRDTeg+R6hcE97W6JpZGeEWKVA6UJK+vI8ZNtjekZO0+rO0uMW8H0TtATlaUgMXry/YcUgAEb0DraVpllZhk0bFc6k1hMdLVH3QjTtIimvzGR6gKIVwQhaSFtlcccwdHSBgOjsPMoqlcxRLXTbJkCF5WkIJdGqBerrrsWolunxbj9H25l8YJJjXy/RTHSiTS+jzK+g+S4rZ+pk9HXknDOXnCNbNs5VnL5zL9TqZ4I6KBYWKtV/IZLO9QsqTlJdo+q9XPwgGtAFEA+FaLZe2cDp7J4YryJVL5AemjAIq2kafvGyYwruNAE+hhLBUMI4snHZcUIoFCc3k3aX4eGvYFwmcNrwwG8DAvW9P4vbn2Q4VMGL68TVCtKXtCyvXaU6uJ+vrn8ffUc/Rn6iiiICmppkZjpKuGcDSqtJxl5gyWowVJrBsXSkEyA1gdOUrCguW56sUbv5BuZbaexyWw20K2birtKzLGHhCocIGpW+QbYeGyURH0IPJKqhYngOjmGgXCZwsro7sEpVSkNbqW/tR2820R0VK9EOzJpmGqEKQtUyMgDLrGFpSeK2gx8IykGG5dYSTiiMZTfpmh3Bz3YDDkxP4e7uZWbwJu7YeAbbikIgsWPtg3n2OXj2Wk/rg0w2nyOkJnFLJVKNQ0TFFCl9gLI3jx946PUSvq7Rn+iBACK1PFJxKZfAUhLnqM9O0ECqCvPXXouv68yaSyAVpKrAal+pvTKFLQy0HUni9QpO3MKbmm5fT06dQNEwsJkxt5Iy6/jJCIrv4rzMtkNfuihCQxPWVauh3wmaDr5oYChhNGEg6yVyMkw0mqNUubxVRSAlgYTxmTCmrFNsXvxcsfHRbIeMXyaVnwEkdfkiqp6AQFWpHkmBJjDsS+cgKSVjB+Hk/n8Bib01/H8OP9DAyXEc9u/fz913333uNUVRuPvuu3nmmWeu+LlarcbQ0BADAwO89a1v5fjx41cca9s2lUrlop81vDLwcVBcH6kJNDN+7vVA1Yh6NQ6dkKzbBUKVNIaqfOuu6zkTbvCF132YTx2sc8IvUzi6yPYtVba+9QkOnzpFaHoS8gtM/u7/Yfj5L168oNu9Gz7089g33U7DNWHfPgBWnjxM6vABmqEk4S1NFB/M6BCMH4FwGr8jRqhVoVC+gLZz6GnYcyvBsWNs/7/3s9DfgxUEWNgII4X0z/c4nUU0nADPQ3ElTT8g8GGL4jF34yb0J0fwEfiBi+U7FKwIwkqg+g7NUAZzZbUqKiHQdMSqFK0dThEqFPGFQSU/x7eeeZCgUaXUfx1WqUz1yFw7hdeockPwl5Rnl7F7BzFGZyGRQaoqejOPqURwg+YlC2A3aKCv+sG8VDhBHSlNTEJU5KXZPl+65J3xl/WdryqEoBEERBRBRBE0FG1NVe9lwpUtdGEhEASvEr3txZBA3ArRbL06ghQ6Ck1e3X1RH34U5fRlEhZBnbpfoNdsizyElMRlabJn/YMaHiT9EpQLGBY0L5imZBAQXTwGmgqNZdxkhEo8g+XNous2uu3iGV5bRCCfZyk+zNyenbjhFpVQHE83WCz5dIX70DQPy6rTq45wR63BSjiGDLkIC4qLkDcd7nzqEOVr1lMYiPDO9P8CBP2lHF/9x4vP0049zOHMTrxyiP4vPUj3mVE8Yxt6vUlLvzw9MdIRJ5RX0dIOgQeuqREtN1EzPSgoBIqG0DUUz6ZVAlNpsTiXQJVN3HiI5dx6RGGZUFqn1p1Fr9SY6kjiCWgt5ZCbssx03cE16VPYRgT8AOIVzDC06hJV6Pg4bcqY0NkUvpOk1k9lYgXr+YMYooIqNFzZRAQ6ankFJ2SyIZ0Ax8fyXD58x0dZrtsoaORm3dXzGBBID7sjjp1OYoRzKFIhUJVzSTIvN4vvSvRf+C2SlRxORwz1RFuswncaBFJBVzzcIItiaEjdfyntcZfACRoYIowuLFz5PSYlzMo5yW+9EXC6WKaidSKLxy47fIQVRKFJzmnQ8lqUWi+ajwgQrofRbFLrTgJQvjBwslsEqiCQ0CilUBAol/Hkc2UTt2Lx0BfmVkWP1nz71vD9ww80cMrn8/i+f0nFqKuri8XFy/sXbNmyhU984hN89atf5dOf/jRBEHDrrbcyOzt72fF/9Ed/RCKROPczMDDwiu/Hv1Z40kE4HkJXMfULAidFR5cupVJAZoMkrLs03Qg9XeN0bz3Owu1NrLjOwsYNbF6cZuDgCH33vcCOiS/DkVNw748zcyiEW/VZvDCZe9ddYCiM17Yyrl4L998Pnod/37dwf+QmJvMqmegKquejxvrg/k9gtVRUReCHdcqnLpAErlUgHKX8xS9z5r0/wkJfN5oPVtDC01KXTlYS1GicwJVsspq8cMInsnGc9R/9Y1YGIsSPT+LF1p0brNbrmIkMEDC5/XVs++OPwlwOx29LAuOv0jdqHqqQBB7kVYNwMkKr1aCVXYfWbKJ+8mPwi7+IXJkjJ7rIhkfovqOH0Auj1NftJIiESXgj1EuSiJa9xC9GIlHEy7vNXdlkedKkOZWmfBlfn3n7OLWr0JO+75CSZgAhRRBXFeqsqeq9XHiyha5YGEoEJ7h8ZeSVRXtBlTRUGv4rt+g5b2TarjjVX22q6cmThL7y8KUv1x+4SMEypCZpBpdm6VtBBUtJ0PAg6hZhevqSipM7V6S55x54448xVzyKcFyW0r0EoXcRV2sYrRaeLqmXJE61RTQd43QxwhectyBjBq1QlKHkGKZvoqqS+HqPDq9IYzqMNllCxhpo0YDZEwFevUhS70LtTFExTDTdxxcKO0ZO0HHiYhPyu8IRDsoEdxaP8eXbf5ze5TnKPdtQNEHjAur2hYhEBXolxPQbbyF8aoFyIkpntQSxNN1KD2GlgJuKoSqS/IEJ9nzpixx/cBRC4HVGyCZ0VLuJ3ZXEXZcA26Nq6dA5yFygIiU0/CEyKZ+mEUP4Eksv07kOlifBECHcoIUnW2jCRBEqilDwTh5E1gN02gfelw5ieRitVsIJWaxLmgQSVD1OyKpTd22mDqtMjzh40sF3BVJKoqU8tcFuQnoLHYGvqecCJ7Ewj0SgbNkDqoKfiqBNngbAc2pIqSCUgHQgCKIRHNVvy7+/zGqwK5sYSoiD3zDxvofASQAY1XOBU8bJEh3KUavuwMxfmiwAWKZO9swEr6kfwKdF8UU5EQcf0fKQuoJjmaiBpBZckOSqlAkMFSkFUcMgQCAUwL7YsmAlV8cwQgyvvx8lsC4rD7+GNbxa+IFT9V4ubrnlFt73vvexe/du7rzzTr70pS/R0dHB3/zN31x2/G/8xm9QLpfP/czMXN1PYw0vHb50EJ4LmoJqRM+/rhoYvkNICZh3fKJqi0opi6tESEiX1lgPR86kGC9u4fSP/QjKW9+P8fZfpOcdP8/YhkEOPiwRdYE0Qpw+XrrofwZz4xStDsyN6yhf/zqCj/w3ctn1iHVdjOUFWb0OCFhZonz9j2DOLKBIiZ8O0zzcbvrl4NOwbQ9885vs276XzmqNaiyKhYMdhNENcVlGmx6Lo0qNd2yt4L5xF5sP/D5adz9aKCA5X6b8njedMx1MPb8fccvtbQXy/u3k77iG4PgoRyYk9VQHSr09obmlFooOoUKOxf71HE7pFBt1hPBRK3VWem8EXac1tcCneAfaiWcRvTHUM5N8+uhe7IFhEoVHWJqAlNZPybt8AuEsXkp2zg1a1KsG9ZFO6i+iIZ39rPZD5p/hBhJdESQ1QUWoaz1OLxOedFCF8cpkqV8iBG2vn+YrGNu40kOsKqeFhUbJf5WppkIQxKMXLW4D6RNXu89RwwA0YbR7B1+ERlAirCbxA4lWWYEvP4Aeujhw8saWSCdtant2csqdwfRdKlaaz328Hy8eItRoUMsmqY2uUC4HbBtyuedGg//0rhyKqTFvWYi0x9cnJpHAtCeItxoEk9N0nZikFI3QTKVxJyaw6y5mj0P4c59n8xe+SGv/LA3FImJFKNTDOE57P0OE8EWTAIH3s+/itdtvJaH5FDfdCV1RivrljUlVVWC6KjIkOJ0Z5GRkkJbfB4pKTEYIC5tmOomWMMh9/m/Y+s2HSfgLCDfATsZIdJYIPI9Kdw8xu4poOKgdCtVEijPveQcBgsCLYZiCptkFhkpXbYGu9bA0AboSwpENqvUmD/9tCHd1f/zZEZaiA2he+8APh25DOGHUehmz0WRTRmknz/YfICjaRBtTOOUwPepuJvLHcZ32lBHK56kPdeL4kbYXoWUiCMC2URbzuJYJQiB1A4EkWKWp2nYRfEGgqXTbZYJIkqJp40St82yFlwgnaKBiMLHPxJXfm0ea3bvM8XkXKSWyXser6CyGdJSlyyfOFCnoGp+iqzmLisuKfXFA40gP4XrU4zF8T2CaEnfxgiau/DJBWCczs8j1+n3Y4QhKRIPJyYu+p1xp4Lphrpl8gPq88X1K9qxhDW38QAOnbDaLqqosLV2smrK0tER399VN1s5C13X27NnDmTOXz4CYpkk8Hr/oZw2vDLxaCakEBLoK+nllQ6lZaJ5Ld1SytLKEoqucjkleqPdi1Vq8ofs5akiiCGbim2lWZyCWQO3qo7MjTmxblXveDXM9u2k+8hjLi+2VVbUgmTz4DPbeIsUbryd/YIapX/9R3NvAqPlMXjeM2tKQKASj+/m7dbs5WbVRZEAzEyUxd6S9gTNnoNiEQoGT1/UR80MEioIlXIrFKLfedpmmZgO0SBzf05DOCve8/jaCG/4INxLD8Fw6+mKcrszS6UgQGrFTp4nu2A0C1LSOLRLUp0fJDIaYUwLUWlukIWg0CJJhzNFpjuzazHD/LpqVMppSRyBY6dsNQGNuhcxgP36hSD2uYm8Y4NpbS9SHh4mMHSU/y6rU7osjvov/bkuSX56ffhYBPoES4Dkqzosm3qI7Q0r77qq2RXcWL3hlzU5fjLSmUJRrgdPLhS8dNGGgK6FXP3CSbf6RBJKqQiN45SKnRuCirypUGsKEV7FHz3ElTxwPENkO3OXzCYtmUCKp99NtbnvRJy7NxpylVQEwPweFEroe0FzVcJErKwzv+ycsr8bxdTopBKofUGpGuGPLBE48ju442L0JKofnaDRgT+Z+lGiI4+UDuLrBomWix+OYtVE8X/JgKsGWkRFu88eo9PTiaAHLrRiJ+Rdw6y5a6TG0dXWe/aU38ti63VT7O9HQ8CIdVE+NwYlnSLlhCkF78dyx8cf5hjfDQo/BYPooQcjEifZc8bgpCBRhoqUslGaMDcNdEI5Bo0pC0fEsCy0IsM7sY+LOW+iePA7FGq1EHCOiIB2XmYFOIq6NWncwLINaK49fr4Bo20YANPUeCOl0z04STQnqZdBFGDdocvSpBpvf7XLfZ1yCQCJLBZTd3URyJ/A92baFAFS3RbjRoNNUkAic9VmGP/Zltj/yIKnmLOu3QS5fRTRjSAFGpUq1O4UeJBGKxI6FESpQLqMuF3BCIZ6XI7RCBqrvoqwKI7hOmcAXuKpG2l6hme3Dc3MUt6+n4+TXrngsH//sZa4p2WSWBZbmA7zv0YvPyDh8/fE686NQrS+z+blnWGp1YjYunUOklCBdogsF0oUVAk/g6Ccu3rZKAYGkqYdoCRMlZSEmL0j2VcpIQ8M1DHp7atRjCfQgQJYv/n+NVgPz0adIHB8jf3BiLXBaw/cVP9DAyTAMrrvuOh5++DzVIQgCHn74YW655ZaX9B2+73P06FF6eq78oF7Dq4SjR3AzYQJNA+1CVb0QuutSXQ4Ilgqo2TiBFiGu61zTGqe7MM2/u02n9YzGXUOD7Jufpe5JPjtt05PsxslOMe08yuzt29ly7BH+6Z8lJ56QHHrEI2Qq2KE0oxUX3y4REjE69p8h1IhRviOCXDbxdY3FVpNwNUogdBAqlc448foYrROj8MhTMDVF8FM/jR5e5kwlSrzpYVgKzVaYSFRcQtVLDYNdjeE5Gl6rRIem4ke6KW/awZb9+3Guv5b3HHieDqeEF+2EIE9TWSFQFEKhGjJQkSM53Os68VJhlGoZ222heDWqN29Df+QFrE0Rbs1uR2k10UWN/FtuR8vPUMlLasWAvl6FwaQPisPM+3+B4cElxGAIPV+gXn1pi8+2JPnVAycAL9KgmS4hX7TYawRFoloGEC9JUGCudeTc7yVv9qoKZN81xKqaHpDRFYqoa1S9l4nzFafvjd7zktB2ewXAUAT+K1hxauGcC5w0YcJllCZfGUgeeN5FURS0Dduwz5y/zut+gYiavsxnLk3I+KvHHYBiEa7fQ+fEIWyvfW95n/p0u0v/5AjKygv4zQq+ULnu2NPsMp6lEssiNEFgBZj5SZqOSnb/1ylOjqMV6niajwgFhBJDdDYm8VWDW2cgH8Ro3XgTsayJm9BoNJuEi0eQFQ81EjC/dRNmh86x196J9/obUKRKs3MH+sMfh2wf0Ye+TEmWiAuYd20azJPZdg/63HPUlRCR5g1XPXoKaUa37GCLYWHe8VaIJqBexhQqmDrSC3jot34OT7hsfOIFPE2j0ZElldiI3qqy0tePVa7jagpK1CAWVFHVeaQi2uYWiop047jJCH0Tk8w+B4uHwFKitIIqgdqiFarS8/oGxx9tojgOwd5dhJaPUL3gEaW4LoqQfHX5OK5lUlvfxezbbiY6Mc3Gwj+RThexZ3sQ1Q5QJXge9RCkyKApAY1oFEWTyGIRpVLDjVj0+b2IcBYjaCKED1IiXAekxFU1ItUSi9l+hosl5m/YQmLqeXzv8s/a/d+E0tLF7wXSo+k7GPEAx/3uez0boSZZzSSIeIwfhGo9R7haRjcM9Oalva9VHMJ+E3zJ8Df3MVPuxOZFz+H500ihUA2lsHUDEdXR5i9oy5ASCPCFQrrfoBqKobsuXrV00dc0bRtz+gTFe/eiTB27ovDKGtbwauAHTtX78Ic/zMc+9jE+9alPcfLkSX7xF3+Rer3OBz/4QQDe97738Ru/8Rvnxv/e7/0eDzzwAOPj4xw4cID3vve9TE1N8fM///M/qF34Vwtx9Dh+0lytOJ0PnKQZQ3dcjFuauOMmsidNUtH408OfoOooxFoFWmHJri6FLlPDpMlHT5XYm9T45lSKvH2U9aGbUXtNkqEGhbJk/gxs/7E50okk1ZX1xDJnUG+J4P23f2Zq+F3M5xVMU8evNQhUleNNUMs6RUcgpEKjM0p6/iTub/4HuP1W5Hvewz8+GhAxPeq+Q8Rvgq7RDFL4TtsK6EKkN4K9qNLwwki7RiMP4SxE9lzHDY8/RP3G17J+YRZL5jk+P8tA9xA1KgS6QVTNYek2rYka5c2vYXJlALuhUluYRAYNRMyi1dNBKtZEdebwUQiZVRp9Q+waPsBTX4DcMqxPlgmFLYxaCUdPEhv6EaRhIdwAJ3R2sSsIVvs62uaM7cWas9qkaymxcxK6V0IgA6Tq4YVbl8mRt7/PUEK4L2GymmuN8Tdj7UBNQaUeXF6B7HvCBQFcSlMoSLXt3riGlwxfOmgY37MS10uCayMN81URP29JF532zasrFuIy9LjvFtPNAwTSX00YCPy5RcyhXuTgNbhj5zPrLb96vor0Ilwu2XDO7LZQgpv3siO3n6maBN9nbhysDZ3Iz/xfusaO0vvwfsqhBFIPk+o0qZpZhK4gsQkvjlLzw6jNGuKhfyZwHITnIXSdxfktRCoVHKOPvtknCAUuKxsTxEyLZjiBmZig5i4RqUA9HKHCHqLBNq6rV0lnu/FaKh2bOji253ehcxCR7GAj67k5VOHzzTHqfoZY30a0XI5GPIWldV71WAppMaJvZ6insy3ZHU5AvYTSiBIkoxiNFvHmCp2dDYJr+pD3bqQWTvONxQ6s2Qw3Rq5FICjdtpFI2EIRHp4jCVBQhQKJDkI1D7c7Se+xMU49GdAqXqAsajQZ+q0/Q+2tMXXqWXTDRDe2oLUqlJfbhsSaDkqzSSsbJzLzAo5ikE8lkR3DlDOdeOE+RLOIWN6IXw4hVdB88HQNRdUJQhqNcBhFSFqzKyiOTTNq8XQJtI17MWoN/EQMFhcRnouCxDU09ECQj0XoatQo93ZiOcUrmuBuuQVOX8b2su426d7hUPseHrfVRBWramFFBI0yeIuLTHbtYn3rNMGLrD2mfZsVmphulajXIjG2iOsZ2C+q+CrzE0gkhXgX9VgUQjrai3yaFNfH03SMdZuRnkANAtzqxTvir9QoxZZYGB4mkhvBf9USJGtYw6X4gQdO7373u/njP/5jfud3fofdu3dz6NAh7r///nOCEdPT0ywsnHf4LBaLfOhDH2Lbtm286U1volKp8PTTT7N9+/Yf1C7864TrwvwCEbuKHwu1zY5WoUQzaI7LypYKP9HtU03FSNeLKH4de0MP/ftfYCSfIxaHahVu2vJ6fi31AlviKjuiYYpBCl2ESHRCalOKWP4Mnesk9aCAIUIgdQbSMSaGr+GkdRtmapCRms36sIYnG1hVm6PJTfzUToMpoxPNCwg6oxQ27qESTsCd9/Lc1Chbt53CqrpgBRDR8GoBRucm6rm26e2FiA9AawFqThJhV6nOQbwPNoV0/u/7f4XlzEYcIyD2tQdQJ3PU7u7Ca+aQkSiGvYI0NZTOGOX4jQyt30AQjrFy9Aie4mF4PtMf+nFcLYrt5PAVjVCsSr5nN/7IEXbcAZFEja33/zncdgdWKUc96MHS7PYN7IJjVhnbLwkpCVqrErl2UMNUouD7fO7XKwSBRAgFibxqtcghwDZdjKSPZ59XWfOlh3lmAjy3bYj4EjyhJuoWmtIWrFCFTvAKLmQvB00IHGXNx+lCSCk5XX8U7yr9Du2Kk44iVCSvsqCC06KlmYTVVz50agUuxmr/nSZMxCtE1ZNSUvLmaPhFPGkjApNEYRpj/QAVowdW2mUKT9ooQj0fDJ3F4uIVJcmDYFXQoliGjgwZy2G5KTn58ZPkMlvpjpbw7RLxF0YxcjVcy0RqKTRTx5FxhKkTry4zNFggO+BRt3RqMQ2ztILiaaiBwnNqElFzOTp8L8bxKSqbttAZvYm4XiXtFPHtGg2zya4DzzJ29/XYSpKbV55j80oeL9uNZ2v0DwjKtdXqWOcg1vIyGdUnUBaQQZqQGoXAAykxQtol+3kWnhblTlflP/cPEdZXj1OyE7nvW3z9r1z8TByz2WTz8RM0N3ay+IE3sZzt4EwiwZmBKKLgEa5PUHnLrXTe0sskNwACORMiOCuEk+4m2WpQ6ukmm1/hrzNLqBrYjdXnnurROXqMyENfpF6GSDQg9ru/A9KlkodmFUIxEM0W5d4OhlbmqckQ8bkZUopJPZ1Efn0/NPIAOMUcWrGILwUqKrklFd8KUzZjqGpAaXwOxfNohixmbYG1bQ+K7yGjBszMgOsh8PE0jVPzRaywhur6VGNRDMWhMH/5azKSuFS6HkDaGh2bbWqX8X1/qbAtG9HU0Ay45p6A0pF5coM9bH7885foVXyivkCBJjKwEQ0HTfh41QDExde7NnEKV9WoJzLUYzGkZZBYunjnFNvFDVkQSWCjo6gC90URYPz0cTZMv8Bo3zaMav6738k1rOG7wA88cAL45V/+ZaamprBtm+eee46bbrrp3HuPPvoon/zkJ8/9/ad/+qfnxi4uLvKNb3yDPXv2/AC2+l85/uIvqP38OzECm8DQQDlfotGjaTTXo+mAgY3UVfpy8yw2JI1JQUMPkVz4KokUlEsgVAMttg63OsG93QaH7K387L4GDV8h2LyDHw1/A2VdE71Yw45kGRp5mD1npgn7u/iadTPOg1NUb0tyve/iqg7hhsdMch1RQ1CPDaHaHvUNWSqJLE//3Mego4eFSpWBjEXimTOIDgs1quJVPIZ3DVJfujRwUlRACpTAxA98KnMQ64M+Q2E208+i41L4iXupvm0XfOBXqIX7KTnzuPE4ZqtAbet6GsPrGbjDYloPWDbitGZGwGyiuT5NyySMQBEajmah63la4S24pTyDOwTZkUeJKmV8p4HqttDLNcQLD6ArUNh9K1u+8occ+BaYMnnOV6YZlAkpCbzTJ7lp4Q9YPtx+PaJmaFxQ+ZFSknPGzv3t4FNXGrRCHs1a9FyFqubnSN73CCzNEVHSlyj4vRgVJ0AjQ1h7FapMF0KIiyZy/yo+TlLKyxoF/38VDb/EZOt5Oo1NLNkjVx0rXqb64ncNp0VLNYmu+m5dxu7nu4aNi3lB4KRIB/8V8KiqeAv0mjup+Xlc2aRUsRiqnCG0fSOFGojVPq2cM0ansfniD3/rW/B7v0ck59K4zD3jNEAzwalXsTsyCKeFANSnv81Aeob6W36G8soIRrGB1BUq4TiB1gs9w4RXatTNBOtWptAbZSJejoXeTsb37kJt1BGGgWJLEqRwpc7J4T6W3vw68pu3cd/cfp4degdx3WPAL2PZGZZ27MaJafxc9T6M2DCp3Dh2PEXTDpFMXVDc7dkAC2NsV3ewIof4xUQWS1gQOOCDFbly4GR2ZWGmSLd2XrI810wyvfvfcq1zkEJPF1axRkR6BDmHWEeEnNHFcnOIge4spVaLaGg97o++h2rvEM/LgGKol9DKOIG2ejGluugljx0J0djQyTsOfZx118CJR0AVBgoutb07sI+fYDZ1K3qlyHSqE6flE175Js1Kuwgmmk2qnSnCUlAjzsE33EZS10idGkdZKcPqoj/IT2EsFnFCOp4noGRQrEeoGVFUEVDMnQBbElYdMqJIMpxGhk18owVzcwjXw/cEYc0l6oxQNVMYLYdyNIYqbApzlx5HpwnGFZwmso/vI2QePNcr93JxNrHmNASW1JgZHCdUsvECHbOwvNqheB69wQjzXpm628AoNqA7Rmh2GVVcXL0OLczRMiwCM0wzliYQKonKxfeEYru0QiEIx3CliRDykopTeG6KWDzALlWQYs3LaQ3fX/xQBE5r+BcIXcdPJ9CE387yXbD60WMJUARWVbJCFSElajOgnjFZF1MphnvZNH6cVtcSpSKMy0Xmo3FOV59GSslrOzT+ZHeIw6UsC3euJ9kxj/vkX9E5WuJkbC8b6yMwfppbt6v86rtUIvPHWNjWR6ZZx6KOoYWQantSVsK9SDvA0iVOPUCYPvtGm3TFDWJffIx6KIKuK4TDPlUvzLpOnfoyRK7gqWwEKn4gqc5LYr1tmk1I0Zm3XfxqDVcXrKNB0YwSCEEzmSZWX0GION9+/+/QlD5zRpGGFFQ8k1J2A7od0LAUVJbQjCyqqSH9FQLZge/YEPioKyuc/MCbcY4+gy4lZX+KYr2AqsDyllvIinnurf0ptf/8SZpOOwPXDCqElDiVz3yT6G2vIfgffwDT04RF4qI+p7w7zoo7ce7vpuvxgR//t4y4S1SLsXMBUq0+g57uh4UpNMX8jgHIE/kSW2JJFOHiBC1UYVzSM/WK4EULY6leWnEqu/NIKSl788y0Dr7y2/BDiJZfJe+OM2jtJan3EbzalaSXCqdFQzOIqq/89GMHLqZytsfJICZ88sH3Xn2sBwVSej+ubFHxlsjlM3TOHSM7f5RidVX2IQjwZAtDeZEU9/Hj8Gd/RuifvkVj1ctJyoDRxuMAtOowkH8eb+wUo1mfqcMRhs48TN/zn8C1Hcy+buT9XyB30y4Wb72GpVg3gToAvRuIrpRw4zFiKyWkZWI2cix1dbAQ7cXoS7Os6gSOyeZjMcLNLMKcoCMTI++FMIjxfLMTJxoh5C+T3/Ra1O0G6XqNRO9dKJ23EC7n8HSoeQkSCc63acVSVOZLBL7gJj1FSqgYmLhdUUS5jhG+cuCUXNdBaWIJDj96TlRo/z7J330yxA17bBZC/ViOx3JnEsP3YWgn3dKjWFnHjbEwJdXFNLqwzDQL3R0kLEGu8xp2zx3ANtq9c4RiJPQ6gapD1GRvcz+dd/mMPgVdxhas6W7EcpmaarBr7wTKySnm9m6iWQPTmaJZbetVYDtUO1LMVCT1cIKJm69hLmGR+/HbmNe78OaW2odkZZpACrpDOtKHvmsNmuEoaCqKkLhLFfA9PMMg449houJFLHyljlxYANfFdhW6RJGymiQnN5F0qlRDFkI6NGuXHsdWDazIpa8DiHIVY/67f875fptB6TQh0V/FrproEmJ+mVJXAtW7OCDqUQRFb4lyuYBSbcG6NB2jY2gvZhj4bfnBlpLEjqURUmLYF3yXlKhNj1YsDlYETUowdeql0kVfE1qchzuGePNffQypBWsEgzV8X7EWOK3hu4b73CheJoL3IqfYcDyF0FVOzxYZb+SpKmGEo9Iwo+wdG6EeW0e0UsSOVygUA0rU8IUkbvaRtydRhSBlKCw0M6Sz15E0BvFyvYhP/C1846skb78eOrpgeZHh9Qq3vjZPLZtgtFZm28RpfDNK0m9vU59pYisGMbvBtN1FWD3OijzIjfNLnNy2gfFb34xwIKS0qDRTxExB7TIVp7OwhMBFJVAqaKtz9LBlMON7GCXwVJPAK+JrcRShU0+kCXlVtmXDXPtaqEmP15UfIcsKjV98H/WOLEajQTUVRtMHWNJtRCqKKM3jeQm0ep3gm/+Aa8UYHbBwmiWEAz2N04wqbYpPLd5H6d1vJv6mPRxN/Tjmx/4JN2i1s9tf/Rbyq/dhVZ5BFkv4/+k3MWcL2BfQ7FpBhYTWiydtAuljzxdIzOfYvvACdtPCDtqztn56BPHat8DyZXgjl8GSU2bj33yWeDngVO0F4lo3lhJ/SeIULxcXVi0CRbukx2nGPkgjKFLxFtGEwdXgSYeie3XbAk/aL0kc4weJvDtOv3kt6moFRnD5HpvvO9wWddUkskrVeyU3yZEuFqtJE6ERQbLsvzwlxxV3ipxzsUprIL22eap0afhF8oUwofIisekjVKot1OtvYvmZz2OIF61kp6Zg3TowDBQpcGWLQPrM28cxRQRXtqiWJIPLL4CESOdGThWGSFfnKA3s5aQcJBEBpVlhJNPP0523YXoBmH3thaWtoCVNtJZNY9dGgs1dzGQ6yS9v4anXvx87EkJZsujpFWh+lM2aw4hdY0s9z+nydlRVUNWj2Nkwmya/QWM4zJYHn0cL9zKjRVH0NMJpUfXiRF+kMD42KjkzCj9mZRBCIISgvL0f1zSvWnHq3JRFnHwWRvZB38b28Q3gJ9+rEIlp1GWW4pYNbG7OY6kGDc1A8+vsFb1sDes0NR9NDeP6FRrSZSicYLzv7UQXCjTNVcq4EEQiIEMWkXKNmOHwFa1AIw/NskLCOk382RfoODmNyH+DQNfxMiGk4+M7czTKYMQkwnapxzqp+RqFzi5k0qSuKhSvHyakCLz/9Q/EMsDKLK6i0Uwmgbb5csVMYvkuCEFryUS4HnYkjGNrSBngRcOIZo7Ab7FyOkC3QKoqLUXDL0YxpYuDhtRV1NalpaNWHazoJS+3e1SLZXoef+Gy99aB+yW1wtVvOtcBTRU4tkdPuMzyfKit6dIqMH77VpLzs5xcalO4HRkgFZOEs0SQryJdib8xS+/0GPqLAicJGI7D1vGjyGgSxQ8wnNX70261hX4cl3okBqoGGMiIjr1ysaiQtTxL4ATEdJ1A8aiVfgiea2v4V4O1wGkN3zXE08cIOsNtatQFsCIp3GSIn5z5PBmjgVk3ccwOFBFAs4IdHkCokCgFzHadwUJnWHTTm7yNxeIzLLLEQTnGjdtG+acFD/WnP8SGlROMvv3XOX7Hz/GJjl3MvOY18PB9MDfDtAvdfpXG2DSJaoUCITYa7W1al1CpqVFizTpHdr2Grn0vcM/GG9FOnOTA9j6CQhSEwHIaLDfWAeDUwLjMhCQlRBSoyDhG9+Fzr9+dCjFnuugFG1fP4ssAXaiYappqWMcMbCxVpabVOe0uoXsmWlTFXcyBCBB2gyBtgNaFhw/d/aiFAkIRTP7IT5N/7kFKHXGM8ib233ULdqab/txJlnNz+EKhke4FS0OMHKf/P0Q40VDJLTzHutBNTH9jAuNn7+GQEpD92Tt5ZPsfIfcdOtfH0jYMFUTVLLWVcXLlY4QfOkHuzm2sf+IFzqaYa16e8FIVBjZgVz2CQKKgXNETasWZJKItohfq6P80Qt3eSURNk9L6KX4Hr6mXjRdxvaQQF63GPWmT0PpYdkbRlRAKGr68vHhE0y+z7IxSdK++jbOtw+Tcy1sg/LBA4q9K1LcRVlMXUTR/YHBtaopB9IIeJ/kKSZK70iWknA+M44pKrvltAukx2bxMF/2L4AR1Gv7KFfv3hqzrGQpdj5QSRQZolUm6a88Rv/1tRPeP0WVuuWi89+0nWei7o/1Hfz+9uQRz9hHiWhf91rUktB7yK5JQSOB0dxB7ukBi2xR6Tzdfuvm3WffTbyKlt9CcBkvxJMsjm1FbHtFIO0AL6SZ+MoZed1gObGRXGKkLNqdSJDJRAj1AeDrXvltgKmHCokmFNEZ+mV+4toP/NpjGjmUQYYV9b383Gw7uJyJb8NjXGfFbmMl1mA0bR4+jKBffZ6oumDjjwfI0nHwWggAt1oW6aRfWVSpOSjhKpngA3v7vYPC8ZPv2HQJSXUTLLvVb95I4Ok/j2iEqpWkCGZDBQFMUPFUlX7Ip1hZx6SVrmjSSNo1skkooirWa0DJNGM/s4kT/FlJ2gWDkMay9Hg9+HKLuKEJIOk7McOsf/R2Fmzbg2+up+gKruMjTX4S85SFsH7vHJG3avPD2N+BrJpuNJNg2sZlJ/GaLdC8kGuM0YhG+vWknEkmvalAJxQm1mgRCQfUlStOhkUpSHu3haHkSJ51Et8s4nktQ9JGmSqDqTIkQ1SdW8B1wfQ0/GSG0NHbJcTxXcbogZpBS4uESOX6a+Y5+eg49RBBcHFQsnIH57/Dosl2JqgbsfPKj7P2Hv6ReWKDuh8k05hm/ZQeJ+UX++WkPKQPK0idz8CCdSgKrXEWqCq3eJN3lxbaB70WQmLZNKz1Eh20jggDNdfFl0FZAikYRrk892lallKpFEDaRpYufW0phmcSxafS+BOFimdniWuC0hu8f1gKnNbx8rLp4OxgYvodrWRe9rUSzCFXh2meeo96VRpufZiWTQlueJ7j2TnqzYarpbsr77icxP8BWBgFQVZ0t1jXsrEiG6eMGbysTwTKEwqR+/yN8tnwzTkdAOr3E15wij05Xyd/3Tb5811vZ5pTwnQpOJsGJ+Ab2ptuB00BCUBBJwnaLlf5+Bj2Fk3++n1r3Tk7O2Uw+qxBJCELFCnPJ6wGQweX7LkIpkEGEvEwRMcdXVevg9niIpuHT8AtEQ0nKtOioxVjvbqAS9jFp0ajDXjZQqqpsrc1S7+/EP3QUJfDBsVHjBugxanhYXX3IWpOwPk98cIDCrR9i4kfezng+wci29+EqBvfd+0GGhE+12YCIhp7PIUNhcvoKcze8h96HJtGEgWuDtz7O6Nt/jOb9D3DTB5NM3n+eMO/IBoYSIaJm8D/z9yhf+WfMI6NUX7eFzjPz2C2BKsIsOieJBFEwTOb/7mlmnq0QVi/f51RYrdYkZkdR995AfWSZY8tt+pKmmK+8ApKUV61aFN0ZsvowraBKl7GFuNZNxVu4ZFwrqDLdOoAT1ImomauKX2jCoOVXzykY/rCh4ZewlMRFr8XULire4hU+8X2EY1PTTCKrC3EjFKbcfGXkhF3ZIqKcb/yIKREcoVL2Fllxv7OR6Lx9gj5zN9DO3L/4eAkhUIWOtbIAiTjIAM2vgaoSVpPnxh39tqReliwfKLDvmQzLkxJuvhlj/3EGrD3EtE4UoZHWB6lOLhGOQCk2yNLfjRLb6NFjL3BQ7WbJ82mePkaQ0MlZYYrbTaIJQSzaPnZxy6KeSSAaDhWnjJAtiiTYMFhmQHeI6JCMrx7njn5C5TplV2Hbnp+hqi6jCkFh25voLuVAP4WWFNSGeqCYJ/ncl2jaeTpbIdzLGNrWY8OEK+Nw8jlIdsK3P0u/uQ0cjVD0yoETQD21haZ3mcpv93oyuTxGR5i5n/kx0t1b2WZ0Ub1A3r3ZvY6P//MpzpTXUynvpWVY2JE8X3vL+6it62nbSdBO+Zzqej1avIum0Lh+4nmOZxwiSdBnT1Lo68ftzeJn0xz84Ht47p3XMb9rC9ajh5C/3GTJKYKQNCMRmkoXXc4CBT9C6HVvZ/19zzL79rfhh016N0PSnaXWEafcE6MuNPoVAxFWkBICBCG1iqg1aW7M8lxC4cHjE9RSaaxGmWbTQWs28AwNtelyJtVL785lprQ4ykodJ2EQWr400jlbcTIscJqrRr7SQQoFR9E5/oZ3kjjxAqXZi01ozXDbDPhqqDk+IekhXGis6+YNX/9DyiJByGnS47XQmk2a5SqjjceoBD5DT+7jhuomUoUavqrR7EiRaa7gS3AvUOALpETzfJpbbmegWERRQPgBTTzIL+MbFsL1qUQz7XOohRAhHaV68bNYSJfpt92Km0lgVerkiq0fjmr6Gv5VYC1wWsPLx8ICVFYobNuDGnj4Z3nlZxHJoFeaKFGDQjyO6rdQwhUsT+eZ4V0M3Xg349p1DCwe5ZjhoojznkBGaied9SIzVNmcMnEDj7onsQP4+TeqlEyfabXKjVmV2//jr/DnN7+PO4caqPM5yukkqpnk4YHXsyHdvrQ7I4KclkGzPW7MlDipCsT8DH9f3wsRhXfftITMhJEBhLJXIIyvIjEIS/ZWar6BWYbKylM4rcW2F42v4Cs5srEOyiosfiLGib/LoGoCBZ96NSAsLLoKk0jTws4OEhs9iIrXVmJSoSJ8Djk2lq5hoxDR5hnuBr+0QnT9HeyMa3SVCzRbAfn+Tno37EIU5gjLOVLEWFyfITo5ix/L4OWXYWEBYQimo2l2Wgb2SoX46GP4VRuxXKDkzlH380SUNDJXZuo5l1TBwlhYILQpTqRWoy4lQbOHQes6BIID+RMkRR77bz9NTOuk6i9fcpzqfo6UPoT8ZpW5kydZ781zeuXylan2+AJ1f6V9DZw8+ZIvw2ZN8s2/lNQrVxcYsIMaITXOtsjdKEJtV9cu4yeVd8bYFL6DdaEbyehDFN3pq/7/uNZF3f/hVHQquJNk9KGLXtMV66rKet83OC2qF1ScYpEYi+VXhr7pSQ9LnK+Adxobael7WLRPEFZT554zvvQovCiQklKuKgsqJLU+5uzD5Jzxy1YnUyP7oSdFJbsZWWqAffHC7uTTMHkYakXJPT8HYweAvj6Yu7TL3584TUQpUkztYPtrTxAxa3TEA8IDKl+bm2X+S3/D3EAf0rZIJDRUT5yjzSUzMRzDwJUK1vw4UnjMLKzj+WgPjfokBAHZehIAc3CISK7J3es6MQ2TQ95+XOmwKd7BE7ffy/ZjB2lls0zvup7JiYP0LC/SWVEx6k0cvW0cb5rQakmCQFJO7SBTPtauKfQMg26QMdfjtVSshHXJfl6Ixt2/xOwqG9Z1ZZuVBZDpIVOvUdND6KGAtKeS6Lyda2Ibz302umcrb/FOkXOvYaplsbI8x4C6hK+kCIwIkXNsAUkQmCTeehdKy6dzaZpGuMXrPwDGxAT1zUMs3byDcKHGaKybdS2H2a5etHoT1a1RWM4hVIUycUh2kirlCQdpXCvC4lvuwunLg3AwmgXUcplmMk6fX2FJpulQdNQQyEBBApriolRbGAnJJucR7IRPJRxFc2xaLQelaSMNHbXQopBch95b5LC1mf7aFF7UQK9dGum0aiAiZVI9UFjNAzmyAUIjkIKO3izFTB/NB7960ec04/LaOd/43+cDj5K/Qn/+W7RSXeTvup2x3a+jK71IKRljfbkIgaRr8AQClXLgoJmC+gt/h1IpE6gq1WgnUa9B4EuKzfPJJbFag0pkO4hLhUARCCmx8SC3hKtYKK5HJdpuMrZ0HYQgCC6eP/TAYTnVSUXV8MIm6uLyq+8/t4Y1rGItcFrDy8fCAoHdBMskCBmgv8j0SFEod3YiBxOcyXWgKyZdYg7fSFGRLr4qaRgmqqkwFZvh0aUWfzJ/vvs1YnbxZHkKV/dIKgq/ebTOXy2P83fNMfqsZ9HdbsYdB1XA7+0M4agt1LExwsTxlF78AMxVdSVFCOZTm9BrNq3kNCd+9H10/+Y7OHXnHL8SG8ZZOolpuTTVaJsTf4VqE7QDJ9vpJVyGcpDCby1TmfkKAE4xguHVCKIWVSdNLKXQsTWE1/JQDIXlSvvBP1SfZCJ7CykjQmp5FmnE8ISCQLDo2YwUI0g9hB/WEc4KAtjRYzMtPK5zn6SzOUpFFYRjFXSlxdS2QbqqjxOWJhODXURHRukNh5jetAfvv/4+QULhhN7LzhOnOPD+X4NHHyD63jch/vAzLDdPMdM6ROjgGCvv+3Wyw3vIFbLkNg6iBAGKoeKFZjl2EAwlDEIw7k4QHwyD66IJA1+65Jyxc94/Zz1uFluS0MwC+2tddMcbaOOT546jqcRo+dVzC9IVd5KcM8bI6P8l+OV/2+5MBlZmJZ/7iKTmrZB3xi85H899NWD3T08yeeJFXiGCy8ogOE3J1/+3ZGw/cAWRirPUtquJX/jSRREaMa2LincFg5UfIKSUBDJAEZfP+n+nzKxAIbgClfEVgdOiophEVsUh0rEY+eplNJW/C0ja9/xZKEIDYVIPiqS0fqr+MjUvx2zrEGVv8SKqaSMoElZTAMS0TgasPXQZWxipP0JGX39+811J//77sPds5vPVHXjFJaifTyC4tqRrPSyvxmWhmGg391/hwaLMTRCqL/Pw7p0825Ulsf9ZzMkD3LZlhndP/hUFN81odx/hms4vdMVRA5XoanBgdveC7bGwcRirUcZPhJhXkyxKjYVak7hr41bbKn/xdb3ER9MolsFcMMNmdSsFucJG3WI2M4T13g+CJml07oXNSTrf/CF4+wfAbRGo7UAonRYUC5DPQabbIhZyqdclp05Intun8dRjCnW3H1W/esWpZ3OS2Zn2dXjyBKxfv3psFJVsxOBwYhgvrGIeOY5YWbxI3j2Z0PCb7fPWMf0k/YszbD1zmK6ah+P1nTs2AGGhUEtl8aNJ0stFgt4ciyWJdWaamTtv59QNexH9XTQKvVz/+CcZipQJpELcn8IsrYAqWPSyWL0ZouUSNCL8yngZVx+m+NohhBLAsQNQbVDp7yJOC0mW48sBlh5GW72+NMMGJOmZPDurJRRVkDeiqNKjUXfR8RC6xCk0aYa7ycQEanYrg6UlGnoCVV9mrnX0onu3VYOc9TyJbpfiYptOPdM6CEIlCATZuMKp296MeuiJq56Ls5g5AV/6H5LKStv6o2PsFI3EJhTVILU8y+y1d6AEkHKb2JpJXMmhyDgVv46fjuAv5ojOzOEmo5RD2zF0iblYZMVutre72UBIia9ppOMqUdNAKgqaH7AiG5Bfxg0E+AF1s91kHFE1pKacmxPOQsdjoTPFTCKJElIxFxdwZPNyu7WGNbziWAuc1vDysbBAxUuQ8ufxwwaBZV4ypJnIUIh0cK/jEMvFGShO0li3h1vVLE8EeZyYzfy1u/i90x/jT2emCSntqtOzzSZfED3sqE3xcKNBotHBr1xb5m3LL/Beu8aWfUf4hekRik6UGXKclnPYtoamSDbOjzGSuYYu/2IKSD21Bb3eAqPIO/sNPrXk0BmDfkvHbE2ScApUZS/piEJxHFIbLr/bsT7QqgJZ1KkmXNJdb0aROoHvsnEig9qq0wqrzJ2Is+NdkLxDslhTUdWAglOiYPvE3AbL8U1EUyFcYRLT43hCRwrJzBS0RiPUNRWnM4NeOIk048h6nlum/gp/fj+oC+QjKhF7E3V3kf29t5ApTnEiWM+JJyYI5avsGArzuL6LM+WN1DvDrD/6KJyaor6zG6/RpPfHtjOx6wOE/7pAh9gGzzzL4Xf+JQMf+SmOZd7K9M2bSDwzjsiGWH/oYZqKy6knHVxFoi9XEQO9hKM2y6dbtIIKCipz9hEafpFl5zRpfZDJios1l+MNb42iJkyGRw/S9CW4LsnFgAXnBOPNp5lu7kegkNHXsf7+BQr/4b1w9Ch2UGPfN6Dzmhwldx5futS889WdwJd4PceJR2JYg/O0KudTqHFVobXK65eyreN3QuZ44nPw+vfD7Kl2cNDwS98hiJCsOJPt69k/v7CvrVbpVKHhyAYl9+IqQmm5wT//cYljj31/qSNnFdtqfo6Y1nHZMWEl9R2rZCE1QTN4ZQKZy8K1qSs61lnl6FicSvWlVZz84OoeZFcqPA6Ye4ioaeZah1lxp1CESlYfZskZoeS2xU4q3iIJtfuiz0XUNB3GMCH1PO1xqQgxp8ii04fx0wZOrkglt9zuq5OSU0/DtluhOFIm1afypSd9Wv6Vt9ltOejFRd5g3kfQITly7x0sr7uD67/6+7QmSnTs9DjVMUyyojC7PI0d6jgfHGR6MRothNRxNnXgdESpxAw+HO/mmLGRntRmZNB+HkZiAscRdCu91KkzqKxjOVhmPDhIwmhQpB8kdFm9DO19P7H7PgtPf+WibU1n4Fv3ST73D5L+ATDueBPT1g1MTkque8s6WJq6Uk7i4uMaETQb7WrTmdOSzVvPn7lUTCNohHm6/22oRgJmRpAXnNntWpiZTAfx4iQDjVmeDb0Ob8mnVS2z0rjtgsBJsDtuMVXUaG7ZRGSxwNae03z9jI1arHBi417KsfXkf+e9xEoq3QuTOL0ppCPpkFOQz4MmwI0S7csQrlWZLNp8eMDlD2fX42txnGyS3MPPoC6WyO0ZxjDgBjPF3x/ycEhiKi6+rtMc7EZUW9SsCJtDGh0RWFSyYKhUC5KIUkE1oJGzUULdJLpu567MApGmzZKvYdUjGK0eFuzj545DqwFC9TG6ViguQMVfbFdVPYkXaGDaLO4Ko9RXzgUedlBDRooX3Sh1v0DemWDzm2fov/cE08egRQXdSyCDEImTOaYT6ynevQPVCdA1nWoqjjlSoema+PkR7FCImXqczpPjFLYNs6RtQ4kbxEfnORUsMEoR8sugSDxNI2YoWKqOrygEMZOV2TPg+7iVCiBw9dUEhq4TmDrKBWI/gee3TYVjUfKJKKqlEFmexwnq3/nCW8MaXgGsBU5rePmYmmLJHcSoLSEMBT9yKcXNUkxa1TDRA1PMXlOgc9lB2XgdCaFjonBdZg+yGKDh8VtH/oFbTn2eTz31AIcOPMrrrASxcIpsa56mHWY8t4++px8iduTb5N/4ZrTcLHvULM+7i6gI3NGT1Lqy5LwwpwZMul8UOIWMDMKVxP0CMcPj1zabpHSFMebR7AjxUoFFYwO9McHiYei+9vK7reqQiECxbFGOdeE//peouQpOdYR10kf1fV4QTTrGs2gWPFrx6M8nyWWzGF6Rp2ZPolgeUbNMuKOb2tY+YjuTuKoK6DRrCoMyzem8g6IqrEiTXH4/3soLPNn7AUa6ryWRTFBWdcL+CorXohXuRJUBD9h7uSmYpjBbp7s8z8Yfcdj62V/F0xUafsBj7/mPdGclldVWks4bb0ebnmbyvyyxcKzF9jvbpp037JlgZShB9IlRgmyEbU8+zbo7XJTcLDP7ltn8V0/RvPV6em+MM/aZkwyHbiVjrGPIuoGyt4giVCJqhurzL6CnI5gbduFFE2wqL/J03oOPfATj/3wWJ6izMfQa+q3d9Jo7iWmdmEqExs3b8J55moNLD9J3bQVrwzjh8g46jc2suJPnFP7GD0LHQJPofc/QVVdQq+d7AJKaoLkaONlBFUuJ8q3qNB3rJaFYe8WQNYZZtE+e86g6Wyk7Z5AJ9JrXoAiV0fpjTLf24QbtjGbRnSGu9QCwPnQTdlC/qPJ0+k/+jKH0R1leuoyG8Cp86V4y0V9JaOOlwA7qzLQOsOycoeTNktT6LxnjOhKtMkjOHbuiOAZASEm8KsqH5yGRqypsAHoig/4ig8sr4WvPBIxOX51uWC9LXOf8eZRAp7GJkJqkFVTxpUtM7SKqZpEEVP32uXODFppycRJICEGHsUoT+7M/g4MHKT19iJDqUWoI9nzjyzgxl3yuCuk0FAosT0HnOsHbdj1ApvA05lc/jSgfoXa2gf1CIYxajdjyOHYtx4Symdl4Jyt9HdjXN5i9rp9n3/sODty2g9TxaXYePsjy9DFaxiass4rnyQ68pkVWbRFfKeLFQtzaN8pUMMb7pI0hBkgkz+8LElJKmkF7C64jEMAudTcdhsefzSZxhMY1RgzRM9zmdEUu7pNLpaFchre9U9DZBZ3rYkw1hgh80Po30sM49Zfaribghefh9jteJDph6HQf384tegZiKZgdoWl2EVulJ65XLQ6t28HrT/0Vzp4bGZ5PkGgm6DDmyRpJzk1HoRi3dDqMLsWoXbcF3bHpWppnetmHIOCU2cGTDDATauDMTbKY6aHW0YcMPLKNGRqTU0hNo+FEeH42jI5HkkWOf/kZAt/EjWRpbluH++Q+aDn0Lc6zfN12tmbDbMoozNQMNBHQzMSxUwn8sE4pkiIUiaHLgJVWF82BND3dS5iVRYQJtaqkS4vzTFGwPqaiegpHt20mWhnDL2TwOZ8gCqQkpmWpq3PYPfvxpceAtQd7uohtRakrLaSi0YqlYHKEqrfMfPMEdEyhJoo4rfb1WHRnkJ5AVVV6t/jk5j2ajRaWXyUZ3Udh3xPkBzeTOf0QxS3DKPEszUyMwdxRGs04sel9mA+dpIyKNp5n+dqdfLongxLRsCbz5MU8BZoEuQWkBCccIiIEihA4qo7TmSDy1DdACPxKDlSBorRPdszQCAwN/QK9cSdfB1XghSyOudsQBEQaKzjBWsVpDd8frAVOa3j5KOSppbsJTZ0BBBeQys/BCGmoaQt9cIlYMosrNDZa7SzSa9QO1m3bxkhrJwt3/xKJbpu5To9Ntef5qflnaH7295ha9rjRm+GYWWfHwTM8/Pp3Md4TRREtHu6KccPsCZrNITaIXrrG9rHS04fnafx07wZC6sUTcUiqBLqOUWswTY5c4BJRYCE/hREPY9SbTAYbWJcU1BYg2nPlXb/55wX7ju9m3dIxjiTiVJsNWsVDvLn7fmrNLK2VMM+lNH756QbHSx6ZSpTTu65hOawzkHuEFaOfLr1JORxghDQqQR3XUPEdAx0D2zI40rLxFYfR7hT+aMDze95BR3eZx8VmCnO9+IqCGZQ5I0HRc8yu6+RX1z9C4+7Xkd8whPj2A3iBB3/764z7ksGeCDMth2wMFisaJ07X2XSDoPvPf409tzeZeP2H6dss+Oc5h5A9SdBnEegafiPAyOpEPvUPbO6ZgLlxWkOvp3jzdpRUmOTSEfDb1DYhBD3mtnOLTPWhp1E2dIERxQ9MMkGD+UMjnHSug2uuYcvSOoQQKEJFXaWU1YqSoLiFsYlD1B67C3Xns/Ro11KYbS+yB6w954x6F5cLrP+nr8HGjWjTs1z78U/S+scv0vTLJFSFhu8wbx9n0TnF2N8G7PrY1+gcPn1Obc9SYgxaeymvikTYQQ1TifLFfzuFfd+32/sgNFL6AH3WNWwM38m8fZycM0ZaH+TRT4NvtwOdLnMzZa9dtXCrOZY7M0i1Af0nLnsN1f0CM62DLDmnL3p9ovksTvDdiSTknDNsDL8GQTsAE5ehhT33FXjs04K0fS1zrcOXvH8WphK7SK7+VUc0hVUvvaShXqOMevILV3xfAse+DQe+ef61tKJRkB6aMBiw9jJg7SGhdSOEoNfciUCsBk1XkamfnqbWkcT+zN8jHnkE40dvx5p4iHj3BjJuneNnWix5/XhjU+f6dZTREQpd69m18ww7Zj/C5FEJu3fDgQPnv/fhh+g/9ThqTLAv9VqihqSSUfmr27ahdwi6kzVm3QgD8wuUe1LsaizhiqHz51fVEG6I1kAnscUV5qxObHcQ4/DTiPoKs0sdbNl2/lqohwfh+W9S/Zs/4sl/HGWndi3f/IqK54b5gP5lFrwO7JJo0wrf+kuwbid+x3rUVXHGWAx+4RcFA4MCVW3/OM7qbRWOkY1UKb20GBhNg3xO0tn1omu1Z5hEY5IduYW20W5xmYI+RCZznn4d3Sz4cnY3I1YPm/s1NvXEyEz14AfR80FlLIVll3E8g77tb0aEVXqeOUKzZiOAlqfzTm+Ur+Vu4nWNxzjTfwv1miDoCmM8fRSrkQNFsBx0klAjSKFy8+wx4tUjZJY9ZKiDyrZBEnaeIBPGtXSCaB+FssU1XQoyEKhAubeTDYe+TfN1myhaacz119O7NIutJMhtHCBeLxMsLCPDGlVp4A8v0mkKBAJNCp6780YsN0/z6ASqMM5TiEMV0l9/GuMfPg6rst9BIKl++wzNvh4UBIqQNBIdyJMHKbhTJGvXk2hcS2jzKLnVFk5JgFYZIqn1EtM7kNE8wekxGikT9cAk0WMTBOkV0gcPo6/vRMsOEvTEsUYXaDQrWIs1DEviLgfYmoGbiPOaeBoZ0vGqoPglPALKi0dRbJdmPEpYUcCKUA3HMFSJOlPBCWw8t11xCpttgZewIfBNDe0CL7bWUgFUhRU/SrYni+L6KIqDu0bVW8P3CWuB0xpePsol3J6Ahh6iFotiqpfal4cUnXjGZayrlzunDVY2bMZS1IvG3LP7Ng6dOMxU5gbsiskTd9xD5R2/iLK3nxsO3M/0c4/z/zv9JSrLOfauu5PMYJhjpXWU+65j7MwDKEGLwGmhBy1kpUk4CJGz0wwmLr6s0yFBI5WmM7fMqLfEZ/zDxNQq8aePMLN7I6aULDVTZMOrakxXERoworB3VxfjynbGOt+GYjvopTK9WoWJeheDEyF+4x6LbZrGdqETJkkxkeHaDo/U9BR6qUJNUajPHKfDUvCLMzTjEYr1CMueSVSFzQ9cRzObpdfIsy/+Czyt38100GJpfoWTXU1ssRUpPerGJoyxKgupNH5uBKejm57iKEv5ZXqXVqhgsG3sYWbvvQ4ndAwEbL1nB4WHVjnvikLozbdz+7tUfCl5LOdxZmaJlFfB7ogjV2yU9Unmb9kJf/4XaG/fTeaDb6EeBnSVTmuJmRO0V02LF6iPOQ7lgkIi4eI/9QQd5eO45Tqbf+f3Gcu+AfmmN8HXvgZ//ufwP/8nPPggNBrMTloc/moaGdzA3Xc22B59A529FiuryuCKUM/JqJsL9xHdexts3ED99a9h5b/8FhP7H2KhOoF0DtMMVujUNjBk3UDHI/9I+N1vZH7ft/H/6+8Ss+doVuVqH1Pbj2nJOYVS6WKv93XmvnCEox8bYW5E4rQkYTWFKjTiWjeB9HEXu9BbFZZufBfjB85WD9oUsuW5J1DWe9TiXcRWJPly7qLrJ5ABy84oQ9YNl1xbAoWCO40vPc40nriImng1NP0yAgVV6HQYGxm0rr9kzMqsBAH3/Dwc+ZaFpljnFmC+dBEXTAWKUF4do+JzeNENpmpYQlLxvrNC4VBwiIa8jFfABWjWoXKB9sd61WLCb/fgZY316IqFEOf3N651M9M6SIe+Wlk6eRLGL+6paz3+bb6wextfuuEuFm64i2D8GN98840cWH8vA3aD+MYyh4t7Wf7882y5GfjUpwCPohUDUxBdKjCVa8Itt8DTT8Mf/EH7vnn0ER541x/S3NJNdscI09kuhucXkFWFkOJye6lK3UjT06dAn0lY81Gb8Yu2zbRDTAzspLarn4P6Fm5cniI2Nka8Ick34mSz5493LnMTRFPs3/ifGaw/y8EXAobWCUrVTcyLGNPmLRw6KFlYWD3/63ZQ7rqOePzss1EQCl18/rZuF3R0rqr8xQK8l9ged++PKLzlbZdZgvRuIl4dhfHDMHwNDF/DktdDKnN+yDsSaQbSP03HSJyBQTBDKlQcWq52PqiMpaGyQthXqIa7cfu7GPr2fm6aux9fCAbUCZ6RScLF7Th+neNDm1jMayzs2EL2qSNcU5nAi5gk4ia7MhGaWoj1cyfRs03uio0zuRRDhnTosKAzihMKcUhsZmxRZU+PQsTXCFCZ2bwJuz+Bl42SNzqJdW4nXVwhEvXxUlFouMjlAiQN6sKgEHJ4W5+BUHR0oSIVYKgT8fhjxC9Uxozlifz9P5K5/j1E5soIBCPPQIccp5buIPPcJJGox+zADbSOPUNaH2J+RNC7UZBMRlmatbGDGroIUclBLAsxtQMZW6Z34RThRw+xWNYILZQYiXWRCGw8W2BYKsFQmtjCDLZTxfctNOFj/vvbKd+xjf39u/hQTwzPMlAqLqoXIKWktngUJZA0w1E0ISCapBUOo+oKXk5SCspIr0YAJFaNkTUVPN1AlQFBq51UcmdmkLpKzUuxYUMIxfEw1HYP2RrW8P3AWuC0hpeNoFxGW95H7rotGL6PYWUvGRPRI5zsXodph6k0y2jpS6lD3V1h3tIV5pR/PZa9iWv9GE8EHsd3vZXMG/4dYssGpjakeHDbT/HFyiiOtonbm8/yo8//PWPhYXYd/ydOPPYZVrYNElsu4IfXM1VWWJe8eGLvjwtyPcMkZlaorZzktXoP6SmLTHOM65/+PHY8ScXJXjZLfzm87YMK31i8BeNQiqPVG8j7Ac1CjbnQJtxyCj0EEV3QcCHZ0YHtmvjROE/e+EtkimU2fuOfCU/NkzY0UrkpSj1ppqRJU5i0ehwWtmsEdi/CEnxez/OjvQYzZxx+uv4NXnfwaejrZbppM3pyE9cvzVMWBlW/ysMLNsff8B8Zj+hs/JP/xfh0AX1ThJ58ncziGV6o58m//jX0nHxo9UQGuB/7n9z/8af55fta/Er9AN9I7mHDyTPUMhkc18LyHKYG0vCetzO/ZQff+Otnaa4ucJN378L507+Cj3wE+Zd/2f5O16X+G3/E2GveAGGf/JnHqL3zDjYkp3n0Lz5C716d/LIB//7fw7/5N/DhD5OfdFn6tT+j1rWVm98GQ//hJ+ErXwEgHBec+soiE2/9bQB0EWLJPk3i0DGUe94AgUdLqKTCaYY33MDMZ9cx+oU9FOQwmu0y+U8j6PfsxuobYvDt7+ep//JBNh39P4zuO38+Z+1DdBlbOPDVCMM7HCbv+BWGJ75MayLHo59e3S1bktT6yCqbePqLcKvyZXr/4tdofPqrlHOSsJKmERRwJwqE9RL9n3maviOPMrE4eU62Htr9RyltACHEOZGMszCUEK5sMNM62KY+Tnxn3yGAJWeEXnPnub+FECxNtJu8oU1D3Pe5Ore+E0JRgV2HuNJ7rjdryR6hw++G6fMVsFc3cLoU60yV52tXl6lvORJLtHDFpYkaWKU6rtLRMls8Fsfa+9CnGMz5V/7uuNbNcPgWdGVVCe7hh+ELX7goGTCSG2NqejMTr92Cuu8Ai4le9HV9nIiZkEgTXpxi4IYI84cb9AxLSvkC39JD1GMt/vC2t1Lr76Q4+pV28/s3vwYdGfj4x2nm5rFvWKGWSGC4LYJ6H57jsvF4mXykAz9ocHP+DOXebczu2EjxhpsRL5Lxjhsm00NbORy/jbn0ANetLJHo3EG2oeHoF1PthIBg681IFNa/YS8nvrKfXdeAqnv05CvI7BDHjsCXvnD+/FfKEL/4ay7Clq2CG29erQZZYaK96SsPfikIRQg7y/iO0zZBvfNdFIsq6Qu+tl812bRJYfJxo01FzPSgJxP09V/wDO8agomj/Pyhv+HhkyOIzX0o/Qluev7/0IqGGTTzdBcNRmYqzMo0Zb2TO4OAUqIbremQqs5R70kTEyqb4iFO37SNXGIdcVWycXODQzMGni2o79mAuyHDYrKT/xQdQLoqpiaI+TpNzaCczpBcnqfQ20E5SPB/D0uiqk44VAchcP0ApVYn/tUDuApotShC9VBD3bjRDJuWpwmiCs5yhYiaoeEXKblzKNoCouVg9G8nerpEShtk4QxIUaHzyceZPVKk+9nnmGzdQWt2lKjawdIkdK2H7lQPZXeRRfsUncYmKiuQyLbFVNIbKujFOuFvHafsmaiBALPJihLBdU0Wq1n0bASjVccPPHS7QNAKUEYX0BSftz/8WYzyIl4sRKxcRqmrSC9PqGHie9AIr3IuoymccAjFUHFna9RlFRHUCBSF9GrghKphGyaKCk6+LcPYmpkgsHT6lSR96UGkohB21qpNa/j+YS1wWsPLhtuUWG6Jue40qu8Tiw5cMkaLZnDTw/TOneZ0X4qNPXsv+12Jzmv4JfVhsk8FZL51kFpOkBUmn4lLzKaHNbyRk6Ek/YbCQ8F2dpZ8nsjeRKY5QcSNUzBy7O++iZ5CkaGbrmO+KumJvThwUpi3+kg2W4iZFJ0ln62P/AULw4M0r/9Jzmg7yWQEMuDK3eUXIBwW/OH7dI4mPZ5K3sNc3mdmJoYxlKTVyJxrXk9ZoPekURsBfVY3ek0hc82NLN/zs1R6+gipYexAUIpHyAmLmKLx/3RHKXY4RK315CJd7NHHOfWgYKA8yn7txxnZsplvLcRJaj6P3Kqxxckx2+rhhWyUnygcIhvyefb29zPpa3SMHsV99200D54m3vdG1k3MMlE+ilmdwZsY49Bv/Q/+sednEI2D/Ph2ydLDR1mwrqX/0ChzPRvBS2G6NvmVdq+O9+A+fko8yCdm6kgZoP7oG1nY9Xby7/8tnnsqQ2G8ifzIR/hW579h7+AKerJEuu815LYOYMV0NjWf5/6+PJ981GXfNySnD+rMnVHY570RJRmn56076FovCHVHwT1PzfiJ6+7H10PIep0uYzMxrRNR6gClrbbUQiWqKph33sKdG/fxzsFHODVuEux7nvqXvo35gduIopMVYRRVI3zHdupHpigvS8Jqmqjawal/qHHz4x/GuGk3d31AIfJf/j0bRj5L1zqYOiZ58ONw6EF49DPwunsXUYp5uP12NndMcPIpSOmDzLYOUz3dYus3D2B9cBfmsw+S/d3PM3/oi0w2n+dM4wmq3hJxra0YldYHzkliy9WLz1RidBqb0I+cIvnfP4kd1Cm5czT9MkV3hrwzflEvlB3UsZQ44kLBhFKJ+b9+iOf/uf3n9EGbW576MMojD4PnseUWmN6XoBmUkDLAkzbGyWPwH/4N/MqvQKuFQFy2D8qTzlX7o75bZHWV6VbxnDrj5TA2Y5NJG3Q+93la9UsDoVZQRYoIru6y76ZjHD/TXkypQlxWZfGq+LVfg09+sv2757FYb9La2CIUg+TccU71bOCayUO8a/JRnsumiMzM0b+zRerHbiT4r/+Nf8ru5O7GIkc29mJGPb727vfw2uIDjH7g12n91//O6GiI8ZkkC+Eotx6d4vS6YaiBVw2zGOrkFv0xWpH1HDN76F6ykV3Xkg1FkaFLg8ZUNorquvy9+m78OGRVDeIZnKkx9OTFEU8mK1hZLWRqG3byk3ecRFVgMJrBPqqxu6eX975fsGkT2Hb7mlpego7Ol3jctt3Mze/e9p3HfQe09r6dlfR1AHzrvoB6HUzz4odzX3+7aCeEgK03sel9P8YNN10wRtPh3vcT/dn/SO/hFwji3czs3UlPeYale66jpUjCPR7Xho4wFRngdetj7EpqzGj9OH1JTNVjcWiYKBqWroJIYe5KIiMDiPos7xj7PKfimwinGyyu72Up0ovrg75KrBi2DMb61tOdW8DNRFnpyFAPTJbrEqSOFA4+Ck1fQbGglUmQLC/zgfmvMOkvoEZ60Ts62ZafxDUkeB7ecpF+61oK7jTJU6fhuht5Nn+adMyhNJ5EBhJjuUR+oJfPveZdRA6NYkXDOF17EI89CLSPV2TfKWTXGcJqCkWoVJZ8Yqv5zz0Dr8FoVakM95CeblBLZ9nz0EGCcBfDAztYqCSIKBJPgq+UMGtFnJCBPjtP0Ytw5rpb4dRzVIZ66SotsVzMELhzBL5EqdlUE6sRcDSJr1kIXSFo1pCVGsJvECgKidAq3zUUxVF0hCpwF9uS7P7iNEHYYEgm6G208C0ds9UgWCs4reH7hLXAaQ0vG04LIrKKopZpSYNUpPfSQdkB7glMlFtuAd9GN+OXjgFIrUe99h3c9I48g3qB1GMjzD0Z5j3qAAfXD7N+bpZfXO9TntjAr/ZGOTm6wIONfqQdYX96PcaWm+nUE+guHPHSOD6oL3K4z4ahygBCh+hUP3MnnsJOpbF3/iz1xcOEQ7cy2CcoTUFy6PKb+WIkLMFv3WGyqUeh+9Y3sHTPALdtnaP8xhALNUl3VLCrS2XcFegueN09bFy4n0Ikxu8+C6PZnShL4+ieTjoeJu0bdFiCtK6wLaPyuUaUxVCWYX2G/bfkifk14j0Jtu79Mayts5xpSe4ub8TrcFiqbKbWmaX/9Avc6DwIeozmG2/g/j//A1qaQW3DHdz7wpdJjS0SOjZC4bqtTP/2H7I5DcPT93Pn3u287sgXuelXfoLffbMgtlDkTOR6/MGbMHNlUqcmOVOrE66VGXvnT3LdU4/wZCsEpRWu/eluvvwnCjf80jqWf/VPeTz+Iew3pgidGiM2Nof25p8gn0ihJVS6z8zzo5sbLKd8ttwE4QTUS/D6D0DHf/9l+m66IKWsqpzl/IS9EsH7f47qb/wx4qMfJfTz/4ZWx6r0oe/RFGrbE2jLFvjoRwnVy+x64otM/dHX2TCwTM1SiPkqzVZAqpRm9N7rufHE/0vtp/4tmWqMpNaH+sS3if3xb8JddyGl5B+eMZi0Brn22Efxf///Zefz/wPngSfo7qwS/cLH4Fd/FQBjsAt3ZglFKPSZ17AyGyXi1ziYsajetIGJu/+Avm+MMKhfS6exibK3cE4mXBPnqYKtoIqlxOk0NhFWk7T++SFYfwOn6g9iB3XK3gK6CGEpcaZa+871QuWdMTruOw6/+Zvnj91TTxGZP8n2Q/+bfZ+rU/69v+G+n/w9Vvadgl/6JQa2CeZPCUCSc8dJ64Nw8FnqZ1aYvO1eeOEF0vogxVUj4wuxZJ8iv9pn9lLxUpq2BRBVJphpHbyiFHpuZoaoFRA7cIz8ty+txlW8BXwlSz3W5F5tPdPJ81THkFD4fPMlem5JCYbRptV94hPIP/xDikMbiZ18nnpL0hGuMjgwS/fKHA17CTeSwncDFsZP0XrLrTySeA2jjsMBL0w6Y5E2dPYP9hIejvDg+v+HZ87spuc3foae6BRHYjvYpJ3kyPD/n737Dq+yvv8//rzvs89JTs7J3oOEBEggQNgbBAHFghNHXbVaq/Zb7fi12uG3k2prWzu+jta6FfcGERCQvWeAkBBC9k5OcpKzz/37I4oiSKBGg/h+XNe5rpz73ONzzp1zzv06n5XD4PJycjUXR0Mz6B6SRHbxNmYc7iTVoDHNmctEczbO7rRjczh9xF6QT3RVBYmDuolubMQTO5hSdTQHYq9gcP7xn4Vp6VD1ienJ1PwJsHMlYzLSGZtoY2SGAYdTISdX4XDph697o0bcyQdpPJEzHmNSymmu/NkScpxU+PPQNI3mJti7+8SrYlVVuPYG9aM7n9nGWmc0ENdl5ogzi1CEGe/QFCqLCuhqsVCWHGCYdQ+VQ0eDosenhNgTPxZFbyB8qJ3W6AQc4Z73q6lxJJuVHDoSB5Gzdj1eawI7UtLZkz6G6rQUFJ2FzTUhRqf0JKecKD2lsTlEuto4cv0cUBS6QxZuHKHniDWV+I56mhUH9alJkBPNkblFDOysJl1zEC5dj6IaiYuLIaO7Bex+UuZnsvvSP7Htob3EGXNw7K7CM3kCyqalxKfDO/+A4SX/i/PdHeyfMR1VMxPWNCypHursl9G9aj3RlhZobUX51a+gPYFYwwAA4pY+irH6MBzaA0Cku5W6C8fijYqlzp6Ia0MChqw4kvOnEk62YwiHCOp0JG1aS8CgJ4yKsb4Vc61Ce3oGBAM0ZOYS5WmnoyVAomkCgWA3epeHuugPh/aPcGAKq6iaxuHBuSheP6rmJayoRNk+PJe2KEIhHYG4SPz7twOgNFUQjLFi2gSWN59AM+nRBfwEus/OycjFuUeCkzgzwSD+Lj9+p4bR303IaEJndZ64njES3PVE51xAYfasU+/THAWZ04iadTkTHLWs8bbxv4caiLDn4WuuJKetA1diOaur1lOjpROfEaar8XzGlb+NLz4Tx4GNdChpVHdofLvIcMLuFUVBU3KozRtCduPjDNy/iiMDzyPOGMDS3IE+NZVsp8r+lyH5xO4hp5QXo+LxDMSTWUBewlzKCLG0NMTUTB0DnAqHW8PYuozsi8tkTexFrPTE8A2bnbWdOTTGJqLTQrjDVlqiQlwS39Or+Zp4K4UkkhkMo+9sIfvAThyRGvYUKwOJYrjOT7grm/MHGPEoFiJ9RiJboYR4dG6Vb0bsxtjSwJX+NirDKhOMlXQPTGXVpJvQMuNpKRpJxDN/xnLnD9k18ypMhw6ydF4ung0raHn/JXThEG3hEYTiBqA0dTF6y0ZeGDoCm0XhAZyM8jby+oAo2LUBR4LCTfe2ok4aDx37KU5NYndXiMxQKUpcAkfNTbiULsJaAJ+hk1rNxYwiPXXGMKmDFHLHKhgtCnW7d9H8/L/4z7pDhDUNhg2DPXvgT3+C6dPJnpPAtlG/JJCdRt0VPyPaXkW4upxXGupo1XcSoSo9F0/33AOXX87B3/yCrF/PwXL9Zbi3bKTmz89R+tCTPL+3gz2E0X3zakoW/gXPP5/k4HefIM1Rx0dXhzv2uplgOcTGgd+AW25hwOK7SX/iJ4yZ1kH+tr/CD38Iej2H/X5YsIDEDc+gaRoR+lhwNYPHQ8zRToJOhZg0aBwxH+29Vdj1iaSbi477/3EYUmkNVtIdasMasuG553d0fv9/2dk+lv0laeS4ckkw5ZJoGkSEPpYIfSxp5pFUe3cTCHsJNzZRu6qB9kmXwHvv9bxFSw7TeNEdJP/qWgpW3Uvc5DYy21/g1cJvw9Sp0NxMVDzYXAW0B6qJ0McRKK+kPCOfHbtqYfdurDon3eHje/lrmkZIC/ZMsnmaukNtVHl3UOHZcmxod44bXLpHQPPR3WolUp9H60kCG0CEr5Lw0vfo+NWPCGxYdcLjAc2HppjojnRj6Tx8XGPDOSYnSreCRzv1xZXfHaT+qEprndbzWuXkcPSeu4g9XMuw7buJem899jg37TYzqxwjWW1MJxxvxFRbT0t7ObvLNaaN9HDl6oc46M3AYO7GUhVDMGjkcHQKV0zbydKyTl7brrHGXETHeQU0+9yct3U9rUmDSCwv5UrtDfAECOn0RKQOJ3LGXT19ixQrrjobiYnHv3pK0gDimxqIc3SQWVvD8pIhHCiPYF9lDInHj65OQiLU1n7ilUnJAZMF1ryI0wkGQ8++MzLhUMnH651uM+a+EhcPdXUaFeVQOFzhtv85+aVKesbplSsQm09YteLsCuBLiqbG6GTumqVcZExC03swJVg43+Rka6ibg+Pj2azNZMt3rqYyJpNYY8+xIzU93eY8opMHEoyJoyUvj7nuWt4eORODFqLdZ6G2QyP3w0Es0uwKXiLpMpjYEzMZnRqmyx9FrFWhwTmc9NYjHNEyaJs2kIYpRRzJS6dmZhGRc79FVGkxaBo2oxFzMEBZ4UisFduo++GvCb32BkfXxRDhaaAzM56YIxUQDnHzn4PQUUbzvNkkOjr5bfGvIUohIbSLPS0FHGrKYFj5P+Hen0JREc6mNDo+/C3B2FINb7wAy1+Fbjfm9k7cCbFY6SLN08HsqOW86pzPVp+X5kg7WihEuz2aJttAQp4goRYPkaoBo+bHG5UIikKnMRO9Aayhdo4+7SQ+oEP1BWgzf9hs32jGZrIRjrRwYOpwQqqesL4nkNk/6sZoiyKg6Qkn2dF2H/6wrLUoJj3xH7xJdVUuYZMezaiju/aLHAlUiI9JcBJnpqGBgBIiHGOju0mHLyIClJP8GxkjIOBBsToxmh2979fiwGxJJDIzxP8MbiVtqZNdG4zsGjmLDwwN3FC9F9Obm/ggaSAOfSwHx+tZZr2C+O3vobn81EcnMS/3sydd9OvsZDsjWZI9k/syf4LbOoKGbWvoNoyhrC2Mtl4ldx5EJH7mLk5qaILK7gaNLF0kVZqHdPS0mwKU+YP8pc7NzpCP6A4LxV3t1CX7KO0I0ZXfwsi0OF4fPInFU2+hoysKL3qGRX88eIaGgTERDrYNmkLeYSM2i0p7QGGnz0dW2E+MLoNMh0KFdRazrEtY7JlNdGYE26tcRDXsIyd9KJa0eSS1NPBExjRqCZJoc1OZnMWQhiM811LFPw51cv2WB2gzeJj0yHLKs6NojPASzIgmypNF2OigYfwQjEEPeXu2sLRgNKMdZew9/0rGb17PvvIa6GjD97cfsbGlGfX39zLn6F8Zv+whnOW7MEybzlF3K0OfeIduTxt5bjf6vXsYmeBlRWMA6qtA0/B6fdQufpa65iAzD69idWOQykHD4Wc/g/nzaSocyBJ9GYq/jKO7fOytKkD95u3sXfk+7a5tVHSHcAfdPcM8Z6cD8F65nZbGDkKDh6B//lUGGRtpSvQys/kd9nnq6Jw8ipHfMLF32PfJchbjcO2BXRsB0L35JPg2kBF8iXKXmboWjeomDWXePPjlL8FmY1mZj7vKmglYrTinD2b9T7ax9/0w6WWrCceYSa3vRrMbyIwroaSzgPKHN6O5XHT+z5OU/vrjId/s+gS6gs10hOqx/P0JNsffwp4Jv2DM/ZNwLBiH983jR95jyRL0jz5OmnkEFZ7NqP/ehOeib7KxajRd63cSDIZpPArZIxVwOCj7x3fpcMaxIn8Wtor/o3PUJFizhuGzYM8KA7m2aT3vj/3VlM27mLiuo8fmfDEqVrpCrUDPUN1lnrXEGDNRUI7rt3UqzYEjDLBMINMyhu5w27Fmhp+uP+jUXBx9LI3NlRY84ZNfAJm0SvTlFUQn5kJ37UnW0AgFNGJr1xH09Ixu99GQy6GARum9kazyfjx314trQrR2fqIkFRXU3/x7dHnZbFseprI0BFOmUL32XSp0OYyPqmCKrYrOTDOuEAQVH9maHlNHA51RcajuI8SavexZu5aS5Gm48hLR+z1ccvABbjywjOpwEOeUudx/xSoWxCxl7Cw/QyLqUex6tLhICgwdNMVcjLOxmtw9taSPuRQGFB5Xk1JdpXFChY6qEuFTGV78NtMScjAYdcyardDhOjHw6HQKERGf6rOUPxGKzocplx+3Xlq6wu6d/dP+SVEUzGbYsF5j0BCIjvl8wS3/wnysh4PYW1yEQhqj3Qd4bsw3GbthFfVD5+HXNPSKwtikQZzX6eHQyHwSujUOm0YyPLHncznCAN+JcJLnSKZk6hVEp7gINjVh8KmoPh0tnigWFnw8OIXNqKDHimbUkXUQ2iKjMentKIpCrDEJRQ2jBhNxhlppHpqG22CjNdDzuGtgNhzcgs4cy87IgZSZY/C21DBo+f1oYwspf3IXBmOQLk8blmW7cS15D6XsAJ3+MB1GO0XNWyizZRGVFYFx+ZOMrPwtwzzLUaMSaGz18O70SxjYtZrDO3omBg9FRhHcu5uWfQ3w3D8IWo3EN9WzM3EsCc5o6vNH06BGscvn4/K4eDxYMFlhU1YG8QcOEw4rRLy7B+MgE4awHeIzMHbbUU0qEZ3NNGoafr8HNI2gLuHYeUmIcOCNjmRo836iWrxomg9/bCQ240c1Tg7cOiu6eCuB0p4fVAzdLsJmA964yYR1NjSjkbDFQOeR4+fTE+KLIsFJnJkj5YTMQXRRBnyuMP6oyJOvZ4qEiISTP/YZFEUhIvsK0jz7ufjivbhbQL8vkcaYLN4JTGfD5Ctwx4ewKAaOxkG9IYKHEscRro/GHT+AONspvlwVBbNOIWAexsgiGxF6HYW6OkZMHIPbD/6jCnH/RdN8i0HB7YdcIjioddDVpMPgCLO83cePUiJxaWF8gyI42N5Isqmbu3KTSd+rI+Dx4LFGkJTZQps1iDNgw6T/uPyRJjCadExyJbFyYRRNw6ZysdVAS9BPWshCttWETlXo0A1ATY/hgrq3eT41h1q6UOrdKEMm0tJ5gFp9OpW6JnbbcklsLqUhIoXIOBs3tTVxccOzHP3GXNwDY7F+5/9hyxtJxchUNLOReMWILi2bUNhIcFA0uTu2kTXOwoUrlxBZ24jmiOCQq42KRx7n4I++j/ndV0irX8n+q77L8Kvn4clMJqTXEfXu4xiCIYLRkSRYLQxZWcxzTfuJ3r6WmjVrWf3w/Sz/08PkxUfjXTAW9/Z1vFPRzcvNKtvv+n9szzKwh1rObzITbXiF7VzF7FsUYgcorJg+g3ErNxG5fhsH162G5a/g2bWeppeeZbBSQ9uajez82Z/RZcXx7ysvIS4ujmk7d5Hx7C5eqDlMVDyMjl3BK4O+w2MLHifQ1MjBf/yN9uxM7NvXkrDkbZZtreVf+32s3vGJWpY178GLv+PiiCCvlnmIvfkCJkWvpOPvzxHOcxIYEEeUqqA6DOx6ez1xEzTsP7uVqh/+m0ODr8fo0LPvzld5/0mNVU9rlD4/DN+tO6iIm065JZqJC3Xo9AoD5qVT/+Y+3n+q5+LV9eQ7lO7SQWQkhsp6jO0+usqsDDwvkunXaaw/pLJ63m9oaVFIzIayYBN7l+/hH7qJXFeYR0qExoZgItTUYDAphAIQCn4431VDG43DpmLwu/AYnFBbS4Ixj9ZABXW+/TT4DzLAMh6bLhqHPoW2YHWv7w1N01D4eL6mGEMWLYEjJ6wD0KFzcNWMDj7Yd/JA1uXVSHvhOSIqmrEeqMBIx7Gh5QE6g42YVTvdLoX8l57AtrmS+ESFsuKeoLbvA5h9gYFgqYGnPI1s3B8mtns/xZf8iI5ujfadh+DVVymPyqCzoZTSmC08VlbHb59ponnNLuYE1hBKSiT6YAWNWUkkdDcxpLiE1KpRxAZdbL70OpLWbaDwjbvYmZ2D57ZCzpteS1pnNTXDb0PzGyBGT13lbthZhfVAOQ071+E/0owr2o4nxgmmbzAodhmR9gLM3UYChqEnvA4dLoiMPPFzTouOxZJXhHnQbCLtYLEqfOf2k38eTpuhMmnKp776IxwnNHUrGq3Q0qxhPHF+8y/F5KkKCy5V0Os/f22XxaZH04y0ROdQPGQSNZ2JjEofhe786ylIzOF2e09tc1RqLgu99awfMJ7DyUOxhVMZ4Ow5/shkHWX1YFRUXPoYIsMdROg7cIRbCOpM+APOE4Nq2ExHYhyhjW/SkhBLtKmnRURRkhGXOQYsbrAaaIyxEVnZwqaokTSG/HiTkgnVHcYQmUOk1YjP66PmygupGDgZZ7iSnLalGE0KoTWrME/KpHRQLKFbv4su6MbRup+OsJkPSmdj93opu/Ma/nzbrRy582aq/R5czW4SD22nqfIQLdWw5U2N9PAWDjvTKel2sX3QcIyubsy76lkx43qM82bw/qXzSR8IV9l7mty3meJxT85l5H/eJeJQLZvOv5YPUu4l2m4lTrVBQiaJ6FEtOqIaGmi1+vF3d6OpKjr7xx3monRWXNYost21RDX70bkDBBKisH00/onNTkgzQWwEIXcngbAXXcBDMMJMOHoQjoF6QnoTYb1Kd60EJ/HlkOAkzkzxPrxxFroiojD4Q2hG3cnXs8VB2sQz3r1itGLGRky0kzunN7KsxkPLimjqrUGMPgtXpEdzdVIUarsTa54DfWUccdEtjMzOP+V+I4wQ1ixcmhoitNhG2spd1NZlUrFST+1WyJh8xkU9ZkSSyt56iEDPEXsTC5JUbk/qmYXRaYTtmMlx+mg5ZGPJhgby9CU4anegqhHEB3Lw2YIkfWqI4TEpOsrarUwbbOXH9mGcRzLtrlXke7dT0pLP2A/b0WuaQlpcDr6pkxi2uYRuSzz1DfVgVGiv3c52ZyE5Rj1XJg8k1N6EwajxweCFWHasIF5RyAj5sTYdoP3tX5G06Q1GrtlCSK8nIRYiByXRbUrAZbNiuHEsCZuqMA+yoVTsp3jCSFJ0nVQkxLCtOBlt4i1UWXxY3voPkS//jsPfu53GvesIWO1smlGA2xlDfXULrTE2Zm1aS4Kvho1bjtAabyHe0MV7uTM52JpMenY28w++wPRVLxNsWM/SMidH/M24lj/He5eOY8b8PbT8/j5W/eFhCt97loSGbtSsJKJWLKPcrPF/EwpoCh7l0poniR6gEj/Iy9vXXcCl3ibyZ1xC9cwiZrYfoiMU5l+bWjiyu5rCKdksnKayuHssXTWHSfFspCR2MNVjxzN31Y+JrV7D2K0/p7qmm/a1b1H10gvktJQwc8VjbGn30eEBJk5k4kXdmGwh2nKzMM+8AUdpDfljVEp21pAwNoGyKT8gLrydjMJ2CrJrmHGJm+nfhPOqFzHoF+fzumskaopG8dGe8KAzqsTcMIvhhx9m4w1vUL/DhWv4+RzKuJz6v71N8Me7aJp3Gy9VB3h/fxfDZ8cwdWQFDRefj6IorKw8xNzmZq71HyDpz98hLtrKgT0Hegbe8HgYOg32rQYOH6LNaOFAg0KbMcxboZnwwgs9c2fVOrDrEzAoFnRKz0VfhD4Od/D4YdZPpjvchk33cb81q85Bd6gd34d9nmp9xVR6txPU/LQF40nx7EQf6EKH8YQJeItLWzE3u+DKb6Ic3I8/JZ7ukm1AT21Ya6ASU3AANeUBvGOysR5oJjfaS0ljz8AmLdUwcpaPiB1m8g023gw1EfnM4+y8bCob7niGw39/mW3DL6EbE4YZA7nsyLNcsfVPfPvQbQwZ0k6c+zDPTJzLC7kXsmbcrcSU1eCwWXDM92BVDOzUm9EaOuhOt1A5fRCDQg3UBozUEMfUzEFk5iwgzuSBdcvg4htZd+UMSi+9ik1j89GH9OiMCg0VEUTmXIHOYMeRUsjO7cfX9tTWaCQknjxERI2ei65xPIcOQm7ehyPcqZ8/cMyYpTLz/C+3md5HrFYF26l+DDtDEcnR7Bm7gOrRMzjQmslFaT1No4cZbGTpPxzFLSqOGHcbdpOK60gz5tjoY2Eo06FwpL3nnFxoSaQ1ZCQPC6P9R+iyWchQT2yy7ghY6I6Pxu6uJGw0EfNh64wEm4IreiBZoRIqc0Zh9nWTUVZDx+AcVvpdxNmG0hg6ihIGNTIKfdBDhVthmP8ZkipeI8ZyGHPlUSxbD7JywULs7mY64h1YUm10FmSwpyuZ5NhGulULyb4qRpo66QocoDETkqbasdlyCe7fgyHGRUL9CgwjRtLZVkfFz38Pr76IcX8dpYXjMMYbeEcxktPg4vzISBzrXwPAbY6FQSmM3vk+Ib2efZ0FFP5sApXf+AHxej044ojHRVe8k4LDuzAnVROobSVkNaFGO469PgZ0NJsdRAfduJR4zBvK8cdFYvxoLkajGUWxoNmM4PXRGWpEF/DjjzDjzIKo/Fg6jbEYCGGrryJ8mjXhQnweEpzEmSkrJRRvoMYSgyWofPacR6oeLI7/7hgDZ2Op2otJ6eI3kyyktkdza14CGbUGhgSL8VS+x3cS7Qz1O8hsjyHeGSTLeeqfRXOiVY5GzCC24xXOn7uS5DFHyJkzjogkiEiG1HH/XVEBRiWr7KoPM8gfw2BfLPWal7VaIxXhLoZldxDjjefiUJifDE0jPrmCnMLxvN1QRGJ5Iyt81YTMZhzdx89NkxalUtqejsu9h/VHNDyuMqLtU/jL9glsrrVgN3/UHATQrFyRkMH5mbk0JVp46qJv0tW4jQ5/mKBJJcfj4LGX3Qw0ZeFpa6Ey3IH+sl/B1Jspj3NgmfItAqPGEEgYSkV2Dg0pKSQPV3AMUOgKpOBsa6U6ZziJdTvZkHspsYlJnFe2HqM9jeJp8SwYEEnRED3pVZ1ED/AQOXIcQ/fVEsSHxxJLZUw2br2RWpPKrlYFml1khStx3DOPCzLHYLtpFpkXOLkyy41pYDpF+zdg9e7B3pLIuI0HCayvYOWAVCY3xrFn7Qs8N/2b1FyUSTh3ILGudgIZuQSsDsq1GkbXrKNj5hwOtZjQtTfjOdrAuPI6nH4XNY/fQb0ti4AxQM57K/A7SqjTxZAf3UFEcyU5gWUk/vI3xNaUkxvvZ8IwJ9WFGdy06e8EfG66fr6Qrn//i4of3UjjxPHo1m0lvWMNK7aHYfJk6uMziFA6qcgdjm7wUGjy0xXXxsDDrxIMaUy6ArKzmwEFrr0W7r8f/vxnfJdfxV92pDFxrsIN8/RsK9Fo69SoadZIvGQ44W+cR1leIgP/chVFcyGo6eGO28l86sdMvNqEfenLOB68k+iKLah1lWTvf5Kq9/7NzHffpKm5kxFH30KbOYqY6irs1dvxf+sm+OMfiQ+VU1sGjY+8RFVsOt85eg/RaUGaGvZT3p0LDzwAy5ZhW/wuCeF0aGrqaQ4JmNQImv3l1PsO0BlsPPZ/2+XRaPSX0lS7m+Ct3yKq/eP3ZVOVRrq5iI5QG0n6rUTq4tEpBup9B2ivHU74+WcZv30xnoODaA4codFfdmxb75HVGAIanrkTcW0ooSNzHN1r3wGgxreXVPNw3toYRtlfSl1yGoaEAUTVV+CKcvf0P9NVoBx8gwEp+6l5xMKIfz3LYE8DMyYnY3Mq5N91Ma7nXyV50hBCcWbe+uZVxC0cTtR130bpjKBmSDaDyjaTfX4WSfVLqLKnMGzAWJwuP8HEDM6r2Mnjf3uYhvMyyXMdJqH5KPouF+m6IJ7qd0k0HMLoCfHSxZNZ1+UlvO4NOle9z/S9G6mIHoBOb8DVAU6nCn4PhpRMurs/rpHTNI21azRGjz35Z9CAmGjaai001GsknGFz49582f2bvigps8aRtfYgl5ZXMSR55mevGOHg+ztXMFDN4ryBH/eZVT4c6h4gQtWRb87isNbJpDY7+zJmkGo4cQLlaL8Zxarjg3Fz8IYMxOg+njDco07H3tFCm5aJfX8T5QmF5JvspOlMqGoMRwtG07blUZw2C51BM62BMLULJ1O+YAEWfzX+b15B1/AMdrZMQ421oOn8HJh3HkcHJJLUHSShcz/+7mjimo6SadhI0uHdWO1FqEkGTBXvkjksncj372Ogazme1lqe/9515CemUHHxUPz5ibxqHI1qCfCNSd9g4MGNjG88AmU7oLESlzGPRHc3JmcQDT3l0dk8H9FOZeIAEnQ6UBQirVA3OIe0ssOE3Lsw2jWCBgMO2/H9kN0mCya/j30x0wn5NEL64x83Gu0oYQ20MN2BFvQBH12RkTiyQHHE0aTmYevqxtbdRlDz/Zf/HUKcvs/uFCLEyTQ3YszWKDYmM9XfgVc58cvic1P1kDUdZ+M+Wp0HGDdFY8MmGD7aS4xxKMH2t3G6n2bW4BuZpFuGYijsdZcDnCrLWuIoyL0O3PWQWdAzylFKmEHmz3dh8NGFxZJDQeYMNBCri6ZDC1CjebjMmsiL7Rpx6aNo61zH5Gg975WrXFEYQ2unh4JtBiZNHMVu84m/YUzLsrPkcDpD47azttJKWYeJ/xmnx/6JYXmLknWUtOVg1B0kevAUxvz9DV68oYuK+hKanOOwejRq32lh0BUGVr84gsFJi9k40kKnYqcifITBagG3+mqJUccyM6GFtooAOmMig3JB1UFERAzBCAvGsjJaDRaS164jKTmVjrK9vD/xAs57fRNh33ba2/bi8YUwDpjFofGzsNXupiInA69/EAf1mQxLisGqtjPQaqMmKobEUaPI6CrHWDibAr0Vf9s+NEXFPTKLx4pm8ePmVqpeXcG0hicYsaWWuuyx+FqaKf32DWT43yEzPg1bnJ3AvLFUDZjMoYIsZj3yFKn+BjZFteJYcCt/DETwE88HRHTt4OgRO3HjvgFl71BpsFLUfpDR7xzhcFscXcs60JmNpG15DbX1IIrJSGunm6NBPxmX/IDW4CJifAG8KQOIGD2R1J1bWGzO546hh5m4/FX2TZ2ILxBLdUkDQ9QAHSmDACgrnIjlQB2ZURGsXrWVrlAG8a44zA2tGDMiyb/zTujq4sCGMm7beS8GbSpsDHPZ5bfw1pYwXV64uvN5mlfu4aIRmTz29lAK88yMmfTx/0rd4rc40qpnyujh1MS34c3PJ7L4EEs1C9e73OyPH4hmycRtSsXq28sQ1cX9dSbu+fnPUf/9KKOKq2ipqCE3toHyzFFkbdjNAbWY3fF3kH7DXPQGFbZuheefp7nFSO0uD8OevZk4Yw5doRai1GQ6gvXUr38WLTONN/ank9R8lPjX3mH0vx9H9+hjYDTSuK6SFXn3s+DHCmESidCPJVJvwaCaaTj6AQOf+Rflo8cS6PRzZJOLmcOH0+gvoyvUSlugCufhMrDo2f4uZMUnEHW0DCUcprx7I3HGbNrLGknu8mKv2UPzJQPYHZxMzopHYEQetaWQFlsCu/xkFhzmoC2PWe9vwPrC8wx54wVW5Xdz+L5/k/WN84hvepOdB6OYomsT/QABAABJREFUUdxO2ew7GNvUSti4gwO5yYzoMKFWuFB2bOG1uDuYkJFOVPludHkFjKl8nq2tRSxJKiT/SCXKrnJyDEbiIhIwxDsID8jDFX2EYZs3YTGuxJ8ygAhlNs4lf+CZaRdzi7WSY0NxDJsKzgTyVYXifVAwFHbvhDFjFXS6k39WKYpCKKR9PDy3OIEhKgpbp5tgWTtJhRd+9oqjZpOS7iMqznzC650apfDbNT4uGaJnRUkOuwcs5P43/gQ3/gKL+8RdZUXqqFdsaCMNqB1BsqI+0Y9VNdISTMDeuYPDzjQ8CbHk2wyMM1h5w9eCNbqQxj3byBwcS3NJOavTRxNubSZptJ6qfaMJ+cA20MG87iXszsqnKCcGo6sGd3wqZruLhJwg1QfTcXZWMKx5P9Xx6cRXv8yGpAzSnB4a479B5g/vQHNGUL6wkB+8+C+W3h5mfNkButLiiA1PIiPRC6pKwQU3wQcvw+U/gp3vo+guRK+9TP1ds1BfKEWNhusdDt5zu5lo7RkyX6dCRfpYsjvWMHnliwTSI+g4ECYl4hOXnZFOOoNuOrNS2T01i7Qx02n0R/DJVvMRqolwWMGfFIV5/VZ0fj9Njhiy04DuWJRgAoZuL3qrj+5QN0bVctr/E65gHSo6IvWnO96+EFLjJM5QqMuDLkKlUnUQHeii3nKSocj7grWniUSM10paWzXTRueSFj8B5chqDAXXYIrKo3vPQ+jbqrEk9d4kMNKk0OalpwlhwtBj7fnXVPSMgPd5jUtTyXKqxFp79mtXDAxW7dg+HHraoLcT65iMI3IYzd0aswboCFDEJTOHUNwYzZjUE9+KOTEq1xWlMiJ9HFcMH8YPJxhIi1KJ+kTQy3IolLuMhMM+gvgpGOdg+PrDlEYPojTgZ+7a58mOfpfEt15m1nlVGFKGYq9u5q+tB2nrzuO+ejdzLJEYNljY0O0kramGw57BqB++JMmTRxOw2EhctgJrbAw7bCPRH3iLJscAstybGTwok4qkSJbceCExo4fQMG4o9WYv9d42TC4PGfETmFWdRHnCUAhH0hw3BI9BpblmHwfCw/E1biDgKoFwkNaodJ5wRjC2czONCRpxt4zDe8dltH3vMgbl6imYo+OCDX9nSl0A05bNpKv5bDPHMD+wlokdm9gw/TJ2zr0Y5cYLmJiZwACnSmj4eTQMW0Ao00d711pKU/PJtDXSkjYAR1sbsbcMZPcIHZWhbez8wUI2jBhEs96Kz2IjdvVyOl58jogJ55Nw299Rr/0ze5tMNGs+zJaJ7I65loxYA4M2/ZIr3+nG6m8lbNZT2mXkH6Ue9o66iMi6Row+GzMa1jF3192Mde9mRIKfQ3vrcZmj0ew2bDveRTVCeONbaM5IbOuXcuV0HQvj91L/ykrS77oNh9nAzVt/jKmhkqffdvP8qhDvrPdRtWU3U5M6GXLHd/FpGgeHmqi0aHi7NJ7OmsOArStoawiwqeogXqcXh6ecW4pfYtW6HYR1TWjeEnIrVlN+wUSiNQ8RuiAJ4SMEUzV+fV+YX/0+yF92j+Dfjut4qnUcRIcpf7cWVem52DCoZmI6I0ncWIfxwRWcv/LvzDd1ELz7Phr8DvjRj1iX+D3apl3JZRPWsPoZOLA/TJJRB2uXYC6rI+Pp7WjOEDk7XqbiG3NxLn8Af3UTEeEBrCnfy/YSPWk7ttA+aDRpIwfh+J8LiNq+lWZXLFlNyUSWt9D2898y5R83MHrlE6TVlLGhoRq1ppHMuDY+eC6EVrGPp44c4dAj7zLi/+7COmsaGAyol32TGdMnob8oDodShTb1fNZPG0UHDkKvvczuTUvQhZswe7z4Dhyk+8B6Wn0RzJ88rGfG0PZGLFYndd4JzKtai7PbyJidOzCn5nFg+gwMV9xLuLaCPTsM5Mdn4E9xoMTaqdWlEOt+hjo1nfmR24mzTiPyo+6iCRlgNJObp1B2qKeK42iFRlb2qQOR1wtjxktoOhXLhVfxVOMtZGadYiVVJTrRctKQOitbz8+mGHm5OMi3Rhr5UVoyP7j4e6Q3xjDtJN8jQ+JUvMFssjmKLxRPTvTHn/MGFSZEOMnbt401MdOJVK3MjbahVxTCGgQ0lVpzLKF1K4i1dnHxwTWMqF1Oa0sLu8dmUxJwE1nTjFNXhyVej+bvpjwlhbScOSR2e3nPOp1wsB73kLmsG3URG8fN5K35M1GTI3HHRrOmLkDbt67h6PyxlA/NRB0fxyX3/4nU9ftYnjGDKSH9sREFURSYejlERoOmkR3pI5wxicPR2WwvuogovUKGwcDNTifGTwT3sC6azqIskrccwJvupMkaS1rUJ4JTYhYWVHRuGGBYg9XXTZnu+HkhHQYDIU1H9bRhqE+/gRoIUROZiqoHIhxE+cKggZ4gBzsbOROdwUaaA+XHanaFOB1S4yROXzBI0NVFMCYWe3cHerubgPMMx+8+E+mToH43DF6AcmQ1hIMwcDaoeowpEzGmnFkfqjirQqM7THxEz5dBc7dGKMzH7ak/hyFxnx2+4mwKDe4wCRE6Wnx2Yq0hFEUhqNmxmAw0dAVIjOj9NwzTSTpJK4pCMAxO+xhc7l0wxEHcYgsW/TbSQga8U2cxNHEoa/atxLP/cVqThhDXEqA+MYED3m5GefbT2TQCx6AgBw/pcXS30nB02MdlT02jbG00oawggeCFZCpraU7MwBpyk7VnP76iOMKpRvKLa9gxIBlT8RHaBg8k4PGi81v542Iz3x1qxN81BltUBfGhGt7sHsudu/5AQ1wZa6ZewdCwix1RHpq699DuDVGTmkpLp4MFcQU0K91EVS1DP+3b6H1dNMfoSQ0kUZKYw6HlD7Mjchbm5GwmdR1hvncL7cnT0ULV1CsDcQU1muhmY3c9gxUdvj0ZXGwv42hKDEvjc8le+EOitHZqQ4fQVVRSbC0ibUgz9d1t1NotlOZO5duv/IPS5lzWr1rFGH88/0koYlJ6AgXNL9KkRRFlHk3m3uX836rvo7praZmbwwXbPiDgbiQ1YTDeggE8Mng8hW8eZGqXjoCpnPDqYgrb3+KRo7dz5e7nSYn24rvsOppKV+FI8KIufpaadW8S395G1v/9A2ISYFAOrFvBwJV/ozB1ANzwXQ6teJ1Q1FHMl3+XyoZVxPrq0arjibZ08i2ngbbV71BWMI49cdMY532SnckFjChfz/tD8nA1ltA8KJ8JWxvQ7rqeysR4otGhJkaStbeUgUOOMDwum+yRcLREo/vl5QwZdITNNXbCT7xGuCSMqoV6hoD3eODOO3l/vcLFRT4a2yNw1GvsPBSmZq9C0kDIuqyIqm/fTXbcPoKHt5Jx4H0I+yFvCK7pF+GpeAT3hAIWrPknT//wt8T+8B8cuWYBk3atwJsyEfuhfWyb+SMSXI/yVHIK1+gCVIyZRt7ixfgOHUS1Oqi56a+UNq+lYM1KBs1zUHLZd8j518NMqT2K7439zJw1mH13XMaMLV649ErCbz8O+aNR9y4jN2Ygrw9PpM7UwMD2Wl4fXER+wiDKWn3cuPynbM67CPVoDU0xKqnRZgoiP/zq1OlJDMdSOyaHxJK9TN63FHMgTMLs24gP+XG54P2auYxQ3qPVNB2Xs41V3WMwNenI7YjjwCwLlyddyN6dUQw5STdNh7NnDiNn9ImPfdrlVypS29SL7BwFk0Ul4iQDbJwuRVH45bSeJqgxWLgqkEtFe5j4wSd+hidGKOiOJhNItGEPZJJi//i4QxNUGt25xAxN4OemI7xfPf3YY7NNDsyKStuk69jctpS8FRtQDC388/xr+PbGlygdOIhC10H2ZQ1hu30oU0vr+L+864hOCZJWtZFDrlEcUh3MsXcQWpdEYNo+wsF0RryyHfeYRBRTF8lx26jPzkLndZPbUE1V1iiGxB7F63DyZGA+N2T6mWg2n/gCTPgGw1c9zxLrBeiCa4lP8FE07CTfX1Fx6DscGJwWOtJiMC4rpT51AoX2T3xXxqeTedDFHnIp6KzA6PVRF5V03G6cJh0VATOuMclE/msJmsNIi//DIc1VHZFmAyG9EZ0RGmprIfpMRnnSsOsS8YTbsepOMq2KECchNU7i9C1bQldkLN1xUeS01OOKszHtTMfvPhOKAknDe5ruZc+EgXN6hjn/L83O0fHWoZ5Rtg42h3l1f5BrC7/43w6mZOp4/0jPcVdXBJn+4S+TkUZo82gEP2d/1lnZOpaWQrR9FNFRoymar7Ljg+twOoYyPXEEkYqB2YOm44i4liENh6kKDeTKkneZufZ3DPd1M2jvH7lVW8ZP4rZhDmkoho+/uFRVxRCRhKqZcNlXoreGYPz1xDeaWFE4kUMH15O0aT8b01J4M34gQZ8RzwYP0YYgR7ryufh8PQcSQlxxeSKDkpJJ8rdxvm45O9RhRHQ20tJYxlO6Dkprcxm8ZjfT3llKaXkjjeYG6ulgQ6AOV6sb7NF0VK8lMqGQQ8MnEV3XyKrhRYSGpdCyz4QaDhPMuZh6SxftwRaW+3eRFt/A464Khh/ZSWR9mEzOx/yNnxBlsVFUsYN3Pc20h5uYWh/DIfskRhohbuth4ttbGWG9gXGZNlbMuYCnI6OxZgU5mrCdhdYaqjt0JHbaMCU3sS4/n0PTLkCZnkDNXddhcPs5aksmevJd7K9uwhjUGJMYjfuG6WwaPoNnLnqA926YT8TYFC4Pvcs70+dRPzaJTk8FocYwS9NS2XbHZaRcfDNP/fB+9lqNxzo8Hx0xnd+Nv5yWyl0Envgz0RuW8d7FfyBqyR+IXbOCzYWDMIybimV4Fs3FHxA7zM6u6elYCovZnz2GhKPVdNvN6PeWkq4NJnXjGg7+6B660wcx2FtGOC2Z9uwhMDaZpkX/y8CingEGslLd5GdVoVx/KyMzgpTOnsPLxtt52vE9duXewuLRP2RNiYGgokMp2Un7Hx8krmYTH6zSePmDMGubNB5fFoZFizD8z63smDmLvY6x8PCTMDCV9jW7COcaifA142wtw99cRensu8he8gAdNhvhZUvwOhyk5bSyLTeFyYdKcGfbiTqwmcZLrqcqaCd1/ig21xRgGWWlPCeVgRs+YC/t1BSNpLvFRfGdl2CvqCd2bynbWusIvvEQpcOstG94hE69jvq8OIZuX8Ls9zeQtb+aq8vexla7lCtbluKymKnZM5D6vAVMXbWJQYP+59j7Y5trFNVvFTMseQjelHnE6KNJnPEtVEMEOnM0O7ZqzL7ETmZ6iMrDOqamzcGRkMrc4HpSnUbm5lyB1eykqUkjLv7Ei/nxExWqqjQmT+39Ql9C0+n5zm19e9lz3gAdFw8++feIoig0uWyg5BIOx6B+4hzlx6tsMhcSnzIDi/V8Ik0f9+2xq3qMikqCzkinbRQBu55/Oi/HETDSNGAIWY3VdOl07I0dSoLOhj1Yx+x0L5rPQeRaN7rcQtKiFZqmjMfS8TdyjIU0exNpzNDh3+Il6LFSm5pCds0mBtRU8JZhMsV7u3kp80oO+wcwyZhCe2qALMOJ8yJiMEFSNhfkerlwQD7h3CwGRJxkvSETKHAXs3X8bLhhLHvvWgDJg7HoPvH66w1kKU4a0xxENLTTZTDTyfGzLVuNKpXGeFLDnRzNH46mU2hy5xx7PCoeXOlpmMJ+rHtLTu+kfUKUIQlX8GTTGwhxchKcxOl75y2a05NpckZTVH+I/el5pNpO8oF5lrIYFEYkqvx7e4ADTWFuGWXA0Ae1Tb2xm3qGLH/9QBBN49jADqNSdPx6tY9Lh3y+8JbpUOn0a2yr6QlnERGR3PzjVgoKjBxu1ShuDGEwmMgal4Nj6CXMUldSUR3FvnE/ZnNLCzumjaG6tZLAgVIqDKPIGXf8axLURZJgm0BEazpF9hvJNcdij1FJ847mwbFXs66okDGNVcw/uIbUlk1kOzfjcius1Yq4aKiO3BiFtUdDbA4MJKpDZdDhYurSvklH1jByN24iaWc7c1c9iDfkIicqjps2LSFp1XvsW/og+e+8TuOw2aza+CaVTguvRcawJlzFW1G5jKssJsO7g9TiA2z16Qn5W/GtKsS7qZMJa17BtGwll+1cQkxTE12Hbif3ukwwR2KwJ+GMVAlseJ2o8oOoK5/E7DtKxLtvsjszm5aEVOqcKwnWtxFKz+XWpleZ09xJa/Zg1K6t5B/cRZernaw2I0ObNtJl6CJWF8Ww8lL8egPdEWms9bxL6Zh4uhs9ZD33dwaveIp0YzeTMjsYlj8BR0o6idcsZM6cdJw+WG+cTtSgIhbsKWZat4ZV28Jtbcupb1zO+4dfZFfnGvYeeZ8r8yv5YMF89jUfwR+dx5yK57EPnYsaUDmQXcQIWwLWTi+mCcNoiIDo2lEkKh4GDplGtMlNfXoyMzc8ycQ9fyUuOZnK5s28GJFEjMfNOnsR67OziPN0UDxkFMG//QYO7oJnHkVbsJAXqvw8M/4Kxla9QYb7TYbntbDepUfdoxKqgaENe2n+7aOkjI4lcfOT/Di4iD/M2843xqlcc55KRpzGAKWKQbYOSs2TWb07gHbh1RRXmtAVmqlPzcM3IIqb3v8NuuRNvJv8EHpHFjGTFPS3zaHF3EiDzobZkEbFBSMoKHuPLb99jaO2QWzzjGHIiNUktlaw5cILUTotjNnyAoM2rqT+ilHYmhvZNWkWyas2E5/SxeFUHSV+N7vHTaRseDztR1dzMD0Hd5SOdx03c8hxGc76clyVezjcdTF2XSSGCBNxP76PzVt6PvMCAY2aQAbhykOsf7mJ1qYistR0lAEf97ns6gZbhIIy7kLS698hNWDh3m3vMixrMnFFVxOvO3UfUZNJ4YJ5qoSis1iEUSHd8dmXUj+cYKSrZgRdgeObg6mKwke/mS0tg9k5J/8euNqSwoBpP+JS13uMLGukLvZC1NQCEhtb0NxxpGZFUxeVia+im9FdB6mr1xOIi+USu5MngtlsK5pG5JPr+e7O9USX3sju5LGEqpqoS43lqGLhgCmDtsQhuFtjODJzKt6ggYLDBiISTxHGh4yH/RuhuoQ5eSOY+mG/puMYjMRmxLM7chr/ir6Y9cl5mLQTXyeHpqc6K4lGUyz7o5Lp6hx4/AqaRolhII7ODjZ890p0tZ242j5ua2mLh+rkIZi6PNj2HiIYPrMBIvSKiaDmP6NtxNebNNUTp6+zE12cQmlUIXOCR6mIzPvKfaGPTNYxMvnz92k6UwsL9HT5NZIiP/7iyHSoPDDHdNyvkP+tq4YaWHUkyBsHg8wfNBSPtwZ3cAjvl4bIdCpUuoLMHagnOm4QxnFXsatqL8EDh1FTUoivC1FXeylMKqXb4+D85OM/FlpzZpOdqmNyhIovqPHT5T7uzBpP2q5d/Cg7FW9xB/mX/xB/VztPbWlkVHgTHB1PYVbP3FRTM/W8dzhIfFYBB8o2MTxzEtm2fTQMyWdwoJbEw+vRJTpoiI8nMncigaZExuzaRKPfiTkuSOzuP1JrtLOqdRD1vlocShVFrc00Rgxk4rYjeJ27qF1jBb2OLNWHMzEZLSINkz6SzloDO4bOYpzHykfXqM6B8/A0/Z02GrDv7eLAlBt5xeTnqnAT3/zgJZpjrqPjg+FkDXcRwSaWa99mmO4thm48SjjsIVPxsrZoLuNdzRgOutgzdjARmysZ1VXDpjGjaDFUc0gbTr6plmdnz2BMcSUZcZHsS8shVe2kSBkI+bMJ1h4hpWIXakIiMQMyaQuF0C/ZQJtVoXLKFYwp2cl5NX7K3e2sr4qmfaiVdNuFLEg04nJGU7FsGYNNFnS1B1iVn8f86sO0hhvxOW0Yu49yIH0cNnUdCUoEJW178LcuZLL1aRpnFtLtsmCJMTOkeh8tw+NxeZzs2x9NYm4yc/wBbNeM4L1nSpj8+GNEDMxk71MPMmrcHIrNiZjmDmHsqjXw8lsMzcggNHk2rZ5YbE//lsB930IX48Q7O4Oo9esIvfdvHE/9jU6THkUxYGppICWoI+8n89n+zE/4z5LhTIttxhbsYkXeJM5vq8VBA3E1u5g6MYT65tuEhsahzP4fQjsfwtGYyg6jlWwNjMFGpoZXwH3Po+/4gOZ9O6lJjSS/tYGHvvdT7nngxwRDHszuCLZPmc6Azm7CaiWaKUB3pJf8vYdYmzmI7rZuUsM+DK3dvGS6hu8XJfNaZyQba77DjZ1vsn/iaDx7TVw2Q49ONQBh3nkzTFiDaecprF5xLSO7XqauPA5/mgc9KqGAhrsTjB/lIpOVxLFD2Pb3t0m+5EpSkz++0HR3apysRZQ4N6iKwkV5RpYfDp7w2IQ0HUtLQwRCfDzp60lYrE7CxnT0B7y077NTNmQ89cE2hsQptNTkYzVu5VWG8O1n3qVpytVkOhRGR5t4KXoA/9iawguJQ0jydlBXGEWxy8n8pldIamzE0d1IcVciQ8vWMPPShTTG67kv8/ucF+kl03aKy0OdHhIyoXgdGOfzWSW3jj2PSf9+ltUXj0X/kgX/ycZtGDiSATX7eCV4OUM9K5ludZzwePzy7ejtfuLjmwgm27GrH79/TMkxdFW3oO/2ETL6qfcdINUy/LPL/qFQOIjfr4C898QZkuAkTo+moXk9qNE2avQOUMHa/QU20zvH2E3KcaPhfaQvQtNHpmfpWVkeZE99iGGJKTy22c/NRXpMeoV3S4NsrQkxOkVHhDWLaQMiOZDRwuCj0cQOjMUyWgGy+Pf2ADE5x5dpXLqe9ZUhzs9W+Nf2AD+bauKtkqFckltBy1v76eq6DaPBjNGRyNV5iRS/OAxnikaK/eMLhfOzez5q6i++maXVYZLeV4ioc1PdMhltxmvUdljofvZCIopqCbsWcmTiEAyRVfgqm6mJHseAZC+5SidXB1rYpDrZN2gCQx8aQOzdPWU9vNHPP5s6uDPORVddOonD3dhaXsdVmEb84pEMub2nHN0BDWtMOubYgWTX7OGm4ddgiupkXnkjIWskjm/8lqiQDW0M2OIcpLXNodAHxrjvsa34RY405xIy1DH45WTcDMMw1c+winrK8uzQ0Em1LYE01cSgLSkcropn5GUrqU60klNXjrWmjoDNiF9dinHybegrisGrUJGYhN+1gVpXHRtH3cDcvctI27aLg40HiA514bKaGdd4mOyXW2nTPU9bfCLRKdkUxtlRQh2sixnJoQFRHPIPIKF+BZEtN1ORsRuHEsYZctFpSuBQu5m2ScMYULIHNc+CsaOFbpORgozJNO1ax/ahOSgVekx7htDscODcvpoBU8wc1g2mxBhFizmFG155gKy6FhoMMXhvvhZMJvSH3ahr38ZZVYL/nsuxpQ9D31YD3m4oHEXb4KFUdO3FpCm4klMZs7uFlfmXM+X9R7H8v2sZt2YVqqEbd8BEtNlJtxbNvitHMWhHCznVh2DuKHTn3UKLqxJLbQNOdxtKuJOQwU/pNVPIb07Eo6vHW7Ufny1EVEkNOc2VbB09nPvP+yMLux+DaAVVB4lZ+bTtrWRt/gyGVdTTNSaa8Vvs+JO30+34KRUHVe5KfJ/Nm8v51oQLOKy2U3xAx6EKI9+bYkD34bxIs2b3DEtt+rDmeN4CHTrdlcRXV/HK+5NRF2uoKuj18I2LP34vxY4pxFk0jCVvawR1Gs1NUDQalrytMf/ir9YPUOLM2E0Klw45sXXG4DiVd0uDXJ7f+6XY1AtvJrTxbZ5vL6csnMqMAWa2txeRG6Niih/LY63reSjpAm6ffvyk9MPidZS749hd5aAz2c//zYjkNf9Cxj3zMKvGjKbFP5Pv2d7AkmYnFpg1KUR8ZJhpJ6tFOq7wY3tup6KqZKVE8vJ+E2P1Gxk84bIT10nLY8K+DSw1JJDgtnFx/qdqYZOzGWPYSrkhlonVu2m5sohx2ccPMGHvPIgSAiXCT2OrhZBzB0nmPIyq7TOL9va7HXjDkQyY92GtU9iHXu2n2Z7FV4qifc2GE+no6CAqKgqXy4Xdbu99A9Fj725cP/9fmq7NYEtKHrm19ZQNvpsrh8jPNWcTTdP4v60BzHqFiek6BsV+XMP1bmmQOrdGlAku+dSXeEV7mFVHQqTZFWZmn/gl/pcNfqKtCpcO0RNhVHhsR4Drh+tRNYVwAPSf+jd4ryzIwJiekQY/7eGtfq7LNUBAQWeEPc+CFoaimzlWK1S9CSrXw5DLYNtaL1quj+jdUSgqDLkUtv8LRn4bLJ/ozxvohtptEJEIxS/CwHkharfqSCwEZXCYw60a646GyHAoXFWgsue9pygdoGBtiyPPVsvW+rlcOTP1uNeyK9DTFOej+93d9ejCMZgjjXQ1wfYnOvBOeBunqY7ShCTocNDUPpqWsigi4mCWsp2uKTaW1rYwq3YfTaEUzHovF4Rd6IZfgGftYzw1YTTDmktojcyhzWjjaIeOyVuW05oQSzDdQWZAT5vJTG1CCt1ujdzdO8kpKwZHNB3Tb2OzbjMWgoSazHg7M8loHUagCurH7ScvvgWDLsR7+wv5xZRo/vCPg4xMP4jb7yMlphx3RDTJ7fW8mD+DUZvGMu1CI5u2/w5vjZHJl36PzdU1jHAdIGrVi+zMn0J8mpHoNe+zK/88dLlXEjjyLoPc+0ic8iNKuhSiD71ClLuD9f4YDmXNpiL6AEn6OJSmMCn+zYzcvo7S7JFUDEii8FA7mY5Wutqq2WrOxdRShLFtPdExNUR2hhiUPZq2AeMp1cVi3f83OhpbWDPmAga9vYvUQoXmCIXZnX5aDSGczT7eTEkic98BdPoEEvd289aEdByGIBmBSv4VupMcu555Bx5hT0wUI7Pj2Vs1hcm5XirfTcJfvBNj0TaKZ89iYagba+kOCAVg2kLeWGJh/iWn16o9ENAIhz4OVSfzxqthdHoYPFhh/36NMWOVz5zYVohP87z/Mvsb/DTF5jN64jBirAr/3OInza4Saer5Ae2TNE2jvE0jy9kzWp9eVdBCQfY/9wZ/SZvComExxDnoGejli+DtgrWv4tEbsUxfeNJVghvf4ue+PPKPvs+VV9yK4VM1U77WFra8/CDDDGXUJMSSUPggMSkfvmfCYRqefgzLmscpS0xh9+BfEevsIn1UiMHxwzCqJw+A764tx6g5GDvSSdjcREgL4DCknP7z8n/YJNAoYetccCbZQIKTOLVAAHQ6+O29lFS00/XtGLR6Pwczh5GffDnDE7/8Zm/i1Ko7wuhVPnOkvnVHQxxuC2NQ4fJ8PRuqQtR1aiws0J9208umrjCv7A/ynVEG9jaE2Vbbc8x6t8b8QXrWHg3x7aKT93+r7gizqSrEZfnHP97p02js0kiKVLAaji/H4eWQMhoUtSdopYyG5FE9c2dlOVUGxx3/XDUNSt6E6GyIzdf4x+YA5+foyY1RePNgiInpKo1LAzgLltBk81K5Zg5zL3OiN/dcaCwtDVHTqWFQwWFWmJenQ6+e+NqEQ7Bx8S6ODKugPcKAqbqAhoNJ/ORbBt4sCTKwKYDiK8E1sha3rpt9OjtNfiMXHNzK4NpaXh47DoNOozg8mtSgRoKnHZ2xnshgkHibj52RMbRrVtI9TfjDPkyGEAa9gl/V47HFENdexUFTDFkrZxJb62PaHfZj53DrFo1S6jkSUcElttEMztCz9DcaR10aF/6Pwi5/PT71PRpM0RRvHcVfZyVhtMGfNqxjwb5/83r+t1D13cwvXsXOGRNojisgL5jKtCX3UVqUg0YGOeE2DjpG01y6ErNqxNldxfJhExlpVUjdt4GdZgsF3mZ0JgdVQR2+tg5MBelkVRfTnpCBq92P2tzJ0ZRBLPcXMC7DwNxnfsITV9yGs7WNcttgbHqVby/5JY9Ouos5zSNIsq1jb9dOErRuqlPHMvfQepoThlBet4M93MwhVyT3tP2eyNFFlFSW8yeuYlJkIrNTrVSv30eXZxmWpAsYcLCM5ClGalsreEE3DF1RHjfERRGv1xPw+DC4GznSkYqrA4aP6Ltg4/NpGI0ymIP47z20peeHrIUFPZ+hDe4w7V7Ii/2Kdlvv7mTL2/9G1zacou9MP+kqK//6KCN97/G/g3/AXy+awHFvn1XPU7JvFY49B+iMyiLUZKOy6Hayv91JjCGfIG3EGDKOre73a6zZu4dhaQUcKVMZMz5Ene8AKeahp1/mJx4CswXMVlhwxX/5xMXZQoLTKUhwOkN/+R1EROF/cxk1w1LYcn0RYzbs4D8zbueXKflfyuAK4ovR1BXmhX1BRibpmJB+5gG4oj3Ma/uDxNoUri38sMN8SOOfWwLcPubUA2+8uC9AMAyeIKhKT9CxmyA+QqGyXcNqgAty9bxxMMisbD3Rlk/MDaJp7KgNs7shzNB4lT0NYdIdCkkRCnVuDYWeiYE/2ua9siDZ0SrZH86h0uXXeONgkMuyDWz4k8bgy8K0HNSRfKHGkkNBmrs1zs/RH6utO9zaUxs3KFZlUsbHr1O9O0yEUcGiKGz4k4YlWuEdX4Cf3aFHryp0+TVeOxBkRtDA4WWQfaEPW3YZu3111PjqUcJeurEyJnIqecYUdqwPsqfZT2K0wuR8IxWNCpV1DZjMXuobkkltNFAV8rNnjIvhuW1EaIdYpY9n4trhpOw3M+5OMHzqx9VDJRqHOwKcX2TgcKtGrEultlPDF4TyMo326C5q90F8oZnvntdzDv/2pJ+cSf8hfkMJFouJyqxESmImYUjqwOXNxHkwwDW7H2LF9MlU2pIptMcQ4+8kVL6eQ450Wu2ZKBYvOVoM4dp3iShV0etdmNvcWJ16muJspGkxhPxN1EfFEDT72dmVy+7WMYyMM5LY9AQDaw/TuvAG/K5GEjYvQd+tY0XaPdhVA1t9Hi7b9SLapC4Uvx8aDOxPT8XssLGiahKzt0ThG+Nh0N4XsSVFcSBqAXk2HaPHKOzfp1Ec8NCw503qY0fjNobpcEUzVR/Fwjk6zGaFujqNVSs0bDYwmeD8uQrqSUKzEP2l1aMRaeSc+v7VXrgfZf7tYD5587qWqka2r36TfxTM480Rn+omUL6bA6uepq7FRSAxgyF1DZiP6Pm/ybeRO7yGaTnJWNUIogw9805WHtWoC+5mdGwW7/3veub8bhrV6iFSzcNPr7BtrbBiCVz+zZ4AdcN3P8czF2cDCU6nIMHpDFQegYcfotWZxbLYGUxtvY+N540m58ARVo76AT8YKH2cvu7CWk9QOdNfz0NhjeZujYTPqBXbVB2iuDHMvFwdb5aEMOlAoydktXZrTM3SMcCpHus3VuUK09ilkRypENJga02YTl/PR5vNAJd+qnbrX9sDfHukns4ahV1PQPx3wqyuCHLDCAPmk8yXBfD4zgAmHXT4NMIaxFgVOn0wMkllZLIOT5vGs+XB42ralpYGibUqjErWUboEWkrAZAdTaoi9nkZSq+IwqnpQYMB5EJMHHVVQswWaD4IlBjraNVKGKOTOg84a2PumxobSMAFHkNQ4HZntOgp+0FMel1fDYjhxbrJndgeIMCrUu8MMTdAxIe3jkdpCYY1n9wS5bnhPuRubNJ5aVkP23N0YwwoRajwdrS46Ah5MUY3UaMnEtbcwfv0yWvPS0VTw+KNoyxxB/KZiapx6krUGrKF2Wn057G64koaQDmdaF1mD92BKjCM6VEdUYDZb92m0xjSx2a3yf2OSaXYrvNLcykU7fofT246iV2hLH8SjkdeQYrVQdkDHN7IsvF16iKu73sIZ6WHt6CloRjetZSPpqozhnok2nFmw90g3O9Z6GTTQydhPTAy7fm2Ybp9GdXSQ4Ul6RiTp6HBpbNqgEQj0tLyZeb7MiSTEl0rT4DTec53BEJH6T/3QFw5T+tAf8YZLMNS0EvZqtE8rxP5BKWGM1P7PDQxNjEWvGIk35rJ5Izhyd1H/v2s5km1iyt7NmB64hVTnuNMr67OPwazpEJMGm9ZDXALknsn8UeJsI8HpFCQ4nYFH/sauw3rarr6VKe1LqK5fxr6sHKKOthA/6xfkOr86Q5GLc0MwrKEqn39QjYr2MOuPhrgsX8/+pjAHmsJcPezU/8/BsEa9WyPVrqJp2rEL61f3B3CYFSpdGoWJKiOSjv9Sf7skiCeocekQPaqiEOiGzjqwp5zYN+wj1a4w/hBkRKroDNDu1djfGMZh6ZlsuedjW6FyLWyKDNCtgSeo4TArdPg0THoFqwHOG6BnR10Im6Gnz5umaexrDLO6IsTsbD1ZToUX9gUZmqBS+Ilmtw+9HGBAp47zrlZprwB9hIf3H9PRPSSAd9IqWjvCjGg0YG89wlGrmcRwJWaXn+rxw4kwROBtH8bI6CQq94Z53NjJ5CQzCY4w6z/opDXoJSbSQY3Dg+ZR0apNjJqo57YBPZ3aX9kfoG3rQbzRIVoHpNPZrDApw8i6Vwz84X8M6AwKexqC/GZPE9kp9WTqfNR5HXTWpzDvUAQzvieBR4ivG8+yVznYsYu9qSmMWL+e1G0lBAJODsQWkKgc4ugl32XSeeNo9JdyYEMmA/MPoL/mZ1TPK+Jo7GQmN+8k7c7f9v6DSXcXPP8oTMoFBbAmwartcO3NX8rzFF8MCU6nIMHpNGkanm9ey+ZbHmTa1BhC//45VYlu1mSMwlDVzdUX3NLfJRTicyluDLG3IUxChHJCh+oztb02RKZDJcZ68i/d6o4wyw+HCIVhaqaOgTE9NW3+kMar+4N0B0CvQkiDYBgSbApmA9S4NBSlZ7LkYYk6ajt7Ql5alEqEEZq7NOIiFKZknFj+Tp/GyvIQCREK49OOD3NhTWPt0RClLRrfyNMR/6mav7Cm8bu3AoQqFUwOMPnh/Bw9rSkhNu4Oc+VgPYZulfIVkDm+m7L8bqp1HhQi6XarHKzVsIRDHG3S+PHoKGxGlfePhDCoQFhj/24/RqOB2VNVOghxYZr5uGMvet9P10odOX4VNU5jty7EZSkGJl718ev73LMhDvm9+Ow+jFVWzKqOH91sOKFjuRDia6DLRdMji1k2ZQKhpA4cTbvIaz5C4rL9uI5YCQ5WKB14Jed980KWr60n4093kjAuRNX4fHRbu6lxxTP2W9OIzpl0ysOEX3oKd0oLDcNGkGkdh+HIOli9v6eJ4cgxMPgM+kmJs4YEp1OQ4HSatm7k4J9eIOe5v6Dbsx7/hod5euYsCg5WsHbQhfw4b1R/l1CIr5yw1hNmql0aGuALalyWbyDOdvq1JMGwRru3p68WQMYpJt/8PIJhDZ3S0wyz3auxsjxIepRKtEXh7ZIgqXYFo17h0C6NrnowKNANZOoVRkToWB0bID5d45sFx486FQprtHRrxFiVY0N8f5o3qLGyLEiHFwbGqRQln3wS2MOtYUpbwuREq+TEfEU7xgsh+kZrPdSW4a8/wmJ7Kubi3QTz7Izdu56YdQfxN3spHn0hSmWQCQ2v0FaQQ3NMBrHuozx75e1c+vRSrD+8gZi4EajKiZ8nfncT/r/eif+ihZjWllM+AEJFsxn+83vhN38juOQ1Vk++kPTUZHItMtvPV4kEp1OQ4HR6PD+4i235VzBxsh/thT+zZfRlbC80MeL93UQsvItCfWx/F1GIr7SwpvXpPF5fJk3T6A6ALwTODyuLPjl0uxBC9JvWesKuJtbEZVG17BWiwtVk0IBT8RD15Baw6PGlxvHiz37BhS88i/3gIZoyUvjntHt4YOVi2kelE5xQRKJpCDqlJwBpoQDeOy7EF9TRZInEmxRP5KEqgm0K3m9ey8Bt21ly192Mfu5BajwaQ2bMIHLcxH5+IcTpkuB0ChKcTkNjPZU3/JD4R3+K8ZGfcvDWB3j5qIdC2xpcTVFcdd51GBQZhlwIIYQQZ7eQL8yKl8pIbHuQZLURmjw8f+N38DZNJ9rbwsgDf2bw5s2EfCFenPEdHAeMTF7/LAaHl7a0AsLBAImlG+mKjWDfhWPZExxG5iA/kTo31pYm3G0mBlbrOVLWTGJ2FA5dB8GtJUQOSkNbMB/LqKmYlIieWvNwEHxusDiOla/KFyRKp2LXqz2DZHTUQFTqZz8h0eckOJ2CBKfeuX/yUw5FFVCof51lY29hnzef2IEl5O96h/Y5dzDbmtXfRRRCCCGEOG0dPo1HdjajOVXifTauzjdhUGF3Q5jNu15m9v43SNx+ABSVrmFphEw6dCVNWF1uAqkOqvKz0WKTsbU302Gw4xo9lfiabegqazCVN4DZgBYwoYQV/ElWDEeaUbb5iIoyEM6Nw6AaMPg9qJ5O2q65lKgRF9AcsPBSiwdfWOO7ThXD079EKy5Di4jFdPd96O2xhDxu1L/8GsUQgFt+hGbvGVZdRv7sO1+54PTPf/6TP/7xj9TX11NYWMjf//53xowZ85nrv/TSS/ziF7+goqKCgQMHct9993HBBRec1rEkOJ2aVlZC+W2/JuM6J/u6Etgz8joi1ADVzj2M2rOZsfMXoTtJ218hhBBCiK+qHbUBnttcTJF7NQ6DlxhPGxF0sb+ggIaIODxeC75OhYMpgzG2e5hWuhxbUCPW0EKU1Y3eH0ANhUAHptYugt0aUd5OfEEdumY3+m4vHcYI3iu8nosPbCQUqKU7XkdUSzvOtXsBaE9KYNnMWxiRnUTWqmegqgZddSNtY/IoGVNAztpttBVk8t7M+ThtDrJsJvIsVuKNAzCoZqivJ9zWTDA3CxQFFT0hTU8IBYsqYeuzfKWC0wsvvMB1113Hww8/zNixY/nrX//KSy+9RElJCfHx8Sesv2HDBqZMmcKiRYuYN28ezz33HPfddx87duygoKCg1+NJcDq1ukuvxTEWWrw63rjoLjx0YfGXMPXINmqGXc7sIdP6u4hCCCGEEF+Y2s4wm6pCdAUgFIaxqToGx504+uhqvwtTWCOt24PX30mDx0OXyURDdTmZ5Vsx6dzEdbWhDwTwa3oiu1zY91aidvlQAM0dIGjUU/ON0QRsJuwdnTi3lGFo7ULTqfgznPiSnYQ9IawH6gmiorZ1Y2rqAL1C2GIkbNSjBkJowTBhiwHMenSt3YTNegJ2GwGHjaDRQEjVE7KZcSUlYTRbsOl1BKOiCJqtGLoCKG4/uD0ETCY88Q58MTEolhgMEQ4sDjPhgJeAz0sgCHp3J5GdDRi72jF2e1B0CqqiQzPpCdksKJFmFLsZzWwjrDOCzgA6PZreiE9nRtFUdCEfCVEjMOj6fyjUr1RwGjt2LKNHj+Yf//gHAOFwmLS0NL73ve/x05/+9IT1Fy5cSFdXF2+//faxZePGjWP48OE8/PDDvR5PgtOHQiHCe3bQvfEDgiW7oL4BXWsbJl87ey6dRfHwweS3HkbRKbTFpFCeOoGb08fLrxVCCCGEEP+l2qCPcCiMuTNEd9iMYoJ4q4rJ0BPMXJ1ejjZ0YTC3oukdGNucJEX46LC20XR0J/rKQygdLsKeLoxtbSgeLwGzHr0+iF4JE1R0BDQjpg4PtrYWzK5OdMEgCmGUYBhdhxeCIQhpKL4gSlgDkwoGXc8tpIE3gOoNQiAMaCj0jASLpvDhrPeE9SqaXgdGHSgKigqEwiiBEPjDPYnzw4Tx8ZWjhqJBONJE86Vj0C28n/ikjC/5DJzoTLJBv46X6Pf72b59O3ffffexZaqqMnPmTDZu3HjSbTZu3MgPfvCD45bNnj2b119//aTr+3w+fD7fsfsdHR2fv+B9qHtePqaaNkA79g92Rj4rx2ineLxn7kw0swGjzYjqtOGPi8Q7JI7KwcPxxzpIjvDQUXQjU23D0ElYEkIIIYT43JL1pp6rb9PJH4+KNDMs0gzE9CxIBNBjw0ZSTCqM/HLK+UXSAUn9XYj/Ur8Gp+bmZkKhEAkJCcctT0hI4ODBgyfdpr6+/qTr19fXn3T9RYsW8atf/apvCvwFML2xF52uf/sMGQHrh3/H9WdBhBBCCCGEOEud87387777blwu17FbVVVVfxfpOP0dmoQQQgghhBC969cap9jYWHQ6HQ0NDcctb2hoIDEx8aTbJCYmntH6JpMJk+kz6kOFEEIIIYQQ4jT0a3WH0WikqKiIlStXHlsWDodZuXIl48ePP+k248ePP259gOXLl3/m+kIIIYQQQgjxefVrjRPAD37wA66//npGjRrFmDFj+Otf/0pXVxc33ngjANdddx0pKSksWrQIgO9///tMnTqVBx54gAsvvJDFixezbds2Hn300f58GkIIIYQQQohzWL8Hp4ULF9LU1MQvf/lL6uvrGT58OO++++6xASAqKytR1Y8rxiZMmMBzzz3Hz3/+c+655x4GDhzI66+/flpzOAkhhBBCCCHEf6Pf53H6ssk8TkIIIYQQQgg4s2wgQ7oJIYQQQgghRC8kOAkhhBBCCCFELyQ4CSGEEEIIIUQvJDgJIYQQQgghRC8kOAkhhBBCCCFELyQ4CSGEEEIIIUQvJDgJIYQQQgghRC8kOAkhhBBCCCFEL/T9XYAv20fz/XZ0dPRzSYQQQgghhBD96aNM8FFGOJWvXXDq7OwEIC0trZ9LIoQQQgghhDgbdHZ2EhUVdcp1FO104tU5JBwOU1tbS2RkJIqi9GtZOjo6SEtLo6qqCrvd3q9lESeS83N2k/Nz9pJzc3aT83P2knNzdpPzc3b7b8+Ppml0dnaSnJyMqp66F9PXrsZJVVVSU1P7uxjHsdvt8gY8i8n5ObvJ+Tl7ybk5u8n5OXvJuTm7yfk5u/0356e3mqaPyOAQQgghhBBCCNELCU5CCCGEEEII0QsJTv3IZDJx7733YjKZ+rso4iTk/Jzd5PycveTcnN3k/Jy95Nyc3eT8nN2+jPPztRscQgghhBBCCCHOlNQ4CSGEEEIIIUQvJDgJIYQQQgghRC8kOAkhhBBCCCFELyQ4CSGEEEIIIUQvJDgJIYToM4qi8Prrr/drGZ544gkcDke/Hf+xxx7j/PPP/1z7qKioQFEUdu3a1TeF+hL5/X4yMzPZtm1bfxdFCCH6lAQnIYQ4C91www0oioKiKBgMBrKysvh//+//4fV6T3sfq1evRlEU2tvb+7x8//u//8vw4cNPWF5XV8fcuXP7/HgfmTZt2rHX5WS3adOmsXDhQg4dOvSFleFUvF4vv/jFL7j33ns/137S0tKoq6ujoKCgj0r25TEajfzoRz/iJz/5SX8XRQgh+pS+vwsghBDi5ObMmcPjjz9OIBBg+/btXH/99SiKwn333dffRftMiYmJX+j+X331Vfx+PwBVVVWMGTOGFStWkJ+fD/RctFssFiwWyxdajs/y8ssvY7fbmThx4ufaj06n+8JfS+ipHTIajX2+32uuuYYf/vCHFBcXHzs3QgjxVSc1TkIIcZYymUwkJiaSlpbGggULmDlzJsuXLz/2eDgcZtGiRWRlZWGxWCgsLOTll18Gepp6TZ8+HQCn04miKNxwww29bgcf11StXLmSUaNGYbVamTBhAiUlJUBPU7hf/epX7N69+1hNzxNPPAGc2FRv7969zJgxA4vFQkxMDLfccgtut/vY4zfccAMLFizgT3/6E0lJScTExHD77bcTCARO+ppER0eTmJhIYmIicXFxAMTExBxbFh0dfUJTvY9qx/7zn/+Qnp5OREQEt912G6FQiPvvv5/ExETi4+P53e9+d9yx2tvb+fa3v01cXBx2u50ZM2awe/fuU56zxYsXc9FFFx237KPn+Pvf/56EhAQcDge//vWvCQaD/PjHPyY6OprU1FQef/zxY9t8uqleb+fkdE2bNo077riDO++8k9jYWGbPng3An//8Z4YOHYrNZiMtLY3bbrvt2HnSNI24uLjj/keGDx9OUlLSsfvr1q3DZDLR3d0N9PzPTZw4kcWLF59R+YQQ4mwmwUkIIb4C9u3bx4YNG46rHVi0aBFPPfUUDz/8MMXFxdx1111885vfZM2aNaSlpfHKK68AUFJSQl1dHQ8++GCv233Sz372Mx544AG2bduGXq/nW9/6FgALFy7khz/8Ifn5+dTV1VFXV8fChQtPKHNXVxezZ8/G6XSydetWXnrpJVasWMEdd9xx3HqrVq3i8OHDrFq1iieffJInnnjiWBDrK4cPH2bp0qW8++67PP/88zz22GNceOGFVFdXs2bNGu677z5+/vOfs3nz5mPbXH755TQ2NrJ06VK2b9/OyJEjOe+882htbf3M46xbt45Ro0adsPz999+ntraWDz74gD//+c/ce++9zJs3D6fTyebNm7n11lv5zne+Q3V19Smfx2edkzPx5JNPYjQaWb9+PQ8//DAAqqryt7/9jeLiYp588knef/99/t//+39ATxieMmUKq1evBqCtrY0DBw7g8Xg4ePAgAGvWrGH06NFYrdZjxxkzZgxr16494/IJIcRZSxNCCHHWuf766zWdTqfZbDbNZDJpgKaqqvbyyy9rmqZpXq9Xs1qt2oYNG47b7qabbtKuuuoqTdM0bdWqVRqgtbW1HXv8TLZbsWLFscffeecdDdA8Ho+maZp27733aoWFhSeUG9Bee+01TdM07dFHH9WcTqfmdruP24+qqlp9ff2x55mRkaEFg8Fj61x++eXawoULe32Njhw5ogHazp07j1v++OOPa1FRUcfu33vvvZrVatU6OjqOLZs9e7aWmZmp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np6OXq8nMTHxiOd9suX129/+ltjYWDIzM4difeWVV5gxYwYGg4EXXngBgKeeeors7GwMBgNZWVn84x//OCKehoYGLrvsMqxWK2azmXHjxrFt27bjvsdOplx+97vfER0dTXBwMNdee+0xW2rPO+88Xn755VN6/SRJkr5RQpIkSfrali5dKhYtWjT094EDB4TNZhOFhYVD237zm9+IrKws8eGHH4qqqiqxfPlyodfrxbp164TP5xOvv/66AERZWZlobm4WPT09JzxPCCHWrl0rAFFYWCjWrVsniouLxbRp08TkyZOFEEI4nU5x5513itzcXNHc3Cyam5uF0+kUQggBiDfffFMIIUR/f7+IiYkRF1xwgThw4IBYs2aNSElJEUuXLj3ieYaEhIgbb7xRlJSUiHfffVeYTCbxxBNPnPA1qqmpEYDYs2fPEduXL18uQkNDh/6+//77RVBQkLjoootEcXGxeOedd4ROpxPz588Xt956qygtLRX//Oc/BSC2bt06dN7cuXPFeeedJ3bs2CHKy8vFnXfeKcLDw0VnZ+dXxhQaGipefvnlI7YtXbpUBAcHi5tvvlmUlpaKp59+WgBi/vz54re//a0oLy8Xv/71r4VWqxX19fXHfG4nKpOT9ZOf/ESEhYWJZ555RlRWVooNGzaIJ598Ughx8uUVFBQkrrzySlFUVCSKioqGYk1OThavv/66qK6uFk1NTeL5558XMTExQ9tef/11YbVaxTPPPCOEEKKvr0+kpqaKadOmiQ0bNoiKigrxyiuviM2bNx/3PXaicnnllVeEXq8XTz31lCgtLRU///nPRXBwsMjPzz/itSgpKRGAqKmpOaXXUJIk6ZsiEydJkqRvwNKlS4VarRZms1no9XoBCJVKJVasWCGEEMLlcgmTySQ2b958xHnXXnutuOyyy4QQn99sd3d3D+0/lfNWr149tP/9998XgBgYGBBCDCYjX74RFeLIxOmJJ54QYWFhor+//4jHUalUoqWlZeh5JiUlCZ/PN3TMxRdfLJYsWXLC1+hUEieTySR6e3uHts2fP18kJycLv98/tC0zM1MsW7ZMCCHEhg0bREhIiHC5XEc8dlpamnj88cePGU93d7cAxKeffnrE9sPP8cvXmjZt2tDfPp9PmM1m8dJLLx3zuZ1MmZxIb2+v0Ov1Q4nSl51seUVHRwu32z10zOFY//zn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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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Pn362K1PSEiwq6U4kgxfX1+jSpUqhoeHhyHJqF+/vnHu3DnDMAzjgw8+MJo1a2ZYrVbbmPz8fMPPz89YvXq1rf5+/frZbdfRcaUdt2EYRr169YxXX33Vbts//xt37tzZGDFihF2fgQMH2v09JBnTpk2z/Z6Xl2dIMr744otS/z6GYRgzZsywO183dOvWzRg/frxdrY8//rjt99OnTxuSjGeffdbWlpaWZkgyTp8+bRiGYSQmJhojR4602+7GjRsNDw8P4+rVq8XW8+qrrxoNGza8qf3nx3hjX/Pnz7e1LVmyxPD19S3x2Bw5J4ZhGJ9++qnh4eFhFBUVFVsjANwJuCMGAHepgwcP6ptvvtGgQYMkSZUqVVJCQoLtrpGj24iKirJr+/nvktSyZUt5enrafq9du7ays7MlSXv27FFRUZGaNm2qqlWr2pb169fbHjPbv3+/oqOj7bYZExPjUI2vvvqqdu3apS+++EKRkZF69913FRQUJEn69ttvdfjwYVWrVs2236CgIF27ds3uEbefc3RcacftqP3796tLly52bV26dNH+/fvt2tq0aWP7uUqVKvL393d6X7fy3/sICQmRJLVu3fqmthv7/fbbb7Vw4UK78xoXFyer1aqjR48Wu4+rV6/K19f3tvd/7do15ebmlngMjpwTPz8/Wa1W5efnl7gdAHC3Su4uAABwe+bPn6/r168rLCzM1mYYhnx8fPT6668rICCgzPbl5eVl97vFYrHNSpeXlydPT0+lp6fb/Qey9NNkC79UaGioGjdurMaNG2vBggXq06ePvvvuOwUHBysvL08dOnTQokWLbhpXq1atErfp6LjSjrusmbGv/97HjXfaimv773P7+9//XuPGjbtpWxEREcXuo2bNmrpw4UKZ7P9W27gx5uf9z58/rypVqsjPz6/E7QCAuxHEAOAudP36db3//vt6+eWX1bNnT7t18fHxWrJkiZ544olbbqdZs2bavn27hgwZYmvbvn27U7W0b99eRUVFys7O1v33319snxYtWmjbtm12bVu3bnVqP9JPd+s6dOigF154QXPnztU999yjZcuWKTg4WP7+/sWO8fb2VlFRkV2bI+McUdy2f65FixbavHmzhg4damvbvHmzIiMjb3u/Zrnnnnv03XffqXHjxg6Pad++vTIzM3XhwgVVr17dhdWVbO/evWrfvr1b9g0AjuLRRAC4C61cuVIXLlxQYmKiWrVqZbcMGDDA4ccTx44dq/nz5+u9997ToUOHNGvWLO3evdt2Z8IRTZs21eDBgzVkyBCtWLFCR48e1TfffKOUlBT961//kiSNGzdOq1at0p///GcdOnRIr7/+ulatWnVbx/7UU0/prbfe0smTJzV48GDVrFlT/fr108aNG3X06FGtW7dO48aN048//ijpp9kCd+/erYMHD+rs2bMqLCx0aJwj6tevrw0bNujkyZM6e/ZssX0mTZqkhQsX6s0339ShQ4f0yiuvaMWKFUpOTr6t4zfT5MmTtWXLFtskK4cOHdKnn35a6oQa7du3V82aNbV582YTK7W3cePGm/4HBQDcaQhiAHAXmj9/vmJjY4t9/HDAgAHasWOHdu/efcvtDB48WFOnTlVycrLuueceHT16VMOGDSvxHZ+SLFiwQEOGDNHEiRPVrFkzxcfHa/v27bbH1zp16qR33nlHc+fOVdu2bfXll19q2rRpTu3jhl69eqlBgwZ64YUXVLlyZW3YsEERERHq37+/WrRoocTERF27ds12p2vEiBFq1qyZOnbsqFq1amnz5s0OjXPEc889px9++EGNGjUq8VHI+Ph4zZ07V3/+85/VsmVLvfXWW1qwYIG6d+9+W8dvpjZt2mj9+vX697//rfvvv1/t27fX9OnT7R6H/TlPT08NHz682Mc+zXDy5Elt2bJFw4cPd8v+AcBRFsNwYp5jAEC59+tf/1qhoaE3fesKcFRmZqZatmypjIwM1atXz9R9T548WRcuXNDbb79t6n4BwFm8IwYAFdiVK1c0b948xcXFydPTU0uWLNFXX32lNWvWuLs03MVCQ0M1f/58HT9+3PQgFhwcrKSkJFP3CQC3gztiAFCBXb16VQ8++KB27typa9euqVmzZpo2bZr69+/v7tIAACjXCGIAAAAAYDIm6wAAAAAAkxHEAAAAAMBkBDEAAAAAMBlBDAAAAABMRhADAAAAAJMRxAAAAADAZAQxAAAAADAZQQwAAAAATEYQAwAAAACT/T/SP3HzD8rXEAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from pathlib import Path\n", + "import time\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectraCollection\n", + "from corems.mass_spectra.calc.lc_calc import PHCalculations, find_closest\n", + "\n", + "# Set the path to the collection of LCMS runs (previously processed)\n", + "collection_path = Path(\n", + " \"/Users/cies677/sandbox/corems/support_code/nmdc/lipidomics/curr/processed/pos\"\n", + " )\n", + "# Path to manifest file\n", + "manifest_file = collection_path / \"manifest_curr.csv\"\n", + "# This file will need to be created by the user or helper script?\n", + "chromatography_file = collection_path / \"long_lipid_gradient_chroma.csv\"\n", + " \n", + "# Set the number of cores to use for loading the data (the parser is parallelized)\n", + "ncores = 8\n", + "\n", + "# Instantiate the parser\n", + "parser = ReadCoreMSHDFMassSpectraCollection(\n", + " folder_location = collection_path,\n", + " manifest_file = manifest_file,\n", + " chromatography_file = chromatography_file,\n", + " cores = ncores\n", + ")\n", + "print(\n", + " \"Loading LCMS collection with\", \n", + " len(parser.manifest), \n", + " \"samples using\", \n", + " ncores, \n", + " \" cores\"\n", + ")\n", + "\n", + "# Load the LCMS collection (minimally load the data)\n", + "start_time = time.time()\n", + "lcms_collection = parser.get_lcms_collection(\n", + " load_raw=False, \n", + " load_light=True\n", + " )\n", + "print(\n", + " \"Time to load LCMS collection \", \n", + " time.time() - start_time, \n", + " \"seconds -\", \n", + " len(lcms_collection), \n", + " \" LCMS runs and \", \n", + " ncores, \n", + " \" cores\"\n", + ")\n", + "#10s for 7 samples, 10 cores; 162s for 70 samples, 10 cores\n", + "\n", + "# Set flag to call _drop_isotopologue() when running _check_mass_features_df()\n", + "lcms_collection.parameters.lcms_collection.drop_isotopologues = True\n", + "print(\n", + " \"Number of total mass features: \", \n", + " len(lcms_collection.mass_features_dataframe)\n", + ")\n", + "\n", + "# Align the LCMS runs between each other\n", + "print(\"Aligning LCMS collection\")\n", + "start_time = time.time()\n", + "lcms_collection.align_lcms_objects()\n", + "print(\n", + " \"Time to align LCMS collection: \", \n", + " time.time() - start_time, \n", + " \"seconds\"\n", + ")\n", + "#1.5s for 7 samples; 15s for 70 samples\n", + "\n", + "# Make some plots \n", + "lcms_collection.plot_tics(type=\"both\")\n", + "lcms_collection.plot_alignments()\n", + "# TODO: Think about other plots that would be useful to have here for assessing the quality of the data and alignment\n", + "\n", + "# Make consensus mass features from the consolidated mass features\n", + "start_time = time.time()\n", + " \n", + "## Inconsistently getting a repeated RuntimeWarning: Mean of empty slice\n", + "## return np.nanmean(a, axis, out = out, keepdims = keepdims)\n", + "## Should check what causes this, make sure output is understood, and if\n", + "## yes suppress the warning" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "15100669-9da5-437f-bc1b-2255ae7ac055", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time to roll up consensus mass features: 218.44262504577637 seconds - 151007 total mass features 8 cores\n" + ] + } + ], + "source": [ + "lcms_collection.add_consensus_mass_features()\n", + "# THIS FUNCTION NEEDS WORK AND NEEDS MECHANISM TO EVALUATE THE RESULTS (plots? reports?)\n", + "\n", + "print(\n", + " \"Time to roll up consensus mass features: \", \n", + " time.time() - start_time, \n", + " \"seconds -\", \n", + " len(lcms_collection.mass_features_dataframe), \n", + " \" total mass features\", \n", + " ncores, \n", + " \" cores\"\n", + ")\n", + "\n", + "#TODO: Add code to load and save information about chromatographic settings\n", + "#TODO: Add code to save and load collection to HDF5 file\n", + "#TODO: Add code to plot a consensus mass feature" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "359b16fc-4efc-4504-83ee-027e47850b10", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkQAAAHHCAYAAABeLEexAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuNSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/xnp5ZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOy9d3xU1533/5leNZqiPupCEpIA0THC2BQbgwFjOxg79sZ2nNjZbJJtTzZ5dveXNUlem03iXWfz5LGTOHFJstiOjQHbYGMwxTQBEiAhQA11jTSapjK93t8fPOdkZjQzGkmjBvf9evGyNXPm3nPPPeV7vudbOAzDMGBhYWFhYWFhuYPhznQFWFhYWFhYWFhmGlYgYmFhYWFhYbnjYQUiFhYWFhYWljseViBiYWFhYWFhueNhBSIWFhYWFhaWOx5WIGJhYWFhYWG542EFIhYWFhYWFpY7HlYgYmFhYWFhYbnjYQUiFhYWFhYWljseViBiYUkQHA4Hu3fvpn+/9dZb4HA46OzsnLE6TSc1NTWoqqqCTCYDh8NBXV3dTFfptiM/Px/PPvss/fvkyZPgcDg4efLkjNVpKlm3bh3WrVs309VguUNgBSIWljh49dVXweFwsGrVqpmuyqzE6/Xiscceg8ViwS9+8Qv86U9/Ql5eXsLv09fXh927d7PCFgsLS8Lhz3QFWFjmAnv27EF+fj4uXryImzdvYt68eTNdpVlFW1sburq68Lvf/Q5f//rXp+w+fX19+OEPf4j8/HwsXrx4yu7DwsJy58FqiFhYxqCjowPnzp3Dyy+/jNTUVOzZs2emqzTrMBgMAAClUjmzFZkgLpcLgUBgpqvBwsIyg7ACEQvLGOzZswcqlQpbt27Fzp07Ey4QPfvss5DL5eju7sa2bdsgl8uh1WrxyiuvAAAaGhqwYcMGyGQy5OXl4e233w75vcViwXe/+10sXLgQcrkcCoUCW7ZsQX19/ah7/epXv0JFRQWkUilUKhWWL18ecj2r1Yq///u/R35+PkQiEdLS0nD//ffj8uXLMet/7733AgAee+wxcDicELuPpqYm7Ny5E2q1GmKxGMuXL8dHH3007mc4efIkVqxYAQD46le/Cg6HAw6Hg7feegvAaPsaQrgdCrG7effdd/H//X//H7RaLaRSKUZGRgAAFy5cwObNm5GcnAypVIp7770XZ8+eDbnmRNopFv/5n/+JqqoqaDQaSCQSLFu2DHv37p3QtSIRT31Pnz6Nxx57DLm5uRCJRMjJycE//MM/wOl0hlxrsv2V2NadOnUK3/jGN6DRaKBQKPD0009jcHBwzGdxu9148cUXMW/ePFrP733ve3C73SHljh49irvvvhtKpRJyuRylpaX4l3/5l4k2IcsdAHtkxsIyBnv27MGjjz4KoVCIL3/5y/j1r3+NmpoaujgnAr/fjy1btuCee+7Bz3/+c+zZswff/va3IZPJ8K//+q946qmn8Oijj+I3v/kNnn76aaxevRoFBQUAgPb2dhw4cACPPfYYCgoKMDAwgN/+9re49957cePGDWRlZQEAfve73+Fv//ZvsXPnTvzd3/0dXC4Xrl69igsXLuDJJ58EAPz1X/819u7di29/+9soLy+H2WzGmTNn0NjYiKVLl0as+ze+8Q1otVr85Cc/wd/+7d9ixYoVSE9PBwBcv34da9asgVarxf/+3/8bMpkM7733Hh5++GF88MEHeOSRR+J+hrKyMvzoRz/Cv/3bv+GFF17A2rVrAQBVVVUTavMf//jHEAqF+O53vwu32w2hUIjjx49jy5YtWLZsGV588UVwuVy8+eab2LBhA06fPo2VK1dOuJ1i8ctf/hIPPfQQnnrqKXg8Hrz77rt47LHHcPDgQWzdunVCzxdMPPV9//334XA48M1vfhMajQYXL17Er371K/T29uL9998Pud5k+ivh29/+NpRKJXbv3o3m5mb8+te/RldXFxVYIxEIBPDQQw/hzJkzeOGFF1BWVoaGhgb84he/QEtLCw4cOADgVr/btm0bFi1ahB/96EcQiUS4efPmKMGWhSUEhoWFJSq1tbUMAObo0aMMwzBMIBBgsrOzmb/7u78bVRYA8+KLL9K/33zzTQYA09HREfMezzzzDAOA+clPfkI/GxwcZCQSCcPhcJh3332Xft7U1DTqPi6Xi/H7/SHX7OjoYEQiEfOjH/2IfrZjxw6moqIiZl2Sk5OZb33rWzHLROLEiRMMAOb9998P+Xzjxo3MwoULGZfLRT8LBAJMVVUVU1xcPO5nqKmpYQAwb7755qg65OXlMc8888yoz++9917m3nvvHVXXwsJCxuFwhNSruLiYeeCBB5hAIEA/dzgcTEFBAXP//ffTzybaTtEIrgfDMIzH42EWLFjAbNiwIeTz8Gckz3LixImY14+nvuF1YBiG+Y//+A+Gw+EwXV1d9LPJ9lcyLpYtW8Z4PB76+c9//nMGAPPhhx/Sz8Lf3Z/+9CeGy+Uyp0+fDqnnb37zGwYAc/bsWYZhGOYXv/gFA4AxGo0xn5mFJRj2yIyFJQZ79uxBeno61q9fD+CWa/3jjz+Od999F36/P6H3CjZGViqVKC0thUwmw65du+jnpaWlUCqVaG9vp5+JRCJwubeGst/vh9lspkcEwUciSqUSvb29qKmpiVoHpVKJCxcuoK+vb9LPY7FYcPz4cezatQtWqxUmkwkmkwlmsxkPPPAAWltbodPpxvUMieSZZ56BRCKhf9fV1aG1tRVPPvkkzGYzra/dbsfGjRtx6tQpameUyHYCEFKPwcFBDA8PY+3atQl79njqG1wHu90Ok8mEqqoqMAyDK1eujCo/0f5KeOGFFyAQCOjf3/zmN8Hn8/HJJ59EreP777+PsrIyzJ8/n74fk8mEDRs2AABOnDhB6wMAH374IWsbxhI3rEDEwhIFv9+Pd999F+vXr0dHRwdu3ryJmzdvYtWqVRgYGMCxY8cSdi+xWIzU1NSQz5KTk5GdnT3q+CA5OTnE1iIQCOAXv/gFiouLIRKJkJKSgtTUVFy9ehXDw8O03Pe//33I5XKsXLkSxcXF+Na3vjXqCOHnP/85rl27hpycHKxcuRK7d++OuJjFw82bN8EwDH7wgx8gNTU15N+LL74I4C/G2PE+QyIJP8JpbW0FcEtQCq/v73//e7jdblqXRLYTABw8eBB33XUXxGIx1Go1UlNT8etf/zphzx5Pfbu7u/Hss89CrVZDLpcjNTWV2oaF12My/ZVQXFwc8rdcLkdmZmbMuF2tra24fv36qPdTUlIC4C/96fHHH8eaNWvw9a9/Henp6XjiiSfw3nvvscIRS0xYGyIWligcP34c/f39ePfdd/Huu++O+n7Pnj3YtGlTQu7F4/HG9TnDMPT/f/KTn+AHP/gBnnvuOfz4xz+GWq0Gl8vF3//934csAGVlZWhubsbBgwdx+PBhfPDBB3j11Vfxb//2b/jhD38IANi1axfWrl2L/fv348iRI3jppZfws5/9DPv27cOWLVvG9Uzk3t/97nfxwAMPRCxDwhfE+wyxiGZ34vf7I7ZjsEYkuL4vvfRSVJd+uVwOILHtdPr0aTz00EO455578OqrryIzMxMCgQBvvvnmKIPkiTJWff1+P+6//35YLBZ8//vfx/z58yGTyaDT6fDss8+OegeT6a+TIRAIYOHChXj55Zcjfp+TkwPg1rs9deoUTpw4gUOHDuHw4cP485//jA0bNuDIkSNR68lyZ8MKRCwsUdizZw/S0tKo90ww+/btw/79+/Gb3/xm1MI63ezduxfr16/H66+/HvL50NAQUlJSQj6TyWR4/PHH8fjjj8Pj8eDRRx/Fv//7v+Of//mfIRaLAQCZmZn4m7/5G/zN3/wNDAYDli5din//938f90JfWFgIABAIBLjvvvsS8gzRhB4AUKlUGBoaGvV5V1cXrUssioqKAAAKhWLM+gKJa6cPPvgAYrEYn332GUQiEf38zTffHNd1JlPfhoYGtLS04A9/+AOefvpp+pujR48mtA7BtLa20qNoALDZbOjv78eDDz4Y9TdFRUWor6/Hxo0bY/YFAOByudi4cSM2btyIl19+GT/5yU/wr//6rzhx4kRc75flzoM9MmNhiYDT6cS+ffuwbds27Ny5c9S/b3/727BaraPcx2cCHo83agf+/vvvU/scgtlsDvlbKBSivLwcDMPA6/XC7/ePOhpJS0tDVlbWKJfmeEhLS8O6devw29/+Fv39/aO+NxqN434GmUwGABEFn6KiIpw/fx4ej4d+dvDgQfT09MRV32XLlqGoqAj/+Z//CZvNFrW+iW4nHo8HDocTYpPW2dlJPaYmSzz1JRqT4HfAMAx++ctfJqQOkXjttdfg9Xrp37/+9a/h8/liCpS7du2CTqfD7373u1HfOZ1O2O12ALfs18IhWr+JvCOWOwNWQ8TCEoGPPvoIVqsVDz30UMTv77rrLhqk8fHHH5/m2oWybds2/OhHP8JXv/pVVFVVoaGhAXv27BmlFdm0aRMyMjKwZs0apKeno7GxEf/3//5fbN26FUlJSRgaGkJ2djZ27tyJyspKyOVyfP7556ipqcF//dd/Tahur7zyCu6++24sXLgQzz//PAoLCzEwMIDq6mr09vbSOEPxPkNRURGUSiV+85vfICkpCTKZDKtWrUJBQQG+/vWvY+/evdi8eTN27dqFtrY2/M///A/V/IwFl8vF73//e2zZsgUVFRX46le/Cq1WC51OhxMnTkChUODjjz+G1WqNq51OnjyJ9evX48UXXwzJcRfO1q1b8fLLL2Pz5s148sknYTAY8Morr2DevHm4evXq+Bs9jHjqO3/+fBQVFeG73/0udDodFAoFPvjgg7jiAk0Uj8eDjRs3YteuXWhubsarr76Ku+++O+qYA4CvfOUreO+99/DXf/3XOHHiBNasWQO/34+mpia89957+Oyzz7B8+XL86Ec/wqlTp7B161bk5eXBYDDg1VdfRXZ2Nu6+++4peyaWOc6M+bexsMxitm/fzojFYsZut0ct8+yzzzICgYAxmUwMw0zO7V4mk436/N57743oJp+Xl8ds3bqV/u1yuZj/9b/+F5OZmclIJBJmzZo1THV19SiX5d/+9rfMPffcw2g0GkYkEjFFRUXMP/3TPzHDw8MMwzCM2+1m/umf/omprKxkkpKSGJlMxlRWVjKvvvpqzPozTHS3e4ZhmLa2Nubpp59mMjIyGIFAwGi1Wmbbtm3M3r17x/0MDMMwH374IVNeXs7w+fxRLvj/9V//xWi1WkYkEjFr1qxhamtro7rdR6orwzDMlStXmEcffZS2U15eHrNr1y7m2LFj42qnjz/+mAHA/OY3vxmz/V5//XWmuLiYEYlEzPz585k333yTefHFF5nwKXoibvfx1vfGjRvMfffdx8jlciYlJYV5/vnnmfr6+lFtPNn+SsbFF198wbzwwguMSqVi5HI589RTTzFms3nUNcPfv8fjYX72s58xFRUVjEgkYlQqFbNs2TLmhz/8Ie3Lx44dY3bs2MFkZWUxQqGQycrKYr785S8zLS0tUduJhYXDMAmydmNhYWFhoXzve9/DO++8g5s3b4bYBt3pvPXWW/jqV7+KmpoaLF++fKarw8JCYW2IWFhYWKaAEydO4Ac/+AErDLGwzBFYGyIWFhaWKSBWAEwWFpbZB6shYmFhYWFhYbnjYW2IWFhYWFhYWO54WA0RCwsLCwsLyx0PKxCxsLCwsLCw3PGwRtVxEAgE0NfXh6SkpDHDxbOwsLCwsLDMDhiGgdVqRVZWFrjc2DogViCKg76+Ppo0kIWFhYWFhWVu0dPTg+zs7JhlWIEoDpKSkgDcalCFQjHDtZldMAwDh8MBqVQaoj0bGBiA2WyGRqNBeno6GIaB3W6H0+mERqMZU1JnYWFhmatcu3YNLpcLDMNAIpFgwYIFMcv7/X709fUhKyuL5pVjSQwjIyPIycmh63gsWIEoDshCr1AoWIEojOHhYQwMDKCgoCCkbfr6+pCZmQmn00k/T05OnqlqsrCwsEwbK1aswLVr18AwDBYuXAiBQBCzfHd3NwoLC2E0GpGbmzvp+/v9fuh0Omi1WlbA+n/EY+7CbtPvMIimxmq1wm63j8owPl46OzuRkZGBzs7OkM/nzZsHt9uNefPmTer6LCwsLLOFQCAAo9GIQCAQs5xAIMCSJUuwdOnSMYUhANBqtTAajdBqtQmpp06nQ2pqKnQ6XUKud6fACkRzlHgHZiAQgMFggM1mo8dbIyMjMJlMsFqtcDgck6pHeXk5DAYDysvLQz7n8/koKSkBn88qIVlYWG4PzGYzkpOTYTabE3pdHo+H3NzchGlzEi1g3SmwAtEcJd6BaTabIRQKYbPZ4HA4wDAMuFwu1QxNVkMkEAiwaNEiCAQCas3f1dUFv98/qeuysLDMDfx+P7q7u+fsmB9P/TUaDYaHh6HRaKahZhMn0QLWnQIrEM1R4h2YGo0GHo8HcrkcUqkUMpkMMpkMaWlpSEpKgkwmS1idHA4Huru7MTw8jJ6enoRdl4WFZfYy149nent7IZPJ0NvbO2ZZLpeL1NTUqE4hXq8XV69ehdfrTXQ1WaYBViCao4w1MMmRGgCkpaVBLpeDw+GAw+FALpdDLpdDJpMlNK4SwzAIBALQ6/VzdrfIwsIyPub68YxarcbIyAjUavWkr9XY2AipVIrGxsYE1IxlumEFotuUgYEB3Lx5EwMDA1N6H7vdjv3796O7uxsSiQQ+nw+5ubnweDxTel8WFpbZwXiPZ+K1f5wu5HI53TROlpSUFFitVqSkpIzrd7OtTe5UWIHoNqW7uxtpaWno7u6e0vscPHgQbW1tOHPmDCwWCxYuXAgul4vi4uIpvS8LC8vcJJGGycRrdjK2kBwOJ6a2fDzCSkZGBrKzs5GRkTGuOhiNRlitVqrVZ5kZWIHoNmXJkiWwWCxYsmTJlN7HarVCpVLBYrFArVZP2LssERMbCwvL7CeRhskOhwNisXjS3rKxGI8AN5YpQzScTieEQiGcTudEq8mSAFiBaI5CBAiXy4WzZ8/C7XaHfC8UCrFixQpYrVa88soraGlpGZc6lmEY2Gy2MYWUXbt2gc/n46mnnoLL5Zrw80zHxMbCwjLzTFRoiIRUKoXL5YJUKk1AzSIzHZ5lOTk5CAQCbIqoGYbDsFvyMRkZGUFycjKGh4dnRaRqt9uNEydOwGg0gsfjYf369Whvb8eaNWtGlf35z38OgUAAp9OJ5557Dlwul3qbxTKottvtCAQCYBgGPB4vpjdatPQd4yER15gL92RhYWFhmT7Gs36zGqI5SG1tLQYGBuB2u+HxeHDhwgUsX748Ytn09HQMDw9DrVbD7XaHxCSKBRESeDzemLuvsc7g4yER1xgvrFaKhYWFhYXACkRzkOXLlyM7OxsSiQSVlZXYtm0bRCJRxLKPPvooSktL8cgjjyA7OzskJlEsiHt+ooSU2WgjNB3qdhaWyTIbxw5LZMxmM1555RX09PSw72sOwh6ZxcFsOzKba3g8HtTU1GD+/PkQi8UJDQbJwnK7Y7fbIRaL4XK52LEzy3n11VexePFiXLlyBc8+++yYpgZWqxX9/f0oKipi0xxNEeyRGcusor6+Hrm5uWhsbGR3TSws44TVZEZmumL3jEdD9/jjj+PKlSt46KGHxnxfDocDOp0OKpUK7e3tiaouyyRgNURxwGqIIuPz+dDe3o7CwsKQ3U0gEIDZbIZGowGXy4XH40F9fT2Ki4uRnJzMGjCzsLBMGqPRSOfl1NTUKbsPSYxNzAgSBashmh5YDRHLtNDe3o6srKxRu5vwuB0kBIBSqYwqDLF2EiwsLONhuhKtcjgcKhAl+roKhQKlpaWsMDRLYAWiOc54MjV7PB5cvHgRbW1t4841FklgKSwsRF9fHwoLC0PKTmSiYj2+WG4HyDghWgWWqWOi8YyIZibe4zapVDqmty27obs9YAWiOYjf70dHRwcuX76M+vp6aDSaiJmm3W43vvjiC9y4cQM+nw9XrlyBXq/H9evXx52NnsQlstvt9LNoUaknMlGxdhIstwMOhwNWqxWBQIAV7sOYLfm6HA4HHA4H+Hx+XNGn4wkJYjabcfz48YSkI2GZOViBaA6i0+lgMpng9XoRCATQ1tYWMdN0bW0tpFIp+vv70d7eDh6PB7/fDx6PN+6dTCLUxiT6tcFgGDUpzkQcIhaWRMMwDCvcRyGROcwmg1QqhVQqhc/nS9hx2/nz51FaWorz588n5HosMwMrEM1BtFotUlJSIBAIkJ2djQULFkTMNL18+XI4HA5kZmaisLAQlZWVKCgoQFlZGXJzc8d1z7HUxl6vF1evXoXX6416DYfDAbvdDqFQOOOTIgvLVCCTycDj8ZCamsoK92FMl83PWHA4HCQlJY2pxR7PMdiGDRvQ2tqKDRs2jKsuPp8PLS0t8Pl84/ody9TAepnFwVz1MpvO1BRXr15FXl4eurq6sGjRIvq5yWTCO++8gx07diA7O5uqq1NSUhKSy4iFhYVlKrDZbLDZbJDL5Qn1LgumubkZIpEIbrcbpaWlU3KPOx3Wy+wOYKzdi8vlwtGjRwFgymwZ3G43TSxbVlaGrq4ulJWVhZR59913sWTJEnz44YdwOp2Qy+VITU2F0+mMWHfWOJGFZfqYyHiLRxt8OzAdjh7Jycmw2+1ITk6esnuwxA8rEM1Rxhqs1dXVWLhwIc6ePTtltgznzp1Dc3Mzzp07B4FAgEWLFkEgEISUeeKJJ3DlyhXs2LGD1iNW3VlvMxaW6SN4vMVr9NzY2Ii8vDw0NjZOUy0Tw3g8cgEgJSUFXq8XKSkpU1antLQ0+o9l5mEFojnKWIabq1evRlNTE9auXQuHwzElGpe+vj6o1Wr09fVFLZOSkoLvfOc7yM3Npcd2serOGqSysEwfweMtXqPnaNrg2Y5Op0NqampEj9xITNStfzxMxz1Y4od9C3OUQCAAk8mE3t5evP7669DpdCFCj1gsxsKFC/HrX/8a169fh9VqjfvaDMNgeHh4TLX4jh074PP5sGPHjpjXIip5hmFgsVhw4sQJ8Hi8iHZNrLcZC8v0ETze4jV6jqYNnu1otVoYjcaIHrmTIRAIQKfT4dixY3C5XAm9Nsv0whpVx8FsM6r2er34/PPP0d7eDrfbjZUrV6K5uRlPPPFESDLBn/70p+BwOHA6ndiyZQvEYjHS09ORlpYGDocT1eDabrejubkZYrEYHo8HlZWVEzbODk5MCQBnzpyBXq9HZmYm7r///mkz+mZhYWGZCvR6PQ4fPgyTyYR58+bh4YcfjlmebGalUim7+ZsGWKPq25zGxkZ0dHRAKpXC6/WioaEBmzdvHnXMlJGRAafTSdXgfD4ffX19MJvNMW11pFIpVCoVXC4XMjIyJmXXE6ySl0qlGBoagkKhgNVqHdd1WWNrFhaW2Yjb7cbg4CDEYjH0ev2Y5Q0GA3p7ezE8PMzaSs4yWIFoDlJWVoalS5ciEAjgsccewze+8Q1otdpRO40vfelLKCkpwbp167Bq1Sr4fD5kZWVBo9HEtNXhcDjIy8tDTk4O0tLSJmXXYzQa8bvf/Q5GoxEcDgfbt2+HUCjEli1bxnVd1tiahWV2cKfEzon3ObOzs3H//fdDIBDg8ccfH/O6w8PDUKlUMJvNrK3kLIM9MouD2XZkNtWQ+EUMw0xapbt79256ZPaDH/wALpdrQtedzphKLCwsowkEAjCbzTCbzcjOzkZfXx9KSkpmulpTRnNzM1QqFQYHBxMaI8jn86G9vR2FhYVsUtdpYDzrN/s25hCBQAAGgwE6nQ42mw1+vx8pKSmQy+XIycnB4OAgGIaBRCIBj8eDWCyGxWKBWq2mmhgiTJAjKA6HM+pzk8kEgUCAwcFBBAIBuN1uiEQiGqmapPEgQpNYLEZvby8EAgH0ej3EYjFUKhV4PB4UCgVGRkYgkUjQ09MDoVAIi8UCmUwGjUYDp9MJiUQCt9tN6ymRSOBwOODz+dDV1YW8vDxwuVy4XC74fD5YLBaIRCIMDw8jIyMDIyMjyM7OptG6o004pM4ikQg6nQ4qlQpcLhcymYw+NznXB0CfTyqVwul00vbUaDTgcrm0DYMDTRL7AIlEQts2+LdqtRpOp5PWh7Q7uSfJFReeJkUqldL6OJ1OaDSaEOGS1DdcYIxUx/D2CH6nNpsNLpeLPmO0smPZQcwmAZa0gd1uh0wmm7SQT54tuD8E2+QxDAOz2UzfNYfDgUgkQl9fH7RaLbhcbkhZ0o4SiYRqDUi/IWPB7/dTr06NRgOLxYJAIEDLkGcSi8Xo7OyETqdDZmYmeDweuFwuHVtOpxMqlQocDgd6vR7Nzc1ITk5GdnY2MjIycPnyZRQUFCA1NRXt7e1QqVRITU1FIBDAlStXqMb4zJkzKCoqgslkwo0bN5CRkUFTCZG+JJPJIBaL4Xa7IZFIIJFI4HQ6YTKZqBE30ZCQY3yJREL7KcMw4HK5kEgksNvtNCSA1+tFVlYWHe9+vx9DQ0NQqVSQyWSwWCxUA+N0OpGdnQ2Xy0XHIwB6L6fTGfJZ8HhSKBSwWCzIyMiA0Wgcc0xE6yfh3/P5fBQXF8PhcMDlcoX0RyJ0ht+LZXpgNURxMFs0REajEb29vdDr9TCbzRAIBBAKhSguLkYgEEBKSgqcTid4PB7UajWGhoaQkZEBvV4PrVZLBx/wl2StDMOAx+OFfC4SidDW1oa0tDQYDAakpaVheHgYarWalrVarTCbzVCpVBgYGIBUKkVjYyM1xJbJZMjLy0NHRwdOnz6NqqoqZGdnw+l00jggfD4fSqWS1tNqtUKr1cJisUAsFuPGjRtITU2F0WhEeno6BAIBXTz6+vqQkZEBg8GA8vJy2O12mo6kpaUFWVlZo3awxMC7ra0NGo0GBoMB2dnZdIESCARwu900Kq3f7weHw4HL5YJarYZOp0NGRgaGh4eRmpoKu90Oq9UKsVgMr9dL60qEyZSUlFG/1ev1UKlUVBglEzCZ/EgC3eDcceRoUSwWw2QyQaVSYWRkBEqlEhwOh/6WaOKCDesj1TG8PchvSJZ2oVAIn88Xsyx5TpfLhaSkpJB7Rio/k5A2IO0Zqb7jvZ5YLA7pD1KplD6vw+FAcnIyfdcMw0Cv1yMnJ4curMFlSTt6PB4oFAq4XC46hslYaGtrg0QigdfrhVAohFwuh16vB5/PB5/Pp4JEf38/+vr64PP5YLPZkJaWhqSkJAQCAfB4PIhEIvj9fjAMg+bmZni9Xng8HhQUFGBwcBBFRUXo7++HUCiEVquFzWZDeno6BgcHwefzYTAY4PV6odFoMDg4SAWGzs5OqNVqeL1ecDgcCAQCuoki44nP58PhcMDpdCIQCEAmk1FBzWq1wu12QywW088B0M2SzWaD3W6HyWRCcnIyOBwOMjIywOFwMDg4CKlUikAgAC6XS+tDnpekL/J6vWhra0NJSQnkcnnIZhH4y3gn40kkEtFNplKppOOeMFYk61hjwG63h9yPfG80GulaE3wvlokznvWbFYjiYLYIRNOlIQre/RKVcbiGyGg0gs/nY2RkBFqtdpSGiJSXSCTw+Xyw2+2YN28eHA4H1aAE74Lj1RCJRCJWQ8RqiMYFqyGKX0N07tw56HQ6zJ8/H0KhEFlZWVRD1NbWBoVCAbVajcbGRigUCigUijmjIWpsbERxcTG6u7tRVFQ0poYo/H2Gj4nOzk6cOXMGd999N/Lz86P2k0hjIJpZAqshSjysQJRgZotARAgeaH6/P67zaKfTiS+++AILFixAVlbWuAZbpIEda+AG734ARFxgWVhYZh8HDx6ERCJBX18fli9fPueCL8bC6/WisbERZWVlCYmh9N5776GoqAhtbW3YtWtX3L+bTZuFOwHW7f42Z2RkBNevX8eHH36IDz/8EBqNBu3t7SFlrFYr3nnnHQwMDIBhGBw7dgydnZ04f/78qEi04cETiXqayMrkeI1oL4DYEVbFYjGGhobAMAzNXcYKQywss49AIAC9Xo/6+np4vV7cddddMBqNKCoqosdW01WPeNKGTIaxAkqSOpDj1bF48MEH0dHRgQcffHBc9bBYLDh9+jQsFsu4fscy9bCr1Byks7MTer0eIyMj4HA4OH36NAoLC0PKHDp0CBUVFTh27BgcDgc967bZbBCLxSFlg13aiRrX7/dTF/dge5ZIhE9mxK7A7XazOyAWllmM2WxGX18fkpKS0NjYCI1Gg61bt6K4uHhabVjiTRsy1XXg8/n0+G4s5HI5du7cGdF+KBaXLl1CVlYWLl26FPJ5IBBAX18fLl68SM0fWKYXViCag5SXl9N4QhqNBlu2bBl1XLZ161Zcv34dGzduhFQqxfbt2yGVSrFhw4ZRBn7hwRM5HA61FyLfB/8djsFggNVqhcFgAIC4UwCwsLDMLBqNBllZWbBarSgrK6NG59Ot1Z0Nc4ZGo8HIyAg1hE8EkQLKLl26FDqdDvn5+SHaqIGBAZw5cwZXrlxBfX09G3NtBmBtiOJgttkQxUv4WfVEPX/GOvPu6uqiWqS8vDz6udVqxaFDh7B161bI5fK4zs1nq1Hh7XLu7/f7odPpoNVqqVEqC8vtyET6+kTHebTfRZpzGYZBT08PkpKS4PF4IJfLIZPJ0NzcjHPnzoHP50MoFGLXrl1zeq6ZLbA2RCzw+XxoaGigrq7AxDPJh0eJZhgGIyMjaG5uhs/nQ3Z2NgDQ/xIOHTqEJUuW4NChQyHXiJWCYzaoziNxu0TKHm/GbxaW6cDpdOLw4cO4efMmDcsRjXjT+Eykr080uXS0+SHSnMvhcJCdnQ2fz0e18gBQVFSEZcuWQSaTYdu2bawwNAOwGqI4mIsaopaWFmRmZqKjowMLFiyg7qUTGWThux+r1YpLly5BLBZDqVRi/vz5EX8XSUPEMAyNiULc3YPd/6O5uM40rIaIhWXqOHLkCNRqNfR6PRYtWkRjikUiXk33VPf13t5e/OlPf8KXvvQlzJs3b1JzbCK5XeaqRDFnNESnTp3C9u3bkZWVBQ6HgwMHDoR8z+FwIv576aWXaJn8/PxR3//0pz8Nuc7Vq1exdu1aiMVi5OTk4Oc///l0PN6UE8kjjFBQUID29nbk5ubCZDLRAGUTIXzXRILFEe+zaMjlcmzfvh1yuZxeg1yP1DfciDuW99pMMtGd42yDx+MhNzeXFYbuEOZKUuS1a9fCZDJh/vz50Gq1EcsQ5w0iDI2l6Z7qvv7mm29CKpXinXfegcPhiDk/kHhTBw8epHNepPficrlw4sQJGixyItwu2uyZYEZXHbvdjsrKSrzyyisRv+/v7w/598Ybb4DD4eBLX/pSSLkf/ehHIeW+853v0O9GRkawadMm5OXl4dKlS3jppZewe/duvPbaa1P6bFMNwzAwGAxobGzEH//4R9y4cYMKJ4FAAI2NjThx4gS6u7tpxFUS4CzW5MgwDKxWK7q6uqKqrrOzs5Geng6tVhszl1HwwCQTgFQqBZfLhVQqhd1upwEMYxltzwRzZSFhYYnFTC2O4x0/EokEmzdvxrx586IKMOQ4naT+SfTmZLx1Li8vx+DgIEpKSmIKQiQo6NmzZ6FUKnHs2DHYbDYYDAYYDAZ6v0AggE8++QQ3btzAiRMnJvwck0nGfaczo7nMtmzZgi1btkT9PiMjI+TvDz/8EOvXrx/lYp6UlDSqLGHPnj3weDx44403IBQKUVFRgbq6Orz88st44YUXJv8QM4Db7cbnn3+OtrY2OJ1OFBYW4ty5c0hLS0NqairMZjP2798PPp+Pffv24emnn8b169exePFiZGZm0t1MJOx2O3p7eyGXy9Hb2xtiJE3g8XgoKCgY9Xm4qpZEVyb/9fv9MJlMNO2FzWajsYpmG8ELyUynnmBhmShk4zFW6IxEMxXjW6PR0OP0qcBqtaKvrw9ZWVlxmUZs3boVKpUKixcvjip8kHnE5XKhsLAQra2tWLhwISwWC7XvJHMMSZyrUCjQ398/4ecI1sazjI/ZdS4Rg4GBARw6dAhf+9rXRn3305/+FBqNBkuWLMFLL70En89Hv6uursY999wDoVBIP3vggQfQ3NyMwcHBiPdyu90YGRkJ+TebqK2tRV9fHwQCARiGQX9/PzZs2EAnCo1Gg+TkZHi9XigUCly8eBGlpaW4cuUKzfsTDQ6HA6VSCavVOu7AbLF2oyTlRHC8o9ms1mV3WSy3A8GR5YMDq041UzG+p/o4vb+/H2q1Om5hRCwWY8OGDVCr1VEFzeB5pLy8HMuXL0dhYSGys7PBMExI+hKNRoP77rsPALBz587EPBTLuJgz2e7/8Ic/ICkpCY8++mjI53/7t3+LpUuXQq1W49y5c/jnf/5n9Pf34+WXXwYA6PX6UdqM9PR0+p1KpRp1r//4j//AD3/4wyl6ksmzfPly2O129PT04IEHHsDChQtD4hBxuVw8/fTTePfdd7F27Vrk5+fjiy++QFVVFc27FA1i2KxQKMYtDARrhIBbXh4HDhzAww8/jKysLJrYMTU1leZtSklJmVgjTDHsLovldoHknptOu7yUlJSEjm+GYXDlyhV89NFHeOKJJ0IcOaIZEY83VUdRURHa29tRVFSUkDoDofMIsWkihBuOc7lcFBQURNS+s0wPs8bLjMPhYP/+/Xj44Ycjfj9//nzcf//9+NWvfhXzOm+88Qa+8Y1vwGazQSQSYdOmTSgoKMBvf/tbWubGjRuoqKjAjRs3IubqcbvdcLvd9O+RkRHk5OTMCi+z8XgQREtyGouxknaOp57//d//jeHhYahUKjz33HMwm81QKpUQCASssMHCMk3cDl5HdrsdL730ErKzs9Hb24vdu3eHfBfJ66yurg4ulwtisRiLFy9OSD1INOmWlhZUVVWNivofD9ESuxL7zf7+fhQVFcU9Z7PEZs54mcXL6dOn0dzcjK9//etjll21ahV8Ph86OzsB3LJDGhgYCClD/o5mdyQSiWgmZ/JvthCvKpphGFy/fh0CgQBtbW1xX99sNkMoFMJms01K3e1wODA8PAyxWIzBwUG4XC4oFAqMjIywx1AsLNPI7eAhKZVKsW3bNvT29o5KpBrteJskmB4rrtF46O/vxyeffIKuri6cOnVqzPKRDLWJPSURjII/7+npAQDcvHkzYXVmiZ85IRC9/vrrWLZsGSorK8csW1dXBy6Xi7S0NADA6tWrcerUKXi9Xlrm6NGjKC0tjXhcNtuJ17bF4XAgJSUFQ0NDMQW68AGr0Who9NRI94g3CaNEIsGjjz4KPp+Pr371q9BoNPD7/cjOzp5TE3MgEIDBYMDAwMCUJp5MBIFAAL29vTh79myIhpNl9jCd3os+nw8tLS0hNpVzFQ6Hg7y8PPzLv/zLqBxr0QS+RYsWQSKRYNGiRQmrR0dHB4RCIZxOJzwez5jlgzewwWEDeDwedTwhEK281+udk2vT7cCMCkQ2mw11dXWoq6sDcKuz1dXVobu7m5YZGRnB+++/H1E7VF1djf/+7/9GfX092tvbsWfPHvzDP/wD/uqv/op2qCeffBJCoRBf+9rXcP36dfz5z3/GL3/5S/zjP/7jtDxjool3tyeVSqFQKKiLfDTCNU5EmCSxg8KJN5K00+lEfn4+vvKVr9Agi7MxvtBYmM1meL1emlJkNmM2m9He3g61Wo3a2tqZrg5LBEjsrukwcG5vb0dWVhba29un/F7TwXjznY2V3R64ZWd09erVkA1zLFasWIHc3Fzk5eVh48aNY5YnG1iJRILW1lYcO3aMaoHC53EOh4Pc3Fykp6dPa2JdliCYGeTEiRMMgFH/nnnmGVrmt7/9LSORSJihoaFRv7906RKzatUqJjk5mRGLxUxZWRnzk5/8hHG5XCHl6uvrmbvvvpsRiUSMVqtlfvrTn46rnsPDwwwAZnh4eELPORUEAgHGZrMxDoeDOX78OGM2m5lAIBCx7MjICPP2228zer1+VBlynUi/9fl8TFdXF+Pz+ehnfr+fMRgMjN/vH1U++LtAIMBYrdaQa8e612zF7/czAwMDjF6vp88cqV1mA36/n+np6WHOnDkzagxEYi6+j7mOzWZjhoeHGZvNNuX38nq9THNzM+P1eqf8XnOV+vp6ZmhoiKmvr5/S+9hsNubdd99lzp07x7z99tuMz+eblj7AMr71e9YYVc9mZlvqDq/Xi/Pnz6Onpwd9fX1YvXo1nE4nVq9eDZlMBpvNhkOHDqGgoABLlizBu+++C71eD41Gg8cffzzE8JCJYuAHAN3d3UhNTYXRaIwZSp9AjpW4XG5ErdREk8vONsbbLomGSZCR7O3yPuYSiXp3LKOZSNuO1xNtMnUzGAw4fvw4tmzZgpGRETZ9zjRx2xlVs4TS2NgInU5Ho1BfvnwZubm59Dz68OHDUKlUaG5uRmNjI1wuF+RyOdxu9yjbhWgGfgCg1WphNBpHhdJnguwg/H4/uru7qQFjIBCIKybHXCZau0SCiZFeZaIkKsbL7fI+5hK3g4HzbCQQCKCnpwdCoXBc4yKeY7VEwOFwIJfLsWvXLoyMjCAjIwMWi2VK78kyflgNURzMRg3RpUuXcPPmTfh8Pqxbtw65ubnUPidcQ+RyufDxxx+jqqoqpBwQ2wU02m4rWLNgMBjg9/vB4/GQl5c3qcSst+PumdiMMAxDE9pOltuxnVhYJoPRaIRCocDAwABycnJm5bgg41YsFsNiscy6BNa3K+NZv1mBKA5mm0AERD/uiCbgjPd4xG63w+/3w+VyITU1NWSCCV6Qu7q64Ha7IRKJkJ+fD+BWgkJiPPjEE09AqVTGtYjbbDbYbDbI5fJZmc5jIhBtGvEomY0TNcudxVQJ1DMpqBOnh6kWMiwWC9588034fD4899xzYxo/Ey9Vt9uN7Oxs9ohsBmCPzO4AIh13MP8vo7LP54PNZoPJZKLHNOM9HmHCUm0Q/H4/enp6IBaLweFwkJOTA4lEgpycHFqmuroabW1tSEpKwr59+wDEd8wz29N5TASiKmePSVhmC4kYZz6fD83Nzejv76fhKKbSgy74mD4S0+XF+t5778FqtcLlcuGdd94Zs3xPTw8OHTqErq4u6l3GMnthBaI5CJkcwnE4HFCpVBgaGgIACIVCmmkZGO3mGQuZTAa5XE4z0xM6OzthNBpp4EsSjj5457N69WoUFRVhaGgIjzzyCIBQgSx4ciM7KJvNBo1GA6/XO2vTebCwzHbcbjfOnj2L3t7eqHGzEmE71t7eDpFIhMHBQRqOYioTyE7VZineuGqEXbt2ISkpCWKxGF/+8pfHLH/x4kVotVq0tLRMS+wplsnBHpnFwWw7Mgu2S3G5XABuBUKUyWRwOp2QSCRwOBx0ggoEAuDxeCG5xiY6adXX14PH48Hv98cMlBnriC74O4fDAYFAALfbTTUps4HggIzl5eVTbnTJcmeS6KOes2fPQq1Ww2g0oqysbMri2fh8PrS1tUGhUFCP0kSk/InGVB3HGQwGeL1eCAQCGsw3kdhsNnzyySeorKzEvHnz2COzGYA9MrvNIZOCw+FAQ0MD3nnnHXR0dMDpdEImk8FsNuP3v/89dDod3bERrQzxeCIQLyiDwQCfzzfmbqm8vByBQADl5eVj1pHsQoN3YUT+djqdkEqlNDK2TCabVd5OZrMZOp0OSUlJaGxsnOnqsNymxBvoNF6WLFmCmzdvIjk5OaHRjsM1KXw+H6WlpcjMzASXy6V5Cp1O55RoiKbKO49hGHC53CnT3hDPstLS0nEJQ2MdEbJMDaxANAdxuVz4/PPP8d5776GmpgYajQanT5+mg+f3v/89PB4P3n77bZrKYWhoCDabDX6/P0QgcjgcsFqtsNlsuHnzJvh8fszJOV431eAJzGAwoLu7G11dXdRYO97I2DOFRqOBVquF1WqNmAA4XoKPBBmGYSc6lhDGG315LEwmExYvXoykpCQMDg4m5JrA2IJbop9jvAQCAeh0Ohw7doxqzeMhNTUVfD5/1kWGvh3tKecCrEA0Bzl9+jQGBgZoPpzBwUE8/PDD9LgpOzsbDocDcrkcJ06cQF5eHo1HFO76LZVK4Xa7IRaLIRQK4fF4xszgTBZ1YkA51uKu1+shlUoxODgIh8MR0Vh7tsHlcpGRkYHKyspJHZeFJ8tlJzqWYBJtDKzVahEIBKj2NVEoFArU1NSAz+dHHO8znZrHbDbj2rVrEAqFOHv2bNy/i6feJpMJv/rVr9DU1DRt+QzZGGEzA3+mK8AyftauXQuHwwGz2YwNGzagpKQkRB37yCOP4MCBAxAIBLjvvvtQV1eH8vJyZGRkjBpkJH+O2WxGenp63IljxWIxzGYz1Go1HA5HiJBlt9tx6NAhBAIBbN++HWVlZbhw4QKys7OhVqtpgMjbZbDHsgPRaDQwmUwhyXKJG/5UGaCy3LmQeGCJprm5GYWFhejq6oJQKJw1tn4EjUaDlJQUGI1GzJs3L6HX3rNnD0QiET799FNoNJpp0SYRDTvL9MIaVcfBbDOqjpd4DRHHGzCMlJdIJNQWKPj6e/fuxY0bNxAIBFBRUYF169bB5/MhEAhAKBTOOvX0ZDEajbR/xPNsbMoMlrkGSXGRl5cHhUIRdT5xuVyorq7G6tWrx9Q0Jxq/3w+dTpfwlBiffvoprl27hoKCAjz66KPj0oI5HA4cP34clZWV0Gq1bCDGGYA1qmahMYmEQmFIPKJIEGFIp9PFZeBJdi9cLjeioSOXy6U5zYhdAZfLhUAgmDEbg6kklv1EJLfeaDGkWNsiltkKsR1MTk6Oubk6d+4c8vPzce7cuYTdOzg9UCwihQCZ6LWC2bBhA6qqqrB9+/ZxCzQfffQRGhsbcfz4cRiNRnaMz3JYgeg2hcQk6u/vh0qlGmWzErwAkwVaq9XGNIyMN2bHvffei/LycqSmpmLbtm002WtaWtptuUOKZYcQyRg1ksfM7WpbxAp6dxaLFi3CtWvXsGjRooRdU6fTITU1FTqdbkauJRKJsGbNGohEonHfz2w2U/tJDodzW47x24nbb3ViAfAXY+ns7Gy43e5R9jrBCzBZoHk8XkwDw3hchP1+P2w2G1asWIEnnnhiQgKQz+dDS0sLfD7fuH8724jX++Z2NKIkWkqRSMQuAjFIpNDocDiwf/9+7N27d0baPBAIYNmyZQk1Po6UTJlszqxW67jabTyJmRPBk08+CYFAgMceewwpKSkJHePsZiPxsALRHIUMBpfLhbNnz8Ltdod8T4Qch8OBTz75BEajMWTgjLUARxpssRZ3Ur63txdpaWngcDiwWq0TOiJrb29HVlYW2tvbx/3byUJU6iMjIwmZaOL1vrkds6ATLeXg4OCsEPQmu4AEB+tM5ILvcDhCQlFMhkOHDqGpqQl6vR6ff/55AmoXneAYZqQ99Ho9XnvtNTQ1NdEQH5MdR5GOwsxmMzweD/XcnMy1Ek1wP1OpVHj++eehVCoTPsZvV63yTMIKRHMQn8+HS5cu4fjx4/jFL34Bn8+HmpoaALcm7fb2drz11lvo6+vD/v37odPp8Nlnn9Gkr/Gk8og02GIt7na7HVarFUqlEkajEWq1GlqtdlwxQQiFhYXo6+tDYWHhuH87WXQ6HaRSKSwWy7RONLfjbo9oKVNSUmaFoDfZBcRsNsPr9VKvwkQRHDw1/HMSSDX8O5/Phxs3buDatWu4ceNGiDaVbIQmEz8rHhwOB2w2G4RCIW2Pffv2ITs7G8ePH4dOp5uyBVssFoPPv+UkPdXC9uDgIH73u9+ht7c3rvEZ3s+mSnC5HbXKMw0rEM1B2tvbYTAYcP36dSgUCly6dAnJyckAbk3aX3zxBfLy8vDpp5/C5XJBJBLB5XKFeJONNTjDB5vf70dXVxdVUYf/7XA4YLfb8bvf/Y664U90sPL5fJSUlNAJbzogKvjMzEw4HA6o1eppnWhux93ebNN6TXYB0Wg0EAgE4HK5CXUOCHZQCIZsYCJpj9ra2mCz2dDb2wufz0e1qQ8++CBycnKwffv2Kd9QSKVSyOVyeDwe2h5PPfUUent7sWXLFrohmopxJJfLIZfLqTZ6KnnzzTeh0+nwpz/9CVardczyIpEITU1NaG1thdfrnTLBZbaNr9sBViCagxQUFEAqlaK0tBQAsH79erobVKvVWLVqFTo7O7FlyxY89NBDUKvV2LRpE6RSaVyDM5K7vk6ng1wuh9lshsPhQG9vL7hcLnp6euBwOJCSkoL/+Z//gc1mw3vvvQeLxTKnBiuxjxoaGkJubm5M1+KpgN3tTT2TXUBIVPX09PSEOgdEqxcZfyQPYTCZmZmQSqU00jIRfgQCATIzM0PS5EwGu92OAwcOREwmzeFwqFBC2kMqlSI9PZ16lk7VHDCdwoDNZgNwK3EuSZwdi4aGBhw4cAC1tbVoaGhgBZc5BCsQzUE8Hg/WrFmDZcuW4Zvf/CaWLFkCPp+PQCCA3t5eaLVaPPbYY8jKykJ6ejq2bduG7OxsmmdorMEZyaZBq9XSjPRSqRRqtRpWqxUqlWpUPqBAIICGhgZ4PJ4pb4vJEGy8PdOpB9hJ8/Y8NgQi29oQYnluEoEjUr9ISkpCQUEBli5divLycqpNra+vB5/Px+Dg4IRs8MLfwdGjR1FZWYmjR4/G9fuPPvoI5eXlOHr06KzUdo43uz0APP/885BKpdi6dWtcxtgnT56EVCpFf39/XBolltkDKxDNQaRSKTweD3JyckJ2qiaTCTweDzdv3qQeGHa7HRKJZFzah0g2DTweDzk5OfToTS6XU00KUfU//fTTAG4JT83Nzaivr0/gUyeeYONtLpeLlJQUOJ3O225BnitM5bGh1+vF1atXMTQ0lPD3Gy5EhP8dydaGYDKZ4PF4YDKZxnXPaAJ0ZWUlfD4fVCrVhI7Mgt+BzWZDVVUV6urqcP/99wMYW6B47LHH0NDQgC1btsBms03aAH0icYNiYTKZIBAIxtXemZmZ+N73vocVK1bEZYz91FNPIRAIYPHixbjrrrsmU12WaYaNVB0HszFSdaSorDabDd3d3WAYBiKRiKqsZTIZ5HJ53NeOdGTm9/tx7do1qNVqiEQipKWlRfzt0NAQ3n//fRQXF6OqqgpCoTDk++A0FxwOh6axGCua9lRAbC8KCwvB5/PZCNJTxHgjpk9FX6irq6MhIZYuXYqkpKSEXTu834T/TQQkcrQcvIkhAgOJ1TVZJtKGZC5RKpWQy+V0U2A0GrF//37s3LkTubm5AGJHZQ8EArh58yZyc3PR0dEBu90Ot9uNefPmTfjZuru7kZqaCqPRSOtA7hUtXU4sbDYbbDYbtUFiuf1hI1XfAeh0Omg0Gly/fp0aNstkMuTm5kKj0SApKQkymQwSiWTci0uk3SeZMMeKeq1UKvH8889j3bp1o4QhhmHQ3d2NoaEhGI3GmEaj00G48TZrxzM1OBwOiESiMfvOVB4bJicnY2RkBEqlckKej7EI7zfhf0eytSEkOtu6yWTCZ599hp6enrg1YcGelU6nk26iDhw4gGXLlmH//v20bLSjZYZh0NvbC41Gg66uLioQkuO7iRItbpDRaIROp8PBgwfH9T5lMhmdG6PBMAysVuu4j9ZY5j6sQDQHGR4exsGDB/H73/8eN27cwLlz5zA8PAwOhwO/34+9e/fi17/+Ndxud4hBZiQbjXhV0lqtFgzDICsrCxqNJupkEcsOhNgmcTickBxokYxGIzGR8//xwNrxTA0kUm+kiOnTRW5uLgoLC6nBbyIJ7zfj6UeJzhJ/5swZ5Ofno7a2Nq62ZhgGPp8Pb7zxBj1eJ2zevBknTpzAypUr6fwQrb4OhwMKhQJWqxVZWVnIzc2FVquFQqGYVLLVaHGDiI1USkoKqqur475ePO/GYrFg//79+OMf/4i2trYJ151l7sEKRHOQffv2IRAIYHBwEJ2dnRgZGUFLSwsA4MCBA/R8/I033kBraysNMhjJRiPeUPY8Hg/5+flIT0+HxWKJeg4ffo/wFCEajQZ6vR41NTVwOBxRjUYjEU+k7Mlwuxr1zjQcDgcpKSkRI6ZPFyQLfEZGRkI9xGZbn9m0aRO6u7uxdu3auNra4XDg448/xtKlS3H06FE4nU76+dWrV1FVVYX29vYx5wepVAqBQICUlBTI5XLweDwUFBSgrKxsSsJnzJs3DyUlJXA4HFi9enVCr11bWwu9Xg+RSIQjR44k9NossxtWIJqDrFu3DoFAAHK5HAUFBVAoFFi8eDEA4OGHH4ZarQYA3HXXXUhOTkZHRwe1Kwg/Eoo3lD3RJPl8PjAMg76+vojZrMPvYbfbEQgEqK3Q8PAwGhoacP36dfz5z38e13NPpScYCWhZXV09KRX/7c5EBYDbUfs2G1OTyGQyPPzww9BoNDCZTNT9Pto7k0gkuO+++1BbW4uHHnqIjlsulwuXy4Vz585RbU8syPuVy+XT8o75fD4WLFiA++67L+I8RJiIVvmee+5BQUEBAoEAnnjiiXHXTa/X42c/+xn279+f8OPZ2U6svjbbNg+RmL7IdywJIy8vD+vWrUN6ejr6+/sxb948aq+TnJyM73znO3A4HPB6vejs7KRxi8ikFQxRSY8F0SS1t7dDoVBApVLB5XJBLpfDZDJBKpVCLBajsbERX3zxBb785S9Do9HQRSMlJQXArThJJpMJfD4fAwMD43puoq5PJMQ4MxAIoK2tDampqairq8OGDRsSep/bBXLsOZPG8LOF4NQkpH/PFvR6PW7cuAG3242lS5eivr4eK1asGLWZMBgMOH78OMrKyuByuei7PH/+PDVkvnr1KtauXQun04nTp09j7dq1IUdr0XC5XKiursbq1atjCi1Ticlkgs/ng8lkiuoIEo5EIsFjjz024Xu+8cYb8Hg8qK+vh0KhwMaNGyd8rblG8AlBpECj0b6bLbAaojkIl8tFRUUFXC4XioqKRnUuIvgkJyejuLg4ZpDBeKV2okkqLCykKvGUlBQaqPGtt97C5cuXceTIEZhMJuzZswcA4HQ6oVQqYTabYbfbIZPJ8NRTT0EoFOLZZ59NSHtMBnIMx+FwUFFRgcHBQVRVVc10tWYtJCTD7RhZO16ItlQkEs2q1CTBtLW1wWq1gsvl4pNPPkFxcTFqa2tHlTt8+DBUKhVaW1tDNKOrV69GSUkJbDYbHn/8cdjtdpw6dQplZWX47LPP4tK4VFdXo6ysLMTGJ5rNYqw0JcFlxqthCA6FMF2QDaZEIolbCLtdiOWYMhecVliBaI5CbCKSkpJi5iMby4Mr3oWNaJL4fH6Ix4xGo8GhQ4ewbNkyXLlyBVarFSKRCBaLBQCQkpJCAzj6/X44nU4UFhbie9/7HjIzMyfeAAmCHMOlpKRAq9Viw4YNUXezwRPyXFD/TgUkzUSiM3fPJYi2tK+vb9YeA5aWlqKlpQUXLlzA3Xffjba2Ntxzzz2jyt13333o6OhAfn4+CgoK6OcCgQDl5eUoKSkBh8OBWCzGokWLcPDgQSxevBhms3nMMbB48WJ8/PHHKCkpoWW6urrQ09ODrq6ukLLhHqfhR10ul4sGhxyPEJ6amgqBQJBwzXIsHn30USxduhRbtmyZ8nxys41YR+Nz4dicFYhuY2IljQw2dB5rYYt1Ds/lcvHkk0/i2rVr2LVrF5555hnweDw888wz9PucnBzweLy4vcmmk/F4+QQLj3eqhoRMalOZlmG2E6/d3URIlKD96aefwul0Qi6X49ChQ9i0aVPEY64zZ85g27ZtsNlsIbGZdDodmpqakJKSgtraWrhcLrS1tWHLli1oamqifT/WGKipqcG6devQ0NBAyxgMBiiVShgMhpCy4R6n4Q4U1dXVWLhwIc6ePTuuOSTRXnzA2O9IKpXioYcewqJFi6Y1HyPL5GEFotuYaMJO8EQWj9Q+lneXSqXC888/D5VKBZlMhhdeeCHkGC9WCoLZQjwLUXB7zgX1L8vUEM0VPBEkQtAmyVYFAgFsNhu+/OUvRyzHMAzuuusuHDt2DCtXrgzZPCmVSuj1ejQ0NKCqqgoymQzLli1Dc3MzVqxYQcc3CZ8RibVr16KjowMrVqygZZYuXQqHw4GlS5eGlA2fI8IdKFavXo2mpibcc889Uz6HOJ1O7Nu3D6+++mpEB4u5shmaCS32VIdGmWpYgWiOQoKHDQwMYGBgADabLaTjMwyDnp4eHDt2DK2trSEdNN4Er2QwjeXdFVw2JSUFXq933EamMz2Qgie5aJNIsPBIwhjcaUdmLFNLIgTturo6DA0Nwev1Ijc3Fy6Xi4bpOHPmDNxuN4Bbfb6hoQGbNm3CtWvXcP36dYyMjAC4lcOMaHI6OjoA3IryvH79egQCATidTojFYnA4nKgCikgkwuLFi6lnKgAIhUKsWLFiVNDWcMI1O2KxGOvXr58W4+zjx4/j6tWrMBgMePfdd0d9H+kdtbe3Y/fu3Th37lzC0oxMhpnymp3q0ChTDSsQzUE8Hg9OnDiB/fv3Y9++fejt7aWpAdxuN06fPo3r16/j1KlTMJvNOH/+fEgHjTfBKxEQYqmdSYh/cvYfXDZeIYdhGHR0dODGjRvQ6/UA/pJ7yuv1TrCVxgeZ5BiGiWv3N9cHfjA+nw/Nzc00XhXLzDFROwu73Y79+/ejsbERRUVF6O7uhlqthtFohNPphNlsxrVr15Cbm0uNq6VSKVasWIFLly4hPT0dOTk51LZn4cKFuHnzJubNm4fW1lYAoWEv4hHczGYzfD4f9eScK/h8Pvr/8drC/PGPfwQAHDlyBL29vVNfyTEwm81oa2uDRqNBXV3dtN13ppNkTxZWIJqD1NfXQ6/Xo6OjA16vF9euXQNwa4Krra1FcnIyFVJEIhF4PN64d1ZkwhOJROjq6oLVaoXP50NXVxf0ej0VcojgFCkparxCg8PhQG9vL1QqFdra2sAwDC5cuICamhpcuHBhWhZpMsnJZLK4duhzfeAH097eDqFQiPb29ll/DMASmSNHjiApKQlXrlzByMgIHnjgAQwNDUEgECA5ORkajQYLFixAV1cXysrKYLfbAdyy6dmxYwdsNhsuXLiAoqIiAEBOTg4WLFgAp9OJBx54YNT94hHcNBoN+Hw+db6YCTweD2pqauDxeOL+zaZNm7BkyRIUFBTgkUceGaV9jwSxvxIKhTMWYiAYjUYzI16zU2GzNZ3MzVrf4SxatAgAkJ6eDg6Hg3vuuYe6d5aXl2NoaAg5OTnYuXMncnJysHnzZnC53LgEC3L8BdzyKOrr66Oxhtrb28HhcGAymegxnFQqBZfLpef/wcQrNEilUixatAhWqxUrV66Ew+FAe3s71Go12trapnSRDgQC6Ovro0cJ8e7Q5/rAB/7yrtPS0miyTlZDNLvx+XxoaWkJ0WIAwLJly2A0GpGfnw+GYdDY2AiVSgWNRoPq6mpwuVyoVCosWLAAV65cgdPphMPhQFlZGerq6iCRSDBv3jzcuHEDAHDt2jVUVlZSLzPg1gaHz+ejvb0dx48fHzPoIJfLRVpaGtLT02dsnNTX16OoqAj19fVx/0YikWDHjh340pe+RLXcNpst5m++9rWvIScnB48//vi0erRFg8vlIisrC6tWrYJIJJrp6swZ2Gz3cTDbst2TLNKtra3Iy8ujgpHVaoXBYIDNZkNFRQX1cBhPFvfwsn6/H729vVCr1ZBIJGhra4PP50NmZib8fj8d/A6HA5999hl4PB7uv//+uAK3RYNhGBowbsOGDUhLS0u4ISVR49vtdnR1dSE9PR1msxlr1qyhdZiqzOtTyXjqHfyuiUfiXHveuUK07OxEKHU4HEhJSRlTcGhpaUFWVhb6+vpQUlJCr2Gz2WCxWKBSqWAymdDQ0IArV65Ao9Hg2WefpRqMM2fOICcnB21tbVi/fj2dN/R6PfR6PVatWgWhUAiHw4Hjx4+jpKQERUVF4PF4sFqtMJvNuHHjBsrKytDZ2Yn169fTupEYQ06nE9nZ2TFDgoy3zYhNTEFBATweT9z9lARIrKysHNNuKRy73Q6DwQChUAihUDgrBJ3xMJ55Pxakf7lcrpD+6/f70d7ejhs3bmDp0qXQarWzcoM4Z7Ldnzp1Ctu3b0dWVhY4HA4OHDgQ8v2zzz5LjfbIv82bN4eUsVgseOqpp6BQKKBUKvG1r31tlDRPIq2KxWLk5OTg5z//+VQ/2pQilUohFAqxYMECKgwBt9picHAQfD4fly9fpqre8Rhqhpfl8XjIycmBy+UCl8tFSUkJcnNz4ff7qeaHYRgcOXIEHo8Hdrsdp0+fntTzcTgcpKen48tf/nLI8yUCsgCR3a5YLIZWq4XJZMLy5ctpubniSRJOrNhT5Nkj9YvZ7gU414l2fEwWXZJdfSwKCwvR19eHwsJC+P1+dHV1wWAwIBAI0MUPAG7evAng1pzgcDhgMBhoVvtPPvkESqUSfr8fDMOAy+UiMzMTd999NxUabt68Ca/XC6vVSmOK8fl8NDc3Y/Hixejo6BiVQ6y3txdmsxkejwc6nW7CY4cIVgaDgbZZfX09GhsbUVNTM65UKfEYcZMo9a+//jqGhobo51KplMYxmotH44nyhCVhRvh8fkj/1el0uH79OhQKBS5fvjyn7MSiMaMCkd1uR2VlJV555ZWoZTZv3oz+/n7675133gn5/qmnnsL169dx9OhRHDx4EKdOncILL7xAvx8ZGcGmTZuQl5eHS5cu4aWXXsLu3bvx2muvTdlzTQeR4gtlZ2dDo9HAbrejoKAARqMxbtf64OuGlx0YGIDRaMTAwAA4HA6SkpJCjoscDgc2btxIY9OsXbs2sQ+bQIigIxaL4fP5IJPJUFRUhLvvvjtEtSyRSGCxWCal6ZoJosWeAv4iLBEt0lwIlDbX8fl8aGpqwsjICCwWy6iFlbQ9eW9jwefzUVJSAj6fD51OB7lcDofDgaGhISoUpaamUo2ASqXC8ePH4fV60dPTg9bWVmRkZKC3t5fajEXyFjOZTMjMzERfXx+tc3V1NUpKStDU1BQxgKlarYZSqQTDMNBqtRNeiEngS7fbjeHhYYjFYvT390MikcBsNmNwcDCh4S6MRiP27duHnp4eGmEf+EsoABKElhAt2vZsI1Hjm4QZ8fl8If1Xq9WioqICIyMjWLp06ZwUGsOZ0ahRW7ZswZYtW2KWEYlEyMjIiPhdY2MjDh8+jJqaGrq7/9WvfoUHH3wQ//mf/4msrCzs2bMHHo8Hb7zxBoRCISoqKlBXV4eXX345RHCaS1itVrS0tMDhcMDlcqGiogKZmZnUHubatWvg8/koLCxMiE3I8PAwOBwOhoaGoFAoRqmrpVIpHA4Hdu7cOesXV1JXuVweEoguHKfTCbVaDafTOWN5dyZybCeTyehvwiHPTrRDLLFJxLFpe3s7GIaB0+mEQCAYdaQglUqRlpZGj8zGg1arRW9vL1JSUiCTyahjA4kyLZPJwOfzsW3bNtjtdvD5fKSlpaGjowOLFy9GYWEhOBwOdDodDTJJnnn16tU4f/487r//flrnyspKXLx4ESUlJThx4sSoHGVyuRxcLhdFRUUxUwWN1aZarRY6nQ7Z2dng8XhgGAarV69GbW0tqqqqoFarEzrPOJ1OeL1e8Pn8uDRPRGDT6XQ0TcdkjuZmO2QTHD5f8ng8FBcXo7i4eIZqlnhm34FfGCdPnkRaWhpKS0vxzW9+M0QtV11dDaVSGXLUcd9994HL5eLChQu0zD333BPSSR944AE0NzdHjc/gdrsxMjIS8m820d7ejpMnT6K6uhp2ux1tbW0wm81obGzE6dOnYTKZ8PHHHycsiV5WVhaAWzvASMdI0XYiU+U6Hysf0liByOLdNc2GwIsTObaLJ3T+dGUkn+vEk/pmLIjQIRaLIRQKMTAwEBKGIpoWIh6C0/eQ+kokEtTW1iI/Px+lpaV46KGHkJSUBLfbDalUips3b8Jms8Hn84HP58NisSA9PR06nY4KK2KxGIFAgMb9IYbFAoEAS5cuRUtLC/Ly8kJylJFnidT3gsdlPH06UuBLsViM+++/HyKRKGooj4lqbnJycrBt2zaoVCo899xzY5aPFKn8T3/6Ew4dOoQ//elP47o3y+xiVgtEmzdvxh//+EccO3YMP/vZz/DFF19gy5YttMPr9fpRyfP4fD7UajWNZ6PX65Genh5ShvxNyoTzH//xH0hOTqb/cnJyEv1ok6KhoQESiQQ+nw/9/f3IyMiARqNBWVkZhEIhvF4vJBIJLl68iK6urkmrdpOSkpCbm4vU1NS4hYRAIIDq6mr09/fTsACJipyq0+mQlJSEjz/+OMTTJZF2P7PhOGk2CGW3CxNNDBrt+DHe+xAbPIVCAb/fTw2FEx2IlNgo9fb2IjU1FZ9++ilEIhFsNhsCgQBN4Do8PIykpCRcv34dwC1P0IGBAWRmZlLNTXifI9d2u92Qy+VYuXIlOjs7UVlZGVfbBI/LifRp8nuz2QybzQahUBiyMSZtTrRlJHRHvPB4PCxatAjf+ta34jr2iSSw9fT0hPyXZW4yqwWiJ554Ag899BAWLlyIhx9+GAcPHkRNTQ1Onjw5pff953/+ZwwPD9N/s62Tb9q0CQKBAAqFAhs2bIBSqQSXy4VAIMBzzz2HtLQ05OTkID09HSMjI9DpdBO6TyAQgF6vR1NTE1wuFxoaGjA8PBxxsgkEAtTDjWEYalzJ4XAwPDwMYOICS/gio9Vq8cUXX2DFihUhu9TZIEAkMlz+bBDKbhcm0veCc7ZN5D7EYNrv90MikdAjM41GA7PZDIVCgd7e3lF9ZSKaDhLiQiQS4erVq8jKysKRI0dw+vRp6PV6cLlc3HXXXVAoFOjq6sKGDRsA/CXXYLDnVvDRavC11Wo1gFua4lWrViE5OTmu9gwelxPp0+T3Go0GcrmcpiYhkDZXq9Xo7e1Ffn7+tDtDbNq0KeS/s5U7NSl1vMxqgSicwsJCpKSkUA+KjIyMUUkCfT4fLBYLtTvKyMjAwMBASBnydzTbJJFIBIVCEfJvNpGWlobi4mJs3boVnZ2dIYa/SqUSf/3Xf40NGzbQuk80CaXZbIbZbAaXy0VNTQ34fD4GBgYiehMMDAygtrYWb7zxBvr7+6HRaDB//nwEAgGsWrUKwMQFlvDFjMfjYfPmzWhpaQnxdJkNAsRc9U4bLzOdaiUa0Sb8ifS98fYnhmHoZiAQCFADdqfTCQ6HExKTJ1wzE0xvby893opn4QoEAjCZTBAIBBgaGkJFRQVu3rwJhUIBm82GtrY2yGQydHd3w+PxYOnSpThy5EjU5wzvwyTmlsvlop+Px+lgPO3odrtx5swZ3Lx5kwqEwQmFIx0vkncrl8tRXFwMr9c76U3ReAWHqqoq7N69e1qDIE4Eh8NB7Z2CA+yy3GJOCUTErTMzMxPArYR/Q0NDuHTpEi1z/PjxkEV49erVOHXqVIgdy9GjR1FaWgqVSjW9D5AgOBwOqqqq0N/fj1WrVkEul4/6Xi6Xo7y8HPn5+VS1O95BrtFoaAyQFStWwOfzIT09PaJaua2tDRcvXoTFYsGHH34ILpcLkUiEDRs20DAIExVYIi1mk81tFAgE0NnZiXfeeQdWq3VC14jEbNBSEaZyNzhbU5dEE0inI/ec3W7HyMgIzGYzGIYBj8ejNlvhWqZwzUwwarUaJpMJ6enpVMsUbntEBK9PP/0UV65cAY/HQ3NzM9XibN26FWq1GlqtFitXrgQAlJSUoLKyEl1dXXj00UejPke0Phz8ebDTwViMpx9evHgRANDV1RW3Zjt4XpnIHMMwDIaHh0PsHW/XjQ3DMOjq6qJHkKSvslqjW8yoQGSz2VBXV0dzrXR0dKCurg7d3d2w2Wz4p3/6J5w/fx6dnZ04duwYduzYgXnz5tFQ8mVlZdi8eTOef/55XLx4EWfPnsW3v/1tPPHEE9QQ+Mknn4RQKMTXvvY1XL9+HX/+85/xy1/+Ev/4j/84U4+dEEQiEdasWQOVSjVq8EcbzOMd5FwuFxkZGSgrK4NGo0FlZSWysrIiGn8ajUb6OQkImaj0FonU/BBDb71ej7Nnz6K4uBiHDh2K+ZvxaENmg5aKMJWT+mxNXRJtMZ8OAY4cD8tkMgwNDUEmk4FhGOzbtw+XL18eFV06vK+QhYnD4UCr1UIgEMDhcMDn88Hv98NoNKK7uxs+nw96vR6fffYZ5HI5+vr6oNPpUFpaisHBQRpZvrCwkB6NnThxAj09PTCZTCgtLY2pHYjWh4M/H4/gT/qhzWaLehRInr2goAAOhwPJyckT1myPF6vVioMHD2Lfvn3UHGOs5wuP6TVXkMlk1PuYbHhvV+FvIsxopOqTJ0+GRDolPPPMM/j1r3+Nhx9+GFeuXMHQ0BCysrKwadMm/PjHPw4xkrZYLPj2t7+Njz/+GFwuF1/60pfwf/7P/wnRmly9ehXf+ta3UFNTg5SUFHznO9/B97///bjrOdsiVY9FNNfWqYy+7HA48Omnn8Jms+HRRx+N6dI+k1y9ehV5eXno6uqCQqFAdXU1tm3bFrO+RqORvv+5FK12rkbbngx+v5+6kQcbvUaLFJ1IAoEABgYG0NPTg8WLF4PP5+P3v/89RCIRrFYrNm7ciLKysqi/J5GgVSoV+Hw+jRRPbBj9fj/9R97n5cuXsXLlShQUFNDnvXDhArxeLzgcDnJyctDT0wOpVIrLly9TIS0jIwMLFiyg/Xkq+wq5tslkQlpaGgwGA6RSaci7IFGVicaCaNamg6amppCs9rt37x7zNwaDAR988AEGBgbw1FNPzWnX89t9nhjP+s2m7oiDuSYQzRSJChU/lXi9XjQ2NqKsrAwCgWDU95Emh+lYTFkSQ2dnJ0ZGRqBQKJCfn5+QawZrbmItGjabDb29vdBoNHA6nZBIJGhpacH58+eRnZ0NlUoFuVyO5cuX0zAgpL/5fD40NzdTDUlubi44HA5sNhsMBgOGh4chFAppsmaNRoP+/n4UFRVRjSzB4/Ggrq4OarUaBQUF8Pl8qKmpgUqlQkNDA9rb2/HII4+gtLSU5jg0mUxQqVRwu91TNnaJsCoUCqFWq0M2GKQdJBIJnE5nxHYmYzcvLw8KhWLU99GE4bEYGRnBa6+9BpvNhszMTHzjG98Y8zfvv/8+9dQTCoX4l3/5l7jvNxe4nYSkOZO6g2VikNwyAwMD6O/vp6H7yedkEj1w4ABN1Dre68d7phxcNhH2MyQdwWQN/qI9g0AgwKJFiyIKQ0DkY6bbIZHrnYLT6YRIJBpl2zIZI3Bie0RiEtlsNuzdu3dUiiCv1wu9Xk9j1Gg0GnpsRbQ+AEISjdpsNhiNRrS1tSE3NxednZ3Iycmhi5Ddboder6fRpIVCIXJzc6FQKJCTk4PPPvts1FGHUCjEypUrMW/ePPB4PIhEItx9991IS0vD0NAQ1q5dizNnztAksG+99RZ4PF7UCNDREsoGE2m8EY0ZmZ+IuzpJJhx83BpsOB3pGNFms+HGjRvgcrlobGyMeLzT09OD4eFhfPTRR2Mmng0mKSkJzz77LJYvX46vfOUrY5ZnGAbZ2dn072jOOXOZO/UYjZ3h5yBDQ0M4ceIEvvjiC+zduxeDg4Mwm83Ug+DKlSv47LPP6Nn4eJWA4xkM5J61tbX44IMP0NPTg0AgAJ/Ph4aGBhw8eBD19fVRJ9Nwd32dTgcOhwOHwzEq1ojVao07rtJEB/RsMopOJHeK4WRxcTE4HM6oIwyTyQSPxwOTyTTua5JdMo/Hg1QqxeHDh7F48WIcPnw4pFxzczMCgQBu3LiBQCBAs72r1WqUlZWBw+HA5XJh0aJF9DculwvJyclQqVQYGhpCaWlpyI5cJpMhPz8fHo+H5hEk2o8TJ05g4cKFOHHiRMR6E0FmaGgIw8PDMJvNWLduHW7cuIGdO3dCp9Ph7Nmz4PF4ePvttyPaIwK3AsFmZWWhvb191HfBmzOSk43Q19eHU6dOoaOjIyRP23g2GMHpZuRyOaxWK1JSUiL2Y4Zh0NzcjJycnFFBI2PB4XCQkpKCbdu2xW0TtXTpUmzfvh0rV67El7/85bjvNVe4XefBsWAFojnItWvX6I5JKpXi9OnT0Gg01FW3qKgIbrcbwK2cXBMRCkhCSL1eD4PBgEOHDkVNGNrV1YXGxkYEAgFcvXoVOp0O7e3t6OzspDmUIk2mwC1jV6FQCJvNBofDAa1WS7VNGo0mZIfY2tqKwcHBEJuKzs7OEA8cImAFAgGqeo9ENI3BbDKKTiREcxiu1YiHuSRMBef6CoZoHicSpJR4bZJ+sXnzZtTV1Y1KNM3n82E0GpGeno7GxkYAt4KIcrlcDAwMID09HeXl5XRzwDAMRCIRDAYDcnJykJOTQ1OvEGQyGZKTk7Fo0aJRz7R+/Xo0NDRQO8zw99Te3o7k5GR0dXWho6MDarUaXC4X3/jGN6BUKmnEcqPRiLKyMvT19UV8/uCEsuEQ7ZnFYoHb7UZnZycdU9euXUNKSgo6Ozvj8kYj1/v4449RV1cHn88HqVQKHo8HDoeD/Px8FBUVISMjI+KxXm5uLhYtWoSRkZFRiWejMZGYTySv19KlS/Hggw9OSb7DmQ5rcbvOg2PBCkRzkMLCQnR2diI9PR09PT3Yvn073W2VlpbCaDTi8ccfR1ZWFpYtWzbuAcvhcDA4OAiBQACTyYQvvvgCRUVFOH78+KiFkXgtFBYWIhAIID8/H1qtFoWFhcjPz4dAIEBOTs6oyZTsXhUKBfXMIZNfXl4eMjIywOVyqabHYrGAx+PB5/PR++t0OvB4PJhMJphMJjAMg97eXggEAmrzEW1AT7fb+Hgm3qkQQFwuF4RC4biOEgi3g/rcYrGAz+ejv79/VLuS9g4EAhHbPfx9yGQybNmyZdSiXFlZidLSUshkMmo8nZWVRb2mLBYLmpqa6HGtw+GA0+lEamoq2tra4Pf7aT4yci+yMBIhPxipVIqtW7dSoT/8PRUWFmJ4eBh5eXkoKCjA4OAgHYcOhwPd3d247777kJubi9zc3IheXT6fD21tbcjIyIhol0O0Z2q1Gi6XC1lZWXRM3XvvvXC73Vi2bFnc0f6PHj0Kh8OBpqYmtLW1haSb4fF4SEtLC0k9E7xh4nK5KCkpiZh4Nhi73Y49e/bgF7/4Ba5fv07zkoUTbRxOh7AwW8JazKXNUCJgjarjYLYZVQcCATQ1NeHcuXPYuHEj8vPzqctvX18fVCoV0tPT6QQ5ESNn4t0iFotpYMZVq1ZBpVKFXI9hGAwNDeHatWt09zaWKtzn8+GLL76ASqUCj8fDggUL6DXDDZiD8x+ZTCY4nU7k5OSAx+PB7/ejtbWVBqAkNhYkQ3e0vErBQtZ0GUp3d3cjNTUVRqORJoSMxlQYp8cyDA+O0ROrzeaygWVnZyeMRiPUavUoDQNpb4vFQhd24o1FIk6rVCoMDg5CKBSiqakJOTk5SE5ODvGE8ng8uHz5MlJTU0OOhCQSCa5duwar1QqpVAo+n49FixbRxYYkC/V6vXC73cjKyqLGzZ2dnbBarZBIJEhOTo7o5UgC7S1atAg+nw8SiQR2u51Gd47Uv4kmldgsKRQKyGSyUX2kpaUFSqUSZrOZ2jaRfhDcL4BbWshY9yT3jdQPiUG0TqdDR0cHpFIpDS4bC2KDpVarwefzIZVKYbFYUFtbi3vuuSfiZnD//v24evUqgFuBbB955JGIhtgz6SQyWxw55oKjzFiwXmYJZrYJREDkRaqlpQU8Hg8dHR0oKipCbm5uSMj8id6DLJQAQu7pdrtx7tw5+P1+FBQU4OrVq1i9enVIJNlIA/v69eu4cOECOBwOli5dinnz5tFrEhf3oaEhcDicMSeE8El5LA8xm80Gk8mEK1euYO3atdBoNNOyyEfygElUeITJCix2u526co83VcVcwe/3o7e3F2q1elRyW2IHQzzDyPekXYBbdntKpRI3btygRtvz5s0LyaV4/vx59Pf3QywWQ6PRoK+vD16vF+vXr4dCoaC2RQsXLgwx6id91Ofzwel0QiaT0et2dHTAbDZDJpNh/vz5IZsEMi5ramqgVCphMBiwZs0aGsyR2N2kp6fTe4jFYsjlcmqbs3//fuTl5aG0tBR8Pn9UeAmiIcrMzASPxwtZHCeyWEYLYUE2DHq9HlarFbm5uUhOTo7ZnxmGQXd3N5KSkuhvHA4HTp8+jXnz5qG9vT1iKo3m5mZ89NFH8Hg8WLx4MbZu3Rr1+tHGlcfjwfnz59Hd3Y0dO3bM2jAjk+V22AyxXma3OWRnGa7KLCwsRFdXF9LT02GxWOhEOtGO7HA4qA2QzWYbpSqura1FRUUFgFsJZxcsWECjnxKI6tdkMtH69vX1QS6Xg8vlIhAIhFyTBPxjGCYulfFYUWrDVc8OhwOXLl2CVqtFdXX1tB0DRUoIGe0oarwq+XiyspOjF6vVGjGtBbHTuB2NKIMn9fB2Jd+RIzWXy0W/J+3C5XKRnZ1Nj3MDgQAyMzORkpISch+Ss8zhcKCrqwtGoxEulwvV1dXwer1YuHAhSktLR3k4Bgc6FAqFNNUHqbNarUZKSgrdGJD3bbfb4XA4kJ2dDb1ej7y8PDgcjpDjJPL/ZrMZfD4fDoeDXvfIkSPIzMxEe3s7BgYGIgbb5PP5KC0thUKhGGVoOxHDW6VSiaamJiiVypDPSQb5nJwcZGdn49KlSxgcHIyZ581qtcJms2F4eDhEe7VixQq0tbVh7dq1Eeswb948fOlLX8LatWtx3333Ra1rtHHIMAxOnjyJ48eP4+bNm3jvvffifv65xp1mS8QKRHOQoaEhnDlzBn6/n6rig8PPd3V1IS0tbdKLm1QqxcDAAFJTU2GxWEZ9v3z5crS2tmLt2rXYtm0b/H4/jX5KIJOsVCqli//atWuhVCpRXFxMBSoC8/9yP5E4JZONhEzur1Qq0dXVBZFIhBUrVmBgYACrV6+eUQEgUZ4cZOGLpewNXxCDCbbTmMqJb6KGomPZX0WzcwjeOIyMjKC1tXVUXkMiXAiFQoyMjITYngQfD5E2Sk9PR05ODlJTU0O0kMQwWa1WY/Xq1VixYgXkcjmEQiEWL15M37VEIhnlLUnqIBKJoNPpqLAgFAqh1+upppRAUmcQu6eMjAzMnz8fKpUKUqkUUqkUqampSEpKgsvlwuHDh6khMPmew+Fgy5YtGBwcRHl5OQoKCqJ6fxF7P7/fH7I4TmSx7O/vR3FxMfr7+0M+JxsGt9uNK1euICsrC1euXImY5438t7+/H6mpqfB4PLTOpK0eeOCBqLaTPB4PhYWFuOeeeyASieKuO8HhcKChoYH+PduSf7NMHFYgmoNcuXIFHR0d+NOf/kS1Lw6HA2+88QZ8Ph9qa2uh0+kmvdByOBzMmzeP7kLDIelDRCIR+Hw+5s+fP8qGiEyyMpmMLv5isRirV68O8YYjEHsKMtmRI4KxDPvcbjfOnj0Ls9kcUpYsakQrReyGtm/fjpSUlBnd+SRq9xVPVnaNRhOyIM4EEzUUJX0iWm6rsVLVcDgc6HQ6pKenj0oGLZVKweVyweVykZKSMio6cvi1w2MSkefi8/nwer2oqKig2qNt27Zh/fr1IcbKJGKzXC6nz0ME2pGREcybNw/Dw8MAbh2XZWVlwePxhBjDEwGN5BLjcrn0OI8YjSclJYHL5eLatWvIzs7G2bNnqZBE+ptIJMLixYtRXl4e8yghltv9eCGaoMzMzIjCsVQqxZIlS9DX14clS5aM6qsikQg+nw8ikQhFRUUYGhpCUVHRpOs1HhiGwc6dO6nw/PWvf31a788ydbAC0RzEYDBQl/ITJ05Ql/ucnBzYbDZIpVK0tbXBaDRO2jsg0lFPJIKFlkg7+vDFP5rXE5kwwxeRsY6EampqoNFoUFdXF1KWLGhqtRo2mw0pKSm33bFQPIIVEUyDF8TpZqL5zzIzM9Hc3AyhUBhRuxRPMtIlS5bAarVSexrglmanv78f9fX1qK+vh8fjGdU24VndORwOxGJxSFJWImwGa38CgQCkUikCgQAaGhqo9onEvLHZbMjKysLIyAh6e3sRCASQlZUFq9UKsViMQCCAwsJCDA4OIjMzk+bg83q9YBgGfD4fly9fpoJMfX09lEolbt68SWMtaTQaLFq0CL29vRGPjxoaGrB//368/PLL6O7ujtj2Op0Ob7/9Nl5//XWkpaVN2uuIzCcGgwF1dXWjNEVE0Nu4cSPUavWo95GWloakpCSkpaVFDbEw1chkMqSkpOD73/8+du/eHXGzyDI3YQWiOcj69evB5/MxPDyMqqoqav+xfft2lJWVISMjA6tWraL2EePF7Xbj9OnTuHLlCmpra1FdXQ2PxxO1PAn9LxKJ4HA4xtzRA39ZRMIXx0gCWDxHQhUVFTAajSgtLaUB9IC/LIpyuRx5eXkzKhDc6cQKyOfz+dDU1DQqqzsADA4OQqFQ0Fxf4YQLhEQgD7ZPEwgEyMrKgtfrxbVr1+D3+2E2mzEwMIC+vj7weDyaZDqY8KzuxEssWLtInsvtdsNqtaKvrw9GoxE6nY6Ggejs7KTCWVJSEvLy8uB2u9HR0YGGhga8+eab+OKLL3Dt2jXw+XwcOXIEBoMBxcXF4PP56OzsRGpqKhobG2G1WlFdXQ25XI6LFy+CYRhUVlbCYDCgoKCA9n1i+7R58+aIx0cXL16kG5I//OEPEcfXW2+9BS6Xi8HBQXz22WdobW2lSWfjgQRUDdcGNTU1ITs7G01NTQBCbdysVmtUoStaHxqPoOb3+9HS0oJ9+/bFjOQfLUL3nWZXcyfBCkRzEHIMtGbNGpw+fTokPsquXbtoclW5XD4hbUhtbS11L+7s7ATDMCHpBsLp7u7GH//4R7S1tUEqlUbU8oRDJjbizRNrIovnSEipVGLp0qXQarUTtnO402JuTDexUkC0t7dDIpHAaDSOEnqkUinVisSKL0MIFsiJp5jRaKT/TU5Ohk6ng0ajoS7yPp8Pq1atGqXZJEFKSTTmSKEJyGIuFAqpi77X66XpO1wuF/Lz80f9TiqVor+/Hz09PXC5XLh69SoYhsHBgweRn5+P1tZWmM1mSKVSFBQUUDuloaEhaLVa6HQ6ZGZmwuFwQCgUYs2aNVCr1XF7fK1atYr+f1JSUkQhZ8OGDQgEAkhKSsKiRYuQnZ2Nzs7OuOcVu90Os9kMHo8X8l7XrFkDvV6PNWvWAPjLsaPRaERTUxNOnjyJwcHBuO4BgOaDI/HIYtHY2Ii3334bV69exd69e6OWa21tRSAQwLVr12gaknfeeSckGjfL7QUrEM1BUlJSsH79erS1tY0KEEcSJMrlcmokS9yKyWI/1sK/fPlyeDwezJ8/n8Y4qqysjFqf9957D1arFR999BFNcRDPMRup71hB/2J5e5C8bR6PB21tbTHzLU20LjMdNfZ2gGEYXLt2DRaLBa2trfRz0rb5+fk0SGG41pAYM5OgfGNBBHKlUgmxWAydTofk5GTweDwqpGu1WnC5XIhEIlRVVWHp0qUwm81ITU1FT08Pfd8k3cbw8DB6e3upO3vw+DEajbBYLDh9+jTOnTsH4FYKER6Ph/z8fJSWliIpKQkWiwXHjh2DxWJBIBCAw+HA3XffjYyMDJrw1OFw4OGHH0ZnZycKCgposEiFQkFz8Gm1WiiVSixevBgZGRlUOCHjhGEYDAwMoLOzkwp3drsdBw4cgF6vp0JfZmYmCgoKoNFowOfzcf78efT19YX08xUrVmDr1q345je/CbVaDZPJhKKiori1IxwOB0qlEjabLeS9isVirF+/HgKBAN3d3VAqlfD5fHC73dDr9fT4OxYOhwMHDhzAmTNnqJ2VUCgcU3t19uxZ+v9dXV1Ry5Ej0KSkJOj1erzxxhtobm7G//zP/8T17CxzD1YgmoNwuVxkZGTg2WefHRWrgxwRBUe7DTcEHUsIEYlEWLt2LZYsWYLly5dj9erVNDt3JOx2O0QiUdyJZIMXlMl4WpGdu1AoRG1tLfLy8tDY2Dhm5OFoRKvLbIkaO5sJjhgcqb2JHRgRMAikbYeHhzF//nykp6cDQIgAGp46YyyIQE68rLRaLYaHh5GSkoLU1FSq9WEYhto1icViZGVlYWBgAK2trVTjCdxavK1WK4aHhyOOHw6Hg+7ubjQ3NyMrKwvnz5+nQg8RUpxOZ4j3FIkJ1NfXh0ceeQRLly7F8uXLsWDBArjdbtx11100US1xrw9+vry8vFFRm4Pb1G63g8/n02Pro0ePori4GEePHoVUKoVOp4NKpUJmZiYsFgtSU1MxMjKCrq4u2s8DgQB6enpQX1+P5uZmaDSauDc6BKlUSqPVRzoqDXeiKCkpQXl5OWw2G6qqqqJel2EYfPrpp3A6nejp6UFraytkMhkNzhiLXbt2UU++r33ta1HL5eTkIC0tDWlpaWhra6MOIOGeiiy3D2xgxjiYK4EZCeEB08hiFRxnJtpv4w3EFVzOaDTi7bffxpNPPhkSqC4aiYp+Sp6LpEZobm5GWVkZPB5PxMjDE2W2RI2dDZAAhyKRKCQA51jBHRmGwcjICDo7O1FeXk5j8URq22jB+yZDcNRpgUAAj8dDhSwi0Ov1epw8eRI3b96kKXC+9a1voaenB/39/UhOTgaXy0VpaWlIMFDi1t/S0oKbN29i48aNSElJwRdffAGn04ktW7ZQQbCurg6VlZVUcyOVSnHs2DFUVVXh7NmzWLZsGZRKJb0+iUmkVqsxODgYEmCU2NaRZ3A4HDQ2ktFoDInqbrfbceTIEaSlpdE6NjQ0oK6uDoFAAAzDYP369SgoKEBaWhqNAfTxxx8jLy8PPT092LJlCzweT8SoztHae6x5JFLA0niwWCw4fPgwTCYTtFottm/fHnPTNhZjBUnl8Xg4cOAAmpqa8MQTT2D+/PkTvhfL9MJGqk4ws1EgIkQayBONLhoIBNDe3o6WlhasWrUqpjfQZISacAFtosaJY01iEomEeuMFAgHcvHkTly5dQkVFBSoqKkZ5p9wOUVkTRbS2iJZKYqz0H+GEC0Lk6MzpdEKr1WJoaCihAijpr06nE06nE2KxGGKxGB0dHSgoKMDQ0BBqamrg8/nQ2toKq9WKe+65B3l5eXA6nRAIBHC73VTgJu0SPA4kEgnMZjNUKhWOHz8OvV4PpVIJt9uNzZs3U02X0WgEn88PSblx6NAheoy2Y8eOEE2WSCRCe3s78vLyMDIygtTUVCqAEiGivb0dx48fR1FRETZt2kQNn91uN9ra2lBZWQm9Xo8LFy4gIyMDzc3NWLVqFT7++GN4PB5kZ2dT93EikA4ODsLhcODEiRNYt24dzSU2VvqZQCCA3t5eZGZmwuPxRJwfxjPWgqNlE6eI48ePQygUoqenB6tWrYqYeHY8EJMCEo+LEPx+Acz5NBZTRaxI8DMNG6n6NoUIEk6nE59//jlqamrQ0dEBn88XolK3WCx46623cPXq1XFlcTabzWhubkZ6ejpqa2tj1gNAzGzyseyUSByViSQMDbbnGSvSM7ERuXHjBo4cOYKLFy9CJpPh+vXrEWOqTCaJ6e1mkB2tLSQSCcRiMbxeb4jAPN7gjuHHkGazGVarlSZgjeaNNlHIcShwywYvKSmJxvnp6OhASkoKiouL4XK54HQ6sWDBArS0tKCvrw98Ph8Mw2DhwoXweDwQCARobW2F3+8PCbjY29sLhUKBwcFB3HvvvUhPT0d3dzcEAgFcLhcCgUCIvYxUKsXatWvR2NgImUyGoaEhSCQSXLlyhQqVUqkUg4ODyM/Pp9GkyfMQb0qGYXD69GkoFAp0dHSgrq4OAwMDuHbtGhoaGpCRkYHLly+ju7sb+fn56O/vR1VVFbxeL43HlZycjIsXL9L3SrRzBQUFeO6551BYWIjs7OwxnSXIu0xPT0d/f3/U+WFgYADvvvsuDZMRi/b2dqhUKmokbzQacdddd9HcjT09PfB6vVF/HxyyIBpE8Azv78HH6IkKpHo7otPpIJfLYTabpy36/1TYdrIC0RyBGEoaDAacPHkSPB4PV69exZEjR6gnGEnY+Pbbb6O4uBinT5+O6foejkajQWVlJYxGI+65556o5YID3kVb/MYSLiY6uYSnAjGZTFEj0jIMgxs3bsBisdBJ3263o6KiImRHSQYW2f1N1J5prmeEDyba+0lNTYVSqURpaWlEgSVewTA8JpFGo0FSUhJ8Pt+YC24kwh0HwiH9lCQ+JYbcfX19KCgogMPhQGFhIe6++26sW7cO3d3dWLx4MRYsWACJRILi4mJwOLfy9/3mN7+BTCajY8vpdOLDDz/E8PAw+vv7odFo6JGiRqMBwzCora3F4OBgiL2MXC6Hx+OBUqlEbm4utcFatGhRiP1USkoKvF4vcnJy4PP5UFNTA6/XS4UmmUyGHTt2wOVyoaKiAiUlJdDr9UhNTQWPx4Ner4fT6UR2dja1T5o3bx5ycnKwYMECFBQUICUlBQMDA2hsbKQeoECoLVe8zhIajQYjIyPIzs6OOj8cPnwYOTk5uHTp0phzVEFBAfR6PbKysuiRJ/H6KykpQVNTExobGyP+1u/3Y+/evdi3bx+OHTsW9R6kjcPTsYyVGojlFlqtlhrOT5fAOBW2naxANEdwOBxwOp3g8/koKipCd3c3enp6oFAocPPmTQC3OohQKMR9992H5uZmrF27dlyLC5fLRXp6OlJTU3HixAkcOHAAx44dGxVNOh5hZqwyE51cyEJKVLQkKWckHA4HSktLIZVK4fV6sWLFCjzxxBOorKwMOS4jA8tisUx4wrvddo+xPPscDgcMBkPEnVm8gmF4PBnS9/Lz80ctuPEIWZEiSIdD3hGxORseHkZRURE6OjqoFsfn80Eul2Pt2rWQSCRISkqi0awB4IMPPsCaNWuwf/9+aLVaOBwOVFdXQ6lU4tSpU+jo6KB5xpKTk6FQKOByuVBSUjJKw+JwONDR0QEAqK6uRnFxMbhcLkZGRkIm+eB3UV9fj6KiopAwGBwOB1lZWXj66aexfv16JCcno6CgADabDatWrcLy5cuxePFi9Pb2ory8nBqZk2ClJSUlcDgcSE9PR1lZGW3viS448RyFP/LII9DpdKiqqhpzjnI6nVAoFLQd3G43pFIpHn30UQwMDKCiogJlZWURf6vT6dDc3AyJRILz589HLENiJfX398fUVs01LXCi6xvresTQPykpCQzDTItX7kQDvcaCtSGKg9lgQ0R2wBaLBUKhEHv37kVycjJ6enqwc+dOlJSU0LP7yZzjXr16lQaUc7vdyM7ORmpqKo0XMpn6j8c+ZyxD5qamJjAMA5fLheLi4oju2MHXCPZmCjfUZY2mRxPtfXV3d4PD4cDr9SIpKWlUW473PQcbxpOYPeG/tdvt4PF4aGhoQElJCV0cI10nnsWYvG+lUom6ujrMnz+f5tgikaU5HA6NhqxSqeB2uyGTyTA4OIi9e/di586dUKlUYBgGZrMZe/fuhUgkQkpKCvLy8lBRUQGHwwGz2QyGYSJqVjweD06dOoWrV6+iuLgYDQ0NWLFiBRYsWID09PSQzPbkmTweD+rq6lBSUhLiYWo0GnHq1CkUFRVh4cKF4PF4YBgGPT092L9/P7Zv34709HRq+8IwDDo7O/HJJ59AJpOhqqoKKpUKZ86cwd13301DALhcrjHHRXAdSV3EYjG4XG5cYRLGoqOjg47P/Pz8kHuN1df8fj8uXLiAo0eP4umnn0ZBQcGoMna7Ha2trUhPT4fRaMSiRYtGlfF4PPjzn/+MtrY2PPDAA1i9evWkn2sqIcFyg/vuZInXbnQqnCImA2tDdBsSPNFLpVLcf//9sFqt2LVrF4qLiwEAJpOJeoxMVK1bVlaGtLQ0aLVaZGdnIykpCcuXL49Y1uPx4MKFC2hvbx/TDiCe9BvB9Pf3RwztT5g3bx5N0hhtcAZHGY61m4gVQXm8zLVdZDSiaXq0Wi1NSxGpLcer+SOeX0KhEDqdLuI9pVIpGhoaoFar0d7eHrEPjcc1n7zv/v5+lJWVoampicbW4XK5NP6Py+WiCwpZgAOBADweD90Bk/AWq1evRmZmJiQSCXJzc6lgJ5FI4Ha7I/aHpqYmtLe3o6CgAI2NjXjkkUfg8XiQmZkZktk+uE2EQiEqKipGBVI8c+YM0tLS0Nrait7eXprU9sMPP0RZWRk+/vjjkDo4HA58/vnnMBqN6O/vx9WrV3HmzBnMnz8fZ86codHd4xkXwXUk/0+846IxVsLe8HLETjK4f9ntdhrVOho8Hg9VVVV48cUXIwpDAEYFvoxEfX09DcPw2WefxazzTBDensTo3mKxJExrHa8WfCo0N9MFKxDNIXp7e+kuNScnB3/zN39D1exkMmIYBqdOnRp1zBUvAoEAy5Ytw4MPPoiHHnqIBoOLRH19PWQyGfR6fVQ7AIZhMDQ0hCtXrtA8TPEQHto/HJLHKDwOUzDBAziRQk8sptuWaKoEsGiTH1GNhyfxDa7P8PDwmEaswfeRyWTUnTvSPTkcDkpKSjAyMhISiHA8hLcTwzBQKpWwWCxYsmQJNXo2mUywWCzUq6y9vT1EA/HnP/8ZZWVl+Oyzz2AymdDb20u9rwoKCrBq1SokJydDJpPBYrFQbcmNGzdC7m21WiGVSlFRUQGLxYIvfelLaG9vx/r160e1T3ibRPps06ZNMJlMWLBgAZKTk+mR5o4dO9Dc3IyHH344ZONAAlYCt1L1ZGVloaqqCk1NTdi0adO4hNpww2MejweZTBbzPcWT3icQCOD69evIycmBx+MZtcAmaqxxOBwkJyfHnOuysrImdY+pJrw9SdsAmPDmOJx4NzvTNddOBeyRWRzMhiMzALBarTCZTHRi6O/vR1FREfWCsdlsOHnyJBYsWACdToe77757UvcjeaWIfUc4Ho8HV65cQWpqKvLy8qiaPliNbbPZcPbsWQwODiIjIwP33ntvXAPU5XKhuroaq1evjpquYTa5yfv9fvT09CAQCFCD2emoU7D79WxwBY7n+GEiRHrX4znqDI7bI5fLQ9T/ZOxwOBx6vGq32+HxeFBYWEgNmBmGgU6nw4EDB7Bp0yZkZWVBJBKhv78fSqWSepuRd0+M9fV6PebPnw+fzweZTAabzYaenh6kpqbC4XBQF/ZE9efOzk7w+Xz4fD7k5+dHLGM0GvHKK6/Qvx999FFkZmYiNTUVHo8HtbW14HA4WLZs2YTj+zidTpw+fZraYwUTT/who9EIuVyOlpYWKBQKZGVlobOzk7rfk+PK6TjqJmE7jh49iieeeGLWaT/C23O6zACCQ5wQDd5smI+DYeMQJZjZIhCRXa7dbseNGzfA5XKRnJxM02r09PTgnXfeQWVlJVatWgWVShXxGvFOugaDAV6vFwKBIGbAxeAYNMSdnpwzGwwGnD59GlwuFwKBANu2bZtcIwQx1pn2dApM3d3dVCsnEolixmlJJOEL/UxDAjB2dXWhrKws6o47EQTbKqSkpMSMSUW8MIngSD4PBALQ6XRIS0ujAgu5RiThPritg783mUw0tpBcLgefz0dtbS2WL19O00kExy7yeDzo6elBeXk53dCMx+YjlkBBkqnKZDKkpaWFCJBGo5EGerx69So++ugjrFmzBgsXLoRQKERbWxv8fj/cbjcCgQDkcjlWrFgxoffz6aefQqVSYXBwEFu2bIlZNpbAK5FIIJFIcP36dWRkZMBisSAnJ2dWbADudMgcbLFYIBaLwTDMrNmcEVgbotsU4vppMBhw/vx5XL58GU6nkx6XvfHGG2AYBufPn6cGmeGMR82ckpICoVA4yhU10jX9fj/18mlra4NIJKLXWLVqFaRSKTZu3DixB4/CWGfa5Fn7+vrwyiuvYO/evVE90iZLVlYWfD4fbDYbMjMzp82WKJ7Et9GYijrGc/wQD/HEGAm2VYjWr4OPDkjcHlJP8n1aWhpMJhNSUlJiJgYmQlLw30KhENXV1Xj//fdhMBho0Mfa2lqUlJRQTUvwdSQSCUZGRmjGe+DWwuLz+ajNR3D290jvJ9IRyccff4z6+nqIxWIkJSXRdiGYzWb4/X74fD4MDg7S4+b+/n7weDy0trYiMzMTfr8fAoEAYrE4Zg7Dsd6VVqtFf38/9SKL1N/Ib8nCGlxfcvQik8ngcrlQVlaGwcFBZGVlxX1s6vf7UVNTg5/+9KfjCkEyl/H7/WhoaMCrr76K3t7ehIzvaHMFmYM1Gg04HE7IGJuLsALRHEMqlaK6uhpJSUlwu93UEFUqldI0FQqFAv39/RGNDeM1jGMYBk6nEykpKWOqXIndAIfDoV41N2/eBMMw4HK5yM7OxgMPPBA1XtBECV5owges3+9Hf38/zp07h3379lGbidOnTye0DgS32w2VSkWjHo/XiDwWYwW5nGiogNkcOykel+9gW4VI/drlcuHcuXO4cOFCROGM2C/5fD7Mmzcvrn7udDqxb98+jIyMALiVOf3KlSvIzs7GsWPHkJWVBYvFguXLl6OlpSWiQ8LAwAA++eQTXLp0iQo8NpsNZrOZ5j8j2d/JZiccksCWCBsnTpyASqVCW1sbDTQZbAwO3BIgeTwe+Hw+xGIx9u7dC4VCgfb2dpw5cwalpaXo7+/HihUrUFVVNWYOQ0K0d5WXl4eysjLk5eUBiNzfyG+JR1ukeYn0cYFAgNLSUigUCvh8vrjs1Hp6enDo0CG4XC689dZbYz4LweFw4L333sNLL70Eo9EY9+9mA729vThy5AiEQiH2798fc3wTjW5TUxNGRkaiCk9jzWfkqGxoaAjHjx/HW2+9hTfeeAMHDhzAiRMnaCqY2Q4rEM0xOBwOtmzZApPJBJfLhby8PJjNZnA4HPzVX/0V0tPTsXbtWqSnp0dcJGPFlzGbzTh27BiuXLmC119/HYcPH4bFYomrTsEh7z0eD53Mw+8xVVqT4GSufr8fly9fxsDAAJRKJRYuXEiTfK5duzYh9wt/FqlUSgPtkWB8RFCbLIkUroKZzbGTxuupEqlfV1dXQyQSUSPv8MWYHH0F52Qb6x5Hjx7FsmXL8NFHHwG45ZW5ZMkS9PX14fHHH6fRpoVCIdasWQORSDSqr3zyySfIyspCQ0MDWlpaYLVa4XK5qB0SeX4SzTrS+wkPkrh+/XoMDg6ioKAASUlJMBgMEIlE1PMU+EusJ5IUdufOnRgZGUFhYSHuu+8+eL1eLFu2jGqLzpw5E5dzRrR3pVAokJOTQ48pIvU3jUaDoaEh+pyR5iwiMA4NDVEhqLGxkSZzjkXw+IvHyJ9w+PBh3LhxA3a7HX/4wx/i/t1sQK1WIz8/HxaLhcZhi4bD4UBfXx8kEgl0Ol3UOSbafBYs5JrNZrS2tqK1tRUGgwFGoxHt7e2wWCy4du0avbbf70dnZye1UZ1NsDZEcTBbbIgI9fX1+Oyzz6BWq2GxWPDMM89Qo+dI8VjisaWx2+24cOECpFIpzpw5Q4WbgoICPPDAA1F/c/jwYeTm5mLx4sUQCAQxc9okKqlrtLoQ42Kz2UxtJHg8HpYsWTKpxI9j3W+q7Zdmm53QXMHlcuHMmTNUKB4ZGQkxMo32joJjIwVrSBmGgcFgwJEjR7Bjxw4oFIqQ+GDZ2dlwuVyjcpuJRCI4nU66aRgaGsIHH3yAnJwcVFZWwu12Q6vVQqfTUWGGOEuMF4PBAKvVCuCWAJCeng6fzzcq51x47sOWlhZcvnwZS5cuhUQioekybDZbXDHI4unv4WWCBRviPZuSkhLye6/XiwsXLqC/vx+ZmZkoKiqi7vFXr16FXq/Hhg0bomqf/X4/6uvrcezYMXzlK19BRkZGXM/x9ttvhxyx7d69e8w2mC0QT8+WlhYsXrw45txHPB77+vqQlZVFc8VFKhfLRo+kkOnv70dTUxN1MFGr1UhOTsbixYuhVCrB4XDQ3d0NLpcLl8sVkg9xqmCNqhPMbBOIvF4vzp49i5qaGmzbti0kjUKkTPfE9TeWsRvDMLBYLKirq4NarUZdXR2USiU2b94cdbJ5//33cf36dcjlcmzcuBFLliyJWe+pNHIOvjYxlB1vBu3xMJ1CylS122SuO9eCWUYSxqMJ6CS+DcnZRibsaNcwGAxQKBTUlZ4cxZlMJiQnJ6OjowPp6enweDwhzgnhhtF2ux3d3d1Ua1JSUjLu5yQpLZxOJ7hcLtRqNWQyGZKSkmI+w8GDB5Geno6BgQFs2bIFAwMDGBgYwIoVK6g94HjbN1oZu92OoaEhmM1mFBYWorOzEyqVKmIy2Pr6etTV1SEQCEAsFtOo1AKBAJ9++mlInROBzWajY/ujjz6CwWDA888/P6YgNduYTR644RCPXIlEMi3u+axAlGBmk0BEOjoQOfNy+EAgmozgHWqiePXVV2G1WuHz+bBmzRqsW7cuaj2mk3hceifLbJ5w4mUiGjvy3CaTCRkZGTT7+mwnmmZkvBoi4rEWbLtms9moAbRWqwXDMEhNTaVCI/G0iifi81geekSrEvw9CcrncrmQmZmJ4eFh+Hw+quHJycmJ+cwMw0Cv1+Pw4cNYtWoVSktLxzVuyHOKxeIQrbDb7UZtbS3Ky8uhVCoBgB6tpKamQq/Xw2q1Ijc3F3K5PKKGqL+/H6dPn4bL5cL8+fOxcuVK+t3NmzfR1NSE+fPnY968eTHfabwYDAYIhcJRwutcw263QyQSRWzTqYT0Yb1eT8MjzPT8yHqZ3caQM1uGYaIGsQu2pSBBCUkU30Ty1FNPITU1FStWrBgVyn4mDXbjCfoGAMPDw/jDH/6A4eHhcd9jMsbMswGGYaL2oVgQeyalUhmSfT0R9ZmIfVms33m9XtTX16O9vR2BQGDU+yLHykSgIfYMkRwKAoEA9Ho96urqcO7cOQwODtKyLpcLHo8HGRkZ1MkB+IvRN5/Pj2snzOFwwOfzUVFRAY/HE7FMJNsZnU4Hm80Gt9uNjo4OKpj4fL5RCVYj9VtyfHXfffdRF+rxQAyjXS5XyHXPnj2LpqYmnD9/PiRGjVKphNFoRG5uLhYtWoTk5GS43e6IC3d6ejruueceVFZWjtJAFxQUYNGiRTQCNcPcSkeyd+9eGAyGqPUlSbAj2bBES/I6HXg8Hly8eBH9/f2Ttq2RSqUYHByESqWa1jnY4XCgv78fIpEopk3SbIUViOYYxDCRxEshcYnIgjA8PIzXX38dV65cgc/nG1dKg/GSnJyM5557Dps2bRqlVp9Jg13igUOyY0daLD0eD1577TV0dHTgnXfemfC9yO58eHh41qXsiOW6ToKpAeOLZEs8Cnk8HnJychKm7p6oAB3rd42NjeDz+RgcHKTpLMLfD0liSmzPGIZBb28vuFwuWltbaduZTCZ0dHRAp9OBw+Ggrq4OAKg3GNmBFhQUxK2FDRbmvF4v6urqMDAwEJKrK5yysjKqQSJkZGRQp4KMjAyIxWI6PzidzjH7JMMwkEgkuHjxImw2W8T4ZbGIZFQdCATQ1dWFpKQkDAwMUAHcZDLRtC8kKGYsuFwuMjIyUFlZOUpjFm5YbrPZcODAAXR0dGDv3r1Rr20ymWCz2eD3+0d5xs1klOXa2lq0trbi5MmTMQW6eOBwOBE9DacaqVSKzMxMahcX6d7TFZJkIrAC0RyD7CJPnjyJo0ePwuVyUQ8khmHw3nvvQSwW4+jRo2N6YEQjnhgw8dRzpjQoZKJ0u91RF8v6+nqaNFSv10/4XjqdDlKplGbKnsodkd/vR1dXV9TYNOHEcl2fqMAa7FGYyHcbXB8yYRJbjnh/F05paSkGBwehUCggFoshFAphMplovKyOjg709PRAoVDAZrPRuD0KhYJGkiZtJ5VKkZubC61WCw6Hg6qqKgB/8QaTy+UoLi6OmHg2GsHC3LVr1zAyMgKTyYTBwcGo1xAIBKNiPOn1ehQXF0OtViMpKYkaY5PnHMvb0+Fw4OrVqygqKsLAwADVfjmdThw5coS2WTQiRSc2m83YsWMHPB4PHnzwQchkMjgcDnp8yDAMBAIBWltbYbPZEqJNtlgsGB4eBo/Hg9FojHo9kmMuUjqQYOLNt5YoLBYL/H4/AoEABgYGJn29seZg0g9GRkYSlp2exCEj4REi3Xs2h/tgBaI5hMfjQU1NDc6dOwe3243h4WF88MEH8Hg8kEqlcDgcWL16NXp7e1FcXDyhoyCyQ1YoFDFjwEyW6dgleL1evPbaa3j77bdp3BgCieYtEAjwwgsvhNTLZrPFXbfMzEz09fUhPT19yoOS6XQ6yOVymM3muCaTWK7rkxFYEyEwx6pPcKDPsZ4z0nOQvjU8PIyVK1dCJBJBo9Ggr68PQqEQRqMRvb298Pl84HK5aGtrg0qlogu7QCBAfn4+ent7qe2LTCaDSqVCVVUV1q1bRxOOEo0CsZUI7tdj9fFgYU6pVEIsFiMQCNDYQvGi1WqpQCeTyei/aIHywhckqVSKJUuWwGw2o7y8nPaX06dPIz8/H5cuXYr4HrxeL65evYqBgQGIRKKQMkRQfPjhh5GamkrblhyNSaVSNDU1QavVwmKxwG6303cyUbKzs7Ft2zbweDx85StfiToWZTIZFAoF8vLyomqCdDodfvzjH+ONN97AF198MeE6jYeNGzciNTUVxcXFKC8vn/L7kXFmsVjA5/OndL4PZjaH+2CNquNgthhV19TUoKioCE1NTWhubkZXVxdKS0uhVqvxwAMP0MVcr9dDr9fTxWAsfD4fWlpaMDw8jOTkZDQ0NKC3txfPPPNMQs/SyQInkUhgMpng9/uRlJQ0bkNvn8+H5uZm9PX1Ye3ataNynRHD04sXL9KjhNTUVPzVX/1VSF0GBgbw+eef46GHHqLvdbxG6MHpI8KNi8dr3B3NmDf4etFCGoyHSMbB4yHWMyeCidaPGHQ2Nzdj0aJF8Pl8cDqdNGCp3+9HX18fMjIywOPxYDKZMDQ0RO18kpKSqJ1dV1cXuFwuAoEADSxIaGlpQVZWFjo6OnDz5k2IxWKsX7+eelARQ3UAtC/J5fKYhr7j6SvBhsN+vx/t7e0oLCykKUDG6/5OPgsP10FykS1dupRGIgZujb/29nYalV2v1yMjIwMGgwHl5eVUexXLaN9ut0MgEKCzsxMFBQWora2Fx+NBSkoKKioqYj5/PEzWC/JnP/tZSFT7ueR2Hy+kH5BULXPFY3S8zBmj6lOnTmH79u3IysoCh8PBgQMH6Hderxff//73sXDhQshkMmRlZeHpp59GX19fyDXy8/NpSgvy76c//WlImatXr9KFMycnBz//+c+n4/ESTmVlJdra2pCWlkYTSjY3NyMjI4N2bqK6X7t2bVzCEAC0t7fDbDbD7XajuroanZ2dUCqV2Lt3b0LrT3amJIUA+Wy8tLe306OO6urqUd/fuHEDqampKCkpgVAohFQqxfbt20fV5ejRo6ioqKBB9oC/7F4kEklcC3EsLUy8xt3BdbLb7RAKhRF3ayTT/GQ9N+LVwETTcJBnVqvVU6LlI1ofkiMsXk2UzWZDbW0thEIhGhoaIJPJkJqaSsNNuN1upKenY3h4GHK5HAUFBSguLgaPx6PRrgkikQherzfiGCosLERfXx/a29upEFZdXT3KUJ0syjabDVarNWLkeNLGbrcbDQ0NOHDgwJhjIljD097ejqysLLS3t4/6Llb7isVi9PT0wOfzUSGcHLHZ7XYYjUaIRCJs2rSJGjuTura1tSEzMxNcLhcGgwEFBQXo7e0dZewdSxMglUrh9XpRXFxMI7u73e5RmtyJEk+k81gEb54effTRhNRptkHGmUKhmLPZ6RPNjLaA3W5HZWVlSNZlgsPhwOXLl/GDH/wAly9fxr59+9Dc3IyHHnpoVNkf/ehH6O/vp/++853v0O9GRkawadMm5OXl4dKlS3jppZewe/duvPbaa1P6bFOBUCjEihUrUFBQgPvvvx92ux33338/KioqaC6k7u7ukAUknqOpwsJCaDQaiEQirF+/HvPmzYPD4cCuXbsilvf5fLh27Ro+//xzuhOOh+C8N0lJSeDxeBPSQBUWFiInJwcjIyMh3m3kzD8nJwfd3d1YsGABdu7cia9//eujgtxJpVLcf//9uH79ekif4nA4dBENnshJdNXOzs4Qm4JYRpjh6RXGgsSvCbZtCD+eSsRRY3CqlbGi2EZaXMkzkyCEibYFIM9IhF6yqI1l02GxWJCZmUnbPFxoJMc42dnZGB4exoULFzA8PIykpCSaHJa0a1paGtUYhdsy8fl8FBcXY/ny5ejv74dQKMTq1atHGapzudwQwTqWPQVJbxAIBHDixImY7RMsaOTn59NEz36/P0Sgj9VPiLBOkrn29fXhz3/+M5qbm2Gz2SIKEyRGT1paGq5fv47h4WFqML1gwQJcv34d8+fPp+XDjzOJJpo4e5DvxGIx8vLyIBKJQozFxyLWWBhvpPNwtFotdu/ejd27d2PRokUTukYsZrNh8Z3MrDky43A42L9/Px5++OGoZWpqarBy5Up0dXXRbOL5+fn4+7//e/z93///7L13eJzXeSV+phfMYAqAQe8dIBpBkAR7FSlSjRRJVctSZDtyXOLY3sRe/5JVdp14k2ycxIlLLMsrWaIqJTaxiwQJggBBAkTvZQAMMMBgeu8zvz+w92pmMAABkpIpRe/z4HkkYvDNV+5373vPe95zvhf1b37961/jJz/5CWZmZqhi549+9CMcO3YM/f39Szq3+6VkZjQaceTIERw4cABSqXSe3tDk5CR6e3tRVlaG3Nxc+u93og69GOQ8ODiI0dFR2mq7devWe32p8yIapB8ZExMTtPOOQPwCgQBOp/Ou9YKIm30wGASfz0daWhpt3Q0tEyz1WpaqlRLp6L4cR/TQICWZxMREjI+PIzs7m3LPllJa8fv9aG1tRUNDAx5//PEwf6p7rcVEypZEcTctLQ1MJhMTExOIj4+nHLnI7yQlRR6Pt6gdh91uR3d3N225T01NpZ1VoUKbJAGItKYhx7h69SrYbDYCgQAeeOCBec+VjFm73R7G64l2jxkMBurq6uByufDggw9ScnmoCna0UhrRP+JyuQgEAnRetNvtYLPZGBgYQElJCdhsdpiGEZPJxNTUFKRSKaxWK44dO4bc3Fyo1Wps27YNMTEx9N0n50jKXBaLhW4+CVqZl5cHiUQCDodDx2VkGZCUGtVqNfLz88O66W73bi/0HCNFaBcbj0t97z4LjbG7Ve3/IuigfVbxuSmZLTfMZjMYDAYlOpL43//7fyMuLg5VVVX4p3/6pzBiXlNTEzZt2hQmX75r1y4MDAzQborIINBt6M/9EEeOHEFNTQ3ee+89qFQqcLncMGLk4OAg0tLS0NPTQ+H5OyWwEQuAaO2fOTk5SEtLg91un6c/9GmFw+FAIBCA1WpdEJGQSCTQ6XSQSqVUaoA4wd/tpJGamgoOhwMOhwOpVAqVSkWtFpbbzbecLotIR3fSpRP6PJdCBCeIwM2bNyGVStHb27uk0gq5d6Ojo6ivr0d6ejo1jLwXnYTRdsoLtfanpqZicnISWVlZUc+bKDO7XK7bkpkLCwvhcDiQkJCA5ORk2gIeeS1Op5MST48fP47m5mZ4vV4IhULaUk4W48j7QUr4MpkMk5OT89DF0HssFAqxd+9ePP744/TZEgFMkUgUVnYN7cIjStRutxs+nw99fX3UA21gYADZ2dm0nNbX14eUlBScPHkSb775JqRSKcRiMex2O3bv3g2lUomamhqkp6cjPj4eGo0Gr732Gtra2ijC6vV6IRaLweFwqPgkaYk3mUxh6HRkyZiUGnNycsLegTuRBiEt83a7nd4vrVaL1157DX/7t3+Lzs7Oec9/sfcudBzebRfUUtCfuyUW38+dWp/n+NwkRC6XC3/1V3+Fp556KizL++53v4t33nkHdXV1+NM//VP8/d//Pf7yL/+S/n5mZob6fJEg/79Qu/XPfvYzSCQS+pOenv4pXNHy48CBA7h58yY2btyI/v5+iogAc0mc1+vFxYsXUVpaGjYp38mi5Xa7wWazo5o7stlsrFixggq5fRaxlFIAm82m8v93Cnwu1EHFYrGQlZVF1WulUilkMhmsVuuCMH9kokIQj+HhYdhstiVNhpGO7tEE7EL5HwtNkKR8V1paCr1ej6ysrGVNyDk5Odi0aRNUKhX27dt3zzpEok3sC7X2s1gs5Ofn04Qk2rF0Oh3YbDZ0Ot2C94K0Bq9YsQI5OTlwu91wuVxoaWkJQ2GIPo/H40F7ezu4XC7UajX6+vrAYDCQlJSE7OxsJCQkhH1X6IIoFAoxNjYG4BPEJDRIGdBkMtFNCCnRCYVCxMfHw2q1QiaThbXJW61WBAIBuFwuJCYmgsPhwGazgclkYnR0FAwGAyUlJZienkZOTg6AOQ2j5uZmanp87Ngx6HQ6ZGdng81mY//+/SgtLQWTyYROp8PZs2eRnJyMW7duYWBgADKZDC6XCxqNBmlpaVAoFNi5cyf4fD7YbDY9RyINkZKSElYyZrPZKCgoAJvNvuuEgMhdmEwmOkauXLlCnek//PDDec9/oe/0+/04ffo0/umf/gnXrl2DQCBY8NyW0mG5VB7X3WwmIq/lTktwbrcbdXV1OH/+/LLoD1/U+FwkRF6vF4cOHUIwGMSvf/3rsN99//vfx5YtW1BeXo6XXnoJ//zP/4x///d/X5JL80Lx4x//GGazmf6oVKq7vYR7EjKZDF//+tcxNjaGzs5OnDx5knZCNDU1obu7Gy6XC2+//faySjihQV4sMomlpaUt+W+Jfka0nfDdBuH2eDweyOXyqJONUCiEx+O5K3VWvV4PsViM4eHhqJMej8fD9PQ0nE4nsrKyogrGkYhMVBwOB0ZGRsDhcKBUKpc9GS40iRLYfLG2f6LNJJVKkZGRgdjY2GVNyGw2G2vWrMEPfvAD2sgQLZY7MS9lYQxNLAEseN4kgfD5fLS1e6Gw2WyYnZ2FyWRCe3s7Ojs7kZSUhNbWVvoZkoTGx8dj3bp18Hg8SElJoQlwVlYWUlNTkZycHKafFIl+5ObmUi/BSD4ZWdjHx8fh8/mo5g/5W7FYjISEBCqrAcy9Z6QLjFxjamoqxGIxAoEATYBCExBgTsPogQceQGlpKQwGA1auXAmVSoWhoSF4PB68//77UCqVFIncvHkzpqenUVtbi5KSEphMJiQnJ0Mmk8FsNiM1NRWNjY0ULWKz2dBqtejr60NXVxecTmeYcGJoRI7l5Uo5SKVS2Gy2sGrBqlWrwo5/OxX/0Gdw8+ZNAMDHH39M72sop4wEEeJcjKy9nGTvThOZyGu5U8SopaWFduJGa1D5rxb3fUJEkqHx8XFcuHDhtjXANWvWwOfz0V1ZUlLSPJEr8v8LGfbxeDzExsaG/dwPQYTSlEolRRwuXboEAKitraULM5/Pv2NRRvJiud3uBSezhUKr1aK3txcqlepTSSJvp756L9RZ4+LiMDExgYyMjKiTnlqtRnx8PJxOJxwOx6KTOOEvkYVVKBTSXfa9NItcTsnh0ypzkVjuxLyU81kKAkbC7/fDZDIt+lyCwSDUajXEYjHa29uRnJwMDocDlUoVtqiS0h3h0+zYsQOrV6+mwn8MBiOs628hWx2CLmZnZ897n1JTU+FwOOZp4pAutdnZWYoECQQC2Gy2MN8w4BP+jUKhQHp6Ov2OaCR0DoeDjRs34tChQ/B6vfRZfvjhhxCJRDhx4gRFIrOzs/H888+jtLQUbDYb8fHxcLlcYLFYyM7OxunTp3Hr1i1cuXKFomgjIyMA5gjuarV6SQu93+/H5cuX8corr6CtrW1Jf0MSRZFIRMdiZmYmnn/+eRQWFuL73//+ksZ4IBCYN04WK5vx+Xx4PJ4FkfHlcnvuVenrThG3VatWUWrBZ0V/uJ/jvk6ISDI0NDSEjz/+eEkdA+3t7WAymbS0UVtbi/r6eni9XvqZCxcuoLCwcNkS9X/suHr1KkpLS6klhdlshtfrhdfrBZ/Pxze+8Q3ExcVhzZo1yMrKuu3xAoEAJiYmcPToUWg0GnR2duLf//3f0dbWdkelMMLJ8ng8YRoe9zJut4CS7p6PPvoI7733XtRW58WCyWQiLy8PVqs16ngLFcHT6/VRBSyjIQXk3DIzM5Genr4k48il7B7vRbfKcnfnDocDPB4PSqUSr776KkwmE/3dpyG6RhaXUJPVhc5rfHwcgUAABoNhQbkDh8OB7OxsGAwGrFu3DjabDeXl5di5cyd4PB69H6E6SHq9HufOncPJkycxPT1Nu7BC7z259oUI1MAnnVZer5eOTYFAQDlTqampcLlc8Pl8aGxshFqths1mAzC3ISIdYGazmaIYoVIWoVIKU1NTiI+Px8jICD1Hp9OJU6dO4dy5c8jJyaEk6LKyMszMzGDFihUUXVGr1fj1r39Nvc0YDAZcLhckEgn6+vowNTUFFouF4eFhtLS0oKCggD6nuLi4BblekTE1NYXr168jOTkZFy5coATupSiVh75jTCYTWVlZeOqppyAWi2/7vcAn7fkHDhxASkoKvv71r1NOV7RxLBKJ6M/tzmcpca/elzvd6JBntWbNmiXLtHyR44+aENlsNrS3t1NfIKVSifb2dkxMTMDr9eLAgQNoaWnB4cOH4ff7qeAgMT5samrCv/7rv1IDx8OHD+Mv/uIv8Oyzz9Jk5+mnnwaXy8WLL76Inp4evPvuu/i3f/s3fP/73/9jXfYdx8aNG9HT0wMOhwMmkwkGg4H+/n6KBsnlcrzwwguorq5GbGwsgsEgrFYrxsfHo5aw9Ho9WlpakJKSgkuXLuH06dNIT0/HxYsXFzV4dDqdeOutt/Dyyy9jfHyc/nteXh6SkpKQnJyM/Pz8e38DlhDBYJDaloyMjODMmTP03+12O9WDWWyivZ2fkd/vx/Xr1+Fyuage1NDQEM6dO0eRI2KSef78eYyMjGBmZoYSb5e6eyRQ9mJJ3b3YYS5Xs4XoA7333nuwWCx4/fXX6e/uBQIVLYLBIAwGQ1gjQbTzIkiLXC6nzvORhHOBQACDwYBgMAg2m42KigokJSXR5x3tfly/fh0ejwculwv9/f0wm83g8/nzEt7bXTvRDerr6wOfz8fExAS0Wi0sFgsVWfT5fOjp6UFsbCyMRiNNfoRCIUQiEbxeLzVtDZWyiJRSSE1NhUqlgkKhgN1ux8zMDH71q19RUdSGhgaUl5ejqKgIlZWVWLduHVatWkWTqhMnTqCyshLvvfcePX9C8lcoFOByuZidnaV/Nzg4iJUrV6KiogLFxcWwWCxLWuhTU1PxwAMPYHZ2FgcOHKBo4GI6WSQpdzqdYaXD5QZBfDIzM/GNb3yDyjUs9Cxv94zJ2CIinZHzTOQGJvJ4n3U7fl9fHxISEjA2NvYlQRt/5Lb7y5cvR23Z/upXv4qXX36ZuhhHRl1dHbZs2YJbt27hz/7sz9Df308h3q985Sv4/ve/H5btdnZ24lvf+hZu3ryJ+Ph4fOc738Ff/dVfLfk875e2ezJBGI1GXLp0CePj46isrMTu3bvB4XBgt9vB4/FgNBqprsrs7CxiY2Nht9tpSy6JQCCAyclJtLa2Yt26ddBoNDhz5gz27NlDyZXR4vz582hsbIRYLIbL5cJPfvKTRc+bcIvGx8cRHx+PrKwsuN1uCAQCKhI4OTkJj8eDnJycZflBhYbVasXx48chkUjQ398PJpOJnJwc7Nu3L0yBmuzIl9PuSu69Tqejgnw+n4/qrnR2diIxMRFGoxE7d+6Ew+FAQ0MDtYooLy+n5PylttuSc17sXO9F++2dqPoODg7iwoUL0Gq1yMzMxAsvvHBH3x16DiqVCg0NDeDxeNizZ09YS7bNZqPE4fT09GW1ZgcCAQSDQXof7XY7lEolNBoNkpOT59kkkPtBuGqkXPXxxx9TrS5SGluuKjRReSayB1qtFkwmE0ajEUlJSbBarXSB1Gg0EIlEyMrKWrB0fbtzIGPI5XLh3LlzyMvLw5UrV1BQUIDdu3eHJRKhreAEITp+/DgeffRRKp5L4qOPPsLw8DCYTCYKCwuxa9cuej6hLfTBYBBarRZOpzOsnEfKgSaTCXl5eWE6YeSaQhG6yGs6ceIEdDodnn76aWpmeyexnPdnKZ8lIpxmsxm5ubk0iX7nnXewYcMG5OTkhCGJkXG37fjLDSLFkJmZecfz7v0ey1m/7xsdovs57peEiLwsBIUIBoNhSEYwGAzTqSE6K4vpmNxJOJ1OHD16FIODg3jhhRfmWRtEhlarpSRlwvPJzc2lO6mRkRHYbDaw2WwIBALk5eUtWzOJdMWUlZWhvb0deXl5sFgs2LZtGwQCAZ3MyDncqeYJ4Q2pVCqkpaXR0uTo6ChGR0exadMmKs7ndDpx5coVJCcnIysri46dSK2ahSbZT1trZLnHJ58nXJaRkRF0dnZi37599Nru9Jy1Wi3Onj2L2dlZxMTEID09HXv37qXHtNlsFAmJTNpCE7pAIIDh4WFIpVIoFAoQLa7Q5x0MBlFfXw+RSAS32420tLSolhkEUQwEAmCz2Usqc0YGMRAmXWplZWVhJHzSeZaSkgKXy0WvMzR5WCxIWSlUPyk0Qp/Z7OwsLly4gEceeYS+a2SDYDAY0NTUhNra2jCbjsgkidzLtrY2DAwMwOfzYd26dXShj3zmWq2WommBQICqaovFYtrA4Ha7UVBQsKT7GQwGceTIEbrhSUxMxIsvvrjkhPRuYikblNnZWdhsNvB4PPj9fqSnp+Pv//7vwWKx4Ha78dxzz0GhUCxZKymUz2QwGO7aXuNuLU0+j/FlQnSP435JiO5kR3o/xJ0iREu9FtKu7HA40NzcjD179kAikSw4ad3JLizauRAiaVpaGuLi4jAwMICioiLaFr7UsthnuSO8m+82m81QKpVISkqiStXRPKqWeszQe0pKnYQD9Nxzzy3Y9hw5oYeKVxqNRmp9kpGRQZHSyOfh8XjQ0dEBmUxGJQkiEdRgMIjZ2VkEg8FFeSO3u8aWlhZaAoqJiQlTPiaIUWZmJgYHB1FXV4cnn3wSfD4f09PTNGlzu93Iz8+fp7gebZFeCmrE5XIxNDQEjUaD2dlZcLlclJSUYHh4mCaikcciG4pgMIhAIICBgQGIRCKkp6cjJiYm6jMnfCyCEI2MjCAlJQVTU1OQSCRREaLI+xcpQNvf34+LFy8iEAiguLgYjz766IL3fynjMRgMYmJiAseOHcOuXbtQUFAQNVlYLPkMTVz0en3YZvVXv/oVZmdnIRKJ8JWvfGWeDMxiYbFYoNfrYTKZkJmZCZ/Pd0eJOYlP24fwfowvE6J7HPdLQhQaJImQyWRUfNBut+PkyZOQSCTYtm3bZ6YRdC8i2iSu1+tx8+ZNZGdnIykpCXq9HkwmE+np6QgGg9TUUqfTUT+o1NRUuN1u+rJHWxjuVdJIRA4JAbekpAS3bt1CcXExYmNj6aT5x0SBFovlfndHRwcSExMxMzOD1NTUqLvM5RwzcrEiSUpFRUWYkGroMXU63bwJPRIh6unpoQ70TCZzwQWRlG0IShQNkSHoFOH5lJaWLlvSwuv1UkmMwsJCKgvAZDKpenNHRwdaW1tRWFiIoaEh7Ny5E1wuF+Pj4+BwOFR7KhJJiXa/F0oCbDYbTp8+jbKyMggEAqjVagwODiIpKQkGgwF+vx87duxAUlLSguhFNEXppTxzh8OBuro6bNy4ETMzM9SM9nYRTY3aarViYmIC09PTWLlyJeRy+V1tEG02G/7lX/4Ffr8fHA4H3/jGN6ImC4sdy2630xJobm4uWCwW/H4/rl27hkuXLkEmk2Hv3r3Iycm5LTJDkmQiZOlyuaguXHp6+rxkbDnGwF8iRF8gpeovYy7sdjtef/111NfX4/3336cdTadOnYLVakVfXx8uX7687OPeDaEvslOJ8JPOnj27pI6zaOTglpYWxMXFob+/n+qjuN1uTE1NhZlakpbz9PR0sNlsJCQkhLVBR5IF7xXxt6KiAkajEVlZWVi1ahV6e3uRm5sLk8kURoRejPj8aZGQlxLL/e6SkhLMzs4iKysLcrk86nNdzjFDO2yCwSBMJhOGh4cpgZlE6P2L5lEVSoJns9koLy+HRCKhDvMLdfEQnhD5TOS4J6VYvV5Pkac7kbPgcDgoKChAaWkpLBYLuFwuTeKzs7OhVquxatUqbN26FUNDQ3jqqaco/yQ/Px/JyclUX2gp7+hC13z27FmkpaWho6MDAJCdnU25TCUlJdi5cydUKlXY2A39voXkHZbyzOvq6lBWVoarV6+GaSPdLiKvhcFgIDY2FitWrMC6deuohdFCsVRZB9J04vV6F+xmXuxYwWAQGo2GqpITK6VLly4hISEBRqNxSckQgLC5LTU1FTweDwkJCRSJi4zlmEjfrmHkv3p8iRAtIe43hOjYsWNwOp0YGBhARkYGZDIZHnvsMQwODuL06dMQi8XIz8/H5s2bl3S80G6OOy3fREKxWq0WN2/eRGZmJqampvDAAw8s6RxCd19OpxNXr15FYWEhpFIpRYjkcjk8Hg9aWlqwYsUKJCUlwWg0ztv1LBcBsdlsOHv2LHbv3j1vF+ZwOPDxxx+juLgYOTk5UXdihBjM4XAoQrTQrvpex71Emm53rE8D1SLoJjCnnltSUoKamppP7fuAT3bLfD4/KtGVjGmj0QiHwwGz2XzHCFFPTw/8fj9ycnLg9XohEAgQCATg8XjmKY+TWAr6s5wSJUGIysvLkZ+fDyaTSY9PvOrkcjnEYjHVybpXJV2CEG3dunXBjrA75bTd7bgIBAI4c+YMbt68iV27di1Jj4eMHR6PR7ltdrsdJpMJMpmMlhjb2tpw6dIlHDx4EAqFYkllqlCEaCmJ43IQIhKR5WqdTgehUPhH25x9mvFlyewex/2WEBmNRvzyl7+Ez+dDQkICDh06hISEBOrIrlarsXr16kV1JWZmZvDGG29g3759sFqtaG1tRVJSEtavXw+pVLrslyISig0EAlCr1eju7sbmzZsp0fhOj0eCcCb6+/shEongdDqRkJCA1NRUOJ3Ou0o8jhw5gsrKSrS3t9P2X5LM1NXVISkpCSqVCiUlJUhLSwOXy8XIyAi4XC6SkpLQ1taG6upq6jrOYDCi8g6CwSAsFgs0Gs2SJ73bxWJlkrNnz2LTpk0UOVss/H4/Ojo60NjYiH379s1TVv60IhgMYnBwEDdu3EB8fDy2b98eVja7m/D5fGFE68jd8UILyr0qL3R2dsLv98Pn80EikaCgoIAqZTscDqqqbTQaqZEtaZognBSTyURLMUsl5S8UoV1cxKYiJiYGarUaQ0NDyM/PR0pKSthnP4uSbrQxvBjJmHCTBAIBGAzGZ4p8aLVasNlsaDQaJCYmwmq10i7SyA65z7pM5fV60dnZiZmZGdpUEhmh99rhcIDD4cDlckEsFn/mXMZPO74smX3BY3h4GD6fjyrmkt2mwWBAdnY2Nm7ceFuRrTfffBNVVVU4cuQIWltbwWAwoFar0dvbe0cTXyQUy2QykZKSgurq6kXPJZpODLCwNg5RDy4oKKBWCikpKVQ3icfjUfuD5ZYAd+/ejfb2duzevRtAuELy2rVroVarkZeXh8TERPj9flo+IbvfgoICtLa2zjP4JAkRCYfDQcX9iPHm3cZiZZLCwkJcuXJlySJ5DQ0NyMnJwYkTJ+7Jud0uyCKXl5eH3bt3Y9euXfcsGQLmShCkSyea1pJWq4VWq6WJC4nQMR1N9XmpQdzlhUIhtdUA5pAwLpcLo9GIqakpiEQiqvLO5XLBYDBgNBqh1+shk8moR1nk+Frurp4kFAwGg5YKCYE8IyMjzNA59PhLfZ+I+CQx2b7dvQsGF/b4iyw3OxwOsFgsXLx4keqlTU1N0aTjs4q4uDj4fD6kpKTAbDZDIpHAbrdDp9PB5/PRTlbC9ZTL5Z9ZstbX1weVSgWRSISrV69G/UzofBEXFwePx0NLzP+V48uE6HMYFRUVKCsrg9/vx4EDBxATExPVnX6xCezZZ59FW1sbDhw4gHXr1oHBYFAuzEJBeB6dnZ1hyt/A3CTY39+P0dFROvGFJjU2mw1HjhyhqrskQpOO0AU7GlcE+GSClkqlqKioQGxsLFwuF02EXnvtNfzud7/DxMTEskULRSIRDhw4QJEcsitlsViIi4vDI488gtLSUng8HjCZTOppJRQKsXXrVgwODkIul+O1116D2Wymv4v0GBMKhUhOTobZbA5bIO8mFloYd+/ejYGBAWzevJmeQyAQwPT0NJqbm2EymcLGR2pqKrZv346xsTE88cQT9+TcbhfkOblcrgV3+YRMG8pTW+oCTdzVSfIduTATsVev17sgD+N2PI3QcyHnOjo6SkVnKyoqqAUGMOfHRlDG1NRUqoAuEAiQmJiI6elpCIVzPmppaWmUg7LcJD+aCjlZDK1WK371q19BqVTCbrejtLQUVqsVpaWlUY8V+T5FJj7ku/r6+tDf34/z58/D6/ViaGgIly9fxtDQ0ILHVSqVUCgUGB8fDxvDpPREOtyEQiEaGhrQ2NiIoaEh1NfXIzU1lSouLyU8Hg9u3rwZxlNbahAivt1uh1AohFgspgR5h8MBLpcbxkdbDr+HxN1wOYG5BDw9PR02mw0bN26M+pnQ+YI4O0SaKf9XjC9LZkuI+6lkRl4W0gJdU1MDHo+HsbEx2O12qFQqbNu2DVwud9n1f0IiXaiWbLVa0dHRgaysLBgMhrD2YTIxBgIBxMbGIiMjAy6Xi2qbHD16FHw+HzabDc8++2xYh8pyODaRbcB8Ph86nQ5KpRJ1dXVwu92QSCQAgO9973v3BO4nO9jBwUEUFBRQQUrS9RLaAfXRRx+huLgYAwMD+OpXv3rH3/lpBtGFIiXH0tLSTwUmX2q5ZSmcJZ1Oh0AgAC6XS0vFyxnfpEuMWHOEtth7vV709vZCJBLNWxhIOS0pKYl215GyWrSxSBZDlUpFSds+ny/sXVksbldeWe47vVib9f/8n/8TbDYbHo8HL7300m399SKfE+mQU6vVKCgooN915swZBAIBaqVx69YtyOVyqNVq7Nu3j+pDkWMBc3Ps+Pg4iouL53G0Iq/hP/7jP6DT6ejvX3755dveBxJ+vx+vvvoq1Go1cnNzw+aipYRWqwWHw6GcRbLZIVpPTqczrFx2J/yeT1uK436UZvk048uS2Rc4HI45nx9i4XHt2jUEg0HI5XKoVCoUFxfTLpLl+OQEAgH09/djfHwcnZ2dsFqt8z7jcrmQn5+PoaEhqtBMgnRQCIVCyjvp6upCRUUFurq6oFAoYDQakZycHIbYLNS5stj1k10qn8/H+Pg4bt26hf7+fhQVFYHBYMBsNiMtLQ0ej+eekAQdDgcGBgaQmpqKwcHBsHsaaq8RFxeHzZs3o7+/H4899tiixyR1/kik7V5GIBDAzMwM+vv76S4eQJjPVEFBQdj4IMTwkydPLhlZWyiWitDdruzjcDioFY/P56NIwFLHN3GRJ/8dyYvicDioqKhAYmIi7HY71R4CPtnhz8zMULNjgoTYbDZ6faHnwmAwkJycTMn/BEmMjGhj4HZdQMt5p4GFkVYAUCgU8Hg8VLrjdhH5nEhbOEE5yXft3LmTdn1mZWVhzZo1GBkZgcViwalTpzA1NUU7uxwOBxgMBiQSCcrLy6MS1iOv4amnnqLJxZNPPrmk+0BiamoKarUaADAyMrLsMU7KS/Hx8TQZIveFyJ+EJtQsFmvZJtnLfcbLjeUi5/+V4kuEaAlxvyFERqMRV65cAfAJPOpwOPDaa68hJycHW7ZsWVSbI1potVpoNBpMTExAJBLRCSp0pzw5OYlAIBCWbCwWHo8Ht27dgkKhoKTjkpKSMNL2cjsqIhGZkZERyGQy9Pf3w2azUefxmJgYcLlcVFVV3fFOiJQ+RkZG0N7ejoyMDGzYsAE+ny9MKO526rXRor29nY6pysrKZZ/bUoI80+WoAWu1WjQ2NiInJwcTExNhIn3LjXu1E73b4wwODkIqlcJgMIS1LhOyNZfLRWZmJvR6PTVdnZqaom3vMzMz6Ovrw549e5CUlBSmhSQUCu9Y56qzsxOZmZkYHx+fhyCFEp+XktTfyT0ymUz44IMPsGbNGhQXF9920b5TArfVasVrr70Gg8EABoOBqqoqbNu2DYFAAC6XC/Hx8dRzMRpCRMLlcqGxsRGFhYVITk5eNieHJLINDQ3o6urC5s2bsWXLljsem+R4DAaDls0+D/ElQrRwfD6e4JcRFm1tbRgdHcXIyAg4HA6CwSAOHz4Mn8+Hrq4uDAwMLMnlnXAqrFYrXeSTkpIQExODjIwMikYFg0FMTU1BoVCAxWLB4/GE7V4ia94mkwm//vWvUV9fj/j4eCQlJWFychLr1q2DTCYLK5f19vYiOTl5QXJxJAfC5/NhZGQE09PT1I5genoaq1evxpNPPolNmzZBLpeDxWKhpKSE7kKX6+hOkJLJyUm0tbUhMzMTWq0WPp8vbHcVyhEKBAIYHR0N4xAtFEQqILRMEclDCUWf7mTfEhcXh/j4eDidziVzleLi4rBy5UqMjY1F9RlcTtwrjaW7PU5OTg4MBgNkMllYx83o6CgCgQAcDgd1hvd6vRgeHoZGo6El4ubmZqSkpODcuXPztJAWMgDl8/nUrHohFLC4uJhyZ0LHJSkR+ny+MEkM8rvIcUE+z+PxlrzrDwaD1EA7MzOTlvoWG2t3iiywWCyaDAWDQWRkZCAmJgZutxtyuRx6vR5KpRIJCQmL6jw1NTUhKSkJvb29d0SgJuT0TZs24eWXX8bWrVvvamxOTEzg6NGjuHXrVhh3836PP6b22f0eXyZEn7NwOBwYGxujfJ3r168jGAxS/zIul4uurq4FE6LQri6iwDs0NEQJiwUFBSgsLASXyw0jOxN7g9TU1HkvE5ko7XY77HY73nvvPUgkEjQ0NGBsbAw9PT3o7OwMq/uTvyssLIRSqYy6YAeDQXR1deHVV1/F1atXqehkTEwMNBoN6urqkJ+fD6vViri4OBgMBkilUqxfvx6bN28Gh8OB0+mk3CgOhzPvHKLdm9nZWeh0OiQmJkIgEGDlypWYnp7G3r17owrFkfuh1+tx5coVFBcX49ixY4s+R4VCgfT09DAZ/tAFx+FwwOPxoLGxEadPn170vBcKJpOJpKQkFBUVLYq+hS6ERAn84Ycf/sJ0nLDZbNqJGErqz8nJod1ABLUUCoWUU8XlciEWi7F+/XqMjIxg48aNEAqFtKzFYDBgMpnQ1NSE1tbWsMRnamoKRqMRwWAQp06dikrg5XA4SE1NpUkBCVIi9Hg89JxCf0da8UPHikwmg9FonPfMFkpyon3H7Owsjh8/vuDifielHHK+zz77LAQCAXbs2IEVK1aEHS8uLg7Z2dnQarULlhcBoLa2FjMzMygpKVkygTo0Fisf3gmRub6+HgkJCVAqlZiZmVn2+dxt3C35+k6/z+/3Q6PRYGxs7I66Lu/n+LJktoS430pm09PT+O1vfwsASEhIwAMPPACxWIzLly/DbDZj+/btUCgUcLvd88iZZrMZly5dQkdHBy3VEH0WsnMjyU40DR2r1YpTp05h8+bNiI2NhUAggN1up47gBME5efIkiouLYbFY4Ha7kZycDAaDgf3794ddy2LQrd1ux69+9SuKMD355JOQy+XUE43oERUVFYHP5y+oWcRisWiyE82TKrQ8YbPZEAgEwGKxaOfKUqHwQCCAsbEx1NfXY9++fRCJRPMIlYuRZkPvh91up+apycnJsFqt2Ldv35LOY7nxx/RTWyzuppwaOZ4GBgZo0lBYWEg/a7fbwePxwGQywWQyIRAIoNPpIBAIwGQyweVyMTo6iuTkZFqOJWG329Hd3Q2HwwE2mw0+nw+FQoGenh7Ex8dDIpGgt7cX69atw8TEBBWaDI3Q8cBgMGC1WmE0GiGXy6N2/ZDzJgRekpwsZikR7dlGu1dvvvkmpFIpjEYj9u3bd0/ERCO/x+12o6WlBYWFhbBarYiPj4dYLJ73d2NjY3jttdcgFotRXV2NyspKSCSSTw3ViLxPfr8fKpUKAoFgQU6XxWLBsWPHkJ+fj1WrVi1bsDM07kSr6LN+b8n3TU1Nwev1Rm1QuB/jy5LZFzgYDEbYTlSr1WJsbAwsFguHDh3CM888Q32/zGYztFpt2N+PjY2ho6MDPB4P7e3tVJBLKBTOm4BDiYIkPvzwQ/T39+Po0aPo6uqCRqMBMKf/w2AwoNFokJmZiUOHDsHlciEzMxNpaWmYnp5GSkoKfD4fent70dfXB6vVuuiEKxAI8PDDD2N6ehq7du1CfHw8bblPTExEamoqKisrqa+WXq+f114cSnxsamqiXSAkvF4vmpuboVQq4ff7wWAwqISA1Wqdt/tarPTGZDKRk5OD559/HhKJJGrLbagUAele6+jogMPhQFdXF9WfiYmJQXZ2NsrLy2E2m2+r9H038WmTOO80Qi0MlhKLlXSIpUp2djZmZ2cxPj4Ol8uF6elpSuwl6E9cXByMRiP4fD44HA7S09NRX19POXQkhEIhCgsLwefzwWKxkJSUhI6ODkpmd7vd2Lt3LyYmJlBRURH1nAnaFAgE0NXVBZ1Oh5iYGOj1eqhUKlgslrAxGErgJckb2bhEQwwWerbRyiZ5eXkwGAzIzMycJ4XhdrtRX18/DwkDPpHcIO/QYt/T0tKCgoICXL9+HbGxsVQ/LDLefPNNAKCocFdX16dKAhYIBDAYDLSkOjU1BTabDbvdvmB5LjY2Fs899xxqa2vvKhkCFtZdWyw+6/eWfF9qaipEIlHUBoXPe3yZEH0OQy6XY82aNQBAtYOIq3co2hEMBuf5TZWUlGD16tVwu93YunUrZDIZ8vPzqcpqaESbNCcmJiCVSjE7OwuxWIzZ2Vmq1SMUCpGXl0d3hAcPHkRqaiosFgvlRIyOjsLn88FisWBqagpWqxWDg4PUGDF0wXE6nUhJScGf/dmfobCwECKRCGq1GgkJCdQviFxrqA4TWRgA0PO/fPkySktLcf36dTqxBoNBtLe3g8ViYXJykorjTUxMQCaTYXp6ep5+yHImrtAyI/k+LpeL/v5+6sGkVCqRmJiIuro6ZGZmUg4FgzHn2VRRUYHHH398QRfx23GilvK5+5VTQLqYUlNTce3aNbjd7kU/v9gCwWazUVBQALPZDK/XS59DYmIiVYwm78zIyAji4+PpTvjw4cNoaWlBfX09VCoVTTh8Ph/Gx8ehUCiwevVqyGQyVFRUgMlkIjk5GcXFxeByuaipqbmt0OTo6Ciys7NhMBgwOjpKNYkMBsOCiYBer4fH44FKpQqzIYns4ox8tguVWggSs3bt2rB3GphLZJhMJoxG4zyez8jICFwuFwwGAz744AP88pe/XDDRWbVqFQYHB7Ft2zbY7XakpaVF/dyzzz4LABCLxcjJyUFJSUnU0hAZ39E2L9EiEAigubkZL7/8Mtra2ujfOJ3OMH++1NRU+Hw+xMTE3FF5brmxWDlvofis31vyfSwWC4mJicjKylpW99znIb4smS0h7qeSGfDJhEZk/wnESkpExDrDZDItSf9iOVoZs7OzePPNN/HAAw/A7/ejpKRk3u6IwM1+vx98Ph+vvPIK/d3/9//9fxgcHASDwUBqaiqmp6eRmpqKvr4+VFRUhOmlkPKVXq+HQCAAl8vF4OAg9QTS6XSIjY2l+ksXLlzA5s2bUVNTQ0sKoUrRdXV1WLt2LS1P2O12sFgsNDQ0QKFQUCsOgUAAHo+H2NhYJCQkhNk93I0MP2npFovFMBgMyMvLg9VqxdjYGPLz8zE8PEy7bEhXn0QigclkQlxcXBiCt5i+TGgs9XP3W4SWWhobG1FQUIDBwUGsX7/+ro5L+GSEd9ff30/LUy6Xi5oIE+5VT08PTp8+DS6XC4vFgtjYWBw6dAhpaWno7OxEMBjE5OQksrKykJWVNQ/xDC37RdpuhAb5HOEtmUwm+P1+JCUlzSvVkbBardDpdBShIgT623Wm3Umpxe12o7m5GTExMfPa47VaLa5evQq3243p6WkqOvrSSy/dk46mxY5BLDRClZaj6ZqROZOIt5L44Q9/CAaDEVaCvN82BncTRCXcZDIhJycHsbGxX6jrW0p86WV2j+N+S4gCgQCmpqbQ3t6O2tpausDfaVvsrVu3UFdXB7FYjK1bt1LPpNv93ULfNTExgYSEBAwNDcHr9WJ6ehotLS1Ys2YNNmzYELawk4UgKyuL7pBIokG6Z/x+PwKBAPr6+lBUVISpqSnU1NTQZKKvrw99fX1ITEyERqPBn/3Zn9GJnpTB/H4/hoaGUFFRAS6XGyZCSXbiLS0tVMcoKysLmZmZsNvtEIlESExMvO29NJvNOHbsGB577DEqDhl5z2w2G9RqNbKzs+H1ehdckCYmJhATE4OhoSHExcWBzWZDoVDQzy81MfusfZTuVYQu2mw2Gzdu3EBubi6SkpLCrmMxMVEyfurq6sBkMrFlyxb6roR+xmKxYGxsDB6PB2q1GjExMeDz+diwYQO8Xi9u3LiB5uZmWCwWrFq1CuPj4/jWt74Fr9eLS5cuUYE+8t6EPtNQ8ULiXG40Ghc0dCUcvOTkZCqcGfqOEaVkt9uN1NRUuN3usAQo9L4RwcDlyAIsdQ4JHVdNTU1IS0uDUqmE0+nE6OgonnrqKcjl8iUlX3eTNIUiYyKRiOob2e122sVGxhDp+GtqaqJ//9/+23+Len5kUwfMoUVkU7KUd+jTeOdcLhcuX74Mp9OJXbt23bZMRhLAqakpmM1mcDgcMBgM5Ofn31c8wc8ivuQQfcFDp9Phxo0bSExMRFNTE4XIDQYDXn/99QXh6mjhcDjQ1NREu6Q6OjqWJDNP0CitVjsPqialosTERNTX10MoFOK5557D6tWrodPp6PkShWCxWAw2m03LfgTOJ/oeMTEx4HA4qKqqwuTkJOLi4hAMBsFisTA8PIxAIACBQACNRoM9e/ZQ7zTCISKqurm5uWhra4NWq6VaMnq9nmqhbNmyBQ6HA+np6VRpmxhHLhZk8jl69Ciqq6sX7DBjMBgQi8UoKCiA1+sFn8/H7OwsNBoNAoFAWCkjISEBTU1N0Gq11B3daDTS0tHtBPxILPVzt4u78fK6kwgtf/F4PKSmpuL06dO4evVqmMikXq8Hl8uFzWaDw+GgXYKknb6hoQF2ux1WqxV1dXX0d6FjzGw2o6CgABwOB8nJyfB4PJQAzeFwsH79evz5n/859u/fj7GxMSoGyOFwsH37dojFYmRlZcFkMoHP54ddR05ODkX6+Hw+jEYjZDLZgmUwt9uNvLw8MJlM6qYe+lm9Xk85TyR5E4lECAQCUCqVGBgYwOzsLAQCwYKcqoVKLdHa98lzt1gsOHr0KMbHx+Hz+dDT04Nr165hZGQEGo0GSqUStbW1ePDBB/Gtb30LcrmcHvfkyZPzrjO02/VuhALJ+CYoGhk3LpcLXC4XU1NT4HK5aG9vp+MmOzsbALBnz555ZVbyDk5OTsLr9cLj8aCvr29Z/J7Fyup32hnW1NQEs9mMQCCAurq62x6XlIATExMpaT07O/u+4wneb/ElQrSEuJ8QomAwCKVSiRMnTsBsNqO8vBy7d++GQCDAz3/+cyQkJECj0eAHP/hB1AmPwMkA8PHHHyM7OxuxsbE4ceIEpFIp1q5duyhCFMpRslqtYeqshEza2dmJwcFB+P1+VFVVobm5GS+88AKcTidkMhnlJnR1ddF239TUVEgkEgwMDCAtLQ1SqXTeTiYYnLNESExMhNFopPwJIv6YkJBAW6YjJ3yPx4OOjg7q4k30lEjnCtnZh+7uACxpp0d2wVqtFufOnVsQIYoMrVZLfdHYbDaEQiHdrfb29sJoNNJut9LSUvT19UGhUMBgMNx16Wi5QVC/6elpTE9PY9WqVbc1EL5X4fF48Ktf/QqJiYmwWCzYunUrFZkMRYgAUPVor9eL+Ph4ihC53W7U1tZCJBKBxWJR5IB0FE1NTSE5OXkeEuDxeHD9+nUMDg5i79691A088t2anZ0Fh8OhpclQtCO0bEmS/sVsSkhiEA1NCkWI0tLSwGQyYbfbMTo6ShNCoVCI8vJyxMTEhH3XUro6I7+TPPcPPvgApaWlUCqVWLFiBXp6eqBQKNDa2orHHnsMXV1dUUU8T506hbKysnm/J1pKROohGAxCo9HAZDLBZrOhtrZ2SeOLIDnBYBDp6elwu900GbTb7RAIBHC5XIiNjcWtW7fAZDKxcuVK+Hw+1NXVYfXq1YiLi4PT6YRAIIBSqcRHH30EmUyG1atXg8fjQSaTUfX7pXBmFkOIFkPMFqIuGAwGvP322+BwOJDJZNiyZQsEAkHYOIw8buhc/0UrAy43viyZ3eO4nxIiu92Od955B06nE2azGRkZGYiNjUVhYSEGBgbQ3d2NiooKbN26dd4LZzab0dvbC4VCgY6ODigUCkxNTaGyshKFhYUAltYKz+fz4XQ64XQ6weVywWazERMTA61Wi66uLtTX14PNZoPD4SAQCMDtdqOmpgZ79+4Ne3E5HA4aGxvhcrlQWlqK6elpxMXFQafTobS0dF57PPDJpBEIBGiJzGg0wmQyUV0fMplEuwaCxhBDQ7VajRMnTuCJJ54I29VGhtvtxo0bNyCVSlFcXBzWBn6nkD9Z3NRqNXg8HvLy8qBWq2l3XkdHB13IORwOpqenMTY2Ni8ZuRc8jdsFue99fX1wOBzg8Xh48MEHFy25BINBjI+P4+rVq9i4cSPtXiKTfiAQQHd3N9hsNrxeL3Jzc8M4DgaDAe+++y7Vxzp37hxiYmLwta99jVp5RH53NG4dudektBITExOVLxJtIbt58yZaWlqo9cwTTzwxr/MSANX0EggEFJlcbnlzofsYyUkaGhpCMBhEQUEB3G43rFYrOBwO2trawOFwIJfLkZOTM+8cQ989nU6H999/HytXrsTKlStpAh/5neS5S6VSXLx4EStXrqTcv46ODqxduxbNzc3YunVrVPTB4XCgrq6O/p7cC0JgJiKWLS0tuH79OhQKBVJTUzE1NQWlUoktW7Zg9erVUWUX/H4/Ghoa0NraiurqaiQlJSE3NxdGoxGxsbHo7OxEfn5+1Hb9U6dOIScnByMjI1izZg3kcjkMBgPee+89BAIBeDweFBUVYcuWLTRxu50avd1ux+nTpyGXy7Fhw4aoCR0Zo3a7fd7GjSSfWq02rJX9N7/5DXJzczE0NISamhpcuXIF+/fvR2JiIj2fT5ur9XmOLxOiexz3U0IUDAbR39+Pd999F8Bcu+i2bdugUChouaCioiJMEZrE+fPn0djYCB6Phy1btmB8fBwxMTFYt24dRURuV/OP1EEJ5S8EAgGcOnUK/f39cLlcKC4uphou09PT+Mu//Muwv9fr9ZiYmIDX60UgEEBJSQkmJiaQmZm5IPmPnJ/NZsPMzAxiY2OhUChoecHtdlNeUDQeBUEU7HY7rl27huHhYaxcuRJKpRLf/va3oxp3MhgMXLt2DWw2m4pULsUGYylBCI88Hg8WiwUCgWBZVhuh9+Ru9EhCkwmBQDBPgoEsbH6/Hz6fDzweD5s2bYLBYIBcLodQKMTY2BjOnz+PlJQUbNy4EX6/H6dPn4ZCoYBGo8HevXvhdDrppG80GqHVajEwMICioiLExcWFcRx+85vfQCaTYWpqap4uFClb3c6QmCxSDAaDlkgir9tms8FgMIDP51M0gBDQl4oQERJ8KAL6aSxMg4ODtBNKIBAgPz+fPjeZTAa1Wn1bDaNAIIDf/e53iI+Ph0qlwvbt21FaWhqGJoTKY+zduzfq5uROIhrJX6vV4qOPPkJ8fDyGhoaQkZGB7u5u+jcHDx5EaWnpvHujUqlw6tQpxMbGQqfT4dChQ2Cz2ZDJZGhuboZcLse5c+eQn5+PHTt2hCUoZDyT7sCBgQHU19ejrKyMIs6PPPII1VlbCtJy7NgxyuPKzMxcEMUlBrEul4uOJ2BxhOjdd9/Fhg0bcO7cOVRXV+PWrVv4i7/4i3tKBL9f9cjuNr5MiO5x3E8JEQCcO3cujBi4evVqZGdnU32ehXahoa7Q2dnZyM3NRV5eHjQaDdVJCd3BhZpVRr6sC708brcbTU1NcLvd2Lx5MywWC9566y08/fTTiI2NRX19PYaGhqDVavHUU0/B7/fTltesrKwl8XVIScFqtYaVR8hECXyy0yX8hNDz1Gq1+PjjjzE5OQm32w2fz4cdO3YgLS2NttmSsiCDMSdKuRhCtNQgZcbp6Wnk5uaCzWZTjobL5UJubi4MBgPVyyFoUX5+/m2VppdShlms+4golxMhS2K4C8whi//yL/8CABCJRCgvL0d1dTUVurRYLGCxWDh37hxtvy4sLKREdplMhh07dqC4uDgMIRofH8f7778PoVCIuLg4bNu2DYmJifMQovz8fNTU1KC+vh4GgwH79++nvAiyuLjd7rBzJnHs2DGYzWYwmUzI5fJ5ZR3S+RcbGwur1Uqf/3I5V/eimy9ULFIul8PtdoeVPxwOB6RSKUZGRihCRLhGyxX002g0+OCDD1BdXY2qqioqtEfQ3sHBQdy8eRNpaWnQarU4cODAks9/sQU6GloWCAQwPj6OhoYGPProoxAKhfjpT39K/2br1q3YvHkzANDmh2AwiI0bN0KpVOLatWtYv349cnNzMTAwgKysLPD5fPzyl7+ExWIBAGzZsgVbtmyJek6zs7N44403YLVaIZPJ8PTTT9NnSIQkV65cCZvNtuh9ttvt+OijjyAUCrFjx44w499IJJJ0BxKeokgkgtfrxfXr12mpfPv27VQXye1248qVKxgZGcHs7Cy++c1vUvL/vUpgvkSIvkyIlhT3W0LkdDrx9ttvY2JiAqWlpXjkkUdgMBigUqngdDqxbt06imaEDu6RkRG88cYbiIuLw6ZNm1BUVER3516vF2w2Gzdv3kRWVha4XC6trTudTphMJoyOjiIvLw+5ublLfnlOnjxJIW2i8aNUKimn4bnnnkNMTMw84m+oUiwRyovsQLtdeYR0FEUmSnw+Hz09Pbh16xYMBgPWrFkDn8+H9PR0iEQiKlhZVlaGYDC4YEfQUsPv92NsbAw6nQ4sFgvp6emUyLtQECIr6Q4J5czcyQJIuu2IoN/U1BRaWlqQnJyMxMREpKenw+VyUYQolHj7+uuvQ6lU0uN9/etfh1QqBY/Hw9TUFORyOQQCAQYGBnD06FEoFAqYzWZ4PB6qXv7888+H2ZQAcyKfPp8PGo0GVVVVWL9+/R11Gd0tQmQymdDT04OsrCykpKTc9r6SRXLVqlXgcrl0TBkMBvpc7mRxIc+JfAcZd2azGa2trbDZbKiurg4Tw9NqtYiNjYVGo0F6evqi30U2NSkpKbBYLGhvb8e6deso6kcWViK6txSEyOfzoa2tDVevXsX27dtRUlKyJOPnyIi8XyqVCq+//jpKS0vDGiXq6uqg0+moyfT69etht9vh9Xrx1ltv0bmwvLw8bAMol8uxbdu2MBsbn8+H9vZ2NDQ0wGQygcViwe/34wc/+AFNuK9du4aCggK0tbVhy5Yty5K5COUERrsfkZIBvb29UKvVmJychEKhgFAoxJ49eyhC3dnZiYSEBNjtdnz1q1+9qwTG6/Wir69vUTT+ixJfJkT3OO63hIhMVunp6TTx6erqgt/vh1gshkgkQm5ubtSXMXRBJS3FfD4fUqkUKpUKqampmJiYQFlZGcRiMex2O6RSKc6ePYuMjAxotVqacHV2dtI29oUidFL60Y9+FIYQ7dixA1lZWWECaGQxmpiYAJPJhMvlQiAQoG35S9l9R+7WQxESUkojJbvY2FhK5J6dncXExAQCgQCdgDdu3Eh36ncaY2Nj6O/vp11mIpEIJSUl81Cf0AmO7JotFgtKS0up7stiSAT5ez6fD51OB5fLRYm3oQiRRqPBiRMn4Ha7wWKxUF5ejvT0dMrx0Wq1VNsmJiYmDCEC5uxiXnrppbCxRRBDg8GAhoYGlJaWoqWlBYODg0hOTsZTTz01790hyYpYLMaWLVvmdWgZjUa8//77KC4uxtq1a6OqAd+p1IRWq8WVK1fw4IMPQq1WU9+7hISE244xskgODg6isrJyHpmVIJjL3b0vhOS1t7djeHiYdtxt376d/k0gEMDk5CTtjgv9LqIgPTExga1bt0Kr1dJy5cjICIqLi9HX10e7K2+nXxQtBgcH8cEHH9BS9RNPPIGMjIxlL7ChiDMAXLhwATt37px37/R6Perr68Hn81FWVoa0tDQEg0G89tprkEgkGBsbw5/+6Z9iamoKfD4fhw8fBo/HQ3V1NTU5LigoQDA455N46dIlilYymUzs378fmZmZ9PyXgxABc0knQdd27tyJ2NjYBcdmpGRAKELk9/vxwAMP0I0JQYhUKhUOHDiwpKaNxYIkVxqNZl4r/r1EipaKXn+aqNSXCdE9jvstIerv78fIyAhu3bqFjRs30p3hwMAAFAoFioqK0N3djYyMDFqCiCzVAHO7raamJng8Hmq4yOfzKQeJdI7p9XrExMSgubkZFRUVkEgkuH79OoqLizEyMhLVo4nEmTNn0NzcjKysLDz99NM0eSIKs6S7I5K74fP5MDw8DJlMFhUhIjE0NITDhw/jwQcfRE1NDRWlDEVRIifb0JePiNu53W5kZGRgdHQUarUaQqEQNTU18Pl8d/2yKpVKTExMwOFwICMjAyUlJWGkVdIlEx8fT9vrSQkwclFdCCHy+/0YHh5GTEwMxsfHcfHiRfD5fOzduxelpaVhJT8Wi4XW1lZMT09DKBRiy5YtyMjIgNPpxLlz55CbmwuxWIzu7m488sgjEIlEmJmZwauvvgoOhwMmk4lvfvObYagIQeIiNXFOnz6N4uJi9Pf3Ry27LEQkDQaD+PWvfw2xWAyz2YxNmzahvLx83t/fCe/BZrPhxIkTKCsrQ29vL2pqajA7O4v09HQkJyffEUJExkho08G96vAxm80YHByEVqvFtm3b5iWOkbw+8p2Dg4Po7+9HQkICrFYrtm/fTsuVXq8XjY2NqKysjMo3XM65vfLKK3ST8eyzzy5Jsyvy/EN5OsePH0dFRQU6Ojrw2GOPRf0sQYbJPW9ubkZfXx9WrFiBmJgYFBcXQ61Wo6CgAC6XCw0NDUhKSkJ+fj5mZmYQCASQlJSEq1evoq+vDw899NCykO+Fwm6349VXX6WNBy+88AI0Gg31wgOi+8653W5cvXoV4+PjePzxx8HhcOi7pFar8cYbb2Dr1q1YtWpV1E635Z73YgjRveQS3e5YnwVv6cuE6B7H/ZYQtba24uTJk+DxePB6vThw4AAsFguuXLmCTZs2gcViITU1FZOTkzTRUalUkMvlMJlMtPxy+PBhDA0N0eM++uijMJvNyMnJocgCEP6yAaDiZ6dPn8aWLVto0hU6MY+NjeGDDz7AQw89BIvFgpSUFMzMzNzW4DJUdXsxgTm73Y4LFy6gvb0d6enpUKlU+Na3vhV1dx+KnISWNQBgfHwcIpEIZrMZbDY7rM12IQRmuZMl4QkxGAykp6eHTWiEP8RgMGiyQXb75LpZLBbOnDmDjo4ObNy4EZs3b57X5TY0NAQejwe1Wo1r165R8iwApKWlIS0tjT5/JpMJDodDOwyJ1tPx48cRFxdHUbJ169ahv78fe/bsAZ/Px/j4OC5duoQDBw5AKpXOQ6siJ7dAIACVSoXm5mbs2bNn0a7BSCIpaSWvq6tDdnY2du/eTReJ0LF4J63FdrsdFosFly9fRlVVFZhMJlgsFlUmv5uIbBpoaGjAAw88EFVCIjSBXOxaltpCPTMzg9OnT6OkpASrVq0CgDCEiPBRyDFnZ2dpt5NCobijBICgbUQFPj09/bYJJZHnGBkZwerVq+Hz+cDlcjE9PY20tDQ4nc4FEaLIIO37Ho8H/f39yMrKQkJCAsbGxhAfH4/jx49DoVBg+/bt4PP5mJiYgM/nQ1dXFwYHB/H4449DLBZT418Wi4X+/n7w+XyUl5dH5e4tlICazWYcOXKECjpKpVJs27YNcXFx6OzspL6OMTEx9B2x2+04d+4c7ThNTEykc9TKlSshFArxu9/9Djk5OVAqldi1axcaGhrw4IMPIjc3l25ECBdTKBRGNcuNPPfFxtG9RHW+RIi+gHE/JUTBYBDvvPMOBgYGAMx5k+3atQuvvPIKbDYbAGDbtm1Uu0YqlQIARYjS09PR0NCAzs5ObN26Fa2trVCr1Thw4ADMZjOys7OhUChomWghSP7atWs06aqqqgKfz8eFCxdw/fp1bNiwAbdu3UJJSQk6Ozvx3HPPYXp6+rbltcjrXIwY/eGHH4LH42FgYAAWi4UiRIFAgCZ5kWRkUrO3WCwU1o9ckKMlZ6HdOcFgEC6XCwkJCffkBSb8os7OTnR0dODJJ5+EUCgMS0ivXbuGCxcugMvlwuv14qmnngrjH9ntdnA4HCiVSsTGxsJiseC9996jfBQOh4PU1FSkp6fD7/dj9erVGBgYQHp6OjweDzIzMwHMkUs//vhjlJaWQqFQ4Nq1a9i8eTPi4uKWpJsS+sycTic++ugj+P1+7NixI2qi4XA4cPHiRWRkZKC0tHRekke6v0L1XyLRvjvZXYaep9VqxeTkJPh8PrKysij/x2q1YmpqipKXl0JqJ3IOZMyeO3cOxcXFGBgYmId0ECVllUqFrKwssNnsBdu7g/9PMFEmk9HSVGhSSBCkW7duISMjA2q1Gps2bVqUo0aI1X6/f54K+p3cx+W8C5OTkzhy5AhMJhNWrlyJhx56CJOTkxCJRPD7/VHHSiQv0GKxYHx8HEVFRdQypL+/Hzt27KD35c033wSLxaII7OrVq7F27Vo0NTWhqakJWVlZmJqaQkxMDFavXo2pqSnweDwIBAIwmUwkJSVFvYeh5eFQPatf/OIXsFqt8Hq9YDKZeOGFF5CUlIRLly5BKpWCyWQiMTERMpkMLpcLcXFx+PDDD8FisWA2m8HlcuF0OmkZu7+/H1u2bIFOp8OpU6ewbds2XL58GbGxsTCZTPja175GeUpTU1MQi8Xw+XyLJvWLITJLfZ6ft260LxOiexz3U0Jkt9vxm9/8hu6MSkpKkJmZic7OTkxNTYHJZCI1NTWsRZjsBi9evEhLIQqFAhaLBc8++2zYC+T3+zE+Po7Z2VmsXLmSXnMkaZOUDYqLiyGTyaBSqfD73/+eHmfPnj04c+YMioqKUF1djby8vAWv6XZE4dAX1e/3Y3R0FCMjIzCbzUhKSgrrHhkcHKRKvgKBgPIFyC7KYDBAJpPRbprbnQ+DwYBOpwOXy4Xb7UYgEJinM3O3MTg4iLfeeotaPLz00ksQiUQUUXG73Th27Bj6+vqwevVqPPDAA7fVQfL7/ejt7cW5c+cglUrx8MMPQ6FQ0PsXzSolWiw0SZIxRYQASYKSkZEBFouFU6dOQavVgslkQiwWY9++ffOOferUKYjFYuj1epSWli5JZiAaWrncBTm02y8pKQkMBiMsySXeeBaLBXw+H3w+/7bJBdmhCwQC2nav0+koQkQQo1DSdVdXF30PSft8NBTIbrdTBIWMEbJBAYDu7m7I5XJMTk5idHQUVVVVyMvLo3wxnU5HPfJIEETCZDIhOTn5joi1d7ownj17FtevXweHw4HP58MPf/hDqiy+EMIRSVYeGhpCYmIiZmZmqGJ9Xl4eVCoV7SQ0mUx45513YDQakZubC61Wi4qKCios2dDQAKfTiZqaGrS1tWHbtm1QqVRIT08PQ4giEWaCJkV24v7617+GRqMBAJSVleHRRx8Fk8nE7OwspqenIZFIkJmZidnZWQwPD8NisSA5ORn9/f2Ij4/Htm3bwGazKfpdW1tLxwMpQx85cgTd3d3Iy8vD008/TRGiaOh3tK7WxZKepT7Pz1s32pfWHV/gEAqF2LdvHy2HqNVqsFgsrF27FkVFRUhLS8OmTZtgNpspMdjhcODo0aMYHBxEa2sr0tPTodfr8cADD1AbDNJ2rdfrodFokJCQgI6ODsTFxdG26ldffRWTk5PU92ndunWQSqXQ6XRITk5GUVERAKCwsBBcLhcbNmxAfn4+xsbGosrV+3w+DA4OQqPRwOVyYWhoKKore2jH0+joKCQSCdLT02kpyO/302vIzs4Gj8cDl8tFTk4OvX5CEk5ISAhz8SYRCAQwPDyM//zP/0RbWxvEYjFmZmZw+fJlAKATn1gspigAsLgUP/kdsYoIBAKwWCwYGBgIs5/IycnB5s2boVarsXfv3nk2BjweDwqFAi+99BIEAsE8tCL0/pBgsVhYsWIFvvnNb+LFF1+kZU3i+n47xGNmZgZvvPEGVCoVent74fV6wz5jt9uhVquh1+uhVCphtVopYgTMtUrHxsaCx+PhgQceiPo9W7dupSVa8qxuF6HXGu26lxIOhwNTU1OQyWSYmZmhnlfknguFQiQnJ4PL5YLJZEImk6GhoQFut3veschzJ2OCxWKBy+Xixo0bOHz4MDZs2ICYmBjKw2ttbYXP5wODwUBJSQksFgtycnKovEO06xEKhTCZTDQZSkxMpPwvsoAqlUpUVFTgT/7kT1BYWIiYmBhMTU1Bo9EgJiZmnkO93W6HwWAAh8Oh9jbA8ixaSCJMmgBIgny7PfbmzZupe/0zzzxDNyo8Hg8GgwE+ny/snQoGg+Dz+TCbzZDL5fD5fAgGg5ienqaoCJPJRE9PDzZt2kT/tquriz7f6elpKBQKWtKrrKzED3/4Qzz++OMYHx9HdXU1zp8/j7y8PFRUVKCqqoq+I6RsXV9fD5vNhpaWFni9XjpeyPcVFxdTCYnVq1djaGgIFy5cwHvvvUc5ii6XC+Pj45iZmcHY2BhFEUkyZDKZcPjwYXi9Xpw+fRo8Ho8iZy0tLVQpfN++fWF6VJHJUOQ4Hx0dBbCwbUvk81ws7vS9+zzElwjREuJ+QoiAOV7AO++8Q/+/srISRUVFSE1NhUgkwvj4OFwuF9xuN8rKysBgMPC3f/u39PPZ2dl4+OGHYbPZIJfLaemILDJWqxUTExOoqqoCl8uF1WrFq6++CpPJBKlUigMHDlBo1uFw0HsjlUrR3d0Nn88HkUiE9PR03Lx5ExUVFRCLxRgbG0NDQwPGx8fx7LPPwuFwIDs7G319fRCJRIiNjaVt5lwuF2q1mjrS5+TkQCKRwO/3Y2RkhLrckxc4MTHxjqBg8juHw4EPP/wQGRkZGBsbowRUkUgEo9GIdevWLdjWvdD3RkLrOp2OLkwSiQSVlZWUe0UsSQjHJ3Jyc7lcaGpqQm1t7TxS7UIRem48Ho/et8TEROq3FI0rZbfb8eGHH6KwsBDNzc147LHH5vG/CLpgtVppOQD4BCH6LOJOd6qRO+doLvShz66/vx+ZmZlQqVSora2dR9iPtLsYHBzEyZMnafnqz//8zxEIBNDa2orCwkLMzMwsWXQzUhcsEgm4ceMGBAIBLBYL0tPTkZ6eDuCTTcBCCNH4+Djcbjc8Hg8UCgUUCgWCwSAtxZMS4lLjdnpQCz0Hh8MBrVYLsViM0dFRrFixAiqVCrm5uWE8GyLG2tPTg8HBQZSUlCA2Npa2zjscDnA4HKxatQrFxcVwu934p3/6J/pdKSkp2L9/P7RaLWQyGbRaLS5fvoynn34afD4fv/jFL5CWlgaVSoXvfve71PYkGAxCKBSitbUVEomEjgUWi0W5R2TeHBgYQF1dHVJSUlBZWUnfWaFQCLPZjJqaGtTU1MDhcOCjjz6inYMA8P3vfx9sNhu///3vwWKxoFQqkZycDB6Ph+effx6Dg4M4c+YMYmNjMT4+jhdffBEpKSlUKiPau7wYQhTJf/oix5cI0Rc0SLv9zMwMtS6IjY1FTU0N0tLSKLeAIBNCoRB6vR4MBgOHDh0CMEf0y87OpvolwWAQTqcT165dQ39/P6xWK1paWpCbm0snUCIWyOPxYDKZ4Ha74XA4oNFowOFwMDk5CaVSiQsXLqCgoCDMjJOIiRGpf9Le+uabb6KwsBBKpRKVlZWQy+V0V8rlctHd3Y3Lly9jcnISVqsV4+PjcDgcYLPZSEtLg0KhgN1uB5PJpARksvArlUqMjY3R4y22oyELB5/Px/bt2zExMYGdO3ciLy8PCoUCVquVJppmsxm/+93vcPHiRVquIHwZUmok30lQIZ1ORxczwg+w2+1wOp2w2+30HJKTk6HRaBAfH0/Jn2azGWq1Gjdu3ACTycTWrVuXnAyRcyMJ4+joKF24yYJOSmaRIRAIsH79egwMDODRRx/FzMwMFe4EQBcJiUSCtLQ0ZGdn05/PKhkCQDt5dDrdogidVqvF8ePH0dXVRdEZYnfDZrOjjg9y7+Li4rBixQpMTExg1apV84w7hUIhjEYjuFwuNTrOycnB6tWroVKpaKmQyWSiqqoKMzMzFA1bDF0kodPpwOFwKMJEUE6SgBYUFECv12NycjLMlDUYDEKv1yMpKQnl5eXzJAvS0tLA4/GQkJCA+Ph4ej/ZbDZFYKIF4XZFnndcXBzlGC6EMEReL7nvBIXOz8+HVqtFTk4O7VoL9SMbHx+HRqOBUCjEwMAAuFwuPB4P3bRxOByMjIzAaDTOOweSvBL+zqVLl1BQUECtYbZt24bx8XFs2bIljKhM5keS4FRXV0MsFiMhIYFy20hC1NPTQ7XbbDYbcnNzUV5eDqfTiZUrV6K8vBxjY2OQSCSU30k6T0dGRmC327Fv3z6Mj48jNjYWMzMz2LRpEzweD+XSjY+Po7CwEEePHqXvdlxcHEwmEwKBQJhJNICwcU6eMY/Hg0qlgt/vp5+LfE4WiwV9fX3U6ui/StwxQkRg/t/85jdhkug6nQ6rV6+mEN0XIe4XhGhwcBAmkwl+vx8zMzNgs9nIzMwMa1smKMrw8DCkUikUCgWdPN1uN27evInMzEw4nU6Mjo5i1apV6OrqgkAggFarhclkQmVlJcbGxrBt2zZqfnn9+nU0NTXhkUceoSUdm80GNpsNl8uFkZERuFwuKBQK7N69G+3t7Th16hRFQP76r/8a9fX16OzshNFoxMMPP4yioiIIhXOeVxMTE7Db7UhNTYXH48GVK1eQkJCAgYEB1NTUUIQotJuNy+VCqVQiJyeHvvATExO0tMHj8cJauUMjdKdEOEeRuyWfz4fR0VF6/N///veQSqWYnZ1FZWUlUlJSkJqaCpfLBa1WC5FIBIPBgPz8fMzOzkKr1SIuLg6xsbH0Po6NjWFqagqlpaXUVyuSQK7VasFisaDX62E2m5GamgqNRoOZmRmkpaVh5cqVUTV5FgufzxeGEEWTJyCx3FbZaDvRO42FtHgiO60AzFN1BjAP5fH7/bh48SLtiktNTcXatWvDErdQjhlxSZ+cnITP50NxcTEsFgtNajkcDnp6esDj8VBQUAAWi4XZ2Vk4HA7I5XLKTVsKemW1WqHVauFwOCiiGplQ2mw2WK1W2O12GI3GeY0JwWAQFy5coL5cRNwyVMV9qZ1zkd2QDAYjavdnKPk7WgfoQhENTQMW5hBGjjOv14vOzk6o1WoUFRUhJyeH+uEZDAbYbDasX7+ecv8IrzElJQVPPfUUGAwGeDwe9Ho9VCoVGhsb8fTTT0MgENAOTalUCpFIRG2JACxaHgqdRyYmJlBfX4+UlBSsWrUKYrEYDocDBoOBrh9isZiKl4rFYhgMBuTk5CA1NRVtbW0YHh5Gfn4+bty4gYcffhgrVqzAuXPn0NzcDKFQSOfr/fv3UzQQmENsSYJI5lTgk1IuOX+n04lLly6hpKSE2gRF+qERxJrJZMLn81Hj7GjX/XlAmD4TUjWTyUReXh6kUilOnDiBpKQkAHPmmSkpKUuqQX9e4n5JiAjnxuVywWQyob6+Hhs2bIBcLkdycjK0Wi3KysowOztLYXaFQkFf7tAk4tq1a9DpdCgsLMTOnTvR3t6OpKQkxMfH4+bNm6itraXoQSRpj7R5k5fP7XajtbUVHA6HkgMnJycxMDCAxsZGAMC6deuwYcMGSoYMTdS0Wi1mZmZgMpnoOQsEAly5cmVB00gguiI12f1Ea3EPjYW6lSI7eEJfepPJhCNHjlA0hNzz9PR02Gw2qNVqZGdnw+v1Qq1WY2JiAiwWCxs2bKBJwkKLAgmyA9fr9ZDJZLDb7ZS3FRcXB71ej6KioqiaPCThDe0uXOhaQst0FotlHtS+nFZZu92OiYkJulO9G5+3SFXt0Jb00IWYPDOHw4Hp6Wm0t7dj9+7dYWT30HJoY2MjJBIJqqurYTQawxJl8p0GgwFcLhcqlQo+n4+WFVatWoWxsTGIRCIMDAwgLi4OPB6PNjAQQjWDwVhW9+H4+DjMZjOsVisSEhLA5/PnJfDkOU1OTiI+Pp6WXkgQaYOuri6sXbs2zJOQaPUsVdE8UkaBlLImJiaQl5dHyeChiWm0DtCFIrJbLlLUM/IY0cZh5L8tVZIg9PtJ5xnp7iPjyOVygclk4u2330ZGRgadW0Pvc3NzM2pqapCZmQmPx4PLly8jJycHycnJEIlE9H7o9XpwuVw0Njbixo0b4HA4eOihh9Da2kpLmSQZeeSRR9DW1oaOjg54PB6YzWaUl5fD4XDgmWeeCaM7AMDzzz8fJh4JfKKnRp633++n3LjQjQtJnomHY+h4DX0OhA8ol8vnuQgs9Bzv1/hMEiIWi4WhoSH88Ic/RHNzM44dO4aampovE6LPILRaLX75y19CoVBgdnYWzzzzDHQ6HYxGI8xmM4qKiiAQCBAMzon98Xg8Cus6HA5MTk7ixIkT9HiRXU2hQRZwnU4HjUZDzTa7u7uxfft22vZJdkcPPfQQrly5Emb1kJmZifHxcezfvz/qQu73+3Hz5k0IBAIEAgGUlpaG1cMjJ0Gn04mPP/4YIyMjWLVqFXVJX06HRGgNHfgkWSDkcmKOudAxo+nnhJ4nQcKEQiGSkpLoQnenOyu73Y6zZ88uihBdu3YNaWlpGB4extq1awEgatJHeBk+nw8mk2lJ2jHRIpTfYrfbMT09jczMTExMTIShdgvFYgvechCiq1evIisrC93d3UhISEB5eXlUCw7yzLxeL958802sXLkSu3btAofDWRQh0mg0YLPZGBsbQ2JiIiYnJ5GYmIi8vDzKPyJlxOXwMogop8/nA5vNXjCBJ7o9U1NT8xAirVYLn8+HQCAADocTtnlYCvE19BmQMhtBakijQUZGBk3aFvt7n8+Hvr6+eXyl0M+SRO3o0aOYnp7GM888g7y8vKiJe+h/hy7aoRuKUDmMpXR+RuvuI0mzRCLBK6+8ApFIBK1Wi5UrV2Lbtm00Kb127RqSk5MxOzuLzZs3o6WlhXa6lZWVQaFQ4PLly+ByuVi7di16e3tx4cIFWl4XiUTYu3cvGhoaEAwGweFwIJfL4fF4sH37drS2ttJ3eHJyklrkaDQa/OEPfwAAKBQKuFwufP/73w+7rkgyO3kvQzcVWq0WANDW1oZVq1aBz+ffNtmMNlfdblN3v8VnhhDNzMxAoVDgxz/+Mf7t3/4Nv/3tb7Fz584vE6JPOaxWK37+85/TF+D5559Hb28vFRlkMpmorq6GRCKhLxBpk/7DH/4AqVQKPp+PmZkZrFixAitXrkRiYuKC7vY6nQ6Tk5PgcrkYHBzE2NgY8vPzodPpcODAAfT29uL06dMAQP2ASBBUY8OGDdi8eXPUiZLo6IyNjSE7Oxsmk2keTE9MR2NiYnD+/Hlcv34dweCc+erevXuxcuXKO9bQsNlsOHv2LHbv3k1RNI/HQ0nj5JgOhwMXLlxAcnIyKisr4fV6F/w+r9eLnp4eSCQSqnm0kLRAqCp36G4sMnmLTBIiw+1249q1aygvL6dco8gEgpwvmUBJd9OdRDQfrcHBQcTGxkKlUqG6unrRRGshZGA5fm1EJ+vq1atgMpnIyMigKMpCpaL/9b/+FyQSCQwGA/bu3UsRF6LeSzoXQ1vkSaJsMpmQl5cXNdn7Y+izEMV38pzJucbExIQthqERWaJd7JzJPYmPj0dSUtI8pCA0ee3o6KDE78rKyqhIIp/Px4cffoienh56HGLvQ+YaLpdL72/kuUWiExwOBx999BE6Ozvx1FNPobCwcNH7FQx+4l1XU1ND6R5kzOl0Opw8eRJ5eXnYvHkz7T7kcrnUHWDt2rUQiUTg8/n4+OOPweVyUVFRgc7OTkxMTEAsFiM7Oxs1NTVobW3FuXPnwOPxsGfPHqxYsQJOpxMejwft7e3QaDTYunUrfV4OhwNvv/02HnnkESQkJNB753a7ce7cOQwMDOC5556bpwZO7jWAeSbNJAiSSeaQ243V5SB393N8JqTq0Bvxs5/9DL/97W/x9a9/HT/+8Y+XfIz6+no8/PDDSElJAYPBwLFjx8J+HwwG8Td/8zdITk6GQCDAjh07wpSVgTnC7zPPPIPY2FhIpVK8+OKLVKCQRGdnJzZu3Ag+n4/09HT84z/+4/Iv+D6Kixcvhu0Gjh8/TsmXXq8Xq1atQlxcHF3Qe3p68POf/5zuMkwmE4A5QqZAIIBUKl2wLMVgMBAfH4+cnBz4fD6UlpZSEbPHHnsMDAaDJkPA3EsX2kK9bds2fOc730FBQcGCvBcul4uenh6o1WoEAgHEx8eH1fBJKcjtdtNJkOxgJBLJPDPSxYKQZTkcDjo7O2E2m3HmzBlUVlbi7NmziI+Ph9frpTuf0GPW1dVBJBJhdHQUN27cCCOxRgaHw0FlZSUlGuv1erDZbOj1+nnk0tHRUcrhImRd4JNSpV6vpwTPhb4PmONMbd26lZquEuSDPEeyANpsNvB4PExOTlK/tIVIyUajEVevXoXRaJz3mbi4OGpLQL4nMTER4+PjyM7ODruWxZ5F5NiLJC4vFiQJevTRR7Fu3Tro9XqUl5dDLpdHJf8CwMGDB2EwGFBRUYGKigr6PHp7exEfH4/e3l76rMi9i4mJgVgsDiOoLvV67jYIR0ur1c4juBKxPyKsSBLc0EQ4Msi4Iota6DlHjs3p6WlIpVK4XK55zyOUeEz+m3SsRn5P6P0pLS0NOw5R9DaZTOByuXQDF+1+kvmICFT29/ejs7MTAPD2228veA/dbjcaGhrQ3d0NvV5PkT9gbs7q6enBe++9B71ej0OHDiE3N5eSnoVCITweD9LT07F//346Pt1uN1JTU5Gfn4+pqSlasrfb7bQcJ5VKkZWVhaeeegrl5eWwWq1488030dTUhOLiYsjlcirMePHiRbz99tt44okn4HQ6MT4+Do/Hg/Pnz+NnP/sZZmdn8Z3vfIeid6FBxijRLGKz2TRBJs+IyWTSDkCiPN/d3U0RrMgxFwwGo47nL9vuo0QoQkSiqakJ+/btg1arXRJCdObMGVy7dg3V1dXYv38/jh49Gqbo+g//8A/42c9+htdffx3Z2dn467/+a3R1daG3t5fugB988EFMT0/jP//zP+H1evHCCy+gpqYGb731FoC57LCgoAA7duzAj3/8Y3R1deFP/uRP8K//+q/4xje+saRrvR8Ron/+53+m/5+TkwMulwuhUIjy8nLExcVRTyyHw4H/+3//b9jfS6VS5Ofn0wmHz+dj8+bN83bj0erzZPIjjvF1dXUoLy/HlStXAADFxcU4ePAg3VkTtWGpVBr2kg4PD+P06dN45plnYDQaoVKpEBsbC4/Hg8rKyjBEqKGhASkpKejo6KAcI5VKBbVajYqKCpSVlS2byBtqbpiSkoL6+nrs3r17UbTE4XDg9OnTkEqldAdMEie/34/JyUmwWCxoNBowmUysWLGC2k3Mzs4CmFsUmExmWCmL7D7vFiEiJaFAIACFQoHh4WGUlJTQOn8oP2dychIqlQpxcXE00Y28drvdjmvXrsFqtSIuLg41NTW3lTQILYssh78SGtG4YYtdu91ux/nz57F27VrMzs6ioKAAQ0NDtASl0+lw8eJFFBUVoaSkhOpmxcTEUN4QIb5OTk7OQ4jIdyyHK3Mvd9AEneJyuVGViD0eD27evInZ2Vns2rXrtgnZYqXKmZkZvP/++8jMzMS+fftoFymHwwGbzQ57ppEIUWTJbKH74Pf7MTAwgKtXr+LBBx9Eb28vSkpKqNTHcsjMJpMJr7zyCv33UDPp0Lh27RqV0EhKSoJAIEBqaircbjcmJycpt2ZoaAhbt25FcXEx5UiG3qOWlhbk5+djYmIC5eXl8Hg86OjoQEpKCng8Hm7cuIG8vDykpKTA6XTi9OnTyMnJwdjYGL7yla/g97//PdUN4vP5tAGFz+ejpaUFKSkp1LOOyWTCarXiwoUL9Do2b96M9evXL2p0HXrfCfeOCI8Cc+NJr9dTdWun04nVq1eH/b1Go4FarQaPx0NRUdFn2j16r+NTLZmF1nejhUajQX9/PzZv3rycw4LBYIQlRMFgECkpKfjBD36AH/7whwDmJOoTExPx2muv4cknn0RfXx9KSkpw8+ZN6t1z9uxZ7NmzB5OTk0hJScGvf/1r/OQnP8HMzAytvf/oRz/CsWPH0N/fv6Rzu98SIsIhIlFQUACZTIby8nIkJyfTRUQgEMBms+HnP/85/eyqVauwZs0axMbGUmPXdevWUadv4JMXymw248SJE9i+fTtSUlLCFoLZ2VkcPnwYhYWFGBgYwJ//+Z8vuPiRF/Ctt96Cy+XCvn37cPHiRZSVlaGnpwff+ta3MDQ0BK1Wi7Vr18Lr9YaVc9xuN86cOYONGzdCo9FgdnYWDAYDa9euDetwjBahSQXRLYqLi4Pf759nbriUhYxwbzweT1j9fGJiAsCcRhQpVcTExKC8vHxezR0AXUhIS/G9KLEQo1Si3puSkgK3200TqtDEinioTUxMoLq6Glwudx4Mb7PZcPXqVeo1Rdy3FwtSYlmIRxIaS73f0UjWoXHs2DFUVFTg4sWL2L59OwYGBqgPW2ZmJt5//31adsjPz0dNTQ0sFgtkMhkkEgktk9lsNmpjcifnSj53rwmnZGNBxi55v8m53Lx5E2NjY4iNjYXT6QSPx0NcXByqqqrg9Xpx4cIFZGdno6SkhFqEhBo2l5eXg8fjIRAI4Fe/+hWEwjlLk3Xr1mHr1q2UQxMfH4+BgQEIBIIFy4a3uw6dTofLly9DKpViw4YNsFqtYLFY6OvroyWspSSfhERM5riLFy/i0KFDVI4kMgjKnJiYiJycHCp/4ff74fP50NnZiZ6eHjzyyCOQy+VRnzMRn5yZmUFpaWnY+LZarfjoo4+wdu1aJCQkUBLz+Pg4Ghoa8Oijj1LLjffffx9paWmora3FjRs3sGHDBrhcLly7dg1KpRJPPPEE9Ho93G43iouLUV9fj8bGRqSlpeHZZ5+F2WwGADrHRJ5npA9k6Hgk1+H3+6FUKuFyuVBcXEybMIC59e7q1atUymDlypXIzs5e1rO+n+JTLZnFx8fjoYcewiuvvIKZmZl5v09MTFx2MhQtlEolZmZmsGPHDvpvEokEa9asQVNTE4A5REoqldJkCAB27NgBJpOJ5uZm+plNmzaFERF37dqFgYEBGI3Guz7PP0bI5XJUV1cDmPMyq62tRV5eHlVAJUHaybdt2wYA9IXk8/ng8XjYsGEDMjMzKQIQ+nd8Ph8fffQR8vLy8Pbbb2NwcDAM9RMKhXR3U1hYiNHRUbqzCwaD6O7uxssvv4yRkREIhUIcO3aMIiLHjx/HQw89hK6uLjz55JPweDwoKSnB+vXrqX4QgWWBOQRl06ZN6OjoQE5ODuRyOSorK6mI3GJBdkg6nQ5WqxUqlQr9/f3UL0gikdDPhUL8oQrToXsGog4cSSYk3UbEGd7n8yEjI4NC/263m/5NKLxtMBjCTDfvJlJTU6n8Qnp6Oi0rOJ1O6sAeExMDkUiEtLQ08Pl86vdGfOtCyzJ8Ph8JCQlQqVSoqamJmgg4nU6cPXsWQ0NDYUlmpDJytIgsqZAIPQ+hUEi1XhbaiO3cuRMdHR3Ytm0bjEYjVqxYgbGxMZSXl4PJZCI9PR0OhwOxsbFYs2YNZmdnkZWVRUuCxcXFmJmZgdFohMfjmffMI4nHk5OTuHTpEh3PkddErB1uh9QQGQalUnlbRJ3JZFJ0JvK+VVRUICUlJaw0pdFo0NfXh7q6OiQkJGBoaAi9vb2UOC2RSFBXV4eMjAy0t7fTctjBgwfhdDpRUFCA2tpaAHPvhkQiQU9PD930jIyMUH2waOduMpnw6quvYmRkhI4nu91Old91Oh1u3boFhUKBuLg4bNiwgW5uFio9ki5bn88Hg8EANpsNp9OJ5ORk/Omf/umCyRAAOt/l5+dTtIOMLavVitraWhw6dAhyuTzs3hJ9MZKASSQSMJnMeZu/I0eOwOv14tSpUwgEAjSBz87Oxle+8hW6EEulUnz961/Hgw8+CKlUip07dyIYDEImk+Ghhx7Ciy++iISEBCQkJGDFihWwWq3YuXMnXn75ZXzta18Dn88Hk8mkiWO0EjopjVutVvT391PbodB5xmw2o7S0FHl5eVSqhSiUExV0gkoSisV/hVh2QtTf349du3bh3XffRVZWFtasWYO/+7u/Q1dX1z09MbKwR+5aCauffCa0ZAcAbDYbcrk87DPRjhH6HZHhdrthsVjCfu6ncDqdKCkpwcMPP4zExEQ4nU709PRAJBKhv78fFosFk5OTaGtrg8/nQ3V1Nf7H//gfyMnJwbp163Du3Dl4vV5cunQJMzMzmJmZiSpKd/DgQTQ1NSE3NxeNjY24cOECjEYjRTxYLBbWr1+P5ORkTE9P05fT4XDgyJEjyM3NxRtvvIFgMIjt27fT79i+fTsKCgrwF3/xFxCLxRAKhfB6vbh48SJYLBbUajVNHIC53VdbWxuKi4sxMTGBoqIiDA4OUquQxYIgP0KhkCJLZIdMgiwwpGYeDAYpdydywY5WPyd/x2Qy4Xa7UVhYCI/HQzuQCIGRdBSREgjRriES/MuJSK4HMEdoJ0rRhM8wPT1NFy8iWtnT0wObzUb5J263G1arFTdu3IBer6fdKGq1mlpCnDx5ktqNhH73+fPnMTk5iePHj2NoaAjFxcVUPI6Uzhfi8iyFQ0TKgHw+P2pCFggEMD09Tc1B4+LiIJFI6GIik8mwceNGrFq1Cs888wxiYmJQVlZGldWZTCa8Xi/cbjfMZjNu3boFv98f9sxDE5DZ2VlcvXoVMzMzOHbsWJhtTLTkd7EggqYnT55EfX39gknRQlycQCAAjUZDu+DKy8uxY8cOOBwOJCYmori4GFu2bKGmzoWFhXA4HFSUc+vWrZiYmEBhYSFFE3NycvBXf/VXOHjwYBgxn8FgoLCwkPqMkW4rog30D//wD7hw4QKGh4fh9/vx4YcfoqioCHV1dbRDVaVSYd26dfD5fIiPjw/byIZGKN/N5XLhypUraGxsxODgIFJSUjA6Ooq0tDQEg0HExcUti7MVCASgVqvR0NBAhSTT09NhsVjosULHJFGr1+v1EArnPNRyc3OpRQ2JhIQE6la/nAQi9NmGcgZlMhmmp6chk8nmJT1LEcE0Go3Q6XRgMplQq9UAQDdGLpcLqampVJjVbDajvr4eUqkUU1NTyM3NRVJSEioqKpCWloYVK1bMO360+eeLEMtOiDIyMvCd73wHH3/8MTQaDb73ve+hq6sLGzduRE5ODr73ve/h0qVLn+sus5/97GeQSCT0J1QA648dpL578eJFnDx5kmqUZGRkYGZmBqtXr8bY2Bg1ExwcHKRK0du3b0dTUxO6u7vxd3/3dzAajTAajXA6nWEvHdkRSyQSfOUrX4HT6UR8fDyys7PR3d1N4VbiJ2a327FixQoKXweDQezbtw8jIyMA5naTaWlpOHjwIA4dOoSioiJ0dHRgenqaEqL7+vqQlZWF4eFhClmTl256eholJSXo6+tDTk4OvF4vVq5cOc9fK1qQXWBMTAw9RyaTCYlEgpmZGXR0dIDD4VB4nkwyZDEObc1fKMhEJpfLkZGRAafTiaqqqjBUY2pqCgkJCRgdHaWLKIPBCJt8Q3fAtxsDOp1uDrRdowABAABJREFUUVK3UDinoDw7O4uEhARMTExArVbT5GRmZgYcDoc2IJAE0WAw0AQtNTUVZWVl6O7uxs6dO9HZ2Ym6ujqqzKxSqcBmszE7OwupVIrh4WFwOByUl5fDZDLBZDJRf7rIJANYmJwZqqI9OjpKF8FoodVqcezYMYjFYrz77ruQy+UIBoN0zLrdbshkMmzduhUCgYB+X+h3kEWQx+NRdDISFSTPiZD6TSYTJBIJJdOSRS0U2bzdgsHj8dDd3Q2JRIKhoaF5i2zk9xOia2gLus1mw8DAAPx+PzQaDQwGAx588EGsXr2alnQee+wxlJaWwufzUQ4b0T1KSkrC4cOHqc5RIBBAS0sL/uEf/oGivqTUOjExgcHBQVy/fh02mw0sFgvBYBCnT5+mavfvv/8+mpubsXPnTly5cgXT09OwWCzQ6XSUx/bkk09i586dYR1eocgk0fzhcrloamqCz+eDzWajyu2k5JWRkXFbU1pSbiTPQq/Xo6urCxqNBu+88w6OHDlC6QIMxpyR89mzZ9HV1QWLxYKUlBS6eXjttddw4sQJjI2NITU1Nex7tm3bRnmZkZv0xSJ0bJH/jouLo+Mx2oaByWRCoVBQ8nxkkITY5XJhenoaPp8PPB4v7PgOh4OKWY6PjyMvLw+tra1ITU0Fm82mfLvKysqope+F0N3Pe9yVdYdEIsFTTz2Fd955B1qtFv/5n/8Jv9+PF154AQkJCTh8+PAdHztU6DE0NBoN/V1SUhIlq5IgcGroZ6IdI/Q7IuPHP/4xzGYz/VGpVHd8Hfc67HY7+vr6aNY/PDyMQCCAwcFBdHV1UdIf6dQiyQAxUA1Fu/r6+hATE4OSkpKwly50h56eno7t27ejqKgIY2NjcLlcOH/+PGJjY2l5Jj8/H16vFxqNBq+88gr+z//5P9R4cdWqVThz5gyYTCbS0tKQkJCAyclJMJlMTExMQKfTAZh7Fm1tbZDJZGE8CTJpW61WVFdXU3XcpXbzhC66BD1MSUmhLbSkU4XU/GdmZnD9+nU0NjZiaGgINpttQWRCo9FgbGyMTlwikQg5OTmorq6GTCaDSCSihGqSOJDJnCRLoQlB6OIfevzIzcVSyjKkGyczMxOjo6O0NEakApKSkhAMBqFUKmE2m2l3T1JSEt0AsFgsyOVyPP7441AqldBqtcjMzER7ezvGxsZw48YNGI1GlJSUQCaT0dIsACpUSJJNFotFeVyhpVWyqIcmDqEk/pycHGpoGq2E6XQ6kZOTg9nZWZSWloLBYNAxQp69wWDAL3/5S6hUKvq3xC+OdP3V1NRAKBSipqZmHlcpdAylpaWhsrISBQUFyM3NRWpqatTxuJQFQ6FQYM+ePTTBT0lJmfeZ0M5CoqoOzC2kRDussLAQLBYLiYmJ8xZqQuANTQZJ6HQ6al1B5mqVSoUzZ87A6XTi3XffhcViwY0bN3Dz5k2Mj49jcHAQPB4P586dox2qoRsTv9+P9vZ2XLt2jSYHFy9ehEQiwfj4OLKysua9T5FdhXq9nhrY1tbW0maMjIwM5OXlgcGYM3nu7++nfL6bN29G7ZYiXD2SkPP5fPj9fopaiUQiSlp2OBy4evUqBAIB2traMDk5ScVojx07Bp1OB7/fjwsXLmB0dBQnT57EzMwM/vCHP+DmzZtIT09HdXU1BgcHoz7HaAly6NgikgCNjY0wmUyQyWT0PVooyLXr9Xq88847sFqtEAqFmJycxNDQEMbHx9Hf308VyMk8d/36dZw5cwatra3IyMiA0WjEhg0baDlxsfEbDAYRCATuabn/fok719jHnOFkZ2cnZmdnaXa/c+dO7Ny5E+np6bfd6S4W2dnZSEpKwsWLF1FZWQlgjhzV3NyMb37zmwCA2tpamEwmtLa2Uk7NpUuXEAgEsGbNGvqZn/zkJ/B6vTTTvXDhAgoLCxesOfN4vNuSdf9YQVyqSSQkJNBdYjAYxOXLl6mHEnmBSdnvl7/8ZVgSWFhYSBVR+/r6kJGRATabjbi4OBiNRurLQ0o+DAYDs7OzVM1627ZtOH/+PG7evAkgXATw+vXrWLVqFYaGhvDss8/SmnpcXBzYbDZu3rwJl8uF/Px8AHPJmVQqhdlspgs4gc1JdwiBgEPl5pcTgUAATqcTPp8PLpcLhYWFGBoawvbt2wHMTcRqtZq6djMYDFpytVgsGBsbQ1ZWFmJjY6HX66k9glqtpsKLhNDocDioy7nX64XRaIRCoQCbzV6QjJqTk0OtQsiEzWAwqKouCXJfbleWYTAY8Pl8qKmpwdjYGMRiMZKSkuh7QPRriPZLWVkZNdMN/S6bzYby8nJ4vV50dHRg3bp1uHDhAlwuF9hsNiQSCbZu3Rr23enp6RgbG4PNZqP3prW1FWVlZZTkrlKpaMIkk8mgUqmQlpYGjUYDqVSKYDAIkUiElJQUiorJ5XJoNBpK+s/IyEBBQQFycnLmEV2Bucn77bffRlVVFU6cOIHnn3+ejp3QhZgIOgII4xuSY4R2U+Xl5SEvLy/sM5HjkTyj0CQpkpjNZDJpeYK8O6GkZ/L+Ef4XkYu4ceMGbty4AafTieeee44itRaLBV1dXUhKSqJK8E6nE3K5HHq9HlevXkVKSgqqq6tpF1hubi5GRkawYsUKOBwO6ssHzFEHRkdHoVQqKWqYmJgIo9GIjRs3QiqVYnp6Gg899BA++ugj2pX3wAMPIDExESdPnoTZbMaTTz4JrVaL/Px8vP7663jiiScop8bpdOLmzZuIjY2lnU4kUSW2OJs2baL8Hb1eT30AiXfZuXPnYDAY0N7ejq9//evzngPxPCTPoqqqinZXmc1mrF+/Ho2NjfB4PCgvL8fNmzeprZDH48HRo0epLQ9JWq5du4aSkhK8+eabKCoqQltbGywWC7Zu3UpFUUMjNMFYaO7S6/UYGRmBQqGgqH5+fj5FBonNS3x8PEWHOjo6kJubi3feeQfbt2/HqVOn8OSTT2JsbIzayqSnp2NycpLKoUxNTaGvrw88Hg9NTU3Iz8+fJ5gbbfyGXgswt04SXuIXJe4YITp79iwyMjKwdu1aPPLII3jsscfoz759+1BVVRUmMR8tbDYb2tvb0d7eDmCOSN3e3k6z2e9973v46U9/ihMnTqCrqwvPPfccUlJSaCdacXExdu/eja9//eu4ceMGrl27hm9/+9t48skn6W7r6aefBpfLxYsvvoienh68++67+Ld/+7d5Sp+flyAkPxLkJQldxAjRtaWlBe+//z6OHz+Ouro6sNlsqqq6adMmbNy4EYFAAENDQ+DxeOjt7UUgEIDRaAyTdJfL5eDxeMjOzkZqaipVN56dnaXJEIAwkmlxcTHKysqwb98+upsl4mOka4mQbwkXwOFw0I4I0u5Pdvik86y/vx8//elPcfjw4WVxbwgZNjExET6fDytWrIDdbgeXy6ULQFxcHFJSUpCeno6kpCSkpaWhtLQUDocDSqUSYrEY/f390Gq1kMvlEIvF8Pl8YbvySLQgLi6OclkikRG/3x9WKmCz2SgoKKA7YtJ2G2nouhCPKXIHShR8jUYjcnJywGQyacIbFxeHrKws2l22YsUK2Gy2eQgD0d8RiUSQy+VU56i2tpY+U4/Hg7q6OtrFAnwi0JmbmwulUom+vj7k5+ejq6uLliSJqCORcEhISEBbWxvVALLb7bBardTgl0hJOJ1OipAODQ3B5XKhoaEBVqt13nN3OBw4dOgQbt26hYceeoiq/s7MzEAuly9ocBt5jKXoQN3uGS20644cM6GJGvkdKeV2dHSgv78fer0eDocDb775Jj22UqkEl8uFWq0OM58l90coFKK7u5vyPRkMBqamppCYmEhRWdKwwmQy8e1vfxt+v5+KX05MTEAmk9FuMKVSiYmJCTQ3NyM9PR27d+/Gt771LWRlZWFgYAAmkwlPPPEEZDIZcnNzcfLkSdTW1lKVfK/XiyNHjsDj8WBsbIzOBaSkZ7FYcOvWLSrUajabaZmXIBOxsbF0gzg1NQWbzRa2QSfXyefzqQlwUlISVq5ciZ07d2LPnj2YmJiARqOhTQGEs5aWloZz585RHg/hLBYXF1NPwmeffRZTU1MwmUwoLy9HW1tb1BJTKFK3UMTFxSE3Nxc6nQ4JCQnIzMzEzMwMHA4HJiYmcPbsWVp+JGOosLAQp06dwqZNm9Da2oo9e/bAbrejsrISsbGxqKqqQmJiIsrKynD06FGcPHkScrkcFRUV1Cg3UttvofEbOl5DkdgvUtxxQvSd73wHBw8exPT0NHXZJT9L5Q+1tLSgqqoKVVVVAIDvf//7qKqqwt/8zd8AAP7yL/8S3/nOd/CNb3wDNTU1VFE4dIE4fPgwioqKsH37duzZswcbNmzAb3/7W/p7iUSC8+fPQ6lUorq6Gj/4wQ/wN3/zN0vWILrfwu12z9vBikQibNy4Eenp6UhNTYVEIkF8fDxmZ2fDkhSfz0cnrk2bNtGden5+PtxuN0pKSiiKQwxW6+vr8Ytf/IJyDEiiQxCTUMJdVVUVysvL8cwzz2Dfvn1gMBgUhQsEArQrxefz4datWxgZGaHkzNjYWAQCAeTm5mJ4eJh2SdhsNvD5fLDZbNy4cQOnTp0Ch8PB0NAQrl69Ou/+kNbYSJdmh8NBvyslJYXuKOvr63H27FkAc4tAUlISqqursW3bNkpQFgqFyM7OpggFKZElJiYiKysrLBmNnEiYTCYKCgrA5XKRkJAAn8+Hrq4usNlsTE1NhZUKCEF4YGAAPB4PHA4HSqVySbwismiHLrZ6vZ4Sh9lsdpiHHOlaImW72NhYahlgtVppYhV5PaQE6PP5sGPHDsjlcvj9fng8HrS0tISdEyl35eTkUEJ8ZWUl5fB4vV5qBltTUwOtVouqqiqw2Wzauky4J2azmZ4zSVzJ2G9oaEBBQQHee++9efeFXN+3v/1tCAQCXL58GVlZWRThivRpihZL6XQjsRjZlCQnkaXDUBIx2RyQRC30dw6HA2VlZcjJyYFMJgOfz8fjjz9Oj52VlQWz2Qw+nw+pVEqdzIVCITZs2IDp6Wnk5eXRRfm9995Dbm4u7Uh7//33qdKyQCBAfHw88vPzqaVOUlISbt68CbFYDI/HQ0uuNpsNFosFvb29AOZKv9evX0d6ejrefPNNisgeOnQIbW1t2LFjB4LBIJVnUKvVkEgkiImJgcPhgMfjQVNTE06ePImUlBS0trbigw8+oNo5UqkU4+Pj4HK5iI+PR0VFBQBg7dq1tDOKvFMkCQ1910LHtFAopAbZTqcT/f39KC0txcWLF2E0GnHgwAHYbDZkZ2djcHAQSUlJ6O/vx1e+8hVs374dbrcbL7zwAvbv3w+lUoknn3ySloH9fj8GBwfxH//xHxgdHaUlMLI5+9WvfoWuri66XhID5v7+frDZbKqNRUp4WVlZ6OjooPNzU1MTTpw4gYqKCqjVamzfvh1DQ0O4dOkSpqamsH79euzYsQNbt25FY2MjvF4vrFYrGhsb4ff7kZWVBR6PFyZkHNpttlAQOZQvojjjHQszxsbGoq2tDbm5uff6nO67uJ90iNRqNS5duoTh4WH6b8XFxUhOTsaKFSswOzsLmUyG0dFR2O12NDQ00M/V1NRgw4YN8Hg8MBgMSE1NhVqtpn4+odobjY2NaGtrox1HwBw5k+xglEol5RAUFxcveL6EwPvGG29AIBBg3759eOutt+ju52tf+xpsNhs8Hg9NZjIyMmi5AJhbZBobGyGVSlFfXw8AyMvLw8MPP0z5P3l5eXQyJ0OamM2ShQGYQzy0Wi20Wi3ef/99SCQSOJ1O/Pf//t/Dzlur1YLNZocJ4dlstjCRs4UmA9LJ0t3djc2bN4ftCgcHB5GcnAylUons7GycO3eOCkKS8yJQPUmMrFYrysrK5pmm+v1+jIyMYGBgAGvXrqVlTZFIBJfLhfr6egQCAaxevRoymSzsfAOBABoaGhAIBMDn86ndjsVioST5aGOdoDYOhwMdHR0oLy+nGky1tbXw+/3zRDwX0u4hvw8EAlSsUqvVUiI78aoi5SNinnn+/Hls2LCBkoD7+/tx5coVHDx4EHq9Hs3NzXC73VixYgWKiopoBxEwxx8kgoCJiYlLmtBJmWR4eJiaJ3d2dmLbtm00QSILidPphFQqRWxsLEXCSCmJiHcGAgGkpaXB4/HQRF8gEECpVGLNmjXUBoWUSS5evIg1a9YgISEBXq8X7e3tqKiomOdFpdVq8dFHH4HNZsPhcOCRRx6hz5DP59N3MS0tDT09PZDL5fjggw/A5XIhEolgtVoRHx+PsbExPPnkk8jPz4fT6cS5c+cwPDwMm82GF198EYFAgLan9/T0UCf2goIC1NTUIBAIoL29HQ0NDTh48CDsdjsUCgVVNQdAO8iam5tRVlZGJR5EIhFaWlpoQqHT6RAIBJCZmYnh4WFUV1fj8uXLUCgUkMlkUCgUKCsro+rNBAWKj4+nyScpOxI+kNPpxPnz51FUVIS8vDwqRSIUCtHQ0IC6ujps3rwZmzZtopudf/3Xf6XdYxs3bqQioMFgECdPnoTf70dycjJ27twJLpcLBoMBo9GI48ePIy0tDaOjo3j22WdpeemNN95AamoqhoaGsGfPHhQVFaGlpQUtLS0UkdqyZQssFguys7NhNBpx69Ytasja3d0Nl8sFj8eD2dlZutEYGxuD1+sFk8lEZmYmLBYLSktLweVycf78ebDZbOzYsQMMBgOXL1+G3+/Htm3b6BxFtMwIP/SLEJ+JdceBAweopsSX8dmFx+MJWxgzMjJQU1OD8vJyGI1GWCwWtLa2IjExETKZjHJ0ioqKsHr1akxPT9OyVU9PD5KTk3HhwgVcvnwZLS0tFFkxGo1hg4eYxcbExCAhIQHFxcX096HWDz6fD01NTfjHf/xHzMzMQCgU4o033gAwV8o7duwYcnNzMT4+Drfbjd7eXvj9fmrOunLlSmRmZobBsUKhEMXFxWhsbKTtwxs3bkR3dzeVvb927Rp4PB5MJhPYbDZYLBYtwxFNIWBuQdfpdLh+/Tqqq6vpDi8y+Hw+PB5PGBoZExMDFot1W0dzYkWgVCpx6tQpAJ+QsJlMJkZHR6ng2urVq6nKd1xcHN1FEl6Oy+VCXl5emB0KiampKfT29iIpKQnNzc1hZOCmpibaxTY0NDSvTEOIq0QzJTU1FQwGg5IyZ2ZmFkQ5iG8V2TWvWLECW7duhd/vDysJ3Y5YTHhAw8PD8Hq9tGPLarXCarXCYDBALBYjKysLYrEYDAYDFy5cQFFRERoaGmipuKSkBF/96lfhcDjQ1NQEtVoNq9WKgYEBtLW1UXfzyclJsNlsPPfcc0hKSrptMkTQHiL7kJCQgPr6erS2toLL5dJyj1arxeTkJPx+P+X42e12jIyMoK2tjWrxDA8Pw2w2UykBh8OBGzduIDY2FtevX4dcLseNGzfoou5wOHD58mUkJyejsbERTqcTHR0dFCkAQFEIIqNhNpsxMjICi8WCjz/+GG63Gz09PbBarRCLxSgoKMDNmzdx6dIl3LhxA3v37kVtbS3kcjk2btwIuVyOJ554gnINr1+/DqPRiJiYGOTm5mJoaAhcLhfnzp3D+Pg4XC4Xdu7cSecT0nm4atUqfPvb36bJkFqtBp/PR29vLy35DA0N0XkrJycHIpEIdrudissKBALs378fO3bswPDwMFasWIHOzk7k5eVBo9HAbDbTpBOYm1+I5o7JZMKtW7dgNpvpexsfHw+bzYb3338ffr8f3d3d6Ovrg1gsxtjYGE6cOIHGxkasWLECV65cwR/+8AecPXsWRqORJnIxMTHUzub06dM4fvw4nXcNBgMuXbpEu3ZTUlKwdu1ajIyMYNu2bVQLiMFg4OGHH8bo6Chqa2vB4XCg0+lQXl6O0tJS2Gw2mgwlJSVhbGwMGRkZ2LhxIzIyMiAUClFYWAgOhwMul4tAIIDW1lbw+XxkZGRAIBAgMzMTAwMDuHz5Mq5duwYA2LdvHx5++GEIBALaQZuSkhJW4iNaZhKJZF7p8Y8Vn2WL/x0jRA6HAwcPHkRCQgLKysrm1U2/+93v3pMTvB/ifkKIrFYrXn/9dXg8Hni9XmRnZ1MhTMK18Xg8dLcqFovh9/tRWVkJPp9PkaP09HTweDwMDw/D7XbTxaegoACJiYkQiURUUfbWrVuoqqpCdXU1heRdLheMRiNkMhn0ej0yMjIQExODwcFBHD16FNnZ2RgfH8cPf/hDHDlyhIq6HTx4EJcvX4ZGo6HtqevWrUNvby8EAgHYbDZ27txJExHyMgwPD8NqtaK3txelpaUoKyuDVqulAnkPPvgg3G43EhISwnSnCJpBCNUajQYDAwOIj4/HyMgInn322agckqUqE0eLQCCAV199lXa2HTx4EDabjfrNEXG9hIQEXL9+HQ888EAYMTGaaSoJspimpqZSAczBwUHs2bMnzHqD8EZ4PB5WrFgBLpdLd8tCoZC2NgeDQboTJDpJXq8XIpEIgUAA6enpUUtKTqcTV69eRVFREdLS0qhVS+g9C1UJj+YCb7VaMTQ0BBaLhfb2dqqgTgQlo5WzbDYbzpw5g82bN9PElBDZdTodpqamcOPGDTgcDpSXl6Oqqoo6whPT3v7+fuzZs2de+Svy/MlxJycn4fP50NHRQUUc29raEB8fj/T0dKxbt45ym7xeL+UAqVQqSjytrq6G1WqlnZ9xcXGQSqXQ6XQYHh6GUCjE2NgYampqIJPJaHI7MTGB69evo7KyEvn5+bRUQgx8JycnkZqaSpOu48ePg8fjwe12U7SCEKcJp/MXv/gFYmJiMDs7i+3bt1OJBmKBEwgEaHeqy+XCpUuXaKKzc+dOnD59GjMzM2Aymdi1axcaGxup7hUh0T/zzDPw+XyUEK7T6WijhtFopOX26elpiipdunQJpaWl8Pv9cLvdyMjICOvUtNvtEAgEqK+vpzw/skFjMpn0HROJRKirqwMwR5nYtm0b5QXp9XrMzs5iYGAAcrmcinRarVYUFhaisbGRErh5PB7MZjMKCwuxbt06XLlyhaJjw8PDiIuLw9TUFB3fcrkcu3btgsvloiKaxKOQlIkJV4zYl+h0OuTl5YHJZKKhoQG7du2CVquFTqdDTk4Opqen6X1QKpVQqVRgMpmora2Fz+fD1atXaXOCUCjEoUOHAADj4+N45513IBaLYbVa8dJLL1ERWmBO3Z00KFRUVNBNNkGdRSIR1YeLtAghSFxkl+ynFXdrmvyZuN2/+uqreOmll8Dn82mtmx70/7VFflHifkqIgsEghoaGqFdbZmYmDhw4AKPRiJSUFKhUKlp2mZiYwMzMDCoqKiCTydDS0oLe3l4IhUJs2bIFGRkZkEgkaG5upoqvfD6fyvKTxTclJQV2ux1jY2PIzMwMUz2dnp5GcnIyRCIR3aE1Nzfj448/RmZmJvbu3QuxWIz6+nqqfZKRkYH6+nqo1Wps2rQJjY2NAECJrunp6di5cyfUajXtNGGz2VQmQKvVQiQSoaOjAzMzM1Qkcv/+/UhMTAxbnEmLqMlkgsViAYvFwuTkJPr7+7F+/Xo4nU6sX7+e3luSPBGbD+KHtdRnY7VaMTU1BbPZjKGhITzwwAOIj4+HXq+Hz+eDxWKB2+1GUlISpqenaRt+pNu7SqWCWCyGRqOBXC5HQkICAoEAtVogCF53dzcKCgrAYDAW9DYiE4rBYIBcLqe8MmInEhcXh+HhYaSlpUGn04HP51MDWMJ9ihakG4ocUygUwufzobe3l+rEiMViWCwWDA4OYnZ2FmvWrKEGvgaDAXV1dZidnUVxcTF0Oh3KysqovhKLxaJyAaHXEmqDEpl4ORwOjI6OIjExEQaDge6oyWJ5/fp1rF27Fj09PcjLywOLxYLBYEBVVRWcTifV3ikvL6cCly6XC9evX6dIml6vx+joKL3nFRUVNHHs7OxEQ0MD7HY7amtrYbVaUVJSAj6fDz6fD5PJhLS0NCoCKZPJcP78eQSDQWzZsgWlpaV04ifJAEnSCMrU3t6OtWvXUqQqJyeHKqV3d3djeHgYRUVFNNEgZZjLly/jkUcegUAgwLvvvouSkhKIRCKMjY3BYDBQYnFFRQUeeughcDgc2O12HD9+HHw+HyKRCCaTiXKFgLmGi8TERJpokNi5cyfy8vJw+PBh7N+/n46toaEhdHd3o6amhq4Re/bsQWNjI0pKSihSZjAYUFhYiKysLGi1WmpMfeDAARQXF4eNYZLsEARoYGAAk5OT8Hg8EIlElGg+NjYGgUBAS7sdHR1ITk6myVlnZyckEglsNhsKCgowMDCA2NhY2kxANnButxsXLlxAT08PsrKy8PDDD1Ok6q233kJubi5NMJqamlBbW0uTYLKRcDgcyMrKoknK4OAgCgsL6VggG9Pc3FzI5XKMjIzQDtjY2FiIxWKsW7cOWq0W586dg9/vx86dO3HhwgXExcVh7dq1GB0dxcWLFyGTybBjxw5aLSDv0fnz55GVlRXmBTkxMYH4+HioVCrIZLKofoRarRYcDgdut5tyiT7NuJvNKfAZJURJSUn47ne/ix/96Ed3ZOD4eYr7KSECgFu3btFODQB0V9nd3Y3Vq1ejuLgYSqUSXq8XPB4PWq0W09PT6O7upn9TVVWFHTt2hA2y2dlZCjmTyTchIQGtra20w2XdunWora2lux+r1UqtBy5evIj8/HxUVFSgqakJqampmJmZoRMvANrVRQi9g4ODYDKZqKurg8MxZ9aYkZGBpKQkVFVVwWq1Ii8vD1arFaOjoxgYGACLxaLWGB9//DGCwSAqKipQXFxMCc8pKSm0tZm0nxPhO7J49Pb20o4Z4BPfLNLaS4jeGRkZ8Hg8aG9vR0FBASQSCb1nXq8XbW1tUCqV2LBhA9rb2zE+Po6UlBR4vV489NBDCAQCYSgJ2YVJJBK0tLRg7dq18zYVdrsdExMTYLPZYDAYkEgkMBqNSExMRHd3N13U5XI5xsfH6TOJFtGQGmBOh4a0TotEIuh0OuTm5oLBYNBdLUk6ok1IZLyYzWakpaXB5XJhZGQEPp+P7pgzMjIwPDyM8fFxCIVzNiLFxcXw+XxQKpWIjY3FxMQENfVlsVhwOp3UTkYgEISViEn7faRXGNm1Op1OtLW1IScnh5bagE/0aILBIC5evEgJpf39/cjIyIDf76doDjDXJVdUVASRSISuri5qUkzeD5fLBbvdjpUrV9JyDwC8++67Yfy6xx9/nJZ/iJ8caa9ms9m4ePEi/c6YmBisXLkSfX19ePDBB5GTkxNGZu/v78fHH38MiUSC0dFRZGdng8PhID09HcnJyWhvb8fKlSshEono4k0Ivu+99x6KioowNDSERx55BL29vaisrKRlV1LCI7F//36UlZXh+PHjUCgUVLSPz+fD5/Ohvb0d6enp2PL/lLBJ2WhgYABCoRB79+7F2bNnUVtbi+bmZnzjG9+ATqejhOHB/5+99w5uOz3vxD8gGgECBAiCKCTBBvbeKVGiOtXbapu3eN0dO8mk3N3Mzd3c5Y/7I8n8MrnL3SSx48Rt1/YWb1+tVqJESpREiWLvDSTAAhAEQBCFIEH03x+a9zGgsrtJHEd29pnxjJciQJTv932f9/N8ytwcxGIxKisrMTMzA5PJhKSkJJSVlRGvjRkHxvvI8Xg8fPnLXybz1oWFBYhEIgSDQUilUmomLRYLFhcXoVAosLq6mtDEZWRk4KmnnoLJZMKdO3dQU1OD6elpaDQaTE9PQ6fTIRgM4siRI3j//fchFArx1a9+ldb+nZ0dvPXWW4ToRaNRnDp1Cv/4j/+I9PR0LC0tobKyEkajEVKpFEtLSzh9+jS4XC5GR0dpLdrZ2UFFRQXOnj0Lr9eLDz/8EDabDcFgECKRCM888wyys7MxNjYGlUqFN998Ezk5OWRJIRQKyQbEYrHg2rVrRA4vKipCeno6DAYDmdGeOHHiketDfMUj0Owg+GBD8ptGiP619RvhEAWDQTz//PO/883Qk1hWq5X+P5uNM64EU4bJ5XJkZGSAx+NhaGgooRli3CKbzUYurjs7OxgfH8fi4iLm5+cxPj4OoVCIW7duQavVYnR0lPyHGBrQ0dGB//N//g9u3bqFjz76CFKplE5njY2NcDgcyMvLg1KppEBCn89HCpnt7W3o9XqEw2EcOHCANsHl5WWIRCJKpPf7/VhaWqJTPpPdLi0toaCggBQ1AoGAXGWZdwrr91mTxG5ggUCAurq6BMUeUxMplUooFIoESf3IyAjS09MxMzNDm+vW1hampqYwNzcHhUJBfihCoRBzc3PIzc1Fb28v2Q0wtRCXy4Ver8eNGzeg1+vR399PvJH415KdnQ0ejwepVIr09HSC0BmfoaysDBsbG2hubiZU7FEZbExVw8YwLIpBqVTC4XDAZDLBbDZjYmICgUCAXI/Z6Oaz5OLxCFFZWRm4XC5kMhmZFpaXlyMrKwvRaBTFxcUIBAKQSqWoqqqCz+dDfX09nnrqKej1eiLUq1QqQj/inYzZ62ZjNVYsq66npwcymQzz8/NYW1ujEdna2hpdG+fOnSN1JMt8KyoqQnZ2NtxuN8LhMPLz80nZVVZWhuTkZGRlZSElJQW5ubmEjLMMOPZ5nzp1inLizp8/n2A3wVAEuVyO3NxchMNhHD16lNCjpqYmzM7OoqioCFevXk24zjY3N7G8vEzNEACyguByuXj33XcxMTGBV199FVNTUxQRw4j2x44dw/T0NBobGzE4OIj8/HwMDg7C7XZDLpcT9yR+Xdna2kJ7ezusVisqKyvhcrnIHVqj0YDH4+Hjjz9GamoqlEol9u3bh/z8fLS1taGnpwdqtRo3b96kyJTs7GwUFhYiFovh2Wefxf79+7GwsEBxH8FgEKurq2RnwUwz40c9J06cICEEO7SEw2FIJBIkJyeDz+fD6/VSpmZBQQGNnFh96UtfAp/PR1VVFV588UUIhUIcOHAAGxsbqKysRCAQwJe+9CXMzMxAIpFALpfj0qVLdD/dvXsXXq8XcrkcIyMjqKysxM9//nOEw2Gsrq7S4aywsBAGgwGBQACDg4Nk9OhyucisdHJyEq+99ho2NzdRVlZGRPtoNAqJRILZ2VkUFxfjjTfewN69ewkRZ9fF4uIiWTEcPnyYVLS1tbUoLy9HaWkpOBwODhw48Ln2lvj4H1YP3v+f5ZT921z/YoToT//0T5GRkfGQOud3sZ40hOj73/9+Qg5bTk4OFAoFTCYTsrKykJubC4lEApvNRqosVvv27aNkb+bJk52djZs3byItLQ0LCwtQq9U0LtDr9ZiamoJAIEBvby8OHTpE0t2//uu/pudtamrC1NQUysrKEmz5Wfo5M32MRCI07tja2sLrr7+OEydOUOZVR0cH9Ho9CgsLUVRURLNxr9eLGzduEOH3woULcLlcpNCpqanB2NgYNBoNBAIBkYRZo+B0OiESieikHj/TfxzkG38y8ng8lJ6u0WhokWAQvclkwrFjx7C8vIyZmRlkZmbC7/ejra2N+FDxs3CLxYK0tDR0dnZi79690Gg0nysd/XEqkHjkxOVyUZPyac/H1HtKpRLd3d1obm6G0WiETqdDUVERlpeXUV1d/VjImv08HA5jbW2N0KUHT5jxFa8EYtyPz+IIMJKnx+N57PjO5/Nhc3MTCwsLsNvtUCgUaGxsxOjoKFQqFTweD4134qM12PhNJBLBbDaTpJvxk+I3B6YkYxuaVquFx+OBVquFSCQCl8sFj8fDzZs3kZqaColEQpsx4yQJBAJYrVZkZWVRfEJ2djZCoRBu3boFiUSCkZERnD17lpAAkUgEg8GAWCyGW7dukQdTSkoKDhw4AIfDgdHRUfKB4vP5ePbZZ5Gbm4vR0VFkZWURh8rv95PhIuMoWSwWHDt2DH6/H3fu3CGU9bnnnkNubi7W19fR3d2N/Px8TE5OknEpcN9tm8Ph4PDhwxgdHcX8/DzUajX4fD4kEgnm5ubw1FNP0WEkEolgcHAQd+/exUsvvQSxWIwPPviA3J05HA6effZZhEIhhMNh3L59m6xC3G43VlZWkJWVRQcZFsGTmpqKO3fuYGFhAcnJyTh27Bh4PB5MJhOWlpYSvHb+5E/+hFDexcVFzM7OYmxsDH6/H8888wz6+/shFApx+PBhSn0/f/48GV26XC68/fbbAIDnnnsOnZ2dCIfDEIlESE9Px7Fjx3D58mVYLBbyFGI8y/T0dLjdboRCIVJnFhcXw+Px4PTp05ifn8f09DQKCwtRUlJCI/apqSkYjUY89dRTKC4uxq1bt+iwxFzB1Wo1jh07RiNnpnLbt28fysvLP/fo/8H6146s/r3rNzIy+6M/+iO8+uqrqKmpQXV19UOk6v/9v//3v+Rpn8h60hqi+fl5MmQD7sudhUIhLUSbm5vQarX4/ve/n/C4uro61NfXUyRJWloandh3dnYwPDyMnJwcJCUlwe12Uwo9S0NmiyjbcIeHh9Hf34/i4mI888wzSEpKIoiWcW8Yz4Sptj744AMsLy+jvb0dd+7coVPUl7/8ZSwvLyMWi0EqlZJxH3MfNhqNePXVV1FeXo5Dhw6RGsXtdkMsFuP69evIy8uD2WzGvn37CI0B7ic7b2xs0DycnZrZqZ3BwA6HgxAWZmDInG5XVlawubmJ0tJSOrE+bpGI38AVCgUsFgu0Wi2pkWpra5GUlITp6WkYjUYcOXIEAD5zwWGvkalD1tbWqPFg4z5G6HwUiRlIXNy2t7eRlJSEoaEhaDQajI2NkYuv0WikHCN2Go2P1Ij/Xi0WCxQKBcmXWcOWlZWV0BzFQ+2RSAQfffQRjhw5ApVKReOkR73ueK4S4/QwKfuD70soFFLUh8lkQiAQgM/ng1QqhVQqTWhw4j8LAKRMy8/PJ/sKhqo5nU709fVhz5494PF45FCu1WrJ10ksFuPOnTs0DtTr9cjOzoZKpaLIhPim1e12k1koiyRi14dYLE5wfmcSdD6fT5tjc3Mzqqur4fV6cf36dRiNRpLbFxYWEoK0vLxMXCw2OmWN1+3bt+l9vvLKK3j33XexuLiIqqoq7N27F3w+n5rFjo4OGnOz4vF4OHPmDFwuF+7du4eSkhKsrKxg9+7duHr1KnJzc+FwOPD8889DLBZjcHAQ/f391HDv3r0ba2trGBoaIul8Q0MDGhoaYLPZIBAIoNPpEI1GcevWLVRUVKCvrw8cDgctLS0UDzMyMoLJyUnKa8vKyqJ4ms7OzoR7YM+ePYhGo2hqaoLVakVnZyc2NjYoa401kBqNBkePHqXrknkkXbt2jaTtSUlJaGhowM2bNyGVSolkbrVayVdpc3MTbW1txL/SarXg8/kwm82EnGs0Ghw6dAgFBQXo6urCwMAACgsLsWvXLoyPj2NhYQHp6enw+Xwk34/FYvjHf/xHxGIxBAIB1NXVoa2tjdaCV199lQj1R48eRVFR0aeuL7FYjPzlysrKKObncY951Lrw4L/FX2//Xs3Ub6QhetCqP+FJORx0dXX9S572iawnrSGy2Wz4wQ9+QOZZ2dnZyM7ORnV1NaXH7+zs4K/+6q/oMaWlpWhra6NmSC6XAwAZ3dlsNrjdbqSmpsJsNuPy5cuQSCRoampCfX09SWwDgQAyMzOxtLREI6Ndu3ahuroai4uLAIDJyUnw+XycOHECb7/9NkwmE3g8HnJychLI9pWVlZiYmMChQ4fQ1taGWCyGpaUlcipnJMmtrS10d3ejqKgIBoMBX/nKV8hnhEnrl5aW0N/fj127dqG4uJg2y1gshtnZWaSkpMDr9UIkEiEQCKC4uDhhY1xZWUEwGITL5UIoFEJ6ejoRNdnCxTx2CgsLH4lksPm7VqulnDGn0wmVSgWDwYBgMAi32w2r1YpnnnkGXV1dqKqqwvj4OE6ePPmZp7D4RosFUjKkiOWBMR8iVsFgkCTjubm52NnZoY2WNUUslHdlZYW+34yMDPodhrzI5XKYTCbs2bMHHo8HoVAIPB4PIpEIq6urSEpKwvr6OjQaDXJzcynQlr3GeDLm5cuXUV1djYmJCVy4cIE2I8Yd6enpQXt7Oy38bHGNRqMQCoUIh8Pkt1VQUPDIOJS5uTla5JkxJY/H+9TwTdZ0+nw+rK6uIhKJIDMzE/39/eBwOIhEIjh69CjS09MpyoGFAnM4HAQCASKyCgQCFBcXQyKRIBwOo7e3FwMDA2hqakJDQwM+/PBDzM7OorW1FXV1dRgZGSFPsPn5eahUKsqjYxlhGRkZmJ6ehtVqxb59+xCJRFBYWIhAIEBeQ4uLi+RztLa2Roagvb29CAQC2LdvH5KTkyEQCDA+Po6ZmRk8//zzkMlkZNvR39+PiooKQnuj0Sjee+89WK1Wyh8UiURoamqCTqeDzWYjxdzXvvY18Hg8dHR0YHV1FVVVVaitrcWdO3eQn5+P8fFxWCwW8haqra1FKBTC4OAgKioqUFtbC71eD6fTiaSkJKysrMDtdiMzMxM3b94kJC8jI4PUa0qlEllZWVhZWYFMJkNzczP6+vogkUjA5/MxNDQEACgvLwePx4NWqyVn9vX1dQwMDGBrawvnz5/H7du3EYlEoFAooFAooNfrwefzkZWVRYHYr7/+OgDQZ/PSSy9BKpWiu7sbZrOZGvf29nbYbDYUFRVhdnYWfr8fa2trpAYWiUTY2dnBgQMHsL29jYqKCrzzzju07hw+fBjl5eXo6emBwWDAU089Ba1WS3SAYDCIN954A0KhEDKZLMH3zO1244MPPkBLSwtF73waYry1tQWDwQCNRoOVlRXU19c/ErVlBxsAhH4/KAyJN6fkcrm/EfL14+o30hD9R6onrSHy+Xzo7+9Hd3c38vLyoNVqyWSxqqoKoVAIWVlZmJycxLvvvovs7Gw0NzcTvC0UCklNoFQqiRQdDofJ+4QZJQLA008/jfX1dfKCYflTjKu0vb2NY8eOQaFQ4J/+6Z+QmpqKYDCI1NTUhGBc5lbMjP8YJ6a4uBharRabm5tYXFyk00QoFMLGxgbKysrQ3d2N2dlZ7N69G0VFRcjJySFiK5vLM3M1nU6XcPOxRoW9x6SkJKSmptL4hfE/urq6oNFo0N3djd27d6O5uRmhUAgCgYC4AAUFBY9N2GbjLLvdDg6HA61Wi42NDVy6dAkZGRng8/mwWCzIz88nlGx4eBgHDx5ELBYjibLf738IAQF+hZSkp6cjFosloC+Pg7VZtEooFEJ2djZ0Ot1DBGtma8Dy/pKTk8l4j6nyhEIh+vr6aKxWUFAAgUBAoaLLy8tYWVmBSCRCOBxGc3PzQwRN1mxsbGyAz+fj5s2bOHDgAPLy8hI4UB999BGam5sxPj6O8+fPU3yHWq2Gx+OB1+tFWVkZJicnySIgJycH29vbD0mbmW8OcF8dlJWVRbwoAISmMV6Z0WgkObzVaqXIEIvFAg6HA51Oh+rqamqOgPsHwPh8vXhEi5HjmR8RAMjlcjQ1NeHu3bsk/X7++efB5XIxPj6OlZUVytBraGig9y2RSHD37l3iIW1tbeHkyZPEgdra2sLo6CihbcywkMPh4Pr167BareByuRS909zcDKVSiVAohPfffx8NDQ0YHh6G0WgkU9L29nbs2bMHdrsda2tr+OCDD0iJJpfLUV1djYGBARgMBpSVlRHPsKioCDqdDj09PSgoKIDBYCAC//Hjx6FSqfDDH/4QWq0WKysraGhoQDAYRDgcpvHd5cuXoVKpoNPp6GA0OTlJBPcHq6WlBa2trXC73bQ2ORwO4uCVlpYiJSUF09PTmJ6ehlKpxNjYGCoqKqDX6ym8mrnqm81mACCLhPz8fGi1Wty6dYsOf+z7ZM3vxMQEvF4votEoZbKdOXMGS0tLyMrKQk9PD6lR2WP3799PWWqscUxJSUFbWxuWl5exurqKCxcuQKVSYWtrC+Pj4xgaGsL58+dJgTY7OwuZTIatrS0cPnw4oUH5vCOvz4sQsYPNzs4OIaj/4RGi/0j1pDVE8VwGsViM9fV1Ck9cX1+nhkClUtGIJT09Hd3d3eRozHx53G43lEolGTampaXBbDaToWB1dTXZwjMCL4/Hg9frxcTEBEZHR7Fv3z6Ulpbi5z//OYqLi3HlyhVK4na73XTjVFdX00mdkWI5nPuJ5sxAbm1tDTs7O8jPz0d9fT0EAgE6OzvR1taGxcVFyOVyBAIB2O125ObmUlI7IwPn5+eTBQA7vbMbkTkFC4VCirAAQKG4FRUV+MlPfoL6+nrMzs7izJkzJPtnSMWnLSysARAIBEhLS4PNZsPg4CAtsNnZ2aQsKygoQEpKCqqrq+n0GwgEsLW1RSG3GRkZCbB0vMQ4LS0Ni4uLj0VHWLlcLoyNjUGpVKK0tJRQsXjeDgt0ZUowrVZLTRbLgVpYWIBCocD4+Dh9j16vlzyIWA7U6uoqKioq6BT74GfF0D+v10tu1PGGkswh/MqVKzhz5gxSUlKwtLSElJQUrK6u0niLkYZZxl1+fj6Sk5Ph9XoTvjP2XlkcCTPb3NzcJPWhRqPBzs4O1tbWoNVqKaKFqSs3NzeRl5eHxcVFVFRUkPUEa3yY4o0p8qxWKy5evIhTp05hbW0N09PT2NjYoE08NzcXL7zwAubm5tDV1YX9+/eDx+PBbDZDIBAkRNKcOXMGxcXFZGook8kwNTWVgBDpdDoMDAyQi7rL5YJGo0Fzc3NCkG13dzccDgfkcjm9r+eeew6vvfYaqqur0dHRgbq6OgwODtLoKTc3F/n5+ejt7aX4mKSkJCgUCpw6dQozMzOYnp5GXV0d+vr6iPPncDigVCqhUqmI77i+vo6ysjKkp6ejsbERLpcLb731Fp599ll88sknmJ+fR0pKCr7+9a/j1q1bKC4uJml8X18fpFIp9uzZg4GBAbr/Z2dnAdxHfvbs2QOv14uMjAxSpEYiEayurpKh7eHDh2E0GiEQCHDp0iUUFxeTTQGHw6GRptFoTMhKLC0txdbWFpmRxnM4GVF/bW0Nc3Nz8Hq9lAHHLARYU8WaYgB0H+fn52NsbIwUanv37oXJZCIDzrS0NLrOCwoKMDMzA51OB6vViu9+97tYW1uDyWSCzWZDW1sbxGLxvynf51EqsyeZZ/RFQ/RrrietIVpdXcX169dhMBhQWFiI2tpaeDwexGIxqFQqSsVmHiwA8LOf/YwI08eOHaMNni2izCI+NTWVFhzm6XLy5EkyeIvffEOhEKampigjy+fz4Z133oFWq8X09DQyMjLA5XJp4927dy95I9XX1+PKlSvgcDiorq5GSkoKPvnkE1qEVCoVWlpakJmZScoqkUgEt9tNhoVms5lGA0qlkm5ONkNnPKHHQbXxiEsoFMLAwAC0Wi0Z/zG/k3ijPgYBfxr8+2Ba+aVLlyCRSFBVVQWZTIbx8XFsbW1R+jzjAJWUlBABnJlFLi4uwmg0IhqNQq1WY3V1lWD65uZm4hFdvXoVe/bsIRM/hoQ8bpQWv4DF/834JvFxROp43gAAakwGBweRnZ0NgUAAqVRKXBnWKLBrZnp6mgw92TUX37SyvCqGSEYiEaysrJBhH7MisNls2NnZwZ49eyjHLR4hYs7bb7/9NiltGELmdDoRiUQSQkNZtEVlZSWN9iYmJkjVU1ZWBolEgtXVVWRmZpIx6erqKpRKJXkb/b//9/9IkcRSxFmsDJ/Px4EDB8iLSSQSkV0AANy+fZtieVgoNY/Hw+DgIHg8Hvbs2UMjw9nZWXi9XvKqcrvdWFpaAo/HQzAYxLPPPguhUIhf/OIXqK2tpfusr6+PQkibmprQ09MDDoeD/fv34/bt2yT939ragtPphEAgQElJCYxGI7nKHzlyBOFwGAUFBZifn8dbb72FWCxGcvOcnBzodDoYjUZqArKyslBdXY2SkhLweDykpKRQjAYL+AbuCzTW1tZgt9uxf/9+dHZ2Ehonk8lQVVUFlUqFnp4eVFRUYH19HdnZ2dQ4zMzMYO/eveBwOBRBxOrcuXPkiC0SifD222+jvLwctbW1WF1dhc1mI+4Z8zdj2YfMTygQCKCrqwtbW1vYv38/FAoFDAYD1Go17t69i1gshn379pEaLb7kcjlFgAD3uUNnzpxBV1cXHQY2NjZw+PBhUqnabDZ4vV6EQiFIpVLk5+fDbDbj/Pnz4HK5GBgYwMrKCp5++mlqBpmz/aeNlD9vfZ5m519rnvhvWb8R2f0X9e9TsVgMU1NTpJqYn5/H5OQk5ubmMDIygs3NTfB4PCI4M6dmtijZbDZsb2+T9JplGDGSbiwWQ2ZmJkQiEWQyGV544QU4nU6888479DfjJecSiQRjY2P467/+a1gsFpLwNzU1YWdnBxqNhvLJent7AQD19fW4fPkyIpEINjY2MDExgdu3byc4RtvtdjidTphMJkKt2KjL4/Fgbm4OTqcTGxsbSE5OJlO+WCz2qWGc8TbwjKS6urpK0HleXh6++tWvkrqGbThsUWIRDQ9WNBqF0WjET37yE2xubpKCis/nIy8vD7t27UJSUhLefPNNLC8vk+cMg6llMhmRnNljnU4nebwwWwTG0xEIBAT7//KXv0RFRQV+8YtfQK1Wo6+vj+IM4uX2Ozs7ePfdd/Hnf/7nRPpkTeODrtBMqv/gAsh+zop95hMTE8jNzYXJZCJCMGsY4uX6brebTuUFBQWw2WyU2cWac7lcTt5HwH1+Avvud3Z24Pf74fP5IBaLaSwikUgSeE/sdX/88cfkizQwMAAulwupVEpqQaamvHz5MtbX10lRFQgEYLPZaCPh8/mwWq2YmZmh5Hc+n08BxZOTkygpKcHk5GSCfUIgEIDD4cDevXtRXFyMI0eOIBKJUAMWHxIcDAZplMKu1ZSUFAwNDcHlcmFzcxO3b9+G3W7H/Pw8ef+kpqbCaDRiZWUFRUVFcDqdyMzMREdHB15//XVkZWVhYGCARkI7OzuUIN/R0YHa2lq43W4MDAxg3759WFhYIF+p/Px8+P1+itl4+eWXUVxcjPfee4/Uajdv3iRfnImJCUpaZyPv+O8+Eomgr68P9+7dg8fjwfXr1xOUg3q9Hn6/nxqvmZmZhPvN5/Ph9u3bWF1dxXe+8x1sbGzA7/ejt7cXVqsV8/PzqKioIHuMuro6sqlgpGIul4usrCwoFAocOnQIRUVFcDgcpPbb3Nwk+XtVVRU0Gg2Fi1utVkQiEWg0GkJ/QqEQVlZW0NnZSQez2dnZhNBUAOQ6n52dTT9bW1vDu+++i9LSUuj1evq8enp6MDQ0hGAwiJqaGhpfMWuSiooKypGbnp6GTCbDpUuXYDQakZmZSY3og6jUv6Q+K4IH+JUFx4Pr7W9bfdEQ/ZaVz+dLgHJZ4OPq6ircbjc++eQTDAwMYGlpCZFIBHfu3EkgV7MRRUZGBlQqFbhcLvLy8uB0OqHVakmeXlBQgNbWVgBAd3c3VCoV3nvvPQD3fZD+7u/+Du+99x4mJycxMDCA2tpavPnmm6iqqqL5OvP72LNnD3Z2dnDw4EFS8Dz33HMUz1BWVgaFQgGXy5XwXmtra6HRaMhNORaLob+/n07HYrEYS0tL2NzcxNDQEPh8PjV7bK7NwiFZxd/c7CZeWlqCVqsl0iNDTDY3N+Fyucj7yGazITU1FS6X66F8HafTiRs3bqC4uBjvv/8+/b3R0VHo9XpMTEygo6OD0KCdnR3i9bDx4oOLCfMeSk9Pp1N5amoqVCoVmVfabDYcOHAAd+7cwfnz52E2m1FRUUFkx/jG5u7duxgfH4dcLk9QKQKPzgti+Wtra2vkWRXvi7O1tZUw9nQ6naipqaH3w/yCgsEgxsbGiKy+vLxMnjvscSyJ/HHNbFZWFqxWK0WzMEdyhUJB8vRHLdynTp2ihqipqemhPKTt7W3cvHkTy8vLuHXrFsLhMPh8Po0qGBrHOEZsrGU2m+FyuaBSqeB2u1FXV4e1tTU0NTXh9OnTEAgEUKvVmJ2dhd1ux+LiIux2O8LhMIaGhijkNd77p6CgAC0tLUhLSwMAHDhwAEKhkHhr0WgU+fn5JBNn71UgEKCoqAi7du2CzWZDSUkJ1tfX0dDQgOeeew4WiwXNzc3Iy8uDz+eD3W5HaWkpFhcX8corr6C3txcajQYbGxt4//33sbGxQXl7LpcLR44cwR/8wR+goKCAokQKCgpw5coVSKVSHD58GAqFghzsh4eHUVlZifr6euzfvx9CoRAcDgcNDQ0Ih8MIBAJYWFjAyMgImpqaKBMzJSUFzc3NOHHiBPh8PoLBIHQ6XULDxMjOvb29+PGPf4yZmRlYrVYIhUI4nU7s2rULZrMZmZmZkEql2N7ehkQiQWZmJvLz84n/wr7XnJwceDweDA8P48c//nGCx1swGMTOzg59HgaDAYODg7h06RIWFhawsbEBs9mMK1eukFpsZWUFFRUVaG1tRWFhYcK1VlNTg6eeeuqhMGzGLYpHsnZ2duieW1tbQ3t7O91n3d3dmJqawubmJnGTnE4nnn76aRQUFJDKUqPR0LoeX/G+Xo+qB9eC+GaHrQkLCwtYXFyk0TX7/p60cdk/t74YmX2OepJGZiyjJr4pKioqgtlsht/vR0pKCjIzMxN8NyQSCXw+HzlaMyk9u3jjRzzRaBSTk5NQKpVITU0lsvHIyAgOHjxI+VNsAeByuXj66adx+fJlnDhxAtevX0dbWxvKysqIJxSNRolcu7Ozg0gkQoqyoqIiXLlyBTqdDlNTU1AqlRRPkZeXh5qaGspZW15eJsSGeb6wRb6wsBCrq6t0CmTKMUZIZQ0S8Cu5PPv/PB4PY2NjKCoqIqQmHA7D5XLRYs64NCx6gTVODGVjGVNMscNUfMFgEKOjoygsLKTxQHp6Oql3Pi/ZMV4qz+fziT+0vb0Nq9UKvV5PaMeD3KlwOAyDwQCfz4e7d+/C4/HgxRdfTHBCjoe8mZtyNBol3ygW3cCuJZZQr1ar6bFMhs/Uf0wBtL29DY1GQ07YLNWeRZdIJBIazdhstodGs6zipf6MpOn1eileJr6Riuc13LhxA6urq9jY2MAzzzxDn1tycjJWVlbw6quvQiqVwuPxoK2tjfhuLpcLCwsLyMrKgk6ng9/vTzDfc7vd+Pjjj5GWlobjx49Tc7Ozs4Pr169jYGCA0A2hUEj8l9bWVphMJuzatQsqlQp8Ph937tyBwWDAs88+S+aSDocDFRUVKCwsJK7S3bt3sbKyQs1wS0sLPvroI8jlcmRnZ0OpVAIAOVhvb2+jq6sLLS0tyM3NhcvlQmdnJzY3N/Hss89CJpNhdXUVP/jBD5CUlASpVIpYLAaFQoGamhrU1NSQcIGpA1kEzaFDh5CWlobe3l60tLTg0qVLxBc8evQoGW86nU6kpKTAaDRiYmKCfMLEYjEKCgrwk5/8hL5jLpeLr33ta1hdXcXOzg5mZ2dpvMXGmePj47Qes8rPz4fFYkF2djZqamqQkZGBQCCA7u5ueDwecLlcOJ1OcnBeXl6GRqNBQ0MDZmZmHvJrA+6r6Orr67G+vo709HQMDQ2RDcLjih0019bWwOFwEAwG4fV6UV1dTaHVIyMjCaRsds+w3DHg/qiVjQ1ZlIrRaMTFixchl8tpTFpYWAiv14uzZ8/SPcMOLgASrEVYfZqvF4sNksvlNNaMX4O2trbg9Xrhdrshk8kQi8Wg1+ufyFEZqy84RL/mepIaokgkgvfffx/j4+P0s5KSEpKPRiIRrK2t0ciEVUtLC4qKiuB2u5GWlkbBiEDiZmi32wkZsdlsMBgMaG9vRywWg9FoRF1dHfli7OzsUMAvUyu99tprOH78OCKRCCkVbDYb5ubmYLVasbq6SqoqHo8Hv9+Puro63L59G/n5+djc3ITb7aZNeffu3VCpVDCbzdBqtdje3qbIB7bAMLK0QqFIMMELh8P0fr1eL0wmE1paWhLcqVnFc2PiYxYeJAazhYHxrxifaG5ujjYXhUKB7OxsbG1twWKx0KJhsVjodM68jJhiRiwW4+2338axY8ewurqaECkS//rYYxjfgI0kH9dEAPfl59vb21hZWaEFMD8/n/Ky4t8XQ9VkMhncbje9z0AgQJs08w3icrn0fPGuykKhkJzFWZMdDofJMNTv92N6ehrZ2dkkSQbu82yYcV18XMfjKhaLYXx8nHx8ysrKYLFYiKzt9/shFArx/vvvk/VCSkoKXnzxRWouhUIhpqen0dHRgRdeeIFIwRsbG/jFL34Bv9+PkpISHDlyJGHz2NnZwQ9+8APs7OwgLS0NOTk5OHbsGACgq6uLxhgLCwtQqVRobGzEwsICcnNzCRnJzs6GWq3GwMAA+vv7yWfmhRdeQGdnJ3Q6HSorK+l7ZeT3oaEhuN1uNDQ04J133qEQztzcXFRUVMBut0Or1UKtVuONN96ARqPB4OAgANC1x0j+1dXV+NGPfgSBQEAqSZZd6PP5UFBQgKGhIbS0tGB6epq4LOz+FQgEyMvLw/LyMjlLA8DRo0fR1NRE8Rurq6sYGhqiBpEdyhYXFyEWixPQEbVajS996Uu4ePEike83NzdpnNTQ0IC1tTWK4nmwdDod6urq6HucnJwkI9RgMAgOhwO5XI5gMIiqqipUVFTgpz/9aUKDBdznK+l0OhQWFlIzMDMzQwgvq9LSUszMzEAgECA1NZUk6Qy5q6+vRzgcxuLiIgYGBiil3u12o6amBqOjo7SOnThxAnl5eTCZTOjt7UVaWhp8Ph++/e1vY2FhAW63G4ODgzh37hwCgQBWVlZQXFyMzMxMej2fxXWMPwA/KqeMx+PB4/HQ+M/n88HlckGr1ZLww+fzgcvlIjs7O0Eg8yTWFw3Rr7mepIYIuM8bunjxItxuNwQCARobG5GamgqdTgfgPv/mgw8+oN8/duwYLVpSqRSBQICaC6YUiEajMJlMZLDGDB5ZnT17FhqNBj09PdDpdMjNzYVAIIDFYkFOTg4CgQBee+01FBYWYmRkBM8++ywsFgvu3LmD0tJShMNhzM/Pk5xXp9OhpKSELP1Zlhfz72CPY3lDzzzzDNRqNUlZ40840WiUGqZgMEg3MUNw/H4/pqamkJmZCbfbTanf8U0QcH+hiMVitJA8iijIfsak2mwhYKGmACiTze12w+fzoba2NkHhFAqFUF5eDovFguXlZeh0OnR0dKClpQVXr14lbtGpU6foM2GNid/vh9VqRSwWg9VqRTAYJNO9kpIS+r7iGzehUIj5+XlKstdqtRCLxQknwPh6cMFk73lychIFBQVkxPhgnhj7PDc2NoinEo1GUV5eTmjj1tYW/H4/cc9yc3PJDmJzc5PQLh6P9xDx2+FwALgP4UskEkL3ZmdnUVZWhpWVFUxPT2NnZ4dGT2xz+OCDD4isWl5eTo0fi3Rh6qysrCz4/X7cvXsXPT099JnU1dWhvb2dkMXr16/D7XbDYDBAKpXiueeeIw7c3NwcZmZm4HA4cO7cOYyPj6O5uRkOhwMLCwsoKSmBxWJBRUUFoY+XLl2C3W4nn5mdnR0KBmXjz/jvlH3mdrsdly5dglKpxJEjR7C5uYlIJAKpVAqJRAKr1Yo333yTTvSBQAD19fUU18FcvF9//XWIRCKcPn0a169fT8j+ysnJwfLyMk6ePInh4WFsbW0hGAyisLCQbAra2tqwsrJC9y2LZpHJZNjY2EAkEsGVK1doY21pacHg4CDS09NJ0h8MBiEWi/HNb34TycnJuHbtGgCQopZJvWUyGaqrqzE6OvoQaRm4j9I0NzdDr9eTGkwoFOKNN95IUL9mZ2ejtbUVAoEAPT09mJmZQTgcRiwWQ1paGuVCSqVSuFwu3LlzBw0NDejs7HwkL4ehp/GVmpoKr9eL1tZW9Pf3g8vlUjPFLCsUCgVmZmao+R0YGIBGo0FeXh4MBgPOnz9PI92mpibk5OSgq6sLlZWVyM/PRygUopxJdg3+SxVfDyrItre34fP5wOfz4fP5iAf121RfNES/5nrSGiKr1YqOjg6Ew2HI5XKo1WrU1tbCbDbTSGppaQlra2uQyWQoKCiAXq9Hamoq5ubmkJGRQSgDU5sB9xUQ77zzDlJSUuB2uxEIBMg5+rnnnoPVaqVgQoFAgKqqKkJf0tLSYLFY8Pbbb6OmpgZCoRB3796l1ywWi8Hn88m9mak6KisrSQ7+oGuwyWTCe++9h9zcXCwtLeGP//iPYbVaiXjLECKHw4HU1FRSn7HHW61W5OfnY319HRaLBV6vF8FgENXV1dQQss2eydEfNXKJj6aIf50PLgw+nw9zc3NwOBxwOp3weDzEiWENos1mQ15eHiFnbHPJy8tDZ2cnRX5kZGSguro6wbGYvdZYLIbR0VFqxDgcDkpKShK4Ah6Ph0jXGo3msQqzz7O4sd8XCAQwmUwoKCj41M+BcRS2t7ehVCoTPEgejMtgxnSPasziG1L2PTB1nkQigcfjwSeffIKnn34acrkc9+7dw9LSEoRCIYRCIdrb27GzswOz2Qyz2UzfcWlpKYLBIPh8foI8mqGSXV1dSEtLg91uJ2VmVVUVPB4PGWgmJSXh9u3bEIlEqKysTBhBx/sv3bx5E2VlZZienkZzczO4XC7GxsZQUlICLpcLkUhEJGaz2Uwj3+vXr9Pj4k1wP0vNs7GxgaGhIeTl5aGgoADr6+tkZuh2u9HY2IiWlhYAoDVNLBbDbrcjJSUFk5OTGBsbe4jPx7LMjh07hkAggKWlJQpHzszMJIftoaEhdHV14ciRIyQqkMlkSElJwcjICDk8Nzc3Y2NjA8PDw+R8LRAIcOHCBZSWluLjjz9GZWUl+vv7cerUKdy4cQP37t2j11NXVweNRoObN28ScgoAWq0WOTk55LXEUEObzQa/3w+TyQSZTAaBQIDMzEzodDo6BF29ehUbGxvIysrCU089RfeM3+/HT37yE/JKa29vfygFgBVDlZOTk8nhnBVrIJmCEbi/Lp49e5YMXH/+85/T7+v1emRkZGBlZQUWiwXl5eVElGYj6dzcXKyurlL8x+HDhwn9fNBV/lGI0GdVLBYj3mq8zB741X3OTHSf1FzTLxqiX3M9iQ3RnTt3MD4+jv3794PL5SIYDKKiooLm6xsbGxRToVAo8O677+LYsWNoaGiAy+WC3W4nQ8OWlhZCZra3t9Hf34/S0lKUlZXhvffew+nTp8nL6Nq1a0hKSoJAIEB+fj70ej0tAuykkpSUhOzsbHR0dBBJkY3opFIpkpOT0dvbi/b2dhQWFpL/C8vf8ng8xDVITk7GlStX8Oyzz1JDxeVyweVyUVhYiGg0io8++gitra0UO8Lk0xsbG+DxeJDJZAiHw5icnCQCeU1NDcRiMcLhMMn2H9UcsNgOJmv+tFFOLBaDx+PB1NQUuFwuNjc3iUwO3OfyvPfeeygsLERjYyMpyNh4IykpCS6XC+Pj48jOzkZeXh4hRA/yn6LRKGUhMXJ6/ILEGqbl5WWUlpZSSOmDETvxZGRGav40cuTjkq7/OeZvj3o/j3pM/O8ybgPbaEKhEH7605+ioqICVqsVr7zyCl03fr8fe/fuRTQapbHJz3/+c4hEIrS3tyfI3TkcDrmdb2xsoLe3F3w+H2tra6irq0NjYyOlsR85coQMND9PThxrnHp7eyleh7ltX7p0CTk5OSgvLyePpXiTTZfLhZGREbS2thJCFP+8Xq8XnZ2dOHv2bMKadPv2bUilUrjdbpSXlyM9PR1GoxHT09MQiUQoLy+HRqMBANokORwOPB4PDAYDqqqqYLFY8NprrwG4P/ry+XxYXl5Ga2sr5ufniSezubkJmUyGmZkZFBcXY2NjA3fu3EFeXh6Gh4eJ48fCfUUiET7++GOEQiEcOnQIV65cgUajIfNQ4D6f5pvf/CbEYjHeffddhMNh7Nq1C4uLi6RSZcVGpfH1pS99CXfv3oVKpQKPxyOjVja2C4VCCIVC0Gg0yM7OpvcwPDyMxcVFpKWloaqqCk6nk7LD7ty5A5fLRe7hTI5vtVqpsQHuN3THjx9HR0cHGhoacO/ePfJuAu6j7OyejbcZqK+vB5fLJWXrjRs3kJ6ejrKyMgwNDdH9Cdy3DeDxeOjq6iKyvd/vx+zsLE6cOAGdTpdAiGbr4YPeZo+z1PB4PJidnUVOTg6tScCjG3GHw0GIGp/Pf2zO4L93fdEQ/ZrrSWqIYrEYxsbG8NFHH9HNduzYMYpzKCsrw8zMDAoKCrCzs4Pi4mL8zd/8DT3+S1/6EjQaDQwGAykUNjc3ceDAAXg8noRRVPx4iC3kzPeCqTaYV0w0GkV/fz9SUlLgcDjA5XJRXl6ODz74gMzvGhoaIBAI8MMf/hBNTU0YGRnB0aNHKfeMRXCYzWbweDziMDQ2NmJqagqjo6Oor6+Hz+dDcXExIpEIrl69Sg67X/rSl+gzGh0dJQPK+KDTsbExlJaWQqvVJjQbn5bXEz/KYXL4x522HiQsxi8+P/vZzyinrbS0FLt370YkEsHU1BTy8vLo2nrca/qspiMcDmNubg4cDgd5eXm4ceMGGXN6vV7U19fj4MGD8Hg8mJycREVFBTWdFouF7BriXZfj/7bP56MAYZ/PB6PRiNbWVgiFwn+RD8k/B6na2toiMrler8fdu3cRiUQwNDSEM2fOQKvVPvbv/+hHP4JcLofdbkdDQwONTOOfe3NzE8nJybTx6nQ6FBQUPKQI+jyvmTWrkUgEKpUKUqk04d/feustrK+vIxwOY9++faitrQUAcn8PBALkBL2zs4NYLEZhs2xk9t5779EB6JVXXgFwX9bOeDrl5eXIzMxEUlISNjY28NZbb+G5556jMaLdbkdPTw/OnDmDYDBIBHeTyYTKykrKFWNxG4uLi/D5fFCpVKQIzc3NJQLu2toaioqK4PF4qJmKRqNITU1Fe3s7wuEwBZIqFApMT0/jyJEjGBsbo7EMACgUCng8Hpw7dw4TExMUeXHgwAH80z/9E32GbKTNXicAXLhwgZzWh4aGyMGfuagfOHAAJpMJGxsbKC0tpUPaysoK3njjDfqsmcfU0aNH0dfXh/379+ODDz6AQCAg52+fz4dgMEivRygUoqWlhUKyH0TYWltbMTs7i2g0Sp+by+VCY2Mj+vv7aQx48OBBrK2tYXJyEhMTE+RZlJycjJaWlgS0MBKJYHFxEevr6ygqKoJAIKCxnc/nQ1JSEnmhxbvfP3ivxosVpqamiHLAEKrHXfe/iwjRk/kOvqjH1vb2NjIzMxNOHkajkQiZg4ODUKlUWF1dJRJhfDEO0fb2NnQ6HZaXl2l85XQ6iRgZDoexvr6OX/ziF/jkk0/oVCSXy5GVlUVy1Q8//BA/+tGPMD8/j9XVVVy6dIl4RWNjYzh16hQOHTpEuUBMAtzX14fjx49jZmYGwWAQXC4XFosFOzs7EAgEGB4ehkQigUwmg8lkwvDwMDQaDYaHh8kTRCaTobW1FcPDwzh16lTC+ywoKCCEICMjA3l5ecjIyMDhw4fJtPBBr51wOIyxsTF0dnbS6HFlZQUpKSlkJhcvEWfFpKh2ux1yuRwGgwFvvfUWnE4nvF4vZmdnMTk5ieLiYvquamtr4XQ6MT09DZVKRb4p8a+JNSF2ux3RaBTb29tkQPioMhqNlJHEIlPS09NhtVohkUgwNTUFp9OJiYkJKJVKMohknJ75+Xn6zB513a2vr0Or1cJiscBoNKKiogIDAwMAfiXNZQoytkmx98BCcxmnh+WFfdr7YcXObEajEQqFAmazGU1NTeDxeHjmmWeg0Who9Mb4RvF/7/z583A4HNDr9SgoKCCrhnhZMVMKVlZW4qmnnoJeryeyd3w9eM2w8XT88zmdTjo5PypigoUls/R3Vnfv3qXR6MTEBI2o2YbFkAIOh4P29naMj4/j7Nmz1FD09vZCLpdjc3MTqamptEG988472LVrF9555x363e7ubpSUlODdd9+F1+vFwMAA3nzzTWRnZ2NlZQU+nw9TU1Po7e3FxMQEKisrcfToUeTm5tKILxgMUj5ecXExYrEYzGYzVCoVcnNzoVQq0dbWBuC+rw6z0DAYDGhoaMCtW7eQkZGBkydPQqfTIT8/n0ZW7B6cmJjA3r17oVarodVqUV1dTSanbBNuaWnByy+/jKqqKuj1ekxNTZGFxq1btzA+Po709HT88Ic/xI0bN+D1ejE0NIS33noLS0tLpAhl319ycjJF+KSkpGBiYgK7d++GWq1GSUkJ9Hr9Q+vq4cOHEQ6HqZni8/koKSlBUlISdDodZmZmaPQ0OzuL6upqFBcX099nGYcbGxvo6+ujkGyZTAahUEgHKI/Hgx//+MeYnJzE9vY28vLy6GAD/EphlpKSkjCOjkeQH/QMYmvazs4OSkpKsLW1hdzc3ARfONb8GI1G2Gy2BJXpg67/jHT924i1fIEQfY56khCiSCSCmzdvoru7GwAIdmdZNrOzs+Rjkp6ejvHxcZJzVlRUoLq6GtnZ2SQ1LygogM/nowZpYmKCOAWLi4swm82kojl37hzee+89jI+PU67PzMwMMjIySAovFovh9/tRVFSEpqYmuN1uMuFjKieHw0EqkfLycphMJnz1q1/FjRs3YLVaKUNIKBTiO9/5DqRSKYaGhjA7O4v29nYAIDv8+BudLYANDQ1kSKbT6Shc9LNqZmYGvb29sNvtRLhUq9UJj3/UPN7hcCAUCmFnZwdcLhcff/wx6urqMDw8jNbWVgQCAbjdbmxtbZEBn16vR1paGtbX17G4uIji4mI64bFiC4tQKEQoFIJIJKLGyOPxPKQsi0eIioqKKJlboVBgeHgY58+fR1ZWFiFELEOMx+ORx4/FYkFjYyONP5kVAfArRVx2djbC4TAGBgbQ2NiYoIZ7ECFjp1UAdGK1WCxQq9VYXFwkfklSUhJl7DkcDtTV1dEiz0606+vr2NjYgFarJU8jm82Gjz/+GKdOnSIOVXxILDPPS01NxdjYGNLS0ogzolar/9VSYWay6fV6KdWebR5+v5/QT+BXp2wul4u+vj4IhUJSaKpUKgSDQdy8eRORSARFRUUkk4/FYlAqldSUAqBRV1JSEqXRBwIBXL16FSUlJVCr1cjPz4dYLKaIDIYQuVwu3Lp1Cx6PB2fPnsWlS5dISh8IBGis+M4776CkpARzc3P4gz/4AwD3Rz0sqoUJKHp6etDc3ExGlcxdvrm5GbFYDJFIBFarFSMjI6isrERycjLsdjuKioqQnZ2N4eFhlJSUULBob28voTWhUAjf/va3EQwGEQgE8P7770MqlUKtVtM6l52dDT6fj97eXkilUty9e5cUkdXV1WSsCtwfa7GcxZaWFhgMBiQnJ2NmZgZ8Ph+hUAhNTU0UZ7SxsUE5iEeOHMGuXbvw/e9/H1qtNkHp+53vfAczMzPkqRRf9fX1WFtbI6f1zMxMTE1N4ctf/jKFAYvFYlLwmkwmGknGYjG0t7eTgKGrqwtlZWVYXFzE4cOHafzLmvCUlBS63j4vR/DzcIyWl5exs7NDI0aZTPbIEdnnTQn4TdYXI7Nfcz0pDRHz7on37QDuk+/UajVGRkagUqkSPC7YAqDX61FeXg6tVguNRvOQesnhcJAR2ebmJpEMWbW1taGqqgp///d/Tz9jSc7r6+s4ffo0rl69CrPZjMbGRhw8eBA8Hg/r6+sIhULkYJyenk5Bhj6fD263G0eOHIHVaoVAIIDX68XMzAz9jZMnT6KhoYHGdiKRCHNzc6RCYq87IyMDy8vLaGlpwdTUFPbu3QuhUJjAzXhUxS8GMzMz6OjoICL5nj17kJKSArVajaWlJbLAjyfNxmIxzM/PQyAQUIih1+vFhx9+iOeeew48Hg8LCwsQCARkHscy2ljz4Ha7iQgZDz2z0xb7OctaYr4on8Vp+qwxFjMKZIq5mzdv4ty5cxAIBJiamiI0qLKyEikpKeSpVFNT80jrgvjPk228LIEdAJGr2YhOKpWSDFqhUGBiYgJmsxk6nQ7BYBCNjY0JQbQ+nw8ikYh4VwyhrKqqwsjICL71rW8lkOHZ42KxGBYWFpCfn0+RMizrj5lw/msIp+zxjwuwjFeHsVO7SCSiWBAej4fU1FRqetjvse9FJBJheXkZXV1dKC0thVKpRDQapfw7kUiEjY0Nis0RiUS0DiiVStTU1MBkMqGkpASpqam4ceMG9Ho9pqenweVy4XK5CIGqr6/HysoK9u3bBwB44403UFRUhJMnTyIajWJ8fBzXr1+HRCLBwYMH8dFHHyE7OxsWiwXPPPMMlpaWsLKygsLCQoyPj4PH48HhcKCsrAwajQaRSITI1r29vcjPz8fu3bvJpoHP54PH41GWYnJyMmQyGV566SUkJSVhaGgIarUa09PT2N7extzcHHQ6HdLS0qBUKsmSgMPhoKmpCQKBAENDQygsLMTExAR4PB7Ky8vhdDrhcDjIUDIpKQkOhwPHjh1DU1MTVldXsbm5icuXL5Palsvl4plnngGPx8Ply5eRm5uLoaEh4kolJydjbW2NmlZWHA4HFy5cIOTm3XffJV7P7//+72NhYYGc0Ds7O+neUalUSE1NxebmJnbv3k3RRUNDQ5QfyYxew+EwdnZ2aHzFDj0P2nf8Sys+Poe5wj/qfnlQCfkkKNK+aIh+zfWkNESM6/Dqq68mjBkyMzORmZmJvLw89PX1EXGPFSOHulwuBAIBckeORCLw+/3Y2NiASqWC0+lEdnY2bt++TcnZzB/j6aefxvz8PBwOB1ZXV5GRkYG2tjZEo1Ho9XqYTCYsLi4iEAggOTkZxcXFSEtLQ3JyMlZXV/HGG28AuO+ZxMZpAoEAWq0WkUgE+/btIyM7kUhENvh+vx8HDx4kmez169cJ2mUL6MbGBiQSCbntxpvkfVbZ7XZqGhUKBalsKisrUVVVBR6PRx5DHo+HYG42q2cnPmYK91nckvjNlzWiwH30hSncFAoFuru7YTKZ8Oyzz9JJGHiY0/RpGUWsodra2iIInSFr7O+vrKxAIBDg7t27qKyshMFgwMmTJx9CiDgcDvr7+ylcsrW1NaH5+DTzN5FIhGAwSFwpFi7KmhWxWEzjRWa8eejQIYRCIfJTibcdYI9VKpXwer345S9/iQsXLhDh+sF7Jjk5GT6fD0tLSygtLUUoFCIEKRaLJRBOP0+xcGWPx4OysjI6mT+u4rl4jMvhdDrB4XAIFcnNzSV1ksPhwJtvvgmfz4fMzEw0NDRgfHwcOp0OBoMBe/bsIasAhoB99NFHyMjIwPT0NPR6Pba2tiCTyeD1eiESidDa2krWFEwBurW1hcXFReTk5EAmk6GkpARDQ0NoamqCRqOBxWLB4OAgRCIRBAIB9u/fj5/97GdwuVyk0svPz8fCwgL27duH9PR0ZGVlgc/n49KlS/B6vVhZWUFubi68Xi/27NmDcDgMlUqFt99+mzLCGhoa6HCTnJyMwcFBqNVq8hmSSCRIS0vDmTNnwOPxMDw8jPT0dLz//vt0HTc2NsLhcBAv0mKxICUlBSdOnIBUKsXFixehUCiwtraGUCiEjIwMZGdng8PhwOl0wm63o6qqCuFwGDabDcXFxTCbzQiHw9SsAUBxcTFaW1uRkpKC8fHxBEPHiooKug76+vro5y0tLSgvLydE+NVXXyXU99lnnyUkaGhoiNaEQ4cOYXFxEW63G2fPnqXMvOTkZOTk5CQc8B5sQra3tzE0NERcpT179iRYjDzJnkGfVQ8edj7Pe/miIfo115PSELET6a1bt+iGE4vFqKqqglqthkAgwI0bNxJOKDk5OThz5gzC4TCMRiO5p+bm5pKk2263Y2xsDEajETU1NdDpdNjZ2cH8/DxWVlZw9OhRbG5uIhAIUM6QSqVCNBpFQUEBrFYrsrKyMDo6CrfbTX5F29vbqKqqSoCWAVByOONp1NfXY2FhAT6fDxKJBMXFxeju7sbg4CB5xohEIty9exc8Ho/m3wcOHMDi4iL5++j1eqhUKtjt9k8NNIy/qZiZJSMSP2pz93g8sFqt0Gg0CIVCEAgE2NjYgFKphEAgoOiSxcVFhEIhVFRUPKTmAh7tEMvg/JWVFayurhK5emBgAOnp6djY2MB3vvMdJCUlferp69MaE+bfEgwGodFoEv4+24hisfuxKIcPH4ZIJHrk8wWDQfT396Ourg5bW1uEaDwKhXqw+evp6SEOWWVl5UPEbXZts5Ecl8tN8JLicDg0cognyX/ae2eNy87ODrKysijPjRH/ZTIZXC4XjfIePPE+7nmXl5fJA4yZ+30eZR3wK1dittHqdDrw+XxIJBLiHP3whz9MOPCUlZWhsrIS9+7dg0KhoIwthUJBxNqdnR1Cfhj3aWZmBlqtlkQHPp8Pe/fuxcbGBra2tnDnzh0A98nY+/btw507d9Dc3Ezfj81mw+3btzEzM4P8/HycOHECTqeTDBPT0tKwsbEBkUhEMnWTyYSrV69CpVJBqVRCKpXCaDRi9+7dGBoaQltbG+bm5iCVSnHjxg1otVpIpVLMzc1Br9cTD4dZdjBklR2cmMP76Ogo7t27B7/fj+zsbGRmZkKv18PpdKKjowPAfeTc5XLha1/7GsLhMP7u7/4O4XAYXC6XVGitra0oLi6G2+0mbymG1uzZswfBYBBms5lEB8xskaHWP/3pT+l70ul0REsYGRnB4OAgJBIJ8vLyqFFlUTV9fX04duwYenp6yA4kMzMTHo8HBQUFZC3BxrufdY09eL253W5MTk6isbGRkgHY/z4rnPpJrgfH4Z9HxPFFQ/RrriepIVpfX8cnn3yCQCCA9fV17Nu3DykpKdBqtTAajXA6nRgYGKDT+8GDB9Ha2gqXywWfz0fSZQ6HAz6fT6jBzZs36STNLPfz8vJoXnzt2jVamNbW1sh/SC6Xo6ioCHa7ncY6165dw+rq6iPfQ1tbG27fvg2tVgubzYavf/3riEQidGIOh8MQCoWYmpqCQCCgcdrs7CxcLheMRiOA+5tEbW0t+faEQiGCjbOyssib43GfIYOlWeo6cxxmCdyP8iOyWq0wGo3Izs6mjZtxdoD7Pk7xlgCRSAQff/wxDh06RKo9FmvBGpzp6WmYTCaMj4/Ta6+trYXVaiUTv9LSUuTl5ZG3D9sUgV/xBOL9feItBOJl8g8iROzzeNChm43vHmx04o0VGafhQc+Tx1UgEEB/fz8hRI+C0x9sQB4l0Wcjszt37kAikZDK7VHFGhfgPpGZ8S0YEhO/qAqFQszMzKCzsxOnTp2ieI94+XK8CSf73gsLC7G5uYnh4WEUFxdDLBaTWWNxcTEZaWo0GkilUkLFmGqOmd2x99fT0wOXy4Xp6Wl6H0ePHiVkbnV1FUlJSQgGgzhw4AAWFhagVCoxPDyM3Nxc3Lp1CwKBABwOB21tbZiamkJxcTECgQDKysowODiItLQ0jIyMYNeuXVheXsbevXvxxhtvQCwWw2w2o729HY2Njejr68Po6Cg2NjYQjUaJS8jQo4mJCXqN2dnZOHv2bMJIPSUlBTk5OeDz+Zifn0dxcTFFALHcuAfrpZdewo0bN2Cz2chrh0Xl6PV6VFZWIhKJ4MaNG2Q/kJGRgfLycmxubkKv18Nms8FsNmNpaQnt7e1EAbh3715CA+xwOJCfn48XXngBMzMz0Ol0FPOiVqvhdrtx4cIFiMVi3Lx5k5DihYUFygtkpq/AfWuRaDQKnU6HoqIiDAwMoKioCHfu3CFUcP/+/ZSzdv36dRpDsXtVq9Xi1KlTCIVCsFgsSEpKQmFhIXg8XsIh4/MoZIHEsTk7XHyBED2+vmiIPkc9KQ0RmxWPjo7izp072NnZQWFhIXJycuiGMZlM5GfDzBAXFhbw4osvQqvVwm63Y25uDklJScjLy6NFemBggE5pPB4PGRkZ2NjYQFlZGebn5+F2u+H3+0mJMzw8jIKCAoTDYWRlZSEYDKKvrw+hUAglJSUJqBCPxwOPx0NpaSkOHTqEoaEh3LhxA7t27UJhYSGCwSAp41hw6+zsLJKTk6npuXv3bkLwolAoRFpaGk6cOAGFQkE3icfjocTrDz/8EMFgEE8//TSmpqbIq0kikRAp1+/3Y319HSqVCg6Hg5QVj3Ko7unpQVJSEsbGxijjiBFM09LSoFAokJSURLEYr732Go0H9+3bh6qqKoRCIZK1vvfee7TRjI+PE5/kzJkzEIvFGBwcpCaLNVjhcBg+nw9yuZy8g8LhMAoKCuB2u0mxwlSHFouFyKJFRUXEOWC8pO3tbWxubsLv92NzcxM5OTkQCAQPNTqM/Lm0tITy8nLU1tbStfNpPkJOpxO9vb04dOjQZyZhb25uYn19nZAF9hwPLvzMlJQlxbe2tj5yc/B6vTCbzYjFYtDpdEhNTX2IV8X+e2FhgbyBlpeX8eKLL5IK7EFUKv45AKC3txfp6elk/siI6hkZGXTo2NnZgU6nIyNUn8+X0Byz52SEZ5abxq6pjIwMXL9+Henp6fB6vSgsLIRMJqPYCLFYjIsXLz70mZaVlcHn86GyspI4Y0ajEYWFhbBarfj2t7+Nubk5Gmkz2XpTUxPsdntC08Puu2g0ilAoRON0DoeDI0eOYGNjgyJCANDhIF4Bxf5GKBR65DWQlZWFcDgMiUQCi8WCuro6TE9PkzWIQCBAU1MTenp6iIxcVVWF1dVV1NfXY3BwEA6HA83NzSgsLMTy8jKNjphHmEwmg8VioVFua2srdDodjEYj9u7di7W1NRgMBpSVlZH/D1O/snEX80Nj5OeMjAyYTCbk5eXB4/GQ7J7FiPT390MikaCmpgbDw8NYWFjAhQsXMD8/D7/fj/r6ekilUni9XnzyySc4ceIEcnJyEkQW8Qa0zEj0UQjJP8fr6z9CfdEQ/ZrrSWmIGH/k5s2bJHfmcDhoaWlJmMEvLi5Cq9VSg8TqO9/5DpmTsceWlpYiJSUFS0tLsNlsRMQUi8WQyWQUy3Hr1i1Krr99+zad3goKCpCcnIx33nknId+HcRxSUlJgsVigVCrR3NyM7e1t2O12OhUlJyejqqoKwWCQSJPxpOq0tDRUVlZSWjpT6UilUlRUVMDhcODMmTO0qTA05PLly+RTEovFKFx1dXUVu3btQkVFBcVJsOy2tLQ02ogfXERisRhWV1dx7do1CpIsLy+Hy+VCNBpFSUkJMjMzCdlYX1+H1+vFBx98gPLycggEAmRnZ5MT8dtvv41oNEr2Afn5+ZiamsKePXuQk5MDi8WCUCiE/v5+5ObmksW/zWaDWq3Gzs4OKVcUCgU1PPHo1/r6OoLBIGw2G3Jzc8HhcMh3ikWIsM2ZoX6BQACFhYUJCzEAcgpmJ8y9e/dia2uL3JWzsrKQkZGBSCRC5oh5eXkYGBhASUkJ5ufnE6wRGHLFuF7b29uEbjHZL4CEsVm8c3BnZydSUlLQ2tqKcDj8yLHdo0aMD54wRSIRqfYmJycxOjqKs2fP0ojtUafR+OcVi8Vwu90YHh5GTk4OyfBTUlJozMFsDzo7OyEUCnH48GGkp6fTdRIfqsve49LSEpxOJ0wmEzQaDe7du4ecnBzMz89TFA5DEmdnZyESiZCSkoLR0dGE700oFKKhoQHNzc1YXV0Fn8/H8PAwbDYb+XZ973vfS0g+P3r0KFwuV4JhIqt4PhdwH6FkAb9isThB0MGKRcSwrDDGNRobG8OBAwewsrKChYWFhCYzOzsbTU1NuHz5MvGuQqEQ/vAP/xChUAhvv/02dnZ2aG0+duwYrl+/DqvVSk3MN77xDfh8PpjNZsoMA4DV1VUEg0EsLy+juLgYTqcT6+vrOHz4MI3sGV+TBVNnZmZicHAQMpkM169fp/fGGlSBQICWlhYsLi6SAz1w3+bkpZdeIv+z7u5uLCwsUDPzZ3/2Z4TwdnR0YGZmBg0NDZiamsKLL74IlUqVcM8wHpjD4cCHH36IoqIitLW1gc/n05oVj+6y7+jTfNb+PRum38Tf/6Ih+jXXk9IQAcDi4iLeeeedhJyx7OxsuN1uFBUVYWxsDCqVCsnJyRCJRAmZRL/3e79HSqVwOIzS0lIi9Pl8PkxPT5PxYEpKCkpLS+H1eslPgxH5hEIhVlZWcOvWLeTl5WF+fv4hIjfbwNLS0pCVlYXU1FTY7XZkZmZS8jpryAKBAORyOSYnJ2E2mylOAbi/AJeUlEAoFMJqtaK1tZVkpgaDAYcOHSKESSQSwWazEYz+7rvvJiBEExMTKC0txdraGvbu3UsqI6Z6SktLI6lo/AkrfqTk9XoxPT1Nkm1mUsc2bTau2t7ehtPpxC9/+UuEw2GcOHECer0ePT092NzcRFtbG7q6uiCVSnH48OEEN2Lg/uI3MDBAQYtZWVmEJrG/JRAIsLq6SggSy4kzGo0QiUTo6OhAbW0tZDLZYxEiAHQ9eL1egvMfHFmNjIxgbGwMVqsVxcXFyMnJwfT0NPh8PoLBIOrq6iCVSuFwOLC+vo61tTWUlJRAp9NhdHT0IYTIbrcjFArBbreDx+ORuSdwP/iTvU7W4D0udw1IDL4F8MhxXDgchsFgQCwWQ3FxMY0QotEohoeHKVy4uroa+fn52N7eJgQxFosRiTveuVcoFMJut2N2dpYaj1AoRCR2tjltb2/Txs4Cby9cuICNjQ1cu3YNAoEADQ0NEAqFsNlsaGxsBJ/Ph9PpJIM9k8mE0dFRQqwaGhqQnJyM2dlZaLVaDA8PUxbfg3XixAlyrGc8wp6eHhQWFuL06dMYGRnBlStX6PcLCwuRmZmJ6elpIv2zksvl5GgcX8nJyXSPxxe7JoH7aMnKygoFUYvFYhQXF6OkpAQTExOYmJhAMBiEUChEeno6UlNTKWQauD9uZ5v/2toa3n33XWi1WpSUlGBtbQ15eXm4d+8ezGYznn/+eSiVSjLzZPy/69evIzMzEykpKZDL5ejo6CDytkgkwh/90R/RdfjJJ58QssTn83H8+HFsbW3h9u3bZAnA5XKJ+3P06FGkpqbinXfeoc8nLy8PJ06cQEpKCu7cuQOZTIaxsTFYLBa88sorKCgoAHAf9ayoqEB/fz/xNgsLC4kv+aAj/Ouvvw6Xy4Xt7W0cOXIEtbW11AQJhUIYjUYynvw0ns2/xFD111m/ib//RUP0a64nqSFaW1vD66+/Tjccs2pnMlvmGstuUrFYDIfDgb1796K4uBg6nY5OEIxPwmbxbHTGNsOkpCSUl5cT0fHSpUuw2Ww4c+YMxsfHkZycjOnpafj9/ke+1rS0NOh0OjQ3N5P3jM1mQ3JyMoxGI50aFQoFBAIBOjs7H/k8LJNKKBSSl0xbWxs1MT6fj5yKDQYDiouLIZVKH+LLbG1t4dKlSygvL4dSqYRSqaS4EoFAgJ2dHTqhx5+wgsEgye5TU1MpB47L5WJhYYGI2c3NzeDxeEhOTkZaWhp+8IMfIBAIgMPhQKfTkW9OcnIy+Hw+ISbxMn4AdCLd3t4mR1u2iDNzSL1eT/46S0tLlLO2tLSE7e1tdHR0YM+ePZiYmMCFCxcoloPJZT/PySyel8Tn82E0GhGNRiEUCokIarVaUVJSQg04n8/HjRs3kJaWBoFAgPb29kc+P4uDYFYLkUgEPB4Py8vLiEaj2LdvH1kzbG1tQa/Xk6z9UcGnn+V/Mj09TTln6enpZNy5uLiIa9eukfvw7t27UV1dDYFAAJfLhc3NTRqzMBWW1+vF0aNHqRmSSCQ01mpubkYoFMLY2Bg2Njag0+kQiUQgFApx5coVcDgcMhXs7OzE+vo6BfOKxWIIhUJMTk7ia1/7GsRiMW7cuIHbt28jNzeXGiB2fWq1WiQlJZEH0aeVQCCgZikajdK4s7S09CFUCbiP8D6OB8hy1z5vcblcUljFV1JSEnJyciCXy4nEvbS0hFAohJqaGoRCIQqGzc7OJmn8qVOnsL29DZVKBYPBQH5aDocDLpcL6enpCAQCKCoqgsFgQGZmJiYmJhAOh3H37l1UVVWRgWI82sPn89He3o6qqiq8+eab8Hq9CcaaqampeOqpp2AwGDA1NYX6+noAIPoCQ8srKipw48YNyGQy6HQ6uFwuLC4uYv/+/XC73bh37x5OnjyJyspKeL1eTE1NwWw2IykpiZonhh6KRCJ0dXXB6XTi6aefJk+59957j9D//Px8PPPMM9Qsb21tQa1WE6/zC4Toi4bo11pPUkNktVrxD//wD/TfPB4PjY2NmJ2dRWFhYQLMrVAoaDPJy8tDdXU1ma9NTU0hKSmJOvPa2lo67ff29hK6Ew6HoVarce3aNZhMJgiFQiQlJeHZZ5/FzZs3sbi4SA3UgyWXy6HVahGLxZCamoqGhgZMTEzAZDJhc3MTeXl5UKvVuHfvHsmzH9VcabVa1NTUoLu7m/49IyMDBw8eJCM0pVJJoye32w21Wg2ZTEYnzsXFRQqDZXJlNqbY2toih+4PPvgATz31FC0kADA2NgaNRgO3242SkhJCNyYmJrCysgKz2UwjsbNnzxKHy+/3o6OjAykpKXj++efB4/Fw8+ZNbG5u4sSJE/T8y8vLUCqVMJvNSE5OJk8l5lGUm5sLh8OB7OxszM/PIysrC/Pz80T8zMnJwfr6OnJycrC6uorh4WEoFArcvn0bzz77LPx+P6nemKGaz+eDzWZDV1cXiouLsWvXLoTD4YfGQltbWxAIBJRqb7FY4Ha7EQ6HSTLNvFL8fj9SUlLA5/Mp+JMRnl0uF375y1+irKwMu3btAo/HI0sA9r2zZoM9B7NWYOMgqVQKsViMq1evoqqqCjMzMxRlwEafbPS7d+/ehJT4wcFBrKysIBqNorm5GXK5HNvb23C73ZiamsLc3BwqKipw+PBhSoxn6j+XywWhUAi32038GWYDwRSTOTk5dD2bTCasra1hZ2cH0WgU5eXlsNvtGB0dRX5+PjQaDYRCIS5dukTI1r59+yCRSHDjxg3K7GMIU3w8BSsul0uj0oKCAgwNDT103zCOT3wxDpRQKCTezIPP/WnF5XKRmpqKSCTyyKT5x5VEIiHDV1ZisRg1NTXY2dmBzWYjtej29jbkcjk4HA4yMjJQUFCAxcVFmEwmlJeXU1AuAELHKioqwOVy4ff74XQ6UVtbCx6PB5vNhunpaWRmZuLjjz9Gfn4+rFYrqqurkZWVhcnJSQwNDSEWi6Gmpga7d+/G4OAggsEguejHr2319fXQaDRYWVmhnDiHwwGlUgmbzUY8wfb2dgwPD8NoNGJxcZHuP6FQiNzcXExOTuKll17CxsYGlpaWSE3X2NhIAgzWRC8tLSEtLQ1utxu7du2CQCBAd3c37HY7xfS8/PLLsFgs5AYeCAQ+1X/tP1J9Ed3xO1wcDifBlyYcDtNGzNQqrDY2NlBbW4uKigrodDpcvnwZnZ2duH79OjY2NjAzM4O1tTVsbW1hZGSEfEnS09PpxJ6VlQW/30+8kkAgAJlMhrt37+LkyZN46aWXIJPJkJmZCZlMRqoINi5ZXV3FzMwMzGYzLl68iFu3bsFsNsPj8WB0dBRDQ0PIzs6mmJGcnBy0tLSQMzQbX01OTiY0Sw6HA2+99RaRT91uNyQSCRYXF8HlcslwMBwO486dO+BwOBSPIRQKqRFkho9yuRzvvfce6uvr8e6775LsmcPhoLy8HDabjeBtJrfPyMggwjgjz/L5fFKxaLVavPDCC/j6178OiUSC5ORkHD16FE8//TQ1Yux1fvjhh+jq6sLa2hqWlpYglUqJRG0ymWCxWGhEsLCwAJlMRhELr7/+OiKRCKn1XC4Xrl27hrNnz2J2dpbGBRKJhEjj29vbhMj19fXhxz/+Me7du0eqLPY5MUI1c8jOy8sDcL/BiW8OJycn6UTN4grY2CsWi+GXv/wlOWbfvXsXDoeDvgf2Pbe0tNAYb9++fcjJyYFKpYJYLCZH5u3tbezZswfj4+PYvXt3wn3B4g8UCgVu3rxJG9n29jbS0tIgFosJsWOcs9XVVRQWFmL37t2orKxEOByGXC4nBRILG2bmekxqzuwL7HY7jUo6OjpgNBqh0+kgEAiQkpKCyspKxGL3s/V0Oh0plVizzsrr9SI7OxsZGRmE/rJ/Zw2LQqEAcH9TLSsrQ1JSEtLS0h7ZDAF4qBkCQA1Ybm4uPB7PP6sZAkCmmV6vF1VVVaRE/axi42ZWjGjtdrsxPj5O65hIJCKhAkPxkpKSsLy8jPz8fMzPzyMvLw+rq6vg8Xjo6+tDTk4OJicnweVywePxUFNTQwaJ29vbNHo6duwY1tbWcPz4cTQ2NmJ1dRVNTU2orq5Geno6ioqKsLCwAIlEArvdjpycHPIcAu4fwiQSCY2Ob9y4gXA4TKPUmpoabG5u4uDBgzCbzdja2iJOIguVbW9vx+TkJNRqNS5evIiSkhKymaiqqqL7k6kpDxw4gOzsbNjtdlRUVJDCsKmpCTKZDOnp6Whvb8fGxgYyMzPhdrsRDAYxPz//WOL6v2Wx8fVvK87yBUL0OepJQYhisRiWl5dx+fJlUlwplUocP34cbrcbSUlJ+PDDD+n3xWIx/uAP/gButxuDg4OUrp6cnIzs7GxSKXm9Xuzfvx8ikQizs7NISkpCIBDAzs4O6urqIBQKsbGxAbFYjFu3bmF9fR15eXkQCASorq5Gbm4uLBYLDAYDZd0kJSWhqakJ9+7d+9T3pFQqiU9w/PhxZGRkIDU1lWIM2ImyvLwcN27cSLjJS0pKiFCal5eHkpIS7OzsQCqVIhaLQSAQIBqNgs/nY25ujpRRAIgcDIA4NbFYDG+88QZOnDiBgoKChDFE/Jyb3fSRSARmsxmrq6uw2+3Izc1FQUFBghM4y9Z60A05Xt3EUB2WcdXU1ETjRbFYjOXlZVitVmRmZmJzcxNnzpzB6OgoJZSzOI69e/diZGQEQ0ND1LyWlJSgqqoKUqkUHo+HTDljsRgGBgbQ09MDAOS0rFKpsHfv3gR7AuZbo1Ao4Ha7ifPDJNHd3d1YXFxEamoqKioqEAqFUFpaCqvVivLycgQCAXg8Hrz33nvIzs5GWVkZJBIJ1tfXydRSpVLR6Olx5oqMT/E4iD0SicBkMmF+fh5tbW3EtRKJRJifn8cbb7wBlUqFvLw8HD16FA6HA263m7gy+/btg9PpJAsKPp8Ph8MBkUiE0tJSlJaWori4mOJCsrKyKAj1rbfeIkVmRUUFeDwe0tLSyPxvcXERnZ2dhE5tbW3BYDBgeXkZOp0Ohw8fRlJSErq7u5GUlETqLo1GQ1l2YrGYTBzjRz3/XpWWlobc3NyE5PZHFXOVj2/QOBwOKioqsLKyAi6Xi42NDQiFQiQnJyMSiSAjIwPhcJjMCNVqNbmXh0Ih8Hg86PV6hEIhjIyMID8/H1euXEFRUREKCgqg0+ng9/thsViIN8asKmKxGGUUXrx4ESaTCSkpKRAIBNi9ezfW1tYwNDSE1NRUFBUVYWdnB3Nzczh48CA0Gg2mp6cTkPjW1la0traS6tRisWBxcRFra2uorKyETCbD5cuXUVNTg6mpKWRnZ2NpaQn19fU4fvw47HY7GeYmJSVhc3MTRqORbBhYxltNTQ3EYjGkUilZTwSDQRQUFFAKgd/vx8jICDIzM2G32xPCYH8T9e/NSXpUfYEQ/Y4WC3ZVq9UAQLwFh8NB5Ln4zC6xWIxwOIxAIACNRkNkT4FAgMrKSjQ1NUEikaC2thYGgwE2mw3l5eVkfqjT6bC0tASxWAy9Xo/NzU2IxWIEg0HMzc1hY2MDHo8HJpMJ+fn52Lt3LxEgo9Eo+vr6CLWKV0vEF4fDIbn5rVu3KDl9bW2NzNN2796NW7duETLGTpezs7MUWxKLxTA1NQUul0sOr0wpFwqFsGfPHqjVajL5Y9A8I3+rVCqkp6fj+PHjpOJisHUsFksg4bJg0u3tbeTm5qKpqYmQqMnJSYyPj8Pn81HDNTU1hV/84heYmpoiczTgvlqqoKAA2dnZKC4uxubmJiFrgUAAqampCIVCyM3NRV5eHlwuF9rb22lckpmZiZycHKytraG8vBxyuZzS6n0+H6LRKCYmJtDT0wOHw4H5+XnYbDZ632VlZTh37hypnhQKBSXBM4J4IBBALBajiAOGGjELA0agZhwOxk+7fPkycnJyyP5Bq9Xiq1/9Kg4dOgSZTAYul4ucnBw4nU5y3o1vUlmxz2tzcxPz8/PkCv0ovgEjlx87dozeA/OKuXHjBpRKJex2O7a3t+H3+wl9mp2dRTAYRHd3N+7evYtIJAKPxwOHw4GkpCTyudJqteBwOJBKpXj22WdRXFyM1dVVdHZ2IjU1lcbW0WgUHo8HRqMRly9fphiGL33pSxQxYbfb4ff7UVxcjPLycgSDQaSkpODkyZPw+/2QSqUoKCigIE3WYHu93sc2Q4+LU/m3KpfL9ZnNEAASA8QXa6ZKSkoI+WVkbQ6Hg9XVVVitVqSnp2Nra4sMCysqKpCXl4eqqiqYzWb85Cc/QUFBAXF2+vv7cfHiRQqK1mq1SEtLw8LCAglItre3IZVKMT8/T35QTCUXCoUwOzuLjIwMBINBWCwW2Gw2igXp6enB2tpawnthKHNycjKi0Si8Xi8FPS8uLqK0tBRf//rXMT09TXE0zNqARaZwOBxYLBZsb2/Ta+/p6aFwWZlMBoPBAJlMBrVajbGxMYTDYYRCIaytrRHXTiQSoaysDDabLQFB/U3Vg8Gxv231BUL0OepJQoi2t7fx4YcfYm5ujjKP2traIBaLkZubC6vViq6uLmxtbZHbb01NDerr6+HxeBKs+b1eL6VkHzhwAKFQCIODg1CpVKiursbY2Bjq6upw9epVJCcnY8+ePXjjjTdISZOZmQmRSISWlhaK07h+/Tp6e3sBgMi7RUVFMBqNpDYRCoVoa2vD8vIywuEwqbQY2VSj0eCdd96B3W6HWq0m9RKfzyfEpL6+njx+WOgp8xfRarVIT08Hn8+HXC6HTCajhZt59qhUKqytreHOnTvY3NzEoUOHwOPxoFAo4HQ6oVQqSb7O/HqEQiGWlpYwMjICi8WCkydPIjU1FYuLi7h9+zZxP0pLS1FVVYXs7GwAwI9+9CPk5eVhYWEB3/rWtwAkIk7hcBgXL17E/Pw8du/ejeLiYiQnJ5NzcbwJImsGgsEghoeHAYDSrJVKJRYXFzE6OopwOIypqSnI5XIEAgHiTLjdbrS1tdH7Ye8P+JXxYbwc3e/3QyaTkdHd1NQU0tPT0drair6+PlLhAPcVNawR0+v12N7exoULF0iqzaB0JmFnxH4WK8J+HovFsLa2hvHxcRoVhMNhlJSU0Ei0u7sb586dg0qlwvT0NBQKBRwOB+RyOXHFGIl+c3MTBQUFlHqem5tL6M21a9cSDBxPnDiB69evU0MyPT1NpphHjhxBfn4+nE4nfvrTn0Kv15OfEJfLhU6nQ0NDA0ZHR7G4uEhRIcypmXlzabVaTE5O0n2dnp5OBqcjIyMPbbiPKoaU/TZXZmYmKVqlUinMZjOqq6thNpuxa9cujI6Owmw2o7W1FXq9nhpnZmj61ltvQa/XY21tDS0tLejt7aXPRK/X4+jRo+BwOCSucDgcyMjIoHH1+vo6/H4/Ll++TNcDa94mJiag0+mQk5OD8fFxktIzg1Pgfko8u49Pnz6N2tpa+P1+TE9P49KlS1AqlThz5gwAkPrXYDBgfX0dRUVFWFtbwyuvvAKn00kIEYfDgc1mg9VqRSAQQG9vL403n376aeLxJSUlkSM34zWx9Sk/P/9zjTH/o9QXpOpfcz0JDVE8G//KlSvUdAD3b4iamhokJyfDbDYjGo3CYDBgbm6OfufChQsoLy8n/5r19XVkZWXBarUiOzsbs7OzdBJLSkpCdnY29u7di3feeQfRaBThcBgajQbl5eX45JNP6JTPkAWdTgeTyYT19XXKSuLxeEhPTycDs4GBAaSkpKCoqAibm5sQCAQkIx8ZGYHL5cJzzz2HiYmJBLsAAGhqaoLZbEZZWRnkcjkFnLJE76WlJUxMTCAtLY1iKsbGxvDcc8+R78f09DQ0Gg3EYjEkEgk1Q8nJyRCLxTh9+jQhARKJhCTXDPp1OBzo7+/H3NwcFAoFwuEwvva1r8Hr9aK/vx/T09OQy+VQq9UIBAK0GDL+z7lz54gjEO8YOzs7iw8++ABpaWnY3t7GH//xH9P3zSwBGD+Lw+HA7/dja2sLwWCQyOiM38DGoD6fDx6PBzdu3MDJkyeRk5ODoaEhlJaWQiAQEOKSn58PgUCA7e1tRKNRSCQSKJVKyoUqLCwkrk1/fz+i0Si4XC4kEglcLhc1Q8nJyYREMi+Y3bt3kwUE4745nU5EIhHw+XxyMM7Pz4fL5UJWVhacTict7PPz8xQGyqIgYrEY+vr6UFxcjOXlZTQ2NiI1NRU3b95EaWkpXbu5ubn4+OOPSVItEAhQW1ubEEPCnKK7urpIecXcryORCMrLy+F2u/HRRx+hvLwcBoMBJ06cwOuvv04Bo+z6VygUqKyshFwux/z8PK5fvw6BQECbH7sG4+0B4isjIwPr6+ufi3uRlJQEoVD4WHXnv2exEfhn1YNCjLy8PJw6dQoCgQAWiwVzc3NYXFxEZWUlsrKykJOTQ82y2WzG9evXoVAoMDg4iOPHj6OyshKvv/46jRczMjLwjW98Ay6XC+FwmJohhqT7/X7I5XL09vZCLBZjdHQUTqcTR48eBZ/Px71791BfX09ct/j1lqHeDI1mzemXv/xl8Pl8fPDBByguLobBYMDzzz+P3t5eaDQaLC8vk33GxMQE2trayM07Eolgfn4eY2NjaGtrQ3JyMkwmE0ZGRnDhwgVqcNgo2+fzgcfjETdQLBZjfn4eGo0GNpsNNTU1D33m8a707LNcWVlBX18fTpw48ZD32O9KfdEQ/ZrrSWiI4mezPp+PEBTWuAiFQjJMVCqVSE9PR29vLwwGA1pbW3Ho0CEEAgH4fD6sr6+Dz+cjFotBpVJhZWWFNpje3l7I5XKUlZVBJpMhFouhu7sbEokEBw4cgNfrxebmJm1s+fn5mJychNVqxd69e6HRaHD37l0olUqMjY0hFovhwoULpG7Z3t5GJBIhRZDP5yPZr0wmw87ODimQWLHR18mTJykAVK1WEyKxsLCAvr4+CIVCcLlcFBUVoaurC3l5eZibm8PXvvY1jI6OEo+moKAAFosFo6OjROo9c+YMKisrydSS/e7MzAyUSiW4XC7BwYODg6RkYps38yGx2WyYn59Hc3MzFAoFLfw2mw2XLl1CVlYWyeG7u7vxla98BWKxGP39/RgbG0N7ezvy8vJo0dra2iKHX4a4paamYmZmBrFYDBKJhL7LwcFBWtiYiVssFsPs7CxWVlZw4MABMpgE7suMA4EAVCoVheqyJpYZAS4vL6OpqYk8XJaXl1FYWIjm5maMj4/DYDDA7/dDIpEgMzMTk5OTkMvlAO7n6OXm5iIajZJlQCQSwc7ODlJTUzExMUFoSUFBATY3N1FcXEzZS/Pz87Db7ZBIJNDpdLDb7SgrK8Pc3BympqZw+vRp5OTk4OOPP4ZGo8HS0hIaGxuhVqtJXdPR0QG3243S0lLEYjGo1WoMDg7CZDIhNzeXuD5zc3MIBAJobm6GVCqlkarf7yd149mzZ4kHcunSJUilUoqMOHToEDU809PT+OSTTxCLxdDU1ITJyUmoVCosLy9DKBSSYi2+HqUke1Qxl2c2Kn+SqrCwkGTzn1Us24zx9L75zW8SkpuSkoLOzk5aAw4ePIjdu3cTMs1Ufn19fTh8+DA1zrW1teRifv78eXg8Hhr/s4bd4/EgJSUFSqUS4XAYfD4f/f391GSz5HmxWAybzYbq6mqMj49T86FUKnHu3Dl4PB709/eT9URBQQFkMhmampqwtraGq1evgsPh4Fvf+haSk5Nx7do1qNVqymsDQN5Wd+7cQW5uLsbGxqDT6bC8vIzS0lLcu3cPJSUlMJlM+MpXvgLgV1YYLG6Icd0YF81isaCsrOyRWYpbW1vkVcXlcrG9vY2uri7o9XosLi7imWeeeez39Sh5PPOvY/xIZovxpNXvVEOUl5eHpaWlh37++7//+/i7v/s7HDhwAN3d3Qn/9nu/93v4/ve/T/+9vLyM7373u7h+/TokEgm+8pWv4C/+4i8+NSk8vp6Ehij+guzt7cX169cRjUZRUVGBrKws8uAwGo3g8/kU/qjT6ZCUlASBQEAuwAzmz87OplEMh8MhzoTZbKZNgSm2PvroI+zfv58ksmyk43K5MDY2BolEAr1ej71790KpVGJpaQlJSUnY2NiAXq+nhmhjYwNZWVlwuVxwOp24evUq8U+A+9yC2trah3gS7e3tUCgU0Ov15F3kcDgwMTFBHBCv14vc3FyKn7h27Rrq6uoQDAbR3t4Ou90OlUpFrshMMnvw4EEolUrMzc1RpEhtbS0mJyfJfTc3NxdJSUlQKpVISUmhBWl6ehp2ux0pKSlkAsnlch+Kh3jrrbfA4/GoubHZbOT1UlxcjJaWFgQCASQnJxOyoFar4fF4KB6AZToZjUakpqbSuIHP59OCOzU1ha9//euQSqUIBAK4dOkS5ufniatRVlaGjIwMMhOMRCKUBJ+Tk4PTp0/DZrMhFothbGwMtbW1tIEzldCdO3fQ0NCA6upq9Pb2oqSkhBZqsViM7u5uZGVloaKiAoFAAG63G2lpafTZs6BghvawgFOBQAC73Y60tDTU1NRQo7e4uAir1QqlUkkWAlNTU+Qn43A4sLy8jKKiImrG4j//QCCAN998E6urq9DpdHA4HMT1YOPGtrY2yGQyLC8vIxAIUPhtT08POTzzeDxwuVyUlpZCrVZjYWEBo6Oj2LNnD1JTU3H79m1qmoVCIYLBIORyOXw+Hz3fysoKkpKSKFQZADV7zN/psyreDPRJqvT0dLpWPmucl56ejqamJnR2dmLXrl1YW1uDx+NBY2Mjbt++nfA5pKWl4fjx4yguLqYDHXuO/v5+CIVCBAIBQkqA+whgIBCAWq0mdWG8Yo0pUpk9w8LCAq5du4ajR49iYGAAKysrAO6T2isrK3H37l1oNBryN1pbW8Pm5iZ2796Nra0tGI1GUhT6/X709fWhtLQUJpMJ3/rWtwidYYaoSUlJWF9fxw9+8AM6aDK3+r1790Imk2FychIjIyM4ceIEhoaGIBQKsWfPHkQikQSxBqMJsOv6cQ3RvwYhehRZemtrC3a7nZp0lUr1xBCp4+t3qiFyOBwJ6oSJiQm0t7fj+vXrOHDgAA4cOIDi4mL8r//1v+h3xGIxvXF2ctBoNPirv/orWK1WvPLKK/jWt76FP//zP/9cr+FJaIji64c//CHdsADw3e9+F4uLi+ju7k5YiBobG6FUKiGXy8nJOL6YxwwbmwG/moszIrFYLMZrr72GlpYW3LlzB2fPniViqslkogWe8Uaef/55+P1+eL1e8s9gbrXM02h7exs6nQ5erxcTExO4evUqcnJyaHHq6upCZWVlQjbTwYMHIZVKkZmZCa/XC6FQiHfeeQf79+/H0tISrFYrhEIhsrKy0NLSAqlUipmZGeJvACB+EPP2YZsv4wb88Ic/RGZmJpaWlvDlL38ZCoUCo6Oj5FmkUCggk8ko42plZQXBYBCTk5NwOBxoamoiBR/LqWKGf8zvhDlCs1Mi4wWxhYzD4YDH4yE3Nxdra2soLi6m18lGfQxyZwt9KBSCz+fD9evX0dLSAovFglOnTmFpaQl9fX3gcrlwOp345je/CZFIhLm5OajVahiNRrhcLsqN4/P5aGtrQ35+PqmrWI4XG5t1dXUhJycHDocDUqkUu3btgslkQn19fQL/iJ1CATySq8QUk8wrh3F+mKxYLpdTOC8jsrNi2WFOp5MM/JaWltDQ0EAj2PjTbE9PD3p6eiCVSrG9vY2CggLMzs6Cw+FAoVBAIpGgvLycAo+3t7cxPT1NvKze3l74fD74/X4UFRXBZrNBpVJhfn4+wYn5wWLmlGlpaZidnU1oYBhaxry/Pm+xzf9JLIFAAIlEkmBmyIoZCrKqra2FxWJBe3s7Ojo6qMnh8XjQ6XQwm82ENJ0/fx56vZ6eI/67DQaDxA1jxPPNzU34fD4yN1WpVNBqtQiHw+jq6iKfMK1WC4PBgIyMDOTk5KC4uBjDw8MJ4zGZTIaXX34ZSqUS3/ve9+D1egnhZMjqiRMnaB0ViURITU2FwWBAX18fXnzxRRqTP1h/+7d/i/z8fIyOjuLll18m01yRSASfz4fFxUUsLi4iGo0SQpWRkYG6ujqIxWKsr69DJpPR6JrF5SwtLaG6uvqhv8cQnfgMvc9bXyBET2j9yZ/8CS5evAiDwQAOh4MDBw6gtrYWf/M3f/PI3//kk09w+vRpSjAGgO9///v4r//1v8LhcHwuZcaT0hCxi7KrqyshRPHo0aMQi8UYHh5OQNMOHDiA5ORkUr7EK73Yhu73+5Gfn08BhY+q9fV1vPHGGzh79iwGBgboxmABjBsbG1CpVHjmmWdoM3S5XLhx4wYMBgOOHz8OnU4Ht9sNs9mMrq4u7N69G4cOHcKbb74JlUqFpaUl6HQ61NbWEg/BbDbj0qVLqK2tRVZWFkmQFQoFXn31VWqUq6qq6G/W1tYiPT0dPB4PJpMJaWlpUKvV5NAc3wQyjyUAFLHwy1/+khAjhkgoFAr4/f4EFVT84hCNRmG1WmEwGKBSqchPxWq1QiaTUUI5U+UNDw8TiTIQCMDhcJB3TVZWFpkRlpaWksqNy+XCaDSipKSEiKVvv/02vF4vJBIJmpubsb6+jpmZGRrTnThxAgsLCzCZTBR5wnhjfD6fPHYWFhawsrICqVSK+vp6SqNnUmnWwLHx4sDAAPLz85GTk4P3338fNTU1KC0tRXJyMjo6Ouj1M6lzcnIyysvLKfPJYDDA5XKRp9H6+jqdXA0GAyFGc3Nz2LVrF7kKM2dypVKJ0dFR1NbWwmazYWhoCAqFAvn5+SgoKKDviykB+Xw+JiYmMDc3Rwje6uoqRCIRDAYD1Go1kegNBgMmJiawubkJj8eDjIwM1NTUICsrC6urq5TnFh9dwVCmB3O82AgpPoomvh5sEv4tKz8/PyHX8N+qGCIaX1wuF1qt9iF365dffhmLi4vo7e1NaCp1Oh3kcjlFUDQ0NKCyspLCeZlaNh7RYKM0s9lMij0WtsrCnNfX18lCghmTBoNBhEIhpKamQq1Wo7a2FoODgzAajQCAPXv2ICMjAysrKwlrLlOvvvTSSzCbzdDpdJBKpeRttr6+TsKVjIyMhzL0xGIxnE4nXn/9dRw9ehRFRUXw+/2EwlgsFoyPj0Oj0dA+xefziYt56tQpqFSqBDuPUCiE6elplJaWIhQKPWRNwd63QCBAOBx+7Hr/u1a/sw1RMBhEZmYm/tN/+k/47//9vwO4v+lPTk4iFotBo9HgzJkz+J//83/SxvVnf/Zn+PDDDxPkoSaTidxd6+rqHvo7gUAg4RTm9Xqh0+n+3RoidjMBIGXOT37yE2xtbUGj0WDXrl3QarUYGRnB3bt3AYDk9JWVlVhaWkJZWRkhPizM1e12IxKJQKlU0mjtwYpGo5ifn0dOTg46OztJ4h+JRJCamkqoDZ/Px49//GMIBAJ8+9vfhtFoxM2bN6HRaLC6uoqzZ89iZmYGw8PDSE1NhdfrxYULF6BUKtHR0QG5XE5xHFqtFjKZDDMzM7SApKamQiwW00b1oN8S+3xSU1PR0tICp9MJiURCZoKsKXqcVXwsFiMlm91uh1arhcvlIgt8nU5Hv/8o+Hh5eZkyj1g2m0ajwezsLORyOZxOJ4D7TuNerxehUAglJSUUqWGz2ZCfn0+jMrZI5+TkIBqNYmpqCiqVCh6PB3q9Hp2dnbDb7bDb7UhNTYVEIsFzzz2H999/Hx6Ph0i3bEFmvlE7Ozuor6+nkanZbIZGo8GNGzfA5XLhdrspYb2npwc5OTmoqKigUZVIJIJarYZIJMKrr74KhUKREMbLSigUIiUlBXq9HmKxmIJoWe5ecnIyeQ5tbW3B7XZTvIvb7aYxk8fjQXl5OZFIORwOVCoV6urqMDMzg7m5OZhMJiQnJyM5OZniaVhkjdfrpZN8NBol8zrmuK5Wq7G0tAQul0tmjNeuXXso5V0ul+PIkSOIRCJ477336Bpmr1Gv12N6ehrA/QaAIXzMkO9RnJ/4eJh/y2ptbUV/f//n4vawzyWesP0oFCwrK4sI6w/Wo94rMwV1uVyIRCLQ6/UwmUwIhUIJn8Ef/MEfICkpCQMDA5iZmYFarcbMzAxOnDhBEnpmfzE7O4uZmRmcO3cOfr8fNpsNqamp6O3thdvtxnPPPUdy9HjPLpY/tri4SOslANTV1VE0TSAQAJ/Px8DAAKRSKaE2bJ3RaDTg8/kQCoXIz89HKBQify22PprNZgiFQhp5j46OYnJyEs8++yxisRjlHrKKX5vC4TBGR0dhsViwd+9e9Pf3QyKRkLWDyWTCV7/6VQD398XR0VHU1NRAIBDQ+uTxeDAwMACJREL5eP9ShOi3uX5nfYjef/99uN1uuhAA4MUXX8TPfvYzXL9+Hf/tv/03vPbaa3j55Zfp39fW1ggZYsX++3Hy1r/4i78gN1mWR/PvWUxtFO+H88ILL0Cv1+PgwYMoLS3F0tISysvLsXv3bpSWliI3Nxc6nQ42mw0lJSWURxVvQhgKhSAQCMirhXm+rK+v4+LFizAajeRebDKZyJgsFArh4MGDOHHiBNra2qDX6/H6668jFosRX6OmpgY1NTUwm804deoUpqamKPrD6/Vi9+7dyMnJgVQqxYULF3DgwAEiBKanp8Pj8aCsrIwW4osXL9KojMvl4umnnwZwvxmKn5d7vV5Eo1EUFRUhHA7T5sg8Qpjc3Gg04uLFi3j11VdhtVpJtSYQCFBcXAy/349bt25henqaNkt2dhCJRNjY2IBAIMDy8jJCoRBxmJhaigV1MjKvXq+HXC7H0NAQLZ719fU4ePAgIpEI6urqUF9fj/b2duIRZWZmUgObk5ND5o8cDgft7e20ycjlcjQ2NsJsNuPs2bPQaDSIRqPEMVpdXcXy8jLZCZhMJsjlcvj9fmi1WiwuLtIJVSwWIz09HT09PZDL5VhaWsL6+jry8/PB4/EoUHZoaAjNzc2PbIaA+01BTU0NXU/BYJDCd1l4rF6vR0NDAzIyMiAUColQXVdXR418IBBAMBikvCq1Wg2JRILvfe971FAxzxi5XI6NjQ0MDQ3R32QxJcyjZW5uDllZWeDxeHRCTktLg91ux8DAAFZXVx/JzXG73bh69So1FYzjsrOzg6eeeooQBQCEim1vbyMUCkEoFGJ7e/uhkfVvohkC7nO+Pq9rMXNOf/Bn8aXRaB7bDLF7JScnB42NjSRRDwQCSEtLw5kzZ0hwwZohhn783u/9HkQiERYWFlBUVITKykrMzMyguroafX19UKvV8Pv94HK5MJlMuHLlCkwmE/7v//2/sNlsSEtLw82bN2G1WsHhcPD+++/j3r175MrP0JbKykpotVrodLoEzotYLEZeXh7y8vKgVCoxMDCAtLQ0QvGY5UV6ejrW1taIT+jxeIj/l5ycTH5czGjS6XRicXERExMTyMzMxNtvv02K0vjicDgJthp1dXU4cuQIBgcHiTOYn58Po9GIp556ih43OjoKvV5P4hTGYxsZGQGPx4PL5cLo6Ch5aDG0/It6uH6rEKJjx45BIBDgo48+euzvdHV1kfJAr9fj29/+NpaWlhLSnLe3t5GSkoJLly7hxIkTDz3Hk4oQMVQjFArh4sWLmJ2dRXl5OTQaDUpLS9Hf34+UlBS43W5ySj127BiNESwWC8RiMcmXV1ZWKCcqOTkZNpsNg4ODmJiYQEtLC5aXl0mVdPDgQezZswfAw87BPp8PU1NT+OSTT8Dj8fDKK68QosJSxl0uFxYWFuDxePDSSy/RSe9BeJ3P52NsbIxOO7FYDP/wD/9APj6nT58Gj8fD5cuXUV9fT8Rkh8OBmZkZtLW1ETmXedUUFhZSs6ZQKGiMZDAYKOD13LlzCTDy66+/jtLSUty+fRv19fWQSqVk7BiLxRAOh/HBBx+gsbERQqEQEomE4jNYxAJ7brbo/u3f/i2ys7MxMzODo0ePEkLEFsDU1FRkZGTAbrfjypUr4PP5OHv2LI0DWcyKy+XC1tYWzp49Cw6HQ/lmzE3aarUiJSUFRqORUJ7KykpwuVxS2LBxg91ux/r6OgQCAZxOJ4qLixEKhbCwsICuri6UlZXh+PHjMBgMRMC+ffs2hc3abDY4nU4kJSWhrKyM0tyPHDkCg8GA1NRUWK1WzM7OIhKJEJppsVhojMrlcjE+Po7U1FSsra2hpqaGZNfMxfzw4cNE3n711VextbUFmUyG559/HgaDATdu3EBRURGSkpJQWlpKar/GxkZSk42OjiIjI4M4T1wulzZPljHH4XBw6tQpzM/PY319HR6PJ6FxiUdLWFNVU1ODy5cvP3TfMuSSXQeMUP+kFfPbelw9iGSxe6yzsxMKheKRnCHgfgNRUFAAo9FI4+bt7e2Ez4HJ0IVCIUpKShAOhzE/P4/09HQ0NDRAKBTSWJ0dPFhodDxKXF1djYyMDMhkMgwNDVHjVFtbi5GRETKZZWOrPXv2wOVyISUlBdeuXYNMJkN5eTlWV1fh8Xjg8Xhw4MABXL16FZWVlUhOTobb7SYVWl5eHqxWKyorK+nQdPjwYaSlpREaAQC3b9/G0aNHIRAIcO/ePczMzODpp5+GQCD43KGrOzs76O7uhlarhUgkQk9PT4IU/0GEiNXOzg5u3rxJCNFv2rjzSanfyZEZSxtnfi6Pq62tLUgkEly+fBnHjh37F43MHqx/bw7Rgw3R8vIy3nrrLYJHz507h7m5ORQVFWF6eppGLCwH7Ctf+QqNi0QiUUIEBVsgnE4nRkdHYTAYqKGoq6vD8PAw1Go1tra28Id/+IcJM3z2/Gxs43Q6KbYiPoJhbm6Ogi7FYjGRZVnFJ5WPjY2hrKwMCwsLJPe+du0a5ubm0NLSgqamJrz++utoaGjAvXv3UFdXh/LycvD5fIKpI5EIXn31Vej1eiwsLOC//Jf/Qq7FGxsbkMvlWF5exvT0NDY2NtDe3k5yc+A+pyUajeLjjz9GRkYGBagy7g1LZd/Y2IBCoUBjYyPy8/MRDAaRlZWFQCCQwBVgi57D4cBPfvITkoYvLy+jtrYWS0tLUCqVyMjIAIfDwbvvvgufz0feTydOnIDJZILH48HS0hJxtoLBII4dOwaj0YiOjg4cOXIE6enpEIlEWFlZgVqtBp/Pp+dlnkrxi/Ds7CzS0tIwPj6OqqoquFwuKBQKeL1eOJ1OGnH6fD5YrVZEo1HI5XKSjbNT5+nTp1FUVIRYLAa73Q6bzQa5XI4rV66QFQJzDt7a2iKEKhgM4t69e4RcsWy4I0eOoKOjA1tbW1AqlWhqakJFRQW2trbwve99j8Ya//k//2f84z/+I8rKyjAxMYGXX34ZNpuNJMtLS0s4cOAAOjs7yQeGOUCLRCJS29y7d4+QobKyMrS0tNAY+HGk6fhiI7QHq6KiAgqFArdu3frn3/iPqMcFKf9bVXV1NWw2GznQA/fT7g8ePEiN+6fVvn37COkLBAIU9cHlclFSUoJoNAqXy0X+VizkNBwOo6WlBWKxGAsLCzAYDPB6vaSYjUQiFKNRUlKCyspKbG9vY2RkBM3NzbRe37lzB3w+H7m5uZidnSVkNzk5GYcPH6bon7GxMfB4PEilUtjtdkIdv/GNb2BwcBDDw8Pwer2UW5icnIycnBxMT0+TpYRarcaFCxewvr6OSCSCn/3sZ0hLS4NWq8XJkyf/1anuDocDH330EVlPMCn+F/Xp9Ts5Mvvxj38MlUqFU6dOfervscZHq9UCADnExqcsX716FampqSgvL/83e72/zmIjM2aex6I3QqEQKisrYTAYMDMzg97eXhQUFECr1SI3NxdbW1vYs2cPsrOzqYGJt1Vnz7u1tQW/34+amhpotVpsb2/j/PnzKCoqIlkp8wBaWVkhJQ5rppRKJXw+HwoKCh4ZwVBQUAAej0fzdq/Xi+HhYfzsZz8j1INFahQVFeG1116DUChET08P3njjDdy7dw9ZWVmoq6sjNGd0dBQnTpxAZWUlqXRYHIRarcYzzzwDo9GIV155heb6TCkWDAah1+tx+vRpvPLKK9BqtcjIyIDNZsNf/MVfkKpo9+7dJJFmzZPBYMDKygqdilmALpfLTfArind7ZhuYWCzGN77xDczMzOD9999HWloaPvnkE/T29qKnpwfXrl2D1WpFQ0MDIpEI8Q8WFhYwOzsLPp9PhNPZ2VmkpKTAYDCgo6MDQqEQ165dA5/Pp7iMhYUFcDgcbG1tkdT91q1beO2116jBSk1NxerqKkpKSiggcnBwEC6XC5cuXYLf74fBYIDFYkFqair4fD6hOsD9Zt3r9dKp3OFwwGq1QiAQ4ObNm1Cr1TRmy8vLQ3p6OlJTUyEQCMiELzc3l6TtzAZibW0N3/nOd1BUVITi4mISBkgkEnzjG9+ASCTCwYMHcenSJZSUlBDh9bXXXgOHw8HJkyeJ+3bjxg0sLy8DuG+S6XA4EAwGMTExQY7Y7PSckZFBBGqLxfLYU3VpaelD9+iDxeVyUVFR8dhm6J8bb6BWq1FVVYX6+vp/1uP+pVVXV4empqYEAjkAmM1mdHZ24tq1a5/5eK/XSyaiJSUl1MCUl5cTT441IkeOHCEOWVFREVJTU5GSkoK1tbWEw8j6+joZhsrlclRWVqKzsxOffPIJUlJSMDQ0hNnZ2QSfMbPZTChYMBhEVVUVhoaGMD8/j8HBQWRkZNDalZ2dDa/Xi3PnzlGED/t+w+EwMjMzaWJw/vx5UvLu378fwH1kpqOjA2KxGBsbGwiHwwlreHxtbW3h/ffff6xhZ3ylp6fjwIEDmJmZwfnz5z/Xd/hF/fPqtwIhikajyM/PxwsvvIC//Mu/pJ8vLCzgF7/4BU6ePIn09HSMjY3hT//0TylwEviV7D4zMxP/3//3/2FtbQ1f/vKX8c1vfvO3RnYfjxDZ7XYsLi7izp07RCDt7OyEXC7HysoKotEompubkZWVReRFHo/3kD8Ee07mZaFSqeBwOMh7iJFtZTIZBgcHUVdXB7PZTC7GGRkZmJycxOrqKgoKClBbW/tI74sHa2trC8vLy7h79y6ysrIIrausrERmZiZ++tOfoqCggEJH46H6F198EXq9njbjQCCAvr4+MkUTiURYXl4mWX/8dxWNRrG2tkaqFZ/PBw6Hg1/+8pdob2+HRqPB3//930OtVhNfqra2FqOjo+DxeDAajYhGo7DZbBR2C9yH/M+fPw+RSITk5GS4XC6IRCLiGfF4PCLr2mw2vPHGGwkjA8YzYIomFjApl8thNpshFosxODhI3IpDhw5hamoKwWCQTu3xyp7CwkLk5eVhc3MTSUlJ4HK5FLA6NzeHkZERIs4WFxdjcnKSxsPV1dUIhUKU8cQq3pKhubkZbrcbLpcLdrudPHdSU1ORl5eHpqYmuFwuyt27e/cu0tPTUV9fj9nZWVitVlRVVWFsbAyHDh2C0+nE0NAQioqKKG8tJSUF+fn5uHPnDnQ6Hfbt20fvUyQSwWKx4K233iI0zOPxgMvlJlhRnDt3DjabjcjY7KCUlpaG1tZWdHd3Q6FQUGaexWJBVlYWmpqaMDc3B6fTCblcDolEgoGBgc+8rh9FkGYjNRbEHF/Mn+jzoE+skpKSkJqa+pCpI3CftPy4kdznNX18VO3duxe3b99O+JlQKER9fT1mZmYeOWrbs2cP5HI5kpOTceXKFXpdMpmMMs2Yoz4bl9XW1iI/Px9+vx+Tk5OwWCw4d+4cTCYT7t27R0008xozGAwA7otqbty4kYBaFhUV0b9zuVzIZDIa3bJrnQkK4os5TU9OTqKkpIR8zCQSCaanp2E0GlFYWAiZTIa2traEsFi32435+XlkZ2dDoVDAaDSit7cXMpmMZPkrKyuIxWLIyclBUlIStre30dHRQevMF03Ov039zo3MOjo6cOzYMczOziaMW1ZWVvDyyy9jYmICW1tb0Ol0eOqpp/A//sf/SHjjS0tL+O53v4sbN24gJSUFX/nKV/CXf/mXv1XGjMD908ndu3fR1dVFqENFRQWys7PR09OTsCAePHgQOTk5FGD5IEzr9Xqxvr6OQCBAeUBZWVngcDgk5WQjrPjxzuLiIsUWsLBNqVT6/7P3n0GOptd5P3wh55waQAPdjc45d0/OacPM7s7OZoorkbKopeWSXCrL31xWlatcpbL9l8tVkmxaJJekyM1xdifspJ3QOecc0Gg0OgCN0Mjp/TDvfdiYmSWXNm2uqTlVrOHOdECjgec59znX9btQWlr6SPbFg5X9/wd1Tk1N4e7du1AqlTTtampqQjKZxE9/+lMCq+1mtHzve9/LEcP29PRQE9Tc3EzCZtaYlZeX0+dubm4Sc8TlcmH//v34H//jf6C2thZDQ0M4efIkOYja29tRUlICo9EIkUiE3t5elJeX486dO3A4HPB4PJidnUUikcCrr76KvLy8nHR4dtHNZDJYXV2FTqfD2toaPB4PMpkMcU7Y+D2RSIDH4yGZTEKlUqGhoYFWLQw+effuXdJ67NmzJ4eVwkqr1SKZTMJqtZKLS6VSEZRxZWUFnZ2diEajyM/PJ7YOe7wM8siIwBwOB0qlEseOHUM2myUbM9NjpVIprKysYGZmBnw+H4cOHcLt27ehVCrR1NQEv98Pj8eDvXv3YmdnBz6fj+IO2tvbEQwGUVFRkaO54PP5pPlj4l6xWIy2tjbs378fXq8XP/vZz1BTU4O+vj7k5eVRfAY7BEkkEhQXF0Mul+c8znQ6DY1Gg6NHj2JlZQXDw8NoaWnBlStXIBKJCJewuLhIImy5XI6mpqZfuxoCfrnOYr8nxkp6VDHt22+rmOniQUbRb+Jke7CpelRemtlsRltbG3p7ex/5+J999llqQMbHx3Hjxg36t6qqKkilUmxvb2NtbS3na0skEjISLC0twWAwUOwGex3U1dVhdXWVIjZisRgGBwfR2NiIe/fuQSqV0kSVFWOHNTQ04Pbt26R3Y5N2dn05c+YMNBoNAoEAhEIhbt26hbq6OszNzaG1tRUDAwPY3NykJv7YsWNIpVLIZrP4/PPPsbW1hbNnz9IBkbHLdkNvGXYiGo3Se9Tn8+HevXs4efJkzqH1q9ywu+tBJ9tjofSj6/euIfpd1++6IcpkMtja2sLCwgJ0Oh0+/vjjnAvX/v37odVqc8TmSqUSL7/8MvLy8h75RmEaGI/Hg/LychQWFuZQTNmbMBgM0umZZQDNzs6itLQUTqfzkRMixjhiGVtcLhexWAydnZ3Ys2cP/H4/udIKCgqIwKpUKrG9vQ0ul4vu7m5sb28jGo3C5XLh4MGDCIfDBGgE7tOeL168iIaGBqJOnzhxgojMOzs76O/vh1qtRnFxMdGxq6urMT09ja2tLUxPT2Pfvn0wmUzY3NxEfn4+xsbG4HA4EIlEyLnE4/HotD80NASr1Yrx8XG89NJLBBxk4aXBYBALCwuorKxEMBhEIpGAw+HA8vIyRkZGIBAIMDk5iUQigZKSEopNWVxcpHy4vXv3kn1/YWEBi4uLmJ+fJ63KgzevvLw8Qig0NjaCz+fD4/FgYmICDocD+/btQ1dXF0UNMO3crVu3kEwmkUql0NzcDIfDgffee49WqSdPniSNkV6vh9PpxBNPPIHt7W1y/rGb8e6pEpt8sX87deoUPB4PIpEI7HY7PB4PbDYbCgsLKXyTBRevrq7mfK3Kykr4fD7U1dWR9u3y5cs4ePAgwRUZL6m7uxtlZWWoqanB+vo6tra2UFlZia6uLkxPT+PAgQM4cuQItre3IRQK8c477yAWi5HAtrm5Gbdu3aLvXVZWhmAwSPTur1NMLM7AdV9VDEr5f6qYK5A1GbunUb/ue3O5XNTW1gIAuZcsFgtaW1vB4XBgNpvx93//9zmfszteY2VlBUKhEMXFxejt7QVw3/7v8Xhw+PBhdHV1YWtr6yF7vlqthl6vJ2F1MplEJBKBRqOBUqmk+CEmbufz+Thy5AhNA8vLyxEKhfDZZ58BuD8xraurw8rKCmKxGEZHRyGVSqHRaLC6ugoej0cxO1KpFF988QWy2SyFzSqVSnJHcrlccLlcfO9738Pk5CTi8Tju3bsHvV4PoVCITCaDP/qjP4JIJMLOzg52dnYgl8sJpsrSBBwOBxKJxEPX2t31KLTHg8XWwOxQ+s+FK/Sb1uOG6Ldcv+uGaHNzE4FAAF988QUkEgmamprw+eefE0umuLgY6XQaExMTtMo5cOAACgsLiSC9+7QSiURw6dIleDweaDQaSCQSHDt2DMlkEoODgxSfwefzc27w6XQaPp8PeXl5iEajqKmpoQy1W7du4dy5c2hsbCR6td/vx+joKPbv34/JyUmyono8HnozWywWfPe73wWHw6ELkMfjIbs0mxJNTU2hrKwMBQUF5FrKZDLIz8/HyMgIwuEwTCYTvF4vvvOd72BmZgarq6ukqTGZTKiqqsLk5CSi0SimpqaIYG2z2ehEf+/ePVRXVxMhloWvFhQUoKioiFANo6OjqK+vRygUwv79+8mpt7q6ivHxcUSjUVoF8Pl8yOVybG5uwu12g8PhYGhoCCUlJfB4PCgsLERBQQFu3LhB+UpFRUXU/CiVSoRCIczPz2NpaQkWiwUejwf5+flwu90oKiqC3+9HZWUl6urqMD09jebmZrz11ltYX18nZ2F7ezuxkux2OwVlRqNRRKNRZDIZsuJ/+OGHFMD61FNPQS6X4+7du2hpaUE8Hsfw8DASiUTOmmp32e12wiQUFxejtraWWFKZTAbz8/MoLCxEMpmkC/5nn30Gj8eT8zU5HA7l1h05cgSRSASdnZ0YGxtDW1sblpeXIZFIKECV5aK1tbUhHA4TUXp3vM/BgwfJBdnV1UVi7vz8fKysrHxti/qvql83mdk9nXiwqqurMTU19b/VLLGGgVHTHwxc/XUhrPv370c4HKZ1+szMDDgcDs6ePQulUomNjQ28//779PFs5c5ek7sfu0KhAJ/PRzwex+HDhzE+Pk6ZYqFQiKzhrFQqFSwWC9HC+Xx+DuCQw+EQjToejxOglwVGDw0NIRKJQKlUIpVKobS0FHq9Hnfv3qXnpa6ujuJflpaWUFhYiMXFRSJdM5aQyWQigGksFoNAIIBWq6V1NeMxFRcXo7GxEQUFBVAoFFhZWSHTjk6nI/s9y/VjzdCDxgtWjydEv7163BD9lut33RBlMhn89Kc/xfb2NjgcDmpqaohaOj09TVoKNk1wu92QyWSoqqqiFQ/jgRiNRnzyySekARGLxSgoKIDBYCDx4ubmJo4cOUIiwZ6eHqyuriIWixFRuaysDCqVCp2dnfjiiy/osX7/+9+HWq3G+Pg4ent7UV9fj6WlJXKS5OXlYWFhgQTie/bswenTpylVmvE8mNsplUqRuymZTILP55Og2Ol0ktB0YWEB09PTOHr0KEKhEEVyLC0tQSKRoLy8HH19fUilUpifn6dpyrPPPkuaJJfLhZKSEgwODgIAkZdZVVVVQaPRIJ1Ow2azYWxsDHV1dXTh1el0yGQy6OzsRDwep/BP4P76IZ1OY3BwMCf/raioCFVVVdjc3IRarcbY2Bg51RhjiN3QFAoFtra2MDg4iIKCAqyvr1OT5/P50NjYiFQqBS6XS3yhra0teL1eFBYWIi8vj0SsEokEoVAIm5ubFBNQUVEBnU6HpaUlik6QSCQ4d+4ctFotMpkM3G43urq6UFJSkgO1U6lUUCgUEAqF1GxUVlbC6XSiuLg4J5WbNca74zzC4TA2NzfR1dWF9fV1+P1+SKVStLW1obm5GalUCouLi/B4PBgbGyOtF/uZUqkU5fglk0mUlJSgvb0d6XQakUgEX3zxBZGA29raEAgEyCAgFArB4/Hg8Xggl8tzGoX/U1Ocr/q6rLH4TS7LJSUlmJub+42+/69riFQqFaXDDw8P00qLz+fTRG43ewn4pVaJuTsBkNtyd5PDfk7G1WLaR+Bht55SqaQmgpkhKisrMTExkUOOZqRxr9ebgwFobW1FZWUlgPtQ1Lt378Jut4PH46GsrAw3b95EXV0dpFIpJicnidfGQpN3dnZw7tw5KBQK/OhHP6LGTaVSIRQKQSKR4A//8A8RiURoaltQUIDLly9Do9FgcnIShw4dAofDgVAoJKK01Woll+qvmgL9r1Q6naapPru2/XOu3+T+/fVENI/rd1osLyYYDCI/P59OM729vaioqCB3EUtBZiUQCKhZSqfTFCjIxItmsxlarRZqtZrEmizD6s6dO2htbQUAyplipxWz2Uw2+7GxMXqxMaJwNBqFQqFAc3MzxsbGUFJSAoVCQVC+1157DR9//DG5hpgFmn2eTCaDUqmkU5LdbofL5SIIWk1NDYaGhtDY2IhkMol4PI6ysjJaUbBMHR6Ph3379kEkEmF8fJySzlmGVCwWw9tvv40nnngCMpkMlZWV1NyxkM/d+paJiQnU1NRga2sLo6OjdKGfmJhASUkJampqUFBQgKqqKoyOjpL1lqW+6/V6WK1W9Pf3w2w2Y2dnh3hBDPmvVCqRTCZJq8Pj8dDR0UEgPK1Wi/b2dgwPD0OhUMDpdNJ6aXBwEAaDgSZK+/fvR39/PywWC00Gw+Ew0biZ6HphYQGlpaVYXl4m9xdrPqPRKObn56HVauH3+2kty7LMGLQuEAigsbERt27dwsrKCqqrq+Hz+TAyMkJIAZFIRK8vNpFjWW8smJI1kFarFQsLC1Cr1YhEIlhbW6MGymKxYHFxEfX19XC73VAoFODxeIhGo1hbW4PJZILD4UAwGERZWRlEIhGRimtra+HxeDA/Pw+z2Qyv10sMnLy8PNJm2Ww20h39nyj2Gtw9jZJKpQgEAr9RM2SxWBAOh3/jGBCFQoHq6uqHgrFZBQIBWCwWjIyMAABNEfPy8hCJRB4p7GZIBolEQg0Pm4Awtg8r5vpdXl7Gt7/9bXz55ZcUmLy7WHPE+Fazs7Pw+/0oKirKabwUCgXp1HYXE/ivrKygu7sbAIgc/dFHHwEAxsfHYbPZaLpdUlJC18WlpSWa8DL9E3t+bDYbLly4gOXlZZSUlJATdH5+HkVFRbh8+TIEAgGWlpbgcDgorJjJD3Q6HT0/v0kFAgF8+OGHaG1tRUVFxUNNz8rKCuLxOFZWVige53F9vXo8Y/t/oD766COIRCKIRCKsrq7i7bffxtLSEjgcDi5fvkzAvAcFnIODg1haWoJerwefz6cLmVQqRWNjI1555RU88cQTKC0thd1uR319PRoaGhCNRnHgwAGCh5WUlMDhcBB7gzl6RCIRTpw4AaFQiFOnTlG4ZiqVwszMDHg8HoWFqtVq8Pl8lJSUQC6Xo7q6GuXl5QgGg+T+kMvlRMsGQGPlZDJJeiW9Xk/ZWL29vQiFQjlU1o2NDcTjcXg8HsRiMUQiEaRSKbS1tUEsFkOv15OuamdnB9XV1bh8+TKEQiG+/PJLDA8PY3h4mCYLzKUCAAUFBVheXs5BBXR1dUEikVCMwNDQEBYWFiCRSNDf349MJoPx8XE4nU6sra1hZGQE7e3tCIVCtLZi3JXNzU3i4rDGhTluWLEmg60z1Gp1jpA2m83CaDTCYrGAy+VSVIhKpcqJE9nZ2cHKygpWV1dRU1OD7e1tnDlzBgAwNzdHZG+WM9fR0UHWZFZs3cHErEx7w35fFy9eRGVlJZkZtre3yXrMokXYY45GowiHw7h37x7FMlgsFvT09CCTyUCpVEKlUsHv96O1tZUmTKdOnYJKpSINCHDfScamjKxBT6fT8Hq98Hg84PP5JND1+XwoLS1FKBQiAKtMJvvKVeDu+lWmjF+VHM7qwdUcw1h83WJRLwaD4SvT5b9qjbK4uIienp5f+fUnJycfWtd4PB5qhh8smUwGm81G7ztWDz62Bx8TM3Y4HA6IRKKvfDyxWAxyuRyBQAAjIyM5qI7FxUUkEokcTAKHw4Fer4fX66VmCADu3LmTw6Xb2dlBIBBAMBiExWJBaWkpVCoV3n77bbz33nsEL9294hQKhTh8+DAuXbqEgoICDA4OEpKip6cHPT09UCgUEAgE2NzcpHBm9phlMhmx2h6M72BaxN3/f3d99NFHcDgc6O7ufiQxnB2kvonLn6/6mb4p9bgh+n+gDh06RJqDbDaLRCJBAMLq6uqvtO4yh1Q0GoVMJsPOzg6uXr2K7u5u8Hi8HGZQJBIhfk1rayu4XC5xbAQCAUwmE4xGI61n0uk0otEo+Hw+vvvd70Iul8NutyMUCsHv9yM/Px8TExO4desWxsfHEQqFUFxcTOGzxcXF4HK5kEgkKCoqgl6vpzVaMBjE8PAwfv7zn+PatWu4ceMGOjo6sLy8jIWFBUSjUdy+fRsWiwUzMzMEfONwOLRiisfj5OzKZrMQCARoaGig7DWHw0GTnBMnTtBzxSoUCsFkMqG0tJQayLy8PBQVFUGj0YDD4SCdTuPo0aNIJpMoKCggnRDTRjQ1NZGGQaPR0O+BuVXq6uqQSCQoJyyRSOD27duIx+MQiUQYGBhAYWFhjrPSYrEgFovBYrEgmUzC6/WitLQUAGgcz1Z6V69epbG/3+9HWVkZYrEYhT8C929GyWQSx44dQ15eHmZmZpBOp5FKpVBUVIRUKgWxWIz6+nosLCzkxOKwqBOVSoXa2lrs27cPwP2JokQiIU3a4cOH6QLPbvrxeBxCoRBLS0uYm5uD0WjE+Pg4amtr4ff7UVVVRY05j8eDVqtFfn4+6uvrcePGDRQVFZFNv6GhAWtra5ifn4fdbsfCwgI2NjZoXTw1NYXu7m5IJBKMjY1RSCiHw0F5eTm6urogEAgwMTGBlZUVhMNhsof/qvqq953RaMxp0L5uMd3d1ym5XI7FxUWYTCaMjo5SHJFarc75uF8ltH0wouNR9Si7PtMB7m4+amtrcfr0adhsNpw5cybnhsccj+zx2e12+rf6+noA98X9LLuQNYVarTYH5REOh0k3WF5ejrm5OWSzWZr8MTs7cH86rlarwePxoFarYbFY6OtwudycxovH46GpqYnWeiwT0u12Q6lU4vPPP6efhzXBra2teO+997B3714sLy+joaEBOzs78Hg80Ol0JAI3GAzYv38/stks+vr6sLy8nCMk39jYwN/+7d/SxGw3r+ir2EXPPvssFhYW0N7eDqvV+tDvx263QygU5jzP35T6qp/pm1KPNURfo74JGqKFhQX87Gc/o79jq7MHGSGs2Mpp7969sFqt0Gg0+OCDDxCPxxGJRFBcXIwjR44gHo/TzplFFTAhZUlJCSQSCQoLCzE4OEiNRjQaRVFRETo7O8kNVVFRgbW1NZSUlGBjYwODg4PY2tqiqBEej4eioiJotVoUFBRQ2rNGo8mBn4lEIrJyx2IxxGKxnMnXd77zHbjdbgSDQUxPT0OhUMBoNMJqtZIAksvlYnBwEOl0GrFYDC0tLbBYLFhYWIDVasXFixeh0+kQi8VgtVpRVFSE7e1tvP322/R9iouLUVBQgP7+fpSXl2NjYwPt7e1IJBKYmJhAUVERVlZWsHfvXiiVSkQiEfh8PnC5XAQCAUpiVygU8Hg8sFgsWF1dxfLyMgwGA0pLS0kYvr6+jqmpKUSjUdJhVFZWIi8vDysrKxS8m06nyZY+OzsLiUQCi8WCYDBIF1mpVIqqqiocOHAAsVgMn332GVnK9+7dC4FAgPX1dQwPD2NoaAgWiwWhUAgvvvgiRkZGMDc3R01dS0sLstksCgsLsbq6SlC9gYEBWK1W+P1+NDc3k4heJpPB5XJBIBDQumt9fR1PPvkkiVlZ3Awjh7OJUywWg8PhwBdffIHS0lJ4PB5MT0/j9OnTcLvd0Ol0pIvq7e2F3+9HTU0N8vPzsb6+jo6ODlRXV5Nmo6OjA4lEAufOnYNIJMLY2BimpqbQ0NCAlpYWcjB2d3fjyJEj+Pjjj3MmNgqFAnl5efD7/TlgPlZFRUXU/O9ujNjq96vqV2mSfl2EBvBLVANbeXu9XgrM/aoIjd9mCYVC1NbWIhwO5+TYyWQylJeXQ6fTYWZmBsvLy6SpcTgc2NjYoKafIQ2i0SiqqqqwtrZG698HmUktLS0YHR3NQUO88cYb+PDDD3Ps9cB96/yDESq7yd6MPWQymUgUXV5eDrfbjWQyCS6Xi9bWVlgsFrzzzjtIp9Pg8XjYu3cvtre3MT4+DuB+M5pKpVBRUYGhoSHs3bsXRqMRKysrMBgMmJycpLDtTCaDN954I6cRstvtKC8vRyQSwQ9+8AO0tbWhp6cHf/7nf57j9AXwv023/l0Xc0hns1ki5v/f/pkei6p/y/W7bogSiQT+9m//9pFd9e439+4SiUQ4fPgwEokEVCoVtre3EQqFKGGaiYErKysRj8dpBcNcNhqNBiaTCcXFxVhYWEB5eTl6enoohbyzsxP19fWYnZ3FE088QRc+doJKpVKYmJjA4OAgVCoVgR3b29uJRgyAso2WlpawurqK0tJSyGQyXL9+HZFIBGVlZeju7qYL5UsvvUTi65s3bxJm3+FwIC8vD2q1mjLUOjs7MT8/Txb6/Px86HQ6GI1GdHd30+qMEaa9Xi9u3LhBeoLOzk6UlpbC5XKhpqYG8Xgc3d3dKCwsxMbGBkwmE0QiEVpaWiCTyWilyZxtDNZ24sQJwv37fD6IRCL6vbhcLnz22Wc5N1GRSASdTgeZTAaFQoHt7W2srq5SRMnQ0BDkcjlFHbjdbtjtdjidTnLisZDW/Px83L17F729vXA4HKitraVU8Gg0itHRUTz33HO0HonFYnA6nairq0NeXh4UCgWi0Sg+/fRTgkE2NDRgdXUVra2tSKVSCAQCCIfD2NraImu+SqWitZNOp0NDQwMmJyfppsQaNq1WS7A6Nubv6+tDZ2cn8vPzMT8/j5MnT5LofHJyEjabDaurq2hpaSGxK2tK2tvbMTIyQs8nl8vFG2+8gampKVoVSqVSHDx4kKJWBgcHsba2RiLhiooKVFZWgsPhYHt7Gzdv3qTfDQsIbWpqgtFoxI0bN2A2mx8SGD+qFAoFMpnMr6QSm83mR4Icd5dAIEBlZSW4XC42Nzfpuf51GiL2u/xVMMhf1bBxuVzs2bMHExMTtPZ48GsxB+Tupqa8vBzT09MkmK6urqbmAni4EWT6I3aos9vtOR9fXV2N5uZm/OQnP8n53hUVFZSF9lVVU1ODtbU1AoH6fD6EQiFks1mk0+mH3H9KpZKifnbX7t8Tc+oyptmRI0fw3nvvIZ1OQywWQ6fT4ezZs/Q7SiaTdOByuVz4+OOP8eqrr5JT9bchsmYmg93X5N9FbW5u0kGDxQj9367HourfsxoeHkZJSQkJHHfX+vo66urqsLCw8BCXRiAQYHBwkNwNDFLG8PZ1dXX46U9/ikwmA6vVin379sHtdpNouqysDIuLi5SRptfrMTs7i+7ubrS3t2NpaQnHjh2DyWSCSqWiGxo75eTl5eHll1/G6uoqIpEIFAoFvvjiC3g8HrS3t2NiYgLZbBY1NTVYXV0Fl8uF0+kEh8NBbW0tfD4f9Ho9Xn75Zbz11luoqKjAvXv3cOTIEVqLMG0KcwoxbtHMzAy2trZoWiGRSBAMBinYNh6Pw+Vyobq6Gnw+H16vl25UNTU15ORKJBKorq5GXl4eabkYS0goFFJTxKZsIpEIbrcbfr8f09PTyM/Px/Xr13Hy5El4vV6y2jL4224tDfDLm2ZDQwN0Oh3u3r1LsRnLy8sIhUIE9SspKYHT6URrayt6e3vxzDPPgMfjwefzUdDr1NQUOjo6kJeXh7m5OaTTaWxtbYHP54PH46G2tpZWfk6nE2KxGGfPniVOFTvJ6XQ6bG1twWQyweVyoba2FnV1dfD7/VhdXUU8Hofdbifu0czMDACQaNpsNiOVSsHr9eL48ePUQKtUKjrFs8kBuwEuLi7iueeew/LyMioqKkiTMTExgWeeeQYKhQIzMzPIZDIkUFYqlTCZTFhaWgJwfyrABNksFNfv9+Py5ct4/vnn8cUXX9DjYiGwLL5GKpViYGAg5/3m8XhQWlqK8fFxpNNpgm4C+JVBpwB+bcPC5XIhlUpRWVmJqampr9RZtLa2ki0/EAhgc3OToJy/yurPbOmPIlcLhULk5eXR6ycQCECn00EqlZJesLW1FbOzs0ilUnRoYlO73XypB7/29PR0znOzu7l51POi1WpRWVmJoaEhBIPBHKK0zWaDRqOh1fvuWl1d/UpkQm1tLWQyGZLJJA4fPozFxUXs27cPs7OzsNvt+OCDDwAgpxnicrkoLy8nMwo7kKrV6pxGcHt7G6+//jp6enrw8ssvo7+/n8KoE4kELly4AJVKhUQiAb/fTw5JptFkxaQLD65av44F/0EO2MzMDMWG/C4jqth1I5vNUlbkN7keT4i+Rn0TJkSXL1/OiVlgVVZWRnbtBydISqWSAGRMZ6JQKNDT00MnkQedLnv37sXGxgby8/Nhs9lQVFSE6elpLC8vY35+HltbW7R/f/rpp9Hd3Q2Xy4W2tjbs27ePQlbZY2H6j/HxcaIuswsC+1mkUikKCwshEolQUVEBLpeLsbEx1NTUoLi4GJOTkxgdHYXL5cK+fftgNpsxNTWFra0trK6u4syZM+RiC4VCmJycRFVVFQYHB0m3E4lEwOFwYLfbc/LcdDod5HI5QqEQ+vr6oNfrEQgE8OSTT2JychLhcBjPP/885ubmEAwG0dvbS+6zcDiMN954AwMDA1heXiatl1AohMfjgd/vp4kKs+WKRCIIBALK+bp79y6EQiFisRhZoY1GI5588knE43EIBAJ0dHSQNZlNWNgpWq/XE4qhq6sLdXV1KCgoQDQahc/nQywWg8FgwKVLl5Cfn49wOExr0oKCAuzs7GDPnj2YmpqiVO93330XHA4HDocDJpMJjY2NWF1dxczMDBG02XMqlUqxtbVF2hemL+vt7aVIg4qKCpSUlKCyshJerxc2my2H6s00V16vF729veBwOOSeYqwo1mTr9focUW4qlcLw8DB6enrwxBNPYG1tDUqlEnK5HFNTU+Q2jEajFMHB5XKhVqtRWFiIlZUVRKNROuWvr68jlUpRftq1a9dyphesmWDCbqFQiJGREbKws9Ugcz39usvrV/GK2tvb0d/f/7WjPQoLC6kJ/FVVXV2Nzc3NnGxHVn/xF3+BdDqNH/7wh5BIJDnrQIFAgJdffhnd3d2YmZnJedwMD+F0OvHkk09ibGzsoXXWb1IajYb0bDKZLMeGz8T+X8Vw2l1s2sOucRUVFZiamqJ1cGdnJ7LZLF3/djtKAdChjuFJdjccZWVl1PQLhUL82Z/9GWEsfD4fOjo6YDQa4ff78cYbb5AcYH5+HqOjo2hpaYFYLMbPf/5zlJSUYGZmBi+88ALKy8vB4/Hg9/sxNjYGhUKBeDyO+vp6mtyJxWJq0EQiEZLJJNRqNbn02P2ANYCtra05zRRbY2UyGZqQc7lcZLNZ+Hw+dHV1QaPRIJFIoLS0FGaz+SvF+ZlMhqQPzDnKvhbTC7Jcx92OOhYb9aCoHMiNlXrUvz/4cb9u/fZ7Ge76z7n4fD6tmh71bysrK490tbCk9sbGRigUCtTU1JAjhzk2HrxgM+dJJpPBxsYGVldXkUgkaPwrk8lIxNzR0QGXywUej4eBgQHMzMzQKYfllbEoECZSLSgoeGgkHIlEaHrFmCN1dXU04g8Gg3A4HCguLiZnFnsjFxUVgcvl0uSEAQidTic54/Ly8nD06FHU1NQgLy+PIgTEYjFmZmbA5XIpVX1rawvNzc3Y3NyE1+tFZWUlOjs7adTOfgeBQAB2ux1Xrlwh7cD169cRCoWQSCRQWFgIrVaLY8eOQaPRUHgs0zfl5+fD4/HkIP6Z5oppsNjF8NixY+SsYxlzzF3CglRv3rwJiUSCzs5OzM7OQiqVEoxyZmYGqVSKTu5MHxMIBLCxsYGrV6+ipKQEQ0NDuHjxIng8Hp3MM5kMnE4n7HY7mpubwefzMTs7i7m5OZoWCIVCbG5u4tKlSzAYDNBqtThw4ACkUilKS0tRUVEBp9NJVnemFWETwitXruD27dvo7e1Ffn4+fD4fenp6KL6FNeM8Hg9bW1s5LhU+n4/m5ma88cYbKCwsxN69e1FdXY2CggK6kKdSqZwA10wmA5/Ph4GBAaTTaUgkEuTl5UEoFEIgEIDD4cDpdGJ7exsnT57MeX9kMhnU1dVh//79EAgEcLlcOYcE1kAwBx57jHK5HMXFxQ+9R79qojM9PY1jx44RGmF3PRg4y+zhuwXILBWeia2B+6tYxgB7VC0uLmJoaAgymYxei6ySySR6enqoedj9uL1eL7a3t5FOp+m1/2CJxeKH/o7P58NisTy00onH4/D5fDCZTDnNEPu+D16zvmoNk0gkyGig0+lI8xQOh/Hll18ikUjkHAYDgUDO48xms1hbW0M6nX5ozTk7O4v9+/dDo9Hg5Zdfxvj4OMbHx+nj2PvbarXi7//+77G1tYWlpSUEAgGUlZVhZWUFKysrqK2txczMDKqqquDz+ej3Mz4+Di6Xi8nJSfD5fIyOjiKbzRL8lYEpl5eXweVysbS0ROaRZDJJ+r/6+vqHRMxer5emouxP4P51uLe3FwKBACMjI4jH45ifn//K+Bn2tVQqFRYWFqBSqXK+FoNR7v6TicXT6TQ1NA/Wr/v33R/32xZoP26I/h8o1lA86oLKuC7MBba7iouLicp78uRJ6HQ6qFQqFBQUQKPRwGaz4amnnsKePXvoc4RCIQoKCiiqQqPRoLy8HCKRCCUlJdBoNCgpKaGROXNbmUwmbGxswOfzUbwHj8fDD3/4Q/zgBz/AjRs3YLFYKJOKWdlZDQ8PY2dnh+CSAwMDSCQSEIlEsFgsmJ6eJtdTJpOBXq+noNUvvvgCiUQCHo8HJSUlqK6uJtdSLBbDxsYGhEIhmpqaUFhYiJ2dHVRWVmJhYQGBQAA9PT1Ei37yySdhMpkQCoVQUFBAROupqamHbkTMsm6xWOgmf+vWLczOzuLKlStYX1/Hj370I1y+fBm9vb2wWCwYGBhANpulZogBFyORCLGHioqKYLVa6YSVzWZJy1VVVUXaHHYxZwJ7r9cLq9VKIEORSAQej4e5uTkUFxfD6XSSRXtnZwerq6skau/t7cWhQ4dw8OBBmhyq1WqalCQSCUgkErjdbkxPT9Mq6ebNm4hEIpiYmEBeXh4uX75MU5IXXngBzz77LFwuF1pbW1FTUwO/3w+lUom5uTlYLBZcu3YN6XSapmEMlqhSqRCNRjE0NEQMLLY6EYlEFBL8wQcf0E0oGo3i6tWrmJubw507d0j8ypLW2c+wu9iKjjmMGJCPJbJrtVpaxwL3b75nzpyB0+nE3r178dRTT+HAgQOIx+MoLS2lg4lcLidNilQqxZkzZ1BdXY3Tp0/jyJEj9P3Z1y0pKcl5XMwQUFdXR+wa4D5lG7g/VS0vL0dxcTH27NmDU6dOEfBPr9fj0KFDOHXqFKLRKJ588knU1dXRpKy4uBhms/mhawlrcjc2NsDn85FMJqnRk0qlaGlpweLiIjVKrJEpLy+n+BX2Pn+wAdp9iq+pqcHp06dRXV0NnU6HEydO5HysQCCARCJ5aMWiVCrhcDhymph9+/Z9ZVPZ2toKvV4PpVIJq9WKvLy8R34cq/Lycnrts2KMK+C+25flI54+fZokCuw1zQ5DVqsVNpuNDg81NTW4cuUKioqKqEGtra1FQUEB8vLy8Mwzz8BqtUKr1cLhcEAqlaK6uprMFTweDw0NDZDJZGQEkUgk8Hg8EIvFdD2Ty+VQqVSw2Wx0KGGu291rOJ1OBz6fTxywtbU1XL58mYKYmcaUUeZ/1apLp9MhEAjA4XDQmhW4//tmrKXdf0qlUkil0hyH84P16/5998c9+LP979bjldnXqN/1yoyNOK9fv47p6emcfysvL4fZbMbi4mLOXp3H46GlpYVWYIODgwiHwyguLobVakUikcDMzAxNDxKJBORyOXQ6HVpaWmAymYiJ43Q6yTWUTCYRCARI98KCGL1eL4lfy8rK4HK50NXVldO9V1RUoLy8HKurq+QC211isZjEy0xz0NLSAqfTicnJSdjtdkilUshkMop/mJqagslkQiKRgFqtRiAQQFNTE/r6+miNIxAI8O1vfxs8Hg9ffPEFIQVYCYVCvPDCC9jZ2YHRaCQdksfjITEwg6rNzMzkaB4sFgsuXLiAO3fuYHl5GTweD4lEAhUVFRgYGIBcLsf29jaKi4uh0Wig1+uxubmJWCyGqqoqDA8PY2lpCRqNBpWVlQTTVKlUZL9nq4NAIIC7d+/mnJBZnMi5c+dIl7Ozs4N0Og2VSkUaslu3buHw4cPY3NyEy+WiiJB4PI729nbs27cPfr+frPKM/aPRaFBXV0ciTTapYQnzR48exfr6OiYnJzExMYHq6mq6CYVCIezbtw8cDgeJRIK4TEVFRRCJRAS3DIVC0Gq14PF4GBwcBJfLhU6ng0gkwtGjRykPzmq1IhAI4NNPP8WePXsofXxpaQnPPvssrl69iry8PPT09BCAsaioCHK5nFxRDNsA/NICXlRURL+Dra0t7Nu3j6aVly9fhtFoJGH12bNnodfrkZ+fT6uN9957DzabjYTAAoEAPp8PiUQCBoMBGxsbOH78OObm5ijNncEqXS4XXn31VajVaoRCIfyX//Jf6HdrNBoJcjg8PIzz58/D6XRi//79xGFaW1sjPlZtbS0mJibw8ccfw2AwID8/H3a7HX19fbDZbOjp6YHZbMbm5iYuXLgAgUCAN998M2f9lJ+fj9XVVZrCsvgaiUSCuro6bG9vY3p6mqjVLS0tSCaTWF1dhdPpJCq00+mkOJgzZ85gcnISy8vLJLo+evQoxa9otVq8//771GAIhUIolUr4fL4cPRKfzye0x4MZaI8qHo9HEyJmPmFrWg6H89DU50GHIFu5MTSI1+vFG2+8gc3NTQwNDWFwcBA2mw3BYBAlJSUwm80YGxsjfWJ9fT2mpqbA5/Px6quvQqfTPTIv8n+lRkZGCOFRWFhIQNtfVSxjUiwWg8PhwGAwYG5uDouLi9jc3ASXyyUkyTfRsv+/Wo9dZr/l+l03RMD9SdCVK1ceclCwnJwHAV1MA7K9vU0nQ3aTZAGIU1NTOQ0Lh8PBsWPHoNPpCC3P4iYUCgWCwSBqa2vhdDpRWlqKqakpjIyMUJdeUlKCQ4cOQSQSwefz4csvv8xx37S1tdGUYHJykhqN3Se+xsZGisVwOByYmpoi3cDCwgLKyspgt9uRyWRykrRZ9pBWq8XGxgYaGxtJePrss8/Sz+lyuTA8PAytVkuOm29/+9vo7++Hz+eDSqWCyWRCNBpFYWEhrly5AovFAr/fT0nVu7UaBw4cINEuc7g1NjbSGqW3txc6nQ52ux1VVVU0yZLL5Ugmk1AoFGT7TaVSUKlUqKysRH9/P7hcLioqKhCNRrG8vAyfz0ekXsZGGhoaQnV1NZqamnJOwEzkzeVyMTU1hf7+fhQUFKCiogLd3d1Ip9Ow2+1oaGiAxWJBJBLBZ599RqfWpaUlKJVK1NbWkpDZ6XQiFApBp9OhrKwMRqMRCwsL1HCxFUd7eztNhTKZDKRSKTo6Oh5ym7BJF5/PB5/Px8DAAK0kNRoNjhw5gp2dHRQXF9NK8Z133sHW1hZ0Oh2am5sxPT2N8+fP00Tp5s2bEIlEiMViqKmpoe/P5/Px4Ycf0gSIaUsSiQRRmT0eD9ra2pBIJNDe3o5wOIxgMIjPP/8c29vbMBqN8Pl8eP7558n99MMf/hAqlQrj4+PQaDTg8/ngcDgoKyvD0tISdnZ20NTUhJ2dHWQyGQwMDBCVXCaTEQRQIpFQ08hu9hUVFcjLy8PQ0BA57lg8y9raGi5cuECRE6WlpZidncX169chFouxublJtPDKykrMzMzA4XCgq6sLlZWVWF9fR1tbG9RqNT799FOEw2GatMhkMsoGDIVCSKfTsFgsUCqVFPQqFotp6gQAU1NTpLX6gz/4A0qHf+655yhi58MPP6QDy+LiImw2G0ZGRuj7sPUmc7j6/f6c91pDQwNEIlEOZHG3K4w55Fhjo1arc7hOrGFsamqC1WolUjVwf5oeCAQemfmm1+uxtraGF198EVarFZlMBmtrawS+fe211yCRSPDhhx8imUzS9DoUClFT9OKLL0IoFP7WXGTJZBKTk5N0ePpVWhtWbIq9sbEBs9kMPp8PjUaDqakpwmXY7XY4HI7fq8iPxw3Rb7m+CQ1ROp3G7du3H0Lt2+12RKNRbG9vk36Bwc1SqRQkEgmd5qVSKdRqNYLBIMrLyzE1NZUjrlSr1aipqUFNTQ0CgQDkcjmWl5ehUCgwOzuLqqoqOJ1OCAQCBAIBaDSaHEtyW1sbDh8+DLFYjNu3byMSiVBWVnl5OWUgNTU1IRwOY3x8HENDQzCZTDSBeu655yiL7fPPP6e0+d3C1vz8fIINsqqqqsL8/Dy56VKpFE6ePAmlUol79+6hvr4eOzs7RJzdHeTIJifJZJJCU81mM11g2HrH7/cjkUjQ99l9etXr9RAIBDh79ixWV1dx69YtVFVVQS6XQ6lU0g2NTXrC4TCUSiXMZjOkUikmJiYgEAiQSqUwMDBA4s7q6mqYTCYMDw8TETqVSuHEiRP0/MZiMYhEIvD5fJSWloLP51PwYyqVwqVLlyCXy2nfz1x98Xgci4uLUKlUEAqFqKiowNLSElpbW6HT6XLElteuXaP1rNFoxP79+wHcb9TD4TD6+/sRDodx4sQJpFIpEs6r1WpaH3300UeQy+U4ffo0nVaXl5eJlD4wMIC+vj4oFAq0tLQglUpBrVZja2sLxcXFWFpawieffEK5VufOnYPP54Pf70dDQwOuXr0KvV5PafOpVApHjx4lZ6BCocD169exublJk4319XXE43HSIqVSKdTW1iIUCkEqleLzzz9HIBBAKpVCMBhEa2sr8vLyYDabkUgkkEgkaCKzsrKC0tJSaLVaEsGvrq7CZrOhsLAQPp8PkUgECwsLJLxOp9MwGo1QqVQ0eUmlUuBwOFAoFAT+W1hYyJmKiMViyOVyHD16lCZYTPjKqM0FBQXk/GOrpdHRUaysrCA/Px+RSAT79u2DVCrFxYsXcw5HGo0GKpUKDocDcrkc0WgUPT09OaLjwsJC2Gw2hEKhHPJzUVERzpw5Qy44jUZDwvd0Og25XI7z588/ZJsH7q8EJyYmCOewu9gUjdGqpVIpxRLt7OwglUqhuroaR48exU9+8hNEo9Gcw1ZxcTF8Ph/OnDmDL7/8Em63GyKRCDKZDH6/n6Zhu2+Je/bsoXUVI0yziefg4CBkMhnsdjs18gMDA5ifn0c4HEZLSwuFUi8sLOCP//iPCT7LHIW/yZSITWmNRiMEAsFv/PnhcBiXL1+GVqslXMiDMoDfx3rcEP2W65vQEIXDYSwuLqKrqyvHhlpaWopsNouFhQW6QRcXFxOFeWlpCVarFRUVFeRCMBgMGBoaglqtplO6UqlEdXU1ieKsVivy8/PpNKvVauF2u4kszKYKu1dPXC4Xr7/+Olncb9y4gWg0imw2iwMHDhC8j/0sCwsLMJvNZAe22+1IJBLE0FlYWMD4+DiEQmHOeLuwsJBEkuzvDQYDrZkY84fpYwoKCtDX14fq6mpwOBzE43Gsr69jbGyMLoDPPfccent7oVAoEIvFyJ2Xl5dHJynmJvoqem91dTVNhRjXpLi4GFqtlmzpTKSZyWSg1WrB5/PR09NDVuYHbcPsOWNCarvdjn379pG4nJG6t7a2YLFYkEgkUFNTg9nZWWQyGdLndHR0wGazobKykujlLDQXuN/QyeVytLa2QiaTwWKxIBAIwGAw0Jj/xo0bNNWwWq3gcDjIZDK4efMmWa5tNhtRlM1mM4Xczs7OUt4W49EUFRXRZITdaMLhMEHy4vE4Njc3KZiX5dlNT0/je9/7HmZmZrC6ugq1Wo3R0VFyWjG+VHl5OTnV1tbW4Ha7KdvN4/GguroagUAAoVCI9Dgs3FWlUuGLL77AysoKPB4PAKC5uRklJSXIz88nlxx7vXZ3d0Oj0cDtdqOiogKZTAaff/45rUxPnz6dA6/MZrMYHByEVquFRCKBy+WCSqVCKpVCYWEhlpeXKT+M/f2Da6LKykpYrVb09PTQgYcR37lcLmkPa2trweVyce/ePayvr9PvfM+ePRgeHkZ1dTWGh4cfeu2dOXOGAKOMTr17qqLX62E0GmEymXIORu3t7aitrcX4+Di6u7vxzDPPAAA+/PBD+pj6+nqaNj1YOp2OqPm7USLsfcYCoMvLy7G9vQ2bzYZ79+6Bx+NBp9NRTt+D79MHg2NZ7XaXGQwGAtDa7XYcOHAAhYWFFKWzsLAAkUhEcTZ2ux1NTU2w2Wz0nrp79y4ZNMrLyzE/P48XX3yRAqczmQzFa/wmk6KZmRmoVCpaBf+mn8+mp0tLS6iuroZEIsmh4P++1uOG6Ldc34SGKJvNIhgMEmRvd73yyiu4dOkSTUz4fD7y8vKwvr4Ok8kEj8dDNutAIPCVNFy1Wk03JKvVipqaGlRWVuLdd98lMfb4+Dg5E9iFiY3ahUIhysvLceDAAXR0dNDHCgQCaDQaNDY2EgfH5XIhHo+THqeoqAjZbBZqtRpmsxlbW1tkXd+t2WFjXWbznJ+fpxttdXU1iSKZSBYARkdH0djYiN7eXkQiEVRWVtJNzOfzQSaTwWw2w+/3w2q1kujZYrFgbW0NZWVlJBJ/VHZQc3MzTCYTrSrGxsawvb0NlUqFqqoqCIVCOBwOrK2tQa/XIxaLwefzUSOzsLDwSH4Kl8tFc3MzwuEwXC4Xjh8/jry8POj1ekSjUayurhIkr6SkBC6XCy0tLZBKpRAKhVhcXEQ6nYbVaiWNwczMDIRCIWZnZ+Hz+TAzMwONRoNjx44REVsikZBIlt1Y2M2VcapYEwPcd/MMDQ0R2mFlZQXFxcUYHh5GIpGgiJIrV65AqVRCKBTCarVieHgYpaWlKCkpwfz8PGlHQqEQzGYz5HI5MpkMPB4PDAYDPvnkE0SjUbz22msQCAS4du0aWeitVisuXboEq9UKsVgMq9UKr9dLnCU21RQIBLh48SJNT6xWK/h8PrLZLJ3cs9ksvF4vJBIJfvzjH5NtWCaT4U//9E/J6cQmKgaDAfPz85iZmUFhYSE9h1wuF5999hnKy8vR29uLc+fOIT8/H6lUClNTU0RhDwQCqKysRFdXF5RKJRHo2TrI4XAgGo2S+4pVW1sbNcTs0NLe3k4uIY1GA4vFArVaja6uLigUClq579u3D3fu3KEmvaioiDAFfD6f3E+nTp3C2NgYNjY2cOjQIXz88cfQaDRYX1+HTCbD8ePH8fnnn+fgAcrLy7G+vp4zwWUNAgA4HA4KNP4qSCUTc+921jKxPhN3q1QqJJPJr8ydY2aFdDqNvXv3oqur6yGHWlNTE1pbW/Gzn/0M6XQaGo0GR48exbvvvouysjI8/fTT4PP5mJiYQDqdpgyyTz/9lICYcrkcRqMRbW1tKCoqwujoKLq7u3Ho0CGisrOMRYvFQs0e00K6XC5a7zGdVzgcRmtrK1QqFb0XmWzgf3VCFIvFcO/ePYrcKSsr+51CG/9v1eOG6Ldc35SGaGNjA7dv384BmwmFQvzVX/0V/H4//vt//+/0pmYCUb/fT+sUxq14lCtDLpdTrhVw/5R2/PhxdHZ2Em6fiWFXVlYI4MYaEEZk3bdvH/Ly8vD555/noP0LCwvJCspoyE6nk05mTHjodrvR19eHSCSCuro6zM/PI5VKUVPE4/HQ2NgIrVYLAOjs7EQoFEJNTQ1ZlIuLi4nVIRaLMTo6Shclphs5ePAgtFotvvjiC1qdFRUVwefzoa2tDevr6wiHw7BarRAKhZiZmSFS827Lt81mg1QqRV1dHUVvbG5uYnZ2FgcPHiQmU3l5OQmzNzc3sbKygoKCAgrZ3C3mlEgkOHz4MCQSCba2thCLxeByuXDw4EHodDqsr6/j6tWr0Gq1SCQSCAaD0Gq1kMvlkMlkqKmpgVAoxGeffYa2tjYYDAb4fD64XC4sLS2hra2NSNXXr19HcXExPT6j0YhYLIY333wTIpEI+/fvJ20SsyGzm9Ta2hrOnDkDqVQKp9OJrq4uVFRUoLS0FHfv3oXf74fdbkcymUQ8HkcikSAopN/vR0FBAYWwut1uyOVyKBQKNDU1YW5uDjU1NfS6vHPnDlQqFUKhEA4cOIDBwUHo9XrCEBQUFCCTyWBmZgZWqxXb29uoqqqCWCxGNBqlINxEIgEOh4Nbt25Rs1BZWYnNzU1q7tnnsPfL9evXEQ6H8corr+S4Ixklmtm1NzY2wOPxaGXX1dUFlUqFjo4OOBwOzM/P4y//8i/B5/OxubmJRCJByIORkREYDAbCEdjtdiwtLaGmpgZbW1toaWkBj8fDyMgIFhcXyeHpcDgwNjZGq5+KigqYTCbE43EYjUZwOBzMzs6Cz+djenoaNpuNIm6y2Sxu3ryJ2tpaYuTE43GUlJSgs7MTCoWC0B3MYMEOBOxAcu7cOchkMvziF7+g50UqlVJ48lcVa25+VT040TEajTQheumll9DV1YXe3l4YDIac3x8rhlHQ6XSIRCIwmUx07eRwOBAKhSgpKUF7eztpMWOxGH784x9Dq9XC5XJRNI/BYMDa2hqkUikEAgEWFxdx7949+lmB+0iRtra2nK/FhM+hUAgCgQDhcBgKhQJLS0uQSCQQiUQEO52enoZEIsHCwgJqa2sB3N8AMETG74Ly/PtQj0nVv4fFQFWVlZVYXl7Gzs4OeDwenn32WSI6/9Vf/RUGBweRTCYxOzuLpqYmEqO63W6Ul5dDLpeT9ZtZPdkkpqmpCaurqxCLxaioqEBxcTEikQh6enogk8mwsLCAs2fPoqCgAKurq+DxeNje3iZg3qlTpyCTybC+vo6VlRWyWFosFhQVFdEKgDmuHsTh8/l8rK2t0SpmeHgY7e3tJFAEQCugSCQCj8eDsrIyBAIBJJNJOnUyG+zGxga8Xi8MBgMSiQRd5IxGI0ZHRyESiWA2m+FyuSCVSuHz+YjA7XA4EAwGyVnHXCZVVVUYGxsjzcLi4iKA+07A5uZmck3V1dUR5C8vLw/d3d00nl5bW4NYLIbH4wGPx0N5eTkmJyep0RAKhdjZ2aEV4MLCAgoLCzEwMICmpibcvHkTNpsNU1NTEIlEBIBjqzmxWIxLly6hsbER3d3d2LNnD3w+H6ampmA0GnHv3j28+uqr+Oyzz2AymTA5OYl0Oo2ysjKEw2G8++67SKVSCIfD6OnpQX19PQoKCjA/Pw+BQECJ58XFxfjiiy9w4sQJ3Lt3D3a7HSMjI9jZ2YFIJKILf35+PnGO1Go1RCIRRSeIRCKEQiFqjMxmMwYGBkiYbTKZIJFIUFxcjNnZWVqX7tu3D5999hmUSiU5jzweDxYWFgjgmclkcgSsFy9ehEajAZfLhcFggNvtRiwWw/LyMpqammhCuDsPL5PJ4MiRI7BarXTjY9Na5s7Zs2cPrTUZh6u3txdcLhczMzOIRCIYGxvD4cOHyYkXi8XQ39+P4uJiCvFla5ljx46hr68Pr7zyCoRCITQaDRYXF6FQKGhqt7S0BLVaDYPBgKamJmLefPnll0in06ivrweXyyUt0ObmJnG4mKN0bm4O3/nOd7CxsYG+vj54vV5aRavVamrIQ6EQvXePHj2K7u5uBAIBNDc3w+v1PsRAS6VSkEqlyMvLg1gshtPppPUVW4d/1VRnd2m1WmqImIlgbGyM8v8KCgqIlM4mo+wwBYDWmYz943Q6qRFjk8WtrS363bJr7NmzZ/Hee++hsrKSIlz6+vroteB0OrG4uAitVgu/3w+dTkect91fazf8lV231Go1FhcXyeTCOG0sNLqnpwctLS2PnBA9rv/z9XhC9DXqmzIhYqsTZgv3+XxEAfZ6vVAqleDxeBgbG0MikUBJSQm0Wi2FjPJ4PKytrZEuaHNzE3w+H93d3WhrayPmxd27d7G1tYXjx4/DarViZGQEN27cIOGyRqMhIejt27cRi8UgFApx4MAB5Ofn47PPPgNwH2lfXl5OCdKhUAgnTpxAPB7H7OwsAoFAzoQIuM8O6ezsBHB/hdfc3AyVSoWLFy/Sz6TT6WAwGCASidDZ2QmTyYTl5WUSvhoMBtrTs1Mqs/EqFAqaJDERc1NTE3g8HiwWCz7//HP4fD7KPWOxDFVVVYTwj8Vi2NraylkbZrNZPPPMM+ju7obH40E8HofBYMCJEyewtLSE0tJS0uL4fD6srKzgwIEDkEgkuHTpEpaXl3M0D6+99hol14fDYaysrKC1tRVisRhutxtXrlxBPB6H2WyG1WpFcXExNjc34XA4wOFwMDIygsnJSTzzzDNk/Z6dncXi4iKOHz+ObDYLrVaLt956C6WlpSgtLSU3FofDwY9//GOIRCK0tbWhoqKCstcmJyeRSqUwMjKCSCSCb33rW+Dz+djY2MCdO3fQ3NxMqzUGYdve3obVaoXFYsHs7CzKy8vh9XrR2NiIeDyOjY0NXLt2jZ6zvXv3YmxsDO3t7chkMhAKhbQWYhlvDH+gVCoRj8dpncB+3wqFAiaTCWazGXa7HR9++CHS6TSWlpbI4WUymUi7xNxUwWAQjY2NcLvdCIVCxOQxm804evQo0b47OjowNDREIautra0oKSmh6JXZ2Vncvn0bbrebtB7FxcU4deoUZmdnqfHv6OgAcB/fUF9fT5BI1twKhUL09/cjmUxCq9XCZDLh2rVrSCQSyGQyUKvVOHz4MKLRKN5///0ciN7evXuRTqdJq8Xn8zE2NgatVotLly6hvb0dPT09OHPmDKE5hEIhjhw5guXlZUxPT+egMVjj/corr0ChUKCzsxNtbW34wQ9+QNMeoVCIs2fPoru7G2azGaWlpfjoo48ox0+v16O+vh7Xr1+nr1tRUQGHw4HPP/8855rX3NyM0dFRJBIJWoM6nU7weDzweDx873vfg8vlQjgcRnd3NywWC2n4gPtrNyaEPnjwIMbHx+FyuSASiejA88wzz9CKk61GHwT9eb1ezM/P07SytrYWP/nJTxAMBmG323HkyJFfaVNnxPaxsTEYjUYEAgF0d3dTrmJzczM5VIH7h6vNzU06aD4YiMpW2KxJYv//q2jS/9zr8crst1zfhIaIFXuzdHV1wWg0IhgMkr6BEXxXV1cRi8WgUCiQn5+PiooKctO43W6ie7JIB/YSaG1tpRwuNp596qmnSPzY3d0NrVZLjCCtVkuJ7cB9WywLf7x9+zYAUGYWSzcXCoU4ceIEhoeHMTExQbEZu6uiogLT09Oora2li1A4HEY4HIbNZiMnz+3btym8tKCgACsrK1AqlVCpVHSTZnoalsW2sbGB0tJS+p7RaBRWqxWnT5/GlStXMD8/T1oqnU4Hr9dLzYBer8fc3BxSqRTxhAoLCzE7O4tnn30Ws7Oz8Hg8OTelmpoanDx5krQf2WwWQ0NDMJvNJN5eWloiazqrJ554AplMBkajkWBpYrEYCwsLFFTK3FZWq5Xyr9ra2ihYdm1tDXw+nxqp/Px8eL1e7OzsgMPhgMvlkmOPIRBYjh2Xy0UikcDU1BRpJJjYtaenh6ZffD4fZ8+epVVsKpWCy+XC8vIy2byFQiHFjuTn58PpdKK+vp6cVplMBvfu3cP29ja8Xi8EAgHRfGtra6HVamGz2fDll1+ipaWFGhgWjVFXV4dMJoPBwUFyWFksFhLZMjbQp59+irW1NWSzWUgkEhQUFCCZTCKRSJBbjxVLIGdVUlICiUSCgwcPEqJgt8i5ra0NDocDNpsNkUgEyWQSN27coAw1q9WK8+fPIxgMQi6Xw+l04uOPP6bPZw5QoVCI8+fPg8fjQSwWo6urCzs7O3RTfuWVV6DX64mnxRpk5oTb/TO0t7djamoKVqsVBw4cwOjoKGZmZnJenyUlJZibm4NCoaDrSXV1NW7dukUxMaxkMhkaGhpQWlqKa9euobi4GHfu3HmIFWQwGFBYWAiXy0VT0+vXr9NKj107mH3+9OnTBHLs7OykjMLCwkKsra0hkUhAKBRi//796OjooPgXg8EAu92Ojz76iJ73SCSC4uJijI2NkQ1fLBbTpDcSiUAmkyE/Px8OhwMOh4Ma/I6ODjQ2NqKlpQWZTIbWgmq1mrIRS0tLiU/lcrnQ0NCAs2fPPtKmnkqlMDQ0hK6uLopY2tzcxNraGjweD7nibDYbiouLoVKp0NfXh/7+fpw8eZI0dFqtFk6nE++++y6eeeYZWvUyHhzDXbBr9teNtPjnUo+jO37Pi7FTpqenwePxKPjT6XTSRCMSiSCRSFBEgUgkwvDwMILBIFncl5eXc0SGExMTlM/Ebky9vb0oKCjA1tYWjh49SnlUGo2GohIYtO3AgQOQy+Uwm82Ua6ZQKEhIq1AocP78eZjNZuzZswd79+6Fw+HI2Y3n5eUhlUrh0KFDkMvlaG9vB5/PRzAYJP0OG2E3NTWhs7MTdrsdOzs7KCoqgsPhwNLSErxeL5qamnD48GEcOXIEJ06cQDAYRENDA5qamnDu3Dl67sRiMVwuF5588kkS4f7BH/wB6urqwOfzIRaLIZFIMD09TVZrt9tNadx/+Id/CLPZjOrqaoooYRWJRAhbz8BsKpUKW1tbqK2thUKhIMfWs88+C4FAgD179tCFfGNjAyMjI7h8+TK6u7sxOzsLpVJJjjKFQoFsNovbt28jnU6jq6sLcrmcokcymQzm5uawvr6OTz/9FN3d3RS1wOfzsbCwQLBPpjnb2NjA2NgYPv74Y9IvbW5uYm5ujnhIGxsbcLvdMBqN6O7upuaDCeaVSiWuXbuGYDCIWCwGs9kMs9kMj8eD+vp6gi2ycFeWMScQCCCXyzE2NgY+n0+srMuXL8PpdOKf/umfkM1miaLOHFQMErhnzx6cP3+e4lL0ej2sVitkMhkOHDiAM2fOQKfTwWQyobCwEO3t7TAYDDh27BhRrBUKBdn0AZDQmmVCscw9thIpKSlBQUEBhcpevHgRW1tb2L9/P01+tFotxauwdeSrr74KoVBIgEjgfkPxwQcfkLCbuUhZjtwHH3wAlUqFc+fOwWg0UnYWgw2y4nK5GB4ehkajobw29prYXXNzcygqKoJYLMb8/Dw2Nzdx69Yt0mft5tswYnhHRwdqamrw5ZdfPuTkSqVSMBgMmJ2dpdc6l8vF2bNn0dTURNiN3TZ9xldjz7NEIiHdmc/ng1gspusMIzWzwNkrV64gPz8fKysrqKiowOnTpzE2NgYANN1iK9BkMkmuV6PRiIqKCkQiESwuLqKzsxNqtRo9PT0YHBxEMBjE3NwcVCoV/H4/KisrUVVVBYVCgYWFBYRCIXIbhsNh/OQnP8GNGzeQSCRomj8zM4OLFy/C7/ejo6MD8XgcZWVl0Gg0NFEbHx+n5IDh4WEMDAwgPz+fCO4saPi9995DcXExPvnkE2SzWdKtmc1mrK+v56zU/k9EWvxzqccToq9R36QJ0ebmJm7fvk1CUQZ2Y+4hmUwGgUAAgUCAjY0NWK1Wuiiur68jGo2ipKSEOCnMUgzcF/OylOZkMgmpVIpwOIympiY0NzeTU2JmZgZms5nEmywHjNlwI5EIPv74YxJQp9NplJaWErRQqVRifX0dLpcLKysr1NhptVqcPXsWANDf34+enh7odDrU19fjzp07dMJiLolgMIgnnngCk5OTEAqFKC4uzkmQLywsxJEjR8iSzJpBsViM6elpih3RarVobGwEj8dDKpUiGrPdbsfKygomJiaIGXTp0iUkk0kSz4bDYbz88sv0eNbX10kYbrPZKBaCJbKz5HqxWAyTyYS+vj4UFRWhsbEROzs7GBoaQk9PD06cOEEj8Fu3bhFgs6ioCEKhEG1tbQRdZOBELpeLkpIStLa2UhSKx+NBYWEhRkZGEAgEUF1dDalUioqKCvB4POTn52NjYwNLS0s0JVhbW8PKygpN2dgp1uv1wuv1UtAtC8b8kz/5Ewq7FQgEmJmZQVdXV45r6/nnn6e8OTYtYzc4iUQCsViM2dlZBINBeL1eyn2rqKhAYWEhrl+/jtnZWQD315cvvvjiI98fu9cJ7PmLRqP45JNPaHKRn5+Prq4uTE1N4fz584jH47TWHBkZwdjYGGWxvfvuu6irq0NbWxtRnsPhMN2QQqEQmpqawOfz4XQ6cfXqVchkMjidTnz3u9+FTqfDzZs3MTc3R06hJ554gpxg9fX1EAqFiEQi+Pzzz+H3+3HmzBkIBAJabYtEImxtbeH999/Ht771LUilUiwuLiKVSsFiseDmzZsknLbZbAgEAjCZTDAYDBgfH0d7eztEIhGy2WyObV4gEOCFF16Ay+XCwMAArRvZ2tThcKC8vBwTExPkThOLxXjjjTfw6aefPgSJZaVQKGgdzeVycejQIUxPTxMlf2pqCnfu3KGPZw6rByGtu/+bccHW19chlUrR3NyMpaUl0ift378fR44cwerqKn784x/nPB72vLB1mVKpxKuvvkqi+UgkgtnZWZoQMRAom/p4PB5MTk5CpVKhtbUVPB4PPT09WF1dxfPPP49PP/2UtIs1NTUk5p+dncWHH36IeDwOnU6HP/uzP0MkEgGPx8PFixcxNjaG1157DSUlJYhEIhAIBOjr68Pw8DBefvllygbTarWYm5vD22+/jXPnzqGuri7H/fnguuyr/p7VP7cJ0uOV2W+5vkkNUSgUohuOQqGgaAIul4u7d++iuroaVqsV4XCYkP9ms5nslhsbG7BYLLBYLLh161YOtv/EiRMYHBzMOUUaDAbw+Xw8++yzEAqF+Pzzz6HT6bC0tASTyUSMFD6fTxfhwcFBBAIBovMyaB3LY9NqtXjvvffINcT29wwKmc1m8eabb9JjYLbq3e464JcXzJdeeonyplZXV3Hnzh2iLLNsp42NDQwMDKCurg49PT3k7BAIBDh69CjlJ42OjhLyXywWky7L6XRifHwchYWF2NjYILbOmTNnsLq6isrKSgiFQiLYsnyz5uZmAv/t7OwgHA4jGo1CoVBgY2ODiNkNDQ3wer3kOtre3sYf/uEf0qicldVqRXt7O/Ly8igypb+/n0TKZWVlEAqF4HK5uH79Oqqrq9Hf34/l5WUKTHzllVeQyWSIx2M2m7G9vU2Tl3Q6jaGhIaIKq9VqRKNRJBIJTE5OQiqVwuv1wu1249SpU1hfX6fMLXahTSQS6OjogN/vp/gOsViMYDCIsbExRCIRnDp1Cj6fDxaLhVLvAaC3t5d0YeXl5TAYDPjFL35BQbwmkwnf+973vvZ75urVq0in09jY2KBctoGBAYRCIej1enz729+GTCaD1+ulDLFMJkPwTuZWYtNDFqnCRPbsdff+++8DuD9JYXEnzzzzDKamptDZ2YlwOIwjR47g1q1baG9vR1lZGS5fvkyEeADo6+tDbW0tVCoVufrGx8dpVczgigMDA4hGozh27BhNAldWVmC1Win8k4mOWWM+Ojqa4xI8e/YskskkjEYjfv7zn+e4vvLz89HW1gaXy4XKysqc9+OFCxcwOzsLq9WK69evI5vNIi8vj/Q9EokEQqGQGl8+n4/i4mL6XoODg/QYWlpa0NfX99DvjK27dheXy6XDhE6nw+bmJk24X3rpJbLYX716lSblAHD48GEUFhZicHAQPp8PTz75JAnBd+drsTR2Np3T6XSYm5sjEwWLtmhra6NmXiaTIRgM4qOPPkJ+fj4OHDgAgUBAzdbExATu3LmDl19+GWKx+JGk6q9qUMLh8K8kW7OVMYfDyRG2/7rP+3X//vtWjxui33J9kxoili0WDAYxMzOD2tpaSlo3GAwIhUJobGwk4BxjmjBBJVsVMA7Mbmv8v/gX/wKTk5O4e/cugPsCSaPRiDNnziAUCiEQCKC4uBgff/wxioqKiAPC1mwM7reysoLu7m7C8LvdbmIRFRUV4c6dO8jLy8Ps7CyMRiPRsg8dOkSag46ODoyMjJAF3ev1wu/3E+Avk8kgHA4jLy8PyWQSr7/+OqLRKO7cuYP19XUA9y/qrGlzOp1QKBSka+js7CThr0QiwXPPPUehsbOzsxCJRMhkMhgdHUVRURFZZ5kzjl0cQ6EQLly4ALPZjNXVVQKnDQ8P48iRI3SDBe5ffJlOxmq1wu/3w+v1wmg0ora2Fn/3d39HTet3v/td+pperxeffPIJLBYLTCYTysrKkJ+fDy6XS6JrdsI9deoU5cV9/PHHEIlERCxPJBI4fPgwgsEgxRHs2bOHHrdIJEIymXxIi8Cea5ZNxeVyodfrkU6n4XQ6UVZWhqmpKdy4cQMFBQV46qmnIBaLkU6nMTc3Bw6HA7fbjfz8fExNTVGOXXFxMV566SUKhQwGgxSi6/P5sLS0hLq6OggEAoyOjsLj8SASiUCv1+OJJ55AUVERwRx/VT7U9vY2fvGLX6CkpAQlJSXgcDh48803IZFIEI/H8ad/+qeQyWSQSCTUFLEmfWVlBYFAgGIjGA9oaGgIhw8fRjweh1QqxY9+9CMoFAr4fD7Y7XZwOBycPHmSnGZMX9XX14eCggKsra1RcC+fz4dSqSRQ3vLyMtra2rCxsUHp4+FwGOl0GtFolKa6bBL8ve99D5988gm5t9i/s0u7QCBAVVUVtre3c5yd5eXlUCgUWFtbowPMg3Xq1Cmk02mk02ncunULtbW1WF9fx8bGBiQSCeRyOfLz84lftLW1hSNHjuDDDz8k3hmXy4VIJEJTUxMikQhGRkao2WloaMDQ0BBljLE1T2lpKfbu3Yt33nmH3j+7k9z379+PixcvIh6P46mnnqKQ0uXlZWxubpJF3+FwkP5vdxAoc5SxlWA8HidwpdFoxIEDB2jqOz09DafTCa1Wi/3799NE7zedsPymjc+vm+Sw18SDgMZf93mPJ0RfXY8boq9R34SGiGXXsLXU7du3SRDJsolmZmZQXl5OOUuMUcMEpzs7O5BIJNDr9WhoaMDi4iKuXLkCALQ+KSgowO3bt8Hj8fDcc88Rf4NdBAQCASoqKjA3Nwen04nZ2VmcP38earUawH1x6MrKCi5duoRIJAKNRgOtVgsOhwOlUony8nK4XC4SZapUKnKVAcAf/dEfYWdnB/F4nMjMDFwWDodpmqFWq+F0OrG8vIxDhw7BaDQiHA7j3r17CAaDZFstKysjizFzhTA2y+7vazabcfLkSQLqbW9vk1PG6XSiqKgIk5OTD2UdMQ0LS5ZmNtxQKISFhQUkEgmk02k4HA7o9Xr4fD6kUim43W5aObHmIhgM4tKlSzh79iwaGxuJneP3+yk5mzVCjH8SDofh9Xrx0UcfoaioCE6nE2+88QZ++MMfgsvlwufzIS8vD5FIBNXV1cSM8nq90Ov1SCaT2LNnDxKJBImt2YmZ6TSY+HttbY2AeayB4PF48Hq9eP/99ynSoKGhASdPniRr9fj4OOm8fD5fThbVv/yX/5LoyiMjI2TBn52dhc1mw9DQELLZLMxmM9nkgftas+effx5SqfTXkn9v3ryJiooKjI6O4vjx4/B6vdjc3MTly5fx9NNPo7S0FIlEAjdv3oTb7cZTTz0FhUJBTRGjjnu9XnC5XCwsLKCqqgojIyM4ceIEOBwOVldX8dFHH6G0tJQmjpFIBPF4HO+99x6Rv91uNzXJr7/+OnG+2tvb0dnZiVgshmeeeQZ5eXlwuVy4fv06dnZ2IBAIwOfzHwKDHjp0iAwMACjo9cGqrKwkoT1wn8NjtVrhcrkeSXUGQJAhBe0AAM/nSURBVH9vMBjw1FNPIRgMIpVK4erVq+RyU6lU4PF4CIVCeOaZZ5BIJKipnJiYQDAYhFgsxvHjxxEOh2GxWHD16lVCZQAg8TYDrFZVVRHxe2BggDAgPT09aG1txcLCAg4fPky6M4VCgdXVVczOzmJubg5yuRxCoRDHjh0Dl8tFIBBANpslZ5lerwcAasCXlpawubmJra0taLVa6PV6FBQUoKGhgazxjDIO/NLx+1VNOIts6evrg9lspnW+UqkEh8PBysoKfvzjH0Mmk+GP//iPCSDJdFxXrlwh8fyvqn9ujc3/aj0WVf8e1uTkJMRiMcbGxjA/P49gMIg7d+6gsLCQcsysViuA+5qJQCAAo9EIu91OCd+VlZXQ6/UoLS2l2AatVkuhg3V1dXQB4vP5cLlc1ECxLCU22QiHwxgaGkIikcAPf/hD+nqRSITWTolEAk6nk+zudrsdfr8fDoeDTs8DAwM5P2dfXx+dmhkjKD8/HwqFAnq9HqOjo8RTqqqqwtGjR2EwGJBMJjE3N0eW+yNHjgC4b2fm8/nkbrl06RIuXrwIlUpFXCCBQEAXZZY3ZTAYYDQa0dHRgT179qCmpgZnzpyhz2Eao3g8jv3790Oj0cDlcmFxcRGffPIJlpeXyb2USCQoKV6pVGJzc5O0BD09PVhaWoJQKITT6cS5c+cwPz+Pjz/+GF9++SW2t7fpec1ms5ibm8M//MM/4MaNG5idnUU6nYZarcaZM2fgcrlw4cIF8Hg8vPbaa8hmszCZTORmsVgskEgk0Gq1sFgslPvmdrsB3L8BssZ3dXWVwmzZCsxqtUIul8NgMJDGQiqVwmq14ujRo+Qaa29vRyQSgVqtRjqdRm1tLeENjh07hsbGRnA4HLz88ssEzUun09RsxWIxnDlzhgS/JpMJXq+XAJF8Ph/JZBI6nY5uBuyxPKrKyspoIsQEtUajEf/qX/0raLVabG1t4Qc/+AHGxsagUChw7do10u+wZHCv14uFhQUMDw9DLpejr68P+/fvx8rKCt555x2k02l8//vfx+nTp8HhcDA6OopkMolf/OIXFGK6s7NDk5D6+nr89Kc/pTyxgYEBxGIxbGxs4IMPPsD6+jomJyeh1WqhVqsfGbhpNBofWjctLy/DZDKRg5DVbtclcP9myhxkmUzmIZaQRCKhbK9EIoFr164RJLCmpoaE8Gyal0wm8cknnwAAoS0YjPXo0aMwm81ECWerR3a9YrElpaWlkMvlcLlcKC0tRVdXF61Zp6am8Oyzz1IzVFlZSTwlqVQKh8OB/Px8GI1G8Pl81NTUPJSHdv36dSwvLyMQCCASiWBpaQnz8/Pw+/3Y2tqCXC6Hz+eDyWQirhFzyO5+bbH3IpvOMncvK6/Xi76+Puzs7KCzsxN+v5/WrwCIih0MBvHJJ58QGoBdn6qqqvDWW2898rW8u3Znqz2u3049nhB9jfqmTIju3LkDk8mEoaEhqFQqckE0NjYiEomAz+djZWWFAl8PHDiAe/fuYW5ujkSO7e3tMBqNWF5eJrvu7Ows6urq6ITi9XphMplQX1+P+vp6LCwsYGhoCCKRCFwul0Ifr169ivX1dRgMBlRXV6OqqopAaJ988gkCgQCd4JqamqhZSafTcLvd2NjYIEsxcB/pf+jQIXKD2Gw2bG9vIxKJ0EWF8VTKysqQSqWIP7O6uorS0lLa3QuFQhw8eBCxWAzT09NEjw4EAjTqLy8vx8zMDKRSKVGexWIxiaU//PBDOiW/9NJLGB0dxZEjRyAQCPDWW2+hoaEBKpUK+fn54PF4mJubw+TkJKamplBeXo76+nokk0masOXn51MoJdP+sHXciRMnYDKZyKrNMuJsNhueeOIJjI+Pw2w247333qOTNBO2q1QqDA0N4fnnn4fNZiOxOPsctpZzuVykeQkGgxgfH4fP50NdXR2USiU8Hg80Gg1NiNhNOZVKoaSkhKY84+PjdEOan5/H6Ogo2bqrq6sJS2A2m7G2tkYUbXbh3k14lkqlFOeyvr6OiYkJ4iGJRCKIRCISpU9NTVGzfObMGXIF2e12ir/g8XjUcOr1eojFYvzt3/4trWZPnjxJwFHg/kTzo48+wvz8PMRiMbRaLc6dOweTyQQAdAKfmZnBwsIC4vE4hRXPzc1Rg7W1tYVDhw5hfn4emUwGpaWlRHTv7OwkiKHP50NxcTHu3btHwb/xeBx79+7N0Yq1t7fDZDLh1q1bZEnf2dmhqQkT1gsEgpwcMVaNjY1YWlrC9vb2QxMgtt7KZrNIpVIoKCig/C+mPWxubkYymcTw8DBSqRT279+PdDpNzbFYLKYV2+3bt3MiOE6cOIGqqirMzMygu7sb29vbMBgM2N7eRklJCdxuN6xWKyYnJ3HgwAGIxWJiLR08eBDRaBSLi4sIhULw+XwoLS2FyWTC6uoq6urqYLFYiOqdSCTQ2dmJwcFBFBQUwGw2w2g0wmAwkG19ZWUFKysrhH+QyWSora0lPdvk5CQUCgXcbje+/e1v/1oI4u4JEYCH1l2ZTAYDAwMYHByEwWCAQCBAXV0dARgDgQDefPNNyGQyfPvb38bc3ByEQiE9Zzdu3MBrr732G5OpH0+MHl2PV2a/5fomNEQMMuh2u6FQKPD+++8jlUpBq9UiEomgtbUV6+vrFEpaVlYGs9lM6elOpxOFhYUEtyssLMTOzg4+/fRTlJaWYnJyEsXFxSQWraysRG1tLZRKJW7cuEH8DPa5bCrl8XhQUFCA6upqFBUVIRwOY3BwEC6XCzMzMyQabmxspKaDZfgMDw9jcXGRTrQNDQ3UBAkEAsTjcYp8WFxcpATzZDKJp556CqFQCBMTE9R4+f1+KBQKcmAVFRVhbm4uJ5bA6XRCKBSipaUFTqeTgmgZbZbRdQHg7bffRjKZBJ/PR0FBAYnU4/E4qqursby8jOPHj8NsNsNgMCAej+Nv/uZvoNFoEI1G8f3vf5+I2kyc63A4MD09TVELPT09FBrK4/Gws7ODjz/+mBxVIpEI3/nOdxAKhbC0tIT19XXSUVksFlqZVVZWYmlpiUbtCwsL6Ovrw/z8PEQiEQ4fPkyTtVQqRcnjzLlntVpht9uxtbVFFvCBgQGiYctkMhQVFcHlcqGkpIQmIBMTE8jPz4fH40FTUxP9e3NzM2ZnZ4mybLVaiRYulUrJecOgc06nE5OTkwgEAvD5fBAIBLBaraRTYr+XVCoFm82Gffv2oaOjAwaDAQMDA5TLZDabaSrAVjnxeBzXrl1DeXk52tvbkUgkoNFoaF35n/7Tf6K4jhdeeAE6nY6mMWxtJhQKMT09jYWFBeTl5WFkZAT5+fm0FmpsbIRGoyFxMZfLRWNjIwBgaGgIg4ODqKurQ0tLC5RKJba3t3H37l34fD6cOnUKoVAI165dw9bWFrhcLkpLS0lYvL6+Ts1jOBzGgQMHqOFjgcBTU1Pwer3g8/kUHvyrioUas0bNbDZjeHiYGsW8vDxUVFRgbW0NDoeDIml4PB7MZjMymQzy8vKgUqnw7rvvYmFhgb62XC7HhQsXcO3atRyOkV6vx87ODpqamjA8PAyLxUJOterqamxvb6OoqAhbW1vg8XjIZDJQKpUQiURYXFwkXVpTUxMZOHp7e9HT00POqtLSUnqfmc1mKBQKcDgciEQizM3NIRaLoaamBgKBAMD9wFSLxQK32/1Q0OnXaTC+an3GXrexWAwWiwULCwtEoK+urqaJXG9vL5LJJOn8ysvLH1rRfd3H889NLP116/HK7PesWI7Z5uYmzGYz+Hw+GhoayAasVqspiTwUCqGsrIzC/0wmE+rq6nDo0CGsr69jdHQUOzs7CIVC1DixcTQLQz106BDKy8uhVCqxsrKCkpISpNNplJeXQ6VSEaeGxXfI5XJy5rDRbywWoxDSgoIC5OfnQ6vVUtSHwWBAS0sLDhw4AA6HQ2Te5eVlzM7OEhOHjdDZ6Jw5n5iNuba2lqIU2ESBOaTW1tYQiUQosqKmpgYHDx4kbVRjYyM9vubmZhQVFdGJTi6X49lnn4VMJsP58+eh1WqpiWAk6PLycmpIWXTI0aNHEYlE8OSTTxLsz+12Y3FxEePj4/jwww+xuLhI07ySkhJqIP1+P9m8mVD1xRdfhEwmo1wqtiIwm80A7k8C2tvbMT4+jqNHj0Kj0SASiWBhYQFLS0tIJBIIhULo6+tDOBxGJBLB5OQkqqqqANy/WOzbtw9WqxVbW1t0k/F4PJiYmIDf76eL+s7ODtGmmXaH3cCefPJJRKNRNDc3o6qqCsvLy/B6vXjnnXcoRyuRSKCvrw8ffvghibSB+3qksrIy1NXVwWQyUR4ah8PBwsIC7HY72cwZFZrD4aClpQXr6+uoqKgAcP+Gq9VqwefzSfhdWVkJhUKBZ555BkePHgWfz4dGo8H4+Djeeust+Hw+fPe734VAIMDTTz+NUChETS/TWonFYsTjcej1ejgcDsjlctTW1mJzcxPRaBQOhwPLy8tQq9VYX19HUVERDh48SKLcoaEhKJVKDA4OYmpqCpubm1haWiK4JmsuWltbYbfbCZ3Apkd5eXl0421oaKCIE6VSCZvNRjd31sgLBIJHggJZsdfT4cOHsbm5SciJ3Y5Tj8eDlZUV1NTUIBgM0oSWNV2Mgt/f3//QqvLYsWOQy+UPcXDkcjleffVVtLe348yZM5idnUU2m0U2m8XY2Bhl27GbOXtt2Ww2lJSUQCAQEKCzv7+fVo81NTV0feJwOFAoFFhfX4fT6cTW1hay2SwSiQQqKyvR2NhIzxdwfyrtdrvhcDgeep6+Ds+Hw+GQg3L3xzFERkVFBYnKA4EAva9ZsYgVuVyO+vr6R67ovu7jkUqlOcLxx/Wb1+MJ0deo3/WEiEU3jIyMwGq1wu12k2snlUpheXkZYrGYcq0AYP/+/QiHw9je3kZeXh6i0SjefvttguOxdGu5XA6JRAKTyYQ9e/aQG8toNNI0YWtrCz6fD9vb2ygsLMS1a9eICWI0GlFUVET5TA6HA5OTk+jr64NarSZBJMtIi0ajUKvVMJlMkEql2NjYQDgcphgRtrYJBoM00RAIBLh9+zakUin8fj81SMzpxk5GTqcTMpmMbOepVAoCgYC0Mcz+y6ZkY2NjhARgjcv58+eRl5dHER19fX3g8XgUGzA6Ogrgvphap9NBpVKhoqICMzMz4HA4WF5eRmFhIbLZLNRqNUZHRxGLxeDxeLCzs4NYLEZE4srKSty7dw9Go5EmEcXFxZiYmMCxY8dgNBrh8XhIJL68vEwnbhYoy+JFamtrsbq6isbGRlrJ/c//+T9JhHz8+HFajaVSKUxOTqKyspJCf9mps7e3F2azGTdv3qRcLpPJRCyUwsJCWqs5nU6kUimCcLITbDabxfz8PHp7e6HT6eByufDcc89henoa4+PjMBgMZEl/UAidSCTo1FxZWYmOjg4MDw/j3Llz0Ov1j4xIYNMBkUhEbsCCggKkUilisbDHJpFIMDc3h4sXL0KpVJLuSi6XI5vNwu12IxqNYmhoCLW1tTh48CAF0g4MDGB+fh6HDx9GXV0dNjY24HK5cO/ePZpKdnR04IknnkA2m0VfXx9Ne+7cuYPi4mIsLi6SwJtlrfH5fLzwwgtEL1er1bh69SqcTif8fj8SiQSqqqqwuLiI8vJyghoKhUI88cQTJDZnzDCFQoG7d+/SlJEVj8dDW1sb0cPtdjuJ15k4emJiAqlUiqaFpaWlaGxsxOXLl7G2tgaFQgGRSAS/34/q6mrIZDIMDQ0hEolAqVTi7NmztMpkERrA/UmbRCJBSUkJAGBlZYUo8Oz1zJqVubk5mjqyxvT06dNIJBL46KOPyKhQVVWFlpYWKBQKaiwXFxcxPz8PrVZLQE6/3w+NRkOgzd0TnXg8jlu3bmF9fR1nzpyhgwfw8ESG/XcikcD7778Ph8OBAwcOUFgvWwl6PB784z/+IyQSCb71rW/RCptN2EZGRjA3N4dvfetbMJvNFBfC3G7vvfce2trayFBSWVmZ8/5i1wrW+Pw6l+U/93o8Ifo9K6ZhEIvFuH37NvE+GHQvPz8fKpWKduQKhQIdHR10I/V4PJDL5Xj++efh9/tx7NgxrKysQKfTURBnOp0mGB97UzFRYSqVgtFohNVqhUAgwJkzZ8Dn81FeXo7GxkbY7XZEIhEUFhbSSqGhoQF2u51orgwyJhAIwOFwiAoskUgQCoUwMjKCzs5O8Hg8+P1+CgeNxWKIRqNQKpWYm5vD4uIikskkfc7y8jJ4PB7m5+fB4XAQi8XA4/HgdrtJYxCNRpGXl0faKXbhLCgoII3M6OgoOBwOPvroI7qp3759G16vFy6XC7Ozs5ifn4dEIgGHw8Hm5iYKCgogk8lw584dsuCzzLSysjJsb2+TGFalUiEvLw9yuZwmDDMzM7QitFqt2L9/P8bGxrBnzx4S8gaDQSSTSVp9ASDhczqdJtjhzMwMWltbSWSZzWZx4cIF2Gw2PPnkk5SYDdxvOmpra+l3sVuYWV9fj7W1NZw8eZIo2q2trfD5fETFZTTxkpISirjwer149913sbW1BalUiqqqKtTW1iIQCOD1119Hfn4+rFYrysvLCfb4KCE0W9vEYjHSnwDA/Pw8iXAfLEYhfvPNNzEzMwM+n4/x8XF6zQG/FKAyYCdb/zGwISM9i0Qi0n7Mz8/j2rVr2NzcxM9+9jP09/dDr9ejv7+fvh6jnQPAvXv3UFhYiIsXL6KnpwfxeJwa7T//8z/H/Pw85Vix1ypwX/D8i1/8Anfu3KGgVaaxSyQS4HK5mJiYQEtLSw7hmYmdWZO3vLyMpaUl3Lt3j5rf3eR0thpnr+HZ2VnCZLC1zaFDh1BXV4dAIIDCwkJ4PB54PB5sb2/Te3VrawtCoRDDw8M0hRWLxXQIcTqdOc0QAFo7MtBpKpXKmUgdPHgQNpsN4+PjZJBghGnm/DQajbRWTqfTqKyshMfjQSgUgtfrxQcffIC+vj7U19fDbrfT2l2pVCIajVK0BxNERyIRdHZ2wuVyQSgU4vr1679yGsR+zvfeew8ajQZTU1O4e/cuEcC1Wi0WFhbws5/9jJqXixcvkmaPz+djfn4ec3NzkEgk+NnPfkZTSPaY3nnnHRQXF6Ojo4Neh5OTkzmv4d0ojAd/nsf1v1ePG6L/B4rD4SAvLw8DAwPQarUkkmRhkl6vF4uLi0in0+ByudjZ2cGxY8eQzWYp7Zql2//5n/85KioqcPz4cXC5XJw/fx5GoxENDQ0oLCyEXq+HVCqFTCYj6FdxcTFisRhh+PPz83Hq1CkcO3YMBQUFMBgMKC0txfb2Nlnc5XI5NBoN3YQikQjFTPB4PAQCAYyPj2N9fR19fX3w+/2IRqOUb8ZAcjs7O3jvvfewuLj4yL24Wq0mCvfKygrW1tYwNDREawmlUgm5XE5RJ+wmxQS/LE6A2aJZzhmbBrjdbiQSCYTDYWquWCTExYsXMTg4CJVKhfHxcWg0GlprMeuxx+OBTqcj58vrr7+OqqoqKJVKigYpLCxEdXU1JBIJJZM7HA6UlZWR3oARjVtbW8navr6+jrGxMYTDYezZswdCoZAcL2x6df78eYJWMvYT+3nC4TCRue/cuQO3242rV6/C7Xbj3r17GB0dxcbGBt5++20YjUbMzs5Cq9UiHA5Tanx/fz/Kysrw9ttvo7i4GF1dXdT4trW14YUXXqCTeUlJCcRiMaqqqiii4cETLQs4ZeJ91oAtLS2hv78fc3NzREvfDRK8ePEi6uvrKeqjtbUVgUCAHi+D2EmlUtIO7d+/H9vb29i3bx9EIhFsNhuamprw/PPPIxaLwW63o7S0FF9++SWt3iKRCC5cuIBIJIKtrS309/fDZrNhcnISBw8exMLCAk6dOoWDBw9CLBZDpVLh8OHD+MEPfpCT2ZdMJrF3717YbDZqjJxOJ2nddouUVSoVjhw5glAo9JAbzOFwELk+nU6jr68PUqkU165dg91upxW1SqXC4uIijhw5Qroao9FIkTdHjhyhpoA12Zubm9izZw98Ph9qamoI0cAmvSwNnulWGG1+N+sIuJ/nV1ZWhvX1dezs7NB1pKmpCSKRiEwNEokE9fX1tIKSy+Xg8Xj0XE5MTECj0UCn06G1tRXb29uk9+rq6kI6nUYikcCdO3ewtLREmkuWes/+t9uVuHfvXuTn5yORSOD48eMPucl2r6jYSurChQvY3t6G3W5HTU0NAFDOn8PhoKmQVCrFCy+8AIPBAJvNBplMhurqapSWliIajeJb3/oWYrEYael4PB5efPFFzM/Po729HXq9HuPj4w9pm9iBZ/dUiMfjgc/no7e3N6fRfFy/WT1emX2N+l2vzADgb/7mb+iNybgtDQ0NcLvdWFpaoo8rLCykXCYGReTxeGTHzcvLg0QiwcrKCuXm3L59G9/61rcgl8sJ/hYOh6n5YfC5paUlXL9+nS5wLNfLYrGQPZzpfID70x/GsYnH4+ByuVCr1VCr1RgbG4PZbMby8jJisVjOibKxsREmkwkKhQKff/45ioqKKKyRNTdisRjHjh1DKpXC9evXH6Lastq7dy+tgpjWIC8vDxaLBel0mqYvbrebtDB+vx/T09NkRwdA4bkikQg8Hg8KhQIqlQpra2vIz89HcXExnaDNZjOmpqaQn5+PxcXFHOeUWq1GNpuFXq+HQqEg4rfL5UIgEKCcMwaL1Gq1mJ+fRzgcxuTkJBKJBE2PGBE4lUohFAph//79qKysRDqdppTs3Y4ulubNTrM6nY5+dqa9YtM1ALQyY1qOtrY2+P1+5OfnQ6fTIS8vDysrK5iZmYFEIoHL5aKTPnD/Bs8Cedn7ZnJyklhDTIy+uzKZDNmwM5kMJicnMTQ0RDlxrLGpr6+H2+1GSUkJlpeX8cknn0AoFOLcuXMwm820UmCTNCZUZeaEyclJdHR04MUXXyRYYzqdxurqKom5+/r6EIvFoNVq0dXVhbNnz0IoFOKtt97CyZMnCZzZ3d2NvLw8TE9PQ6vVory8HHa7HTKZDNPT0/jss88eel0yanFBQQHGxsYIsHfs2DGsrq5iYmICKpUKOzs7aG5uhl6vh9/vx9zcHIFMmYbQZrNhcXERwH0rfiKRQHl5Oa2Xme7r8OHDmJ6expEjR3Dt2jXEYjFUVlbCZDIhkUjg6tWr9PguXLiAbDaLnp4emM1myOVyVFdXo7OzE9vb2zhw4ABEIhFu3bqFmZkZ+rympiZMTExQkyeVStHW1oYvv/wS2WwWCoUC5eXleOqpp/DFF1/Qa8fv9+PUqVMoLi7Gl19+iZGRERQWFlIuXygUQm1tLU1+Dx48CL1ej/X1ddhsNkSjUVy5cgU8Hg8OhwNSqRTT09OoqqoiRMBvWr9OxMym6uwgw4T2eXl5FJmRTqexuLiIyclJHDlyBOFwGPF4nJypu7+GwWAg0Kjb7abvKRKJYLfbKdibCep3OzeB+wLt4uJizM/Po7W19Tf+eX9f6/HK7PewdrtGtra2yNr5YJio0WiE2+1GOBwml8PQ0BDEYjHW19fJldTX1we9Xo/PPvsMOzs7+Pu//3sC7bHxdjqdpgytjo4O3Lp1C9XV1RgfH6fTqtVqxfb2NrmHWCipSqWCVqul6Yter0d9fT0ljDM2S3l5OYRCYc7PIBAIYDAYkEgkcOrUKczPz0Oj0SCdTiMQCMBgMBCzZWFhAYWFhTmfz1ZL9fX1SCQS6O/vR3NzMyYnJykwlK3IBgcHMT8/j9XVVaTTady4cQNOp5MEv6zYdCoej8PhcKCkpASbm5vYu3cvpFIpVldXMTMzg2w2i+3tbVqJLS0tQSAQYGtrCx6Ph1Z2m5ubEAgEUKvVWFhYgNfrhUqlwsrKCukEPv74Y1y9ehV3796l7Cin04lEIkHWdhZJYTKZcP36dQwNDSGZTMLtduP27duYn5/H9PQ0XC4XtFotJicnEQ6HoVarSfPF1nYsPoSFpWazWVRUVCAWi+G5555DMBiEyWSCw+GA1WrF0tISOBwOPB4PTCYTNRSpVAperxehUAh8Pj8nRqG4uBjA/UgYrVaLnZ0dbGxs0OubIR9YttXevXvx/e9/H3v37kUsFoPRaERLSwvcbjetvW7evImamhrIZDJYrVaalrCbGROqsmZodXUVXV1dsFgs+MUvfoEbN24gGo1ifn6e4hp6enoQi8WwurqK6elp0qK9/fbbKCgowLvvvosPPvgA+fn5KCwsRHd3N/x+PxYWFjAzM4PZ2Vm4XC589tlnD71HgV/GUHg8HjIrNDU1oa+vj9hNjES8tLREIacmkwk2m430gi+88AK8Xi8cDgeMRiN0Oh0OHjxIEMXy8nIEAgE8/fTTmJ6exosvvojZ2VnweDxaYUkkkpyJFAB0dHTg3r17EIlEWF1dpTy12dlZFBYWoq+vD5ubmzh+/Dj9TsvKytDY2EgxJACoIWWHiFAohOPHjwMAmpubUVxcjJ2dHezZs4cQIN3d3YhGo2SuYGHNux8Pl8tFMBiE1WpFJBIhppZWq4VKpUI8HkddXR0A0KqVCdPZuon9+VUzgV/H+WHTHYlEgv7+foLF7o4+crlc6O/vx/b2Nr744gtEo1HI5XICbK6uroLP59Paj63AmM6Oz+fT42fvKYFAQJO/3VVfX4/5+XnU19c/8vE+rl9fjydEX6O+CROi5eVl/OhHP8r5Oz6fD4FAkHPz5nK52LNnD600stksMpkMotEogdrEYjGWl5exsrJC+2mpVIrvf//7xJ9hQkSZTIaRkRGUlZVRoGFNTQ1aWlqIJp2Xl0fODSZsDQaDMJvNNOpWKpW0OspmsxgeHkY4HIbL5UI2m4Xf76eVAku4ZxMO5uDaXQxYx8JamTBRp9MhnU5Do9Egm81CpVKhp6cHAPD000+jv7+fNCoajSYn3JYVgznuHv2zG6pCoaAp1/79+zEyMgKlUonx8XGo1WoIBAIi6LIJWmdnJ02wlEollEoljhw5khNjIBAIsLi4iIaGBgSDQbK/x2Ix+h2x369UKqXIEiaE7+7uJufT4cOHsby8jHg8To1GbW0t2eBZcCgbxWezWSwuLlIESCAQIIv6zs4OFhcXyQbP5XLJwZRMJnH79m1YrVZcvnyZJkFWqxXNzc1YXV3F0NAQ9u7dS2uBBxlEbFrEIkMymQy2trZo/cFO0o+qcDgMoVCIgYEBjI+P49ixY4STYK97Fs+iVCrR29tLN+ve3l709/fDaDSira0NPp8P7e3t+PDDD5FOp3HixAncunUL8XgcbrcbdrsdqVQKBw8exAcffIBkMklwS4agYPlcAMi5Nj8/jy+++ILeG/F4nESxIpEITz75JDo6OqBSqeB2u8kl2NDQgKWlJYTDYcRiMRQWFkKtVsPr9SIajcJkMkGtVqO1tRXT09NYXFzExsYG1Go1PYfscZ04cQJarZbo8C0tLaSbOnbsGCEjfvrTn9KNWq/XE/MpPz+fVuFsbVxXV4d4PE4IiLNnzxLd+4MPPqDEeeCXrrMvv/wSr732GoxGI0KhENbW1kicPTY2BplMhr6+PqyuriKZTJIQ2+fz0TRFrVbT9OuVV16h18DVq1dJW2c0GlFSUvLQ9JGZL9h0lZkrmDOLPabi4mK6Tj1qQsRSA3Q6HZRKJU2Dbty4gYaGBphMJszPz9N7fnx8HKlUCkqlEoWFhYhEItQIplIp3L17Fw0NDaipqQGPx6NmbW1tDYuLizhw4ABNhjQaDRKJBGw2W877kJkkdjvoHtf9+r3hEP37f//v8dd//dc5f1deXk75W7FYDH/5l3+Jt956C/F4HKdPn8bf/d3fEVQNAEUZ3Lx5E3K5HK+//jr+43/8j+Dz+V/7cXwTGqKuri7cu3cvR4fwVVVXV0e5XJlMhtw27MSztrYGq9VKJ+d3332XyMAcDgd79+5FKBTC1NQU2Ue3trYoNJbD4WBsbAwWiwVGoxGZTIYiMbLZLKLRKFQqFVm2mWaFkZPZZGZpaYkmLz6fD4lEgiyzjKDNbqK79SJyuZyS1fl8PlKpFIVHBoNB7OzsQKvVwmw2o7+/H3w+H2KxGAaDAWq1GsPDw1AoFCTYBUBaJ4fDgbm5ObI6s1w0k8lEP0NVVRXMZjMSiQRNdaRSKTY3N1FSUkJ6pO3tbcRiMTrZBoNBNDU1UXI8Y0OxG19FRQVNDBhzJRqN4tKlS/Q42cezlZvRaERVVRWCwSD6+/tRX1+Pe/fuESRQLpcTpTs/Px9ra2tIpVKoqamh90A4HEYwGITb7YbBYKCbFJsuXrt2jWzxhYWFOXlQ8Xgcn332GUpKSmhlcPLkSQiFQnKsud1usvmzScTu9UA4HKabjkwmw9bWFoLBIIRCIcXQ7K5sNotQKITt7W1q2n0+HwH1mFXe7XaTS2tsbAytra2YmZnBiRMnsLOzg+XlZXC5XDidTpw5cwadnZ1YWVmhrDyHw4Hu7m5aY54+fZpWxVeuXEEoFMLevXuRSCRw/fr1nIPJv/k3/wZSqRR9fX1wOBzo6OhAf39/znUkFAqBw+HQe7OwsBDz8/Ow2Wxoa2uDQqHARx99BLVajaqqKtjtdoTDYUxMTFAzzLLqpqamsLGxQTbwlZUVLC0twWg0UgCpwWDAxsYG2tra0Nra+tAN/z/8h/+Q8z47ePAg7HY7kskkeDwe+vv7EQ6HYTAYoFKp6L+Zc7GgoIDW9R988AHm5+fR1NSEkydP0nuSMXKWl5exuroKpVKJTCZD05xgMIg333yTxNAAqEF+4403sLm5icHBQWrAstksVlZWoFKpcPPmTYhEItTV1cFsNj+U9L7bBcky+ZgOh7GwdDod/H4/ysrKvpLrMzIyQs9pfn4+DAZDDs8IuC8iD4VCpJfyeDxEtmfuQo1Gg46ODoKzHjp0iJo45mg1m81YWFigSTqHw3lIUzQyMkKRLex5fFy/rN+rlVl1dTXW1tbofyx4FAD+9b/+1/j000/x7rvv4ssvvyTbNKt0Oo2nnnqKkrfffPNN/PjHP8a/+3f/7nfxo/xvVSQSeWQz9KBLx2g0YnR0FBMTE3S63N7ehsvlQl9fH+7du0d8FA6Hg0AgAB6Phw8++IBsrr29vZibm0M6ncbOzg7W1tYo+Z0RkMViMTY2Nkg0DACpVIrG/G63GxKJhKYKmUwGUqkUPp8PXq+XnFc8Hg8FBQWwWq3Q6XRYX1+H1WqFz+eDx+MhzQxwX1xqs9mQSqVw/PhxPPXUUxQlIRaLkZeXR04hn88HiUSCJ554grRL+fn5kEqlqK2tfcixFIvFyPJrtVqRSqVQWVmJwsJCHD58GAaDATKZDA0NDdBoNBgeHkYwGEQ2m4VcLodWq8Xhw4cJHMnWN3l5eTh//jxkMhm++93vYv/+/QScYxTwU6dO4amnnoLX60VJSQlqamogl8uh1+shEolyGnymF9va2qLpzQcffACVSoULFy7gzp07OHr0KPr6+lBcXEy8mNraWojFYtjtdlrHsRUe4yrdvn0bCwsLsNls5GTp7OwkF1tnZye6urpoXckytth7bM+ePaTfYQ0Fc0yFw2GMj4+Dx+OBy+XSzYi57hiPhk35mJ6NZdjtPrdFIhH4fD7IZDJqOvV6PbhcLq0eY7EY5UdJpVIcP34ck5OT2Lt3L4RCIaRSKQoKCug1kkgkoNfryUFXVVWFTCaDhoYGCAQCnD9/HqWlpRCLxdja2sITTzyBU6dOwWQyoaCggL4ecD+Pj61a6urqMDU1hcLCQjzzzDPgcDiorq5GMBikXMBYLEb8JeB+Q8dCgFUqFfbu3YuioiLE43HweDxYLBaK0GFrbRZj09zcTJRmppE7efIkXnrpJWxubqKsrAz19fXE/vnP//k/47/9t/+GnZ0dfPvb36bn+Ny5c6itrcXQ0BDu3buHdDqNU6dOobKyEmfOnEFDQwPq6+thMBiQTqdhNBrB5XJx9epViMVinDlzhl537PU+MDBAEwwWvJxOp1FRUUHCd7lcjmeeeQYCgQCnTp0Cn89HJpMhw4DdbsfLL79MjraVlRWCcba1tdHv68FmCPjlCkwmk+U0Q+xPtv5nTKKv4vpUVlYiHA7DZDLRtW83z8jhcEAoFEKtVsNgMKCyshLHjh1DXl4epFIplEolSktLIRAI0NDQgDt37sBms0Gr1dL3sFgssNlshANgCBQOh4Mf/ehHOZEklZWVWF5eRmVl5UM/8+P6zeobPyH66KOPcqymrJiW5Oc//zkuXLgAAJiamkJlZSU6OzuxZ88eXLp0CU8//TTcbjfdVP7hH/4B//bf/ltsbm4+pF35qvomTIhu376NGzduPPT3LAGaFZuYAPft2U1NTdQQMWaPVCpFU1MT8vPz8emnn+aIh6VSKZqbmyGXy+F2uynKQiKRYHR0FBaLBdPT09jZ2SEq7qFDh8jCz7QfyWSSXDmskUomk+QqWVlZweLiItGWWSYTuxlvbGzk3ARZU6VSqchR9dlnn8FqtVJIJWPauFwu1NbWwu/3o6CggEbT7Ouw0FuNRoPOzk4YjUZyhbEqKiqCRqMhMavP5yPdjcfjIfcSo/Xy+XxaZc3OzhLkkjVzjOsEgNxdk5OTsFgsiEQisFgsxHuSyWTIz8/Hzs4Orl27RpgAtnJhZTKZaIIlEAjwrW99CzKZDJ988gkOHDhAWiG73Q6fz0csobt372JlZYUiGrxeL5aWlojztHfvXrq5x+NxfPDBBxAIBLBYLEilUpDJZBQXw1ZH0WgUvb29KC8vRzQaJZDj8PAwbDYbZmZmcOzYMSwvL5Mzkd20WCPEWC07OzuoqqqiAFHmTGQneWYzDgQCxETa2dnBzMwMXC4XWlpaqBFnxU77zPHIHEeRSAThcBh+vx9WqxVjY2MYHBwkxyWDTQYCAczNzSGVSoHH40Gv16OwsBBisRj/9b/+V9jtdnKasT8rKipoIsAchxqNBu+88w6MRiPm5+dRWVmJtrY2mM1m/OM//iPR2VlTXlRUBK/XS7qbnZ0dsonLZDLw+XwYjUY4nU5YLBaaVr7zzjuoqKjA/Pw8zp8/D51OlzPtCIfD+P/+v/+P3hfl5eWw2WyYmppCfX09RCIR0a9FIhEkEgnOnj37SN4Nc3atrq7i2LFjyGQy+Oijj1BXV4fh4WH88R//Mebn579yisEOUQxF8E//9E9obGzE8PAwampq0NHRQQDUP/mTP0E8HiezgdlsxsDAAK2y2cHtQTfeg/VNoTr/4Ac/QH19PYaGhvDaa6/RY3nU49vc3MSnn36KyspKzMzM4PXXX/+dPe7/l+r3akLEbi4OhwOvvfYa6TpY9s2JEyfoYysqKmC32ynFvLOzE7W1tTkn7NOnT1OO01dVPB5HMBjM+d/vupjt+sFqb2/P+W9GMAbur5bGx8extrZGYmtGcL5+/TpZmHeXzWZDf38/pT8zEODExAQJG+PxOAKBADY2NqBQKChhngmCd4cnZrNZLC0tUTYRWyUlEgkIhULE43GMj48jnU4jlUrB5/OhoaEhpxliKwzgfiPc0dGBTz/9lHKKJicn4XK5aHJhMBhw7949BAIB3L59G7dv3yatkM/nA3D/hDo/P09NzIOnK8YuymazlEPGcrnKy8sRDoeh1+tJk8OCbD/88EN606XTaQwODsLr9eLOnTvkvBEIBBgbG4PL5cIHH3yARCIBl8uFtbU1mspFo1HweDy0tLQgk8lQM8QE5EyXs/s54nK5+Kd/+iccPnwYQqGQ4jii0SiSySRGRkawvb2NtbU1yOVybGxsED23qqoK4XAYbW1tRNOVy+XQ6XT4i7/4C3znO98hIfDBgwexvr6OUCgEk8mEaDSKgYEBYqYUFhZCp9MRKXprawtPP/00AoEAbDYbNBpNjvCUndz9fj+2t7fB5/MxOTkJqVRK+jKxWExThEwmAz6fD7vdTpT0paUlOJ1OqNVqjIyMPDQhYKd9RmmWy+XkupPL5SgpKYHP58Po6Cj0ej0WFxeRzWYpJmN+fh5qtRqrq6tIJBLw+/3Y2dnByMgIqqqqMDU1hYaGBkxMTBBraWFhgZoHJo6Vy+V4+eWXsb6+jv379+Ppp5+m5u3ChQvg8/mUI9jS0oLNzU3U19eTBZy9xxmN+/3338eXX34JjUaDcDhMIuOnn34a8/PzOHr0KKxW60PTDqlUipdeegl8Ph96vR42mw3z8/MwGAzo7u7GlStXUFdXR40rw3iEQqGHBNgsWf65555DOp2G1WrFyy+/jKGhIZw9exbA/Wvz7ikGA3AySzxb3+t0Opw/fx5DQ0N45ZVXsG/fPhKGnzp1Cjwej3Q/DocDIyMj9N4IBAKkj/t1xWJQXC5XzprwwWJC/N3C/99mXbhwAUNDQ3jqqadyJlGPmk7pdDocOXKEkgUe12+/vtETokuXLlGQ4traGv76r/8aq6urGBsbw6effoo/+qM/+v+x997BcZ/nnfhne++7wO4Ci947SIAEC9jEThVKsmxZsixHE8fnOyd3N3M3mdzN3F1u5ue0mcxdkkucXJJz3K1eKYkUSbECIEASvdfFLrb3xfby+4PzPl6AIE3Zsi0rfGY0Nhdbvrv73e/7vJ/nU+5a0Hfs2IGDBw/iz/7sz/B7v/d7WFlZwYcffkh/j8VikMlkOHPmDE6cOLHl627FXQLwG0WILl68iEuXLt11u0gkgk6ng9vthkajITdUgUBAxmbAHZUaIxKyEgqFUCqV8Hq90Gq1MBqNmJ2dJQiWy+WSZ4ler8fq6ioAkCEYyxk6ePAg5ubmMD8/j+rqapJdy+VyBAIBhEIhLC4uklyXHWc4HEY2m4XJZCIfEY1GA7fbjba2Nhr7sKBTVhqNZgP/B7hzsfD5fOjt7cWVK1c2/I2hZo2NjbBYLODz+ZidnaUcJQB4/vnn8dOf/pQujk1NTdi7dy8EAgHC4TDGxsaoSWptbSWVHgCCr61WKwwGA/x+P3p7e3Hr1i1q2vbv349wOIyDBw9idHQULpcLN2/eJK+YJ554Ak6nE9euXaPv7Wtf+xq4XC5eeeUVyuWqrKxERUUFhEIhbt++TX5Nx48fx/nz59He3o5bt27hueeeIzM9ANT8MEfj2dlZ7Nq1C5lMhvKfWO4cM85k0vVCNCCXy2Fubo7CVxlhP51OY2VlBQ0NDaQ62qqYqzRzkC4sxoVxOp0U+rlVEGwikSAOEiu/348f//jHEIlEePbZZ++JEIRCIbzxxhtkKsnIu5FIBCsrKzCZTHjrrbc2xG8AdzZJAwMD1AAajUYiba+urtKYqry8HGNjY9Q4M8NHFqTqdDohFApx8eJFVFZWYvv27ZBKpUTSFQgENP5jnxUbOTHvJavVCqlUihs3bqC8vBx2ux0HDx5Ec3MzFAoFeWatrq6isbGRfI1YYDIj4y4sLOD73/8+2trasHv3bkxMTGB1dRVra2vYuXMnpqam8K1vfYs+O7fbTeTxoqIiiMVi4m5ls9kNxF6GcDBkcjMSs5VEfPO5kc/n4fF4cPHiRfT09NB1xW63Y2JiguJN5ubmEI/HyXNtcXFxyw3kZs7UVjlmm+/DmnCRSETE/0J7hs2E/4cBq5+9+tyQqjcXG4H85V/+JSQSya+sIWKwNatwOAyLxfIbbYgSiQT+9E//9K7bFQoFKQ+cTiepMZisll3wM5nMhmaI+V4UKqlKS0vh9/sRi8WgVquh1+uRTCZx8uRJrK+vk1/K2NgYLZjsb6Ojo9SU7N27l/hBOp2O8ruAO+qwL37xi4QkjY6Owu12Q6vVwuVykakYh8PBiRMniKfCTAIL33cikcCzzz6LyclJ3L59Gzt27IDT6SQzNuCOWiYQCKChoQEqlQqBQABTU1OUfl5YSqUS4XAYSqUS27Zt27CoLi4uEuG7oqIC5eXlWFxcJEUWk9vbbDZqfux2O1wuF2QyGeRyOfbs2QOxWEyZXgxVO3nyJKlNmDqIRSR0dnZCJpPhzTffpMausrKSlF8KhQJf+tKXyJzttddew6lTp2gEEQqFiKMTjUYpyJPP51NTwzhWbAydTqcpuiKbzW5YyNbX1xGJROh7sVgs1CRJJJINC+4nrfX1dbjdbiiVSuKLsEVSq9UiFAphZGQEu3btIrSELUBvv/02LBYLrl69itLSUjz11FOkPCxcnP7lX/6FuGrd3d1oa2sjdV1xcTHcbjdqamroMWyhv3r1KknMWbPCyL7sM6moqCDTR5Ycz0Z+0WgUr732Gk6ePIlz587BYDDA6/WSKzkb9dbU1EAoFMJgMGwY8y0uLiKXy2F6ehrJZBLpdBoCgQDLy8tob29HbW0tffaM98fMR4E7SqR8Po+ioiJCGf/4j/8YarUagUAAvb296O7uBpfLhd1ux9mzZykomBWThrOw32AwSA7oDodjw0iMNQ1ms5l8zDaP2UZGRjY0Lh6PBwqFAlarFTU1NYjH43jvvfcIgfvCF74Aj8dD3khutxu9vb2QyWTweDzgcrnkFh6Px+9CzzePoZgpaWEI7Ob7MKk+8/5hJHy9Xg+bzUaZe/d6jYf1m6/P1cissNRqNerq6kgFxKDrwmLOtsCdxGbGsSj8O/vbvUokEpE8mv33my6Whry5jEYjmR+qVCqScDOFEo/HQzqdvstXJx6P34WyBAIBMn8MBoMQCoX44he/iFwuRzJSZhjG0IPJyUlyxF5dXSVC9MrKCmKxGN588014PB56DR6PB4fDAZlMhoGBAdjtdspq2+ywarPZMDs7i+npacpqYxWJRHDgwAGcOXMGN2/eRFVVFXFXamtrUVZWhqeeego8Hg8ikYiy15gXkdvtvkuiykajIpEIk5OTiEQiSCQSyGQyMJvN8Pv9qK6uhkgkwvj4OMbHx+F0OvH222+T/DiRSODSpUu4cOECLSB79+7FI488ApFIhEgkgqWlJWzbtg21tbWorKwkMjpTuEgkEuTzeTz77LPo6uqC2+1Gd3c3MpkMxGIxNZfpdJpI54wf9vWvf514VdFolByfZTIZjEYjNW3M8be4uBgSiQSvvfYalpaWcO3aNVy6dAlVVVUYGRlBKBRCf38/jfPYBZ81QxzOnbR6hn4wld/mfRYzzvN4PDR+YH5MbBQhlUqh0+ng9Xo3cLCkUim5QptMJgwODhLRmo2SDh06hOvXr9P45OLFi1uGYZ4+fRperxf19fVobGxELpejTdL3v/99CAQCiEQieozdbseFCxfgdDo3nMssSsJkMqG6upoiapjf1OTkJHlO8Xg8nDlzBt3d3fjggw/w+OOPIxKJ0DGw3DCj0Qin04nR0VF8+9vfxrVr1xCNRiGVSlFWVkZjODZ+Li4uxlNPPYX6+nqYTCb4fD6srKzQ34LBIFQqFSGqYrGYyOvr6+t4/vnnEQgE0NbWRgGvMpkM9fX1+P3f//0NzRD7fljjD9zx92Fu4OXl5VheXqaRmN/vp/NtKy8f5rpeiOLodDpYrVaUlZXB5/NBKpXiwIEDmJqawvHjx+k+bW1tsFqtpERlt7PzhilgC4t5DhVmgDFLARaHwd5j4aiKjY6LiooQj8fxxhtvEJmbSehZsduZserD+u2r3yqEKBqNoqysDP/jf/wPvPjiizAYDPjxj3+Mp59+GgAwMzODhoaGu0jVjEMDAP/wD/+A//yf/zPcbveGBfZ+9VkgVf/0pz/d4EfDil2Y19fXKTPnF5l1sxy0wnn6oUOHUF9fj5GREUJeGA+GPYZFHrALDZPoxuNx+Hw+KBQKhEIh5PN5CAQCcnINBoPgcrmYnp6+Sz3H5XJRXV1N2WDMYFIoFBIc39HRAb/fD6vVSiRulsxdW1uLpaUluFwuJBIJQku2bduGcDiMubk56PV6VFdXY2BggF6XKd/i8Th6e3uRTqcJXREKhZBIJPB6vTQCjEajiMVikMvllAXFiNlmsxlOpxP79u3D2toajhw5AolEAo/Hg3w+j4GBAdhsNuIiNTY2YnJyEiUlJVhaWoJCocAXv/hFIq7Pzc3B7XZTEjurb3zjG5DL5XA4HIRQFBIzmQpJp9Phn//5nxEMBlFTU4PGxkZy6/7e975H5zVLHGfmgMvLy5QvZzAYyHelpKSEyOUrKyuYnZ2FWq3G0tISampqUFdXR9y9WCyGq1evoqamBiMjIzh06BAJAdjviqE5EokEUqmURncAqLkPh8OwWq3krswWLoYCra+vk2P1yZMnt0SINhcbyf3t3/4turq6cPPmTTz33HPkw+PxePB3f/d3dP/u7m7s3bsXSqWSnpOR5BcXF8kQkvkBSaVS1NXVweFw4L333sOpU6dgNBrpuBiZPB6Pw+/3w+/346OPPiLS/smTJ1FdXQ2Xy4V33nkHyWSSSPiVlZXUDLGGlf29pKRkg1mmQqGA2+2mcGP2+ixWhdVWI03WeDJ7BrFYTFYbzFmcBdOyXD7mps7GZff7DhjaJpVKyS5hq3EUK7aB0mg0CIVCNO6KRqMYGxuDWq0Gh8Mh1V7h98xQUFafZMT15ptvoqysDEtLS+jq6qKYI/Y49hvw+XzkVP6wfvP1uRmZ/af/9J/w2GOPoby8HGtra/jv//2/Y3h4GJOTkzAYDPjmN7+JM2fO4Lvf/S6USiV+//d/H8Adl1XgzoW0o6MDZrMZf/7nfw6n04kXXngBv/u7v4tvf/vbD3wcn4WGaHx8HK+++uqWf1Or1YhEIshms1CpVIhEIg/cFDHFSWlpKdbW1uhxzAk3GAyiqqoKS0tLG8jlhYontVpNrtVGoxEjIyMb4kQ0Gg0RR5nHEPNUcTqd8Hg8xMNhadNyuZwCW1kzVaimE4vF5AXCmjDgjlqGGRAGg0EaFej1euh0OuIXiMXiDRYO1dXV5E/CIgLC4TAUCgWUSiUWFhaQSqWwa9cuuFwu8u3h8/nkKRIOhzcglm1tbXC5XDhw4AAMBgPefPNNKBQKCIVCpFKpDaGNzK2YNWhMNm0wGFBZWUn+NDU1NfjRj35En/9Xv/pVIpmOjo6ip6cHLpcLHA6HYlckEgk++ugjMojjcDh48sknkU6n8fbbb0Oj0cDlcqG0tBQajQYGg4Gax0wmg9HRUaRSKajVaphMpg0u5iaTCcFgELFYDAMDA6iqqqIFu7q6GjKZDGKxGMFgEENDQ9i2bRtyuRxkMhk1mExZVlNTQ3lj7DtmjVHhZYo1TJs5KiKR6OcuppuLNQD5fB4//elPcfr0aSKVs0X05s2buHr1KvR6PVpbW8Hn81FbW0sjFb/fj6GhIZSWlsLj8cBsNiOdThP5m3lWsffDPHBisRjeeOMNPP7446SqNBgMsNls6OvrQ01NDWKxGI4dOwav14u3334bmUwGRqMRFosFHR0dMBqN1HDkcjnKEVSpVBtGvoWjnFgsRuNX1hwUNj2F41P2WOCO0lUul8NkMqGyshI8Hg8ejwdKpRIul4tGcm63G3q9nmJiNBoNkskkjakWFxdRVVVFPliM38bcrLfiHBUWQ6m2Gp9uNldkt0ejUXKWZvykT8L1yefz8Hq9uHLlCpqbm1FVVYVUKrXhGLcawT2s33x9bhqiZ599lhLHDQYD9u7di//v//v/iLTJjBl//OMfbzBmLByHrays4Jvf/CY+/vhjyGQyvPjii/jTP/3T3zpjxpGREQwODpLqq7CYsoJdED4pQmQwGBCPxxGNRrf8GzPgu58aQ6lUorq6GlKpFCMjI1s+F3Cn8WDeI3a7ndKqC5EnuVxOHJV0Ok076U9azDuIjUSWlpZgMBjIcmHziK68vJx8a9jn6PF4iBjLDCe3b9+ORCIBj8cDuVyO0dFRNDQ0IB6PY3Z2lng5nZ2dMJvNkEql5AzMVGQmkwnpdBp2ux1NTU2wWCwYGhqiiJV8Pg+xWIydO3fiypUrqKurQyAQwPbt28HlcnHp0iXs378fZrMZIpEIFy9eRGdnJyYmJhAKhWAymaDX6xEKhdDe3o6BgQHcunULcrkc+/btg1AoxPXr12GxWHDr1i10dnYim81ieHgYPT09qKiogMlkgtPphMPhwNTUFIRCITo6OlBfX0/ZcsxV+fr167hx4wZqa2uhUqlQV1dHaBIbewGg75Etzj6fD6FQCOl0Gul0Gg0NDQgGgzCbzQgGg9DpdIjH4zh37hwhgEqlEhKJhAzrWL7amTNnMD8/D4vFgmPHjpGajYUVb16g7kXwZrdLJBJ8+OGHcDqdiEajaGlpoXH9Y489RqhUf38/KioqcO3aNdTW1kKhUFATy0Jxi4uL8eabb5LnUTKZxOXLl9HZ2Ynh4WH09vYik8mQX9b3v/99xGIx7Nq1C8vLy8TDYTE2v/M7vwOlUkmLfSQSoYiPubk5PPPMM4TUABuRkHw+f9f7Zg2GQqEgs8FChOjcuXOQy+UIh8OoqqpCbW0tjSRZU8rsC5gVgt/vh0ajgd/vRzKZRF9fH7xeL06fPo1oNIrq6mrY7XZCIFlDyLyCWCO8VeOyFSH6XrUVr+eTcn0K71+ISj5sej779blpiD4r9VloiILBIMbGxnD+/Pm7/tba2op0Ok22/vdqRrYqrVaL6upqOByOu5ot9l7NZjO5gxdWIWrD4/FgMBjQ0tKClZUVzM3Nbfl6zLBQpVKBw+EgEolgfn5+A/rD4XCgVCoRCoWgVqsRDAapSXqQYghGWVkZIWZut5tGc4xTUdgUMfibz+ejvr6epNZKpRIymQyzs7Nk3ldVVUW74TfffBNmsxk+nw8HDhzA8vIylpeX0dLSQk1UNpuFXC6HzWaDzWbDzp07sbq6ioaGBlRUVNDtrCngcrnQ6XTo6urC2bNnadGsra2F1+tFXV0dZmdnKaJgdXUVOp0Ot2/fxvr6Orl+l5SUYMeOHbh+/ToRqGOxGHp7ezE5OQmZTIb5+XkcPHgQuVwOr776KnQ6HTweD1544QVqUi9evEgNd3t7OzUho6OjRM5/77336Li+8Y1vwO/3k9dPYdBqIerDUI2VlRUKCfb5fKR85PP5uHLlClKpFFpaWnDjxg3U1NRAIpEgnU5TUGZvby/m5+cxPDxMnKAdO3agsbERSqUSdrsd7733HsrKyvD4448TCuX1erccozBk6Ny5c1hdXYXT6SQnc51OB6FQiPLychw9epTQiuvXr6O2thZ+vx/FxcUoKiqCQCDAyMgIqqur8U//9E/o6elBX18fHnnkEQiFQjgcDoyNjeHxxx+H3+9HPp8n3hxzx5ZKpXjhhRcgFovx93//90gkEigrK4NQKMRXvvIVUkExS4W5uTmS3r/44osPvHhfvXqVfIAsFstdTUY8HsfHH38MnU6Huro6QkZv374NjUaD0tJSEmGw12QjS5vNhqmpKaytrdE1QygUoqKiAj09PXQ7q82eXVs1LlvFVbANIfOUYijhVk0Vu43luDEn90JksfBxwJ11YHl5GU1NTQ8jMn6L6nNLqv7XWvl8nhbXzbVz5060tLSgtLQUFouF4jEMBgP279+PxsbGe44PVCoVxGIxotHoXc7NSqUSe/fuRV1dHcrLy7fchQkEArowSKVS7Ny5ExwOB0ajER0dHQRlF5ZIJEJFRQXxAGZnZ6HT6aBWqze8X+axs76+jtra2gdqhpinTCqVQjabJbL5wsLCBp6SUCiE2WzeoGhjEQXpdBrvv/8+rl+/jlgsRo1eaWkpFAoFWQVcvnwZr776KoWXMoWbSCRCdXU1Jicn0d/fj5GREczOzsLhcEAoFMJisWBychJisRjT09OwWq1ESHa5XIQg3bp1C4FAAPX19dBqtXj22WcRi8Xg8Xgot+m9997D7du3KQokHA7D5XIhEAhAIBBALBZjcXERJ0+ehFarRTqdxv79++HxeNDc3Ay73Y6amhoasfT09MDj8eDUqVPk/u33+9Hd3U1xBgydZeGazJDvyJEjmJ+fx65du0iJ5PF4yNzvn/7pn2Cz2TA+Pk7mnCyUs6qqCg0NDVAoFNDr9YhGo1Aqlbh27RqdB0z1FovFCIFjTdXExARaWlrQ0dFB7tDbt29HSUkJkskk3nrrLcRiMUxPT5OTciwWg1arhcPhQDqd3uAzo9PpEAqF0NvbS+hSMpmExWIhEviePXvo3NFqtThx4gQSiQSuXr1KCEcmk0FXVxfEYjFefPFF3Lx5E52dnTRS2b59O3p6elBeXo6dO3fC6XTCarUinU7D6/WisbER/+7f/TsolUqo1Wr87u/+Ljo6Okhp+O1vfxsXLlxAOp1GeXk59Ho9amtrEQqFcOTIEfqMRSIR5ubm8L//9/8mDtvmcNPu7m6ywGBOzYXF3nN5eTk4HA7sdjtu374NLpcLj8cDh8OBQCCwIYKFy+UiHo+jqKgIlZWVkEgkZNVQUVGBubk5XLp0CUVFReR0zfyU2HFt5cfD7A70ej35nTFfrKWlJYjFYspkY9/RZoSQ3RaPx+H1ejcErrIqJOVzOBysrKygoqJiSy7nw/p81EOE6AHqN40QseT6jz76iHaSrCQSCcmsb926BaVSieXlZbowsl1TodKrsIRCIYRC4ZYNBwuaVKlUd0nUN1dFRQVKS0tJMcMW+62KNUCLi4v3TJr+RWqrMdiDFpfLhcViwcrKyobbNRoN7SIL7fILq6qqipohpvbbHBrL5XJRVlZGTt4sosLn81EuWyFKxhYz5vxrMpkwNDRErtnZbJZ2zvX19ZiZmSFeiEwmg1qtRm1tLXg8HpF7a2tr4XK5IJFIMDExQZYEbHSSy+VQUlKChYUFtLS0IJvNktVCfX09zpw5A7FYjNOnT8PtduOVV14hN+fHH38cuVwOCoWCmoZoNAoOh4P+/n60tbVhbGwMjzzyCJkyNjY2krCBRa5YrVYkk0mYTCYEAgFcvXoVLS0tKCsrw+TkJPR6PbRaLTQaDW7fvo1gMIi2tjbKlXrjjTewsLCAr3/96yguLsbY2BgmJiYwNzcHuVyO/fv3o6mpiT7faDRKZo+5XA61tbXEMRkbG4NCocDKygpu3LhBqFssFsMXv/jFDfwbr9eLH/zgB9i2bRtGRkbwB3/wBzR6U6vVcDgcZHnAFnOHw4Ha2lry4mF5Z5lMBt3d3ejs7MT6+jq0Wi2SySRtjNbX1/H//t//g0QiQSAQwCOPPILOzk5qIFhwrN1uh0aj2eD+PDQ0hKNHj6K+vh7hcJi8p8LhMDweDyoqKogwzhCSVCqF1157DY2NjeR+zfyPgsEgRCIRqqqqUFxcvGH0mM1msbKygmAwiHA4THl2uVwOZ86cQWlpKWprawmVkkqlNKpk6FIgEIDZbEYikdigOEwmk0ilUohEIlAoFBgdHYVKpUJ1dTWuXbuGxx9/fMtRGHtPfD4ft2/fpnPb5/Nt4B2x4ywcLW5lFfCrqod+Rp9ePRyZfcr1m26IWFMzOzuLc+fObVAZ7dy5ExUVFXRbf38/wc2NjY1wu91YWFiA1+u95/Pfr5FgI6vNxbg4rMxmM8xmM5qamvDqq68ilUpRxEJhMW8i5oj9IKXT6RCJRH5us8Pn82nUls1mKQxxs1dVYbwJK5YsHwgEqDExmUwoLi6GzWa75+fHmsZkMgkulwuJREJZSswVmynx2KKm0+kQi8UQDoeJ48Ky5QQCAR0zSxSXSqUk+799+za0Wi1SqRSNVBiyVXhMOp2OOCxs7MdCONnf+/v7IRaLoVKpSCUUCoXQ0NAAt9tNC9XS0hL6+vpoUSoqKsKePXvgcrkwNDSEnTt3YseOHWT8qNFoaOfNPLEGBgbQ1dWFWCyGeDxOTtENDQ1kM8Cy69h/5eXl4PF4xJPxer0IBoOEMOTzeSSTSRojXrx4EcvLy5Rl1tHRAZfLRfYQWq0WX/3qV8Hj8eD3+1FSUkJcMGa2yPLThoeHUV1djXPnzpFZ6fj4OMRiMdrb23Hs2DFa/FlDOzIygqtXr+IrX/kK/T74fD5WV1dRW1tLtgRKpRJ8Ph8//OEPsXPnTuzZs4d+g8zMs7a2FouLiygpKaHsNYlEgkQiQQ3tO++8g7a2NjzyyCOIRqMQi8WQy+XgcDiwWq0kOmDRNK+99hp2796NiooKuFwutLS0IB6PY319HXa7HQKBAJlMBiUlJYS+iMVi/PM//zPKysqwsrKC5uZm1NXVUUwSa/jEYjG5TbNFnDW3NpsNarV6A5KVSCTItFStVpM1BPCzZsDr9aKoqIjyyliEC1NrMpuNDz/8EAqFAplMhkxdbTYbjh07dlczwUZw/f39SKfT5CLe09Nz11iu0AeKNYhsc+R0OjcQw39e3c+QdKt66Gf06dXDhuhTrt90Q8Rqbm4OL7/8MkH7XC4Xx48fh9lspvGEVCqFyWRCJBKBRCLB2toaFhYWwOfzqbHR6/WU58WQidraWrqAsdJqtVAqlRuMDtnrsrFFPB4nBVdTUxMuXLgAmUy2QbGm0WhI3cQWBKFQeN8mjRWHw4FGo0EkEtmw6G8uoVAIk8lEIbiFJG3gznhPKBSivr4e09PTRO5lDrQMGSiEw5laiJnmbUUqZ0hG4esxPxfmX8TOm9bWVoRCIQwPD5OXC4/HA5/Pp516SUkJFhcXEY/H0dLSQg2TUCik6BWRSISdO3fCZrOhrKwMw8PD9N0WFRWhs7MT586dIw8qkUhEsRHFxcWYm5sj0qzf74der0c4HIZarUZVVRWdRyKRiMJfWe6bWCymEYfT6UQ2m8XevXuxurqKHTt24MqVK9BoNCgqKoLD4SCCscfjwcDAACKRCLZt20aO2fF4HOXl5VAoFESydbvdpHQym82wWq00FmHnbCaTocBaloGmUCgokqe7u5uen41REokEmpqa0NDQAI/HA6vVit27d0OhUODixYvYvXs3xXaYTCa8/PLL5PzO4/HQ1taGubk5crhub2+HSqWCzWbDK6+8guPHj1OqOZ/Ph1gspt+Ky+WiBPYPPvgA4+PjOHjwIG7evIkXXngBOp0OqVQKH3/8MbhcLrRaLTlot7a2gsfj4aOPPsLS0hKef/55aLVaahyY4lGtVqO4uBiZTAaTk5OIx+OoqKhARUUFNZahUAhTU1OQy+WIRCK4du0ann76aWrqY7EYbt26BZPJRMn1iUQCL7/8Mjo6OmhsWFRUhEgkArlcTtEqzIjRaDRSgzI7O0vxPHq9HiUlJSgqKiLH55GREXLUFgqFyOfzhKZmMhncuHGDTGLZRi8UCuGtt95CRUUFdu3aBQ6Hg7GxMUJzz58/j/r6emSzWXR2dhKPjGWyJZNJ8Pl8zMzMIJvNkmKOjeeYASMbLTIX7EQigXg8Tj5eiUSCpP3su2DO7Uz5yZrDe8n+71W/iAruIaK0dT1siD7l+qw0RH/1V39FC6BIJEJpaSldkNbX12mXbzAYKPhzbm6ORizM2ZiZtGWzWaRSKRiNRpqjF/oYHT9+HP39/WhtbcXg4CCy2SwFVTIuAPOkYeaAdXV1OH/+PF1I7nd6Mcn/5nHRL1oSiYQcgreqzeGorLZCjFgxAva9jlEkEkEkEm2Zd8eI4AqFAhUVFVAoFFhdXaUIlML7Mf7M9PT0htdh4wMWWZHJZCAQCKBSqVBRUUEGkoXFolQKSfL19fV07qRSKRrfVVRUkIqTcTeYhQCT/dtsNoRCIeIdCQQCItmXlJQgnU7j6NGjuHz5MkwmEx1TV1cXEasnJiZgt9uh0+mIR8bMOPl8Pnbs2AGVSkULrtfrhUwmI4PPlZUVcl0GQAT4jo4OBINBkoCz5pydW6urq9Dr9bh+/TohW4FAALOzs7Ros+bdZDLh+eefh8fjwa1btyhwGAChsOXl5ZidncX27duRy+XQ0NCAH/7whygtLcXU1BSOHDlC6kPW6DEELJ/P48MPP4TBYMDi4iK8Xi+eeOIJVFZWIhQKYXx8nMZ3jLtWVlZGCrXR0VEoFAqIxWJ87WtfI5uI+fl5rK+vY2FhAYcOHSJelNVqRXNzMykRWVgzIx5fuXKFkKinn34aTqcTt2/fBnCHP1NfX4/du3cjGo1icHAQZrMZkUgE4XAY9fX1EAqFZHfAvksWz6PT6SjX7fr16zRCNZvNMJlMkMvl+MlPfgKxWIzy8nIKSuZwOIhGo6isrCTidjKZpN+HWCzGv/zLvxA62tnZiUOHDpGi7dy5cxAIBFhYWEB7ezupVJeXlxGNRtHT07MBjVpbW4NMJqPfHLPjyOfzOHXqFADQZoQZ0Pp8Pvh8Prjdbord4XK5yGazZDMQDocJgZTJZPeN+/g06iGidO96SKr+nNYzzzyDyclJWjwcDgdGRkbgcDjg9XpJcqzT6TA6OgqpVLphfMSUOWq1GvF4HJlMBhUVFXC73Th48CB27969QbL/wQcf0IVz7969lCWkUqmQz+ehUqlQWloKoVCItrY2CgY9fvw4Ojs7YTAYwOfz73JtFYlE5KxsNBp/oWaIHUth6fV6qFSqLe/PoHy2W2bF0Iatio2bKioqIJfLie/CYHIOh4OmpiaSWrP3yS7yrBmKRCLkrLvV2I+RwZkdQGGxXfGBAweIAG02m2GxWDA2Ngaz2bzB8+T5559HY2PjhtiC0tJSNDQ00GhDoVDA4XDg4MGDEAqFOHXqFAQCARwOB/R6Pfh8PiwWC6qqquhCW1tbS+eZzWaDXC4nhDCfz+P999/HsWPHsL6+TrymmzdvQqFQ0HMVFRWRr05TUxMUCgU110wp9cYbb2B9fZ0+y6qqKnC5XLjdbvLfqa+vh1wux5e+9CXs378fu3btIt+b4eFh3LhxA2VlZdi2bRsef/xxVFdXo7i4GGazGcXFxSgrK0N9fT0ymQyWl5cRj8eRTCbh8/kwODiI5eVlGkUJhULweDzYbDZotVrMzMygubmZlG9arRa1tbUYHx9Hc3Mz0uk0NBoNTCYTfTZsU8Dj8bB//36sra2htrYWv/M7vwOHw4HvfOc76O/vR2dnJwQCASQSCbq7u4l8Ho/HsbKyQoiRx+PB3/7t3xKSqFarsby8jEwmg7fffpucnmtqahAMBvH+++9jYWGBxj0stJepIo8ePUrE6ObmZiSTSej1ephMJohEIty4cYPON6fTCZ/Ph+npaUIlJRIJIXpGoxEOhwNzc3OwWCwYHR1Fe3s7eDweoaACgQCvv/46iSX8fj/lDAaDQRQVFSEcDlPEDGtWGcFeoVCQmKO4uBg//OEPceHCBSwuLqK8vBzj4+Ooq6tDKpVCVVUVNXEMAV5ZWUEgECAULRwOQyKRUOPs8XiQyWTwwQcfwOVy4fLly/jJT36CbDZLBqWsqc7lcnj99dchkUiIDsCQTYYQMa8qdhtwx7NoZmaGNqCMRM6KoVNbub6zyuVymJqawt/8zd8gFovdRT5/WJ+8HiJED1CfFYRodHQUAoEAAwMDNCZgfh8ymQwKhQIKhYJ2QhcuXIDZbMbS0hISiQQtyCwOIBaLoaioCE8//TQRIAtz3wqrubmZFkePx4OSkhLodDq0t7dTg8AWslgshtXVVQQCAeIYjI6OEk/DYDDQyM3r9WJxcXHDaxUVFUGhUNwV6vpJqzAEVqvVUpI6u2gVoknMfLLw59DQ0ACtVouSkhKcPXuWHlMYNZHJZKDX6+H1ejc0O9XV1RviSFheGOMVsWLqF6VSSf4tt2/fRiaTAZ/PB4/Hw+7du9He3o5gMIh/+Zd/uYtUf/LkSfqOu7u7EY/HcebMGSLKM5SJGZy6XC7i15SWlmLbtm14++23aRefzWbJP2lxcRECgYAI5YX5WIUlFouh0WjQ09ODlZUVjI+Po6Ojg9SL09PTCIfDNGY7cOAASkpKMDMzA7FYTEiQSqWCQCCgcdjq6ipu3LiB4uJikjw/9thjhHQwh+6hoSF8/PHHhGBWVVWhq6sLyWSSiOHLy8tobW3FI488gkAgQFJ9dg6wz0itVkOpVMJkMuG1115DNBolxOnIkSNIJBLYuXMnjXwuXLiAoqIihEIhvPjii3A6naS2zOVyCAaDxAFi/kkSiQQjIyMbzDmbmpoIxdFqtVAoFFCpVOju7sbc3BzeeOONDeeYQqHA4cOHsbi4iLm5ORqZGAwG5PN5mM1m2Gw27NmzBwMDA3SMLHuuo6OD1HFvvPEG1Go1XC4XdDod7HY7Ghoa0NbWBg6Hg7Nnz6KxsREff/wxcYFMJhMuX74MmUyG8vJyuN1u4hGm02mySRgZGSGeGwsubmxspPFmZ2cnSkpKyBDzypUrKCkpgUKhwPDwMNra2hCPx7Ft2zbYbDa8+uqrhGSyIOJUKoVQKIRMJkMigpdeegkcDgfXrl3D/Pw8stksFAoFxXDI5fINHCir1Yq2tjbMz88jGAxCJpOhuLgYIyMjqKmpwcrKCnp6esitPpvNIhQKUd6gxWIhPzD2O0kmk6iqqoJMJiNbFK1Wi+XlZUgkEnzwwQeor6/Hvn37NuQGBgIBXLt2DTabDUePHkVlZSUA0HecTqfx8ccfY2hoCC0tLVhaWkJxcTFOnDhBvMPNyrp/rWO1hyOzT7k+Kw0RI13y+XxMTU2Rbwgz0GNxASUlJZiamsKePXuICOpyuSiVu66uDteuXYNarYZOp0NFRQVqa2sxODi4gehssViwurpKcv6GhgbMz89DoVBQkOyJEycIDcnlckT6tNlsGBsbg9FoxMrKCnQ6HVZXV9Ha2opUKoVcLgen00mqETbe4fF46OzshNfrhUgkwszMDICtE+7vVyx89l7FglxZMZ7M5oZl//79WF1dRSKRQCQSofHU5vEbQ4J+XjFlWWEJhULw+Xy0tbUhGAySFD+VSkGlUqG8vBwlJSW4cuXKBjUga+7KysooXFUkEqGnpwfnzp3b8DoMtmdIWSQSIXNILpcLsViM+fl5el6Gmm31ORYXFyMSidxllllVVQWfz0cLG5/PJ+WQSCTC1NQUoYEsvFcoFJJaTKfTIZlMYseOHeByuYhEItSUjY6O0gLF0A2r1QqdToe5uTm0tbXho48+wsjICCwWC7Zv345kMknjjsHBQZhMJnA4HOzbtw8CgQB+vx+jo6M0DqmsrEQkEkF1dTVqa2tx6dIluN1uev81NTUIh8M4duwYioqKMDk5idHRUSJ29/b2Ur4WiyNhsS/5fJ7k+wKBADabDfPz83Tei0Qi8Pl8yoYTiUQwm83Q6/VoampCLBbD2toaIpEIhoeHwefzib+SyWQ2+IRJpVIaQzGzzsbGRlitVkSj0Q0bjSNHjqC0tBSrq6sYHh6GQqGgcSUbZRYVFSGfz2NwcBB+vx/xeBxKpRJOp5OQZuDOWDGbzaKtrQ2Li4vQaDS4efMmXafuV4888ggUCgWJMQKBAFZXV8nl/PDhwxQ6bLPZaNRaXl6OcDhMnmWFaljGZUskEvD7/cQXVCqVxKliv7NIJEJNXWdnJ6xWKyGXcrkcy8vLhDaxjaFQKERRURG5rg8NDREXs7S0FCKRiJDUmpoaoiW43W5CtsxmM7xeL7Zt24auri5qVK5evYrh4WGoVCpkMhl88YtfBPAzQ9PJyUk4nU7Y7XYEAgFIJBKcOHEC/f39eOKJJ5BMJiGXy7ckif9rG6s9HJl9TisYDGJ5eRlzc3NEdC4rK0NJSQlqa2uJcBoIBPCVr3wF9fX1OHz4MORyOaRSKaqqqlBVVQWlUolvfvObKC0tRU1NDc26l5eXKfOtpaWFuC4stNVsNuP06dOIx+NIpVLg8Xi4du0aXewZ70Eul0Oj0aC9vR3hcBjV1dWIRqM0aiguLkYgECBXY3aRkkqlhFRFo1FqhgAQuvOgxVyl71WF8DKfzyciaFVVFTQaDQCgq6sL6+vr2LZtGwwGA41xmKkk805h/1tRUUFQflVV1Qb4ur6+Hi0tLVhfX0dDQwM1QY2NjUTgjMfj6OzspBwtg8FA2WJqtRonTpzYsLMTiUR49NFHKeoklUpBp9Mhn8/jwIEDdD82ajMajSgvL4dMJsP+/fuJ/7V//35kMhk6LqZEYzyO8vJyaDQaCAQCfPWrX0VbWxtKSkrIZ0omk1G2HFNXFRUV0QizvLwc+Xx+w+ezf/9+dHR00ILDSMUHDhyAxWJBJpMhc8PW1lYcO3YMFosFMpkMvb29FIbqcDhQX18Ph8OBxx57DP/1v/5XtLW1YWZmhsaPPT09+MIXvgAul4uenh5ST5WWlmLv3r0oLy+n7/bo0aMoLi4mewGFQgG5XE6u27t378bk5CTF3dTV1UGhUOD06dPYt28fqqqqaFRqMBigUqkgkUgQDofJ3Vsmk6GhoQF79+5FfX09DAYD9uzZgz179kAgEKCoqAjNzc0IhUJYW1uDz+fDT3/6U1y5cgXDw8N48skn0dvbi/b2drS1td3l98Xj8eB2u9Hc3IxEIoGnn34aarWafl9sDFtSUoK6ujpy9mdEbpaNWF1djbq6OmrSmIu0XC7HoUOHyF1bIpGQWanFYoFGo0FHRwepwFhsCRtVs00P8x5jY3upVIqamhrw+XwoFAqUlZUhm80SgV8sFmP37t3QaDQQi8UwmUywWCzgcrkoLS2FWCwmwjJrQiKRCLm+K5VKNDY2QqVSgcvloq6ujoJbmQ3CgQMHYDKZ6DOpra3FkSNH8G/+zb9BR0cHEa4VCgUaGxvJLZ7H4+HRRx+lHEJG1GZEfqlUCq1Wi2AwSEjzCy+8gHA4jObmZpSWlm4Ym3V3d6Ourg7RaJQ8pQo9mdrb26HValFVVYVvfOMbeOmll3D79m2cPn2aIkU2j8/Y4wvHuJ+FYuPBX+aYPo3nAIAHX2Ee1m+8Ll26BIVCgcHBQUgkEjgcDpIYu1wu4ggx6J1BwkzBwXKU2MXr8OHDBJHLZDLs2rULg4ODePrpp/HWW29teG23242amhoMDQ2hsrIS2WwWmUwGx48fJ2ddlkOWTCYxPT0NkUiE7du3UwBqIBBATU0NhVZOT0+TWzALSmUjua1KpVJBKpVibW0NTU1NWFpaImUHq127duH27dskIa+rq4PBYIDVaoXf7yeoPRAIQKlUwmg0wmazoby8HAsLC6ipqYFSqUQgEACHwyE5rk6nw/j4OPh8PsVhZLNZHD58GLOzs0R4ZunizPOnp6cHg4ODSCQSaG1thcFgoGwwADQyYdEHt2/fJv+myspKOBwOrK6u0sJjNBoRiURQVlaGxcVFfPzxx2hqaoLdbqdd0KVLlzb4IC0sLFDjFYlEaNEvKirC8vIyfD4fMpkM7eINBgO8Xi+OHj2KkpIS5HI5LC0tQS6XY319nQikPB4P7e3tiMfjiMfj6O7uxsrKCkwmE1wuF4RCIRYXF7Fr1y5UVVVhbW0NXC4XTz75JHk3bd++He+++y555oRCIcrPY1YECwsLcLlcqKiowJ49e7CysoKpqSlEo1H4fD7s3bsXPB4Pg4ODiEajGBgYgFKpJCQylUpRk3b9+nWcPHkS6+vrUKlUePvtt2lkxrgjy8vLOHfuHNra2tDU1ISJiQnyVmJqJdZsWK1WxONxTExM0GP27t2L5eVlTExMYGhoCB0dHaS2Ygql1tZW1NbW0tj1o48+Qj6fx/Hjx1FSUoL33nuPVJibDQPfeOMNlJSUIJVKoaGhARaLBdFolBA+g8EArVaLhYUFVFRU0AgqGo2Cx+OhoqICdrsd8Xgc4+PjMBgMWFtbo1DmUChE3+nMzAwSiQT6+/sJsWLj4FgsRuNihoY1NzcjEonQpoGNmI1GI3g8HoqLi0lVyawq2JhvYWEBPp+PxBpMscXn88nDiR3/9PQ0UqkUbt68CZVKhdXV1Q3cIoaK6/V6LC8vQyAQQK/X0+vodDqcO3cOAKgpFAqF8Hg80Gg0UKlUuHXrFtra2jA6Ogo+n4+zZ89CIpGgq6uLUJZXXnkFDQ0N6OjoQCKRgFKpxNjYGOx2O3p7eynzjKkq5+fncfPmTYyMjKCrqwsWiwVKpRLnzp1DfX09NU83b97E9evXyTzUZDIBuBNF1dfXh+bmZmqw6+rqoNVqcfLkSQQCAZSWliIWi+EHP/gB1tfX0dzcjJ07d5L/GbMKEQgEOHfuHG7evInDhw9j586d5Fvm9XqJRD4xMYFbt26hoqICe/fuJS4l40FFIhHU19dDIBAQeZw1p8wGIp/PIxgMYmJiAt3d3eR95/F4iOvFIpYK60HtCgpNNH8Z9OshQvRbUslkEhqNBsvLy6irqyOI32QyQSwWQyAQkBGez+dDMBiEUqmESCSi+bJWq4XZbMbIyAiduJFIBKFQCLdv30ZNTQ2efPLJLXcXXV1dsFqtZLxnNBqxc+dOgpZjsRiNaIaGhugiNjExAYlEAoVCgc7OTmg0Gho9SKVSkmAbDAbU1NTcRTpmPwKLxUIkVZb+zcZ0rNra2uBwODbA8/Pz89Q0scgAprLz+/1YW1tDcXEx+vv7IZFI0NfXR1D14OAgXC4X+cEYDAZkMhlwuVxMTEygoqICfX19SKfTFPURiUQQCARw9uxZbN++nZ53ZWUFly9fxtDQ0F0jM+BnXjaFfKobN25gdXUVAoEAt27dwsLCApkJTk5OkmKQkYhZuOzmhjKfz2NychJzc3NQKpWYmZnB0tISLe79/f3UjGg0Gng8HlRVVWF0dBR+vx9TU1NwOByYmJjAzMwMhoeH6aI3MTEBhUKB2tpacDgcCIVCGh1OTk6iqKgIQ0NDSCQSGBsbg8lkwvnz5zegSXw+H2azGclkEisrK0S2ZagZe9/T09OYnZ3F0tISUqkUZmZmIJfL8fHHH9OohFkGhEIhrK6uwmg04tq1awgGg5ifn4dOp8OlS5dQXV2Njz766C7E7ebNm1hYWIBUKqXjZscYjUaRSqUgFApx6dIljI2NweVyUX4Yj8fD6OgoLly4gHw+j4GBAZSXl2NwcJAWRb/fj3Q6jcXFRQwNDUEqleLy5cuIxWLIZDIYGhoinxtWW2UT2u12+Hw+alikUikFLLOmkiESer0eTqcTcrmcGgUmPbfZbIjFYoQSA3dk7SqVCpOTk+BwOHScbLTIZOh2u51G2xMTEzAYDLh16xaEQiHZObDsteXlZRQXF5P/E2tyJRIJnE7nhpGWz+eDw+GAw+Gg75LP55P60Ol00u9ApVIhHA4TadvlchF3pqqqijyEUqkU7HY7pqenyUeJ1ezsLMLhMDX80WgUFy9eREdHB9544w2YTCacPXuWRAQjIyPQ6XQYGBiASqWC1WrF1NQUnE4npqenweFw4Pf7SRnJrrWMcyYSieBwODAwMACBQICLFy8SYd/v98Nut+Ps2bMAQOeW3W6H3W7H9evXodfr8eGHH8Lv9yMSieDq1av0OOa4/d5775Hp69zcHEZGRqgZYo7iLB9TIpHgwoUL1HizDRI73pGREXA4HMzPz29QXi4uLiIQCJCBazQaxdLSEjKZDHw+H7xeL43VY7EYxsfHUVZWhqGhIVov1tfXweFwsLy8vCUh3OfzQaVSbXnNLKytHM1/kXrYEP2W1NDQEIqKilBWVgaz2YxDhw6hpKQE5eXl0Gq1EIlEEAqF4HK5lEjucDig1WpRXFwMkUhEIZlMUcQUQePj46itrcXKygpB/adPnyb4+Wtf+xoqKiqwe/duRCIRihLI5/NQKpVE4mMox/bt20lN0tLSQtEgU1NTOHPmDL773e9uMBXs6OhAXV0d5ufn0djYiGeeeYbedy6Xo/ciFArhdrvB5XJRXV1Nu0HGLamrq8OxY8eg1+vp8cXFxVhZWYHFYoFYLCZpPtvNWSwW2oEVOmuHQiEcP34cMpkMO3bsgNvtpvgSsViM48ePw+1248SJE2hubqamgKFOzz77LCYmJnDy5EkaN+j1+i0Xt5qaGhw7dgyHDh1CR0cHjQbb2tpo3HT06FFCJSwWCywWCxk+Pvnkk1Aqldi9ezeKi4tpfFBYTDYuEonQ2dmJqqoqcl0uKyvDwsICjXKOHDkCPp+PnTt3QqfTUXgwh8OBWq1GRUUFEVNPnz4NHo8HuVwOtVqNxsZG6HQ6ZLNZ7N+/H4lEAidPnsT27duxZ88eWK1W6PV6uFwuXLt2DXK5HEeOHIFYLEZVVRV6e3vhdrsxNjaGV155hUjT6XQakUgE58+fR11dHWQyGZGmn3zySRQXFyOfz+PEiRMoLi7G9u3b8aUvfQlzc3Oorq6G2WzeEF+STqcptoaVUCjEyZMn0dXVBQDYvXs3/H4/Bd9WVlZCrVYjHA5DoVBgamoKarWamvxUKoWamhoAwPvvv48dO3ZgZWUFXV1dhKiVlpYiHA5jdHQUH3/8Mc6dO4cdO3YQUsTUZb29vcR3qK6uJiSIRacUFxfD4XCAw+GQ1UB5eTn4fD5qampgMBjIzJKJLxiXp7GxkRq7rq4uKBQKCuZlSILP56Mmu729HT6fjxSDTLVXUVEB4M5mjbmlm0wmlJaWIpVK0aIeCARw4MABhEIhTE9P08aBKV9ra2uxtraG5eVljI6OQqPREMl6dXUVUqkUXC4X27dvp/edTqeRyWTw2GOP4fTp02hubiZSNxud5PN5tLS00DG2trais7OTPIFYMWGBTqdDZWUldDod9u7di+vXryORSGBwcBDHjh1DLpcjg1OJRIJ9+/Yhl8uhrKwMJpMJ7777LmpqasDhcNDa2oqmpiZyJr958ybOnj2L+vp6pNNpVFdXEzdMp9NhamoKPB4Per0eGo0GBw8eBAC6LZ1Ow2g0Yvfu3VhdXSWTSpFIhD179pD4IxqNoqioiJphgUCA2tpatLe3QyqVkoIwHo8Tori+vk7rCXCH58iuQcyvzu/3o7Kykn4bAIhewOFwoNPpiC/G/s3yIYE7DUtLSwusVivZcbDJBIs72orozQQwTGF4r9oqnuUXqYek6geozwKpOplMYmBggAzZvF4vuUjfunULmUwGSqUScrkccrkce/fupR9bLpeDw+HAzZs3SeHQ29sL4A56wKIF2tvbCVbN5/P4x3/8R7S0tGBiYgJ/8Ad/sCEMkcH5Op2OZN+FpD1GiI5EItTs2Gw2zM3NEUnzwIEDyGaz+OCDD2gnEAqF6IfDPnsGubJGqLq6miBYgUAAu92O8vJyUhGVlpZibW0N8/PzWFlZgcFggM/nQ0dHB/r6+lBeXo7V1VUa8TDpvUKhAI/Hg8/nw6lTp8gyQC6XY3V1Fe+//z5JaH//938fXC6X1EcikYjIjcCdpomhT+Pj44jH40in05iZmUEgEEBlZSUSiQS2bdtG5o83btzAnj17cOvWLYTDYRw5cgSxWAxqtRrnzp0jFZ/f78fzzz8Pr9cLt9tN0QiZTAb79+8n8j0L2H388cdx/vx57Nu3D16vF2VlZWhsbEQymcSNGzdw69YtQnUYH6S5uZkUYTMzM6isrCSDxdLSUuh0Orz11lsQi8W0eLa2ttL4qba2ljLlPB4PIpEIZDIZrl27BgB0gWVN6tLSEpGprVYr+vr6qMHu6emB1WqFzWYjp+Jnn32WHI/T6TT6+/vR39+P9vZ2LC4u4plnnoHP5yM5ciAQoGgVtqimUimsra1tII13d3fTKJnt7q9fvw6j0Uj8LAAYHBxESUkJAoEAvvnNbyKfz8PlcmF2dhZjY2OoqamhkeC5c+fows+OIRgMEhnZYrFAKBRS7qBAIEA8Hsfk5CSOHDmCCxcuQCKRYGxsDBKJBE8++STOnj2LhoYG2Gw2VFdXI51Ow263IxwOQyaTka0Fc4tnSExdXR2Gh4exbds2TE1N4eTJk3j33XdRWVmJ6elpQprZubBt2zb09fVRLMv27dtRVVUFnU6Hf/zHfyShg0QioTFya2srvve979H7Y1yXoaEhQiiY4o4ZkbLxGDODZZJ9pm7s6ekhOxGn00nk/O7ubvT29uLs2bNYX1/H0tISfZd1dXXknaZUKpHNZqHVajE7O7sBgevq6kI+n8e+ffvw9ttvI5FI3DWm3LdvH0wmEz788EO6BrMmNpVKwWazYfv27ZicnMTjjz8OvV6PlZUVyGQy3Lp1C9PT00RXqKurg1qthlwux/j4OILBIKanp1FcXAyj0Yi2tjaEQiEsLCwgnU6T6Wl5eTmKi4vx3e9+FzU1NZicnIRarYbFYkFXVxeNKZny0Wg0wu1246WXXiLeGFOZvffee3A4HGTt8IUvfOGuNWd0dJRCrUUiEWpra9HW1nbX/TweD27fvg2pVIpoNIqmpiaUlZVttYz9RuohqfpzWGzXaLFYkEqlsL6+jqGhIVy6dIkcVF0uF11U2JjM5XJheHgYGo0GTU1NWF1dRXl5OREJi4uLoVKp0N7eTuZlDF3YsWMHrl69Ss2TVCqlZGi9Xo+RkRHcuHGD4hTy+TxxANbW1shnZmVlhXatSqUS6XQaJ0+ehEKhwNmzZynHKBQKEYGS7XaYQge403wVFRWRcZ3T6YTD4UBnZyfW1tZQU1NDx8CUMMznZO/evYTs+Hw+dHZ2Ip1Oo7u7G7lcDhwOB5WVlaipqcGLL75IqAizJygpKYFGo4HT6URpaSk++ugjag5zuRySySTUajUld7OIjaGhIchkMsRiMbhcLpLrMiXK1NQUkskkzp07h7KyMrz//vu0y/vggw9QVFREzZDH46EG78KFC/D5fLBarVhYWIDNZkMikcD58+eRyWSoGQKADz/8EF/5yldo/LaysoLFxUUagxSq4+LxOHw+H27evEmu2nK5HIuLi1heXobFYsH6+jo++OADNDU1YXFxETKZDHNzczTCqq+vRzAYpFGBy+VCIpGA1+uFTqej5h24szudnp6mhlAsFsNsNqOxsRHr6+vkjlxdXU0Gesy8kY3tZmdn0d/fD5VKhZGREVRWVuL111+nuAjgzk6bfYY2mw0ymQwCgQAajYY4EcCdRWBoaAgGg4FGYMwpncPhoK6ujpRfLpcLp06dAofDweLiIl599VWIxWJs27YNVqsVR44cwdjYGCmhxGIxkevZzpk5mTNui9FoRCaTQTAYRGVlJa5fv47HH38cs7OztBsfGBjAs88+SwGw1dXVpJQq3N+y37lOpwOHw0FDQwMcDge6urowPT2N3t5euFwu7N+/HxMTE9BoNBuidlgQMUNiWbYYI2gXoiypVArBYBC1tbUoKSnZEBArFoshlUpRVFREDZRYLEYul0MmkyG1IyNeC4VCyOXyDf5g8/PzMJvNaGtro2MUi8XkEl9SUnKXnxhTyTFZfnl5+QZVHKuhoSFUVVXhzTffJLdurVZLf9doNPB6vZiZmYFOp6MIlmAwCLvdjuXlZYjFYty+fRtHjx6FwWCgbMFwOIxEIgGBQEBjV2YrwrysvF4vDAYDnSNs81daWgqn04mamhpYrVYipz/xxBOYn58Hj8eDWq2G0+nEzMwMjacqKirQ1NQEm81GAoRYLLaBZ9PT00Ok9uPHj2OramxsRHl5OUQiEW2itiqdTofm5mbE43H6/n9b6yFC9AD1WUCIACAajWJ2dhZWqxWhUIjm8puDRPft20cXYY/HA4lEQpyCQCCAhoYGSjEH7ry/mzdvoqurC4FAgFROb731FsrLy2G1WvHEE08AACWy37hxg5qFsrIyNDQ00FiOmZGx3RqTf16/fh3JZBJ+vx9f+9rXCBn44IMP0NbWhsrKSpw5cwZmsxnj4+NkAMkCbQUCAXbv3o1du3bh5ZdfptcqKSlBZ2cnOXI7nU5MTEyQ0mZxcRE7duwAcMfjqL+/H/v370dpaSk9hnk5lZSUbAjQZSMqphj56KOPkEwmsW/fPgAgsvr6+jpmZmZgMBiwtLRE6e9NTU04f/48JiYmaMcWj8cp94mNHxsaGjA8PIynn34aV65cwezsLJ5//nmsr6+jqKgI77zzDpnqyWQy7N69m8YhwWCQZvm7du2CVCrFysoKPv74YwDASy+9RDYBY2Nj5HDOvj/GR2D+UEqlkrxzuFwufD4fenp6UFZWBrvdjsbGRiQSCbz77rtoamrCwMAAzGYzOjs7YTQayXohHo9vQIjYCJbxBWw2G/R6PbLZLH1HLEKFuWaz52FhpW63GzqdDsXFxRsysf75n/8ZgUCAvr9Tp04hEomQLDqdThNCOj8/D6lUir1798JkMhERfX5+HgcOHEBbWxtFbTgcDly5cgXpdBqlpaUwm804f/48GhsbUVVVRcTRv/zLv0R9fT0mJyfxta99DXq9HplMBv39/ZiZmUFPTw/kcjll2rHxxdWrV2nBra6uhsFgIMM9iUSClpYWCAQCBINBvPzyy6ivr8euXbtIQZdMJtHX10dBqwqFAvX19WhoaIBcLsfa2hpu3bqFlpYWIpZbLBZ4vV4sLS1heHgYnZ2daG5uxqVLl8Dn80n1V1dXh/X1dfzkJz9BPp9HbW0tOjs7NxhG/vSnP4XL5cLBgwdRXl5O52cqlUJ/fz+4XC6Ki4tJzOByuVBUVITx8XEYjUYUFRWR9YBerycDQ4fDAZlMhrGxMaTTafT29lJ+3dLSEkZHR5FIJHD48GFotVpMTExQ5h0j7jY3N8PhcFCEys2bN8k9Pp1OY2pqCrlcDgcOHCCTWIbYbN++nVAiLpeLXbt2kacVl8uFUCiEz+dDMpkk6wrGv4zFYkin07R5UalUuHDhAlKpFPbv3w+tVksNCjuHz549C7PZTM7vJSUltNG8ceMGGhoaUFlZSecOQ+Tff/99FBUVUWP1wQcf4Mtf/jKZeCaTSeh0ursQosL//3n3JHroQ/Qp12+6IWKSQo/HA4/Hg1AoBK/XS661PT09uHLlCt1fJpPhiSeeoPEFyznicDhwuVzgcrk4duwYUqkUXn/9dej1etTX12N1dRU9PT0YHx8n4jKbnxcXFwO4s5hls1lSTEilUrS1tdFCyE4nmUyGfD5PIYzxeBzT09OYnp6GxWIh7ohAIMDc3ByCwSCi0SjKy8tx48YNGi8wM7XJyUk0NTWRgZnT6cTY2Biy2Sza29thNBohk8kwOjqKvr4+lJWVkRkgcEf2brFYMDQ0hB07dmBoaAjPPPMMvS+32435+XmMjIzA6/WioaEBJSUlRBY9ePAg0uk0vv/97xPEvmPHDng8HmSzWbhcLiJW6/V6CAQCHD9+HEtLS/jwww+h0+mwsrKC6upq2Gw28Pl8xGIx4neZTCbs2LEDCwsLWFxcRFVVFWZnZ/H4448TghCNRvHOO+8AAB599FEinG91gfN6vfje976HkpISHDx4EIlEAkajkcZgbDQIgMiozLtmYGCA4kpUKhWUSuWGHSU7J9lo9X6z+8L7AXdGK4xDwna8paWld+U7fZIwzGvXrmF5eZk+p7a2NrjdbhgMBtjtduzYsQMKhQK5XA6Dg4PkDjw8PIzTp09TE1Z4vGKxGF6vFxwOB0tLS8hms4jFYhgZGSG5/6FDh2CxWGAwGOB2u/HDH/4QTz31FMrKysDhcDA7OwuTyYSlpSW0trZu+Rmx17PZbNDpdDQKl8lkNJK912OkUimuX78Or9cLq9UKjUYDmUyGo0eP0ij3woUL0Gq1+Oijj7B3717yYPJ4PHjnnXdgsVhgt9tpRPzyyy+jubkZlZWVeOWVV/DlL3+Zmme2CItEIrjdbkxNTSEQCBC3kI2cC5tt4A6Ss76+TjxAhtwJhUK88sorZAWi0+kgFAoRDocJDS68hqhUKjidTvD5fKRSKUgkEhgMBlJMeTweGAwGTE9PY35+HkePHoVWq8XIyAg+/vhjKBQKyGQynDhxApFIBDabDbdu3cLx48eRzWahVCqxuroKHo+HpaUlVFZW4ubNm6ioqEBvby/W1tZQVVVFCr0LFy6gqamJ7BLYOSsSibC+vo5QKEQ2AqlUCn19fZidncUzzzxDnnFLS0v4+OOP8eSTT8Ln8+Gjjz7C4cOHiVNTaD5qt9uRTCbJtLLwfPJ4PPjRj36ElpYWTE1N4Vvf+tZ9fzP/muphQ/Qp12+6IVpfXydDsHg8TiaB0WgUiUSC1EaFvj1lZWU4ceIEKTTC4TDtRpubm6HX63H58mUUFxdjfn4eFRUV2L59O86fPw+1Wk2+NIFAAHa7HWVlZWhrawOXy8XCwgIhUIwLwFABhoAwKS0jVy8sLCAQCMBqtSKfz+PRRx9FLBbD8vIygDteR8zUzmAw4Cc/+QmkUikqKyvB5XLB5/OxZ88epNNp/OAHP0A8HsehQ4dolMLhcJDJZDA6Oop8Po/R0VFUV1dTc5VMJol0fPnyZco/6uzsRDwex+rqKvr7++F2u8m0jV1wWQbT/Pw8qqurCWUpLy/HI488glgshlAoRMhdLpdDaWkpTCYTNBoNbty4gStXrmDfvn2wWq0UMKtQKBAMBtHV1YWKigrMzMyguroa4+PjFKfCgiK5XC459DIuxOHDh+mz37xw/s3f/A05TJeWlkKj0WBhYQGPP/44stks7XLZZ69Wq/Hmm2+SRxTb6TKPHo1GQygbOyeZ0eO9Fu7N9+NwOBQ1sbq6SknsRqPxrqbH4/FAqVTC5XLBYrHcdxebTCbx2muvkXdTT08P0uk0hoaG0NnZCQA0/sxms5ibm8Obb76JHTt2YHJyEi+99BIdP+PBscy2fD4PDoeDmZkZGgH39/eTEzazmigMAU0mkxTuy/K8MpnMlp8R+3xyuRxcLhfJ09kxb/W+C7l6fD4f/f39JJPev38/9Ho98e58Ph++853v0Jjx6NGjaGtrQy6Xw/LyMi5duoT9+/ejoqIC//RP/0SITTQaRW9vL/r7+/Hv//2/p3OM8RenpqbgcrkI6ezp6aHvkEW0FCbFM7sBu90OtVoNg8GAl19+mQwud+3aRQ048yFSKBQU1pzJZOByuSjiRiqVQiwWEzGXjb2dTidu3LhB8R8NDQ2Ym5ujsZHZbEZHRwfC4TAuXryIqqoqeL1ePPvss4jFYnA4HLRZWFhYIA8wDoeDo0ePYnp6Gnq9HmfPnoVOp6PMvra2NlIjOp1OSgZIJpOoq6vD4OAgBgcHYTQaEQwG8dJLL8Hj8eCtt95CY2MjxsbGkMvlCB37whe+AKlUusF8VKVSUdQN8+RilcvlsLCwgPfffx/PPffcBmHJv/Z62BB9yvWbbogYQsT4KouLiyTNZU7QdruddmUajQbd3d1YXl5Ge3s7AGB4eBjxeJwWuZMnTyIej+PNN99EbW0tWenfuHEDPB4PtbW1UKvVJKsH7qgKqquriUDMfnSMbM0S4dmoQ6VSQa/XY25ujmTuTDEEAOl0GtlsFgsLC/TeOjs78dprryGdTiMajUIkEkGtVpOax+l0YnV1lVAO5vuiUqlw5coVuFwuaLVa7N+/H3Nzc5iYmMDU1BS2bdsGrVYLmUwGh8MBPp+Pbdu2UUNXiBD5/X5s27YN3d3dOHPmDPkp+f1+fPzxx1CpVHQxfvTRR+Hz+cjDZGVlhbgcdrsdR48eJc+dqakpQreUSiVqampQUVGBTCaDN954g3bvTH1TWlq6IXeNIUR8Ph+nTp2imJStYG+v14sf/OAHxH2amZmBVqtFIpGgiy37HMViMeUxOZ1OUjLt378fVqsVuVwOXV1dG3LWCpEfRvRmSpBCZGcrhOhBnHJzuRxsNhtMJhMZzd2v2AiksbGR0Dd2zjEOC3N7Hh4exs6dO/Huu+/iscceI0Sn8H0VIkSswWAO0mtraxtCOtm5yzyP2P0Ln+9eo4lIJII333wTXC4Xp0+fpsX9fujYJ41g8Hq9+OEPf4jGxkY4nU50dXWhvr7+rpDRQCDwcxEi9vpMzMFiTFhOGbvf5mPMZDLEkWIO5qFQiHx8pFIpWltbkUwm6Xk2o8wMIeLxeIhEItBoNGQWGY/HyQBzcnLyLoSI2VR0d3dDpVKhr68PsVgMU1NT+OpXv0qKSz6fT01PNBrF+Pg4pFIpent7MTU1RQ1rIBAgZ+ru7u5PBSGy2+24cuUKent70dzc/IkQood173rYEH3K9ZtuiADQzra/vx+hUAg8Hg+hUAhCofCuxHitVguxWIwdO3aAx+MhnU7D6/XSjm7Xrl0U1xAIBFBRUQGXy4W+vj6sr68jFoth165dqKurg8vlwvT0NBEa2WKTyWRIrs4CGNkFgcVDMHlnJBJBIpEg0qROp0MsFoPX6yXCYeFFcHl5GR9//DF0Oh1FXfD5fIK6f/jDHwK4Y0DX2dlJSENfXx+NGw4dOgSbzYZgMEiBnKFQCHa7HfX19VhfX0d7e/uGxQvYelTDLuYmkwkSiQRTU1NYXl7G4cOHsb6+Dj6fT4GRrHFdWVmhdPBTp04hm83inXfeIemuQqGAWq1GZWUlbty4QRl07DtjZPL7qTUedGH0+XwYGBiAx+PBiRMnYDAYsLq6SgRqpVJJxnNGoxEdHR3k3P0gF10mY2aKIJVKBa/XSwn3Tz31FJRK5SdeyD+t7CX2PF6vF9euXUNlZSXsdvuWyppf5HXZhoWd95/kWF999VVEIhFwuVzo9XqcOnVqw+e5eZT4y9RPfvITlJaWYnl5Gfv27ftMKYE+Sf2yERSbTQLZhu/nfe+FhoUGg+HnjnJ/keP618Lr+XXWw4boU67PQkNktVppUQuFQkgmk0TKZQtRYTGvFpbUzuFwMDg4iO7ubqTTaSgUClJEMadRZhpoMBhw6NAh8Hg8uFwuLC0tUewDU+usrKyAx+MhFouhurqa+EMs12d5eZkQKi6Xi/n5eSIMMvlyPB4n2TZTabG060AggLm5OYyNjZHTdVtbG1ZXV1FTU4PBwUF0dHQgmUyipqaGTNvcbjd2795NRm6jo6MoKyuD2+1GY2MjOBwOJiYmsH37dpjNZlpwbt26hXfffRd8Ph9CoRBarRZf/OIXSQbNdphFRUX0eeZyOdq9qtVq+Hw+ajojkQimp6dx5MgRVFVV4Qc/+AFlNDFybSaTQW9vL958802S5Pb29pJcnMUo3OvCG41Gkclk4HQ6odVqcfv2bezbt4+k/4X3W19fp9gF4E6DzdyTg8Eg6uvrUVxcfM/XSqfTGBsbA4fDIaIvq8ImkiFjy8vLmJmZgUajQSKRwFe+8pUHOs/ZosDI1D9vcfgki0g2m6VA11OnTtFnsbnYGIvFbNyr4vE47eg3f+b3q8JjXl9fxzvvvEPRD1Kp9BPxp7Z6fyMjIzh58uRd7y8SieDdd99Fe3v7lggROzYm1w8EAhgZGcEjjzxCPLWtmj/GX3nyySc3jDdzuRxWVlZw5coVckdnf/N6vfjxj3+MI0eOoK6ubgOauBll2sxT23y/eDyOixcvQqFQYOfOnRuQzIf1sB42RJ9yfRYaomw2i4GBATI7Y8V2SptLKBSSkZxcLsfMzAyZ6bEQS6awUigUpC6Jx+Oor6+nXRMLgszlcrBYLCgrKyPPFYfDQUgCI1sywurS0hKNuLRaLYUalpaWgs/nUyI3I34zoqZGo8HY2BgWFhbgcDg2GBmq1WocPHgQH3zwAfbv34+VlRU0NDSgpqYG165doxRuJtm2Wq2UBXXo0CFwOBxMT08jn8/j+vXraGpqIrv6//N//s+Gz4+5LrOsKrPZTAGkjBTOOAOpVIrUNexiXfjd+Hw+SCQSvPbaa2S0lkgksGvXLvT392Pbtm0YGRnB0aNHIRQKcfHiRUgkEuzYsYNMHbcq9h7lcjk9z8LCAo4ePXrX/bZa4JmLrVqtJjUdC4FlkRSlpaVQKBQYGxuj6BeZTLalHwl7TrFYjHA4jKGhoQ0I0YPU+vo6hEIhFhYWiE+mUCjue/9PO7CSuWSzjLh71dmzZ9Hc3IyJiYm7PvP7VeExs1HevUZNn6SsViuuXbtG45V7IWBbVWHzwXhCt27dQm1tLVZXV3Hq1Ck6dmaxwc6nv/qrv0JHRwdu3bqFr3/966Qyu3DhAkZHR9HR0QGXy4WnnnqKvqO//uu/Rm1tLWZnZ/HlL38ZBoPhru/yQXlqZ8+epQ1WSUkJqWcf1sMCHjZEn3p9FhqifD6Pv//7v0c4HL4rZbywWFK5SqVCSUkJstksVCoV5QTJZDLU1NTA7XaTT49YLEY6nSa/FZfLhSNHjkCtVlOcBAsiZeZfhWnnjEBqtVpRV1eHUCiEaDSKubk5ysMRCoUQCoWEeigUCuIH8Pl8zM/Pw2Qywe/3w2azEVeIlUgkwuOPPw6/34+2tjZSblVVVWF5eRmVlZUIBAIwm83gcrlkAMd4MwzNuXbtGhwOBwwGA4LBIA4ePIjW1lZqNtm4ioWLMp8UnU5HbrDMo4jt5MViMaFG7KLOyKT5fJ6S3pPJJCWOsx034ya0t7dDo9Hg3LlzEIlE8Pv9KCoqovHmvc4JlucllUrviRDda5HN5/OIRCJYW1uD2WymEZnVakU4HEY2m4VQKERZWRmEQuE9EaIHea0HrXw+j9XVVSSTSQB3Gvvy8vL73v/THjM86HN+EoQok8lgfn6eCMWFzdDmJuAXbfB+HkJ0vyp8XdYU/TIIEYu9icVisNvt+PKXv7wBIVpdXcXrr7+OAwcOoLW19RMhRJvrIUL0sO5XDxuiT7k+Cw1ROBzGO++8s8FwD7izYLD8LzZmAO6ozAwGAxobG3H79m1MTEwQz6ioqAitra0UWMlM/VQqFTweD8xmM/x+P1566SWK92CKCpYZo1QqyQeJQe/pdBp+vx9NTU3g8Xi4cOECeajU1dWhtraWGgjmZh2Px7G4uEi5ZM3NzUilUpibm4Pf7yeXWeYNYzabIRQKkUgkwOVy4ff7UV9fj+XlZVKDqNVqrKysoLKyEi6XCysrKygrK8Ply5chl8uxvLxMC/vevXuRTqchEAgwMzOD2dlZxONx7NmzB319fZBIJOju7qZIgLfffpsURoxPtZlwutVFnYXWrq6uwuPxQCQSobGxkewKLly4gLq6OkxMTIDP58NoNFIUyIOMjrZ6bWaMyAjAD7LAsCbL4/EgkUgQQvRJm41UKoWrV6/i9u3b+OpXv7oB5WJEYpPJhN7e3g3GiMAdNNLtdiMUCqG6uppiBD6NCoVCeO2112AymXDo0KG7XvtXVbOzsxAKhYT4sc+j8DsD8MBcpGg0infffRcKhQKPPPLIXVEtwB313dDQELq6urZ8n4WvnUwmceXKFVKhLi4uIhQKEQ+wqamJVHexWAzJZBJTU1Po6uoCh8PBtWvXcOPGDRgMBnzpS18Cn89HX18fkskk9u/ff9fxMT+hV155BY8++ijq6+vhdruxsLCAHTt23PN7icfjxC/s6OhAOp2+L+dnbW0NExMTW24UHta/jnroVP05rGAwCLPZDI1Gs+H2VCqFbdu2obGxcUMEgdVqxcTEBEZGRogrxFAgsVgMq9VKZGzmVOxyuahB6u7uxsLCAlQqFXQ6HUZGRhCNRhEMBhEMBnHt2jWEw2FMT08jFAphZmYGNpsN6XSazAJZNEJZWRlEIhF8Ph+uXr2KpaUl8Hg8zM7OwufzYWxsDFarlST+fD6fAlNDoRBu3bqFN954A16vF/F4HIFAAMXFxTT+c7vdWFpaQjqdJqWUxWLB8PAwhW7OzMygtLQUS0tLqKqqQi6Xw/r6OtxuNyKRCG7fvo3p6WkKYRwfH8dzzz2HJ598EkajEXNzc3jttdcIbRsfH8fc3Bz4fD69/uTkJMLhMCmqChccHo8Ho9GIrq4u9Pb2oqSkBGKxGKurqzh//jwsFgvOnj2LmpoaZDIZInt7vV5ylwVASNdmV2JgY+IzcCcAlAVssmPJZrMbxnqbix1vcXExmpqaoFQqH6gRY7wTViw4srS0FD/+8Y833P+9996DXC6H1WrdEBbJisvlQqFQoKamhpCiT6vefPNNyGQyrK6ubvnav6qqqqoiKXZhLlNhBhPjTrHb71cffPABhYj29fVteZ+hoSHU1dXd830Wni83b96EVCpFMBjElStX4Pf7EY1GKel9amqKHpPNZnH79m2UlpZiaGgIIyMj6O/vRyaTgdfrxblz54gfxxziNxeHw8Frr72G5uZmvPvuu1hcXMTc3ByKi4vv+71cuXIFUqkUTqcTIyMjG873zeXz+TA6OoqSkpINPm35fB7hcBgzMzN3uVbfr7Y6zx/W56seNkS/JcVGQcz6npVGo4HRaLwrJR64c9EZGxujvCc+nw8+n49QKIS5uTksLCxgbGyMOC0AoFAoCMFh7sxWqxVCoRDRaBSrq6vwer0QCoWkuFhZWaEIhYGBAXz44Yd47bXXoNPpKD3d7XbjjTfeQEVFBebm5mCz2VBcXIyRkREUFxeTYo7H42F4eBhOpxPxeJyS20OhEDweD2ZnZ3HmzBncvHmTMspu374NiURCnBWRSASr1YqmpiZwOByEw2GUlZWR8m56epp8ec6ePUtSfY1GA6VSiVwuh56eHuLSzM/PQyQSkepOoVBAIBCgpKSELPNZQjQz8bNarXC73RCJRBuamdXVVbjdbkilUnLhbWtrg9Vqxb59+zA6OkpKP7/fT4gcSylnYyuWJ5fNZuFwOHDjxg3w+fwNic8lJSWw2WwoKyvD6uoqqfwYArFV/SKp0ZsbMQBob29Hd3c3bDYbvvzlL2+4/6lTpxCNRlFWVrYhLPKXPY4HqdOnT2N9fZ3yn35dlUqlsLCwAD6ff89mh71nFglzv4X3+PHjyOfz0Gq12LVr15b36erqwuzsLL1P1giMjY3h/fffx9raGr7zne/A7/dj+/btNGpvamoi7mFbWxsymQzFNrDmvqOjAwsLC5Rh19PTAz6fD71ej3379uH69ev47ne/i1AodNf7yOVymJubQy6Xw9DQEB599FFUVVWhoqIC/f39GBoa2nCdY41INBqljEaj0Yj29vb7niM6nQ5tbW2w2+0UPwSAxngajQaLi4v3/Iw311bn+cP6fNXDhui3pJLJJEZHR++6vbW1Ffl8fkOuEHCHc8PhcFBfX494PA4ul0sjnEJ1iUgkgl6vR2NjI0pKShCJRKDT6chcLpFIIJVKIZfLIRwOU8xAPp+HWCxGMpmEXC7H0tISEokE1tfXsba2BrFYjMnJSTJOs9lsaGhowMTEBA4cOICqqipkMhl0dnYS14fD4cBkMqGzsxNFRUUQCASUKWQ0GlFVVYWxsTFUVVVhcHAQbrcbWq0WFRUVCAQCNN6Jx+MwGo2w2+3YtWsXqqqqMDk5Se6/X/rSl0i51tXVRWaMm3eydrudLAIYoZqhakKhkCT8jO+xsLAADoeDQCBA0GwgEKALts/ng1arxeLiIqanp2E0GgHcGXUeOnQIzc3N5McyPj5OHisymYx27BqNBmtra0gkElCr1bDb7URgHx0dhVAoxODgIF555RX4/X7iixUXF1M8CeM7bbXj/UVSo7dqXrhcLvk1bVYzKRQKvPDCCzh8+PA9RyOfVno18LMRXDQahVKpxEsvvYQTJ0782sZlAHDhwgWUlpbi+vXr91xQ2Xtm6st73Y+NQrlc7pbjKFYsCV0kElEzvrq6irm5OSL5t7a24t1330U2myWDRmaWuXv3btTV1VHoc+ExisViNDU1QaFQIBKJ4ODBg/jDP/xDvPTSS+QDxOPxcPbsWXg8ng3nmc/nw3vvvYeuri7o9Xo0NzfTRi0cDqOqqgqvvvoqvY9CZDOfz+PEiRPYsWMHhELhfc8Rlgd27NixDeMyFvcTCAQoc40hr4wwXni82WwWy8vLcLvdG8abD+vzVw85RA9QnwUOUT6fx9zcHH70ox/RbWKxGHv27IFEIqHMIlYmkwlut3tDWKNYLKZYCY/Hg2g0SgtkRUUFJiYmKOkaADo7O9HS0oJEIkE2/ZlMBgaDgXZwjMBoMpnIh4eZ9TF0hqmy+Hw+Ojo6MDs7C71eD5fLhXA4TMqV1dVVmEwmlJeXI5/Pw+l0QqPRkLScBbaOjY1h9+7daG5uxtWrV7G2toZgMAiVSkVu1sw4cXp6GnNzc6iursbKygqam5uxsLCAnTt3YmpqCmNjY9RURSKRDRytZ599lrKABgYGMDs7C51OB5fLhYqKCjK9TKfTuHr1KmWhNTQ0QCAQYHh4GAaDATqdjhRwZ86coXBco9EIo9GIaDSKeDyOcDiMjo4OnD9/HqdOnYJUKsXi4iKZw1VWViIej2N5eRktLS1Ip9MwGo2w2Wy4fPky2RpEIhGoVCrKclMqlYjFYhSNUFpaSoHACoUCExMTcDgcKCoqQl1dHd0/Go0ilUphZmYGZrOZpNqM58LlctHX10dGcaxp1Gq1uHHjBq5fvw6DwQCXy4WvfvWrAO4gfRUVFbBaraiqqgKfz99gPMp4cMy+gRkr5nI5Mo0s5BQVcrR8Ph95xBSOoGw2GxQKBZLJJKmVNsedAD/j7/w8yf/9SOr34mh5vV709fWho6MDpaWlG6TpmyX2P48LxtRk5eXlWFtbwxe+8IWfSwRnzt8Oh4NiK2pra3Hu3Dk89thjKCkpISuJB5H8M66Z3++/y0A0EAjg0qVLcLlc2LFjB6qrq5HP58m0Mp/PY2FhAWfOnMFTTz1Fn0cqlcL169cxNTWFZ599Fmq1esN3y3zKPo0mOZPJUEQOs+hg13jmgs02OlarFYlEAhwOh4JOH9ZvTz0kVX/K9VloiHK5HMbHx/H6669vuP3QoUMU3dHX10fmjPcri8VC6jHgDkIhEAgo94uVQqHA8ePH4fV6sby8jFgshng8DqFQSETM9fV1Cg+sr69HLpeDUCjE9PQ0jdKYtb/RaKSIDsZt2rZtG8LhMGw2G2WXsQRytqgzt9ZYLAaVSrUhl4xFfxQec0NDA3K5HILBICKRCCQSCRwOB2pra7G8vIyenh7cvn0bfr+fVHl6vZ74Qaz0ej2eeeYZXLt2DQDI50itVqOsrIyiHFhjl8lkoFAo0N7eDqvVSscvlUpRW1uLtbU1RKNRuN1u5PN51NTUUPNhs9lgsVgQDAZx6NAheDwe5PN53Lhxg+JPWEim0WjEwsICamtrIZFIMD09TdEPSqWSRjNNTU0AQKo4LpcLs9kM4E4Dm8vlMDs7C4fDgWQyCR6Ph+rqaphMJhrbzc7OQi6XI5vNorKyElqtlpCV/v5+6PV6WK1WaLVaqNVqcLlcpNNp5PN59PX1wePxoLu7m5A+s9mMxcVFdHV1UcYXa76y2SxCoRCR41OpFBQKBUKhEHHoGEGfVWHUBmuUBAIBLWp+vx9qtZpQS4YUsAW80LaCoQPJZJLUifeK29hKCXY/mThrIJhAgTUbD2LCaLVaYTAY4PF4UFZWtkFNtnv3bqytrZE9BDumbDaLmZkZDAwMUBYhcxNnI6t8Po+6urp7ktaTySQuXbqE1dVVnDx5EkajEdlsltzfOzo6KPrn4sWL2L17N3p6eqhpjsVi4HK5uH37NrRaLVpaWpDJZCCRSDA/P493330Xzz33HKnhNkefRCIROJ1OWK1W7N69mywfPo2anJykxq+pqWlDI8ia6cKGlCleWdDzw/rtqYcN0adcn4WGyOVy4R/+4R82ID4AsHv3bkxMTCCZTCKXyyGbzYLP59+TjMp2v0qlEmKxmNLD+Xw+tFotkSeBOwiJRqOhC+jKygqkUinMZjPC4TB8Ph9UKhV52HA4HPT392PPnj3gcrkYGxuD0+lEb28vioqKoNPpkMlkcO3aNdy6dQuNjY2wWq146qmnIBKJMDk5CZFIBLlcDrFYjLGxMbS3t2NpaQlDQ0PI5XIwGo04fPgwstks3nvvvQ1NHLPmT6VSlH20tLSE4uJitLa20ihrbGyMHLZv374NjUaDlpYWcvFlJNUTJ06gvLwcTqcTi4uLJEH3+XwIBoMwmUyora2F3W7HwsICstksTCYTZRitrKxAq9VieXkZiUQCTz31FBwOByEWdXV1CIfDWF1dhUgkQiqVQnd3N7xeL8xmMzweD5aWlij/afv27cjlcrh58ya5ZnO5XAgEAgwODkIsFlMYLUsNr6qq2oAayGQyWCwW2vEKhULMzMxgeXmZECI+n0+2Ctlslly3LRYLpFIpjd7uhRD5/X7Y7Xbkcjkkk0mYzWZqxBhCNDs7SxlfEokEXq8XuVwOEokEgUAAOp2OXuuXRYgY2gNsVHEBvz6ECNi6+XkQRGYzQsRqfX0d4+PjKC0thd1up5Ers0746KOPUFpaSigSe99zc3MkwJBIJBsazMK6du0aRkdHyVzzmWeegd1ux+DgIDQaDXw+H1pbW/HBBx9Ar9fD5/PhySefpOdbX1/HrVu3IBaLEY1GUVJSgtraWni9Xnz/+99HS0sLJicn8fWvf33L6JOVlRUsLi6SwnX37t33RIc+qf1CX18fZRxuxcF6UMn/w/rs1ydZvz89PevD+pUWh8PB4cOH8eGHH9JtSqUS/f39tDPk8/kQi8UQiUSUetza2oqpqSni/wQCAfT29iKZTGJubg6NjY1YWVlBY2Mj7YLEYjHEYjHMZjNlXs3Pz2P//v2k0GG79mw2i23btkEqleK9995DVVUVLl++jJaWFoTDYcjlckSjUdTW1mJiYgJ+vx+rq6vo6OigHe7169chk8mwsrICPp9P/kkGgwEDAwOQSCRk0BgOh7GysgKbzQaVSoVYLAYej4e9e/fC6XSCw+GAz+cjGo1iaWkJEomEktyLioqwvr4OqVRKx3b69GksLi6SEi2RSKC6upoS0iORCClSSkpKYDQaCfmZnp4Gj8dDfX09amtrcfnyZYTDYfB4PORyObS0tGB0dJS4TufPn0dTUxPkcjn8fj9u3LiBqqoq1NTUIJFIIJPJwG63w+12w+v1ktFjdXU1WlpasLq6CofDgUQigWw2i0gkQsTy3bt3Y2BgAN3d3RgZGaEMJRYy6/f7cf36dVRUVODMmTNob2+HUCgkngWzMJicnMS2bdvA4XAQDAbB5XKRSCTgcDgIATIYDMjlchgdHcXw8DDKy8vB5/OpKRYKhVhbW8PCwgL8fj+EQiGMRiPkcjm4XC7W1tYoU85isRBnhsPhwO12w263QygUQi6XQ6/XIxwOY2lpicI+AVBmVCgUQklJCbhcLnQ6HWw2G0KhEJxOJ4RCIQkGWFo9G3swYr1MJkM0Gt1gm8DQjWg0So9nCy1rYLRaLSKRCCFXLCeQNYHMS4rD4UCn0xEyuLCwgM7OToRCISwtLUEgEGBsbAwqlQoHDx6kHKxIJIJUKgWTyYS1tbUNysVIJIL5+Xncvn0bhw8fht1uR1tbG6GhwB0T07q6Oly9epUMGlmOnsvlws2bN9He3o49e/ZQJAXb0IRCIZw+fRpdXV1k/nnw4EHk83ka205MTKC1tRVFRUU4dOgQLly4gB07dqCyshLpdBo3b96k0Fa3242ysjJUVFTQ5/Hoo4/ivffew9NPP02ZfDweD1KplJoRNgJeWVnBnj177mtcyXLmvF7vXXE8W1VnZyf6+vqoGdpsf8D4RMxNnIX3MpPZezXM93Nav1/0R2E8EDMiXV9fJ/S8o6MDQqGQnoOdXxKJhBo2dm7YbDa0t7cjk8lssP9gnmUqlYosTNh7YeG4wB2OFftNMlSRCTkKzXTj8Tj0ej3x3hhVgok92PfOxpCMRsGmDE6nk1Swer2eUgDy+TuhxGq1GjabDblcjrLq5HI5tFotHA4HVCoV8WPVajVtWvV6/Ybv6JPUQ4ToAeqzgBDlcjnMzMzgvffeI56PUChEe3s7BgcHacSUy+Wg1+uxtraGcDhMcRiFVVlZCb/fT5EfDGLf7HjNwiCBOwRNp9OJqqoqrK+v0wWD/SAbGhpQUlKCCxcuQK/XY2ZmhjyEtFotLBYLvF4v7ZL9fj/a29sxPj6ObDa7QVXCnKwLF7l4PE6QularRTqdRiqVAo/HQ3l5OW7cuEGWAjqdDh6PB4FAgMjTMpmMgiVdLhctauyHura2RhcZs9kMDoeDPXv2IJFI4Ny5c8hms4hGo2hpaUEsFqPFz2w2o66uDl6vF+vr60in03A6nSguLoZSqcTKygqSySQikQh27NiBUCiE5eVl8Hg8GAwGSKVS1NTUwO/3Q6FQYGpqCmKxGAKBgC5ozDAzlUoRmpROp4n3ZLFYMDAwgJKSEni9XkLeysrK4HA40NraitHRUcp7Y+qv1tZWWkQYH6SoqAh8Pp925VNTU+BwOODxeCgtLUVjYyMMBgOsViteeeUVlJWVYXl5Gc899xzZHgB3xmmsYcxkMti+fTvFlbDzTKvVQqFQQCKRIJ/Pw+/3Y2lpiUja27dvBwDMzc1RA8jS5QUCAex2O4xGI3K5HCF3MpkMk5OTkMvlyOVyRO5mSj3W0KjVakSjUeh0ug2+WIXGhFKpFOl0GiUlJTSKYuev0+lELpej5lalUlHzXFRURDwx4M5iF4vFEA6HiQDNxnPstysQCGAymbBjxw5ykVcoFHA6nVCpVOByuRAKhdDpdLBarfj4449RU1ODtbU1vPjii/B4PODxePSestksPvzwQ7S0tKC/vx/xeBzbt29HNpvF8PAwiouLEQ6H8eijjwK4M2a8cOECvF4vZduxRmord+rCZHeG6up0OiQSCSwsLOD69evQ6/UIBoPYt28fqqurweVyEYvFIBQKCbll5xYbkbndbqRSKZjNZvB4PAp1ZagaG1fGYjH6ThnyxRzhL126BI1Gc1cgcaGRqt/vh9FoJCNYhUKBK1euYPfu3SgvLydO45kzZ/DYY48R1y0ej1ODyK6dhcWOz+/33zV29Xg8SKVS4HK5dI1jNTs7C7VaDb/fD4vFAgAU/1NSUoJgMIju7m54PB4IBAIsLy9DJpORqIU1lVarFSUlJXC5XOjp6dlg+Ol2uyEQCLC0tISysjLE43HodDryg2NoGLOHYL8v1tAUNnTBYJDOE5lMRiKeQCAAgUCA2dlZGAwGOBwOlJeXI5FI0GhUKBRidXUVRqMRo6OjMJlMiEQiqKyshMFgIGrE9PQ0cQAFAgE4HA6MRiPW19dhNptht9tJFW232yGTyRCLxahJY58/Myd+ODL7lOqz0BABwHe+8x1qUFgZDAZKa2djDEbS/SQlk8mg0Whgs9noNh6PR8qwwtLr9WSaCID8kRjhM51O04nIHKY5HA4++ugjcoJub2+H1+ulXTYjVwN3kK+Wlhbw+XwaiQ0PDwO4c+Fua2sDj8eDWq2GVCrFtWvXaOdiMBig0WgQjUaJT8VQgaKiIhgMBiwuLiIWixF522q1wmKxEG8nGo3i+PHjlPI9Pj6Ojz76CAaDAbW1tSguLsbY2BgAoKKigjgIg4ODWF5eJq+k0tJStLS0YGVlBTt37kQ2m8Xk5CRJ6ouKitDd3Y1cLgeVSoXFxUWkUim6ADFjyXw+j+LiYtjtdni9XoTDYVrIlUolnE4nzGYzhoeHYTKZUF1dDR6PhytXrqC6upp27VeuXEFtbS1GR0exa9cuiMVicLlcSnD3+XzIZrPo6emh5iudTpNkv7q6mvLsstksRkdHcfHiRTzxxBPU2LDdaDKZxOXLl7G0tISWlha0tLRALpeTEpGdc4XjLbFYDKfTiaWlJTQ3N0OtVgMA+V1pNBqoVCpIJJK7ECKFQkEeWuwzYSqkXC5HxpTsXHA4HIQQsdcOBoMUY1J4rt8LIVpfX78nQlRSUnIXQsSQntraWsRisU+MEJWVlYHL5W5AiL7whS9QM+bz+chvTKfTIR6P49q1a3C5XGhubsbY2BiOHz8Ol8uFyclJHDx4EBUVFQCwJULEFrCt3KlZw5DNZklJyH4vmUwGfX19mJubQ3NzM9ra2uD1eumcHRsbg0ajIRUkc8Vmo9hEIgGFQgG9Xo/19XUkk0kibrNj8fv9kMvl9FvgcrmQSqU4d+4cNVLFxcUbYjxYE8eu4wsLC0in0ygrK8PLL7+M5uZmWK1WPPHEE5BKpfj7v/979PT0YGBgAM8//zz8fj8WFxfR0NCAhYUFHDly5CFC9FuAEDGRycOG6FOqz0pD9Morr2xoThQKBbRaLQQCAVKpFPx+P411JicnaTdR2Bjk83kaHbETu6ioCB0dHbBarXSRUCqVaGpqoh9+X18flpaW0NDQQMqo9fV1pFIp2mksLy+jo6MDN2/epLT22tpadHR0wG63w+/3w+VyobS0FEVFRRgaGiL/Ip1OR92/xWKBRCKhi/Xg4CCmpqbIIdtisWDnzp1kARCLxTAwMACz2UyjIqbMYtlpLIA1mUwST4VJ/dPpNHw+HywWC+3iCy/C0WgUkUgEgUAAHo8HlZWVEIvFJD92uVywWq3Q6/UIhUK4ceMGeSoJhUJUVFRArVaTkoctlDabbYOLcKF5IlO3sWah8ILFLijsYsB2zG63m3g57Jh5PB6KiopoFDE/P4+ysjIatxUWyxFbW1sjuwT2moVEYqFQiKmpKTQ2Nt4V4VG4IDCvJOCOc3rhgsZ2cg/Km7nfQnO/ehBuyS/63L/JisfjOH/+PFKpFI4fPw6JRELydhaTw77fQCCAn/70p9i7dy8aGxvvSQre6rO6H8fpft8ley6GBlVVVVEYdCQSodGWQqGA1+ul0WwsFkNxcTHkcjk9njVIDGkpPE/FYjGkUilZSSQSCXKkvx9CtLa2BovFAqfTSYHW58+fx4EDB1BZWQkulwu3240f/ehHeO6551BUVESN2/DwMHbv3n1Pu4OH9dmqhxyiz2mdPHkSyWQS8/PzAO7sKioqKuB2uyl7inFkuru7MT8/T87RrItXq9VoamrC8PAwcSrS6TQlvGcyGZLSstcIh8N45JFHMDQ0hEQigf7+flIpmUwmJBIJyhgbGxtDV1cXPB4PpqamYLfbkU6naQzFVEwsOPL8+fNQq9WIRCIQCATwer2UvB6Px3H79m3I5XIYjUYsLy8TYjQ2Nob6+nqkUikEg0HijjQ3N8Pn86G0tBTz8/OUKaZWqzE9PY09e/YgEAgQTCsQCMDn80nNxC7k4XAYr7/+Ovbs2YPS0lL4fD7cunWLMpe2b98OsVgMm82GlZUV8Hg8uN1uMoyz2+1IJpM0BqurqwOHw6FIlYmJCZSVlWF0dJR2sQypYPwoj8dDu59gMEiNDyODKhQKysNi7t0zMzNobW2Fy+XC+fPn0draSqMb5uztcDjo+y0sxhdjGXfAz5potmuUSqUYHR1FcXExJicn0dbWtmERZbtI1qhGo1GCvfV6PS1KKpWKFiLgbhKr3W6HwWCA3W6HxWKh12CfUSFiwXaUWy3azDfnQSoWixFviyEaWy3yP49svbmxYsjgpUuX8OUvf5nQJXZftpizYqomiURyF5LA6sqVKwiHwxAIBDh//jx27NhBIx02qmal0WjwwgsvkJM0h8OBRqOBw+Eg/lUsFqPfPts0yWQy+Hw+KJVK2Gw2lJaW0jVDKpUim83CZrNBKBQS2sg+a4b8KpVKKBQKzM3N0WsZDAbweDwiZjMxiFKppDELAEIMGZexcMPAjpONatjtXq8XRUVFqKiogEAgoMa6qKgIs7OzKC8vh0QigUgkwvLyMsxmM/HImF9VIpGARqOB1WpFMpnE6uoqbdQYR5E9F5fLpVH+2toaNBoNoSAMpWONXTAYRDqdpjE2Q0BDoRAmJiaQyWTQ09MDoVBIRpRM2avVagnNZGgJy5BcXl5GPp9Hc3MzgsEg4vE4LBYL2RsYjUa6bjFUSa/Xbxj9h0Ih1NTUkEKQjf89Hg+Ff2+lsMvn80QDYOcx20ixc5ehWizjkX2XgUAAarWartFisRipVIrQQ6YqZI0328SyLECG5bDrApuMMFHL/TZc96qHCNED1GcFIQoEAvjrv/7rDQnwdXV1kMlkWFpa2sAVKisrg0QiQTAYRCwWw/r6Okmf+Xw+AoEAKdEMBgMpqJgknKk8ysvL4fV6UVtbi0gkgpmZGbrgMwifNTzsuQDg3LlzJJ1uaGiA2WxGOp0mFVY6ncaZM2cA3FHQsde2WCyw2Wx46qmnyKbf7XZjfX0dmUyGYNknnngC6XQa2Wx2g4mb0WhEb28vFhcX6XGs+WlsbITNZsOhQ4dogebxeDSaYE2H2+3G66+/jvLycthsNhw8eBBer5fGlcXFxThy5AhdTILBIJaWlmA2m1FcXEwcGL/fj8HBQeh0OhgMBjKOBO5EsczPzxO5GfjZgspiRbhcLrRaLYWvBgIByq4rJI4yqNzhcJAK63vf+x6KioqwurqKw4cPE0r089ASdvEqXMw3y8gZIbiyspKI/JsTytkFmI0IZDIZdDodvYdgMAiLxUIXrM2SdcYLYPydzenwwM8k84xzcD/p+v1q82vf6/l+XvBqNBpFNBol7luhj82bb76JhoYGzM/P48UXX0QkEqFRHuO4sbJareByuWQ7sNV7KkSIuru7wefziUPD4/FobFB4bGyh02q1sFqtqK2thcfjoVEWi3jJZrMb+F02m23D74NxiZiSkG3C2HcFgLh7a2tr4PF4EAgEyOVyKCkpAQBa0CKRCKHJWq0Wc3Nz4HK5NO4xmUxIp9PEH8pmsxuaDJZlxlCfws+guroaa2trKC0txcDAAOrr60m4UVFRAZ/PBx6Ph7W1NQwPD6OsrAxWq5W4VmzjZ7Va8eyzz5K1BaMlGI1GKJVKap5KSkrgdrshFouh0WgQDofJSsTj8YDD4WBhYYF8wLZt2wYAGB8fJ+RKJBKho6MD0WgUXq8XoVCIEgYY1y6bzYLH4xEqyMZbmUwGxcXFxJVj5q1ra2vkE8ec6oVCIY1n2fiMjXqZwpQ1u+FwGJWVlVt6MK2vr2N+fp6MLRnqbjabiSfFJhVMmJFIJIhTxLhyuVyOjt/j8dB4ncPhgMPh0DiYx+NBoVBQI8WutaxJFggEWF9fR0VFBZ03nxvZ/Z/8yZ/g9ddfJ3LV7t278Wd/9meor6+n+xw4cACXLl3a8LhvfOMb+M53vkP/tlqt+OY3v4mLFy9CLpfjxRdfxJ/8yZ88cGjkZ6Uh+r//9/9CLpdjZmYGwM9cpouLizeYMrJjrauro8wrdoFlJ0oikYDdbodUKqVmKBQKUZAnu1gtLy/jmWeegUgkwuLiIpaXlzE/Pw+j0Yjy8nJUVlbC6/VibW0NVVVV8Pv9tLtgBow1NTWIRCJoaGgg3oZIJMLKygqRiI1GI86dO4dwOIxjx44hl8vB7/djZmYG1dXVCIfDNJt+4oknyD/o6tWrtBvl8Xg4ffo0tFotJiYmyBuIXVgnJydx6NAh2g0qlUr4fD5UVlaSwmV9fR2Li4tIp9O4cOECWltb0dLSgmQyieXlZeRyOezbt28DOdbn89Guhs3JAdDzMcSNLbabSZeFCIxYLKZdvFarpV0RI/2urq6Cw+GgtLQUyWSSnkssFm9AGoLBIF5++eUNTsOFKE8hH4YtcqzhYDvPyclJQgqYWSJbuAsVOYVN1lYoyvr6OkQiEcnptxpL3U/mvNVzFt7GECL2OX3Skdfm174X4vTzECK32w2hUEhE18Kmcnx8HBcvXsTzzz9P3JhfBiEqLLYYsf+fTCbv2s2zXTZDkbZCiEQiESn82Iiq8DtlxN1ChGh+fn6Dmo/9LmKxGG00/H4/7dwZoqJSqUhIwUKEE4kEKQQ1Gg01lWynX7hhALABKYnH44QQhcNhQog4HA6pL6empqDVaqFSqbC2toaKigosLS1BLpfDZrNhdnYWjY2NCIVCaGlpgcPhwKVLl3D48GEShkQiEYyNjUGtVj9EiHA3QpTP5ykCRqlUbkBc2TVAo9EQz1KhUBBNgTXazEWcqfzi8ThSqRRFKHV2dpK4gVELotEoFhYWIBKJUFJSQptGLpf7+WmIjh8/jmeffRbd3d3IZDL4L//lv2B8fJxiGIA7DVFdXR3+5//8n/Q4qVRKbzybzaKjowNGoxF/8Rd/AYfDga9+9av4+te/jm9/+9sPdByflYYoEAjgJz/5CaRSKZaWlmgnwiDmwtq3bx80Gg0EAgEmJycp8yyTyUCr1WJ1dZXUZslkkpRRbPxTV1dHIznmuRMKhTA7O0t+PWzXkMlkEAwGMT4+jmQyST8EZtLo8/lQV1eHeDyOhoYGBIPBDcqGfD6PS5cuIRqNUoPW1NQEp9NJF8ndu3djfHwcLS0tmJ+fR0NDAyYnJyGVSjE5OQmxWAyTyUQ+RAaDAX19fZDL5YQ6aTQaTE1NobKyEiaTCTMzM6ivryf1EgBKeQ8EAkTELtz1sffGqtAYUKlUkhsyW5zZWIjtnthFyWQykYKIcYpKSkqwtLREUnOpVLoBafB4PAiHw8Qfqq6uJqVFKpXagDR4PB6EQiEas1VXV9+lfCkkmQIg3pNcLsfCwgIMBgPcbje9781cjgetB+HxfBr18xCcX3Wl02lMTU2hvLz8rlBc1iylUinio/wqPpMHMXr8VdVW5Out6l68pAd9/Cc5nrGxMRoT19bW3sV52uxYPTo6ivLycqysrKCtre2Xev0HqUJEdvNG4F7+U5/lKlRhFqozgY0KPHb9Z3zIUCgElUp1F2LKHrOwsECbwnA4jPb29g2vwRSmjBtZ+Pv/3DREm8vj8aCoqAiXLl3Cvn37ANxpiDo6OvC//tf/2vIx77//Ph599FGsra2huLgYwB211h/+4R/C4/FsIN3dqz4rDRFwB+1699134Xa7AdxBicRiMS1qUqkUKpUKx48fh0wmo9ERm70y/oVAIEAymSSisUAgwO3btzE3NwedTkcS4FQqRWZ7LILi+vXrWFxchEgkwpEjRwCAzPj8fj+4XC4OHTpE6fWlpaVYXFwkqWdtbS1mZ2dRVFSERCKBZDIJr9dLydqnT59GLpfDysoKKbA8Hg9B9tu2bYPdbicHakZ85nK5qKurQ0VFBWw2G+RyOVwuFzUda2trkMvlUKlUkMvlpOCRSCQbUBPmnRMIBAjxYcqV2tpa2m0xUjoAaorYTo6hINlsli64iUQCkUiEOA9s980WSh6PB6PRiJmZGVgsFhoHsAslg+xXV1fJd4YhU5uVK5lMhnbvZWVlZNxZaHDI5XJJrcVUiSzWIhgMYnh4GB0dHVAoFFhcXCQFDLtox2IxXLx4EQcPHrzL7+PX1QT9pl+zsKxW6z0vymycJpfLIZfLfyXN24MS1v811eaG5+cVa2q3Egz8KoqNkxiquHl0WuhQ/ttQhSrMzWjtZo5dPp+n0V7hSLywMWSPYeg484kLhUIbXoMpTJlPVOHv/5Os379Vvxi26DMuDKsf/vCH0Ov1aGlpwR/90R8RxwC440ja2tpKzRAAHDt2DOFw+C45OatkMolwOLzhv89KlZSUIBqN0o+1vLwcKpWK/h2LxUhlxeBjNkfNZDKE2jBOTjqdRmVlJWQyGYaHhxEOhxEMBsHn84k3wjxzmESbefy43W5cvHgRRUVFqK+vR1NTEyoqKvD4449DrVaDz+eTRL6lpYWeh6FTjGsRiURw/vx5GAwGtLa2kmyeBVhyOBxkMhnasUUiEVy7do123Yx0KZfLiRQ5Pj6Oubk5+Hw+vPvuu+QLxLg3LNONNYmM3MlIn+l0mi6iLFLDYDAQJ4ZB2kyqzxyZ2RiAXdhYgCTja7G4k3A4DIVCQSaOzBHc6XSioaGBpKJ2ux3RaHTD2LOqqop8S3w+H0HP4+PjyGQyAECWBi6Xi86dfD6PpaUlxONxzM/P03tlkmWWa8dy1dra2uB2u7G4uIjy8nIkk8kNF5qLFy+itbUVFy9evOs8ZaPBwt/ir7oKCZ0PWoWhnr9ssd8mc9kuLJlMtgFdY9yHexnHFY64HrTYYsPsN1itrKzgj//4j/H666/f08H+81p8Pv++8SSbi9l6/DqaIQDQ6XRIpVIb8vVYlZSUwOPxEO/qt6EKSfObf4vs98nWFblcDqVSiaKiIvrfzc0Mewyfz0dlZSUaGxtJQVn4GsyPrnDD9gsd/y/17n+Nlcvl8B/+w3/Anj170NLSQrc/99xz+MEPfoCLFy/ij/7oj/D9738fX/nKV+jvbBRUWOzfmz19WP3Jn/wJVCoV/ceMsj4LxePx8Hu/93tQqVSorq6GRqNBZWXlhhMhEomAy+VicHAQHA4HLpcL1dXVKC0tRXl5OfR6PVKpFKXYRyIRvPLKK9DpdADunITsM9rqgl1SUkIkyfLycng8HlKwdHZ20phMqVQiGAxS184WhNLSUlKfBINBnDt3jlRLUqkUIyMjJJsfHx+HzWZDdXU1ZmZm0NjYiPfffx9VVVW4fv06IpEIBgcHYbVaye32woULUCgUuHXrFvr6+mAymfDOO++goqKCzBSZc7FUKiVlAyPxicViaLVauhgFAgFYLBa4XC5a5BnKw8z8pFIpHA4HiouL4fP5ANxZoJg7OPscmZGcTqcjIjbjJqjVavLWMZvNFJvg8/lo1s98k0pLS+FwOChLbmBgAJWVlZRP53K5SL2xuLhIpENmwRAMBqkJYOgT27WxUR1TgzEjRnZ+sDp48CDGxsbIpK6wft6C/1mpQsXbL1v3uyhvbtZ+XvP2izSU9/rMf/CDH0Cr1WJ0dBRDQ0Of8F09rF9lcbncLRsB4M75xOwqHtavp35rRmbf/OY38f777+Pq1atbSoZZXbhwAY888gjm5+dRXV2N3/u938PKysqGyAsmrz1z5gxOnDhx13Mkk8kNOymW2P6bHpkxh9CRkRFyHWWjDI1Gg5mZGej1euj1eprlMhi2ubl5w4gnnU4TSqDX6yEWi/Haa6+RR5BOpyPiIIPhGUkwnU5jbGyMeDWZTAaRSIRUHE1NTZidnSV4k/EpCuFUNqLJZDIYGBhAX18fNBoNDh48iIaGBqysrKC/v5+cqZmKJZvNIhwO49y5c+ju7sbS0hLy+Tz5lvT09CAajWJwcJBUKKyx6e7u3gBLM64CcG+SMPCzOT8bnxXOtwvHE5u5EZtHOIWk0LW1NZSXl9N4ZTOXwOv1Qi6XY25uDpWVlZDL5fB6vcT5kUqlRM5lrrsrKyuEaqXTaUxMTEAgEKC+vh6BQIAy7lZXV+l8MBgMRHosDNZk59tWgaSfp3rQdPdfd32a47+VlRV897vfRWtrK06dOkW+Vw/r81u/6fHxZ6k+dxyib33rW3jrrbdw+fJlVFZW3ve+6+vrkMvl+OCDD3Ds2DH8t//23/D222+T0zEAMgq7desWOjs7f+7rf1Y4RCws0W63I5FIwOPxwGAwkIlgZWUlNBoNMfeXlpaQTCZRX19PzQFTJDF+DpO7hsNhdHR0YHBwEMXFxYjFYjCZTCgqKkI0GoVIJEIwGCTvD/Y5MPk6k/c3NjZiaWmJZJRKpZLm30xttLa2BrVaTfb7k5OTxGvS6/XYu3cvgDukwuXlZbJ1ZyZuzBlXKpUiFAphcnISyWQSLS0tiMfjEIlENDbk8/mw2+1ErNy5cyelxv+mCLjMWI75AW21EG+1UBeqwrbaUd6vCtVwLFNrx44dd5lC/qovoEyVMjs7S+67v85im4qJiQl0d3f/WpqDTCaDubk5JBIJtLS0/ErGMYlEAn19fWhvbyel0y9arBFm6siHCMVvX/2mBQafpfrccIjy+Ty+9a1v4Y033sCFCxd+bjME/CziwWQyAQB27dqFsbExIiEDdzxymBPzb1NJpVK0tLSQdHTnzp3gcDiora3Frl27UF5eTiGacrkcGo0GFRUVkMvlWF1dxT/8wz9gbGwMfr8fxcXFyOfzlFu1fft2nD9/HiaTicZBJpMJUqkU0WgUf/d3f0fGhMlkksIq7XY73n33XchkMvLAMZlM6O/vx4cffgg+n4+LFy+Scdb8/DyKiooQCAQob6aurg5SqRR8Pp/yq4CfzaMLSb8s6mF1dRU//elPEY1GkUwmaQRYWlpKHCC1Wo1QKIS9e/ciEomgs7OTUBWJRPJrHekUckJY9ITf70c2mwVwp/mzWq3070J+z+bPo3Akw8zkGM/oXsUeGwgE4HQ6UVpaSvYNwC/Gv9l8zA9SsViMgnJHRkZ+pa91r9cfHx9HWVnZr218tLi4SEaXU1NTv5LX6OvrQ3l5OW7fvv1L87bW19dhs9nIIPNh/fbVb8vI+rNWn2mE6N/+23+LH/3oR3jrrbc2eA+xPKOFhQX86Ec/wsmTJ6HT6TA6Oor/+B//I0pLS8mbiMnuzWYz/vzP/xxOpxMvvPACfvd3f/e3TnYPgBw+BwcHkc1m0dLSgmw2S/4nDJkRCARQq9Xwer0QCoV45ZVXSA65Z88eChZlC+XY2BiZyKXTaezcuZPcp//iL/4COp0ODocDvb29qK+vJ0Ou1dVV4iQdO3YMPB4P165dIw8bp9OJ5557Djdv3kRnZyc1BoVEY5FIhIWFBRQVFSGdTpMEniVts3GbwWCAwWDAysoKLl++jNLSUkxOTsJkMpFcs6enBzKZDLOzs+S6nEqlsGfPHnrO38TOqfB1gTuBpUajEW63G21tbb+womSzSqXQ0HCrnT1roP7/9u49KMrqjQP4d0V2AZfbAnJR7piGAiXIyk9TSwytKTCmzLLAjMywUrIcmgydmsF0xuzi5B9NZhftYqmVFRUJqZGlRqYpympBIaCbwHI39vz+cPYdVlYWFPddfb+fGWbcd19enj0e2GfPe855amtrMXr0aAwePPiSR4YuJeZLHSEaqBU3HCGyr3uRYkuRVd56oavVNXPL7GK/gBs2bEB2djaqq6sxZ84cHDp0SNpafebMmXjuueesXvhff/2FBQsWoKSkBEOGDEFWVhZWrlx51W3MaJnrYZlobNkF1VKB3lKpOjAwUFpKbik8aTAY8PXXX+OGG27AzTffDBcXF1RWVsLHxwdarRaurq7YtWuXVN3baDRi4sSJ0g6jGzduREJCgrQM+/Tp0zh37pxUTiMpKQn+/v7Szra//PILjEYjMjIy8Mcff0Cv10sr2KKjo9HZ2Sl9ejlz5gxcXV1x9uxZBAQEWBWVNJlM+Oeff+Dr64uhQ4dKxS2rq6tx4MABTJkyBcePH5d27LXsYKpWq1FRUYF///0Xer3e4beGbP3fdZ+ndOGb8qXuOXLh3KPq6up+JQ6XkyD2J2aTySRNbB83bly/k4L+Lp9WqoHo3903qrR8WVb00NVNiXOLrpmEyFk4S0JkmXvyyy+/oLq6Gm1tbUhKSsLRo0elHVdNJhPGjx+PkJAQbN++XSpEOnLkSEREREAIgRMnTsDLy0uqLdPR0QGNRoO6ujr4+PigqakJo0ePtnqTs8xBcXd3l1Ynda8n4+fnh46ODmnZ/9mzZ6XllQcOHMCkSZPQ3Nxsc9M4y6hF94KqvbG1S7FlXo1ldY4ltoHa5G2gWYpKWoraDpT+JlaO+gP5wQcfSPOXEhIS+r3p3YX7+JBtAzkCOtAbJZL8lDi36JqZQ0TWLBvmDR06FNHR0Zg4caI00dnT01NaHt7Q0IAvv/wS/v7+OHjwIM6ePYvKykr8888/OHHiBEJCQtDQ0ICWlhbs3r0bO3bswMmTJ6XK1LYqYlturVn2JbJsWT948GDU19ejuLgYLi4u8PPzQ2NjIzo6OjB48GDs3r0bMTEx2LVrl/Tchcu329ra0NzcjCNHjmDbtm3SfKGL6T7fxWg0Srd9LG/slmTIUvfHkXvh9FVDQwO0Wq1V/blL1X1+Un+X6l7K3KFLcfvtt8NoNCI6OhrXX399v79fjn2NrkYDOXdEpVJJqzKZDF0bOLeodxwh6gNnGSECrOeAWEpotLa2oqGhAa6urujs7MTo0aPh6emJd999F2FhYRgyZAhGjRoljRAZDAYEBwfj4MGDOHz4MDw8PGAymTB27FipVIdOp7vobrdCCPz1118oLS2V9iuyFEucMGGCNAfBsjzcMkLk7u5u81r19fX49ddfceTIETQ2NiIyMhIPPvhgn/4IX2zl1ZX4dDuQoyltbW3YtWsXbrrpJpvt0h9yfepz5PD7tXbLTIm3Luji2B+uHN4yG2DOlBC1tLTAZDLBzc0Nhw8fRlRUFOrr6xETEyPVp7LEaqkCfeEbpeUNtKmpCfv27ZOqy6tUKtTW1kqFKS3Vgm3FsH37dvj7+6OmpgZxcXFoa2uT5sP0VqjT1rX+++8/HDt2DDt27IBGo4FGo8H8+fOdbkh3IBOPgayZJNcfU0cmYnLW6LoSlHjrgi6O/eHKYUI0wJwpIeper8jb2xsVFRVWxSS7719jWT5/scritt5A+1IPyTKq89133yExMREjRozocYumr7/gQgicPn0aXV1dqKurQ3FxMe6++26EhoY63SelgUw8HF0z6UpwZCLmrBsoXiqOCFB37A9XDhOiAeZMCdHVoj+/4Jy8SUREV0J/3r+v/pvx5JS6l7joy7lcOURERHK6+seeiYiIiC4TEyIiIiJSPCZEREREpHhMiIiIiEjxmBARERGR4jEhIiIiIsVjQkRERESKx4SIiIiIFI8JERERESkeEyIiIiJSPCZEREREpHhMiIiIiEjxmBARERGR4jEhIiIiIsVjQkRERESKx4SIiIiIFI8JERERESkeEyIiIiJSPCZEREREpHhMiIiIiEjxmBARERGR4jEhIiIiIsVjQkRERESKx4SIiIiIFI8JERERESkeEyIiIiJSPCZEREREpHhMiIiIiEjxmBARERGR4ikqIVq3bh0iIiLg5uYGvV6Pn3/+We6QiIiIyAkoJiH68MMPkZeXh4KCAhw4cAAJCQlIS0tDfX293KERERGRzBSTEK1ZswY5OTmYO3cuYmNjsX79enh4eOCtt96SOzQiIiKSmSISos7OTuzfvx+pqanSsUGDBiE1NRVlZWUyRkZERETOYLDcATjCmTNn0NXVhcDAQKvjgYGBOHr0aI/zOzo60NHRIT1ubGwEADQ1NV3ZQImIiGjAWN63hRB2z1VEQtRfhYWFWLFiRY/joaGhMkRDREREl8NkMsHb27vXcxSREPn7+8PFxQV1dXVWx+vq6hAUFNTj/Pz8fOTl5UmPGxoaEB4ejqqqKrsNqlRNTU0IDQ1FdXU1vLy85A7HKbGN7GMb2cc2so9tZJ9S2kgIAZPJhJCQELvnKiIhUqvVSExMRHFxMTIyMgAAZrMZxcXFWLhwYY/zNRoNNBpNj+Pe3t7XdMcZCF5eXmwjO9hG9rGN7GMb2cc2sk8JbdTXgQxFJEQAkJeXh6ysLCQlJSE5ORlr165FS0sL5s6dK3doREREJDPFJESzZs3C6dOn8fzzz6O2thY33HADvv766x4TrYmIiEh5FJMQAcDChQtt3iKzR6PRoKCgwOZtNDqPbWQf28g+tpF9bCP72Eb2sY16Uom+rEUjIiIiuoYpYmNGIiIiot4wISIiIiLFY0JEREREiseEiIiIiBSPCVEfrFu3DhEREXBzc4Ner8fPP/8sd0hOY/ny5VCpVFZfo0aNkjssWf3www+44447EBISApVKhW3btlk9L4TA888/j+DgYLi7uyM1NRXHjx+XJ1iZ2Guj7OzsHv1q+vTp8gQrg8LCQowbNw6enp4YOnQoMjIyUFFRYXVOe3s7cnNz4efnB61Wi8zMzB678V/L+tJGU6ZM6dGPHn30UZkidrw33ngD8fHx0uaLKSkp+Oqrr6Tnld6HLsSEyI4PP/wQeXl5KCgowIEDB5CQkIC0tDTU19fLHZrTGD16NE6dOiV97d69W+6QZNXS0oKEhASsW7fO5vOrVq3Cq6++ivXr12Pv3r0YMmQI0tLS0N7e7uBI5WOvjQBg+vTpVv1q8+bNDoxQXqWlpcjNzcVPP/2Eb7/9FufOncOtt96KlpYW6ZzFixfj888/x8cff4zS0lLU1NTgrrvukjFqx+pLGwFATk6OVT9atWqVTBE73vDhw7Fy5Urs378f+/btwy233IL09HQcPnwYAPtQD4J6lZycLHJzc6XHXV1dIiQkRBQWFsoYlfMoKCgQCQkJcofhtACIrVu3So/NZrMICgoSq1evlo41NDQIjUYjNm/eLEOE8ruwjYQQIisrS6Snp8sSjzOqr68XAERpaakQ4nyfcXV1FR9//LF0zpEjRwQAUVZWJleYsrqwjYQQYvLkyeLJJ5+ULygn5OvrK9588032IRs4QtSLzs5O7N+/H6mpqdKxQYMGITU1FWVlZTJG5lyOHz+OkJAQREVF4f7770dVVZXcITmtkydPora21qpPeXt7Q6/Xs09doKSkBEOHDsXIkSOxYMECGI1GuUOSTWNjIwBAp9MBAPbv349z585Z9aNRo0YhLCxMsf3owjayeP/99+Hv748xY8YgPz8fra2tcoQnu66uLnzwwQdoaWlBSkoK+5ANitqpur/OnDmDrq6uHuU9AgMDcfToUZmici56vR5vv/02Ro4ciVOnTmHFihW46aabcOjQIXh6esodntOpra0FAJt9yvIcnb9ddtdddyEyMhIGgwHPPvssZsyYgbKyMri4uMgdnkOZzWYsWrQIEyZMwJgxYwCc70dqtRo+Pj5W5yq1H9lqIwC47777EB4ejpCQEBw8eBBLly5FRUUFPv30Uxmjdazff/8dKSkpaG9vh1arxdatWxEbG4vy8nL2oQswIaLLMmPGDOnf8fHx0Ov1CA8Px0cffYR58+bJGBldze69917p33FxcYiPj0d0dDRKSkowdepUGSNzvNzcXBw6dEjxc/N6c7E2euSRR6R/x8XFITg4GFOnToXBYEB0dLSjw5TFyJEjUV5ejsbGRmzZsgVZWVkoLS2VOyynxFtmvfD394eLi0uPWfd1dXUICgqSKSrn5uPjg+uuuw6VlZVyh+KULP2Gfap/oqKi4O/vr7h+tXDhQnzxxRfYuXMnhg8fLh0PCgpCZ2cnGhoarM5XYj+6WBvZotfrAUBR/UitViMmJgaJiYkoLCxEQkICXnnlFfYhG5gQ9UKtViMxMRHFxcXSMbPZjOLiYqSkpMgYmfNqbm6GwWBAcHCw3KE4pcjISAQFBVn1qaamJuzdu5d9qhd///03jEajYvqVEAILFy7E1q1b8f333yMyMtLq+cTERLi6ulr1o4qKClRVVSmmH9lrI1vKy8sBQDH9yBaz2YyOjg72IRt4y8yOvLw8ZGVlISkpCcnJyVi7di1aWlowd+5cuUNzCkuWLMEdd9yB8PBw1NTUoKCgAC4uLpg9e7bcocmmubnZ6hPoyZMnUV5eDp1Oh7CwMCxatAgvvvgiRowYgcjISCxbtgwhISHIyMiQL2gH662NdDodVqxYgczMTAQFBcFgMOCZZ55BTEwM0tLSZIzacXJzc7Fp0yZs374dnp6e0pwOb29vuLu7w9vbG/PmzUNeXh50Oh28vLzw+OOPIyUlBePHj5c5esew10YGgwGbNm3CbbfdBj8/Pxw8eBCLFy/GpEmTEB8fL3P0jpGfn48ZM2YgLCwMJpMJmzZtQklJCYqKitiHbJF7mdvV4LXXXhNhYWFCrVaL5ORk8dNPP8kdktOYNWuWCA4OFmq1WgwbNkzMmjVLVFZWyh2WrHbu3CkA9PjKysoSQpxfer9s2TIRGBgoNBqNmDp1qqioqJA3aAfrrY1aW1vFrbfeKgICAoSrq6sIDw8XOTk5ora2Vu6wHcZW2wAQGzZskM5pa2sTjz32mPD19RUeHh5i5syZ4tSpU/IF7WD22qiqqkpMmjRJ6HQ6odFoRExMjHj66adFY2OjvIE70EMPPSTCw8OFWq0WAQEBYurUqeKbb76Rnld6H7qQSgghHJmAERERETkbziEiIiIixWNCRERERIrHhIiIiIgUjwkRERERKR4TIiIiIlI8JkRERESkeEyIiIiISPGYEBER9dGff/4JlUollYAgomsHEyIiktXp06exYMEChIWFQaPRICgoCGlpadizZ4+scWVnZ/copxIaGopTp05hzJgx8gRFRFcMa5kRkawyMzPR2dmJjRs3IioqCnV1dSguLobRaJQ7tB5cXFwUWwmc6FrHESIikk1DQwN27dqFl156CTfffDPCw8ORnJyM/Px83HnnnVbnzZ8/H4GBgXBzc8OYMWPwxRdfAACMRiNmz56NYcOGwcPDA3Fxcdi8ebPVz5kyZQqeeOIJPPPMM9DpdAgKCsLy5csvGtfy5cuxceNGbN++HSqVCiqVCiUlJT1umZWUlEClUqGoqAg33ngj3N3dccstt6C+vh5fffUVrr/+enh5eeG+++5Da2urdH2z2YzCwkJERkbC3d0dCQkJ2LJly8A1LBH1G0eIiEg2Wq0WWq0W27Ztw/jx46HRaHqcYzabMWPGDJhMJrz33nuIjo7GH3/8ARcXFwBAe3s7EhMTsXTpUnh5eWHHjh144IEHEB0djeTkZOk6GzduRF5eHvbu3YuysjJkZ2djwoQJmDZtWo+fuWTJEhw5cgRNTU3YsGEDAECn06Gmpsbm61i+fDlef/11eHh44J577sE999wDjUaDTZs2obm5GTNnzsRrr72GpUuXAgAKCwvx3nvvYf369RgxYgR++OEHzJkzBwEBAZg8efJltysRXQK5q8sSkbJt2bJF+Pr6Cjc3N/G///1P5Ofni99++016vqioSAwaNEhUVFT0+Zq33367eOqpp6THkydPFhMnTrQ6Z9y4cWLp0qUXvUZWVpZIT0+3Onby5EkBQPz6669CCCF27twpAIjvvvtOOqewsFAAEAaDQTo2f/58kZaWJoQQor29XXh4eIgff/zR6trz5s0Ts2fP7vNrJKKBxVtmRCSrzMxM1NTU4LPPPsP06dNRUlKCsWPH4u233wYAlJeXY/jw4bjuuutsfn9XVxdeeOEFxMXFQafTQavVoqioCFVVVVbnxcfHWz0ODg5GfX39gLyG7tcODAyEh4cHoqKirI5ZflZlZSVaW1sxbdo0aYRMq9XinXfegcFgGJB4iKj/eMuMiGTn5uaGadOmYdq0aVi2bBkefvhhFBQUIDs7G+7u7r1+7+rVq/HKK69g7dq1iIuLw5AhQ7Bo0SJ0dnZanefq6mr1WKVSwWw2D0j83a+tUql6/VnNzc0AgB07dmDYsGFW59m6ZUhEjsGEiIicTmxsLLZt2wbg/OjL33//jWPHjtkcJdqzZw/S09MxZ84cAOfnHB07dgyxsbGXFYNarUZXV9dlXcOW2NhYaDQaVFVVcb4QkRNhQkREsjEajbj77rvx0EMPIT4+Hp6enti3bx9WrVqF9PR0AMDkyZMxadIkZGZmYs2aNYiJicHRo0ehUqkwffp0jBgxAlu2bMGPP/4IX19frFmzBnV1dZedEEVERKCoqAgVFRXw8/ODt7f3QLxkeHp6YsmSJVi8eDHMZjMmTpyIxsZG7NmzB15eXsjKyhqQn0NE/cOEiIhko9Vqodfr8fLLL8NgMODcuXMIDQ1FTk4Onn32Wem8Tz75BEuWLMHs2bPR0tKCmJgYrFy5EgDw3HPP4cSJE0hLS4OHhwceeeQRZGRkoLGx8bJiy8nJQUlJCZKSktDc3IydO3ciIiLisq5p8cILLyAgIACFhYU4ceIEfHx8MHbsWKvXTESOpRJCCLmDICIiIpITV5kRERGR4jEhIiIiIsVjQkRERESKx4SIiIiIFI8JERERESkeEyIiIiJSPCZEREREpHhMiIiIiEjxmBARERGR4jEhIiIiIsVjQkRERESKx4SIiIiIFO//PuAfK9ytmiUAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "df = lcms_collection.mass_features_dataframe.copy()\n", + "## generate mass feature figure\n", + "fig = plt.figure()\n", + "plt.scatter(\n", + " df.scan_time_aligned, #switched to aligned b/c aligned is the only scan time in the summary\n", + " df.mz,\n", + " c = 'tab:gray',\n", + " alpha = 0.75, ## ask katherine about how we want this to look\n", + " s = 0.005 ## ask katherine about how we want this to look\n", + ")\n", + "\n", + "#plt.legend(loc = 'lower center', bbox_to_anchor = (0.5, -0.25), ncol = 2)\n", + "plt.xlabel('Scan time')\n", + "plt.ylabel('m/z')\n", + "plt.ylim(0, np.ceil(np.max(df.mz)))\n", + "plt.xlim(0, np.ceil(np.max(df.scan_time)))\n", + "plt.title('All mass features, all samples')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f8de4ea2-7614-43b1-b5ea-e42712847e81", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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icuXL2P8+PE4efIk+vfvL/fpISIiok+URqdReXvxNfDqmEtvFlHqOHnyJOrXry9df13IBAYGYtmyZRgxYgQyMjLQq1cvPHnyBLVr10ZERITKDNeqVavQv39/NGzYEAYGBmjTpg3mzZsntdvY2GD37t3o168ffHx8ULhwYYSGhqocy6lmzZpYvXo1xowZg59++gleXl7YvHkzypcvL+vxEBER0adL9nGa6tSpg7Zt22LAgAGwsrLC2bNn4enpiQEDBuDatWuIiIjQVVa99r7jPPA4TURERPpJznGaZM80TZkyBU2aNMHFixfx8uVLzJ07FxcvXsSRI0dw4MABjUMTERER6TPZa5pq166NuLg4vHz5Et7e3ti9ezccHR0RExMDHx8fXWQkIiIiKnBqzTQNGTIEkyZNgoWFBaKjo1GzZk0sXrxY19mIiIiI9IZaM03z589Heno6AKB+/fpISUnRaSgiIiIifaPWTJOHhwfmzZuHRo0aQQiBmJgY6RhHb6tTp45WAxIRERHpA7WKphkzZqBPnz6YOnUqFAoFWrVqlWc/hUKBnJwcrQYkIiIi0gdqFU0BAQEICAhAeno6rK2tceXKlTyP1URERET0qZJ1yAFLS0vs27cPnp6eMDLS+Fy/RERERB8d2ZVP3bp1dZGDiIiISK/JPk4TERER0eeIRRMRERGRGlg0EREREakh30VTWloaNm/ejEuXLmkjDxEREZFekl00tWvXDr/++isA4Pnz56hatSratWuHChUqYMOGDVoPSERERKQPZBdN0dHR+PrrrwEAmzZtghACT548wbx58zB58mStByQiIiLSB7KLptTUVNjb2wMAIiIi0KZNGxQqVAjNmjXDtWvXtB6QiIiISB/ILprc3NwQExODjIwMREREoFGjRgCAx48fw8zMTOsBiYiIiPSB7INbhoSEoFOnTrC0tIS7uzvq1asH4NVuO29vb23nIyIiItILsoumvn37onr16rh9+za++eYbGBi8mqz68ssvuaaJiIiIPlkanUCuatWqqFq1KgAgJycH586dQ82aNWFnZ6fVcERERET6QvaappCQEPz5558AXhVMdevWRZUqVeDm5ob9+/drOx8RERGRXpBdNP3zzz+oWLEiAGDr1q2Ij4/H5cuXMXjwYIwePVrrAYmIiIj0geyi6eHDh3B2dgYA7NixA23btkXJkiUR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EhKCyZMnw8vLC56enhg7dixcXV0REBAAAChTpgwaN26Mnj17Ijw8HNnZ2ejfvz/at28PV1dXAEDHjh0xYcIEBAcH48cff8T58+cxd+5crexiJCIiok+D7KKpS5cuusiRp/nz52Ps2LHo27cv7t+/D1dXV/Tu3RuhoaFSnxEjRiAjIwO9evXCkydPULt2bURERKgUdatWrUL//v3RsGFDGBgYoE2bNpg3b57UbmNjg927d6Nfv37w8fFB4cKFERoaysMNEBERkUQh3jy8thoyMjIQFhaGqKgo3L9/X9pV9tqNGze0GvBjkZaWBhsbG6SmpqrsqgQAj5Hb1R7nZlgzbUcjIiKid3jf+/fbZM809ejRAwcOHECXLl3g4uIChUKhcVAiIiKij4Xsomnnzp3Yvn07atWqpYs8RERERHpJ9rfn7OzsYG9vr4ssRERERHpLdtE0adIkhIaG4tmzZ7rIQ0RERKSXZO+emzlzJq5fvw4nJyd4eHjA2NhYpf306dNaC0dERESkL2QXTa+Pf0RERET0OZFdNI0bN04XOYiIiIj0Wr5O2EtERET0uZA905STk4PZs2dj3bp1SEhIQFZWlkp7SkqK1sIRERER6QvZM00TJkzArFmz8P333yM1NRVDhgxB69atYWBggPHjx+sgIhEREVHBk100rVq1CosXL8bQoUNhZGSEDh064I8//kBoaCiOHj2qi4xEREREBU520ZSUlARvb28AgKWlJVJTUwEA3377LbZvV/8ca0REREQfE9lFU9GiRZGYmAgAKF68OHbv3g0AOHHiBExNTbWbjoiIiEhPyC6aWrVqhaioKADAgAEDMHbsWHh5eaFr164ICgrSekAiIiIifSD723NhYWHSz99//z2KFSuGmJgYeHl5oXnz5loNR0RERKQvZBdNb/P19YWvr682shARERHpLY2Kpnv37uHQoUO4f/8+lEqlStvAgQO1EoyIiIhIn8gumpYtW4bevXvDxMQEDg4OUCgUUptCoWDRRERERJ8k2UXT2LFjERoailGjRsHAgGdhISIios+D7Krn2bNnaN++PQsmIiIi+qzIrnyCg4Oxfv16XWQhIiIi0luyd89NnToV3377LSIiIuDt7Q1jY2OV9lmzZmktHBEREZG+0Kho2rVrF0qVKgUAuRaCExEREX2KZBdNM2fOxJIlS9CtWzcdxCEiIiLS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c61v8hi5Xe1xboY103Y0IiIieof3vX+/TfbuueTkZHTp0gWurq4wMjKCoaGhyoWIiIjoUyR791y3bt2QkJCAsWPHwsXF5YN9k46IiIioIMkumg4dOoSDBw+iUqVKOohDREREpJ9k755zc3MDl0ERERHR50Z20TRnzhyMHDkSN2/e1EEcIiIiIv2k1u45Ozs7lbVLGRkZKF68OAoVKgRjY2OVvikpKdpNSERERKQH1Cqa5syZo+MYRERERPpNraKpoE+OS0RERFTQZK9pMjQ0zHUCXAB49OgRj9NEREREnyzZRdO7vjmXmZkJExOTfAciIiIi0kdqH6fp9bnaFAoF/vjjD1haWkptOTk5iI6ORunSpbWfkIiIiEgPqF00zZ49G8Crmabw8HCVXXEmJibw8PBAeHi49hMSERER6QG1i6b4+HgAQP369bFx40bY2dnpLBQRERGRvpF9GpV9+/bpIgcRERGRXlOraBoyZAgmTZoECwsLDBky5L19Z82apZVgRERERPpEraIpNjYW2dnZ0s/v8uZRw4mIiIg+JWoVTW/ukuPuOSIiIvocyT5O0969e5GZmamLLERERER6S/ZC8BYtWuDly5eoVq0a6tWrh7p166JWrVowNzfXRT4iIiIivSB7punx48eIiopCkyZNcPz4cbRq1Qq2traoVasWxowZo4uMRERERAVOId51XhQ1XbhwATNmzMCqVaugVCqRk5OjrWwflbS0NNjY2CA1NRXW1tYqbR4jt6s9zs2wZtqORkRERO/wvvfvt8nePXf16lXs378f+/fvx4EDB5CZmYmvv/4av/zyC+rVq6dpZiIiIiK9JrtoKl26NIoUKYJBgwZh5MiR8Pb25qEGiIiI6JMne03TwIED8cUXX2DixIno06cPRo8ejd27d+PZs2e6yEdERESkF2QXTXPmzMHp06eRlJSEUaNGISsrC6NHj0bhwoVRq1YtXWQkIiIiKnCyi6bXcnJykJ2djczMTLx48QKZmZm4cuWKNrMRERER6Q2Nds9VqFABTk5O6N27N+7du4eePXsiNjYWDx480EVGIiIiogIneyF4YmIievXqhXr16qF8+fK6yERERESkd2QXTevXr9dFDiIiIiK9pvGaJiIiIqLPCYsmIiIiIjWwaCIiIiJSA4smIiIiIjXILppu376NO3fuSNePHz+OkJAQLFq0SKvB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77//xrp169C0aVNd5CEiIiLSSxotBC9RooQushARERHpLdlF09ChQzF37ly1dp0RERERfSpk7547dOgQ9u3bh507d6JcuXIwNjZWad+4caPWwhERERHpC9lFk62tLVq1aqWLLERERER6S3bRtHTpUl3kICIiItJrstc0AcDLly+xZ88e/P7773j69CkA4N69e0hPT9dqOCIiIiJ9IXum6datW2jcuDESEhKQmZmJb775BlZWVpg2bRoyMzMRHh6ui5xEREREBUr2TNOgQYNQtWpVPH78WOVktq1atcp1/jYiIiKiT4XsmaaDBw/iyJEjuU7O6+Hhgbt372otGBEREZE+kV00KZVK5OTk5Np+584dWFlZaSUUfbo8Rso7CfLNsGY6SkJERCSP7N1zjRo1wpw5c6TrCoUC6enpGDduHE+tQkRERJ8s2TNNM2fOhL+/P8qWLYsXL16gY8eOuHbtGgoXLow1a9boIiMRERFRgZNdNBUtWhRnzpzB33//jbNnzyI9PR3BwcHo1KmTysJwIiIiIl37kMs+ZBdNAGBkZITOnTtrfKdEREREHxvZRdOKFSve2961a1eNwxARERHpK9lF06BBg1SuZ2dn49mzZzAxMUGhQoVYNBEREdEnSfa35x4/fqxySU9Px5UrV1C7dm0uBCciIqJPlkbnnnubl5cXwsLCcs1CEREREX0qNFoInudARka4d++etoYjkk3ONyh40EwiIpJLdtG0ZcsWletCCCQmJuLXX39FrVq1tBaMiIiISJ/ILpoCAgJUrisUChQpUgQNGjTAzJkztZWLiIiISK9odO45IiIios+NVhaCExEREX3qZM80DRkyRO2+s2bNkjs8ERERkV6SXTTFxsYiNjYW2dnZKFWqFADg6tWrMDQ0RJUqVaR+CoVCeymJiIiICpjsoql58+awsrLC8uXLYWdnB+DVAS+7d++Or7/+GkOHDtV6SCIiIqKCJntN08yZMzF16lSpYAIAOzs7TJ48md+eIyIiok+W7KIpLS0NDx48yLX9wYMHePr0qVZCEREREekb2UVTq1at0L17d2zcuBF37tzBnTt3sGHDBgQHB6N169a6yEhERERU4GSvaQoPD8ewYcPQsWNHZGdnvxrEyAjBwcGYMWOG1gMS6QOeooWIiGQXTYUKFcJvv/2GGTNm4Pr16wCA4sWLw8LCQuvhiIiI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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "sum_data = lcms_collection.cluster_summary_dataframe\n", + "freq_fig, ax = plt.subplots()\n", + "sum_data.sample_id_count.value_counts().sort_index().plot(ax = ax, kind = 'bar')\n", + "plt.xlabel('Number of mass features in a cluster')\n", + "plt.ylabel('Frequency of clusters with this many mass features')\n", + "#hist = plt.figure(figsize = (20, 2))\n", + "#plt.hist(df.cluster, bins = df.cluster.unique().shape[0])\n", + "#plt.xlim(0, np.ceil(np.max(df.cluster.unique())))\n", + "#plt.ylim(0, 5)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "406167c7-483d-4113-be61-d14079cd2415", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkQAAAHHCAYAAABeLEexAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuNSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/xnp5ZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAByWklEQVR4nO3deXxU9dn//9fJZJLJHgKBEA1hE5ABl1JEdNxAFkvdClppBW3d6s/Wu2619lsrSC3V9pa21tbaeqv3bdv7VtxXVlEjIOOCyqBIwk4SAsQkZJ/MnN8fx4wZEiCBSc5M5v18PEaZc86cc51l5lz5nM9imKZpIiIiIhLHEuwOQERERMRuSohEREQk7ikhEhERkbinhEhERETinhIiERERiXtKiERERCTuKSESERGRuKeESEREROKeEiIRERGJe0qIREREJO4pIRKJciUlJdxwww0MHToUl8tFZmYmZ555Jn/84x9paGiwO7xead68eRiG0eHrkUce6ZZtvvbaa8ybN69b1i0iR5ZodwAicmivvvoql112GcnJycydO5cxY8bQ3NxMUVERd9xxBz6fj0cffdTuMHutv/71r6Snp4dNmzBhQrds67XXXuPhhx9WUiRiEyVEIlFq69atXHHFFRQWFrJy5UoGDhwYmnfTTTdRXFzMq6++amOEvd+sWbPo16+f3WEck7q6OtLS0uwOQyTq6ZGZSJR64IEHqK2t5bHHHgtLhloNHz6c//iP/wi9b2lpYcGCBQwbNozk5GQGDx7ML37xC5qamsI+N3jwYL797W9TVFTEaaedhsvlYujQofz3f/932HJ+v5/58+dzwgkn4HK56Nu3Lx6Ph2XLloUt9/nnnzNr1ixycnJwuVx885vf5KWXXgpb5oknnsAwDN59911uvfVWcnNzSUtL49JLL2Xv3r1hy77//vtMmzaNfv36kZKSwpAhQ/jhD38Ymr9q1SoMw2DVqlVhn9u2bRuGYfDEE0+EppWXl/ODH/yA448/nuTkZAYOHMjFF1/Mtm3bDnncu+Kpp55i3LhxpKSkkJOTwxVXXMHOnTvDlnnnnXe47LLLGDRoEMnJyRQUFHDLLbeEPe68+uqrefjhhwHCHs91dX+vvvpq0tPTKSkp4Vvf+hYZGRl8//vfByAYDPKHP/wBt9uNy+ViwIAB3HDDDXz55Zdh6z3S8RfprVRCJBKlXn75ZYYOHcoZZ5zRqeWvvfZannzySWbNmsVtt93Ge++9x8KFC/nss894/vnnw5YtLi5m1qxZXHPNNVx11VX813/9F1dffTXjxo3D7XYDVj2ahQsXcu2113LaaadRU1PD+++/z4cffsiUKVMA8Pl8nHnmmRx33HH8/Oc/Jy0tjaeffppLLrmEZ599lksvvTRsuz/5yU/o06cP99xzD9u2beMPf/gDP/7xj/m///s/ACoqKpg6dSq5ubn8/Oc/Jzs7m23btvHcc88d1TGcOXMmPp+Pn/zkJwwePJiKigqWLVvGjh07GDx48BE/X1lZGfbe4XDQp08fAO677z7uvvtuLr/8cq699lr27t3LQw89xNlnn81HH31EdnY2AM888wz19fXceOON9O3bl3Xr1vHQQw+xa9cunnnmGQBuuOEGSktLWbZsGf/zP/9zVPvaqqWlhWnTpuHxePj9739PampqaBtPPPEEP/jBD7j55pvZunUrf/7zn/noo4949913cTqdET/+IjHFFJGoU11dbQLmxRdf3Knl169fbwLmtddeGzb99ttvNwFz5cqVoWmFhYUmYL799tuhaRUVFWZycrJ52223haadfPLJ5owZMw673cmTJ5tjx441GxsbQ9OCwaB5xhlnmCeccEJo2uOPP24C5vnnn28Gg8HQ9FtuucV0OBxmVVWVaZqm+fzzz5uA6fV6D7nNN9980wTMN998M2z61q1bTcB8/PHHTdM0zS+//NIEzN/97neH3YeO3HPPPSbQ7lVYWGiapmlu27bNdDgc5n333Rf2uU8//dRMTEwMm15fX99u/QsXLjQNwzC3b98emnbTTTeZHf0kd3Z/TdM0r7rqKhMwf/7zn4ct+84775iA+c9//jNs+htvvBE2vTPHX6S30iMzkShUU1MDQEZGRqeWf+211wC49dZbw6bfdtttAO3qGo0ePZqzzjor9D43N5eRI0eyZcuW0LTs7Gx8Ph+bN2/ucJuVlZWsXLmSyy+/nAMHDrBv3z727dvH/v37mTZtGps3b2b37t1hn7n++utDj4IAzjrrLAKBANu3bw9tE+CVV17B7/d3at8PJSUlhaSkJFatWtXusVBnPfvssyxbtiz0+uc//wnAc889RzAY5PLLLw/t9759+8jLy+OEE07gzTffDIujVV1dHfv27eOMM87ANE0++uijY9rHQ7nxxhvD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utoEzOXLl/dMUHHqUOdi165d5nHHHWdu2LDBLCwsNBctWtTjscWbjs7FVVddZV588cXHvO64KSE6nK1bt1JeXs75558fmpaVlcWECRNYs2aNjZGJRJ/q6moMwwgbdFl6XnNzM48++ihZWVmcfPLJdocTd4LBIHPmzOGOO+7A7XbbHU7cW7VqFf3792fkyJHceOON7N+/v8vriKrBXe1SXl4OwIABA8KmDxgwIDRPRKCxsZE777yT2bNna2BLm7zyyitcccUV1NfXM3DgQJYtW0a/fv3sDivu3H///SQmJnLzzTfbHUrcmz59Ot/5zncYMmQIJSUl/OIXv+CCCy5gzZo1OByOTq9HCZGIdIrf7+fyyy/HNE3++te/2h1O3DrvvPNYv349+/bt4+9//zuXX3457733Hv3797c7tLjxwQcf8Mc//pEPP/wQwzDsDifuXXHFFaF/jx07lpNOOolhw4axatUqJk+e3On16JEZkJeXB8CePXvCpu/Zsyc0TySetSZD27dvZ9myZSodslFaWhrDhw/n9NNP57HHHiMxMZHHHnvM7rDiyjvvvENFRQWDBg0iMTGRxMREtm/fzm233cbgwYPtDi/uDR06lH79+lFcXNylzykhAoYMGUJeXh4rVqwITaupqeG9995j4sSJNkYmYr/WZGjz5s0sX76cvn372h2StBEMBmlqarI7jLgyZ84cPvnkE9avXx965efnc8cdd7BkyRK7w4t7u3btYv/+/QwcOLBLn4ubR2a1tbVh2eLWrVtZv349OTk5DBo0iJ/+9Kf8+te/5oQTTmDIkCHcfffd5Ofnc8kll9gXdC92pPNRWVnJjh07Qv3dbNq0CbBK81RqF1mHOxcDBw5k1qxZfPjhh7zyyisEAoFQvbqcnBySkpLsCrtXOty56Nu3L/fddx8XXXQRAwcOZN++fTz88MPs3r2byy67zMaoe6cj/UYd/IeB0+kkLy+PkSNH9nSovd7hzkVOTg7z589n5syZ5OXlUVJSws9+9jOGDx/OtGnTurahY26nFiPefPNNE2j3uuqqq0zTtJre33333eaAAQPM5ORkc/LkyeamTZvsDboXO9L5ePzxxzucf88999gad290uHPR2u1BR68333zT7tB7ncOdi4aGBvPSSy818/PzzaSkJHPgwIHmRRddZK5bt87usHulI/1GHUzN7rvP4c5FfX29OXXqVDM3N9d0Op1mYWGhed1115nl5eVd3o5hmqbZtRRKREREpHdRHSIRERGJe0qIREREJO4pIRIREZG4p4RIRERE4p4SIhEREYl7SohEREQk7ikhEhERkbinhEhEBNi2bRuGYbB+/Xq7QxERGyghEpFus3fvXm688UYGDRpEcnIyeXl5TJs2jXfffdfWuK6++up2w/IUFBRQVlbGmDFj7AlKRGwVN2OZiUjPmzlzJs3NzTz55JMMHTqUPXv2sGLFCvbv3293aO04HA6NkycSx1RCJCLdoqqqinfeeYf777+f8847j8LCQk477TTuuusuLrroorDlbrjhBgYMGIDL5WLMmDG88sorAOzfv5/Zs2dz3HHHkZqaytixY/n3v/8dtp1zzz2Xm2++mZ/97Gfk5OSQl5fHvHnzDhnXvHnzePLJJ3nxxRcxDAPDMFi1alW7R2arVq3CMAyWLFnCqaeeSkpKCpMmTaKiooLXX3+dE088kczMTL73ve9RX18fWn8wGGThwoUMGTKElJQUTj75ZBYvXhy5Aysi3UIlRCLSLdLT00lPT+eFF17g9NNPJzk5ud0ywWCQCy64gAMHDvDUU08xbNgwNm7ciMPhAKCxsZFx48Zx5513kpmZyauvvsqcOXMYNmwYp512Wmg9Tz75JLfeeivvvfcea9as4eqrr+bMM89kypQp7bZ5++2389lnn1FTU8Pjjz8OQE5ODqWlpR3ux7x58/jzn/9Mamoql19+OZdffjnJycn861//ora2lksvvZSHHnqIO++8E4CFCxfy1FNP8cgjj3DCCSfw9ttvc+WVV5Kbm8s555xzzMdVRLpJxIelFRH5yuLFi80+ffqYLpfLPOOMM8y77rrL/Pjjj0PzlyxZYiYkJJibNm3q9DpnzJhh3nbbbaH355xzjunxeMKWGT9+vHnnnXcech1XXXWVefHFF4dN27p1qwmYH330kWmaX4+wvXz58tAyCxcuNAGzpKQkNO2GG24wp02bZpqmaTY2Npqpqanm6tWrw9Z9zTXXmLNnz+70PopIz9MjMxHpNjNnzqS0tJSXXnqJ6dOns2rVKr7xjW/wxBNPALB+/XqOP/54RowY0eHnA4EACxYsYOzYseTk5JCens6SJUvYsWNH2HInnXRS2PuBAwdSUVERkX1ou+4BAwaQmprK0KFDw6a1bqu4uJj6+nqmTJkSKiFLT0/nv//7vykpKYlIPCLSPfTITES6lcvlYsqUKUyZMoW7776ba6+9lnvuuYerr76alJSUw372d7/7HX/84x/5wx/+wNixY0lLS+OnP/0pzc3NYcs5nc6w94ZhEAwGIxJ/23UbhnHYbdXW1gLw6quvctxxx4Ut19EjQxGJHkqIRKRHjR49mhdeeAGwSl927drFF1980WEp0bvvvsvFF1/MlVdeCVh1jr744gtGjx59TDEkJSURCASOaR0dGT16NMnJyezYsUP1hURijBIiEekW+/fv57LLLuOHP/whJ510EhkZGbz//vs88MADXHzxxQCcc845nH322cycOZMHH3yQ4cOH8/nnn2MYBtOnT+eEE05g8eLFrF69mj59+vDggw+yZ8+eY06IBg8ezJIlS9i0aRN9+/YlKysrErtMRkYGt99+O7fccgvBYBCPx0N1dTXvvvsumZmZXHXVVRHZjohEnhIiEekW6enpTJgwgUWLFlFSUoLf76egoIDrrruOX/ziF6Hlnn32WW6//XZmz55NXV0dw4cP57e//S0Av/zlL9myZQvTpk0jNTWV66+/nksuuYTq6upjiu26665j1apVfPOb36S2tpY333yTwYMHH9M6Wy1YsIDc3FwWLlzIli1byM7O5hvf+EbYPotI9DFM0zTtDkJERETETmplJiIiInFPCZGIiIjEPSVEIiIiEveUEImIiEjcU0IkIiIicU8JkYiIiMQ9JUQiIiIS95QQiYiISNxTQiQiIiJxTwmRiIiIxD0lRCIiIhL3lBCJiIhI3Pv/AWZZ3bKNpyTcAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "sum_df = lcms_collection.cluster_summary_dataframe.copy()\n", + "trunc_df = sum_df[sum_df.sample_id_count > 10]\n", + "\n", + "## generate mass feature figure\n", + "fig = plt.figure()\n", + "plt.scatter(\n", + " df.scan_time_aligned,\n", + " df.mz,\n", + " c = 'tab:gray',\n", + " s = 1 ## larger dots when scaled in\n", + ")\n", + "\n", + "plt.scatter(\n", + " trunc_df.scan_time_aligned_median,\n", + " trunc_df.mz_median, \n", + " c = 'tab:orange',\n", + " alpha = 0.7, \n", + " s = (trunc_df.sample_id_count**2)/10\n", + ")\n", + "\n", + "## add a scale bar for the orange dots when zoomed in\n", + "plt.xlabel('Scan time')\n", + "plt.ylabel('m/z')\n", + "#plt.ylim(0, np.ceil(np.max(df.mz)))\n", + "#plt.xlim(0, np.ceil(np.max(df.scan_time)))\n", + "plt.ylim(500, 550)\n", + "plt.xlim(10, 15)\n", + "\n", + "plt.title('Consensus Features')\n", + "plt.show()\n", + "\n", + "## 3rd option: also just map of consensus/ also can zoom in" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda env:corems] *", + "language": "python", + "name": "conda-env-corems-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/support_code/nmdc/lipidomics/lipidomics_collection_working.py b/support_code/nmdc/lipidomics/lipidomics_collection_working.py new file mode 100644 index 000000000..c7ea272f8 --- /dev/null +++ b/support_code/nmdc/lipidomics/lipidomics_collection_working.py @@ -0,0 +1,96 @@ +from pathlib import Path +import time +from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectraCollection + + +if __name__ == "__main__": + # Set the path to the collection of LCMS runs (previously processed) + collection_path = Path( + "/Users/cies677/sandbox/corems/support_code/nmdc/lipidomics/curr/processed/pos" + ) + # Path to manifest file + manifest_file = collection_path / "manifest_curr.csv" + # This file will need to be created by the user or helper script? + chromatography_file = collection_path / "long_lipid_gradient_chroma.csv" + + # Set the number of cores to use for loading the data (the parser is parallelized) + ncores = 8 + + # Instantiate the parser + parser = ReadCoreMSHDFMassSpectraCollection( + folder_location = collection_path, + manifest_file = manifest_file, + chromatography_file = chromatography_file, + cores = ncores + ) + print( + "Loading LCMS collection with", + len(parser.manifest), + "samples using", + ncores, + " cores" + ) + + # Load the LCMS collection (minimally load the data) + start_time = time.time() + lcms_collection = parser.get_lcms_collection( + load_raw=False, + load_light=True + ) + print( + "Time to load LCMS collection ", + time.time() - start_time, + "seconds -", + len(lcms_collection), + " LCMS runs and ", + ncores, + " cores" + ) + #10s for 7 samples, 10 cores; 162s for 70 samples, 10 cores + + # Set flag to call _drop_isotopologue() when running _check_mass_features_df() + lcms_collection.parameters.lcms_collection.drop_isotopologues = True + print( + "Number of total mass features: ", + len(lcms_collection.mass_features_dataframe) + ) + + # Align the LCMS runs between each other + print("Aligning LCMS collection") + start_time = time.time() + lcms_collection.align_lcms_objects() + print( + "Time to align LCMS collection: ", + time.time() - start_time, + "seconds" + ) + #1.5s for 7 samples; 15s for 70 samples + + # Make some plots + lcms_collection.plot_tics(type="both") + lcms_collection.plot_alignments() + # TODO: Think about other plots that would be useful to have here for assessing the quality of the data and alignment + + # Make consensus mass features from the consolidated mass features + start_time = time.time() + + ## Inconsistently getting a repeated RuntimeWarning: Mean of empty slice + ## return np.nanmean(a, axis, out = out, keepdims = keepdims) + ## Should check what causes this, make sure output is understood, and if + ## yes suppress the warning + + lcms_collection.add_consensus_mass_features() + # THIS FUNCTION NEEDS WORK AND NEEDS MECHANISM TO EVALUATE THE RESULTS (plots? reports?) + print( + "Time to roll up consensus mass features: ", + time.time() - start_time, + "seconds -", + len(lcms_collection.mass_features_dataframe), + " total mass features", + ncores, + " cores" + ) + + #TODO: Add code to load and save information about chromatographic settings + #TODO: Add code to save and load collection to HDF5 file + #TODO: Add code to plot a consensus mass feature \ No newline at end of file diff --git a/support_code/nmdc/lipidomics/lipidomics_workflow.py b/support_code/nmdc/lipidomics/lipidomics_workflow.py index 753599eee..2e1783cd8 100644 --- a/support_code/nmdc/lipidomics/lipidomics_workflow.py +++ b/support_code/nmdc/lipidomics/lipidomics_workflow.py @@ -616,11 +616,11 @@ def run_lipid_workflow( if __name__ == "__main__": # Set input variables to run cores = 1 - file_dir = Path("/Users/heal742/LOCAL/corems_dev/corems/tmp_data/thermo_raw_mini") + file_dir = Path("/Users/cies677/sandbox/corems/support_code/nmdc/lipidomics/curr/raw/") out_dir = Path("tmp_data/_test_250115") - params_toml = Path("/Users/heal742/LOCAL/05_NMDC/02_MetaMS/data_processing/configurations/emsl_lipidomics_corems_params.toml") + params_toml = Path("/Users/cies677/sandbox/corems/support_code/nmdc/lipidomics/curr/emsl_lipidomics_corems_params.toml") verbose = True - scan_translator = Path("tmp_data/thermo_raw_collection/scan_translator.toml") + scan_translator = Path("/Users/cies677/sandbox/corems/support_code/nmdc/lipidomics/curr/emsl_lipidomics_scan_translator.toml") # Set up output directory out_dir.mkdir(parents=True, exist_ok=True) From 0e56d845c77266a3614b68a6c1553ca145cf1370 Mon Sep 17 00:00:00 2001 From: "Ciesielski, Danielle K" Date: Mon, 14 Apr 2025 15:09:50 -0700 Subject: [PATCH 037/158] added consensus plot --- corems/mass_spectra/calc/lc_calc.py | 60 ++ .../nmdc/lipidomics/lipidomics_collection.py | 5 + .../lipidomics/lipidomics_collection_nb.ipynb | 302 --------- .../lipidomics_collection_working.py | 96 --- .../nmdc/lipidomics/lipidomics_workflow.py | 639 ------------------ 5 files changed, 65 insertions(+), 1037 deletions(-) delete mode 100644 support_code/nmdc/lipidomics/lipidomics_collection_nb.ipynb delete mode 100644 support_code/nmdc/lipidomics/lipidomics_collection_working.py delete mode 100644 support_code/nmdc/lipidomics/lipidomics_workflow.py diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 1d51f5dc1..4a7604590 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -2038,6 +2038,66 @@ def plot_mass_features_across_samples(self, alpha = 0.75, s = 0.005, return_fig return fig else: plt.show() + + def plot_consensus_mass_features(self, xb = 'xb', xt = 'xt', yb = 'yb', yt = 'yt', show_raw = True, return_fig = False): + """ + Generate Scan Time vs m/z plot of the consensus features scaled by size + with option ('show_raw') of leaving the raw data in the figure. + """ + df = self.cluster_summary_dataframe.copy() + + fig = plt.figure() + if show_raw: + plt.scatter( + df.scan_time_aligned, + df.mz, + c = 'tab:gray', + s = 1 + ) + + m = plt.scatter( + df.scan_time_aligned_median, + df.mz_median, + c = 'tab:orange', + alpha = 0.7, + s = (df.sample_id_count**2)/5 + ) + + plt.xlabel('Scan time') + plt.ylabel('m/z') + + if xt == 'xt': + xt = np.ceil(np.max(df.mz)) + if yt == 'yt': + yt = np.ceil(np.max(df.scan_time)) + if xb == 'xb': + xb = 0 + if yb == 'yb': + yb = 0 + plt.ylim(xb, xt) + plt.xlim(yb, yt) + + kw = dict( + prop = 'sizes', + num = len(df.sample_id_count.unique())/3, + color = 'tab:orange', + alpha = 0.7, + func = lambda s: np.sqrt(s*5) + ) + + plt.legend( + *m.legend_elements(**kw), + title = 'Features\nper cluster', + bbox_to_anchor = (1.01, 0.4, 0.225, 0.5) + ) + plt.tight_layout() + plt.title('Consensus Features') + + if return_fig: + plt.close(fig) + return fig + else: + plt.show() def add_sparse_distance_matrix(self, features): if features is None: diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 1051e8b69..0fccee113 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -43,6 +43,11 @@ # Make some plots lcms_collection.plot_tics(type="both") lcms_collection.plot_alignments() + lcms_collection.plot_mass_features_across_samples() + lcms_collection.plot_mass_features_per_cluster() + lcms_collection.plot_consensus_mass_features() ## zoomed out + lcms_collection.plot_consensus_mass_features(xb = 10, xt = 15, yb = 500, yt = 600) ## zoomed in + # TODO: Think about other plots that would be useful to have here for assessing the quality of the data and alignment # Make consensus mass features from the consolidated mass features diff --git a/support_code/nmdc/lipidomics/lipidomics_collection_nb.ipynb b/support_code/nmdc/lipidomics/lipidomics_collection_nb.ipynb deleted file mode 100644 index 71405307e..000000000 --- a/support_code/nmdc/lipidomics/lipidomics_collection_nb.ipynb +++ /dev/null @@ -1,302 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "6b2609cf-1a43-47c1-9685-255054bc5393", - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Loading LCMS collection with 22 samples using 8 cores\n", - "Time to load LCMS collection 181.71068596839905 seconds - 22 LCMS runs and 8 cores\n", - "Number of total mass features: 151007\n", - "Aligning LCMS collection\n", - "Time to align LCMS collection: 12.283224821090698 seconds\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA04AAANQCAYAAAAffD9qAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuNSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/xnp5ZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOz9eZTlV33fe7/3bzhjjT2PEhISYhACWQYjfG3AAWMW4YEV24lJnohk2X6CF9x4SOwsxXlyl+048nC5xNfGYF8/tmywLMdgIMaDkMGSbEuABGohITS31GNVd03n1Jl+w977+eNUV1d1zV3VXVWnP6+1iqU6U+1TVP/O7/P77v3dxnvvERERERERkSUFmz0AERERERGRrU7BSUREREREZAUKTiIiIiIiIitQcBIREREREVmBgpOIiIiIiMgKFJxERERERERWoOAkIiIiIiKyAgUnERERERGRFSg4iYiIiIiIrEDBSUREREREZAVXdHB64IEHeM973sOBAwcwxvC5z31uza9xzz338KY3vYn+/n52797ND/7gD/Liiy9u+FhFRERERGTzXNHBqdls8rrXvY6PfexjF/X8o0eP8t73vpfv+77v48iRI9xzzz2MjY3xz/7ZP9vgkYqIiIiIyGYy3nu/2YPYCowxfPazn+V973vf7G1JkvDzP//z/Mmf/AlTU1PceOON/Oqv/ipvfetbAfj0pz/N+9//fpIkIQi6GfQv/uIveO9730uSJMRxvAnvRERERERENtoVXXFayYc//GEeeugh7r77br75zW/ywz/8w/zAD/wAzz77LAC33HILQRDwB3/wB1hrqdVqfPKTn+Ttb3+7QpOIiIiISA9RxWnGhRWnY8eOce2113Ls2DEOHDgw+7i3v/3tvPGNb+S///f/DsD999/PP//n/5zx8XGstdx666381V/9FUNDQ5vwLkRERERE5FJQxWkJjz/+ONZaXvGKV9DX1zf7df/99/P8888DMDIywo//+I/zgQ98gIcffpj777+fQqHAD/3QD6E8KiIiIiLSO6LNHsBW1Wg0CMOQr3/964RhOO++vr4+AD72sY8xODjIr/3ar83e96lPfYrDhw/z1a9+lTe96U2XdcwiIiIiInJpKDgt4eabb8Zay5kzZ/ie7/meRR/TarVmm0Kccy5kOecu+RhFREREROTyuKKn6jUaDY4cOcKRI0eAbnvxI0eOcOzYMV7xilfwr/7Vv+K2227jz//8zzl69Chf+9rXuOOOO/jLv/xLAN797nfz8MMP84u/+Is8++yzfOMb3+Df/tt/y9VXX83NN9+8ie9MREREREQ20hXdHOK+++7jbW9724LbP/CBD3DnnXeSZRn/7b/9N/7oj/6IkydPsmvXLt70pjfxC7/wC7z2ta8F4O677+bXfu3XeOaZZ6hUKtx666386q/+Kq985Ssv99sREREREZFL5IoOTiIiIiIiIqtxRU/VExERERERWQ0FJxERERERkRVccV31nHOcOnWK/v5+jDGbPRwREREREdkk3nump6c5cODAgm7ZF7rigtOpU6c4fPjwZg9DRERERES2iOPHj3Po0KFlH3PFBaf+/n6g+8sZGBjY5NGIiIiIiMhmqdfrHD58eDYjLOeKC07npucNDAwoOImIiIiIyKqW8Kg5hIiIiIiIyAoUnERERERERFag4CQiIiIiIrICBScREREREZEVKDiJiIiIiIisQMFJRERERERkBZsanD7+8Y9z0003zbYGv/XWW/nrv/7rJR9/5513YoyZ91UqlS7jiEVERERE5Eq0qfs4HTp0iF/5lV/h+uuvx3vPH/7hH/Le976XRx99lNe85jWLPmdgYICnn3569vvV9FwXERERERFZj00NTu95z3vmff/Lv/zLfPzjH+crX/nKksHJGMO+ffsux/BERERERESALbTGyVrL3XffTbPZ5NZbb13ycY1Gg6uvvprDhw/z3ve+l29961vLvm6SJNTr9XlfIiIiIiIia7Hpwenxxx+nr6+PYrHIBz/4QT772c/y6le/etHH3nDDDfz+7/8+n//85/nUpz6Fc443v/nNnDhxYsnXv+OOOxgcHJz9Onz48KV6KyIiIiIi0qOM995v5gDSNOXYsWPUajU+/elP83u/93vcf//9S4anubIs41WvehXvf//7+aVf+qVFH5MkCUmSzH5fr9c5fPgwtVqNgYGBDXsfIiIiIiKyvdTrdQYHB1eVDTZ1jRNAoVDguuuuA+CWW27h4Ycf5jd+4zf4nd/5nRWfG8cxN998M88999ySjykWixSLxQ0bryzvj45kvOv6iN1VNe0QERERkd6x6VP1LuScm1chWo61lscff5z9+/df4lHJaj1yynK25TZ7GCIiIiIiG2pTK063334773rXu7jqqquYnp7mrrvu4r777uOee+4B4LbbbuPgwYPccccdAPziL/4ib3rTm7juuuuYmpri13/913nppZf4sR/7sc18GzLDeU9owCk3iYiIiEiP2dTgdObMGW677TZOnz7N4OAgN910E/fccw/veMc7ADh27BhBcL4oNjk5yY//+I8zMjLC8PAwt9xyCw8++OCq1kPJpZdaqBYMdlNXzYmIiIiIbLxNbw5xua1lAZiszVTH89tfy/iB60K+40C42cMREREREVnWWrLBllvjJNtXJ/dUY1RxEhEREZGeo+AkGybJoaKpeiIiIiLSgxScZMN0croVJzWHEBEREZEeo+AkG6aTeyqxwV5Zy+ZERERE5Aqg4CQbpjtVT+3IRURERKT3KDjJhulYTzU25ApOIiIiItJjFJxkw3RyqMTgNFNPRERERHqMgpNsmDSHaqyueiIiIiLSexScZMN0ck+loK56IiIiItJ7FJxkw3Sn6qniJCIiIiK9R8FJNozzEAdgtchJRERERHqMgpNsGGMgDNQcQkRERER6j4KTbBjvITSoHbmIiIiI9BwFJ9lQqjiJiIiISC9ScJINY0y34qTmECIiIiLSaxScZEMZY/AKTiIiIiLSYxScZMOcC0zGbO44REREREQ2moKTbDhVnERERESk1yg4iYiIiIiIrEDBSTaE8352ip6m6omIiIhIr1Fwkg0x2YbhUjcxaaqeiIiIiPQaBSfZEKMNx94+lZpEREREpDcpOMmGGG169lS7wUlT9URERESk1yg4yYbo5FCJu/+tqXoiIiIi0msUnGRDWNfd/FZEREREpBcpOMmG8ECornoiIiIi0qMUnGRDWAfBTGDSVD0RERER6TUKTrIhnPezwUlEREREpNcoOMmGcP58xUlT9URERESk1yg4yYZwHsKZvyZN1RMRERGRXqPgJBvCzqk4iYiIiIj0GgUn2RBeU/VEREREpIcpOMmGsP58O3IRERERkV6j4CQbwnttgCsiIiIivUvBSUREREREZAWbGpw+/vGPc9NNNzEwMMDAwAC33norf/3Xf73sc/7sz/6MV77ylZRKJV772tfyV3/1V5dptCIiIiIicqXa1OB06NAhfuVXfoWvf/3rPPLII3zf930f733ve/nWt7616OMffPBB3v/+9/OjP/qjPProo7zvfe/jfe97H0888cRlHrmIiIiIiFxJjPdba9edHTt28Ou//uv86I/+6IL7/sW/+Bc0m02+8IUvzN72pje9ide//vV84hOfWNXr1+t1BgcHqdVqDAwMbNi4r3R/dCTjttfHAHzysYx//bp4k0ckIiIiIrK8tWSDLbPGyVrL3XffTbPZ5NZbb130MQ899BBvf/vb5932zne+k4ceemjJ102ShHq9Pu9LRERERERkLTY9OD3++OP09fVRLBb54Ac/yGc/+1le/epXL/rYkZER9u7dO++2vXv3MjIysuTr33HHHQwODs5+HT58eEPHLyIiIiIivW/Tg9MNN9zAkSNH+OpXv8pP/MRP8IEPfIAnn3xyw17/9ttvp1arzX4dP358w15bRERERESuDNFmD6BQKHDdddcBcMstt/Dwww/zG7/xG/zO7/zOgsfu27eP0dHRebeNjo6yb9++JV+/WCxSLBY3dtAiIiIiInJF2fSK04WccyRJsuh9t956K1/60pfm3XbvvfcuuSZKRERERERkI2xqxen222/nXe96F1dddRXT09Pcdddd3Hfffdxzzz0A3HbbbRw8eJA77rgDgJ/8yZ/kLW95Cx/5yEd497vfzd13380jjzzC7/7u727m2xARERERkR63qcHpzJkz3HbbbZw+fZrBwUFuuukm7rnnHt7xjncAcOzYMYLgfFHszW9+M3fddRf/5b/8F/7zf/7PXH/99Xzuc5/jxhtv3Ky3ICIiIiIiV4Att4/TpaZ9nC4N7eMkIiIiItvNttzHSUREREREZKtScJINd2XVMEVERETkSqDgJBvOmM0egYiIiIjIxlJwEhERERERWYGCk4iIiIiIyAoUnERERERERFag4CQiIiIiIrICBScREREREZEVKDjJhlM7chERERHpNQpOIiIiIiIiK1Bwkg2nfZxEREREpNcoOImIiIiIiKxAwUlERERERGQFCk4iIiIiIiIrUHASERERERFZgYKTiIiIiIjIChScZMMZwGkzJxERERHpIQpOsuECA9Zt9ihERERERDaOgpNsuMCAU8FJRERERHqIgpNsuDBQcBIRERGR3qLgJBsuMGAVnERERESkhyg4yYYLjcGtsMbpfz2V00iVrkRERERke1Bwkg0XGFipN8SZpufpMXWQEBHpVe22Lo6JSG9RcJINt5queoFBwUlEpIf97RcVnESktyg4yYbrNodY/gMzCiCxl2lAIiJy2WXp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ww29bgcf11StXLmSUaNGYbVamTBhAiUlJUBPU7hf/epX7N69+1hNzxNPPAGc2FRv7969zJgxA4vFQkxMDLfccgtut/vY4zfccAMLFizgT3/6E0lJScTExHD77bcTCARO+ppER0eTmJhIYmIicXFxAMTExBxbFh0dfUJTvY9qx/7zn/+Qnp5OREQEt912G6FQiPvvv5/ExETi4+P53e9+d9yx2tvb+fa3v01cXBx2u50ZM2awe/fuU56zxYsXc9FFFx237KPn+Pvf/56EhAQcDge//vWvCQaD/PjHPyY6OprU1FQef/zxY9t8uqleb+fkdE2bNo077riDO++8k9jYWGbPng3An//8Z4YOHYrNZiMtLY3bbrvt2HnSNI24uLjj/keGDx9OUlLSsfvr1q3DZDLR3d0N9PzPTZw4kcWLF59R+YQQ4mwmwUkIIb4C9u3bx4YNG46rHVi0aBFPPfUUDz/8MMXFxdx1111885vfZM2aNaSlpfHKK68AUFJSQl1dHQ8++GCv233Sz372Mx544AG2bduGXq/nW9/6FgALFy7khz/8Ifn5+dTV1VFXV8fChQtPKHNXVxezZ8/G6XSydetWXnrpJVasWMEdd9xx3HqrVq3i8OHDrFq1iieffJInnnjiWBDrK4cPH2bp0qW8++67PP/88zz22GNceOGFVFdXs2bNGu677z5+/vOfs3nz5mPbXH755TQ2NrJ06VK2b9/OyJEjOe+882htbf3M46xbt45Ro0adsPz999+ntraWDz74gD//+c/ce++9zJs3D6fTyebNm7n11lv5zne+Q3V19Smfx2edkzPx5JNPYjQaWb9+PQ8//DAAqqryt7/9jeLiYp588knef/99/t//+39ATxieMmUKq1evBqCtrY0DBw7g8Xg4ePAgAGvWrGH06NFYrdZjxxkzZgxr16494/IJIcRZSxNCCHHWuf766zWdTqfZbDbNZDJpgKaqqvbyyy9rmqZpXq9Xs1qt2oYNG47b7qabbtKuuuoqTdM0bdWqVRqgtbW1HXv8TLZbsWLFscffeecdDdA8Ho+maZp27733aoWFhSeUG9Bee+01TdM07dFHH9WcTqfmdruP24+qqlp9ff2x55mRkaEFg8Fj61x++eXawoULe32Njhw5ogHazp07j1v++OOPa1FRUcfu33vvvZrVatU6OjqOLZs9e7aWmZmp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np6OXq8nMTHxiOd9suX129/+ltjYWDIzM4difeWVV5gxYwYGg4EXXngBgKeeeors7GwMBgNZWVn84x//OCKehoYGLrvsMqxWK2azmXHjxrFt27bjvsdOplx+97vfER0dTXBwMNdee+0xW2rPO+88Xn755VN6/SRJkr5RQpIkSfrali5dKhYtWjT094EDB4TNZhOFhYVD237zm9+IrKws8eGHH4qqqiqxfPlyodfrxbp164TP5xOvv/66AERZWZlobm4WPT09JzxPCCHWrl0rAFFYWCjWrVsniouLxbRp08TkyZOFEEI4nU5x5513itzcXNHc3Cyam5uF0+kUQggBiDfffFMIIUR/f7+IiYkRF1xwgThw4IBYs2aNSElJEUuXLj3ieYaEhIgbb7xRlJSUiHfffVeYTCbxxBNPnPA1qqmpEYDYs2fPEduXL18uQkNDh/6+//77RVBQkLjoootEcXGxeOedd4ROpxPz588Xt956qygtLRX//Oc/BSC2bt06dN7cuXPFeeedJ3bs2CHKy8vFnXfeKcLDw0VnZ+dXxhQaGipefvnlI7YtXbpUBAcHi5tvvlmUlpaKp59+WgBi/vz54re//a0oLy8Xv/71r4VWqxX19fXHfG4nKpOT9ZOf/ESEhYWJZ555RlRWVooNGzaIJ598Ughx8uUVFBQkrrzySlFUVCSKioqGYk1OThavv/66qK6uFk1NTeL5558XMTExQ9tef/11YbVaxTPPPCOEEKKvr0+kpqaKadOmiQ0bNoiKigrxyiuviM2bNx/3PXaicnnllVeEXq8XTz31lCgtLRU///nPRXBwsMjPzz/itSgpKRGAqKmpOaXXUJIk6ZsiEydJkqRvwNKlS4VarRZms1no9XoBCJVKJVasWCGEEMLlcgmTySQ2b958xHnXXnutuOyyy4QQn99sd3d3D+0/lfNWr149tP/9998XgBgYGBBCDCYjX74RFeLIxOmJJ54QYWFhor+//4jHUalUoqWlZeh5JiUlCZ/PN3TMxRdfLJYsWXLC1+hUEieTySR6e3uHts2fP18kJycLv98/tC0zM1MsW7ZMCCHEhg0bREhIiHC5XEc8dlpamnj88cePGU93d7cAxKeffnrE9sPP8cvXmjZt2tDfPp9PmM1m8dJLLx3zuZ1MmZxIb2+v0Ov1Q4nSl51seUVHRwu32z10zOFY//zn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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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Pn362K1PSEiwq6U4kgxfX1+jSpUqhoeHhyHJqF+/vnHu3DnDMAzjgw8+MJo1a2ZYrVbbmPz8fMPPz89YvXq1rf5+/frZbdfRcaUdt2EYRr169YxXX33Vbts//xt37tzZGDFihF2fgQMH2v09JBnTpk2z/Z6Xl2dIMr744otS/z6GYRgzZsywO183dOvWzRg/frxdrY8//rjt99OnTxuSjGeffdbWlpaWZkgyTp8+bRiGYSQmJhojR4602+7GjRsNDw8P4+rVq8XW8+qrrxoNGza8qf3nx3hjX/Pnz7e1LVmyxPD19S3x2Bw5J4ZhGJ9++qnh4eFhFBUVFVsjANwJuCMGAHepgwcP6ptvvtGgQYMkSZUqVVJCQoLtrpGj24iKirJr+/nvktSyZUt5enrafq9du7ays7MlSXv27FFRUZGaNm2qqlWr2pb169fbHjPbv3+/oqOj7bYZExPjUI2vvvqqdu3apS+++EKRkZF69913FRQUJEn69ttvdfjwYVWrVs2236CgIF27ds3uEbefc3RcacftqP3796tLly52bV26dNH+/fvt2tq0aWP7uUqVKvL393d6X7fy3/sICQmRJLVu3fqmthv7/fbbb7Vw4UK78xoXFyer1aqjR48Wu4+rV6/K19f3tvd/7do15ebmlngMjpwTPz8/Wa1W5efnl7gdAHC3Su4uAABwe+bPn6/r168rLCzM1mYYhnx8fPT6668rICCgzPbl5eVl97vFYrHNSpeXlydPT0+lp6fb/Qey9NNkC79UaGioGjdurMaNG2vBggXq06ePvvvuOwUHBysvL08dOnTQokWLbhpXq1atErfp6LjSjrusmbGv/97HjXfaimv773P7+9//XuPGjbtpWxEREcXuo2bNmrpw4UKZ7P9W27gx5uf9z58/rypVqsjPz6/E7QCAuxHEAOAudP36db3//vt6+eWX1bNnT7t18fHxWrJkiZ544olbbqdZs2bavn27hgwZYmvbvn27U7W0b99eRUVFys7O1v33319snxYtWmjbtm12bVu3bnVqP9JPd+s6dOigF154QXPnztU999yjZcuWKTg4WP7+/sWO8fb2VlFRkV2bI+McUdy2f65FixbavHmzhg4damvbvHmzIiMjb3u/Zrnnnnv03XffqXHjxg6Pad++vTIzM3XhwgVVr17dhdWVbO/evWrfvr1b9g0AjuLRRAC4C61cuVIXLlxQYmKiWrVqZbcMGDDA4ccTx44dq/nz5+u9997ToUOHNGvWLO3evdt2Z8IRTZs21eDBgzVkyBCtWLFCR48e1TfffKOUlBT961//kiSNGzdOq1at0p///GcdOnRIr7/+ulatWnVbx/7UU0/prbfe0smTJzV48GDVrFlT/fr108aNG3X06FGtW7dO48aN048//ijpp9kCd+/erYMHD+rs2bMqLCx0aJwj6tevrw0bNujkyZM6e/ZssX0mTZqkhQsX6s0339ShQ4f0yiuvaMWKFUpOTr6t4zfT5MmTtWXLFtskK4cOHdKnn35a6oQa7du3V82aNbV582YTK7W3cePGm/4HBQDcaQhiAHAXmj9/vmJjY4t9/HDAgAHasWOHdu/efcvtDB48WFOnTlVycrLuueceHT16VMOGDSvxHZ+SLFiwQEOGDNHEiRPVrFkzxcfHa/v27bbH1zp16qR33nlHc+fOVdu2bfXll19q2rRpTu3jhl69eqlBgwZ64YUXVLlyZW3YsEERERHq37+/WrRoocTERF27ds12p2vEiBFq1qyZOnbsqFq1amnz5s0OjXPEc889px9++EGNGjUq8VHI+Ph4zZ07V3/+85/VsmVLvfXWW1qwYIG6d+9+W8dvpjZt2mj9+vX697//rfvvv1/t27fX9OnT7R6H/TlPT08NHz682Mc+zXDy5Elt2bJFw4cPd8v+AcBRFsNwYp5jAEC59+tf/1qhoaE3fesKcFRmZqZatmypjIwM1atXz9R9T548WRcuXNDbb79t6n4BwFm8IwYAFdiVK1c0b948xcXFydPTU0uWLNFXX32lNWvWuLs03MVCQ0M1f/58HT9+3PQgFhwcrKSkJFP3CQC3gztiAFCBXb16VQ8++KB27typa9euqVmzZpo2bZr69+/v7tIAACjXCGIAAAAAYDIm6wAAAAAAkxHEAAAAAMBkBDEAAAAAMBlBDAAAAABMRhADAAAAAJMRxAAAAADAZAQxAAAAADAZQQwAAAAATEYQAwAAAACT/T/SP3HzD8rXEAAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from pathlib import Path\n", - "import time\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectraCollection\n", - "from corems.mass_spectra.calc.lc_calc import PHCalculations, find_closest\n", - "\n", - "# Set the path to the collection of LCMS runs (previously processed)\n", - "collection_path = Path(\n", - " \"/Users/cies677/sandbox/corems/support_code/nmdc/lipidomics/curr/processed/pos\"\n", - " )\n", - "# Path to manifest file\n", - "manifest_file = collection_path / \"manifest_curr.csv\"\n", - "# This file will need to be created by the user or helper script?\n", - "chromatography_file = collection_path / \"long_lipid_gradient_chroma.csv\"\n", - " \n", - "# Set the number of cores to use for loading the data (the parser is parallelized)\n", - "ncores = 8\n", - "\n", - "# Instantiate the parser\n", - "parser = ReadCoreMSHDFMassSpectraCollection(\n", - " folder_location = collection_path,\n", - " manifest_file = manifest_file,\n", - " chromatography_file = chromatography_file,\n", - " cores = ncores\n", - ")\n", - "print(\n", - " \"Loading LCMS collection with\", \n", - " len(parser.manifest), \n", - " \"samples using\", \n", - " ncores, \n", - " \" cores\"\n", - ")\n", - "\n", - "# Load the LCMS collection (minimally load the data)\n", - "start_time = time.time()\n", - "lcms_collection = parser.get_lcms_collection(\n", - " load_raw=False, \n", - " load_light=True\n", - " )\n", - "print(\n", - " \"Time to load LCMS collection \", \n", - " time.time() - start_time, \n", - " \"seconds -\", \n", - " len(lcms_collection), \n", - " \" LCMS runs and \", \n", - " ncores, \n", - " \" cores\"\n", - ")\n", - "#10s for 7 samples, 10 cores; 162s for 70 samples, 10 cores\n", - "\n", - "# Set flag to call _drop_isotopologue() when running _check_mass_features_df()\n", - "lcms_collection.parameters.lcms_collection.drop_isotopologues = True\n", - "print(\n", - " \"Number of total mass features: \", \n", - " len(lcms_collection.mass_features_dataframe)\n", - ")\n", - "\n", - "# Align the LCMS runs between each other\n", - "print(\"Aligning LCMS collection\")\n", - "start_time = time.time()\n", - "lcms_collection.align_lcms_objects()\n", - "print(\n", - " \"Time to align LCMS collection: \", \n", - " time.time() - start_time, \n", - " \"seconds\"\n", - ")\n", - "#1.5s for 7 samples; 15s for 70 samples\n", - "\n", - "# Make some plots \n", - "lcms_collection.plot_tics(type=\"both\")\n", - "lcms_collection.plot_alignments()\n", - "# TODO: Think about other plots that would be useful to have here for assessing the quality of the data and alignment\n", - "\n", - "# Make consensus mass features from the consolidated mass features\n", - "start_time = time.time()\n", - " \n", - "## Inconsistently getting a repeated RuntimeWarning: Mean of empty slice\n", - "## return np.nanmean(a, axis, out = out, keepdims = keepdims)\n", - "## Should check what causes this, make sure output is understood, and if\n", - "## yes suppress the warning" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "15100669-9da5-437f-bc1b-2255ae7ac055", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Time to roll up consensus mass features: 218.44262504577637 seconds - 151007 total mass features 8 cores\n" - ] - } - ], - "source": [ - "lcms_collection.add_consensus_mass_features()\n", - "# THIS FUNCTION NEEDS WORK AND NEEDS MECHANISM TO EVALUATE THE RESULTS (plots? reports?)\n", - "\n", - "print(\n", - " \"Time to roll up consensus mass features: \", \n", - " time.time() - start_time, \n", - " \"seconds -\", \n", - " len(lcms_collection.mass_features_dataframe), \n", - " \" total mass features\", \n", - " ncores, \n", - " \" cores\"\n", - ")\n", - "\n", - "#TODO: Add code to load and save information about chromatographic settings\n", - "#TODO: Add code to save and load collection to HDF5 file\n", - "#TODO: Add code to plot a consensus mass feature" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "359b16fc-4efc-4504-83ee-027e47850b10", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "df = lcms_collection.mass_features_dataframe.copy()\n", - "## generate mass feature figure\n", - "fig = plt.figure()\n", - "plt.scatter(\n", - " df.scan_time_aligned, #switched to aligned b/c aligned is the only scan time in the summary\n", - " df.mz,\n", - " c = 'tab:gray',\n", - " alpha = 0.75, ## ask katherine about how we want this to look\n", - " s = 0.005 ## ask katherine about how we want this to look\n", - ")\n", - "\n", - "#plt.legend(loc = 'lower center', bbox_to_anchor = (0.5, -0.25), ncol = 2)\n", - "plt.xlabel('Scan time')\n", - "plt.ylabel('m/z')\n", - "plt.ylim(0, np.ceil(np.max(df.mz)))\n", - "plt.xlim(0, np.ceil(np.max(df.scan_time)))\n", - "plt.title('All mass features, all samples')\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "f8de4ea2-7614-43b1-b5ea-e42712847e81", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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icuXL2P8+PE4efIk+vfvL/fpISIiok+URqdReXvxNfDqmEtvFlHqOHnyJOrXry9df13IBAYGYtmyZRgxYgQyMjLQq1cvPHnyBLVr10ZERITKDNeqVavQv39/NGzYEAYGBmjTpg3mzZsntdvY2GD37t3o168ffHx8ULhwYYSGhqocy6lmzZpYvXo1xowZg59++gleXl7YvHkzypcvL+vxEBER0adL9nGa6tSpg7Zt22LAgAGwsrLC2bNn4enpiQEDBuDatWuIiIjQVVa99r7jPPA4TURERPpJznGaZM80TZkyBU2aNMHFixfx8uVLzJ07FxcvXsSRI0dw4MABjUMTERER6TPZa5pq166NuLg4vHz5Et7e3ti9ezccHR0RExMDHx8fXWQkIiIiKnBqzTQNGTIEkyZNgoWFBaKjo1GzZk0sXrxY19mIiIiI9IZaM03z589Heno6AKB+/fpISUnRaSgiIiIifaPWTJOHhwfmzZuHRo0aQQiBmJgY6RhHb6tTp45WAxIRERHpA7WKphkzZqBPnz6YOnUqFAoFWrVqlWc/hUKBnJwcrQYkIiIi0gdqFU0BAQEICAhAeno6rK2tceXKlTyP1URERET0qZJ1yAFLS0vs27cPnp6eMDLS+Fy/RERERB8d2ZVP3bp1dZGDiIiISK/JPk4TERER0eeIRRMRERGRGlg0EREREakh30VTWloaNm/ejEuXLmkjDxEREZFekl00tWvXDr/++isA4Pnz56hatSratWuHChUqYMOGDVoPSERERKQPZBdN0dHR+PrrrwEAmzZtghACT548wbx58zB58mStByQiIiLSB7KLptTUVNjb2wMAIiIi0KZNGxQqVAjNmjXDtWvXtB6QiIiISB/ILprc3NwQExODjIwMREREoFGjRgCAx48fw8zMTOsBiYiIiPSB7INbhoSEoFOnTrC0tIS7uzvq1asH4NVuO29vb23nIyIiItILsoumvn37onr16rh9+za++eYbGBi8mqz68ssvuaaJiIiIPlkanUCuatWqqFq1KgAgJycH586dQ82aNWFnZ6fVcERERET6QvaappCQEPz5558AXhVMdevWRZUqVeDm5ob9+/drOx8RERGRXpBdNP3zzz+oWLEiAGDr1q2Ij4/H5cuXMXjwYIwePVrrAYmIiIj0geyi6eHDh3B2dgYA7NixA23btkXJkiUR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EhKCyZMnw8vLC56enhg7dixcXV0REBAAAChTpgwaN26Mnj17Ijw8HNnZ2ejfvz/at28PV1dXAEDHjh0xYcIEBAcH48cff8T58+cxd+5crexiJCIiok+D7KKpS5cuusiRp/nz52Ps2LHo27cv7t+/D1dXV/Tu3RuhoaFSnxEjRiAjIwO9evXCkydPULt2bURERKgUdatWrUL//v3RsGFDGBgYoE2bNpg3b57UbmNjg927d6Nfv37w8fFB4cKFERoaysMNEBERkUQh3jy8thoyMjIQFhaGqKgo3L9/X9pV9tqNGze0GvBjkZaWBhsbG6SmpqrsqgQAj5Hb1R7nZlgzbUcjIiKid3jf+/fbZM809ejRAwcOHECXLl3g4uIChUKhcVAiIiKij4Xsomnnzp3Yvn07atWqpYs8RERERHpJ9rfn7OzsYG9vr4ssRERERHpLdtE0adIkhIaG4tmzZ7rIQ0RERKSXZO+emzlzJq5fvw4nJyd4eHjA2NhYpf306dNaC0dERESkL2QXTa+Pf0RERET0OZFdNI0bN04XOYiIiIj0Wr5O2EtERET0uZA905STk4PZs2dj3bp1SEhIQFZWlkp7SkqK1sIRERER6QvZM00TJkzArFmz8P333yM1NRVDhgxB69atYWBggPHjx+sgIhEREVHBk100rVq1CosXL8bQoUNhZGSEDh064I8//kBoaCiOHj2qi4xEREREBU520ZSUlARvb28AgKWlJVJTUwEA3377LbZvV/8ca0REREQfE9lFU9GiRZGYmAgAKF68OHbv3g0AOHHiBExNTbWbjoiIiEhPyC6aWrVqhaioKADAgAEDMHbsWHh5eaFr164ICgrSekAiIiIifSD723NhYWHSz99//z2KFSuGmJgYeHl5oXnz5loNR0RERKQvZBdNb/P19YWvr682shARERHpLY2Kpnv37uHQoUO4f/8+lEqlStvAgQO1EoyIiIhIn8gumpYtW4bevXvDxMQEDg4OUCgUUptCoWDRRERERJ8k2UXT2LFjERoailGjRsHAgGdhISIios+D7Krn2bNnaN++PQsmIiIi+qzIrnyCg4Oxfv16XWQhIiIi0luyd89NnToV3377LSIiIuDt7Q1jY2OV9lmzZmktHBEREZG+0Kho2rVrF0qVKgUAuRaCExEREX2KZBdNM2fOxJIlS9CtWzcdxCEiIiLS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c61v8hi5Xe1xboY103Y0IiIieof3vX+/TfbuueTkZHTp0gWurq4wMjKCoaGhyoWIiIjoUyR791y3bt2QkJCAsWPHwsXF5YN9k46IiIioIMkumg4dOoSDBw+iUqVKOohDREREpJ9k755zc3MDl0ERERHR50Z20TRnzhyMHDkSN2/e1EEcIiIiIv2k1u45Ozs7lbVLGRkZKF68OAoVKgRjY2OVvikpKdpNSERERKQH1Cqa5syZo+MYRERERPpNraKpoE+OS0RERFTQZK9pMjQ0zHUCXAB49OgRj9NEREREnyzZRdO7vjmXmZkJExOTfAciIiIi0kdqH6fp9bnaFAoF/vjjD1haWkptOTk5iI6ORunSpbWfkIiIiEgPqF00zZ49G8Crmabw8HCVXXEmJibw8PBAeHi49hMSERER6QG1i6b4+HgAQP369bFx40bY2dnpLBQRERGRvpF9GpV9+/bpIgcRERGRXlOraBoyZAgmTZoECwsLDBky5L19Z82apZVgRERERPpEraIpNjYW2dnZ0s/v8uZRw4mIiIg+JWoVTW/ukuPuOSIiIvocyT5O0969e5GZmamLLERERER6S/ZC8BYtWuDly5eoVq0a6tWrh7p166JWrVowNzfXRT4iIiIivSB7punx48eIiopCkyZNcPz4cbRq1Qq2traoVasWxowZo4uMRERERAVOId51XhQ1XbhwATNmzMCqVaugVCqRk5OjrWwflbS0NNjY2CA1NRXW1tYqbR4jt6s9zs2wZtqORkRERO/wvvfvt8nePXf16lXs378f+/fvx4EDB5CZmYmvv/4av/zyC+rVq6dpZiIiIiK9JrtoKl26NIoUKYJBgwZh5MiR8Pb25qEGiIiI6JMne03TwIED8cUXX2DixIno06cPRo8ejd27d+PZs2e6yEdERESkF2QXTXPmzMHp06eRlJSEUaNGISsrC6NHj0bhwoVRq1YtXWQkIiIiKnCyi6bXcnJykJ2djczMTLx48QKZmZm4cuWKNrMRERER6Q2Nds9VqFABTk5O6N27N+7du4eePXsiNjYWDx480EVGIiIiogIneyF4YmIievXqhXr16qF8+fK6yERERESkd2QXTevXr9dFDiIiIiK9pvGaJiIiIqLPCYsmIiIiIjWwaCIiIiJSA4smIiIiIjXILppu376NO3fuSNePHz+OkJAQLFq0SKvB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77//xrp169C0aVNd5CEiIiLSSxotBC9RooQushARERHpLdlF09ChQzF37ly1dp0RERERfSpk7547dOgQ9u3bh507d6JcuXIwNjZWad+4caPWwhERERHpC9lFk62tLVq1aqWLLERERER6S3bRtHTpUl3kICIiItJrstc0AcDLly+xZ88e/P7773j69CkA4N69e0hPT9dqOCIiIiJ9IXum6datW2jcuDESEhKQmZmJb775BlZWVpg2bRoyMzMRHh6ui5xEREREBUr2TNOgQYNQtWpVPH78WOVktq1atcp1/jYiIiKiT4XsmaaDBw/iyJEjuU7O6+Hhgbt372otGBEREZE+kV00KZVK5OTk5Np+584dWFlZaSUUfbo8Rso7CfLNsGY6SkJERCSP7N1zjRo1wpw5c6TrCoUC6enpGDduHE+tQkRERJ8s2TNNM2fOhL+/P8qWLYsXL16gY8eOuHbtGgoXLow1a9boIiMRERFRgZNdNBUtWhRnzpzB33//jbNnzyI9PR3BwcHo1KmTysJwIiIiIl37kMs+ZBdNAGBkZITOnTtrfKdEREREHxvZRdOKFSve2961a1eNwxARERHpK9lF06BBg1SuZ2dn49mzZzAxMUGhQoVYNBEREdEnSfa35x4/fqxySU9Px5UrV1C7dm0uBCciIqJPlkbnnnubl5cXwsLCcs1CEREREX0qNFoInudARka4d++etoYjkk3ONyh40EwiIpJLdtG0ZcsWletCCCQmJuLXX39FrVq1tBaMiIiISJ/ILpoCAgJUrisUChQpUgQNGjTAzJkztZWLiIiISK9odO45IiIios+NVhaCExEREX3qZM80DRkyRO2+s2bNkjs8ERERkV6SXTTFxsYiNjYW2dnZKFWqFADg6tWrMDQ0RJUqVaR+CoVCeymJiIiICpjsoql58+awsrLC8uXLYWdnB+DVAS+7d++Or7/+GkOHDtV6SCIiIqKCJntN08yZMzF16lSpYAIAOzs7TJ48md+eIyIiok+W7KIpLS0NDx48yLX9wYMHePr0qVZCEREREekb2UVTq1at0L17d2zcuBF37tzBnTt3sGHDBgQHB6N169a6yEhERERU4GSvaQoPD8ewYcPQsWNHZGdnvxrEyAjBwcGYMWOG1gMS6QOeooWIiGQXTYUKFcJvv/2GGTNm4Pr16wCA4sWLw8LCQuvhiIiI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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "sum_data = lcms_collection.cluster_summary_dataframe\n", - "freq_fig, ax = plt.subplots()\n", - "sum_data.sample_id_count.value_counts().sort_index().plot(ax = ax, kind = 'bar')\n", - "plt.xlabel('Number of mass features in a cluster')\n", - "plt.ylabel('Frequency of clusters with this many mass features')\n", - "#hist = plt.figure(figsize = (20, 2))\n", - "#plt.hist(df.cluster, bins = df.cluster.unique().shape[0])\n", - "#plt.xlim(0, np.ceil(np.max(df.cluster.unique())))\n", - "#plt.ylim(0, 5)\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "406167c7-483d-4113-be61-d14079cd2415", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkQAAAHHCAYAAABeLEexAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuNSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/xnp5ZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAByWklEQVR4nO3deXxU9dn//9fJZJLJHgKBEA1hE5ABl1JEdNxAFkvdClppBW3d6s/Wu2619lsrSC3V9pa21tbaeqv3bdv7VtxXVlEjIOOCyqBIwk4SAsQkZJ/MnN8fx4wZEiCBSc5M5v18PEaZc86cc51l5lz5nM9imKZpIiIiIhLHEuwOQERERMRuSohEREQk7ikhEhERkbinhEhERETinhIiERERiXtKiERERCTuKSESERGRuKeESEREROKeEiIRERGJe0qIREREJO4pIRKJciUlJdxwww0MHToUl8tFZmYmZ555Jn/84x9paGiwO7xead68eRiG0eHrkUce6ZZtvvbaa8ybN69b1i0iR5ZodwAicmivvvoql112GcnJycydO5cxY8bQ3NxMUVERd9xxBz6fj0cffdTuMHutv/71r6Snp4dNmzBhQrds67XXXuPhhx9WUiRiEyVEIlFq69atXHHFFRQWFrJy5UoGDhwYmnfTTTdRXFzMq6++amOEvd+sWbPo16+f3WEck7q6OtLS0uwOQyTq6ZGZSJR64IEHqK2t5bHHHgtLhloNHz6c//iP/wi9b2lpYcGCBQwbNozk5GQGDx7ML37xC5qamsI+N3jwYL797W9TVFTEaaedhsvlYujQofz3f/932HJ+v5/58+dzwgkn4HK56Nu3Lx6Ph2XLloUt9/nnnzNr1ixycnJwuVx885vf5KWXXgpb5oknnsAwDN59911uvfVWcnNzSUtL49JLL2Xv3r1hy77//vtMmzaNfv36kZKSwpAhQ/jhD38Ymr9q1SoMw2DVqlVhn9u2bRuGYfDEE0+EppWXl/ODH/yA448/nuTkZAYOHMjFF1/Mtm3bDnncu+Kpp55i3LhxpKSkkJOTwxVXXMHOnTvDlnnnnXe47LLLGDRoEMnJyRQUFHDLLbeEPe68+uqrefjhhwHCHs91dX+vvvpq0tPTKSkp4Vvf+hYZGRl8//vfByAYDPKHP/wBt9uNy+ViwIAB3HDDDXz55Zdh6z3S8RfprVRCJBKlXn75ZYYOHcoZZ5zRqeWvvfZannzySWbNmsVtt93Ge++9x8KFC/nss894/vnnw5YtLi5m1qxZXHPNNVx11VX813/9F1dffTXjxo3D7XYDVj2ahQsXcu2113LaaadRU1PD+++/z4cffsiUKVMA8Pl8nHnmmRx33HH8/Oc/Jy0tjaeffppLLrmEZ599lksvvTRsuz/5yU/o06cP99xzD9u2beMPf/gDP/7xj/m///s/ACoqKpg6dSq5ubn8/Oc/Jzs7m23btvHcc88d1TGcOXMmPp+Pn/zkJwwePJiKigqWLVvGjh07GDx48BE/X1lZGfbe4XDQp08fAO677z7uvvtuLr/8cq699lr27t3LQw89xNlnn81HH31EdnY2AM888wz19fXceOON9O3bl3Xr1vHQQw+xa9cunnnmGQBuuOEGSktLWbZsGf/zP/9zVPvaqqWlhWnTpuHxePj9739PampqaBtPPPEEP/jBD7j55pvZunUrf/7zn/noo4949913cTqdET/+IjHFFJGoU11dbQLmxRdf3Knl169fbwLmtddeGzb99ttvNwFz5cqVoWmFhYUmYL799tuhaRUVFWZycrJ52223haadfPLJ5owZMw673cmTJ5tjx441GxsbQ9OCwaB5xhlnmCeccEJo2uOPP24C5vnnn28Gg8HQ9FtuucV0OBxmVVWVaZqm+fzzz5uA6fV6D7nNN9980wTMN998M2z61q1bTcB8/PHHTdM0zS+//NIEzN/97neH3YeO3HPPPSbQ7lVYWGiapmlu27bNdDgc5n333Rf2uU8//dRMTEwMm15fX99u/QsXLjQNwzC3b98emnbTTTeZHf0kd3Z/TdM0r7rqKhMwf/7zn4ct+84775iA+c9//jNs+htvvBE2vTPHX6S30iMzkShUU1MDQEZGRqeWf+211wC49dZbw6bfdtttAO3qGo0ePZqzzjor9D43N5eRI0eyZcuW0LTs7Gx8Ph+bN2/ucJuVlZWsXLmSyy+/nAMHDrBv3z727dvH/v37mTZtGps3b2b37t1hn7n++utDj4IAzjrrLAKBANu3bw9tE+CVV17B7/d3at8PJSUlhaSkJFatWtXusVBnPfvssyxbtiz0+uc//wnAc889RzAY5PLLLw/t9759+8jLy+OEE07gzTffDIujVV1dHfv27eOMM87ANE0++uijY9rHQ7nxxhvD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utoEzOXLl/dMUHHqUOdi165d5nHHHWdu2LDBLCwsNBctWtTjscWbjs7FVVddZV588cXHvO64KSE6nK1bt1JeXs75558fmpaVlcWECRNYs2aNjZGJRJ/q6moMwwgbdFl6XnNzM48++ihZWVmcfPLJdocTd4LBIHPmzOGOO+7A7XbbHU7cW7VqFf3792fkyJHceOON7N+/v8vriKrBXe1SXl4OwIABA8KmDxgwIDRPRKCxsZE777yT2bNna2BLm7zyyitcccUV1NfXM3DgQJYtW0a/fv3sDivu3H///SQmJnLzzTfbHUrcmz59Ot/5zncYMmQIJSUl/OIXv+CCCy5gzZo1OByOTq9HCZGIdIrf7+fyyy/HNE3++te/2h1O3DrvvPNYv349+/bt4+9//zuXX3457733Hv3797c7tLjxwQcf8Mc//pEPP/wQwzDsDifuXXHFFaF/jx07lpNOOolhw4axatUqJk+e3On16JEZkJeXB8CePXvCpu/Zsyc0TySetSZD27dvZ9myZSodslFaWhrDhw/n9NNP57HHHiMxMZHHHnvM7rDiyjvvvENFRQWDBg0iMTGRxMREtm/fzm233cbgwYPtDi/uDR06lH79+lFcXNylzykhAoYMGUJeXh4rVqwITaupqeG9995j4sSJNkYmYr/WZGjz5s0sX76cvn372h2StBEMBmlqarI7jLgyZ84cPvnkE9avXx965efnc8cdd7BkyRK7w4t7u3btYv/+/QwcOLBLn4ubR2a1tbVh2eLWrVtZv349OTk5DBo0iJ/+9Kf8+te/5oQTTmDIkCHcfffd5Ofnc8kll9gXdC92pPNRWVnJjh07Qv3dbNq0CbBK81RqF1mHOxcDBw5k1qxZfPjhh7zyyisEAoFQvbqcnBySkpLsCrtXOty56Nu3L/fddx8XXXQRAwcOZN++fTz88MPs3r2byy67zMaoe6cj/UYd/IeB0+kkLy+PkSNH9nSovd7hzkVOTg7z589n5syZ5OXlUVJSws9+9jOGDx/OtGnTurahY26nFiPefPNNE2j3uuqqq0zTtJre33333eaAAQPM5ORkc/LkyeamTZvsDboXO9L5ePzxxzucf88999gad290uHPR2u1BR68333zT7tB7ncOdi4aGBvPSSy818/PzzaSkJHPgwIHmRRddZK5bt87usHulI/1GHUzN7rvP4c5FfX29OXXqVDM3N9d0Op1mYWGhed1115nl5eVd3o5hmqbZtRRKREREpHdRHSIRERGJe0qIREREJO4pIRIREZG4p4RIRERE4p4SIhEREYl7SohEREQk7ikhEhERkbinhEhEBNi2bRuGYbB+/Xq7QxERGyghEpFus3fvXm688UYGDRpEcnIyeXl5TJs2jXfffdfWuK6++up2w/IUFBRQVlbGmDFj7AlKRGwVN2OZiUjPmzlzJs3NzTz55JMMHTqUPXv2sGLFCvbv3293aO04HA6NkycSx1RCJCLdoqqqinfeeYf777+f8847j8LCQk477TTuuusuLrroorDlbrjhBgYMGIDL5WLMmDG88sorAOzfv5/Zs2dz3HHHkZqaytixY/n3v/8dtp1zzz2Xm2++mZ/97Gfk5OSQl5fHvHnzDhnXvHnzePLJJ3nxxRcxDAPDMFi1alW7R2arVq3CMAyWLFnCqaeeSkpKCpMmTaKiooLXX3+dE088kczMTL73ve9RX18fWn8wGGThwoUMGTKElJQUTj75ZBYvXhy5Aysi3UIlRCLSLdLT00lPT+eFF17g9NNPJzk5ud0ywWCQCy64gAMHDvDUU08xbNgwNm7ciMPhAKCxsZFx48Zx5513kpmZyauvvsqcOXMYNmwYp512Wmg9Tz75JLfeeivvvfcea9as4eqrr+bMM89kypQp7bZ5++2389lnn1FTU8Pjjz8OQE5ODqWlpR3ux7x58/jzn/9Mamoql19+OZdffjnJycn861//ora2lksvvZSHHnqIO++8E4CFCxfy1FNP8cgjj3DCCSfw9ttvc+WVV5Kbm8s555xzzMdVRLpJxIelFRH5yuLFi80+ffqYLpfLPOOMM8y77rrL/Pjjj0PzlyxZYiYkJJibNm3q9DpnzJhh3nbbbaH355xzjunxeMKWGT9+vHnnnXcech1XXXWVefHFF4dN27p1qwmYH330kWmaX4+wvXz58tAyCxcuNAGzpKQkNO2GG24wp02bZpqmaTY2Npqpqanm6tWrw9Z9zTXXmLNnz+70PopIz9MjMxHpNjNnzqS0tJSXXnqJ6dOns2rVKr7xjW/wxBNPALB+/XqOP/54RowY0eHnA4EACxYsYOzYseTk5JCens6SJUvYsWNH2HInnXRS2PuBAwdSUVERkX1ou+4BAwaQmprK0KFDw6a1bqu4uJj6+nqmTJkSKiFLT0/nv//7vykpKYlIPCLSPfTITES6lcvlYsqUKUyZMoW7776ba6+9lnvuuYerr76alJSUw372d7/7HX/84x/5wx/+wNixY0lLS+OnP/0pzc3NYcs5nc6w94ZhEAwGIxJ/23UbhnHYbdXW1gLw6quvctxxx4Ut19EjQxGJHkqIRKRHjR49mhdeeAGwSl927drFF1980WEp0bvvvsvFF1/MlVdeCVh1jr744gtGjx59TDEkJSURCASOaR0dGT16NMnJyezYsUP1hURijBIiEekW+/fv57LLLuOHP/whJ510EhkZGbz//vs88MADXHzxxQCcc845nH322cycOZMHH3yQ4cOH8/nnn2MYBtOnT+eEE05g8eLFrF69mj59+vDggw+yZ8+eY06IBg8ezJIlS9i0aRN9+/YlKysrErtMRkYGt99+O7fccgvBYBCPx0N1dTXvvvsumZmZXHXVVRHZjohEnhIiEekW6enpTJgwgUWLFlFSUoLf76egoIDrrruOX/ziF6Hlnn32WW6//XZmz55NXV0dw4cP57e//S0Av/zlL9myZQvTpk0jNTWV66+/nksuuYTq6upjiu26665j1apVfPOb36S2tpY333yTwYMHH9M6Wy1YsIDc3FwWLlzIli1byM7O5hvf+EbYPotI9DFM0zTtDkJERETETmplJiIiInFPCZGIiIjEPSVEIiIiEveUEImIiEjcU0IkIiIicU8JkYiIiMQ9JUQiIiIS95QQiYiISNxTQiQiIiJxTwmRiIiIxD0lRCIiIhL3lBCJiIhI3Pv/AWZZ3bKNpyTcAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "sum_df = lcms_collection.cluster_summary_dataframe.copy()\n", - "trunc_df = sum_df[sum_df.sample_id_count > 10]\n", - "\n", - "## generate mass feature figure\n", - "fig = plt.figure()\n", - "plt.scatter(\n", - " df.scan_time_aligned,\n", - " df.mz,\n", - " c = 'tab:gray',\n", - " s = 1 ## larger dots when scaled in\n", - ")\n", - "\n", - "plt.scatter(\n", - " trunc_df.scan_time_aligned_median,\n", - " trunc_df.mz_median, \n", - " c = 'tab:orange',\n", - " alpha = 0.7, \n", - " s = (trunc_df.sample_id_count**2)/10\n", - ")\n", - "\n", - "## add a scale bar for the orange dots when zoomed in\n", - "plt.xlabel('Scan time')\n", - "plt.ylabel('m/z')\n", - "#plt.ylim(0, np.ceil(np.max(df.mz)))\n", - "#plt.xlim(0, np.ceil(np.max(df.scan_time)))\n", - "plt.ylim(500, 550)\n", - "plt.xlim(10, 15)\n", - "\n", - "plt.title('Consensus Features')\n", - "plt.show()\n", - "\n", - "## 3rd option: also just map of consensus/ also can zoom in" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [conda env:corems] *", - "language": "python", - "name": "conda-env-corems-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.13" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/support_code/nmdc/lipidomics/lipidomics_collection_working.py b/support_code/nmdc/lipidomics/lipidomics_collection_working.py deleted file mode 100644 index c7ea272f8..000000000 --- a/support_code/nmdc/lipidomics/lipidomics_collection_working.py +++ /dev/null @@ -1,96 +0,0 @@ -from pathlib import Path -import time -from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectraCollection - - -if __name__ == "__main__": - # Set the path to the collection of LCMS runs (previously processed) - collection_path = Path( - "/Users/cies677/sandbox/corems/support_code/nmdc/lipidomics/curr/processed/pos" - ) - # Path to manifest file - manifest_file = collection_path / "manifest_curr.csv" - # This file will need to be created by the user or helper script? - chromatography_file = collection_path / "long_lipid_gradient_chroma.csv" - - # Set the number of cores to use for loading the data (the parser is parallelized) - ncores = 8 - - # Instantiate the parser - parser = ReadCoreMSHDFMassSpectraCollection( - folder_location = collection_path, - manifest_file = manifest_file, - chromatography_file = chromatography_file, - cores = ncores - ) - print( - "Loading LCMS collection with", - len(parser.manifest), - "samples using", - ncores, - " cores" - ) - - # Load the LCMS collection (minimally load the data) - start_time = time.time() - lcms_collection = parser.get_lcms_collection( - load_raw=False, - load_light=True - ) - print( - "Time to load LCMS collection ", - time.time() - start_time, - "seconds -", - len(lcms_collection), - " LCMS runs and ", - ncores, - " cores" - ) - #10s for 7 samples, 10 cores; 162s for 70 samples, 10 cores - - # Set flag to call _drop_isotopologue() when running _check_mass_features_df() - lcms_collection.parameters.lcms_collection.drop_isotopologues = True - print( - "Number of total mass features: ", - len(lcms_collection.mass_features_dataframe) - ) - - # Align the LCMS runs between each other - print("Aligning LCMS collection") - start_time = time.time() - lcms_collection.align_lcms_objects() - print( - "Time to align LCMS collection: ", - time.time() - start_time, - "seconds" - ) - #1.5s for 7 samples; 15s for 70 samples - - # Make some plots - lcms_collection.plot_tics(type="both") - lcms_collection.plot_alignments() - # TODO: Think about other plots that would be useful to have here for assessing the quality of the data and alignment - - # Make consensus mass features from the consolidated mass features - start_time = time.time() - - ## Inconsistently getting a repeated RuntimeWarning: Mean of empty slice - ## return np.nanmean(a, axis, out = out, keepdims = keepdims) - ## Should check what causes this, make sure output is understood, and if - ## yes suppress the warning - - lcms_collection.add_consensus_mass_features() - # THIS FUNCTION NEEDS WORK AND NEEDS MECHANISM TO EVALUATE THE RESULTS (plots? reports?) - print( - "Time to roll up consensus mass features: ", - time.time() - start_time, - "seconds -", - len(lcms_collection.mass_features_dataframe), - " total mass features", - ncores, - " cores" - ) - - #TODO: Add code to load and save information about chromatographic settings - #TODO: Add code to save and load collection to HDF5 file - #TODO: Add code to plot a consensus mass feature \ No newline at end of file diff --git a/support_code/nmdc/lipidomics/lipidomics_workflow.py b/support_code/nmdc/lipidomics/lipidomics_workflow.py deleted file mode 100644 index 2e1783cd8..000000000 --- a/support_code/nmdc/lipidomics/lipidomics_workflow.py +++ /dev/null @@ -1,639 +0,0 @@ -""" -Notes --------- -Assumes that ms1 are collected in profile mode, persistent homology not applicable for centroided data. -""" - -import sys - -sys.path.append("./") -from multiprocessing import Pool -from pathlib import Path -import datetime -import toml -import warnings - -import pandas as pd -import time - -from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectra -from corems.mass_spectra.input.mzml import MZMLSpectraParser -from corems.mass_spectra.input.rawFileReader import ImportMassSpectraThermoMSFileReader -from corems.mass_spectra.output.export import LipidomicsExport -from corems.molecular_id.search.molecularFormulaSearch import SearchMolecularFormulas, SearchMolecularFormulasLC -from corems.molecular_id.search.database_interfaces import MetabRefLCInterface -from corems.encapsulation.input.parameter_from_json import ( - load_and_set_toml_parameters_lcms, -) - - -def instantiate_lcms_obj(file_in): - """Instantiate a corems LCMS object from a binary file. Pull in ms1 spectra into dataframe (without storing as MassSpectrum objects to save memory) - - Parameters - ---------- - file_in : str or Path - Path to binary file - verbose : bool - Whether to print verbose output - - Returns - ------- - myLCMSobj : corems LCMS object - LCMS object with ms1 spectra in dataframe - """ - # Instantiate parser based on binary file type - if ".raw" in str(file_in): - parser = ImportMassSpectraThermoMSFileReader(file_in) - - if ".mzML" in str(file_in): - parser = MZMLSpectraParser(file_in) - - # Instantiate lc-ms data object using parser and pull in ms1 spectra into dataframe (without storing as MassSpectrum objects to save memory) - myLCMSobj = parser.get_lcms_obj(spectra="ms1") - - return myLCMSobj - - -def set_params_on_lcms_obj(myLCMSobj, params_toml, verbose): - """Set parameters on the LCMS object - - Parameters - ---------- - myLCMSobj : corems LCMS object - LCMS object to set parameters on - params_toml : str or Path - Path to toml file with parameters - - Returns - ------- - None, sets parameters on the LCMS object - """ - # Load parameters from toml file - load_and_set_toml_parameters_lcms(myLCMSobj, params_toml) - - # If myLCMSobj is a positive mode, remove Cl from atoms used in molecular search - # This cuts down on the number of molecular formulas searched hugely - if myLCMSobj.polarity == "positive": - myLCMSobj.parameters.mass_spectrum["ms1"].molecular_search.usedAtoms.pop("Cl") - elif myLCMSobj.polarity == "negative": - myLCMSobj.parameters.mass_spectrum["ms1"].molecular_search.usedAtoms.pop("Na") - - if verbose: - print("Parameters set on LCMS object") - - -def load_scan_translator(scan_translator=None): - """Translate scans using a scan translator - - Parameters - ---------- - scan_translator : str or Path - Path to scan translator yaml file - - Returns - ------- - scan_dict : dict - Dict with keys as parameter keys and values as lists of scans - """ - # Convert the scan translator to a dictionary - if scan_translator is None: - scan_translator_dict = {"ms2": {"scan_filter": "", "resolution": "high"}} - else: - # Convert the scan translator to a dictionary - if isinstance(scan_translator, str): - scan_translator = Path(scan_translator) - # read in the scan translator from toml - with open(scan_translator, "r") as f: - scan_translator_dict = toml.load(f) - for param_key in scan_translator_dict.keys(): - if scan_translator_dict[param_key]["scan_filter"] == "": - scan_translator_dict[param_key]["scan_filter"] = None - return scan_translator_dict - - -def check_scan_translator(myLCMSobj, scan_translator): - """Check if scan translator is provided and that it maps correctly to scans and parameters""" - scan_translator_dict = load_scan_translator(scan_translator) - # Check that the scan translator maps correctly to scans and parameters - scan_df = myLCMSobj.scan_df - scans_pulled_out = [] - for param_key in scan_translator_dict.keys(): - assert param_key in myLCMSobj.parameters.mass_spectrum.keys() - assert "scan_filter" in scan_translator_dict[param_key].keys() - assert "resolution" in scan_translator_dict[param_key].keys() - # Pull out scans that match the scan filter - scan_df_sub = scan_df[ - scan_df.scan_text.str.contains( - scan_translator_dict[param_key]["scan_filter"] - ) - ] - scans_pulled_out.extend(scan_df_sub.scan.tolist()) - if len(scan_df_sub) == 0: - raise ValueError( - "No scans pulled out by scan translator for parameter key: ", - param_key, - " and scan filter: ", - scan_translator_dict[param_key]["scan_filter"], - ) - - # Check that the scans pulled out by the scan translator are not overlapping and assert error if they are - if len(set(scans_pulled_out)) != len(scans_pulled_out): - raise ValueError("Overlapping scans pulled out by scan translator") - - -def add_mass_features(myLCMSobj, scan_translator): - """Process ms1 spectra and perform molecular search - - This includes peak picking, adding and processing associated ms1 spectra, - integration of mass features, annotation of c13 mass features, deconvolution of ms1 mass features, - and adding of peak shape metrics of mass features to the mass feature dataframe. - - Parameters - ---------- - myLCMSobj : corems LCMS object - LCMS object to process - scan_translator : str or Path - Path to scan translator yaml file - - Returns - ------- - None, processes the LCMS object - """ - myLCMSobj.find_mass_features() - myLCMSobj.add_associated_ms1( - auto_process=True, use_parser=False, spectrum_mode="profile" - ) - myLCMSobj.integrate_mass_features(drop_if_fail=True) - # Count and report how many mass features are left after integration - print("Number of mass features after integration: ", len(myLCMSobj.mass_features)) - myLCMSobj.find_c13_mass_features() - myLCMSobj.deconvolute_ms1_mass_features() - myLCMSobj.add_peak_metrics() - - scan_dictionary = load_scan_translator(scan_translator=scan_translator) - for param_key in scan_dictionary.keys(): - scan_filter = scan_dictionary[param_key]["scan_filter"] - if scan_filter == "": - scan_filter = None - myLCMSobj.add_associated_ms2_dda( - spectrum_mode="centroid", ms_params_key=param_key, scan_filter=scan_filter - ) - - -def molecular_formula_search(myLCMSobj): - """Perform molecular search on ms1 spectra - - Parameters - ---------- - myLCMSobj : corems LCMS object - LCMS object to process - - Returns - ------- - None, processes the LCMS object - """ - mol_search = SearchMolecularFormulasLC(myLCMSobj) - mol_search.run_mass_feature_search() - print("Finished molecular search") - - -def export_results(myLCMSobj, out_path, molecular_metadata=None, final=False): - """Export results to hdf5 and csv as a lipid report - - Parameters - ---------- - myLCMSobj : corems LCMS object - LCMS object to process - out_path : str or Path - Path to output file - molecular_metadata : dict - Dict with molecular metadata - final : bool - Whether to export final results - - Returns - ------- - None, exports results to hdf5 and csv as a lipid report - """ - exporter = LipidomicsExport(out_path, myLCMSobj) - exporter.to_hdf(overwrite=True) - if final: - # Do not show warnings, these are expected - exporter.report_to_csv(molecular_metadata=molecular_metadata) - else: - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - exporter.report_to_csv() - - -def save_times(myLCMSobj, time_start, out_path, time_end=None): - """Get times for processing steps - - Parameters - ---------- - myLCMSobj : corems LCMS object - LCMS object to process - time_start : float - Start time of processing - out_path : str or Path - Path to output file - time_end : float - End time of processing - - Returns - ------- - None, writes out times to a file within the output directory - """ - # Check if out_path (with .corems) exisits - out_dir = Path(str(out_path) + ".corems/") - if not out_dir.exists(): - print("Output directory does not exist") - - time_toml_path = out_dir / "times.toml" - if not time_toml_path.exists(): - raw_data_creation_time = myLCMSobj.spectra_parser.get_creation_time().strftime( - "%Y-%m-%dT%H:%M:%SZ" - ) - processed_data_creation_time = time_start.strftime("%Y-%m-%dT%H:%M:%SZ") - time_dict = { - "raw_data_creation_time": raw_data_creation_time, - "metabolomics_workflow_start_time": processed_data_creation_time, - } - # save as a toml file - toml_string = toml.dumps(time_dict) # Output to a string - with open(time_toml_path, "w") as f: - f.write(toml_string) - elif time_toml_path.exists() and time_end is not None: - time_dict = toml.load(time_toml_path) - time_dict["metabolomics_workflow_end_time"] = time_end.strftime( - "%Y-%m-%dT%H:%M:%SZ" - ) - toml_string = toml.dumps(time_dict) - with open(time_toml_path, "w") as f: - f.write(toml_string) - - -def process_ms2(myLCMSobj, metadata, scan_translator): - """Process ms2 spectra and perform molecular search - - Parameters - ---------- - myLCMSobj : corems LCMS object - LCMS object to process - metadata : dict - Dict with keys "mzs", "fe", and "molecular_metadata" with values of dicts of precursor mzs (negative and positive), flash entropy search databases (negative and positive), and molecular metadata, respectively - - Returns - ------- - None, processes the LCMS object - """ - # Perform molecular search on ms2 spectra - # Grab fe from metatdata associated with polarity (this is inherently high resolution as its from a in-silico high res library) - fe_search = metadata["fe"][myLCMSobj.polarity] - - scan_dictionary = load_scan_translator(scan_translator) - ms2_scan_df = myLCMSobj.scan_df[myLCMSobj.scan_df.ms_level == 2] - - # Process high resolution MS2 scans - # Collect all high resolution MS2 scans using the scan translator - for param_key in scan_dictionary.keys(): - ms2_scans_oi_hr = [] - if scan_dictionary[param_key]["resolution"] == "high": - scan_filter = scan_dictionary[param_key]["scan_filter"] - if scan_filter is not None: - ms2_scan_df_hr = ms2_scan_df[ - ms2_scan_df.scan_text.str.contains(scan_filter) - ] - else: - ms2_scan_df_hr = ms2_scan_df - ms2_scans_oi_hr_i = [ - x for x in ms2_scan_df_hr.scan.tolist() if x in myLCMSobj._ms.keys() - ] - ms2_scans_oi_hr.extend(ms2_scans_oi_hr_i) - # Perform search on high res scans - if len(ms2_scans_oi_hr) > 0: - myLCMSobj.fe_search( - scan_list=ms2_scans_oi_hr, fe_lib=fe_search, peak_sep_da=0.01 - ) - - # Process low resolution MS2 scans - # Collect all low resolution MS2 scans using the scan translator - for param_key in scan_dictionary.keys(): - ms2_scans_oi_lr = [] - if scan_dictionary[param_key]["resolution"] == "low": - scan_filter = scan_dictionary[param_key]["scan_filter"] - if scan_filter is not None: - ms2_scan_df_lr = ms2_scan_df[ - ms2_scan_df.scan_text.str.contains(scan_filter) - ] - else: - ms2_scan_df_lr = ms2_scan_df - ms2_scans_oi_lri = [ - x for x in ms2_scan_df_lr.scan.tolist() if x in myLCMSobj._ms.keys() - ] - ms2_scans_oi_lr.extend(ms2_scans_oi_lri) - # Perform search on low res scans - if len(ms2_scans_oi_lr) > 0: - # Recast the flashentropy search database to low resolution - metabref = MetabRefLCInterface() - fe_search_lr = metabref._to_flashentropy( - metabref_lib=fe_search, - normalize=True, - fe_kwargs={ - "normalize_intensity": True, - "min_ms2_difference_in_da": 0.4, - "max_ms2_tolerance_in_da": 0.2, - "max_indexed_mz": 3000, - "precursor_ions_removal_da": None, - "noise_threshold": 0, - }, - ) - myLCMSobj.fe_search( - scan_list=ms2_scans_oi_lr, fe_lib=fe_search_lr, peak_sep_da=0.3 - ) - - -def run_lipid_sp_ms1( - file_in, - out_path, - params_toml, - scan_translator=None, - verbose=True, - return_mzs=True, - ms1_molecular_search=True, -): - """Run signal processing, get associated ms1, add associated ms2, do ms1 molecular search, and export intermediate results - - Parameters - ---------- - file_in : str or Path - Path to binary file - out_path : str or Path - Path to output file - params_toml : str or Path - Path to toml file with parameters - verbose : bool - Whether to print verbose output - return_mzs : bool - Whether to return precursor mzs - - Returns - ------- - mz_dict : dict - Dict with keys "positive" and "negative" and values of lists of precursor mzs - """ - time_start = datetime.datetime.now() - myLCMSobj = instantiate_lcms_obj(file_in) - set_params_on_lcms_obj(myLCMSobj, params_toml, verbose) - check_scan_translator(myLCMSobj, scan_translator) - add_mass_features(myLCMSobj, scan_translator) - myLCMSobj.remove_unprocessed_data() - #myLCMSobj.parameters.mass_spectrum['ms1'].molecular_search.verbose_processing = False - if ms1_molecular_search: - molecular_formula_search(myLCMSobj) - export_results(myLCMSobj, out_path=out_path, final=False) - save_times(myLCMSobj, time_start, out_path) - if return_mzs: - precursor_mz_list = list( - set( - [ - v.mz - for k, v in myLCMSobj.mass_features.items() - if len(v.ms2_scan_numbers) > 0 and v.isotopologue_type is None - ] - ) - ) - mz_dict = {myLCMSobj.polarity: precursor_mz_list} - return mz_dict - - -def prep_metadata(mz_dicts, out_dir): - """Prepare metadata for ms2 spectral search - - Parameters - ---------- - mz_dicts : list of dicts - List of dicts with keys "positive" and "negative" and values of lists of precursor mzs - out_dir : Path - Path to output directory - - Returns - ------- - metadata : dict - Dict with keys "mzs", "fe", and "molecular_metadata" with values of dicts of precursor mzs (negative and positive), flash entropy search databases (negative and positive), and molecular metadata, respectively - - Notes - ------- - Also writes out files for the flash entropy search databases and molecular metadata - """ - metadata = { - "mzs": {"positive": None, "negative": None}, - "fe": {"positive": None, "negative": None}, - "molecular_metadata": {}, - } - for d in mz_dicts: - metadata["mzs"].update(d) - - metabref = MetabRefLCInterface() - - print("Preparing positive lipid library") - if metadata["mzs"]["positive"] is not None: - metabref_positive, lipidmetadata_positive = metabref.get_lipid_library( - mz_list=metadata["mzs"]["positive"], - polarity="positive", - mz_tol_ppm=5, - format="flashentropy", - normalize=True, - fe_kwargs={ - "normalize_intensity": True, - "min_ms2_difference_in_da": 0.02, # for cleaning spectra - "max_ms2_tolerance_in_da": 0.01, # for setting search space - "max_indexed_mz": 3000, - "precursor_ions_removal_da": None, - "noise_threshold": 0, - }, - ) - metadata["fe"]["positive"] = metabref_positive - metadata["molecular_metadata"].update(lipidmetadata_positive) - fe_positive_df = pd.DataFrame.from_dict( - {k: v for k, v in enumerate(metadata["fe"]["positive"])}, orient="index" - ) - fe_positive_df.to_csv(out_dir / "ms2_db_positive.csv") - - print("Preparing negative lipid library") - if metadata["mzs"]["negative"] is not None: - metabref_negative, lipidmetadata_negative = metabref.get_lipid_library( - mz_list=metadata["mzs"]["negative"], - polarity="negative", - mz_tol_ppm=5, - mz_tol_da_api=0.01, - format="flashentropy", - normalize=True, - fe_kwargs={ - "normalize_intensity": True, - "min_ms2_difference_in_da": 0.02, # for cleaning spectra - "max_ms2_tolerance_in_da": 0.01, # for setting search space - "max_indexed_mz": 3000, - "precursor_ions_removal_da": None, - "noise_threshold": 0, - }, - ) - metadata["fe"]["negative"] = metabref_negative - metadata["molecular_metadata"].update(lipidmetadata_negative) - fe_negative_df = pd.DataFrame.from_dict( - {k: v for k, v in enumerate(metadata["fe"]["negative"])}, orient="index" - ) - fe_negative_df.to_csv(out_dir / "ms2_db_negative.csv") - - mol_metadata_df = pd.concat( - [ - pd.DataFrame.from_dict(v.__dict__, orient="index").transpose() - for k, v in metadata["molecular_metadata"].items() - ], - ignore_index=True, - ) - mol_metadata_df.to_csv(out_dir / "molecular_metadata.csv") - return metadata - - -def run_lipid_ms2(out_path, metadata, scan_translator=None): - """Run ms2 spectral search and export final results - - Parameters - ---------- - out_path : str or Path - Path to output file - metadata : dict - Dict with keys "mzs", "fe", and "molecular_metadata" with values of dicts of precursor mzs (negative and positive), flash entropy search databases (negative and positive), and molecular metadata, respectively - - Returns - ------- - None, runs ms2 spectral search and exports final results - """ - # Read in the intermediate results - out_path_hdf5 = str(out_path) + ".corems/" + out_path.stem + ".hdf5" - parser = ReadCoreMSHDFMassSpectra(out_path_hdf5) - myLCMSobj = parser.get_lcms_obj() - - # Process ms2 spectra, perform spectral search, and export final results - process_ms2(myLCMSobj, metadata, scan_translator=scan_translator) - export_results(myLCMSobj, str(out_path), metadata["molecular_metadata"], final=True) - time_end = datetime.datetime.now() - save_times(myLCMSobj, time_start=None, out_path=out_path, time_end=time_end) - - -def run_lipid_workflow( - file_dir, - out_dir, - params_toml, - scan_translator=None, - verbose=True, - ms1_molecular_search=True, - cores=1, -): - """Run lipidomics workflow - - Parameters - ---------- - file_dir : str or Path - Path to directory with raw or mzml lipid files - out_dir : str or Path - Path to output directory - params_toml : str or Path - Path to toml file with parameters - verbose : bool - Whether to print verbose output - cores : int - Number of cores to use - - Returns - ------- - None, runs lipidomics workflow and exports final results - """ - # Make output dir and get list of files to process - out_dir.mkdir(parents=True, exist_ok=True) - files_list = list(file_dir.glob("*.raw")) - out_paths_list = [out_dir / f.stem for f in files_list] - - # Run signal processing, get associated ms1, add associated ms2, and export temp results - if cores == 1 or len(files_list) == 1: - mz_dicts = [] - for file_in, file_out in list(zip(files_list, out_paths_list)): - # Check if folder already exists - if Path(str(file_out) + ".corems").exists(): - print("File already exists, skipping: ", file_out) - continue - if verbose: - print("Processing file: ", file_in) - mz_dict = run_lipid_sp_ms1( - file_in=str(file_in), - out_path=str(file_out), - params_toml=params_toml, - scan_translator=scan_translator, - verbose=verbose, - ms1_molecular_search=ms1_molecular_search, - ) - mz_dicts.append(mz_dict) - elif cores > 1: - pool = Pool(cores) - args = [ - ( - str(file_in), - str(file_out), - params_toml, - scan_translator, - verbose, - ms1_molecular_search, - ) - for file_in, file_out in list(zip(files_list, out_paths_list)) - ] - mz_dicts = pool.starmap(run_lipid_sp_ms1, args) - pool.close() - pool.join() - - # Skip ms2 spectral search for now, will add back once we have an improved MetabRefLCInterface method - """ - # Prepare ms2 spectral search space - metadata = prep_metadata(mz_dicts, out_dir) - - # Run ms2 spectral search and export final results - if cores == 1 or len(files_list) == 1: - for file_out in out_paths_list: - mz_dicts = run_lipid_ms2( - file_out, metadata, scan_translator=scan_translator - ) - elif cores > 1: - pool = Pool(cores) - args = [(file_out, metadata, scan_translator) for file_out in out_paths_list] - mz_dicts = pool.starmap(run_lipid_ms2, args) - pool.close() - pool.join() - """ - print("Finished processing, data are written in " + str(out_dir)) - - -if __name__ == "__main__": - # Set input variables to run - cores = 1 - file_dir = Path("/Users/cies677/sandbox/corems/support_code/nmdc/lipidomics/curr/raw/") - out_dir = Path("tmp_data/_test_250115") - params_toml = Path("/Users/cies677/sandbox/corems/support_code/nmdc/lipidomics/curr/emsl_lipidomics_corems_params.toml") - verbose = True - scan_translator = Path("/Users/cies677/sandbox/corems/support_code/nmdc/lipidomics/curr/emsl_lipidomics_scan_translator.toml") - - # Set up output directory - out_dir.mkdir(parents=True, exist_ok=True) - - # if cores > 1, don't use verbose output - if cores > 2: - verbose = False - - run_lipid_workflow( - file_dir=file_dir, - out_dir=out_dir, - params_toml=params_toml, - scan_translator=scan_translator, - verbose=verbose, - cores=cores, - ) From ae844e14e5077c863de1a4ad96e2bb125d06b351 Mon Sep 17 00:00:00 2001 From: "Ciesielski, Danielle K" Date: Mon, 14 Apr 2025 15:31:08 -0700 Subject: [PATCH 038/158] replacing workflow file --- .../nmdc/lipidomics/lipidomics_workflow.py | 639 ++++++++++++++++++ 1 file changed, 639 insertions(+) create mode 100644 support_code/nmdc/lipidomics/lipidomics_workflow.py diff --git a/support_code/nmdc/lipidomics/lipidomics_workflow.py b/support_code/nmdc/lipidomics/lipidomics_workflow.py new file mode 100644 index 000000000..753599eee --- /dev/null +++ b/support_code/nmdc/lipidomics/lipidomics_workflow.py @@ -0,0 +1,639 @@ +""" +Notes +-------- +Assumes that ms1 are collected in profile mode, persistent homology not applicable for centroided data. +""" + +import sys + +sys.path.append("./") +from multiprocessing import Pool +from pathlib import Path +import datetime +import toml +import warnings + +import pandas as pd +import time + +from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectra +from corems.mass_spectra.input.mzml import MZMLSpectraParser +from corems.mass_spectra.input.rawFileReader import ImportMassSpectraThermoMSFileReader +from corems.mass_spectra.output.export import LipidomicsExport +from corems.molecular_id.search.molecularFormulaSearch import SearchMolecularFormulas, SearchMolecularFormulasLC +from corems.molecular_id.search.database_interfaces import MetabRefLCInterface +from corems.encapsulation.input.parameter_from_json import ( + load_and_set_toml_parameters_lcms, +) + + +def instantiate_lcms_obj(file_in): + """Instantiate a corems LCMS object from a binary file. Pull in ms1 spectra into dataframe (without storing as MassSpectrum objects to save memory) + + Parameters + ---------- + file_in : str or Path + Path to binary file + verbose : bool + Whether to print verbose output + + Returns + ------- + myLCMSobj : corems LCMS object + LCMS object with ms1 spectra in dataframe + """ + # Instantiate parser based on binary file type + if ".raw" in str(file_in): + parser = ImportMassSpectraThermoMSFileReader(file_in) + + if ".mzML" in str(file_in): + parser = MZMLSpectraParser(file_in) + + # Instantiate lc-ms data object using parser and pull in ms1 spectra into dataframe (without storing as MassSpectrum objects to save memory) + myLCMSobj = parser.get_lcms_obj(spectra="ms1") + + return myLCMSobj + + +def set_params_on_lcms_obj(myLCMSobj, params_toml, verbose): + """Set parameters on the LCMS object + + Parameters + ---------- + myLCMSobj : corems LCMS object + LCMS object to set parameters on + params_toml : str or Path + Path to toml file with parameters + + Returns + ------- + None, sets parameters on the LCMS object + """ + # Load parameters from toml file + load_and_set_toml_parameters_lcms(myLCMSobj, params_toml) + + # If myLCMSobj is a positive mode, remove Cl from atoms used in molecular search + # This cuts down on the number of molecular formulas searched hugely + if myLCMSobj.polarity == "positive": + myLCMSobj.parameters.mass_spectrum["ms1"].molecular_search.usedAtoms.pop("Cl") + elif myLCMSobj.polarity == "negative": + myLCMSobj.parameters.mass_spectrum["ms1"].molecular_search.usedAtoms.pop("Na") + + if verbose: + print("Parameters set on LCMS object") + + +def load_scan_translator(scan_translator=None): + """Translate scans using a scan translator + + Parameters + ---------- + scan_translator : str or Path + Path to scan translator yaml file + + Returns + ------- + scan_dict : dict + Dict with keys as parameter keys and values as lists of scans + """ + # Convert the scan translator to a dictionary + if scan_translator is None: + scan_translator_dict = {"ms2": {"scan_filter": "", "resolution": "high"}} + else: + # Convert the scan translator to a dictionary + if isinstance(scan_translator, str): + scan_translator = Path(scan_translator) + # read in the scan translator from toml + with open(scan_translator, "r") as f: + scan_translator_dict = toml.load(f) + for param_key in scan_translator_dict.keys(): + if scan_translator_dict[param_key]["scan_filter"] == "": + scan_translator_dict[param_key]["scan_filter"] = None + return scan_translator_dict + + +def check_scan_translator(myLCMSobj, scan_translator): + """Check if scan translator is provided and that it maps correctly to scans and parameters""" + scan_translator_dict = load_scan_translator(scan_translator) + # Check that the scan translator maps correctly to scans and parameters + scan_df = myLCMSobj.scan_df + scans_pulled_out = [] + for param_key in scan_translator_dict.keys(): + assert param_key in myLCMSobj.parameters.mass_spectrum.keys() + assert "scan_filter" in scan_translator_dict[param_key].keys() + assert "resolution" in scan_translator_dict[param_key].keys() + # Pull out scans that match the scan filter + scan_df_sub = scan_df[ + scan_df.scan_text.str.contains( + scan_translator_dict[param_key]["scan_filter"] + ) + ] + scans_pulled_out.extend(scan_df_sub.scan.tolist()) + if len(scan_df_sub) == 0: + raise ValueError( + "No scans pulled out by scan translator for parameter key: ", + param_key, + " and scan filter: ", + scan_translator_dict[param_key]["scan_filter"], + ) + + # Check that the scans pulled out by the scan translator are not overlapping and assert error if they are + if len(set(scans_pulled_out)) != len(scans_pulled_out): + raise ValueError("Overlapping scans pulled out by scan translator") + + +def add_mass_features(myLCMSobj, scan_translator): + """Process ms1 spectra and perform molecular search + + This includes peak picking, adding and processing associated ms1 spectra, + integration of mass features, annotation of c13 mass features, deconvolution of ms1 mass features, + and adding of peak shape metrics of mass features to the mass feature dataframe. + + Parameters + ---------- + myLCMSobj : corems LCMS object + LCMS object to process + scan_translator : str or Path + Path to scan translator yaml file + + Returns + ------- + None, processes the LCMS object + """ + myLCMSobj.find_mass_features() + myLCMSobj.add_associated_ms1( + auto_process=True, use_parser=False, spectrum_mode="profile" + ) + myLCMSobj.integrate_mass_features(drop_if_fail=True) + # Count and report how many mass features are left after integration + print("Number of mass features after integration: ", len(myLCMSobj.mass_features)) + myLCMSobj.find_c13_mass_features() + myLCMSobj.deconvolute_ms1_mass_features() + myLCMSobj.add_peak_metrics() + + scan_dictionary = load_scan_translator(scan_translator=scan_translator) + for param_key in scan_dictionary.keys(): + scan_filter = scan_dictionary[param_key]["scan_filter"] + if scan_filter == "": + scan_filter = None + myLCMSobj.add_associated_ms2_dda( + spectrum_mode="centroid", ms_params_key=param_key, scan_filter=scan_filter + ) + + +def molecular_formula_search(myLCMSobj): + """Perform molecular search on ms1 spectra + + Parameters + ---------- + myLCMSobj : corems LCMS object + LCMS object to process + + Returns + ------- + None, processes the LCMS object + """ + mol_search = SearchMolecularFormulasLC(myLCMSobj) + mol_search.run_mass_feature_search() + print("Finished molecular search") + + +def export_results(myLCMSobj, out_path, molecular_metadata=None, final=False): + """Export results to hdf5 and csv as a lipid report + + Parameters + ---------- + myLCMSobj : corems LCMS object + LCMS object to process + out_path : str or Path + Path to output file + molecular_metadata : dict + Dict with molecular metadata + final : bool + Whether to export final results + + Returns + ------- + None, exports results to hdf5 and csv as a lipid report + """ + exporter = LipidomicsExport(out_path, myLCMSobj) + exporter.to_hdf(overwrite=True) + if final: + # Do not show warnings, these are expected + exporter.report_to_csv(molecular_metadata=molecular_metadata) + else: + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + exporter.report_to_csv() + + +def save_times(myLCMSobj, time_start, out_path, time_end=None): + """Get times for processing steps + + Parameters + ---------- + myLCMSobj : corems LCMS object + LCMS object to process + time_start : float + Start time of processing + out_path : str or Path + Path to output file + time_end : float + End time of processing + + Returns + ------- + None, writes out times to a file within the output directory + """ + # Check if out_path (with .corems) exisits + out_dir = Path(str(out_path) + ".corems/") + if not out_dir.exists(): + print("Output directory does not exist") + + time_toml_path = out_dir / "times.toml" + if not time_toml_path.exists(): + raw_data_creation_time = myLCMSobj.spectra_parser.get_creation_time().strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) + processed_data_creation_time = time_start.strftime("%Y-%m-%dT%H:%M:%SZ") + time_dict = { + "raw_data_creation_time": raw_data_creation_time, + "metabolomics_workflow_start_time": processed_data_creation_time, + } + # save as a toml file + toml_string = toml.dumps(time_dict) # Output to a string + with open(time_toml_path, "w") as f: + f.write(toml_string) + elif time_toml_path.exists() and time_end is not None: + time_dict = toml.load(time_toml_path) + time_dict["metabolomics_workflow_end_time"] = time_end.strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) + toml_string = toml.dumps(time_dict) + with open(time_toml_path, "w") as f: + f.write(toml_string) + + +def process_ms2(myLCMSobj, metadata, scan_translator): + """Process ms2 spectra and perform molecular search + + Parameters + ---------- + myLCMSobj : corems LCMS object + LCMS object to process + metadata : dict + Dict with keys "mzs", "fe", and "molecular_metadata" with values of dicts of precursor mzs (negative and positive), flash entropy search databases (negative and positive), and molecular metadata, respectively + + Returns + ------- + None, processes the LCMS object + """ + # Perform molecular search on ms2 spectra + # Grab fe from metatdata associated with polarity (this is inherently high resolution as its from a in-silico high res library) + fe_search = metadata["fe"][myLCMSobj.polarity] + + scan_dictionary = load_scan_translator(scan_translator) + ms2_scan_df = myLCMSobj.scan_df[myLCMSobj.scan_df.ms_level == 2] + + # Process high resolution MS2 scans + # Collect all high resolution MS2 scans using the scan translator + for param_key in scan_dictionary.keys(): + ms2_scans_oi_hr = [] + if scan_dictionary[param_key]["resolution"] == "high": + scan_filter = scan_dictionary[param_key]["scan_filter"] + if scan_filter is not None: + ms2_scan_df_hr = ms2_scan_df[ + ms2_scan_df.scan_text.str.contains(scan_filter) + ] + else: + ms2_scan_df_hr = ms2_scan_df + ms2_scans_oi_hr_i = [ + x for x in ms2_scan_df_hr.scan.tolist() if x in myLCMSobj._ms.keys() + ] + ms2_scans_oi_hr.extend(ms2_scans_oi_hr_i) + # Perform search on high res scans + if len(ms2_scans_oi_hr) > 0: + myLCMSobj.fe_search( + scan_list=ms2_scans_oi_hr, fe_lib=fe_search, peak_sep_da=0.01 + ) + + # Process low resolution MS2 scans + # Collect all low resolution MS2 scans using the scan translator + for param_key in scan_dictionary.keys(): + ms2_scans_oi_lr = [] + if scan_dictionary[param_key]["resolution"] == "low": + scan_filter = scan_dictionary[param_key]["scan_filter"] + if scan_filter is not None: + ms2_scan_df_lr = ms2_scan_df[ + ms2_scan_df.scan_text.str.contains(scan_filter) + ] + else: + ms2_scan_df_lr = ms2_scan_df + ms2_scans_oi_lri = [ + x for x in ms2_scan_df_lr.scan.tolist() if x in myLCMSobj._ms.keys() + ] + ms2_scans_oi_lr.extend(ms2_scans_oi_lri) + # Perform search on low res scans + if len(ms2_scans_oi_lr) > 0: + # Recast the flashentropy search database to low resolution + metabref = MetabRefLCInterface() + fe_search_lr = metabref._to_flashentropy( + metabref_lib=fe_search, + normalize=True, + fe_kwargs={ + "normalize_intensity": True, + "min_ms2_difference_in_da": 0.4, + "max_ms2_tolerance_in_da": 0.2, + "max_indexed_mz": 3000, + "precursor_ions_removal_da": None, + "noise_threshold": 0, + }, + ) + myLCMSobj.fe_search( + scan_list=ms2_scans_oi_lr, fe_lib=fe_search_lr, peak_sep_da=0.3 + ) + + +def run_lipid_sp_ms1( + file_in, + out_path, + params_toml, + scan_translator=None, + verbose=True, + return_mzs=True, + ms1_molecular_search=True, +): + """Run signal processing, get associated ms1, add associated ms2, do ms1 molecular search, and export intermediate results + + Parameters + ---------- + file_in : str or Path + Path to binary file + out_path : str or Path + Path to output file + params_toml : str or Path + Path to toml file with parameters + verbose : bool + Whether to print verbose output + return_mzs : bool + Whether to return precursor mzs + + Returns + ------- + mz_dict : dict + Dict with keys "positive" and "negative" and values of lists of precursor mzs + """ + time_start = datetime.datetime.now() + myLCMSobj = instantiate_lcms_obj(file_in) + set_params_on_lcms_obj(myLCMSobj, params_toml, verbose) + check_scan_translator(myLCMSobj, scan_translator) + add_mass_features(myLCMSobj, scan_translator) + myLCMSobj.remove_unprocessed_data() + #myLCMSobj.parameters.mass_spectrum['ms1'].molecular_search.verbose_processing = False + if ms1_molecular_search: + molecular_formula_search(myLCMSobj) + export_results(myLCMSobj, out_path=out_path, final=False) + save_times(myLCMSobj, time_start, out_path) + if return_mzs: + precursor_mz_list = list( + set( + [ + v.mz + for k, v in myLCMSobj.mass_features.items() + if len(v.ms2_scan_numbers) > 0 and v.isotopologue_type is None + ] + ) + ) + mz_dict = {myLCMSobj.polarity: precursor_mz_list} + return mz_dict + + +def prep_metadata(mz_dicts, out_dir): + """Prepare metadata for ms2 spectral search + + Parameters + ---------- + mz_dicts : list of dicts + List of dicts with keys "positive" and "negative" and values of lists of precursor mzs + out_dir : Path + Path to output directory + + Returns + ------- + metadata : dict + Dict with keys "mzs", "fe", and "molecular_metadata" with values of dicts of precursor mzs (negative and positive), flash entropy search databases (negative and positive), and molecular metadata, respectively + + Notes + ------- + Also writes out files for the flash entropy search databases and molecular metadata + """ + metadata = { + "mzs": {"positive": None, "negative": None}, + "fe": {"positive": None, "negative": None}, + "molecular_metadata": {}, + } + for d in mz_dicts: + metadata["mzs"].update(d) + + metabref = MetabRefLCInterface() + + print("Preparing positive lipid library") + if metadata["mzs"]["positive"] is not None: + metabref_positive, lipidmetadata_positive = metabref.get_lipid_library( + mz_list=metadata["mzs"]["positive"], + polarity="positive", + mz_tol_ppm=5, + format="flashentropy", + normalize=True, + fe_kwargs={ + "normalize_intensity": True, + "min_ms2_difference_in_da": 0.02, # for cleaning spectra + "max_ms2_tolerance_in_da": 0.01, # for setting search space + "max_indexed_mz": 3000, + "precursor_ions_removal_da": None, + "noise_threshold": 0, + }, + ) + metadata["fe"]["positive"] = metabref_positive + metadata["molecular_metadata"].update(lipidmetadata_positive) + fe_positive_df = pd.DataFrame.from_dict( + {k: v for k, v in enumerate(metadata["fe"]["positive"])}, orient="index" + ) + fe_positive_df.to_csv(out_dir / "ms2_db_positive.csv") + + print("Preparing negative lipid library") + if metadata["mzs"]["negative"] is not None: + metabref_negative, lipidmetadata_negative = metabref.get_lipid_library( + mz_list=metadata["mzs"]["negative"], + polarity="negative", + mz_tol_ppm=5, + mz_tol_da_api=0.01, + format="flashentropy", + normalize=True, + fe_kwargs={ + "normalize_intensity": True, + "min_ms2_difference_in_da": 0.02, # for cleaning spectra + "max_ms2_tolerance_in_da": 0.01, # for setting search space + "max_indexed_mz": 3000, + "precursor_ions_removal_da": None, + "noise_threshold": 0, + }, + ) + metadata["fe"]["negative"] = metabref_negative + metadata["molecular_metadata"].update(lipidmetadata_negative) + fe_negative_df = pd.DataFrame.from_dict( + {k: v for k, v in enumerate(metadata["fe"]["negative"])}, orient="index" + ) + fe_negative_df.to_csv(out_dir / "ms2_db_negative.csv") + + mol_metadata_df = pd.concat( + [ + pd.DataFrame.from_dict(v.__dict__, orient="index").transpose() + for k, v in metadata["molecular_metadata"].items() + ], + ignore_index=True, + ) + mol_metadata_df.to_csv(out_dir / "molecular_metadata.csv") + return metadata + + +def run_lipid_ms2(out_path, metadata, scan_translator=None): + """Run ms2 spectral search and export final results + + Parameters + ---------- + out_path : str or Path + Path to output file + metadata : dict + Dict with keys "mzs", "fe", and "molecular_metadata" with values of dicts of precursor mzs (negative and positive), flash entropy search databases (negative and positive), and molecular metadata, respectively + + Returns + ------- + None, runs ms2 spectral search and exports final results + """ + # Read in the intermediate results + out_path_hdf5 = str(out_path) + ".corems/" + out_path.stem + ".hdf5" + parser = ReadCoreMSHDFMassSpectra(out_path_hdf5) + myLCMSobj = parser.get_lcms_obj() + + # Process ms2 spectra, perform spectral search, and export final results + process_ms2(myLCMSobj, metadata, scan_translator=scan_translator) + export_results(myLCMSobj, str(out_path), metadata["molecular_metadata"], final=True) + time_end = datetime.datetime.now() + save_times(myLCMSobj, time_start=None, out_path=out_path, time_end=time_end) + + +def run_lipid_workflow( + file_dir, + out_dir, + params_toml, + scan_translator=None, + verbose=True, + ms1_molecular_search=True, + cores=1, +): + """Run lipidomics workflow + + Parameters + ---------- + file_dir : str or Path + Path to directory with raw or mzml lipid files + out_dir : str or Path + Path to output directory + params_toml : str or Path + Path to toml file with parameters + verbose : bool + Whether to print verbose output + cores : int + Number of cores to use + + Returns + ------- + None, runs lipidomics workflow and exports final results + """ + # Make output dir and get list of files to process + out_dir.mkdir(parents=True, exist_ok=True) + files_list = list(file_dir.glob("*.raw")) + out_paths_list = [out_dir / f.stem for f in files_list] + + # Run signal processing, get associated ms1, add associated ms2, and export temp results + if cores == 1 or len(files_list) == 1: + mz_dicts = [] + for file_in, file_out in list(zip(files_list, out_paths_list)): + # Check if folder already exists + if Path(str(file_out) + ".corems").exists(): + print("File already exists, skipping: ", file_out) + continue + if verbose: + print("Processing file: ", file_in) + mz_dict = run_lipid_sp_ms1( + file_in=str(file_in), + out_path=str(file_out), + params_toml=params_toml, + scan_translator=scan_translator, + verbose=verbose, + ms1_molecular_search=ms1_molecular_search, + ) + mz_dicts.append(mz_dict) + elif cores > 1: + pool = Pool(cores) + args = [ + ( + str(file_in), + str(file_out), + params_toml, + scan_translator, + verbose, + ms1_molecular_search, + ) + for file_in, file_out in list(zip(files_list, out_paths_list)) + ] + mz_dicts = pool.starmap(run_lipid_sp_ms1, args) + pool.close() + pool.join() + + # Skip ms2 spectral search for now, will add back once we have an improved MetabRefLCInterface method + """ + # Prepare ms2 spectral search space + metadata = prep_metadata(mz_dicts, out_dir) + + # Run ms2 spectral search and export final results + if cores == 1 or len(files_list) == 1: + for file_out in out_paths_list: + mz_dicts = run_lipid_ms2( + file_out, metadata, scan_translator=scan_translator + ) + elif cores > 1: + pool = Pool(cores) + args = [(file_out, metadata, scan_translator) for file_out in out_paths_list] + mz_dicts = pool.starmap(run_lipid_ms2, args) + pool.close() + pool.join() + """ + print("Finished processing, data are written in " + str(out_dir)) + + +if __name__ == "__main__": + # Set input variables to run + cores = 1 + file_dir = Path("/Users/heal742/LOCAL/corems_dev/corems/tmp_data/thermo_raw_mini") + out_dir = Path("tmp_data/_test_250115") + params_toml = Path("/Users/heal742/LOCAL/05_NMDC/02_MetaMS/data_processing/configurations/emsl_lipidomics_corems_params.toml") + verbose = True + scan_translator = Path("tmp_data/thermo_raw_collection/scan_translator.toml") + + # Set up output directory + out_dir.mkdir(parents=True, exist_ok=True) + + # if cores > 1, don't use verbose output + if cores > 2: + verbose = False + + run_lipid_workflow( + file_dir=file_dir, + out_dir=out_dir, + params_toml=params_toml, + scan_translator=scan_translator, + verbose=verbose, + cores=cores, + ) From 135a37ba27a7b82b4594200bdb30113bf2d9c397 Mon Sep 17 00:00:00 2001 From: "Ciesielski, Danielle K" Date: Wed, 16 Apr 2025 15:19:45 -0700 Subject: [PATCH 039/158] adding to docstrings, change mass feat to mz feat --- corems/mass_spectra/calc/lc_calc.py | 63 +++++++++++++++++-- .../nmdc/lipidomics/lipidomics_collection.py | 8 +-- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 4a7604590..20d2d5851 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1990,11 +1990,27 @@ def summarize_clusters(self, features): summary_df = summary_df.reset_index(drop=True) self.cluster_summary_dataframe = summary_df - def plot_mass_feature_per_cluster(self, return_fig = False): + def plot_mz_features_per_cluster(self, return_fig = False): """ Plot the number of mass features in a cluster against how many clusters contain that number of mass features + + Parameters + ----------- + return_fig : boolean + Indicates whether to plot composite feature map (False) or return figure object (True). Defaults to False. + + Returns + -------- + matplotlib.pyplot.Figure + A figure displaying the frequency with which clusters contain the given number of m/z features + + Raises + ------ + Warning + If consensus features haven't been added to the object yet """ + if not hasattr(self, 'cluster_summary_dataframe'): raise ValueError( 'cluster_summary_dataframe is not set, must run add_consensus_mass_features() first' @@ -2011,11 +2027,27 @@ def plot_mass_feature_per_cluster(self, return_fig = False): else: plt.show() - def plot_mass_features_across_samples(self, alpha = 0.75, s = 0.005, return_fig = False): + def plot_mz_features_across_samples(self, alpha = 0.75, s = 0.005, return_fig = False): """ Generate Scan Time vs m/z plot of all the mass features across all samples in collection where intensity of color on the plot indicates density of mass features, NOT INTENSITY + + Parameters + ----------- + alpha : float + Desired transparency for plotted m/z features. Defaults to 0.75. + s : float + Desired size of plotted m/z features. Defaults to 0.005. + return_fig : boolean + Indicates whether to plot composite feature map (False) or return figure object (True). Defaults to False. + + Returns + -------- + matplotlib.pyplot.Figure + A figure displaying a scan time vs m/z scatterplot of all the m/z features identified in the collection. + Parameters alpha (transparency) and s (marker size) allow the user to emphasize the density of features. + Intensity of features is not represented. """ df = self.mass_features_dataframe.copy() fig = plt.figure() @@ -2039,10 +2071,32 @@ def plot_mass_features_across_samples(self, alpha = 0.75, s = 0.005, return_fig else: plt.show() - def plot_consensus_mass_features(self, xb = 'xb', xt = 'xt', yb = 'yb', yt = 'yt', show_raw = True, return_fig = False): + def plot_consensus_mz_features(self, xb = 'xb', xt = 'xt', yb = 'yb', yt = 'yt', show_all = True, return_fig = False): """ Generate Scan Time vs m/z plot of the consensus features scaled by size - with option ('show_raw') of leaving the raw data in the figure. + with option ('show_all') of leaving the individual m/z features in the figure. + + Parameters + ----------- + xb : float + Desired starting scan time value for the x-axis. Defaults to 0. + xt : float + Desired ending scan time for the x-axis. Defaults to the maximum scan time value in the provided data. + yb : float + Desired starting m/z value for the y-axis. Defaults to 0. + yt : float + Desired ending m/z for the y-axis. Defaults to the maximum m/z value in the provided data. + show_all : boolean + Indicates whether to display all identified m/z features (True) or just the consensus features (False). Defaults to True. + return_fig : boolean + Indicates whether to plot composite feature map (False) or return figure object (True). Defaults to False. + + Returns + -------- + matplotlib.pyplot.Figure + A scalable figure that overlays the consensus features over all the m/z features identified in the collection. + Consensus features are scaled by how many m/z features are represented in the consensus. Figure can be scaled by + inputting desired boundaries on the scan time (xb, xt) and m/z values (yb, yt). """ df = self.cluster_summary_dataframe.copy() @@ -2194,7 +2248,6 @@ def add_sparse_distance_matrix(self, features): self._sparse_distance_matrix = distances def evaluate_clusters_for_repeats(self, features): - summary_df = self.summarize_clusters(features) summary_df = self.cluster_summary_dataframe.copy() # Arrange by decreasing median intensity diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 0fccee113..172107b3a 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -43,10 +43,10 @@ # Make some plots lcms_collection.plot_tics(type="both") lcms_collection.plot_alignments() - lcms_collection.plot_mass_features_across_samples() - lcms_collection.plot_mass_features_per_cluster() - lcms_collection.plot_consensus_mass_features() ## zoomed out - lcms_collection.plot_consensus_mass_features(xb = 10, xt = 15, yb = 500, yt = 600) ## zoomed in + lcms_collection.plot_mz_features_across_samples() + lcms_collection.plot_mz_features_per_cluster() + lcms_collection.plot_consensus_mz_features() ## zoomed out + lcms_collection.plot_consensus_mz_features(xb = 10, xt = 15, yb = 500, yt = 600) ## zoomed in # TODO: Think about other plots that would be useful to have here for assessing the quality of the data and alignment From 3259172f6df651d1af92f36b11f3ac94be6552b7 Mon Sep 17 00:00:00 2001 From: "Ciesielski, Danielle K" Date: Wed, 16 Apr 2025 15:32:43 -0700 Subject: [PATCH 040/158] modifying cluster_summary_dataframe --- corems/mass_spectra/calc/lc_calc.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 20d2d5851..e9b74e883 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1953,18 +1953,19 @@ def add_consensus_mass_features(self): mfs_with_clusters.set_index('coll_mf_id', inplace = True) self.mass_features_dataframe = mfs_with_clusters - self.summarize_clusters(mfs_with_clusters) + self.cluster_summary_dataframe = self.summarize_clusters() - def summarize_clusters(self, features): + @property + def summarize_clusters(self): """ Summarize the clusters of mass features by median attributes """ # First check if there are minimum columns in the features dataframe - if len(features.columns) < 1: + if len(self.mass_features_dataframe.columns) < 1: return None summary_df = ( - features.groupby("cluster") + self.mass_features_dataframe.groupby("cluster") .agg( { "mz": "median", @@ -1988,7 +1989,7 @@ def summarize_clusters(self, features): ] summary_df = summary_df.rename(columns={"cluster_": "cluster"}) summary_df = summary_df.reset_index(drop=True) - self.cluster_summary_dataframe = summary_df + return summary_df def plot_mz_features_per_cluster(self, return_fig = False): """ From 19947788c7f3ebc7500cbec89cd7af19bb806a4b Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 17 Apr 2025 11:15:23 -0700 Subject: [PATCH 041/158] Move cluster_summary_df to be a property --- corems/mass_spectra/calc/lc_calc.py | 2 -- corems/mass_spectra/factory/lc_class.py | 4 ++++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index e9b74e883..a68ac3e86 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1953,9 +1953,7 @@ def add_consensus_mass_features(self): mfs_with_clusters.set_index('coll_mf_id', inplace = True) self.mass_features_dataframe = mfs_with_clusters - self.cluster_summary_dataframe = self.summarize_clusters() - @property def summarize_clusters(self): """ Summarize the clusters of mass features by median attributes diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 551da08d9..63e4505ae 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1555,6 +1555,10 @@ def mass_features_dataframe(self, df): raise ValueError("coll_mf_id must be unique") self._combined_mass_features = df + @property + def cluster_summary_dataframe(self): + return self.summarize_clusters() + @property def samples(self): manifest_df = self.manifest_dataframe From f367493730b60034dcd1fd843d09ad4c23ad16be Mon Sep 17 00:00:00 2001 From: "Ciesielski, Danielle K" Date: Thu, 17 Apr 2025 11:18:52 -0700 Subject: [PATCH 042/158] update show_all --- corems/mass_spectra/calc/lc_calc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index a68ac3e86..3c4110d4e 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -2100,7 +2100,7 @@ def plot_consensus_mz_features(self, xb = 'xb', xt = 'xt', yb = 'yb', yt = 'yt', df = self.cluster_summary_dataframe.copy() fig = plt.figure() - if show_raw: + if show_all: plt.scatter( df.scan_time_aligned, df.mz, From 42ab4a9867617160c60744e65b64b80b11e810fc Mon Sep 17 00:00:00 2001 From: "Ciesielski, Danielle K" Date: Thu, 17 Apr 2025 11:25:26 -0700 Subject: [PATCH 043/158] moved plotting functions after consensus features are added --- .../nmdc/lipidomics/lipidomics_collection.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 172107b3a..89f8f3599 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -42,12 +42,7 @@ # Make some plots lcms_collection.plot_tics(type="both") - lcms_collection.plot_alignments() - lcms_collection.plot_mz_features_across_samples() - lcms_collection.plot_mz_features_per_cluster() - lcms_collection.plot_consensus_mz_features() ## zoomed out - lcms_collection.plot_consensus_mz_features(xb = 10, xt = 15, yb = 500, yt = 600) ## zoomed in - + lcms_collection.plot_alignments() # TODO: Think about other plots that would be useful to have here for assessing the quality of the data and alignment # Make consensus mass features from the consolidated mass features @@ -55,6 +50,12 @@ lcms_collection.add_consensus_mass_features() # THIS FUNCTION NEEDS WORK AND NEEDS MECHANISM TO EVALUATE THE RESULTS (plots? reports?) print("Time to roll up consensus mass features: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") + + lcms_collection.plot_mz_features_across_samples() + lcms_collection.plot_mz_features_per_cluster() + lcms_collection.plot_consensus_mz_features() ## zoomed out + lcms_collection.plot_consensus_mz_features(xb = 10, xt = 15, yb = 500, yt = 600) ## zoomed in + #TODO: Add code to load and save information about chromatographic settings #TODO: Add code to save and load collection to HDF5 file From a10deaaf076698323b12c9f9e03eb3049df4c661 Mon Sep 17 00:00:00 2001 From: "danielle.ciesielski@pnnl.gov" Date: Thu, 5 Jun 2025 12:50:28 -0700 Subject: [PATCH 044/158] scaled distances used for clustering --- corems/mass_spectra/calc/lc_calc.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 39ae82cb2..fdeae5854 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -2316,7 +2316,7 @@ def add_sparse_distance_matrix(self, features): else: features = features.copy() - # Define how to calculate the distance between features + # Parameters for calculating distance between features dims = ["mz", "scan_time_aligned"] relative = [True, False] mz_tol_relative = self.parameters.lcms_collection.consensus_mz_tol_ppm * 1e-6 @@ -2334,6 +2334,11 @@ def add_sparse_distance_matrix(self, features): ) # Make connectivity matrix for masking within sample mass features + ## Masking matrix cmat will mark all features from the same sample as 0 + ## To mask, a matrix can be multiplied by cmat and features from same + ## samples are multiplied by 0, while features from different samples + ## are multiplied by 1 + if "sample_id" not in features.columns: cmat = None else: @@ -2341,12 +2346,12 @@ def add_sparse_distance_matrix(self, features): cmat = scipy.spatial.distance.cdist(vals, vals) # Convert to binary (0 if same sample, 1 if different) cmat = np.where(cmat == 0, 0, 1) - # Convert to coorindate matrix for sparse operations later + # Convert to coordinate matrix for sparse operations later cmat = sparse.coo_matrix(cmat) # Compute inter-feature distances using sparse matrix approach - distances = None - for i in range(len(dims)): + distances = None # clear the distances object before starting + for i in range(len(dims)): # iterate through all dimensions to be considered # Construct k-d tree values = features[dims[i]].values @@ -2378,6 +2383,9 @@ def add_sparse_distance_matrix(self, features): shape=(len(values), len(values)), ) + # Scaled distances (between 0 and 1) + sdm.data = sdm.data / max(sdm.data) + # Stack distances for dimensions where na_allow is False if distances is None: sdm.data = sdm.data * dist_weight[i] @@ -2385,17 +2393,23 @@ def add_sparse_distance_matrix(self, features): else: # Prepare sdm to match shape of existing distances distances_truth = distances.copy() + # make new sparse matrix with same positions as previous + # distance matrix but all ones for values distances_truth.data = np.ones_like(distances_truth.data) + # multiply the new sparse matrix (sdm) by this mask to remove + # data that doesn't exist in original sparse matrix sdm = distances_truth.multiply(sdm) sdm.data = sdm.data * dist_weight[i] + # use same process as before to remove data from previous + # distances matrix that isn't in new distances matrix sdm_truth = sdm.copy() sdm_truth.data = np.ones_like(sdm_truth.data) # remove the distances that are not sdm distances = distances.multiply(sdm_truth) - # Add the new distances + # Sum the new distances distances = distances + sdm # Multiply by connectivity matrix for more masking From 2ef770d2566027ea48d8fc878793d88ec790b12f Mon Sep 17 00:00:00 2001 From: Nellie Ciesielski Date: Thu, 19 Jun 2025 22:37:48 +0000 Subject: [PATCH 045/158] Add scaling to clustering distance matrix, generate figure to evaluate distance matrix --- corems/mass_spectra/calc/lc_calc.py | 166 +++++++++++++++--- .../nmdc/lipidomics/lipidomics_collection.py | 2 +- 2 files changed, 140 insertions(+), 28 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index fdeae5854..a0d35a8c5 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -2082,36 +2082,38 @@ def add_consensus_mass_features(self): # Embed a new cluster id into the mass features dataframe and set as index mfs_with_clusters["idx"] = mfs_with_clusters.index - # Check if any clusters can be merged into a single cluster - eval_dict = self.evaluate_clusters_for_repeats(mfs_with_clusters) + try: + # Check if any clusters can be merged into a single cluster + eval_dict = self.evaluate_clusters_for_repeats(mfs_with_clusters) - # Merge clusters identified in eval_dict - while len(eval_dict["merge_these_clusters"]) > 0: - list_of_clusters_to_merge = [ - [x[0], x[1]] for x in eval_dict["merge_these_clusters"] - ] - # Convert to a dataframe with columns "new_cluster" and "cluster" - df = pd.DataFrame( - np.array(list_of_clusters_to_merge), columns=["new_cluster", "cluster"] - ) - # Drop duplicates of "child" clusters - df = df.drop_duplicates("cluster", keep="first") - df = df.drop_duplicates("new_cluster", keep="first") - mfs_with_clusters = mfs_with_clusters.merge(df, on="cluster", how="left") - mfs_with_clusters["cluster"] = mfs_with_clusters["new_cluster"].fillna( - mfs_with_clusters["cluster"] - ) - mfs_with_clusters = mfs_with_clusters.drop(columns=["new_cluster"]) + # Merge clusters identified in eval_dict + while len(eval_dict["merge_these_clusters"]) > 0: + list_of_clusters_to_merge = [ + [x[0], x[1]] for x in eval_dict["merge_these_clusters"] + ] + # Convert to a dataframe with columns "new_cluster" and "cluster" + df = pd.DataFrame( + np.array(list_of_clusters_to_merge), columns=["new_cluster", "cluster"] + ) + # Drop duplicates of "child" clusters + df = df.drop_duplicates("cluster", keep="first") + df = df.drop_duplicates("new_cluster", keep="first") + mfs_with_clusters = mfs_with_clusters.merge(df, on="cluster", how="left") + mfs_with_clusters["cluster"] = mfs_with_clusters["new_cluster"].fillna( + mfs_with_clusters["cluster"] + ) + mfs_with_clusters = mfs_with_clusters.drop(columns=["new_cluster"]) - # Re-evaluate clusters for repeats - eval_dict = self.evaluate_clusters_for_repeats(mfs_with_clusters) + # Re-evaluate clusters for repeats + eval_dict = self.evaluate_clusters_for_repeats(mfs_with_clusters) + self.mass_features_dataframe = mfs_with_clusters + except: + mfs_with_clusters.set_index('coll_mf_id', inplace = True) + self.mass_features_dataframe = mfs_with_clusters + # TODO KRH: Deal with isomers better? Pool them together and then split them out using samples with 2 as the template? - mfs_with_clusters.set_index('coll_mf_id', inplace = True) - - self.mass_features_dataframe = mfs_with_clusters - def summarize_clusters(self): """ Summarize the clusters of mass features by median attributes @@ -2383,8 +2385,8 @@ def add_sparse_distance_matrix(self, features): shape=(len(values), len(values)), ) - # Scaled distances (between 0 and 1) - sdm.data = sdm.data / max(sdm.data) + # Scaled distances wrt the maximum tolerance for the dimension + sdm.data = sdm.data / tol[i] # Stack distances for dimensions where na_allow is False if distances is None: @@ -2419,6 +2421,7 @@ def add_sparse_distance_matrix(self, features): self._sparse_distance_matrix = distances def evaluate_clusters_for_repeats(self, features): + raise NotImplementedError('evaluate_clusters_for_repeats not implemented yet') summary_df = self.cluster_summary_dataframe.copy() # Arrange by decreasing median intensity @@ -2553,3 +2556,112 @@ def cluster_mass_features_agg_cluster(self, features): features["cluster"] = np.arange(len(features.index)) return features + + def cluster_inspection_plot(self, clu, return_fig = False): + """ + Generate Scan Time vs m/z plot for a narrow range around a given + cluster. This tool is meant to support the user in fine tuning the + tolerances used for the clustering algorithm. The user-provided cluster + ID is highlighted in larger, magenta marker and the ten largest of the + remaining clusters are idenfitied with different colors while the + smallest clusters are light gray. + + Parameters + ----------- + clu : integer + A cluster ID that exists in self.mass_features_dataframe + return_fig : boolean + Indicates whether to plot cluster inspection figure (False) or + return figure object (True). Defaults to False. + + Returns + -------- + matplotlib.pyplot.Figure + A figure displaying a scan time vs m/z scatterplot of small region + around a given cluster with the ten largest clusters in the region + distinctly identified + + Raises + ------ + Warning + If consensus features haven't been added to the object yet + """ + + if not hasattr(self, 'mass_features_dataframe.cluster'): + raise ValueError( + 'Cluster information is not yet added to mass_features_dataframe, must run add_consensus_mass_features() first' + ) + + else: + mztol = self.parameters.lcms_collection.consensus_mz_tol_ppm * 1e-6 + rttol = self.parameters.lcms_collection.consensus_rt_tol + clu_features = self.mass_features_dataframe.copy() + + inclu = clu_features[clu_features.cluster == clu] + exclu = clu_features[clu_features.cluster != clu] + + dt_ymin = np.floor(min(inclu.mz)) - 1 + dt_ymax = np.ceil(max(inclu.mz)) + 1 + dt_xmin = np.floor(min(inclu.scan_time_aligned)) - 1 + dt_xmax = np.ceil(max(inclu.scan_time_aligned)) + 1 + + exclu = exclu[ + ( + exclu.mz.between(dt_ymin, dt_ymax, inclusive = 'both') + ) & ( + exclu.scan_time_aligned.between(dt_xmin, dt_xmax, inclusive = 'both') + ) + ] + + bigclulist = list(exclu.cluster.value_counts()[:10].index) + bigclu = exclu[exclu.cluster.isin(bigclulist)] + smclu = exclu[~exclu.cluster.isin(bigclulist)] + + colors = np.arange(0, 10) + colordict = dict(zip(bigclulist, colors)) + bigclu['color'] = bigclu.cluster.apply(lambda x: colordict[x]) + + fig = plt.figure(figsize = (7.5, 5)) + + plt.scatter( + inclu.scan_time_aligned, + inclu.mz, + c = 'm', + s = 3, + label = 'Cluster ' + str(clu) + ) + + plt.scatter( + bigclu.scan_time_aligned, + bigclu.mz, + c = bigclu.color, + cmap = 'tab10', + s = 1.5 + ) + + plt.scatter( + smclu.scan_time_aligned, + smclu.mz, + c = 'silver', + s = 2, + label = 'Small clusters' + ) + + plt.ylim(dt_ymin, dt_ymax) + plt.xlim(dt_xmin, dt_xmax) + plt.legend(ncol = 2, bbox_to_anchor = (0.8, -0.1)) + plt.xlabel('Scan time') + plt.ylabel('m/z') + title_str = 'Cluster ' + str(clu) + title_str += ': representing ' + str(len(inclu.sample_id.unique())) + title_str += ' of ' + str(len(clu_features.sample_id.unique())) + title_str += ' samples\n' + title_str += 'M/Z tolerance: ' + str(mztol) + '\n' + title_str += 'Scan Time tolerance: ' + str(rttol) + plt.title(title_str, fontsize = 10) + + if return_fig: + plt.close(fig) + return fig + else: + plt.show() \ No newline at end of file diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 89f8f3599..8f909c023 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -55,7 +55,7 @@ lcms_collection.plot_mz_features_per_cluster() lcms_collection.plot_consensus_mz_features() ## zoomed out lcms_collection.plot_consensus_mz_features(xb = 10, xt = 15, yb = 500, yt = 600) ## zoomed in - + lcms_collection.cluster_inspection_plot(11391) #TODO: Add code to load and save information about chromatographic settings #TODO: Add code to save and load collection to HDF5 file From ca02ea06d3a588f537b653281a0476b4753b21d2 Mon Sep 17 00:00:00 2001 From: Nellie Ciesielski Date: Mon, 14 Jul 2025 14:50:22 +0000 Subject: [PATCH 046/158] Cluster outlier frequency plot --- corems/mass_spectra/calc/lc_calc.py | 113 ++++++++++++++++-- .../nmdc/lipidomics/lipidomics_collection.py | 12 +- 2 files changed, 114 insertions(+), 11 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index a0d35a8c5..219e1f89d 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1518,6 +1518,7 @@ def find_mass_features_ph_centroid(self, ms_level=1): if self.parameters.lc_ms.verbose_processing: print("Found " + str(len(mass_features)) + " initial mass features") + def cluster_mass_features(self, drop_children=True, sort_by="persistence"): """Cluster mass features @@ -2126,14 +2127,14 @@ def summarize_clusters(self): self.mass_features_dataframe.groupby("cluster") .agg( { - "mz": "median", - "scan_time_aligned": "median", - "half_height_width": "median", - "tailing_factor": "median", - "dispersity_index": "median", - "sample_id": ["nunique", "count"], - "intensity": ["max", "median"], - "persistence": ["max", "median"], + "mz": ["median", "mean", "std"], + "scan_time_aligned": ["median", "mean", "std"], + "half_height_width": ["median", "mean", "std"], + "tailing_factor": ["median", "mean", "std"], + "dispersity_index": ["median", "mean", "std"], + "sample_id": ["nunique"], + "intensity": ["max", "median", "mean", "std"], + "persistence": ["max", "median", "mean", "std"], } ) .reset_index() @@ -2584,7 +2585,7 @@ def cluster_inspection_plot(self, clu, return_fig = False): Raises ------ Warning - If consensus features haven't been added to the object yet + If cluster data haven't been added to the object yet """ if not hasattr(self, 'mass_features_dataframe.cluster'): @@ -2664,4 +2665,96 @@ def cluster_inspection_plot(self, clu, return_fig = False): plt.close(fig) return fig else: - plt.show() \ No newline at end of file + plt.show() + + def plot_cluster_outlier_frequency(self, dim_list = ['mz', 'scan_time_aligned'], clu_size_thresh = 0.5, return_fig = False): + """ + Generate histogram showing the frequency of outlier occurrences by + clustering dimension across all clusters + + Parameters + ----------- + dim_list : list + List of strings describing dimensions that can be used in + clustering. Available list items: + - 'mz' + - 'scan_time_aligned' + - 'half_height_width' + - 'tailing_factor' + - 'dispersity_index' + - 'intensity' + - 'persistence' + clu_size_thresh : float + Value between 0 and 1 that indicates what percentage of samples + need to be present in a cluster before it's evaluated for outliers. + Defaults to 0.5. + return_fig : boolean + Indicates whether to plot cluster inspection figure (False) or + return figure object (True). Defaults to False. + + Returns + -------- + matplotlib.pyplot.Figure + A figure displaying the frequency of outlier occurrences across all + clusters in the provided measurement dimensions + + Raises + ------ + Warning + If cluster data haven't been added to the object yet + """ + + if not hasattr(self, 'cluster_summary_dataframe'): + raise ValueError( + 'cluster_summary_dataframe is not yet added, must run add_consensus_mass_features() first' + ) + + mfdf = self.mass_features_dataframe.copy() + sumdf = self.cluster_summary_dataframe.copy() + + numsamples = mfdf.sample_id.max() + 1 + sumdf = sumdf[sumdf.sample_id_nunique > numsamples * clu_size_thresh] + + ## find the ranges for non-outlier values and add them to sumdf + mergelist = ['cluster'] + for dim in dim_list: + maxtag = dim + '_outmax' + mintag = dim + '_outmin' + mergelist.append(maxtag) + mergelist.append(mintag) + sumdf[maxtag] = None + sumdf[mintag] = None + for i in range(len(sumdf)): + sumdf.loc[i, mintag] = sumdf[dim + '_mean'].iloc[i] - 3*sumdf[dim + '_std'].iloc[i] + sumdf.loc[i, maxtag] = sumdf[dim + '_mean'].iloc[i] + 3*sumdf[dim + '_std'].iloc[i] + ## If NaN shows up anywhere in dim_min, dim_max calculations, value is set to NaN and it's + ## not flagged. This happens when there's not enough values to compute median/std for that + ## dimension therefore can't have outliers + + ## add ranges to mfdf and identify mass features that fall outside the ranges + outdf = pd.merge(mfdf, sumdf[mergelist], on = 'cluster') + outtags = ['cluster'] + for dim in dim_list: + dimtag = dim + '_outlier' + outtags.append(dimtag) + outdf[dimtag] = np.where( + ((outdf[dim] > outdf[dim + '_outmax'])) | ((outdf[dim] < outdf[dim + '_outmin'])), + True, + False + ) + + ## identify number of outliers in each cluster + outliers = outdf[outtags] + outliers = outliers.groupby(['cluster']).sum() + + ## plot number of clusters that contain any outliers + fig = plt.figure() + plt.bar(dim_list, outliers.sum().values, width = 0.5) + plt.xticks(rotation = 90) + plt.title('Frequency of outliers across all clusters by category') + + if return_fig: + plt.close(fig) + return fig + else: + plt.show() diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 8f909c023..4c454dd5b 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -56,7 +56,17 @@ lcms_collection.plot_consensus_mz_features() ## zoomed out lcms_collection.plot_consensus_mz_features(xb = 10, xt = 15, yb = 500, yt = 600) ## zoomed in lcms_collection.cluster_inspection_plot(11391) - + dim_list = [ + 'mz', + 'scan_time_aligned', + 'half_height_width', + 'tailing_factor', + 'dispersity_index', + 'intensity', + 'persistence' + ] + lcms_collection.plot_cluster_outlier_frequency(dim_list, clu_size_thresh = 0.25) + #TODO: Add code to load and save information about chromatographic settings #TODO: Add code to save and load collection to HDF5 file #TODO: Add code to plot a consensus mass feature \ No newline at end of file From 2289c22aa2e9a8424d849cb72cef38cadab95a90 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 17 Jul 2025 18:48:02 -0700 Subject: [PATCH 047/158] Convert spectra_parser to a property within MassSpectraBase --- corems/mass_spectra/factory/lc_class.py | 29 +++++++++++++++---- corems/mass_spectra/input/corems_hdf5.py | 29 ++++++++++--------- .../nmdc/lipidomics/lipidomics_collection.py | 12 ++++---- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 95809a3ce..cca922923 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -5,7 +5,6 @@ import warnings import multiprocessing -import matplotlib.pyplot as plt import matplotlib.pyplot as plt from corems.encapsulation.factory.parameters import LCMSParameters, LCMSCollectionParameters @@ -84,15 +83,19 @@ def __init__( self.file_location = file_location self.analyzer = analyzer self.instrument_label = instrument_label + self._raw_file_location = None # Add the spectra parser class to the object if it is not None if spectra_parser is not None: self.spectra_parser_class = spectra_parser.__class__ - self.spectra_parser = spectra_parser - # Check that spectra_pasrser.sample_name is same as sample_name etc, raise warning if not + if self.spectra_parser_class.__name__ == "ReadCoreMSHDFMassSpectra": + self.raw_file_location = spectra_parser.get_raw_file_location() + + # Check that spectra_parser.sample_name is same as sample_name etc, raise warning if not if ( self.sample_name is not None and self.sample_name != self.spectra_parser.sample_name + and self.spectra_parser_class.__name__ != "ReadCoreMSHDFMassSpectra" ): warnings.warn( "sample_name provided to MassSpectraBase object does not match sample_name provided to spectra parser object", @@ -119,6 +122,20 @@ def __init__( self._ms = {} self._ms_unprocessed = {} + @property + def spectra_parser(self): + """Returns an instance of the spectra parser class.""" + return self.spectra_parser_class(self.raw_file_location) + + @property + def raw_file_location(self): + """Returns the file_location unless the _raw_file_location is not None.""" + return self._raw_file_location if self._raw_file_location is not None else self.file_location + + @raw_file_location.setter + def raw_file_location(self, value): + self._raw_file_location = value + def add_mass_spectrum(self, mass_spec): """Adds a mass spectrum to the dataset. @@ -1600,5 +1617,7 @@ def consensus_mass_feature_dataframe(self): # Clean up column names cluster_summary.columns = ['_'.join(col).strip() for col in cluster_summary.columns.values] - - + @property + def raw_files(self): + """Returns a list of raw files in the collection.""" + return [x.raw_file_location for x in self] diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index addeefa2a..7c9bdfbd3 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -485,6 +485,7 @@ def get_lcms_obj( analyzer=self.analyzer, instrument_label=self.instrument_label, sample_name=self.sample_name, + spectra_parser=self ) # This will populate the majority of the attributes on the LCMS object @@ -502,6 +503,20 @@ def get_lcms_obj( return lcms_obj + def get_raw_file_location(self): + """ + Get the raw file location from the HDF5 file attributes. + + Returns + ------- + str + The raw file location. + """ + if "raw_file_location" in self.h5pydata.attrs: + return self.h5pydata.attrs["raw_file_location"] + else: + return None + def add_original_parser(self, mass_spectra, raw_file_path=None): """ Add the original parser to the mass spectra object. @@ -513,19 +528,6 @@ def add_original_parser(self, mass_spectra, raw_file_path=None): raw_file_path : str The location of the raw file to parse. Default is None, which attempts to get the raw file path from the HDF5 file. """ - # Try to get the raw file path from the HDF5 file - if raw_file_path is None: - raw_file_path = self.h5pydata.attrs["original_file_location"] - # Check if og_file_location exists, if not raise an error - raw_file_path = self.h5pydata.attrs["original_file_location"] - - raw_file_path = Path(raw_file_path) - if not raw_file_path.exists(): - raise FileExistsError( - "File does not exist: " + str(raw_file_path), - ". Cannot use original parser for instatiating the lcms_obj.", - ) - # Get the original parser type og_parser_type = self.h5pydata.attrs["parser_type"] @@ -535,7 +537,6 @@ def add_original_parser(self, mass_spectra, raw_file_path=None): parser = MZMLSpectraParser(raw_file_path) mass_spectra.spectra_parser_class = parser.__class__ - mass_spectra.spectra_parser = parser return mass_spectra diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 8f909c023..282e1602b 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -5,14 +5,14 @@ if __name__ == "__main__": # Set the path to the collection of LCMS runs (previously processed) - collection_path = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/UDN_neg/processed_data") + collection_path = Path("/Users/heal742/Library/CloudStorage/OneDrive-PNNL/Documents/_DMS_data/_NMDC/_stegen_lipidomics/20241221_processed") # Path to manifest file - manifest_file = collection_path / "manifest_very_small.csv" + manifest_file = Path("/Users/heal742/Library/CloudStorage/OneDrive-PNNL/Documents/_DMS_data/_NMDC/_stegen_lipidomics/20241221_processed/manifest_very_small.csv") # This file will need to be created by the user or helper script? - chromatography_file = collection_path / "long_lipid_gradient_chroma.csv" - + chromatography_file = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/UDN_neg/processed_data/long_lipid_gradient_chroma.csv") + # Set the number of cores to use for loading the data (the parser is parallelized) - ncores = 10 + ncores = 5 # Instantiate the parser parser = ReadCoreMSHDFMassSpectraCollection( @@ -26,7 +26,7 @@ # Load the LCMS collection (minimally load the data) start_time = time.time() lcms_collection = parser.get_lcms_collection(load_raw=False, load_light=True) - print("Time to load LCMS collection ", time.time() - start_time, "seconds -", len(lcms_collection), " LCMS runs and ", ncores, " cores") + print("Time to load LCMS collection ", time.time() - start_time, "seconds -", len(lcms_collection), " LCMS runs and ", ncores, " cores") #10s for 7 samples, 10 cores; 162s for 70 samples, 10 cores # Set flag to call _drop_isotopologue() when running _check_mass_features_df() From 6e8bcefd9b4a8365c3ed664a9d16dc5d554fe473 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 18 Jul 2025 16:40:37 -0700 Subject: [PATCH 048/158] Fix raw path file finder, add test to check original parser functionality --- corems/mass_spectra/input/corems_hdf5.py | 21 ++++++++++++++++++--- tests/test_wf_lipidomics.py | 5 +++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 7c9bdfbd3..7c30996ad 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -485,7 +485,6 @@ def get_lcms_obj( analyzer=self.analyzer, instrument_label=self.instrument_label, sample_name=self.sample_name, - spectra_parser=self ) # This will populate the majority of the attributes on the LCMS object @@ -500,6 +499,8 @@ def get_lcms_obj( # If use_original_parser is True, instantiate the original parser and populate the LCMS object if use_original_parser: lcms_obj = self.add_original_parser(lcms_obj, raw_file_path=raw_file_path) + else: + lcms_obj.spectra_parser_class = self.__class__ return lcms_obj @@ -512,8 +513,8 @@ def get_raw_file_location(self): str The raw file location. """ - if "raw_file_location" in self.h5pydata.attrs: - return self.h5pydata.attrs["raw_file_location"] + if "original_file_location" in self.h5pydata.attrs: + return self.h5pydata.attrs["original_file_location"] else: return None @@ -531,11 +532,25 @@ def add_original_parser(self, mass_spectra, raw_file_path=None): # Get the original parser type og_parser_type = self.h5pydata.attrs["parser_type"] + # If raw_file_path is None, get it from the HDF5 file attributes + if raw_file_path is None: + raw_file_path = self.get_raw_file_location() + if raw_file_path is None: + raise ValueError( + "Raw file path not found in HDF5 file attributes, cannot instantiate original parser." + ) + + # Set the raw file path on the mass_spectra object so the parser knows where to find the raw file + mass_spectra.raw_file_location = raw_file_path + if og_parser_type == "ImportMassSpectraThermoMSFileReader": + # Check that the parser can be instantiated with the raw file path parser = ImportMassSpectraThermoMSFileReader(raw_file_path) elif og_parser_type == "MZMLSpectraParser": + # Check that the parser can be instantiated with the raw file path parser = MZMLSpectraParser(raw_file_path) + # Set the spectra parser class on the mass_spectra object so the spectra_parser property can be used with the original parser mass_spectra.spectra_parser_class = parser.__class__ return mass_spectra diff --git a/tests/test_wf_lipidomics.py b/tests/test_wf_lipidomics.py index 97f626d1d..ecf9072b8 100644 --- a/tests/test_wf_lipidomics.py +++ b/tests/test_wf_lipidomics.py @@ -196,7 +196,12 @@ def test_lipidomics_workflow(postgres_database, lcms_obj): # Check that the parameters match assert myLCMSobj2.parameters == lcms_obj.parameters + + # Check that the spectra parser class is the same as the original parser and that we can plot a mass spectrum using the original parser assert myLCMSobj2.spectra_parser_class.__name__ == "ImportMassSpectraThermoMSFileReader" + myLCMSobj2.spectra_parser.get_mass_spectrum_from_scan(1, spectrum_mode="profile").plot_centroid() + + # Check that the mass features dataframe is the same as the original df2 = myLCMSobj2.mass_features_to_df() assert df2.shape == (130, 16) myLCMSobj2.mass_features[0].mass_spectrum.to_dataframe() From 5b4abdffd442da53135311069d9959398c2ad834 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 28 Jul 2025 08:32:36 -0700 Subject: [PATCH 049/158] Fix lcms spectra parser re-instantiation with example --- corems/mass_spectra/input/corems_hdf5.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 7c30996ad..5613e1c32 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -767,7 +767,7 @@ def get_lcms_collection(self, load_raw = False, load_light = True) -> LCMSCollec # Create a pool of workers (one for each core or sample, whichever is smaller) pool = multiprocessing.Pool(ncores) # Load the LCMS objects in parallel - do not instantiate the original parser by default - use_original_parser = False + use_original_parser = True args = [(sample, load_raw, load_light, use_original_parser) for sample in samples] lcms_objs = pool.starmap(self.get_lcms_obj, args) for sample_name, lcms_obj in zip(samples, lcms_objs): From 41560155cad785ea83d0ba6655dc69ac84b0b406 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 28 Jul 2025 08:34:19 -0700 Subject: [PATCH 050/158] Add examplar of new parser capability --- support_code/nmdc/lipidomics/lipidomics_workflow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/support_code/nmdc/lipidomics/lipidomics_workflow.py b/support_code/nmdc/lipidomics/lipidomics_workflow.py index 99e198458..07ec89520 100644 --- a/support_code/nmdc/lipidomics/lipidomics_workflow.py +++ b/support_code/nmdc/lipidomics/lipidomics_workflow.py @@ -629,8 +629,8 @@ def run_lipid_workflow( if __name__ == "__main__": # Set input variables to run cores = 1 - file_dir = Path("/Users/heal742/LOCAL/corems_dev/corems/tmp_data/thermo_raw_mini") - out_dir = Path("tmp_data/_test_250115") + file_dir = Path("/Users/heal742/Library/CloudStorage/OneDrive-PNNL/Documents/_DMS_data/_NMDC/_blanchard_lipidomics/mini_collection_test") + out_dir = Path("/Users/heal742/Library/CloudStorage/OneDrive-PNNL/Documents/_DMS_data/_NMDC/_blanchard_lipidomics/mini_collection_test_out") params_toml = Path( "/Users/heal742/LOCAL/05_NMDC/02_MetaMS/data_processing/configurations/emsl_lipidomics_corems_params.toml" ) From f3d2083fc497192f5a38a971423dde744324fc79 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 28 Jul 2025 08:41:04 -0700 Subject: [PATCH 051/158] Add examplar of new parser capability --- support_code/nmdc/lipidomics/lipidomics_collection.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 282e1602b..06387397a 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -5,9 +5,9 @@ if __name__ == "__main__": # Set the path to the collection of LCMS runs (previously processed) - collection_path = Path("/Users/heal742/Library/CloudStorage/OneDrive-PNNL/Documents/_DMS_data/_NMDC/_stegen_lipidomics/20241221_processed") + collection_path = Path("/Users/heal742/Library/CloudStorage/OneDrive-PNNL/Documents/_DMS_data/_NMDC/_blanchard_lipidomics/mini_collection_test_out") # Path to manifest file - manifest_file = Path("/Users/heal742/Library/CloudStorage/OneDrive-PNNL/Documents/_DMS_data/_NMDC/_stegen_lipidomics/20241221_processed/manifest_very_small.csv") + manifest_file = Path("/Users/heal742/Library/CloudStorage/OneDrive-PNNL/Documents/_DMS_data/_NMDC/_blanchard_lipidomics/mini_collection_test_out/manifest.csv") # This file will need to be created by the user or helper script? chromatography_file = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/UDN_neg/processed_data/long_lipid_gradient_chroma.csv") @@ -29,6 +29,11 @@ print("Time to load LCMS collection ", time.time() - start_time, "seconds -", len(lcms_collection), " LCMS runs and ", ncores, " cores") #10s for 7 samples, 10 cores; 162s for 70 samples, 10 cores + # Check parsers and the ability to load raw data + #TODO KRH: Make this into two methods on the collection - lcms_collection.load_raw_data(sample_idx); lcms_collection.drop_raw_data(sample_idx) + # Add error handling into those methods; make sure it works with both thermo and mzml data + lcms_collection[0]._ms_unprocessed[1] = lcms_collection[0].spectra_parser.get_ms_raw(spectra="ms1", scan_df=lcms_collection[0].scan_df)['ms1'] + # Set flag to call _drop_isotopologue() when running _check_mass_features_df() lcms_collection.parameters.lcms_collection.drop_isotopologues = True print("Number of total mass features: ", len(lcms_collection.mass_features_dataframe)) From 901fd7170c132320bc6342faf057575a52c4cf06 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 29 Jul 2025 16:17:45 -0700 Subject: [PATCH 052/158] Add methods and demo for loading and dropping spectra data within a collection --- corems/mass_spectra/factory/lc_class.py | 92 +++++++++++++++++++ .../nmdc/lipidomics/lipidomics_collection.py | 8 +- 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index cca922923..d1c9c13ef 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1544,6 +1544,98 @@ def _drop_isotopologues(self): self.isotopes_dropped = True self._combined_mass_features = cmb_mf_df2 + + + def load_raw_data(self, sample_idx: int, ms_level = 1) -> None: + """Load raw data for a specific sample index in the collection. + + Parameters + ----------- + sample_idx : int + The index of the sample in the collection. + ms_level : int, optional + The MS level to load raw data for. Defaults to 1. + + Raises + ------- + IndexError + If the sample index is out of range. + ValueError + If raw data for the specified MS level is already loaded for the sample index. + ValueError + If the spectra parser is not set for the LCMS object or if the parser type does not support loading raw data. + + Returns + -------- + None, but updates the LCMS object with the raw data for the specified MS level. + """ + if sample_idx < 0 or sample_idx >= len(self.samples): + raise IndexError("Sample index out of range.") + + # Check that the sample does not already have raw data loaded + if ms_level in self[sample_idx]._ms_unprocessed: + raise ValueError(f"Raw data for MS{ms_level} already loaded for sample index {sample_idx}. Drop data first if you want to reload it.") + + # Check the parser type of the LCMS object + if self[sample_idx].spectra_parser is None: + raise ValueError("Spectra parser is not set for this LCMS object.") + + # Instantiate the parser and load the raw data using the correct method + parser = self[sample_idx].spectra_parser + parser_class_name = self[sample_idx].spectra_parser_class.__name__ + scan_df = self[sample_idx].scan_df + + # Get raw data for the specified MS level using the appropriate method + if parser_class_name == "ImportMassSpectraThermoMSFileReader": + self[sample_idx]._ms_unprocessed[ms_level] = parser.get_ms_raw( + spectra=f"ms{ms_level}", + scan_df=scan_df + )[f"ms{ms_level}"] + + elif parser_class_name == "MZMLSpectraParser": + data = parser.load() + self[sample_idx]._ms_unprocessed[ms_level] = parser.get_ms_raw( + spectra=f"ms{ms_level}", + scan_df=scan_df, + data=data + )[f"ms{ms_level}"] + + elif parser_class_name == "MassSpectraParser": + raise ValueError( + "MassSpectraParser does not have a method to load raw data. Need to instantiate the original parser to access the raw data." + ) + + def drop_raw_data(self, sample_idx: int, ms_level = 1) -> None: + """Drop raw data for a specific sample index in the collection. + + Parameters + ----------- + sample_idx : int + The index of the sample in the collection. + ms_level : int, optional + The MS level to drop raw data for. Defaults to 1. + + Raises + ------- + IndexError + If the sample index is out of range. + ValueError + If raw data for the specified MS level is not loaded for the sample index. + + Returns + -------- + None + """ + if sample_idx < 0 or sample_idx >= len(self.samples): + raise IndexError("Sample index out of range.") + + # Check that the sample has raw data loaded + if ms_level not in self[sample_idx]._ms_unprocessed: + raise ValueError(f"No raw data for MS{ms_level} found for sample index {sample_idx}. Load data first if you want to drop it.") + + # Drop the raw data + del self[sample_idx]._ms_unprocessed[ms_level] + @property def parameters(self): diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 2f097251d..c9b7c5387 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -29,10 +29,10 @@ print("Time to load LCMS collection ", time.time() - start_time, "seconds -", len(lcms_collection), " LCMS runs and ", ncores, " cores") #10s for 7 samples, 10 cores; 162s for 70 samples, 10 cores - # Check parsers and the ability to load raw data - #TODO KRH: Make this into two methods on the collection - lcms_collection.load_raw_data(sample_idx); lcms_collection.drop_raw_data(sample_idx) - # Add error handling into those methods; make sure it works with both thermo and mzml data - lcms_collection[0]._ms_unprocessed[1] = lcms_collection[0].spectra_parser.get_ms_raw(spectra="ms1", scan_df=lcms_collection[0].scan_df)['ms1'] + # Check and demonstrate the parsers' ability to load raw data + lcms_collection.load_raw_data(sample_idx=0, ms_level=1) + assert lcms_collection[0]._ms_unprocessed[1] is not None, "Raw data for MS1 should be loaded successfully." + lcms_collection.drop_raw_data(sample_idx=0, ms_level=1) # Set flag to call _drop_isotopologue() when running _check_mass_features_df() lcms_collection.parameters.lcms_collection.drop_isotopologues = True From e48086617d2bf46216f35e24a94dc120e3977e0e Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 4 Aug 2025 10:56:46 -0700 Subject: [PATCH 053/158] Remove chromatography from Collection instantiation --- corems/mass_spectra/factory/lc_class.py | 31 ------------ corems/mass_spectra/input/corems_hdf5.py | 47 +------------------ .../nmdc/lipidomics/lipidomics_collection.py | 6 +-- 3 files changed, 2 insertions(+), 82 deletions(-) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index d1c9c13ef..8c02ba0b7 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1306,7 +1306,6 @@ def __init__( self.collection_parser = collection_parser # These attributes are generally set by the parser during instantiation of this class - self._chromatography_df = None self._lcms = {} self._combined_mass_features = None self.consensus_mass_features = {} @@ -1351,30 +1350,6 @@ def _prepare_lcms_mass_features_for_combination(self, lcms_obj): mf_df = mf_df.merge(scan_df, left_on="apex_scan", right_index=True) return mf_df - - def _convert_solvent_A(self, x): - """ - Converts the solvent A fraction based on the scan time. - - Parameters - ----------- - x : float or numpy.ndarray - The scan time in minutes or an array of scan times in minutes. - - Returns - -------- - float or numpy.ndarray or None - The solvent A fraction at the scan time or an array of solvent A fractions at the scan times (if the function is set) - """ - if self._chromatography_df is not None: - xp = self._chromatography_df.time_min.values - yp = self._chromatography_df.A.values - - # Define a function to interpolate the chromatogram data - pred_y = np.interp(x, xp, yp) - return pred_y - else: - return None def _combine_mass_features(self): """ @@ -1412,12 +1387,6 @@ def _combine_mass_features(self): # Make coll_mf_id the index combined_mass_features = combined_mass_features.set_index("coll_mf_id") - # If _chromatogram_df is set, combine it with the mass features dataframe to add A fraction for each mass feature - if self._chromatography_df is not None: - - # Add solvent A fraction to the combined mass features dataframe - combined_mass_features['solvent_A_frac'] = self._convert_solvent_A(combined_mass_features.scan_time.values) - self._combined_mass_features = combined_mass_features def _check_mass_features_df(self): diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 8b352e981..5508b9c84 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -588,9 +588,6 @@ class ReadCoreMSHDFMassSpectraCollection: The location of the manifest file containing the sample names, order, and batch. This must be a csv with the following columns: 'sample_name', 'order', 'batch'. Other fields can be included in the manifest file, but these are required. - chromatography_file: str, optional - The location of the chromatography file containing at a minimum time_min and A (fraction of solvent A). - Default is None. cores : int The number of cores to use for multiprocessing. Default is 1. @@ -605,7 +602,6 @@ def __init__( self, folder_location: str, manifest_file: str, - chromatography_file: str = None, cores: int = 1 ): # Check for folder location and manifest file @@ -619,41 +615,12 @@ def __init__( raise ValueError("Manifest file must be a CSV.") self.folder_location = folder_location - self._chromatography_df = None self._manifest_dict = None self._parse_manifest(manifest_file) self._validate_manifest() self._validate_parameters() self._validate_cores(cores) - if chromatography_file is not None: - self._validate_chromatography_file(chromatography_file) - - def _validate_chromatography_file(self, chromatography_file): - # Check if the chromatography file exists - if not chromatography_file.exists(): - raise FileNotFoundError(f"Chromatography file {chromatography_file} not found.") - # Read in the chromatography file, if the first line starts with #, skip it - with open(chromatography_file, "r") as f: - first_line = f.readline() - if first_line.startswith('\ufeff"#'): - chromat_df = pd.read_csv(chromatography_file, skiprows=1) - else: - chromat_df = pd.read_csv(chromatography_file) - - # Check if the following columns exist in the chromatography file - if not all(col in chromat_df.columns for col in ["time_min", "A"]): - raise ValueError("Chromatography file must contain the following columns: 'time_min', 'A'.") - - # Check if the time_min column is in ascending order - if not chromat_df["time_min"].is_monotonic_increasing: - raise ValueError("Chromatography file must be in ascending order by 'time_min'.") - - # Check if the A column is between 0 and 1 - if not chromat_df["A"].between(0, 1).all(): - raise ValueError("Chromatography file 'A' column must be between 0 and 1.") - - self._chromatography_df = chromat_df - + def _validate_cores(self, cores): # Check if the cores parameter is an integer greater than 0 and less than the number of cores available if not isinstance(cores, int) or cores < 1: @@ -774,7 +741,6 @@ def get_lcms_collection(self, load_raw = False, load_light = True) -> LCMSCollec # Set the number of cores on the LCMSCollection object from the ReadCoreMSHDFMassSpectraCollection object lcms_coll.parameters.lcms_collection.cores = self._cores - lcms_coll._chromatography_df = self._chromatography_df # Add LCMS objects to the collection samples = self._manifest_dict.keys() @@ -817,13 +783,6 @@ def get_lcms_collection(self, load_raw = False, load_light = True) -> LCMSCollec # Collect the mass features from the LCMS objects and combine them into a single dataframe for the collection lcms_coll._combine_mass_features() - - # If chromatography file is provided, interpolate the chromatography data - if self._chromatography_df is not None: - # Add the chromatography data to the combined mass features dataframe - combined_mf_df = lcms_coll.mass_features_dataframe - combined_mf_df['fraction_A'] = lcms_coll._convert_solvent_A(combined_mf_df.scan_time.values) - lcms_coll.mass_features_dataframe = combined_mf_df # If load_light, remove the mass_feature attribute from the individual LCMS objects if load_light: @@ -843,10 +802,6 @@ def manifest(self): def manifest_dataframe(self): return pd.DataFrame(self._manifest_dict).T - @property - def chromatography_df(self): - return self._chromatography_df - @property def hdf5_files(self): return [ diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index c9b7c5387..647d1ec3b 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -8,8 +8,6 @@ collection_path = Path("/Users/heal742/Library/CloudStorage/OneDrive-PNNL/Documents/_DMS_data/_NMDC/_blanchard_lipidomics/mini_collection_test_out") # Path to manifest file manifest_file = Path("/Users/heal742/Library/CloudStorage/OneDrive-PNNL/Documents/_DMS_data/_NMDC/_blanchard_lipidomics/mini_collection_test_out/manifest.csv") - # This file will need to be created by the user or helper script? - chromatography_file = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/UDN_neg/processed_data/long_lipid_gradient_chroma.csv") # Set the number of cores to use for loading the data (the parser is parallelized) ncores = 5 @@ -18,7 +16,6 @@ parser = ReadCoreMSHDFMassSpectraCollection( folder_location = collection_path, manifest_file = manifest_file, - chromatography_file=chromatography_file, cores = ncores ) print("Loading LCMS collection with", len(parser.manifest), "samples using", ncores, " cores") @@ -57,7 +54,7 @@ print("Time to roll up consensus mass features: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") lcms_collection.plot_mz_features_across_samples() - lcms_collection.plot_mz_features_per_cluster() + lcms_collection.plot_mz_features_per_cluster() #TODO: Fix this function, - errroring out with 'DataFrame' object has no attribute 'sample_id_count' lcms_collection.plot_consensus_mz_features() ## zoomed out lcms_collection.plot_consensus_mz_features(xb = 10, xt = 15, yb = 500, yt = 600) ## zoomed in lcms_collection.cluster_inspection_plot(11391) @@ -72,6 +69,5 @@ ] lcms_collection.plot_cluster_outlier_frequency(dim_list, clu_size_thresh = 0.25) - #TODO: Add code to load and save information about chromatographic settings #TODO: Add code to save and load collection to HDF5 file #TODO: Add code to plot a consensus mass feature \ No newline at end of file From f2e8fb90b7dc99fe3675d336c6d1ea415b35eb16 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 13 Aug 2025 16:30:12 -0700 Subject: [PATCH 054/158] WIP exporter, importer for LCMSCOllection --- corems/mass_spectra/factory/lc_class.py | 6 +-- corems/mass_spectra/input/corems_hdf5.py | 50 ++++++++++++++++++++++++ corems/mass_spectra/output/export.py | 33 ++++++++++++++++ 3 files changed, 86 insertions(+), 3 deletions(-) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 8c02ba0b7..e329d4716 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1568,10 +1568,10 @@ def load_raw_data(self, sample_idx: int, ms_level = 1) -> None: scan_df=scan_df, data=data )[f"ms{ms_level}"] - - elif parser_class_name == "MassSpectraParser": + + elif parser_class_name == "ReadCoreMSHDFMassSpectra": raise ValueError( - "MassSpectraParser does not have a method to load raw data. Need to instantiate the original parser to access the raw data." + "ReadCoreMSHDFMassSpectra does not have a method to load raw data. Need to instantiate the original parser to access the raw data." ) def drop_raw_data(self, sample_idx: int, ms_level = 1) -> None: diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 5508b9c84..28d1c3dca 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -3,6 +3,7 @@ from threading import Thread +import h5py import toml import json import multiprocessing @@ -826,3 +827,52 @@ def parameters_files(self): return toml_files else: raise ValueError("Parameters files are not saved for all samples.") + +class ReadSavedLCMSCollection(ReadCoreMSHDFMassSpectraCollection): + """ + Subclass to read and re-instantiate a LCMSCollection from a saved HDF5 file. + + + Parameters + ---------- + collection_hdf5_path : str or Path + Path to the saved LCMSCollection HDF5 file. + cores : int, optional + Number of cores for processing. Default is 1. + """ + + def __init__( + self, + collection_hdf5_path: str, + cores: int = 1 + ): + # Convert to Path objects + self.collection_hdf5_path = Path(collection_hdf5_path) + + # Validate the collection file exists + if not self.collection_hdf5_path.exists(): + raise FileNotFoundError(f"Collection HDF5 file {self.collection_hdf5_path} not found.") + + # Validate cores + self._validate_cores(cores) + + # Set data + self.h5pydata = h5py.File(self.collection_hdf5_path, "r") + + # Load metadata from saved collection + self._load_collection_metadata() + + if not self.folder_location.exists(): + raise FileNotFoundError(f"Folder location {self.folder_location} not found.") + + # Load the mass spectra data + self._validate_manifest() + + def _load_collection_metadata(self): + """Load metadata and manifest from the saved collection HDF5 file.""" + with h5py.File(self.collection_hdf5_path, 'r') as f: + self.folder_location = Path(f.attrs.get('lcms_objects_folder', '')) + manifest_json = f.attrs.get('manifest', '{}') + if isinstance(manifest_json, bytes): + manifest_json = manifest_json.decode('utf-8') + self._manifest_dict = json.loads(manifest_json) diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index bbe740272..edba2192d 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -1972,4 +1972,37 @@ def to_report(self, molecular_metadata=None): return report +class LCMSCollectionExporter(): + """A class to export an LCMS collection. + This class provides methods to export lipidomics data to various formats and summarize the lipid report. + + Parameters + ---------- + out_file_path : str | Path + The output file path, do not include the file extension. + mass_spectra_collection : object + The high resolution mass spectra collection object. + """ + def __init__(self, out_file_path, mass_spectra_collection): + self.out_file_path = Path(out_file_path) + self.mass_spectra_collection = mass_spectra_collection + + def export_to_hdf5(self, overwrite = False): + """Export the LCMS collection to an HDF5 file.""" + # TODO: Add hdf5 export to each of the mass spectra in the collection to ensure that scan time alignment, induced mass features, and mass features into clusters are retained + + if overwrite: + if self.out_file_path.with_suffix(".hdf5").exists(): + self.out_file_path.with_suffix(".hdf5").unlink() + + with h5py.File(self.out_file_path.with_suffix(".hdf5"), "a") as hdf_handle: + # Add basic attributes to the HDF5 file + timenow = str( + datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M:%S %Z") + ) + hdf_handle.attrs["date_utc"] = timenow + hdf_handle.attrs["lcms_objects_folder"] = str(self.mass_spectra_collection.collection_parser.folder_location) + hdf_handle.attrs["manifest"] = json.dumps(self.mass_spectra_collection.collection_parser.manifest) + + #TODO: save parameters \ No newline at end of file From 754d76e3ae87c2a0b907c31eff9accbbaa27f7a2 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 13 Aug 2025 16:51:15 -0700 Subject: [PATCH 055/158] Basic export/import functionality for hdf5 collection working --- corems/mass_spectra/input/corems_hdf5.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 28d1c3dca..7cf7267e4 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -720,8 +720,8 @@ def get_lcms_obj(self, sample_name: str, load_raw=False, load_light=True, use_or lcms_obj.mass_features = {} lcms_obj.light_mf_df = mf_df return lcms_obj - - def get_lcms_collection(self, load_raw = False, load_light = True) -> LCMSCollection: + + def get_lcms_collection(self, load_raw = False, load_light = True, use_original_parser = True) -> LCMSCollection: """Return a LCMSCollection object Parameters @@ -754,8 +754,6 @@ def get_lcms_collection(self, load_raw = False, load_light = True) -> LCMSCollec ncores = self._cores # Create a pool of workers (one for each core or sample, whichever is smaller) pool = multiprocessing.Pool(ncores) - # Load the LCMS objects in parallel - do not instantiate the original parser by default - use_original_parser = True args = [(sample, load_raw, load_light, use_original_parser) for sample in samples] lcms_objs = pool.starmap(self.get_lcms_obj, args) for sample_name, lcms_obj in zip(samples, lcms_objs): @@ -764,7 +762,7 @@ def get_lcms_collection(self, load_raw = False, load_light = True) -> LCMSCollec elif self._cores == 1: # Load the LCMS objects sequentially - do not instantiate the original parser by default for sample_name in samples: - lcms_coll._lcms[sample_name] = self.get_lcms_obj(sample_name, load_raw=load_raw, load_light=load_light, use_original_parser=False) + lcms_coll._lcms[sample_name] = self.get_lcms_obj(sample_name, load_raw=load_raw, load_light=load_light, use_original_parser=use_original_parser) else: raise ValueError("Number of cores must be greater than 0 and set on the ReadCoreMSHDFMassSpectraCollection object.") @@ -856,9 +854,6 @@ def __init__( # Validate cores self._validate_cores(cores) - # Set data - self.h5pydata = h5py.File(self.collection_hdf5_path, "r") - # Load metadata from saved collection self._load_collection_metadata() From b9acb2301f7781c073759d338af578475f722d48 Mon Sep 17 00:00:00 2001 From: Nellie Ciesielski Date: Tue, 2 Sep 2025 17:57:45 +0000 Subject: [PATCH 056/158] Inducing mass features --- .../factory/chroma_peak_classes.py | 4 +- corems/mass_spectra/calc/lc_calc.py | 196 +++++++++++++---- corems/mass_spectra/factory/lc_class.py | 197 +++++++++++++++--- .../nmdc/lipidomics/lipidomics_collection.py | 10 +- 4 files changed, 338 insertions(+), 69 deletions(-) diff --git a/corems/chroma_peak/factory/chroma_peak_classes.py b/corems/chroma_peak/factory/chroma_peak_classes.py index 9a264392c..c6bbfc167 100644 --- a/corems/chroma_peak/factory/chroma_peak_classes.py +++ b/corems/chroma_peak/factory/chroma_peak_classes.py @@ -122,7 +122,7 @@ class LCMSMassFeature(ChromaPeakBase, LCMSMassFeatureCalculation): The scan number of the apex of the feature. persistence : float, optional The persistence of the feature. Default is None. - + Attributes -------- _mz_exp : float @@ -183,7 +183,7 @@ def __init__( intensity: float, apex_scan: int, persistence: float = None, - id: int = None, + id: int = None ): super().__init__( chromatogram_parent=lcms_parent, diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index a13e33b6c..f52dbdf09 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -8,13 +8,14 @@ from sklearn.svm import SVR from sklearn.cluster import AgglomerativeClustering import matplotlib.pyplot as plt +from ast import literal_eval from corems.chroma_peak.factory.chroma_peak_classes import LCMSMassFeature from corems.mass_spectra.calc import SignalProcessing as sp from corems.mass_spectra.factory.chromat_data import EIC_Data from corems.mass_spectrum.input.numpyArray import ms_from_array_profile -warnings.filterwarnings("ignore", category=RuntimeWarning) +warnings.filterwarnings("ignore", category=RuntimeWarning) def find_closest(A, target): """Find the index of closest value in A to each value in target. @@ -185,20 +186,25 @@ def find_nearest_scan(self, rt): return real_scan - def add_peak_metrics(self): + def add_peak_metrics(self, induced_features = False): """Add peak metrics to the mass features. This function calculates the peak metrics for each mass feature and adds them to the mass feature objects. """ # Check that at least some mass features have eic data - if not any([mf._eic_data is not None for mf in self.mass_features.values()]): + if induced_features: + mf_dict_values = self.induced_mass_features.values() + else: + mf_dict_values = self.mass_features.values() + + if not any([mf._eic_data is not None for mf in mf_dict_values]): raise ValueError( "No mass features have EIC data. Run integrate_mass_features first." ) - for mass_feature in self.mass_features.values(): + for mass_feature in mf_dict_values: # Check if the mass feature has been integrated - if mass_feature._eic_data is not None: + if mass_feature._eic_data is not None and mass_feature.area is not None: # Calculate peak metrics mass_feature.calc_half_height_width() mass_feature.calc_tailing_factor() @@ -382,7 +388,7 @@ def find_mass_features(self, ms_level=1, grid=True): raise ValueError("Peak picking method not implemented") def integrate_mass_features( - self, drop_if_fail=True, drop_duplicates=True, ms_level=1 + self, drop_if_fail=True, drop_duplicates=True, ms_level=1, induced_features=False ): """Integrate mass features and extract EICs. @@ -399,6 +405,8 @@ def integrate_mass_features( Default is True. ms_level : int, optional The MS level to use. Default is 1. + induced_features : bool, optional + Whether the mass features to be intergrated were induced. Default is False. Raises ------ @@ -414,24 +422,41 @@ def integrate_mass_features( ----- drop_if_fail is useful for discarding mass features that do not have good shapes, usually due to a detection on a shoulder of a peak or a noisy region (especially if minimal smoothing is used during mass feature detection). """ + # Check if there is data if ms_level in self._ms_unprocessed.keys(): raw_data = self._ms_unprocessed[ms_level].copy() else: raise ValueError("No MS level " + str(ms_level) + " data found") - if self.mass_features is not None: - mf_df = self.mass_features_to_df().copy() - else: - raise ValueError( - "No mass features found, did you run find_mass_features() first?" - ) + # Check if mass_spectrum exists on each mass feature - if not all( - [mf.mass_spectrum is not None for mf in self.mass_features.values()] - ): - raise ValueError( - "Mass spectrum must be associated with each mass feature, did you run add_associated_ms1() first?" - ) + if induced_features: + mf_dict = self.induced_mass_features + if len(mf_dict) == 0: + raise ValueError( + "No mass features found, did you run search_for_targeted_mass_feature() first?" + ) + if not any( + [mf.mass_spectrum is not None for mf in mf_dict.values()] + ): + raise ValueError( + "Mass spectrum must be associated with induced mass features, did you run add_associated_ms1() first?" + ) + ## remove not found induced mass features + mf_dict = {k:v for k, v in mf_dict.items() if v.mass_spectrum is not None} + + else: + mf_dict = self.mass_features + if len(mf_dict) == 0: + raise ValueError( + "No mass features found, did you run find_mass_features() first?" + ) + if not all( + [mf.mass_spectrum is not None for mf in mf_dict.values()] + ): + raise ValueError( + "Mass spectrum must be associated with each mass feature, did you run add_associated_ms1() first?" + ) # Subset scan data to only include correct ms_level scan_df_sub = self.scan_df[ @@ -441,7 +466,7 @@ def integrate_mass_features( raise ValueError("No MS level " + ms_level + " data found in scan data") scan_df_sub = scan_df_sub[["scan", "scan_time"]].copy() - mzs_to_extract = np.unique(mf_df["mz"].values) + mzs_to_extract = np.unique([mf.mz for mf in mf_dict.values()]) mzs_to_extract.sort() # Get EICs for each unique mz in mass features list @@ -468,14 +493,13 @@ def integrate_mass_features( self.eics[mz] = myEIC # Get limits of mass features using EIC centroid detector and integrate - mf_df["area"] = np.nan - for idx, mass_feature in mf_df.iterrows(): + for idx, mass_feature in mf_dict.items(): mz = mass_feature.mz apex_scan = mass_feature.apex_scan # Pull EIC data and find apex scan index myEIC = self.eics[mz] - self.mass_features[idx]._eic_data = myEIC + mf_dict[idx]._eic_data = myEIC apex_index = np.where(myEIC.scans == apex_scan)[0][0] # Find left and right limits of peak using EIC centroid detector, add to EICData @@ -500,7 +524,7 @@ def integrate_mass_features( else: consecutive_scans = 0 if consecutive_scans < self.parameters.lc_ms.consecutive_scan_min: - self.mass_features.pop(idx) + mf_dict.pop(idx) continue # Add start and final scan to mass_features and EICData left_scan, right_scan = ( @@ -509,25 +533,28 @@ def integrate_mass_features( ) mf_scan_apex = [(left_scan, int(apex_scan), right_scan)] myEIC.apexes = myEIC.apexes + mf_scan_apex - self.mass_features[idx].start_scan = left_scan - self.mass_features[idx].final_scan = right_scan + mf_dict[idx].start_scan = left_scan + mf_dict[idx].final_scan = right_scan # Find area under peak using limits from EIC centroid detector, add to mass_features and EICData area = np.trapz( myEIC.eic_smoothed[l_a_r_scan_idx[0][0] : l_a_r_scan_idx[0][2] + 1], myEIC.time[l_a_r_scan_idx[0][0] : l_a_r_scan_idx[0][2] + 1], ) - mf_df.at[idx, "area"] = area myEIC.areas = myEIC.areas + [area] self.eics[mz] = myEIC - self.mass_features[idx]._area = area + mf_dict[idx]._area = area else: if drop_if_fail is True: - self.mass_features.pop(idx) + mf_dict.pop(idx) if drop_duplicates: # Prepare mass feature dataframe - mf_df = self.mass_features_to_df().copy() + if induced_features: + mf_df = self.mass_features_to_df(induced_features = True).copy() + mf_df = mf_df[mf_df.start_scan.notna()] + else: + mf_df = self.mass_features_to_df(induced_features = False).copy() # For each mass feature, find all mass features within the clustering tolerance ppm and drop if their start and end times are within another mass feature # Kepp the first mass fea @@ -2182,14 +2209,14 @@ def summarize_clusters(self): self.mass_features_dataframe.groupby("cluster") .agg( { - "mz": ["median", "mean", "std"], - "scan_time_aligned": ["median", "mean", "std"], - "half_height_width": ["median", "mean", "std"], - "tailing_factor": ["median", "mean", "std"], - "dispersity_index": ["median", "mean", "std"], + "mz": ["median", "mean", "std", "max", "min"], + "scan_time_aligned": ["median", "mean", "std", "max", "min"], + "half_height_width": ["median", "mean", "std", "max", "min"], + "tailing_factor": ["median", "mean", "std", "max", "min"], + "dispersity_index": ["median", "mean", "std", "max", "min"], "sample_id": ["nunique"], - "intensity": ["max", "median", "mean", "std"], - "persistence": ["max", "median", "mean", "std"], + "intensity": ["max", "median", "mean", "std", "max", "min"], + "persistence": ["max", "median", "mean", "std", "max", "min"], } ) .reset_index() @@ -2233,7 +2260,7 @@ def plot_mz_features_per_cluster(self, return_fig = False): else: sum_data = self.cluster_summary_dataframe fig, ax = plt.subplots() - sum_data.sample_id_count.value_counts().sort_index().plot(ax = ax, kind = 'bar') + sum_data.sample_id_nunique.value_counts().sort_index().plot(ax = ax, kind = 'bar') plt.xlabel('Number of mass features in a cluster') plt.ylabel('Number of clusters with this many mass features') if return_fig: @@ -2329,7 +2356,7 @@ def plot_consensus_mz_features(self, xb = 'xb', xt = 'xt', yb = 'yb', yt = 'yt', df.mz_median, c = 'tab:orange', alpha = 0.7, - s = (df.sample_id_count**2)/5 + s = (df.sample_id_nunique**2)/5 ) plt.xlabel('Scan time') @@ -2348,7 +2375,7 @@ def plot_consensus_mz_features(self, xb = 'xb', xt = 'xt', yb = 'yb', yt = 'yt', kw = dict( prop = 'sizes', - num = len(df.sample_id_count.unique())/3, + num = len(df.sample_id_nunique.unique())/3, color = 'tab:orange', alpha = 0.7, func = lambda s: np.sqrt(s*5) @@ -2813,3 +2840,94 @@ def plot_cluster_outlier_frequency(self, dim_list = ['mz', 'scan_time_aligned'], return fig else: plt.show() + + def search_for_missing_mass_features_in_one_sample(self, samplename, threshold = 0.5, tol_flag = 0): + ''' + Work in progress temporary code + threshold default to 0.5 --> + only consider clusters that contain at least 50% of the sample + tol_flag default to 0 --> + don't check for possible mass features on the edges of the cluster + for sampleindex == 7, tol_flag == 1 picks up 6 more mass features + ''' + summarydf = self.cluster_summary_dataframe + mfdf = self.mass_features_dataframe + sampleindex = self.samples.index(samplename) + self.load_raw_data(sampleindex, 1) + + sample_ct = len(self.samples) + missingdf = summarydf[[ + 'cluster', + 'sample_id_nunique', + 'mz_min', + 'mz_max', + 'scan_time_aligned_min', + 'scan_time_aligned_max' + ]] + missingdf = missingdf[missingdf.sample_id_nunique > threshold*(sample_ct)] + missingdf = missingdf[missingdf.sample_id_nunique != sample_ct] + + missingdf['missing_samples'] = None + for c in missingdf.cluster.to_numpy(): + cludf = mfdf[mfdf.cluster == c] + missingdf.loc[c, 'missing_samples'] = str( + [x for x in mfdf.sample_name.unique() if x not in cludf.sample_name.unique()] + ) + missingdf['missing_samples'] = missingdf.missing_samples.apply(literal_eval) + + ## to get clusters missing data based on sample name: + sampledf = missingdf[ + missingdf.missing_samples.apply(lambda x: samplename in x) + ].reset_index(drop = True).copy() + + if tol_flag == 1: + mz_clu_tol = self.parameters.lcms_collection.consensus_mz_tol_ppm * 1e-6 + rt_clu_tol = self.parameters.lcms_collection.consensus_rt_tol + sampledf['mz_max_allowed'] = sampledf.mz_max + mz_clu_tol*sampledf.mz_max + sampledf['mz_min_allowed'] = sampledf.mz_min - mz_clu_tol*sampledf.mz_min + sampledf['sta_max_allowed'] = sampledf.scan_time_aligned_max + rt_clu_tol*sampledf.scan_time_aligned_max + sampledf['sta_min_allowed'] = sampledf.scan_time_aligned_min - rt_clu_tol*sampledf.scan_time_aligned_min + + ms1df = self[sampleindex]._ms_unprocessed[1].copy() + scan_df = self[sampleindex].scan_df[['scan', 'scan_time_aligned']] + ms1df = pd.merge(ms1df, scan_df, on = 'scan') + + for i in range(len(sampledf)): + mz_min = sampledf.mz_min.iloc[i] + mz_max = sampledf.mz_max.iloc[i] + st_min = sampledf.scan_time_aligned_min.iloc[i] + st_max = sampledf.scan_time_aligned_max.iloc[i] + found_feature = self[sampleindex].search_for_targeted_mass_feature( + ms1df, + mz_min, + mz_max, + st_min, + st_max, + set_id = 'c' + str(sampledf.cluster.iloc[i]) + '_' + str(i) + '_i', + obj_idx = sampleindex, + st_aligned = True + ) + + if tol_flag == 1 and found_feature.apex_scan == -99: + mz_min = sampledf.mz_min_allowed.iloc[i] + mz_max = sampledf.mz_max_allowed.iloc[i] + st_min = sampledf.sta_min_allowed.iloc[i] + st_max = sampledf.sta_max_allowed.iloc[i] + + found_feature = self[sampleindex].search_for_targeted_mass_feature( + ms1df, + mz_min, + mz_max, + st_min, + st_max, + set_id = 'c' + str(sampledf.cluster.iloc[i]) + '_' + str(i) + '_i', + obj_idx = sampleindex, + st_aligned = True + ) + + self[sampleindex].induced_mass_features[found_feature.id] = found_feature + + self[sampleindex].add_associated_ms1(induced_features = True) + # need to set drop_if_fail to false for induced features as they will fail + self[sampleindex].integrate_mass_features(drop_if_fail = False, induced_features = True) + self[sampleindex].add_peak_metrics(induced_features = True) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 8c02ba0b7..a57119a84 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -12,6 +12,7 @@ from corems.molecular_id.search.lcms_spectral_search import LCMSSpectralSearch from corems.mass_spectrum.input.numpyArray import ms_from_array_profile from corems.mass_spectra.calc.lc_calc import find_closest +from corems.chroma_peak.factory.chroma_peak_classes import LCMSMassFeature class MassSpectraBase: @@ -380,6 +381,13 @@ class LCMSBase(MassSpectraBase, LCCalculations, PHCalculations, LCMSSpectralSear mass_features : dictionary of LCMSMassFeature objects A dictionary containing mass features for the dataset. Key is mass feature ID. Initialized as an empty dictionary. + induced_mass_features: dictionary of LCMSMassFeature objects + A dictionary containing mass features from a collection that don't + satisfy criteria for initial mass features. Key is mass feature ID. + Initialized as an empty dictionary. + missing_mass_features: pandas.DataFrame + A table of clusters in a given sample for which a mass feature was + sought and not found spectral_search_results : dictionary of MS2SearchResults objects A dictionary containing spectral search results for the dataset. Key is scan number : precursor mz. Initialized as an empty dictionary. @@ -404,6 +412,9 @@ class LCMSBase(MassSpectraBase, LCCalculations, PHCalculations, LCMSSpectralSear Sets the scan number list from the data in the _ms dictionary. * plot_composite_mz_features(binsize = 1e-4, ph_int_min_thresh = 0.001, mf_plot = True, ms2_plot = True, return_fig = False) Generates plot of M/Z features comparing scan time vs M/Z value + * search_for_targeted_mass_feature(ms1df: pd.DataFrame, sample: pd.Series, tol_flag = 0) + Searches for mass features in specific M/Z and scan time windows that + were missed by the persistent homology search """ def __init__( @@ -424,6 +435,7 @@ def __init__( self._tic_list = [] self.eics = {} self.mass_features = {} + self.induced_mass_features = {} self.spectral_search_results = {} def get_parameters_json(self): @@ -556,7 +568,7 @@ def add_associated_ms2_dda( ] def add_associated_ms1( - self, auto_process=True, use_parser=True, spectrum_mode=None + self, auto_process=True, use_parser=True, spectrum_mode=None, induced_features=False ): """Add MS1 spectra associated with mass features to the dataset. @@ -570,6 +582,8 @@ def add_associated_ms1( The spectrum mode to use for the mass spectra. If None, method will use the spectrum mode from the spectra parser to ascertain the spectrum mode (this allows for mixed types). Defaults to None. (faster if defined, otherwise will check each scan) + induced_features : bool, optional + If True, add associated MS1 of the induced mass features instead of the primary mass features Raises ------ @@ -584,14 +598,23 @@ def add_associated_ms1( raise ValueError( "mass_features not set, must run find_mass_features() first" ) + + if induced_features: + mf_dict = self.induced_mass_features + else: + mf_dict = self.mass_features + scans_to_average = self.parameters.lc_ms.ms1_scans_to_average + + ## sketchy work around for induced mass features + scan_list = [ + int(mf_dict[x].apex_scan) for x in mf_dict if int(mf_dict[x].apex_scan) != -99 + ] if scans_to_average == 1: # Add to LCMSobj self.add_mass_spectra( - scan_list=[ - int(x) for x in self.mass_features_to_df().apex_scan.tolist() - ], + scan_list = scan_list, auto_process=auto_process, use_parser=use_parser, spectrum_mode=spectrum_mode, @@ -601,7 +624,7 @@ def add_associated_ms1( elif ( (scans_to_average - 1) % 2 ) == 0: # scans_to_average = 3, 5, 7 etc, mirror l/r around apex - apex_scans = list(set(self.mass_features_to_df().apex_scan.tolist())) + apex_scans = list(set(scan_list)) # Check if all apex scans are profile mode, raise error if not if not all(self.scan_df.loc[apex_scans, "ms_format"] == "profile"): raise ValueError("All apex scans must be profile mode for averaging") @@ -683,22 +706,28 @@ def get_scans_from_apex(ms1_scans, apex_scan, scans_to_average): ) # Associate the ms1 spectra with the mass features - for mf_id in self.mass_features: - self.mass_features[mf_id].mass_spectrum = self._ms[ - self.mass_features[mf_id].apex_scan - ] - self.mass_features[mf_id].update_mz() - - # Re-process clustering if persistent homology is selected to remove duplicate mass features after adding and processing MS1 spectra - if self.parameters.lc_ms.peak_picking_method == "persistent homology": - self.cluster_mass_features(drop_children=True, sort_by="persistence") - - def mass_features_to_df(self): + for k in mf_dict.keys(): + ## another induced feature work around + if mf_dict[k].apex_scan != -99: + mf_dict[k].mass_spectrum = self._ms[ + mf_dict[k].apex_scan + ] + mf_dict[k].update_mz() + + if not induced_features: + # Re-process clustering if persistent homology is selected to remove duplicate mass features after adding and processing MS1 spectra + if self.parameters.lc_ms.peak_picking_method == "persistent homology": + self.cluster_mass_features(drop_children=True, sort_by="persistence") + + def mass_features_to_df(self, induced_features = False): """Returns a pandas dataframe summarizing the mass features. The dataframe contains the following columns: mf_id, mz, apex_scan, scan_time, intensity, persistence, area, monoisotopic_mf_id, and isotopologue_type. The index is set to mf_id (mass feature ID). - + Parameters + ----------- + induced_features : bool, optional + If True, calls the induced_mass_features dictionary. Defaults to False. Returns -------- @@ -739,6 +768,11 @@ def mass_spectrum_to_string( ] return "; ".join(mz_abun_str) + if induced_features: + mf_dict = self.induced_mass_features + else: + mf_dict = self.mass_features + cols_in_df = [ "id", "_apex_scan", @@ -754,34 +788,35 @@ def mass_spectrum_to_string( "isotopologue_type", "mass_spectrum_deconvoluted_parent", ] + df_mf_list = [] - for mf_id in self.mass_features.keys(): + for mf_id in mf_dict.keys(): # Find cols_in_df that are in single_mf df_keys = list( - set(cols_in_df).intersection(self.mass_features[mf_id].__dir__()) + set(cols_in_df).intersection(mf_dict[mf_id].__dir__()) ) dict_mf = {} for key in df_keys: - dict_mf[key] = getattr(self.mass_features[mf_id], key) - if len(self.mass_features[mf_id].ms2_scan_numbers) > 0: + dict_mf[key] = getattr(mf_dict[mf_id], key) + if len(mf_dict[mf_id].ms2_scan_numbers) > 0: # Add MS2 spectra info - best_ms2_spectrum = self.mass_features[mf_id].best_ms2 + best_ms2_spectrum = mf_dict[mf_id].best_ms2 if best_ms2_spectrum is not None: dict_mf["ms2_spectrum"] = mass_spectrum_to_string(best_ms2_spectrum) - if len(self.mass_features[mf_id].associated_mass_features_deconvoluted) > 0: + if len(mf_dict[mf_id].associated_mass_features_deconvoluted) > 0: dict_mf["associated_mass_features"] = ", ".join( map( str, - self.mass_features[mf_id].associated_mass_features_deconvoluted, + mf_dict[mf_id].associated_mass_features_deconvoluted, ) ) - if self.mass_features[mf_id]._half_height_width is not None: - dict_mf["half_height_width"] = self.mass_features[ + if mf_dict[mf_id]._half_height_width is not None: + dict_mf["half_height_width"] = mf_dict[ mf_id ].half_height_width # Check if EIC for mass feature is set df_mf_single = pd.DataFrame(dict_mf, index=[mf_id]) - df_mf_single["mz"] = self.mass_features[mf_id].mz + df_mf_single["mz"] = mf_dict[mf_id].mz df_mf_list.append(df_mf_single) df_mf = pd.concat(df_mf_list) @@ -1069,6 +1104,114 @@ def plot_composite_mz_features(self, binsize = 1e-4, ph_int_min_thresh = 0.001, else: plt.show() + + def search_for_targeted_mass_feature( + self, + ms1df, + mz_min, + mz_max, + st_min, + st_max, + set_id, + obj_idx = 0, + st_aligned = False + ): + """ + Returns an LCMSMassFeature from a specific sample within a specific mass and time range. Returns an empty + LCMSMassFeature if no satisfactory peak is found in the given window. + + Parameters + ----------- + ms1df : Pandas DataFrame + Dataframe containing all the possible MS1 values to consider, collected by calling _ms_unprocessed[1] on the sample. + mz_min : float + Identifies lower bound of the weights to use to find a peak. + mz_max : float + Identifies upper bound of the weights to use to find a peak. + st_min : float + Identifies lower bound of the scan times to use to find a peak. + st_max : float + Identifies upper bound of the scan times to use to find a peak. + set_id : str + Indicates string used as ID in LCMSMassFeature. + obj_idx : int + Identifies index of sample in a collection that LCMSMassFeature should be assigned to. Defaults to 0 and is not used + if data provided is an LCMSBase instead of an LCMSCollection. + st_aligned : boolean + Indicates whether to call scan time from scan_time or from scan_time_aligned if using a collection. Defaults to False. + + Returns + -------- + LCMSMassFeature + Object from ChromaPeak that contains data on selected MS1 peak. If no peak is found, will contain missing + information and list the apex scan value as -99. + + Raises + ------ + Warning + If appropriate scan time data is not contained in ms1df. + """ + + if st_aligned == True: + if not 'scan_time_aligned' in ms1df.columns: + raise ValueError( + "Aligned scan times not contained in ms1df. Merge ms1df with scan_df on 'scan' to import aligned scan times." + ) + else: + if not 'scan_time' in ms1df.columns: + raise ValueError( + "Scan times not contained in ms1df. Merge ms1df with scan_df on 'scan' to import scan times." + ) + + mzfit = ms1df[( + ms1df.mz >= mz_min + ) & ( + ms1df.mz <= mz_max + )].reset_index(drop = True) + + if st_aligned == True: + mzfit_st = mzfit.scan_time_aligned + else: + mzfit_st = mzfit.scan_time + + rtfit = mzfit[( + mzfit_st >= st_min + ) & ( + mzfit_st <= st_max + )] + + if len(rtfit) == 0: + row_dict = { + 'apex_scan': -99, + 'mz': np.nan, + 'intensity': np.nan, + 'retention_time': np.nan, + 'persistence': np.nan, + 'id': set_id + } + else: + inducedpeak = rtfit[rtfit.intensity == rtfit.intensity.max()] + if st_aligned == True: + rt_induced = inducedpeak.scan_time_aligned.values[0] + else: + rt_induced = inducedpeak.scan_time.values[0] + + row_dict = { + 'apex_scan': inducedpeak.scan.values[0], + 'mz': inducedpeak.mz.values[0], + 'intensity': inducedpeak.intensity.values[0], + 'retention_time': rt_induced, + 'persistence': np.nan, + 'id': set_id + } + + if self.__class__.__name__ == 'LCMSBase': + output = LCMSMassFeature(self, **row_dict) + elif self.__class__.__name__ == 'LCMSCollection': + output = LCMSMassFeature(self[obj_idx], **row_dict) + + return output + def __len__(self): """ diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 647d1ec3b..38f7fafb8 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -54,7 +54,7 @@ print("Time to roll up consensus mass features: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") lcms_collection.plot_mz_features_across_samples() - lcms_collection.plot_mz_features_per_cluster() #TODO: Fix this function, - errroring out with 'DataFrame' object has no attribute 'sample_id_count' + lcms_collection.plot_mz_features_per_cluster() lcms_collection.plot_consensus_mz_features() ## zoomed out lcms_collection.plot_consensus_mz_features(xb = 10, xt = 15, yb = 500, yt = 600) ## zoomed in lcms_collection.cluster_inspection_plot(11391) @@ -69,5 +69,13 @@ ] lcms_collection.plot_cluster_outlier_frequency(dim_list, clu_size_thresh = 0.25) + ## WORK IN PROGRESS: temporary code for testing + ## want to adjust function to iterate throught samples by index, not name + ## want to be able to do that in parallel/multiprocess + samplename = 'Blanch_Nat_Lip_H_11_AB_M_13_POS_23Jan18_Brandi-WCSH5801' + lcms_collection.search_for_missing_mass_features_in_one_sample(samplename) + print(lcms_collection._lcms[samplename].mass_features_to_df(induced_features = True)) + + #TODO: Add code to load and save information about chromatographic settings #TODO: Add code to save and load collection to HDF5 file #TODO: Add code to plot a consensus mass feature \ No newline at end of file From fd85bd043b3298380be2113e39106714c9ba9f16 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 16 Sep 2025 09:22:03 -0700 Subject: [PATCH 057/158] Fix on integrations --- corems/mass_spectra/calc/lc_calc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 27da64fd5..7c6b2e26a 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -499,7 +499,7 @@ def integrate_mass_features( self.eics[mz] = myEIC # Get limits of mass features using EIC centroid detector and integrate - for idx, mass_feature in mf_dict.items(): + for idx, mass_feature in list(mf_dict.items()): mz = mass_feature.mz apex_scan = mass_feature.apex_scan From 96446f899e0c9be4f992daf93dfd386f882ac196 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 16 Sep 2025 09:24:15 -0700 Subject: [PATCH 058/158] Modify integration calculation for speed --- corems/mass_spectra/calc/lc_calc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 7c6b2e26a..9a4c26068 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -506,7 +506,7 @@ def integrate_mass_features( # Pull EIC data and find apex scan index myEIC = self.eics[mz] mf_dict[idx]._eic_data = myEIC - apex_index = np.where(myEIC.scans == apex_scan)[0][0] + apex_index = np.searchsorted(myEIC.scans, apex_scan) # Find left and right limits of peak using EIC centroid detector, add to EICData centroid_eics = self.eic_centroid_detector( From f7480466355f2f3be643f6fd5075665538de276f Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 16 Sep 2025 10:08:10 -0700 Subject: [PATCH 059/158] Add functionality for saving attribute of retention time alignment --- corems/mass_spectra/calc/lc_calc.py | 42 ++++++++++++++++--- corems/mass_spectra/factory/lc_class.py | 3 ++ .../nmdc/lipidomics/lipidomics_collection.py | 25 +++++++++-- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 9a4c26068..c9482fd12 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -2345,6 +2345,10 @@ def align_lcms_objects(self, overwrite=False): This function has been adapted from the original implementation in the Deimos package: https://github.com/pnnl/deimos """ + # Initialize rt_alignments dictionary if it doesn't exist + if not hasattr(self, 'rt_alignments'): + self.rt_alignments = {} + # Prepare the center LCMS object center_obj_ids = self.manifest_dataframe[ self.manifest_dataframe["center"] @@ -2368,6 +2372,13 @@ def align_lcms_objects(self, overwrite=False): center_scan_df = self[center_obj_id].scan_df.copy() center_scan_df["scan_time_aligned"] = center_scan_df["scan_time"] self[center_obj_id].scan_df = center_scan_df + + # Store alignment data for center object (identity mapping) + center_sample_name = self.samples[center_obj_id] + self.rt_alignments[center_sample_name] = { + 'adjusted_times': center_scan_df["scan_time"].values, + 'alignment_function': None + } index_steps = (1, -1) # Run this twice, once going forward (+1 indexing) and once going backward (-1 indexing) @@ -2403,6 +2414,11 @@ def align_lcms_objects(self, overwrite=False): new_scan_info = self[i].scan_df.copy() new_scan_info["scan_time_aligned"] = new_times self[i].scan_df = new_scan_info + + # Store alignment data + self.rt_alignments[sample_name] = { + 'adjusted_times': new_times.values, + } else: # Set aligned retention times on scan_df for lc_obj using the original retention times new_scan_info = self[i].scan_df.copy() @@ -2410,6 +2426,11 @@ def align_lcms_objects(self, overwrite=False): "scan_time" ] self[i].scan_df = new_scan_info + + # Store alignment data (identity mapping) + self.rt_alignments[sample_name] = { + 'adjusted_times': new_scan_info["scan_time"].values, + } i += index_step if i >= len(self) or i < 0: @@ -2450,6 +2471,12 @@ def align_lcms_objects(self, overwrite=False): new_scan_info = self[i].scan_df.copy() new_scan_info["scan_time_aligned"] = new_times self[i].scan_df = new_scan_info + + # Store alignment data + self.rt_alignments[sample_name] = { + 'adjusted_times': new_times.values, + 'alignment_function': spl + } # Get the batch that this object belongs to batch = self.manifest[self.samples[i]]["batch"] @@ -2457,15 +2484,20 @@ def align_lcms_objects(self, overwrite=False): for j in range(len(self)): if self.manifest[self.samples[j]]["batch"] == batch: if j != i: - sample_name = self.samples[j] - self._manifest_dict[sample_name]["use_rt_alignment"] = ( + sample_name_j = self.samples[j] + self._manifest_dict[sample_name_j]["use_rt_alignment"] = ( use_spline_alignment ) new_scan_info = self[j].scan_df.copy() - new_scan_info["scan_time_aligned"] = spl( - self[j].scan_df["scan_time_aligned"] - ) + aligned_times = spl(self[j].scan_df["scan_time_aligned"]) + new_scan_info["scan_time_aligned"] = aligned_times self[j].scan_df = new_scan_info + + # Store alignment data + self.rt_alignments[sample_name_j] = { + 'adjusted_times': aligned_times.values, + 'alignment_function': spl + } # Set final mass_features_dataframe with the aligned scan_time center_sample_name = self.samples[center_obj_ids[0]] diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 43fa34f27..adabc8365 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1455,6 +1455,9 @@ def __init__( self._parameters = LCMSCollectionParameters() self.isotopes_dropped = False + # These attributes are set during processing + self.rt_alignments = {} + def _reorder_lcms_objects(self): """ Reorders the LCMS objects in the collection based on the order in the manifest. diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 38f7fafb8..cd0fbf1a4 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -1,7 +1,7 @@ from pathlib import Path import time -from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectraCollection - +from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectraCollection, ReadSavedLCMSCollection +from corems.mass_spectra.output.export import LCMSCollectionExporter if __name__ == "__main__": # Set the path to the collection of LCMS runs (previously processed) @@ -24,12 +24,29 @@ start_time = time.time() lcms_collection = parser.get_lcms_collection(load_raw=False, load_light=True) print("Time to load LCMS collection ", time.time() - start_time, "seconds -", len(lcms_collection), " LCMS runs and ", ncores, " cores") - #10s for 7 samples, 10 cores; 162s for 70 samples, 10 cores - # Check and demonstrate the parsers' ability to load raw data lcms_collection.load_raw_data(sample_idx=0, ms_level=1) assert lcms_collection[0]._ms_unprocessed[1] is not None, "Raw data for MS1 should be loaded successfully." lcms_collection.drop_raw_data(sample_idx=0, ms_level=1) + #10s for 7 samples, 10 cores; 162s for 70 samples, 10 cores + + # Save the LCMS collection to a new location + exporter = LCMSCollectionExporter( + out_file_path="test_lcms_collection_out", + mass_spectra_collection=lcms_collection + ) + exporter.export_to_hdf5(overwrite=True) + + # Reload the LCMS collection from the saved location and check that we can load raw data + parser2 = ReadSavedLCMSCollection( + collection_hdf5_path=Path("test_lcms_collection_out.hdf5"), + cores=ncores + ) + lcms_collection2 = parser2.get_lcms_collection(load_raw=False, load_light=True) + lcms_collection2.load_raw_data(sample_idx=0, ms_level=1) + assert lcms_collection2[0]._ms_unprocessed[1] is not None, "Raw data for MS1 should be loaded successfully." + lcms_collection2.drop_raw_data(sample_idx=0, ms_level=1) + del parser2, lcms_collection2 # Set flag to call _drop_isotopologue() when running _check_mass_features_df() lcms_collection.parameters.lcms_collection.drop_isotopologues = True From 7414a3d805fa7e365de7d456e34ac8f5e830666e Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 16 Sep 2025 11:56:30 -0700 Subject: [PATCH 060/158] Add functionality for saving retention time alignment on collection, also fix a typo in a parameter --- .../factory/processingSetting.py | 28 +++++----- corems/mass_spectra/calc/lc_calc.py | 53 +++++------------- corems/mass_spectra/factory/lc_class.py | 15 ++++- corems/mass_spectra/output/export.py | 55 ++++++++++++++++++- .../nmdc/lipidomics/lipidomics_collection.py | 38 ++++++++----- 5 files changed, 117 insertions(+), 72 deletions(-) diff --git a/corems/encapsulation/factory/processingSetting.py b/corems/encapsulation/factory/processingSetting.py index a328479a3..ffc21b319 100644 --- a/corems/encapsulation/factory/processingSetting.py +++ b/corems/encapsulation/factory/processingSetting.py @@ -1002,10 +1002,10 @@ class LCMSCollectionSettings: alignment_hold_out_fraction: float, optional Hold out fraction for testing retention time alignment. Default is 0.3. - alignment_acceptance_techinque: list, optional + alignment_acceptance_technique: list, optional List of alignment acceptance techniques for retention time alignment. Default is ['fraction_improved', 'mean_squared_error_improved']. - alignment_acceptance_techinques_available: tuple, optional + alignment_acceptance_techniques_available: tuple, optional Tuple of available alignment acceptance techniques for retention time alignment. Default is ('fraction_improved', 'mean_squared_error_improved'). alignment_acceptance_fraction_improved_threshold: float, optional @@ -1032,8 +1032,8 @@ class LCMSCollectionSettings: mass_feature_anchor_techniques_available: tuple = ("deconvoluted_mass_spectra", "absolute_intensity") mass_feature_anchor_aboslute_intensity_threshold: int = 10000 alignment_hold_out_fraction: float = 0.3 - _alignment_acceptance_techinque: list = dataclasses.field(default_factory=lambda: ["fraction_improved", "mean_squared_error_improved"]) - alignment_acceptance_techinques_available: tuple = ("fraction_improved", "mean_squared_error_improved") + _alignment_acceptance_technique: list = dataclasses.field(default_factory=lambda: ["fraction_improved", "mean_squared_error_improved"]) + alignment_acceptance_techniques_available: tuple = ("fraction_improved", "mean_squared_error_improved") alignment_acceptance_fraction_improved_threshold: float = 0.5 alignment_mz_tol_ppm: int = 5 alignment_rt_tol: float = 0.4 @@ -1047,13 +1047,13 @@ class LCMSCollectionSettings: def __post_init__(self): self.consensus_mz_tol_ppm = self.alignment_mz_tol_ppm - self._validate_alignment_acceptance_techinque(self.alignment_acceptance_techinque) + self._validate_alignment_acceptance_technique(self.alignment_acceptance_technique) self._validate_mass_feature_anchor_technique(self.mass_feature_anchor_technique) - def _validate_alignment_acceptance_techinque(self, techniques): + def _validate_alignment_acceptance_technique(self, techniques): for technique in techniques: - if technique not in self.alignment_acceptance_techinques_available: - raise ValueError(f"Alignment acceptance technique '{technique}' is not available. Alignment acceptance technique must be passed as a list. Available techniques: {self.alignment_acceptance_techinques_available}") + if technique not in self.alignment_acceptance_techniques_available: + raise ValueError(f"Alignment acceptance technique '{technique}' is not available. Alignment acceptance technique must be passed as a list. Available techniques: {self.alignment_acceptance_techniques_available}") def _validate_mass_feature_anchor_technique(self, techniques): for technique in techniques: @@ -1061,13 +1061,13 @@ def _validate_mass_feature_anchor_technique(self, techniques): raise ValueError(f"Mass feature anchor technique '{technique}' is not available. Alignment acceptance technique must be passed as a list. Available techniques: {self.mass_feature_anchor_techniques_available}") @property - def alignment_acceptance_techinque(self): - return self._alignment_acceptance_techinque + def alignment_acceptance_technique(self): + return self._alignment_acceptance_technique - @alignment_acceptance_techinque.setter - def alignment_acceptance_techinque(self, value): - self._validate_alignment_acceptance_techinque(value) - self._alignment_acceptance_techinque = value + @alignment_acceptance_technique.setter + def alignment_acceptance_technique(self, value): + self._validate_alignment_acceptance_technique(value) + self._alignment_acceptance_technique = value @property def mass_feature_anchor_technique(self): diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index c9482fd12..fed9cedee 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -2310,7 +2310,7 @@ def attempt_alignment(self, matches_c, matches_i): if ( "fraction_improved" - in self.parameters.lcms_collection.alignment_acceptance_techinque + in self.parameters.lcms_collection.alignment_acceptance_technique ): fraction_improved = np.sum(fit_diff < og_diff) / len(og_diff) use_spline_alignment = ( @@ -2319,11 +2319,13 @@ def attempt_alignment(self, matches_c, matches_i): ) if ( "mean_squared_error_improved" - in self.parameters.lcms_collection.alignment_acceptance_techinque + in self.parameters.lcms_collection.alignment_acceptance_technique ): mse_og = np.mean(og_diff**2) mse = np.mean(fit_diff**2) use_spline_alignment = mse < mse_og + # Convert to boolean + use_spline_alignment = bool(use_spline_alignment) return use_spline_alignment, spl @@ -2345,10 +2347,7 @@ def align_lcms_objects(self, overwrite=False): This function has been adapted from the original implementation in the Deimos package: https://github.com/pnnl/deimos """ - # Initialize rt_alignments dictionary if it doesn't exist - if not hasattr(self, 'rt_alignments'): - self.rt_alignments = {} - + # Prepare the center LCMS object center_obj_ids = self.manifest_dataframe[ self.manifest_dataframe["center"] @@ -2375,10 +2374,6 @@ def align_lcms_objects(self, overwrite=False): # Store alignment data for center object (identity mapping) center_sample_name = self.samples[center_obj_id] - self.rt_alignments[center_sample_name] = { - 'adjusted_times': center_scan_df["scan_time"].values, - 'alignment_function': None - } index_steps = (1, -1) # Run this twice, once going forward (+1 indexing) and once going backward (-1 indexing) @@ -2410,27 +2405,18 @@ def align_lcms_objects(self, overwrite=False): if use_spline_alignment: # Set new retention times on scan_df for lc_obj using the spline fitting matches_i["scan_time_fit"] = spl(matches_i["scan_time"]) - new_times = spl(self[i].scan_df["scan_time"]) - new_scan_info = self[i].scan_df.copy() - new_scan_info["scan_time_aligned"] = new_times - self[i].scan_df = new_scan_info + + # Add "scan_time_aligned" to LCMSObject's _scan_info dict + self[i]._scan_info["scan_time_aligned"] = {k: spl(v) for k, v in self[i]._scan_info["scan_time"].items()} + + # Retrieve the new aligned times for all scans in the LCMS object + new_times = [x for k, x in sorted(self[i]._scan_info["scan_time_aligned"].items())] - # Store alignment data - self.rt_alignments[sample_name] = { - 'adjusted_times': new_times.values, - } + # Switch the rt_aligned flag to True + self.rt_aligned = True else: # Set aligned retention times on scan_df for lc_obj using the original retention times - new_scan_info = self[i].scan_df.copy() - new_scan_info["scan_time_aligned"] = new_scan_info[ - "scan_time" - ] - self[i].scan_df = new_scan_info - - # Store alignment data (identity mapping) - self.rt_alignments[sample_name] = { - 'adjusted_times': new_scan_info["scan_time"].values, - } + self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"] i += index_step if i >= len(self) or i < 0: @@ -2472,11 +2458,6 @@ def align_lcms_objects(self, overwrite=False): new_scan_info["scan_time_aligned"] = new_times self[i].scan_df = new_scan_info - # Store alignment data - self.rt_alignments[sample_name] = { - 'adjusted_times': new_times.values, - 'alignment_function': spl - } # Get the batch that this object belongs to batch = self.manifest[self.samples[i]]["batch"] @@ -2493,12 +2474,6 @@ def align_lcms_objects(self, overwrite=False): new_scan_info["scan_time_aligned"] = aligned_times self[j].scan_df = new_scan_info - # Store alignment data - self.rt_alignments[sample_name_j] = { - 'adjusted_times': aligned_times.values, - 'alignment_function': spl - } - # Set final mass_features_dataframe with the aligned scan_time center_sample_name = self.samples[center_obj_ids[0]] self._manifest_dict[center_sample_name]["use_rt_alignment"] = False diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index adabc8365..923a41d78 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1456,7 +1456,7 @@ def __init__( self.isotopes_dropped = False # These attributes are set during processing - self.rt_alignments = {} + self.rt_aligned = False def _reorder_lcms_objects(self): """ @@ -1828,3 +1828,16 @@ def consensus_mass_feature_dataframe(self): def raw_files(self): """Returns a list of raw files in the collection.""" return [x.raw_file_location for x in self] + + @property + def rt_alignments(self): + """Returns a dictionary of retention time alignments for the collection.""" + if self.rt_aligned: + _rt_alignments = {} + # Construct a dictionary of aligned retention times (stored on each LCMS object within the collection, not the collection itself) + for i, lcms_obj in enumerate(self): + aligned_times = [x for k, x in sorted(lcms_obj._scan_info["scan_time_aligned"].items())] + _rt_alignments[i] = aligned_times + return _rt_alignments + else: + return None diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index edba2192d..45e2e6b94 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -1997,12 +1997,61 @@ def export_to_hdf5(self, overwrite = False): self.out_file_path.with_suffix(".hdf5").unlink() with h5py.File(self.out_file_path.with_suffix(".hdf5"), "a") as hdf_handle: - # Add basic attributes to the HDF5 file + # Add basic attributes to the HDF5 file, always overwrite these timenow = str( datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M:%S %Z") ) hdf_handle.attrs["date_utc"] = timenow hdf_handle.attrs["lcms_objects_folder"] = str(self.mass_spectra_collection.collection_parser.folder_location) - hdf_handle.attrs["manifest"] = json.dumps(self.mass_spectra_collection.collection_parser.manifest) - #TODO: save parameters \ No newline at end of file + # Add the manifest to the HDF5 file, always overwrite this + hdf_handle.attrs["manifest"] = self._convert_manifest_to_json() + + # Save retention time alignments if they exist, only overwrite if specified + self._save_rt_alignments_to_hdf5(hdf_handle, overwrite) + + #TODO KRH: save parameters + + + def _save_rt_alignments_to_hdf5(self, hdf_handle, overwrite): + """Save retention time alignments to HDF5 file.""" + if self.mass_spectra_collection.rt_aligned: + group_name = "rt_alignments" + # grab dictionary of rt_alignments + rt_alignments = self.mass_spectra_collection.rt_alignments + + if rt_alignments: + # Check if group exists and handle overwrite logic + if group_name in hdf_handle: + if not overwrite: + return + del hdf_handle[group_name] + + grp = hdf_handle.create_group(group_name) + + # Save each alignment as a dataset + for sample_idx, alignment_data in rt_alignments.items(): + grp.create_dataset(str(sample_idx), data=alignment_data) + + def _convert_manifest_to_json(self): + """Clean the manifest for export to HDF5.""" + manifest = self.mass_spectra_collection.collection_parser.manifest + + # Process the manifest to convert numpy.bool_ or bool values for the 'use_rt_alignment' key + def convert_bool_values(data): + if isinstance(data, dict): + # Process each key-value pair recursively + return {k: (int(v) if k == 'use_rt_alignment' and isinstance(v, (bool, np.bool_)) else convert_bool_values(v)) for k, v in data.items()} + elif isinstance(data, list): + # Recursively process lists + return [convert_bool_values(item) for item in data] + else: + # Return non-dict/list types unchanged + return data + + # Clean the whole manifest + cleaned_manifest = convert_bool_values(manifest) + + # Serialize the cleaned manifest into JSON format + json_manifest = json.dumps(cleaned_manifest) + return json_manifest \ No newline at end of file diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index cd0fbf1a4..b299c8c60 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -30,6 +30,28 @@ lcms_collection.drop_raw_data(sample_idx=0, ms_level=1) #10s for 7 samples, 10 cores; 162s for 70 samples, 10 cores + + # Set flag to call _drop_isotopologue() when running _check_mass_features_df() + lcms_collection.parameters.lcms_collection.drop_isotopologues = True + print("Number of total mass features: ", len(lcms_collection.mass_features_dataframe)) + + # Align the LCMS runs between each other + # For now, adjusting this parameter to force alignment to run quickly for testing + lcms_collection.parameters.lcms_collection.alignment_acceptance_fraction_improved_threshold = -1 + lcms_collection.parameters.lcms_collection.alignment_acceptance_technique = ['fraction_improved'] + print("Aligning LCMS collection") + start_time = time.time() + assert not lcms_collection.rt_aligned, "LCMS collection should not be marked as retention time aligned yet." + assert lcms_collection.rt_alignments is None, "LCMS collection should not have rt_alignments yet." + lcms_collection.align_lcms_objects() + assert lcms_collection.rt_aligned, "LCMS collection should be marked as retention time aligned." + print("Time to align LCMS collection: ", time.time() - start_time, "seconds") + + # Make some plots + lcms_collection.plot_tics(type="both") + lcms_collection.plot_alignments() + #1.5s for 7 samples; 15s for 70 samples + # Save the LCMS collection to a new location exporter = LCMSCollectionExporter( out_file_path="test_lcms_collection_out", @@ -47,21 +69,7 @@ assert lcms_collection2[0]._ms_unprocessed[1] is not None, "Raw data for MS1 should be loaded successfully." lcms_collection2.drop_raw_data(sample_idx=0, ms_level=1) del parser2, lcms_collection2 - - # Set flag to call _drop_isotopologue() when running _check_mass_features_df() - lcms_collection.parameters.lcms_collection.drop_isotopologues = True - print("Number of total mass features: ", len(lcms_collection.mass_features_dataframe)) - - # Align the LCMS runs between each other - print("Aligning LCMS collection") - start_time = time.time() - lcms_collection.align_lcms_objects() - print("Time to align LCMS collection: ", time.time() - start_time, "seconds") - #1.5s for 7 samples; 15s for 70 samples - - # Make some plots - lcms_collection.plot_tics(type="both") - lcms_collection.plot_alignments() + # TODO: Think about other plots that would be useful to have here for assessing the quality of the data and alignment # Make consensus mass features from the consolidated mass features From af6c278af461539826999d538e4772acee237796 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 16 Sep 2025 12:56:55 -0700 Subject: [PATCH 061/158] Add saving and loading of retention time to export/import classes for collection --- corems/mass_spectra/calc/lc_calc.py | 4 +- corems/mass_spectra/input/corems_hdf5.py | 53 +++++++++++++++++-- .../nmdc/lipidomics/lipidomics_collection.py | 7 ++- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index fed9cedee..103c0852d 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -2566,9 +2566,7 @@ def add_consensus_mass_features(self): except: mfs_with_clusters.set_index('coll_mf_id', inplace = True) self.mass_features_dataframe = mfs_with_clusters - - # TODO KRH: Deal with isomers better? Pool them together and then split them out using samples with 2 as the template? - + def summarize_clusters(self): """ Summarize the clusters of mass features by median attributes diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 7cf7267e4..148bbc2ce 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -867,7 +867,52 @@ def _load_collection_metadata(self): """Load metadata and manifest from the saved collection HDF5 file.""" with h5py.File(self.collection_hdf5_path, 'r') as f: self.folder_location = Path(f.attrs.get('lcms_objects_folder', '')) - manifest_json = f.attrs.get('manifest', '{}') - if isinstance(manifest_json, bytes): - manifest_json = manifest_json.decode('utf-8') - self._manifest_dict = json.loads(manifest_json) + + # Call the _load_manifest function to process the manifest + self._manifest_dict = self._load_manifest(f) + + def _load_manifest(self, hdf_handle): + """Load and clean the manifest from the HDF5 file.""" + manifest_json = hdf_handle.attrs.get('manifest', '{}') + if isinstance(manifest_json, bytes): + manifest_json = manifest_json.decode('utf-8') + loaded_manifest = json.loads(manifest_json) + + # Convert integer values for 'use_rt_alignment' back to booleans + def convert_back_to_bool(data): + if isinstance(data, dict): + # Process each key-value pair recursively + return {k: (bool(v) if k == 'use_rt_alignment' and isinstance(v, int) else convert_back_to_bool(v)) for k, v in data.items()} + elif isinstance(data, list): + # Recursively process lists + return [convert_back_to_bool(item) for item in data] + else: + # Return non-dict/list types unchanged + return data + + # Clean the loaded manifest + return convert_back_to_bool(loaded_manifest) + + def _load_rt_alignments(self, lcms_collection): + """Load retention time alignments from the saved collection HDF5 file.""" + with h5py.File(self.collection_hdf5_path, 'r') as f: + if "rt_alignments" in f: + # Set the lcms_collection + lcms_collection.rt_aligned = True + # Iterate over the group `rt_alignments` containing datasets and add to the corresponding lcms object + rt_alignments_group = f["rt_alignments"] + for sample_idx, lcms_obj in zip(rt_alignments_group.keys(), lcms_collection): + alignment_data = rt_alignments_group[sample_idx][:] + scan_df = lcms_obj.scan_df + scan_df["scan_time_aligned"] = alignment_data + lcms_obj.scan_df = scan_df + + def get_lcms_collection(self, load_raw=False, load_light=False): + """Get the LCMS collection from the saved HDF5 file.""" + # First load the LCMSCollection object exactly as in the parent class + lcms_collection = super().get_lcms_collection(load_raw=load_raw, load_light=load_light) + + # Add retention time alignments if they exist + self._load_rt_alignments(lcms_collection) + + return lcms_collection diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index b299c8c60..ae034b8f5 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -36,7 +36,7 @@ print("Number of total mass features: ", len(lcms_collection.mass_features_dataframe)) # Align the LCMS runs between each other - # For now, adjusting this parameter to force alignment to run quickly for testing + # For now, adjusting this parameter to force alignment for testing lcms_collection.parameters.lcms_collection.alignment_acceptance_fraction_improved_threshold = -1 lcms_collection.parameters.lcms_collection.alignment_acceptance_technique = ['fraction_improved'] print("Aligning LCMS collection") @@ -45,6 +45,7 @@ assert lcms_collection.rt_alignments is None, "LCMS collection should not have rt_alignments yet." lcms_collection.align_lcms_objects() assert lcms_collection.rt_aligned, "LCMS collection should be marked as retention time aligned." + assert lcms_collection.rt_alignments is not None, "LCMS collection should have rt_alignments now." print("Time to align LCMS collection: ", time.time() - start_time, "seconds") # Make some plots @@ -53,6 +54,7 @@ #1.5s for 7 samples; 15s for 70 samples # Save the LCMS collection to a new location + print("Saving LCMS collection to test_lcms_collection_out.hdf5") exporter = LCMSCollectionExporter( out_file_path="test_lcms_collection_out", mass_spectra_collection=lcms_collection @@ -60,11 +62,14 @@ exporter.export_to_hdf5(overwrite=True) # Reload the LCMS collection from the saved location and check that we can load raw data + print("Reloading LCMS collection from test_lcms_collection_out.hdf5 and checking functionality") parser2 = ReadSavedLCMSCollection( collection_hdf5_path=Path("test_lcms_collection_out.hdf5"), cores=ncores ) lcms_collection2 = parser2.get_lcms_collection(load_raw=False, load_light=True) + lcms_collection2.plot_alignments() + assert lcms_collection2.rt_aligned, "Reloaded LCMS collection should be marked as retention time aligned." lcms_collection2.load_raw_data(sample_idx=0, ms_level=1) assert lcms_collection2[0]._ms_unprocessed[1] is not None, "Raw data for MS1 should be loaded successfully." lcms_collection2.drop_raw_data(sample_idx=0, ms_level=1) From 0ec4dcf03101d7e2cb35c0ff503b1dc78ebfda84 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 16 Sep 2025 14:05:18 -0700 Subject: [PATCH 062/158] Add code for saving / loading cluster assignments --- corems/mass_spectra/calc/lc_calc.py | 12 ++-- corems/mass_spectra/input/corems_hdf5.py | 22 ++++++++ corems/mass_spectra/output/export.py | 29 +++++++++- .../nmdc/lipidomics/lipidomics_collection.py | 55 ++++++++++--------- 4 files changed, 84 insertions(+), 34 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 103c0852d..188843280 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -2715,8 +2715,8 @@ def plot_consensus_mz_features(self, xb = 'xb', xt = 'xt', yb = 'yb', yt = 'yt', fig = plt.figure() if show_all: plt.scatter( - df.scan_time_aligned, - df.mz, + df.scan_time_aligned_median, + df.mz_median, c = 'tab:gray', s = 1 ) @@ -2733,9 +2733,9 @@ def plot_consensus_mz_features(self, xb = 'xb', xt = 'xt', yb = 'yb', yt = 'yt', plt.ylabel('m/z') if xt == 'xt': - xt = np.ceil(np.max(df.mz)) + xt = np.ceil(np.max(df.mz_median)) if yt == 'yt': - yt = np.ceil(np.max(df.scan_time)) + yt = np.ceil(np.max(df.scan_time_aligned_median)) if xb == 'xb': xb = 0 if yb == 'yb': @@ -3040,9 +3040,9 @@ def cluster_inspection_plot(self, clu, return_fig = False): If cluster data haven't been added to the object yet """ - if not hasattr(self, 'mass_features_dataframe.cluster'): + if "cluster" not in self.mass_features_dataframe.columns: raise ValueError( - 'Cluster information is not yet added to mass_features_dataframe, must run add_consensus_mass_features() first' + 'Cluster information is not yet added to mass_features_dataframe, must run add_consensus_mass_features() first' ) else: diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 148bbc2ce..0edee238f 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -907,6 +907,25 @@ def _load_rt_alignments(self, lcms_collection): scan_df["scan_time_aligned"] = alignment_data lcms_obj.scan_df = scan_df + def _load_cluster_assignments(self, lcms_collection): + """Load cluster assignments from the saved collection HDF5 file.""" + with h5py.File(self.collection_hdf5_path, 'r') as f: + if "cluster_assignments" in f: + # Access the group containing cluster assignments + cluster_grp = f["cluster_assignments"] + + # Reload index and cluster data + index = cluster_grp["index"][:] # Extract index + index = [idx.decode('utf-8') for idx in index] # Convert byte strings back to regular strings + cluster_data = cluster_grp["cluster"][:] # Extract cluster column + + # Reassemble the DataFrame + cluster_df = pd.DataFrame({"cluster": cluster_data}, index=index) + + # Assign cluster data back to lcms_collection.mass_features_dataframe + lcms_collection.mass_features_dataframe = lcms_collection.mass_features_dataframe.join(cluster_df, how='left') + + def get_lcms_collection(self, load_raw=False, load_light=False): """Get the LCMS collection from the saved HDF5 file.""" # First load the LCMSCollection object exactly as in the parent class @@ -915,4 +934,7 @@ def get_lcms_collection(self, load_raw=False, load_light=False): # Add retention time alignments if they exist self._load_rt_alignments(lcms_collection) + # Add cluster assignments if they exist + self._load_cluster_assignments(lcms_collection) + return lcms_collection diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index 45e2e6b94..aafa18b1c 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -1990,8 +1990,6 @@ def __init__(self, out_file_path, mass_spectra_collection): def export_to_hdf5(self, overwrite = False): """Export the LCMS collection to an HDF5 file.""" - # TODO: Add hdf5 export to each of the mass spectra in the collection to ensure that scan time alignment, induced mass features, and mass features into clusters are retained - if overwrite: if self.out_file_path.with_suffix(".hdf5").exists(): self.out_file_path.with_suffix(".hdf5").unlink() @@ -2010,6 +2008,9 @@ def export_to_hdf5(self, overwrite = False): # Save retention time alignments if they exist, only overwrite if specified self._save_rt_alignments_to_hdf5(hdf_handle, overwrite) + # Save cluster assignments if they exist, only overwrite if specified + self._save_cluster_assignments_to_hdf5(hdf_handle, overwrite) + #TODO KRH: save parameters @@ -2054,4 +2055,26 @@ def convert_bool_values(data): # Serialize the cleaned manifest into JSON format json_manifest = json.dumps(cleaned_manifest) - return json_manifest \ No newline at end of file + return json_manifest + + def _save_cluster_assignments_to_hdf5(self, hdf_handle, overwrite): + """Save cluster assignments to HDF5 file.""" + # Check if column "cluster" is present in self.mass_features_dataframe + if "cluster" in self.mass_spectra_collection.mass_features_dataframe.columns: + group_name = "cluster_assignments" + cluster_assignments = self.mass_spectra_collection.mass_features_dataframe[["cluster"]].copy() + + # Check if group exists and handle overwrite logic + if group_name in hdf_handle: + if not overwrite: + return + del hdf_handle[group_name] + + grp = hdf_handle.create_group(group_name) + + # Save the index, converting strings to bytes + grp.create_dataset("index", data=cluster_assignments.index.astype(str).values.astype('S')) + + # Save the "cluster" column + grp.create_dataset("cluster", data=cluster_assignments["cluster"].values) + \ No newline at end of file diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index ae034b8f5..d17d4e559 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -52,6 +52,30 @@ lcms_collection.plot_tics(type="both") lcms_collection.plot_alignments() #1.5s for 7 samples; 15s for 70 samples + + + # Make consensus mass features from the consolidated mass features + start_time = time.time() + lcms_collection.add_consensus_mass_features() + # THIS FUNCTION NEEDS WORK AND NEEDS MECHANISM TO EVALUATE THE RESULTS (plots? reports?) + print("Time to roll up consensus mass features: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") + + lcms_collection.plot_mz_features_across_samples() + lcms_collection.plot_mz_features_per_cluster() + lcms_collection.plot_consensus_mz_features() ## zoomed out + lcms_collection.plot_consensus_mz_features(xb = 10, xt = 15, yb = 500, yt = 600) ## zoomed in + lcms_collection.cluster_inspection_plot(11391) + dim_list = [ + 'mz', + 'scan_time_aligned', + 'half_height_width', + 'tailing_factor', + 'dispersity_index', + 'intensity', + 'persistence' + ] + # BROKEN - needs fixing + #lcms_collection.plot_cluster_outlier_frequency(dim_list, clu_size_thresh = 0.25) # Save the LCMS collection to a new location print("Saving LCMS collection to test_lcms_collection_out.hdf5") @@ -69,35 +93,18 @@ ) lcms_collection2 = parser2.get_lcms_collection(load_raw=False, load_light=True) lcms_collection2.plot_alignments() + lcms_collection2.plot_consensus_mz_features() ## zoomed in + lcms_collection2.plot_consensus_mz_features(xb = 10, xt = 15, yb = 500, yt = 600) ## zoomed in + lcms_collection2.cluster_inspection_plot(11391) + #lcms_collection2.plot_cluster_outlier_frequency(dim_list, clu_size_thresh = 0.25) + assert "cluster" in lcms_collection2.mass_features_dataframe.columns, "Reloaded LCMS collection should have cluster assignments in mass_features_dataframe." assert lcms_collection2.rt_aligned, "Reloaded LCMS collection should be marked as retention time aligned." lcms_collection2.load_raw_data(sample_idx=0, ms_level=1) assert lcms_collection2[0]._ms_unprocessed[1] is not None, "Raw data for MS1 should be loaded successfully." lcms_collection2.drop_raw_data(sample_idx=0, ms_level=1) del parser2, lcms_collection2 - - # TODO: Think about other plots that would be useful to have here for assessing the quality of the data and alignment + print("Reloaded LCMS collection successfully and checked functionality, then deleted temporary objects.") - # Make consensus mass features from the consolidated mass features - start_time = time.time() - lcms_collection.add_consensus_mass_features() - # THIS FUNCTION NEEDS WORK AND NEEDS MECHANISM TO EVALUATE THE RESULTS (plots? reports?) - print("Time to roll up consensus mass features: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") - - lcms_collection.plot_mz_features_across_samples() - lcms_collection.plot_mz_features_per_cluster() - lcms_collection.plot_consensus_mz_features() ## zoomed out - lcms_collection.plot_consensus_mz_features(xb = 10, xt = 15, yb = 500, yt = 600) ## zoomed in - lcms_collection.cluster_inspection_plot(11391) - dim_list = [ - 'mz', - 'scan_time_aligned', - 'half_height_width', - 'tailing_factor', - 'dispersity_index', - 'intensity', - 'persistence' - ] - lcms_collection.plot_cluster_outlier_frequency(dim_list, clu_size_thresh = 0.25) ## WORK IN PROGRESS: temporary code for testing ## want to adjust function to iterate throught samples by index, not name @@ -106,6 +113,4 @@ lcms_collection.search_for_missing_mass_features_in_one_sample(samplename) print(lcms_collection._lcms[samplename].mass_features_to_df(induced_features = True)) - #TODO: Add code to load and save information about chromatographic settings - #TODO: Add code to save and load collection to HDF5 file #TODO: Add code to plot a consensus mass feature \ No newline at end of file From 2e567a632ed9777f2b375fbddcffc83e1c5014be Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 17 Sep 2025 13:28:32 -0700 Subject: [PATCH 063/158] Refactor to_hdf function for LCMSExport class to deal with different types of mass features --- corems/mass_spectra/input/corems_hdf5.py | 1 - corems/mass_spectra/output/export.py | 243 +++++++++++++---------- 2 files changed, 138 insertions(+), 106 deletions(-) diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 0edee238f..5cd3a1479 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -925,7 +925,6 @@ def _load_cluster_assignments(self, lcms_collection): # Assign cluster data back to lcms_collection.mass_features_dataframe lcms_collection.mass_features_dataframe = lcms_collection.mass_features_dataframe.join(cluster_df, how='left') - def get_lcms_collection(self, load_raw=False, load_light=False): """Get the LCMS collection from the saved HDF5 file.""" # First load the LCMSCollection object exactly as in the parent class diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index aafa18b1c..b16547627 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -1021,63 +1021,31 @@ class LCMSExport(HighResMassSpectraExport): def __init__(self, out_file_path, mass_spectra): super().__init__(out_file_path, mass_spectra, output_type="hdf5") - def to_hdf(self, overwrite=False, save_parameters=True, parameter_format="toml"): - """Export the data to an HDF5. + def _save_mass_features_to_hdf5(self, hdf_handle, group_name = "mass_features", overwrite=False): + """Save the mass features to the HDF5 file. Parameters ---------- + hdf_handle : h5py.File + The HDF5 file handle. + group_name : str, optional + The name of the group to save the mass features to. Default is 'mass_features'. overwrite : bool, optional - Whether to overwrite the output file. Default is False. - save_parameters : bool, optional - Whether to save the parameters as a separate json or toml file. Default is True. - parameter_format : str, optional - The format to save the parameters in. Default is 'toml'. - - Raises - ------ - ValueError - If parameter_format is not 'json' or 'toml'. + Whether to overwrite the existing group. Default is False, where existing group will be updated. """ - export_profile_spectra = ( - self.mass_spectra.parameters.lc_ms.export_profile_spectra - ) - - # Write the mass spectra data to the hdf5 file - super().to_hdf(overwrite=overwrite, export_raw=export_profile_spectra) - - # Write scan info, ms_unprocessed, mass features, eics, and ms2_search results to the hdf5 file - with h5py.File(self.output_file.with_suffix(".hdf5"), "a") as hdf_handle: - # Add scan_info to hdf5 file - if "scan_info" not in hdf_handle: - scan_info_group = hdf_handle.create_group("scan_info") - for k, v in self.mass_spectra._scan_info.items(): - array = np.array(list(v.values())) - if array.dtype.str[0:2] == " 0: - if "mass_features" not in hdf_handle: - mass_features_group = hdf_handle.create_group("mass_features") - else: - mass_features_group = hdf_handle.get("mass_features") + # Create group for each mass feature, with key as the mass feature id + if len(self.mass_spectra.mass_features) > 0: + if group_name not in hdf_handle: + mass_features_group = hdf_handle.create_group(group_name) + else: + mass_features_group = hdf_handle.get(group_name) - # Create group for each mass feature, with key as the mass feature id - for k, v in self.mass_spectra.mass_features.items(): + # Create group for each mass feature, with key as the mass feature id + for k, v in self.mass_spectra.mass_features.items(): + if str(k) not in mass_features_group or overwrite: + if str(k) in mass_features_group and overwrite: + del mass_features_group[str(k)] mass_features_group.create_group(str(k)) # Loop through each of the mass feature attributes and add them as attributes (if single value) or datasets (if array) for k2, v2 in v.__dict__.items(): @@ -1118,32 +1086,94 @@ def to_hdf(self, overwrite=False, save_parameters=True, parameter_format="toml") or isinstance(v2, np.bool_) ): mass_features_group[str(k)].attrs[str(k2)] = v2 - else: - raise TypeError( - f"Attribute {k2} is not an integer, float, or string and cannot be added to the hdf5 file" - ) + + def to_hdf(self, overwrite=False, save_parameters=True, parameter_format="toml"): + """Export the data to an HDF5. + + Parameters + ---------- + overwrite : bool, optional + Whether to overwrite the output file. Default is False. + save_parameters : bool, optional + Whether to save the parameters as a separate json or toml file. Default is True. + parameter_format : str, optional + The format to save the parameters in. Default is 'toml'. + + Raises + ------ + ValueError + If parameter_format is not 'json' or 'toml'. + """ + export_profile_spectra = ( + self.mass_spectra.parameters.lc_ms.export_profile_spectra + ) + + # Write the mass spectra data to the hdf5 file + super().to_hdf(overwrite=overwrite, export_raw=export_profile_spectra) + + # Write scan info, ms_unprocessed, mass features, eics, and ms2_search results to the hdf5 file + with h5py.File(self.output_file.with_suffix(".hdf5"), "a") as hdf_handle: + # Add scan_info to hdf5 file + if "scan_info" not in hdf_handle or overwrite: + if "scan_info" in hdf_handle and overwrite: + del hdf_handle["scan_info"] + scan_info_group = hdf_handle.create_group("scan_info") + for k, v in self.mass_spectra._scan_info.items(): + array = np.array(list(v.values())) + if array.dtype.str[0:2] == " 0 and export_eics: - if "eics" not in hdf_handle: + if "eics" not in hdf_handle or overwrite: + if "eics" in hdf_handle and overwrite: + del hdf_handle["eics"] eic_group = hdf_handle.create_group("eics") else: eic_group = hdf_handle.get("eics") # Create group for each eic for k, v in self.mass_spectra.eics.items(): - eic_group.create_group(str(k)) - eic_group[str(k)].attrs["mz"] = k - # Loop through each of the attributes and add them as datasets (if array) - for k2, v2 in v.__dict__.items(): - if v2 is not None: - array = np.array(v2) - eic_group[str(k)].create_dataset(str(k2), data=array) + if str(k) not in eic_group or overwrite: + if str(k) in eic_group and overwrite: + del eic_group[str(k)] + eic_group.create_group(str(k)) + eic_group[str(k)].attrs["mz"] = k + # Loop through each of the attributes and add them as datasets (if array) + for k2, v2 in v.__dict__.items(): + if v2 is not None: + array = np.array(v2) + eic_group[str(k)].create_dataset(str(k2), data=array) # Add ms2_search results to hdf5 file if len(self.mass_spectra.spectral_search_results) > 0: - if "spectral_search_results" not in hdf_handle: + if "spectral_search_results" not in hdf_handle or overwrite: + if "spectral_search_results" in hdf_handle and overwrite: + del hdf_handle["spectral_search_results"] spectral_search_results = hdf_handle.create_group( "spectral_search_results" ) @@ -1151,48 +1181,51 @@ def to_hdf(self, overwrite=False, save_parameters=True, parameter_format="toml") spectral_search_results = hdf_handle.get("spectral_search_results") # Create group for each search result by ms2_scan / precursor_mz for k, v in self.mass_spectra.spectral_search_results.items(): - spectral_search_results.create_group(str(k)) - for k2, v2 in v.items(): - spectral_search_results[str(k)].create_group(str(k2)) - spectral_search_results[str(k)][str(k2)].attrs[ - "precursor_mz" - ] = v2.precursor_mz - spectral_search_results[str(k)][str(k2)].attrs[ - "query_spectrum_id" - ] = v2.query_spectrum_id - # Loop through each of the attributes and add them as datasets (if array) - for k3, v3 in v2.__dict__.items(): - if v3 is not None and k3 not in [ - "query_spectrum", - "precursor_mz", - "query_spectrum_id", - ]: - if k3 == "query_frag_types" or k3 == "ref_frag_types": - v3 = [", ".join(x) for x in v3] - if all(v3 is not None for v3 in v3): - array = np.array(v3) - if array.dtype.str[0:2] == " Date: Wed, 17 Sep 2025 14:16:17 -0700 Subject: [PATCH 064/158] Add to test for lcms_metabolomics for import / export --- tests/test_lcms_metabolomics.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_lcms_metabolomics.py b/tests/test_lcms_metabolomics.py index 06195c0c1..61b5326b8 100644 --- a/tests/test_lcms_metabolomics.py +++ b/tests/test_lcms_metabolomics.py @@ -3,6 +3,7 @@ import numpy as np from corems.mass_spectra.output.export import LCMSMetabolomicsExport +from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectra from corems.molecular_id.search.database_interfaces import MSPInterface from corems.encapsulation.factory.parameters import LCMSParameters, reset_lcms_parameters, reset_ms_parameters @@ -102,6 +103,25 @@ def test_lcms_metabolomics(postgres_database, lcms_obj, msp_file_location): report = exporter.to_report(molecular_metadata=metabolite_metadata_negative) assert report['Ion Formula'][1] == 'C24 H47 O2' assert report['chebi'][1] == 28866 + + # Reload the saved lcms object and check that mass features are still present + parser = ReadCoreMSHDFMassSpectra( + "Blanch_Nat_Lip_C_12_AB_M_17_NEG_25Jan18_Brandi-WCSH5801_metab.corems/Blanch_Nat_Lip_C_12_AB_M_17_NEG_25Jan18_Brandi-WCSH5801_metab.hdf5" + ) + myLCMSobj2 = parser.get_lcms_obj() + + # Check that the parameters match + assert myLCMSobj2.parameters == lcms_obj.parameters + + # Check that the spectra parser class is the same as the original parser and that we can plot a mass spectrum using the original parser + assert myLCMSobj2.spectra_parser_class.__name__ == "ImportMassSpectraThermoMSFileReader" + myLCMSobj2.spectra_parser.get_mass_spectrum_from_scan(1, spectrum_mode="profile").plot_centroid() + + # Check that the mass features dataframe is the same as the original + df2 = myLCMSobj2.mass_features_to_df() + df1 = lcms_obj.mass_features_to_df() + assert df2.shape == df1.shape == (130, 13) + myLCMSobj2.mass_features[0].plot(return_fig=False) # Delete the "Blanch_Nat_Lip_C_12_AB_M_17_NEG_25Jan18_Brandi-WCSH5801.corems" directory shutil.rmtree( From 0ac073f3a44b1d54e08f01a25ff5f66877f961d5 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 18 Sep 2025 13:45:17 -0700 Subject: [PATCH 065/158] WIP exporter for collection --- corems/mass_spectra/factory/lc_class.py | 1 + corems/mass_spectra/output/export.py | 11 +++++- .../nmdc/lipidomics/lipidomics_collection.py | 36 ++++++++++--------- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 923a41d78..9900c96fc 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1457,6 +1457,7 @@ def __init__( # These attributes are set during processing self.rt_aligned = False + self.missing_mass_features_searched = False def _reorder_lcms_objects(self): """ diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index b16547627..9f0d06f10 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -2034,6 +2034,7 @@ def export_to_hdf5(self, overwrite = False): ) hdf_handle.attrs["date_utc"] = timenow hdf_handle.attrs["lcms_objects_folder"] = str(self.mass_spectra_collection.collection_parser.folder_location) + hdf_handle.attrs["missing_mass_features_searched"] = self.mass_spectra_collection.missing_mass_features_searched # Add the manifest to the HDF5 file, always overwrite this hdf_handle.attrs["manifest"] = self._convert_manifest_to_json() @@ -2044,6 +2045,10 @@ def export_to_hdf5(self, overwrite = False): # Save cluster assignments if they exist, only overwrite if specified self._save_cluster_assignments_to_hdf5(hdf_handle, overwrite) + # Save induced mass features onto the LCMSBase objects, only if lcms_collection.missing_mass_features_searched is True + if self.mass_spectra_collection.missing_mass_features_searched: + self._save_induced_mass_features_to_hdf5(overwrite) + #TODO KRH: save parameters @@ -2110,4 +2115,8 @@ def _save_cluster_assignments_to_hdf5(self, hdf_handle, overwrite): # Save the "cluster" column grp.create_dataset("cluster", data=cluster_assignments["cluster"].values) - \ No newline at end of file + + def _save_induced_mass_features_to_hdf5(self, hdf_handle, overwrite): + """Save induced mass features onto each of the LCMSBase objects in the collection to HDF5 file.""" + for lcms_obj in self.mass_spectra_collection: + print("here") \ No newline at end of file diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index d17d4e559..76b30eb7f 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -60,11 +60,11 @@ # THIS FUNCTION NEEDS WORK AND NEEDS MECHANISM TO EVALUATE THE RESULTS (plots? reports?) print("Time to roll up consensus mass features: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") - lcms_collection.plot_mz_features_across_samples() - lcms_collection.plot_mz_features_per_cluster() - lcms_collection.plot_consensus_mz_features() ## zoomed out - lcms_collection.plot_consensus_mz_features(xb = 10, xt = 15, yb = 500, yt = 600) ## zoomed in - lcms_collection.cluster_inspection_plot(11391) + #lcms_collection.plot_mz_features_across_samples() + #lcms_collection.plot_mz_features_per_cluster() + #lcms_collection.plot_consensus_mz_features() ## zoomed out + #lcms_collection.plot_consensus_mz_features(xb = 10, xt = 15, yb = 500, yt = 600) ## zoomed in + #lcms_collection.cluster_inspection_plot(11391) dim_list = [ 'mz', 'scan_time_aligned', @@ -77,6 +77,20 @@ # BROKEN - needs fixing #lcms_collection.plot_cluster_outlier_frequency(dim_list, clu_size_thresh = 0.25) + #TODO KRH: Add code for saving/loading induced mass features within each LCMS object, + # and a helper function on the collection for after searching for induced mass features in one or more samples + + ## WORK IN PROGRESS: temporary code for testing + ## want to adjust function to iterate throught samples by index, not name + ## want to be able to do that in parallel/multiprocess + #samplename = 'Blanch_Nat_Lip_H_11_AB_M_13_POS_23Jan18_Brandi-WCSH5801' + samplename = 'Blanch_Nat_Lip_C_14_AB_O_09_POS_23Jan18_Brandi-WCSH5801' #Katherine's working sample :) + print(f"Searching for missing mass features in sample {samplename}") + lcms_collection.search_for_missing_mass_features_in_one_sample(samplename) + lcms_collection.missing_mass_features_searched = True #TODO toggle this automatically when you run the function to backfill missing mass features + print(lcms_collection._lcms[samplename].mass_features_to_df(induced_features = True)) + + #TODO: Add code to plot a consensus mass feature EIC # Save the LCMS collection to a new location print("Saving LCMS collection to test_lcms_collection_out.hdf5") exporter = LCMSCollectionExporter( @@ -103,14 +117,4 @@ assert lcms_collection2[0]._ms_unprocessed[1] is not None, "Raw data for MS1 should be loaded successfully." lcms_collection2.drop_raw_data(sample_idx=0, ms_level=1) del parser2, lcms_collection2 - print("Reloaded LCMS collection successfully and checked functionality, then deleted temporary objects.") - - - ## WORK IN PROGRESS: temporary code for testing - ## want to adjust function to iterate throught samples by index, not name - ## want to be able to do that in parallel/multiprocess - samplename = 'Blanch_Nat_Lip_H_11_AB_M_13_POS_23Jan18_Brandi-WCSH5801' - lcms_collection.search_for_missing_mass_features_in_one_sample(samplename) - print(lcms_collection._lcms[samplename].mass_features_to_df(induced_features = True)) - - #TODO: Add code to plot a consensus mass feature \ No newline at end of file + print("Reloaded LCMS collection successfully and checked functionality, then deleted temporary objects.") \ No newline at end of file From fa44efc13222e0cf33b7da8d5c24dacac005a9b7 Mon Sep 17 00:00:00 2001 From: "danielle.ciesielski@pnnl.gov" Date: Thu, 2 Oct 2025 12:07:37 -0700 Subject: [PATCH 066/158] initiating new branch for mr --- support_code/nmdc/lipidomics/lipidomics_collection.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 38f7fafb8..010a009fd 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -69,13 +69,10 @@ ] lcms_collection.plot_cluster_outlier_frequency(dim_list, clu_size_thresh = 0.25) - ## WORK IN PROGRESS: temporary code for testing - ## want to adjust function to iterate throught samples by index, not name - ## want to be able to do that in parallel/multiprocess - samplename = 'Blanch_Nat_Lip_H_11_AB_M_13_POS_23Jan18_Brandi-WCSH5801' - lcms_collection.search_for_missing_mass_features_in_one_sample(samplename) - print(lcms_collection._lcms[samplename].mass_features_to_df(induced_features = True)) + lcms_collection.search_for_missing_mass_features_in_collection() + print('Sample output:') + print(lcms_collection[0].mass_features_to_df(induced_features = True)) #TODO: Add code to load and save information about chromatographic settings #TODO: Add code to save and load collection to HDF5 file - #TODO: Add code to plot a consensus mass feature \ No newline at end of file + #TODO: Generate pivot tables to collect regular and induced mass features \ No newline at end of file From 364f8d6c0f125e20335f9d34f37b9c7a70559846 Mon Sep 17 00:00:00 2001 From: "danielle.ciesielski@pnnl.gov" Date: Thu, 2 Oct 2025 12:47:57 -0700 Subject: [PATCH 067/158] initiating new branch for mr --- support_code/nmdc/lipidomics/lipidomics_collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 010a009fd..c9d8d5408 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -75,4 +75,4 @@ #TODO: Add code to load and save information about chromatographic settings #TODO: Add code to save and load collection to HDF5 file - #TODO: Generate pivot tables to collect regular and induced mass features \ No newline at end of file + #TODO: Generate report of summarize_clusters as a table with both regular and induced mass features \ No newline at end of file From 6644b4d050cf2605640ab08554daa8fd4eb135bc Mon Sep 17 00:00:00 2001 From: Nellie Ciesielski Date: Thu, 2 Oct 2025 22:26:13 +0000 Subject: [PATCH 068/158] Allow induced feature search to run in parallel --- corems/mass_spectra/calc/lc_calc.py | 188 ++++++++++++------ .../nmdc/lipidomics/lipidomics_collection.py | 9 +- 2 files changed, 126 insertions(+), 71 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 9a4c26068..9b55a40c0 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -1,8 +1,7 @@ import numpy as np import pandas as pd -import warnings +import warnings, scipy, multiprocessing from ripser import ripser -import scipy from scipy import sparse from scipy.spatial import KDTree from sklearn.svm import SVR @@ -2706,12 +2705,13 @@ def plot_consensus_mz_features(self, xb = 'xb', xt = 'xt', yb = 'yb', yt = 'yt', inputting desired boundaries on the scan time (xb, xt) and m/z values (yb, yt). """ df = self.cluster_summary_dataframe.copy() + mfdf = self.mass_features_dataframe.copy() fig = plt.figure() if show_all: plt.scatter( - df.scan_time_aligned, - df.mz, + mfdf.scan_time_aligned, + mfdf.mz, c = 'tab:gray', s = 1 ) @@ -2728,9 +2728,9 @@ def plot_consensus_mz_features(self, xb = 'xb', xt = 'xt', yb = 'yb', yt = 'yt', plt.ylabel('m/z') if xt == 'xt': - xt = np.ceil(np.max(df.mz)) + xt = np.ceil(np.max(mfdf.mz)) if yt == 'yt': - yt = np.ceil(np.max(df.scan_time)) + yt = np.ceil(np.max(mfdf.scan_time)) if xb == 'xb': xb = 0 if yb == 'yb': @@ -3157,10 +3157,10 @@ def plot_cluster_outlier_frequency(self, dim_list = ['mz', 'scan_time_aligned'], ) mfdf = self.mass_features_dataframe.copy() - sumdf = self.cluster_summary_dataframe.copy() + summarydf = self.cluster_summary_dataframe - numsamples = mfdf.sample_id.max() + 1 - sumdf = sumdf[sumdf.sample_id_nunique > numsamples * clu_size_thresh] + numsamples = len(self) + sumdf = summarydf[summarydf.sample_id_nunique > numsamples * clu_size_thresh].reset_index(drop = True).copy() ## find the ranges for non-outlier values and add them to sumdf mergelist = ['cluster'] @@ -3179,7 +3179,8 @@ def plot_cluster_outlier_frequency(self, dim_list = ['mz', 'scan_time_aligned'], ## dimension therefore can't have outliers ## add ranges to mfdf and identify mass features that fall outside the ranges - outdf = pd.merge(mfdf, sumdf[mergelist], on = 'cluster') + outdf = pd.merge(mfdf, sumdf[mergelist].dropna(), on = 'cluster') + outtags = ['cluster'] for dim in dim_list: dimtag = dim + '_outlier' @@ -3206,55 +3207,43 @@ def plot_cluster_outlier_frequency(self, dim_list = ['mz', 'scan_time_aligned'], else: plt.show() - def search_for_missing_mass_features_in_one_sample(self, samplename, threshold = 0.5, tol_flag = 0): - ''' - Work in progress temporary code - threshold default to 0.5 --> - only consider clusters that contain at least 50% of the sample - tol_flag default to 0 --> - don't check for possible mass features on the edges of the cluster - for sampleindex == 7, tol_flag == 1 picks up 6 more mass features - ''' - summarydf = self.cluster_summary_dataframe - mfdf = self.mass_features_dataframe - sampleindex = self.samples.index(samplename) - self.load_raw_data(sampleindex, 1) + def _search_for_targeted_mass_features_in_sample(self, obj_idx, missingdf, threshold = 0.5, tol_flag = 0, inplace = True): + """ + Searches through a sample in a collection to find all missing mass + features across all clusters in the collection. - sample_ct = len(self.samples) - missingdf = summarydf[[ - 'cluster', - 'sample_id_nunique', - 'mz_min', - 'mz_max', - 'scan_time_aligned_min', - 'scan_time_aligned_max' - ]] - missingdf = missingdf[missingdf.sample_id_nunique > threshold*(sample_ct)] - missingdf = missingdf[missingdf.sample_id_nunique != sample_ct] - - missingdf['missing_samples'] = None - for c in missingdf.cluster.to_numpy(): - cludf = mfdf[mfdf.cluster == c] - missingdf.loc[c, 'missing_samples'] = str( - [x for x in mfdf.sample_name.unique() if x not in cludf.sample_name.unique()] - ) - missingdf['missing_samples'] = missingdf.missing_samples.apply(literal_eval) - - ## to get clusters missing data based on sample name: + Parameters + ----------- + obj_idx : int + Index of the sample being processed + missingdf : pandas DataFrame + DataFrame containing information about the clusters with columns: + 'cluster', 'sample_id_nunique', 'mz_min', 'mz_max', + 'scan_time_aligned_min', 'scan_time_aligned_max' extracted from + self.cluster_summary_dataframe and 'mz_min_allowed', + 'mz_max_allowed', 'scan_time_aligned_min_allowed', + 'scan_time_aligned_max_allowed' computed in + search_for_targeted_mass_features_in_collection + threshold : float + Decimal representation of percent of sample included in a cluster + before the remaining samples are searched + tol_flag : 0 or 1 + Indicates whether expand cluster boundaries to search for mass + features near the boundary if none are found in the given + parameters. Defaults to 0 (False). + inplace : boolean + Indicates whethere to assign induced_mass_features attribute in + place or to return the result when the function is run in parallel + """ + + ## to get clusters missing data based on sample index: sampledf = missingdf[ - missingdf.missing_samples.apply(lambda x: samplename in x) + missingdf.missing_samples.apply(lambda x: obj_idx in x) ].reset_index(drop = True).copy() - - if tol_flag == 1: - mz_clu_tol = self.parameters.lcms_collection.consensus_mz_tol_ppm * 1e-6 - rt_clu_tol = self.parameters.lcms_collection.consensus_rt_tol - sampledf['mz_max_allowed'] = sampledf.mz_max + mz_clu_tol*sampledf.mz_max - sampledf['mz_min_allowed'] = sampledf.mz_min - mz_clu_tol*sampledf.mz_min - sampledf['sta_max_allowed'] = sampledf.scan_time_aligned_max + rt_clu_tol*sampledf.scan_time_aligned_max - sampledf['sta_min_allowed'] = sampledf.scan_time_aligned_min - rt_clu_tol*sampledf.scan_time_aligned_min - - ms1df = self[sampleindex]._ms_unprocessed[1].copy() - scan_df = self[sampleindex].scan_df[['scan', 'scan_time_aligned']] + + self.load_raw_data(obj_idx, 1) + ms1df = self[obj_idx]._ms_unprocessed[1].copy() + scan_df = self[obj_idx].scan_df[['scan', 'scan_time_aligned']] ms1df = pd.merge(ms1df, scan_df, on = 'scan') for i in range(len(sampledf)): @@ -3262,14 +3251,16 @@ def search_for_missing_mass_features_in_one_sample(self, samplename, threshold = mz_max = sampledf.mz_max.iloc[i] st_min = sampledf.scan_time_aligned_min.iloc[i] st_max = sampledf.scan_time_aligned_max.iloc[i] - found_feature = self[sampleindex].search_for_targeted_mass_feature( + # TODO: Adjust function to input queries as tuples so we can search + # for all targeted mass features at once instead of in a loop + found_feature = self[obj_idx].search_for_targeted_mass_feature( ms1df, mz_min, mz_max, st_min, st_max, set_id = 'c' + str(sampledf.cluster.iloc[i]) + '_' + str(i) + '_i', - obj_idx = sampleindex, + obj_idx = obj_idx, st_aligned = True ) @@ -3279,20 +3270,87 @@ def search_for_missing_mass_features_in_one_sample(self, samplename, threshold = st_min = sampledf.sta_min_allowed.iloc[i] st_max = sampledf.sta_max_allowed.iloc[i] - found_feature = self[sampleindex].search_for_targeted_mass_feature( + found_feature = self[obj_idx].search_for_targeted_mass_feature( ms1df, mz_min, mz_max, st_min, st_max, set_id = 'c' + str(sampledf.cluster.iloc[i]) + '_' + str(i) + '_i', - obj_idx = sampleindex, + obj_idx = obj_idx, st_aligned = True ) - - self[sampleindex].induced_mass_features[found_feature.id] = found_feature - self[sampleindex].add_associated_ms1(induced_features = True) + self[obj_idx].induced_mass_features[found_feature.id] = found_feature + + self[obj_idx].add_associated_ms1(induced_features = True) # need to set drop_if_fail to false for induced features as they will fail - self[sampleindex].integrate_mass_features(drop_if_fail = False, induced_features = True) - self[sampleindex].add_peak_metrics(induced_features = True) + self[obj_idx].integrate_mass_features(drop_if_fail = False, induced_features = True) + self[obj_idx].add_peak_metrics(induced_features = True) + + if inplace == False: + return self[obj_idx].induced_mass_features + + def search_for_targeted_mass_features_in_collection(self, threshold = 0.5, tol_flag = 0): + ''' + Iterates through a collection to find all relevant induced mass + features and assigns them to the induced_mass_features attribute + + Parameters + ----------- + threshold : float + Decimal representation of percent of sample included in a cluster + before the remaining samples are searched + tol_flag : 0 or 1 + Indicates whether expand cluster boundaries to search for mass + features near the boundary if none are found in the given + parameters. Defaults to 0 (False). + ''' + + summarydf = self.cluster_summary_dataframe + mfdf = self.mass_features_dataframe + + sample_ct = len(self.samples) + missingdf = summarydf[[ + 'cluster', + 'sample_id_nunique', + 'mz_min', + 'mz_max', + 'scan_time_aligned_min', + 'scan_time_aligned_max' + ]] + missingdf = missingdf[missingdf.sample_id_nunique > threshold*(sample_ct)] + missingdf = missingdf[missingdf.sample_id_nunique != sample_ct] + + missingdf['missing_samples'] = None + for c in missingdf.cluster.to_numpy(): + cludf = mfdf[mfdf.cluster == c] + missingdf.loc[c, 'missing_samples'] = str( + [x for x in mfdf.sample_id.unique() if x not in cludf.sample_id.unique()] + ) + missingdf['missing_samples'] = missingdf.missing_samples.apply(literal_eval) + + mz_clu_tol = self.parameters.lcms_collection.consensus_mz_tol_ppm * 1e-6 + rt_clu_tol = self.parameters.lcms_collection.consensus_rt_tol + missingdf['mz_max_allowed'] = missingdf.mz_max + mz_clu_tol*missingdf.mz_max + missingdf['mz_min_allowed'] = missingdf.mz_min - mz_clu_tol*missingdf.mz_min + missingdf['sta_max_allowed'] = missingdf.scan_time_aligned_max + rt_clu_tol*missingdf.scan_time_aligned_max + missingdf['sta_min_allowed'] = missingdf.scan_time_aligned_min - rt_clu_tol*missingdf.scan_time_aligned_min + + if self.parameters.lcms_collection.cores == 1: + for i in range(sample_ct): + self[i]._search_for_targeted_mass_features_in_sample(i, missingdf, threshold, tol_flag) + + if self.parameters.lcms_collection.cores > 1: + if self.parameters.lcms_collection.cores > len(self): + ncores = len(self) + else: + ncores = self.parameters.lcms_collection.cores + pool = multiprocessing.Pool(ncores) + mp_result = pool.starmap( + self._search_for_targeted_mass_features_in_sample, + [(x, missingdf, threshold, tol_flag, False) for x in range(sample_ct)] + ) + + for i in range(sample_ct): + self[i].induced_mass_features = mp_result[i] \ No newline at end of file diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 38f7fafb8..3e28e8cd4 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -69,12 +69,9 @@ ] lcms_collection.plot_cluster_outlier_frequency(dim_list, clu_size_thresh = 0.25) - ## WORK IN PROGRESS: temporary code for testing - ## want to adjust function to iterate throught samples by index, not name - ## want to be able to do that in parallel/multiprocess - samplename = 'Blanch_Nat_Lip_H_11_AB_M_13_POS_23Jan18_Brandi-WCSH5801' - lcms_collection.search_for_missing_mass_features_in_one_sample(samplename) - print(lcms_collection._lcms[samplename].mass_features_to_df(induced_features = True)) + lcms_collection.search_for_missing_mass_features_in_collection() + print('Sample output:') + print(lcms_collection[0].mass_features_to_df(induced_features = True)) #TODO: Add code to load and save information about chromatographic settings #TODO: Add code to save and load collection to HDF5 file From dbcb6cc29865862df30afeb16a40a128cf73e851 Mon Sep 17 00:00:00 2001 From: "danielle.ciesielski@pnnl.gov" Date: Mon, 6 Oct 2025 15:20:03 -0700 Subject: [PATCH 069/158] first pass at pivot tables --- corems/mass_spectra/factory/lc_class.py | 41 +++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index a57119a84..bf26ce020 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -772,6 +772,12 @@ def mass_spectrum_to_string( mf_dict = self.induced_mass_features else: mf_dict = self.mass_features + + if len(mf_dict) == 0: + # Warn that no mass features were found, quit function + raise ValueError( + "No mass features found in dataset. Have the mass features been added? If this is part of a collection, summary data is aggregated in the attribute 'mass_features_dataframe'" + ) cols_in_df = [ "id", @@ -1748,7 +1754,42 @@ def drop_raw_data(self, sample_idx: int, ms_level = 1) -> None: # Drop the raw data del self[sample_idx]._ms_unprocessed[ms_level] + def collection_pivot_table(self, attribute = 'coll_mf_id'): + """Generate a pivot table of all regular and induced mass features in + a collection. Default attribute presented is the mass feature ID, also + prints a list of other available attributes. + + Parameters + ----------- + attribute : str + The desired attribute to be presented in the pivot table. Defaults + to mass feature ID + Returns + -------- + pd.DataFrame + A DataFrame that displays one given attribute across all clusters + and samples in a collection + + """ + mf_pivot = self.mass_features_dataframe.copy() + mf_pivot.reset_index(inplace = True) + + for i in range(mf_pivot.sample_id.unique().max() + 1): + imf_pivot = self[i].mass_features_to_df(induced_features = True).copy() + imf_pivot.reset_index(inplace = True) + imf_pivot['sample_id'] = i + imf_pivot['cluster'] = np.nan + imf_pivot.rename(columns = {'mf_id': 'coll_mf_id'}, inplace = True) + for j in range(len(imf_pivot)): + imf_pivot.loc[j, 'cluster'] = imf_pivot.loc[j].coll_mf_id.split('_')[0][1:] + mf_pivot = pd.concat([mf_pivot, imf_pivot], axis = 0) + mf_pivot.reset_index(drop = True, inplace = True) + mf_pivot.cluster = mf_pivot.cluster.astype(int) + available_attributes = [x for x in mf_pivot.columns if x not in ['cluster', 'sample_id']] + print('Attributes available for pivot table:', available_attributes) + return mf_pivot.pivot(index = 'cluster', columns = 'sample_id', values = attribute) + @property def parameters(self): """ From b4b79cf65508d317b2391fe87800a266007df6b7 Mon Sep 17 00:00:00 2001 From: "danielle.ciesielski@pnnl.gov" Date: Mon, 6 Oct 2025 15:29:11 -0700 Subject: [PATCH 070/158] update docstring for mass_features_to_df --- corems/mass_spectra/factory/lc_class.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index bf26ce020..73d4e5d93 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -729,6 +729,11 @@ def mass_features_to_df(self, induced_features = False): induced_features : bool, optional If True, calls the induced_mass_features dictionary. Defaults to False. + Raises + -------- + ValueError + If the sample provided doesn't contain the mass feature data. + Returns -------- pandas.DataFrame From 5e6709078ef5e7683c6a8ee50dcd0aff1558827e Mon Sep 17 00:00:00 2001 From: "danielle.ciesielski@pnnl.gov" Date: Fri, 10 Oct 2025 09:47:44 -0700 Subject: [PATCH 071/158] working through issues processing on 1 core --- corems/mass_spectra/calc/lc_calc.py | 9 ++++++- corems/mass_spectra/factory/lc_class.py | 36 ++++++++++++++++--------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 9b55a40c0..96e347327 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -3242,6 +3242,13 @@ def _search_for_targeted_mass_features_in_sample(self, obj_idx, missingdf, thres ].reset_index(drop = True).copy() self.load_raw_data(obj_idx, 1) + + ## print lines added to see what is coming out + print(self) + print(self[obj_idx]) + print(self[obj_idx]._ms_unprocessed) + + ## this is the line that bugs due to _ms_unprocessed not having key 1 ms1df = self[obj_idx]._ms_unprocessed[1].copy() scan_df = self[obj_idx].scan_df[['scan', 'scan_time_aligned']] ms1df = pd.merge(ms1df, scan_df, on = 'scan') @@ -3339,7 +3346,7 @@ def search_for_targeted_mass_features_in_collection(self, threshold = 0.5, tol_f if self.parameters.lcms_collection.cores == 1: for i in range(sample_ct): - self[i]._search_for_targeted_mass_features_in_sample(i, missingdf, threshold, tol_flag) + self._search_for_targeted_mass_features_in_sample(i, missingdf, threshold, tol_flag) if self.parameters.lcms_collection.cores > 1: if self.parameters.lcms_collection.cores > len(self): diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 73d4e5d93..37c49e30d 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1462,6 +1462,7 @@ def __init__( # These attributes are generally set by the parser during instantiation of this class self._lcms = {} self._combined_mass_features = None + self._combined_induced_mass_features = None self.consensus_mass_features = {} self._parameters = LCMSCollectionParameters() self.isotopes_dropped = False @@ -1481,12 +1482,14 @@ def __getitem__(self, index): def __len__(self): return len(self.samples) - def _prepare_lcms_mass_features_for_combination(self, lcms_obj): + def _prepare_lcms_mass_features_for_combination(self, lcms_obj, induced_features = False): """ Prepares the mass features in the LCMS objects in the collection for combination. - """ + """ + if induced_features == True: + mf_df = lcms_obj.mass_features_to_df(induced_features = True) # Check if lcms_obj has attribute light_mf_df - if hasattr(lcms_obj, "light_mf_df"): + elif hasattr(lcms_obj, "light_mf_df"): mf_df = lcms_obj.light_mf_df else: mf_df = lcms_obj.mass_features_to_df() @@ -1505,21 +1508,24 @@ def _prepare_lcms_mass_features_for_combination(self, lcms_obj): return mf_df - def _combine_mass_features(self): + def _combine_mass_features(self, induced_features = False): """ Concatenates the mass features from all the LCMS objects in the collection. Returns -------- - None, sets the _combined_mass_features attribute. + None, sets the _combined_mass_features or _combined_induced_mass_feature attribute. """ - + print('starting fxn') if self.parameters.lcms_collection.cores == 1: # Prepare mass features for combination sequentially mf_df_list = [] + ct = 0 for lcms_obj in self: - mf_df = self._prepare_lcms_mass_features_for_combination(lcms_obj) + print('starting sample', ct) + mf_df = self._prepare_lcms_mass_features_for_combination(lcms_obj, induced_features) mf_df_list.append(mf_df) + ct += 1 if self.parameters.lcms_collection.cores > 1: # Parallelize the mass feature preparation @@ -1528,20 +1534,26 @@ def _combine_mass_features(self): else: ncores = self.parameters.lcms_collection.cores pool = multiprocessing.Pool(ncores) - mf_df_list = pool.starmap(self._prepare_lcms_mass_features_for_combination, [(lcms_obj,) for lcms_obj in self]) + mf_df_list = pool.starmap( + self._prepare_lcms_mass_features_for_combination, + [(lcms_obj, induced_features) for lcms_obj in self] + ) combined_mass_features = pd.concat(mf_df_list) - + print('df concatted') # Move coll_mf_id, sample_name, and sample_id to front cols = combined_mass_features.columns.tolist() top_cols = ["coll_mf_id", "sample_name", "sample_id", "mz", "scan_time_aligned"] cols = [x for x in top_cols + [col for col in cols if col not in top_cols] if x in cols] combined_mass_features = combined_mass_features[cols] - + print('df formatted') # Make coll_mf_id the index combined_mass_features = combined_mass_features.set_index("coll_mf_id") - - self._combined_mass_features = combined_mass_features + print('df index set') + if induced_features == True: + self._combined_induced_mass_features = combined_mass_features + else: + self._combined_mass_features = combined_mass_features def _check_mass_features_df(self): """Checks if the mass features dataframe has expected columns. If not, adds them. From 894474e6d42ae24b92cbb72bcb90b894841c0899 Mon Sep 17 00:00:00 2001 From: "danielle.ciesielski@pnnl.gov" Date: Mon, 13 Oct 2025 12:06:53 -0700 Subject: [PATCH 072/158] mostly working --- corems/mass_spectra/calc/lc_calc.py | 13 ++- corems/mass_spectra/factory/lc_class.py | 126 +++++++++++++++--------- 2 files changed, 89 insertions(+), 50 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 96e347327..51c2661d6 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -3244,9 +3244,9 @@ def _search_for_targeted_mass_features_in_sample(self, obj_idx, missingdf, thres self.load_raw_data(obj_idx, 1) ## print lines added to see what is coming out - print(self) - print(self[obj_idx]) - print(self[obj_idx]._ms_unprocessed) +# print(self) +# print(self[obj_idx]) +# print(self[obj_idx]._ms_unprocessed) ## this is the line that bugs due to _ms_unprocessed not having key 1 ms1df = self[obj_idx]._ms_unprocessed[1].copy() @@ -3360,4 +3360,9 @@ def search_for_targeted_mass_features_in_collection(self, threshold = 0.5, tol_f ) for i in range(sample_ct): - self[i].induced_mass_features = mp_result[i] \ No newline at end of file + self[i].induced_mass_features = mp_result[i] + + self._combine_mass_features(induced_features = True) + + for sample_name in self.samples: + self._lcms[sample_name].mass_features = {} \ No newline at end of file diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 37c49e30d..c0fb98b52 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1516,46 +1516,49 @@ def _combine_mass_features(self, induced_features = False): -------- None, sets the _combined_mass_features or _combined_induced_mass_feature attribute. """ - print('starting fxn') - if self.parameters.lcms_collection.cores == 1: - # Prepare mass features for combination sequentially - mf_df_list = [] - ct = 0 - for lcms_obj in self: - print('starting sample', ct) - mf_df = self._prepare_lcms_mass_features_for_combination(lcms_obj, induced_features) - mf_df_list.append(mf_df) - ct += 1 - - if self.parameters.lcms_collection.cores > 1: - # Parallelize the mass feature preparation - if self.parameters.lcms_collection.cores > len(self): - ncores = len(self) - else: - ncores = self.parameters.lcms_collection.cores - pool = multiprocessing.Pool(ncores) - mf_df_list = pool.starmap( - self._prepare_lcms_mass_features_for_combination, - [(lcms_obj, induced_features) for lcms_obj in self] - ) + + ## TODO: See why this function runs slower on multiprocessing, + ## especially for induced features + ## has only been considered so far on ~20 samples +# if self.parameters.lcms_collection.cores == 1: +# # Prepare mass features for combination sequentially +# mf_df_list = [] +# for lcms_obj in self: +# mf_df = self._prepare_lcms_mass_features_for_combination(lcms_obj, induced_features) +# mf_df_list.append(mf_df) + +# if self.parameters.lcms_collection.cores > 1: +# # Parallelize the mass feature preparation +# if self.parameters.lcms_collection.cores > len(self): +# ncores = len(self) +# else: +# ncores = self.parameters.lcms_collection.cores +# pool = multiprocessing.Pool(ncores) +# mf_df_list = pool.starmap( +# self._prepare_lcms_mass_features_for_combination, +# [(lcms_obj, induced_features) for lcms_obj in self] +# ) + + # Prepare mass features for combination sequentially + mf_df_list = [] + for lcms_obj in self: + mf_df = self._prepare_lcms_mass_features_for_combination(lcms_obj, induced_features) + mf_df_list.append(mf_df) combined_mass_features = pd.concat(mf_df_list) - print('df concatted') # Move coll_mf_id, sample_name, and sample_id to front cols = combined_mass_features.columns.tolist() top_cols = ["coll_mf_id", "sample_name", "sample_id", "mz", "scan_time_aligned"] cols = [x for x in top_cols + [col for col in cols if col not in top_cols] if x in cols] combined_mass_features = combined_mass_features[cols] - print('df formatted') # Make coll_mf_id the index combined_mass_features = combined_mass_features.set_index("coll_mf_id") - print('df index set') if induced_features == True: self._combined_induced_mass_features = combined_mass_features else: self._combined_mass_features = combined_mass_features - def _check_mass_features_df(self): + def _check_mass_features_df(self, induced_features = False): """Checks if the mass features dataframe has expected columns. If not, adds them. Returns @@ -1565,16 +1568,22 @@ def _check_mass_features_df(self): Notes ------ - If scan_time_aligned is not in the _combined_mass_features, tries to add it. + If scan_time_aligned is not in the _combined_mass_features or + _combined_induced_mass_features, tries to add it. - """ + """ + + if induced_features: + cmf_df = self._combined_induced_mass_features + else: + cmf_df = self._combined_mass_features # Check if parameters are set to drop isotopologues and drop if so if self.parameters.lcms_collection.drop_isotopologues: if not self.isotopes_dropped: self._drop_isotopologues() # Check if scan_time_aligned is in combined_mass_features, try to add if not - if self._combined_mass_features is not None and "scan_time_aligned" not in self._combined_mass_features.columns: - cmb_mf = self._combined_mass_features.copy() + if cmf_df is not None and "scan_time_aligned" not in cmf_df.columns: + cmb_mf = cmf_df.copy() cmb_mf = cmb_mf.reset_index(drop=False) lcms_aligned = [True for x in self if "scan_time_aligned" in x.scan_df.columns] if len(lcms_aligned) == len(self): @@ -1590,8 +1599,11 @@ def _check_mass_features_df(self): cmb_mf_merged = cmb_mf.merge(scan_time_aligned_df, on=["apex_scan", "sample_name"]) cmb_mf_mergd = cmb_mf_merged.set_index("coll_mf_id") # Merge scan_time_aligned_df with combined_mass_features on apex_scan and sample_name - self._combined_mass_features = cmb_mf_mergd - + if induced_features: + self._combined_induced_mass_features = cmb_mf_mergd + else: + self._combined_mass_features = cmb_mf_merged + def plot_tics(self, ms_level=1, type = "raw", plot_legend=False): """Plots the TICs for all the LCMS objects in the collection. @@ -1789,22 +1801,25 @@ def collection_pivot_table(self, attribute = 'coll_mf_id'): and samples in a collection """ + mf_pivot = self.mass_features_dataframe.copy() mf_pivot.reset_index(inplace = True) - - for i in range(mf_pivot.sample_id.unique().max() + 1): - imf_pivot = self[i].mass_features_to_df(induced_features = True).copy() - imf_pivot.reset_index(inplace = True) - imf_pivot['sample_id'] = i - imf_pivot['cluster'] = np.nan - imf_pivot.rename(columns = {'mf_id': 'coll_mf_id'}, inplace = True) - for j in range(len(imf_pivot)): - imf_pivot.loc[j, 'cluster'] = imf_pivot.loc[j].coll_mf_id.split('_')[0][1:] - mf_pivot = pd.concat([mf_pivot, imf_pivot], axis = 0) + imf_pivot = self.induced_mass_features_dataframe.copy() + imf_pivot.reset_index(inplace = True) + for j in range(len(imf_pivot)): + imf_pivot.loc[j, 'cluster'] = int(imf_pivot.loc[j].coll_mf_id.split('_')[1][1:]) + mf_pivot = pd.concat([mf_pivot, imf_pivot], axis = 0) mf_pivot.reset_index(drop = True, inplace = True) - mf_pivot.cluster = mf_pivot.cluster.astype(int) - available_attributes = [x for x in mf_pivot.columns if x not in ['cluster', 'sample_id']] - print('Attributes available for pivot table:', available_attributes) + mf_pivot['cluster'] = mf_pivot['cluster'].astype(int) + + print( + 'Attributes available for pivot table:\n', + [x for x in mf_pivot.columns if x not in ['cluster', 'sample_id']] + ) + print( + '\nAttributes that have no value for induced mass features:\n', + imf_pivot.columns[imf_pivot.isna().all()].tolist() + ) return mf_pivot.pivot(index = 'cluster', columns = 'sample_id', values = attribute) @property @@ -1830,7 +1845,7 @@ def parameters(self, paramsinstance): def mass_features_dataframe(self): self._check_mass_features_df() return self._combined_mass_features - + @mass_features_dataframe.setter def mass_features_dataframe(self, df): # Check that the dataframe has the expected columns @@ -1844,6 +1859,25 @@ def mass_features_dataframe(self, df): if not df.index.is_unique: raise ValueError("coll_mf_id must be unique") self._combined_mass_features = df + + @property + def induced_mass_features_dataframe(self): + self._check_mass_features_df(induced_features = True) + return self._combined_induced_mass_features + + @induced_mass_features_dataframe.setter + def induced_mass_features_dataframe(self, df): + # Check that the dataframe has the expected columns + expected_cols = ["sample_name", "sample_id", "mz", "scan_time"] + if not all([col in df.columns for col in expected_cols]): + raise ValueError(f"Expected columns not found in dataframe: {expected_cols}") + + # Check that coll_mf_id is the index and it is unique + if df.index.name != "coll_mf_id": + raise ValueError("coll_mf_id must be the index of the dataframe") + if not df.index.is_unique: + raise ValueError("coll_mf_id must be unique") + self._combined_induced_mass_features = df @property def cluster_summary_dataframe(self): From 19a78f356b1730b6ce43d1d84e92526e1693ae9e Mon Sep 17 00:00:00 2001 From: "danielle.ciesielski@pnnl.gov" Date: Mon, 13 Oct 2025 15:27:49 -0700 Subject: [PATCH 073/158] found the last bug, should be ready --- corems/mass_spectra/factory/lc_class.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index c0fb98b52..ecc025ceb 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1553,6 +1553,7 @@ def _combine_mass_features(self, induced_features = False): combined_mass_features = combined_mass_features[cols] # Make coll_mf_id the index combined_mass_features = combined_mass_features.set_index("coll_mf_id") + print('line 1556, _combine_mass_features', combined_mass_features) if induced_features == True: self._combined_induced_mass_features = combined_mass_features else: @@ -1577,6 +1578,7 @@ def _check_mass_features_df(self, induced_features = False): cmf_df = self._combined_induced_mass_features else: cmf_df = self._combined_mass_features + print('checking if _combined_mass_features is set', cmf_df) # Check if parameters are set to drop isotopologues and drop if so if self.parameters.lcms_collection.drop_isotopologues: if not self.isotopes_dropped: @@ -1597,10 +1599,11 @@ def _check_mass_features_df(self, induced_features = False): # Rename scan to apex_scan scan_time_aligned_df = scan_time_aligned_df.rename(columns={"scan": "apex_scan"}) cmb_mf_merged = cmb_mf.merge(scan_time_aligned_df, on=["apex_scan", "sample_name"]) - cmb_mf_mergd = cmb_mf_merged.set_index("coll_mf_id") + cmb_mf_merged = cmb_mf_merged.set_index("coll_mf_id") + print('line 1603, _check_mass_features_df', cmb_mf_merged) # Merge scan_time_aligned_df with combined_mass_features on apex_scan and sample_name if induced_features: - self._combined_induced_mass_features = cmb_mf_mergd + self._combined_induced_mass_features = cmb_mf_merged else: self._combined_mass_features = cmb_mf_merged @@ -1688,7 +1691,7 @@ def _drop_isotopologues(self): cmb_mf_df2 = pd.concat([cmb_monos, cmb_nomonos, cmb_decon_parent]) cmb_mf_df2 = cmb_mf_df2[~cmb_mf_df2.index.duplicated(keep='first')] - + print('line 1694, _drop_isotopologues', cmb_mf_df2) self.isotopes_dropped = True self._combined_mass_features = cmb_mf_df2 @@ -1858,6 +1861,7 @@ def mass_features_dataframe(self, df): raise ValueError("coll_mf_id must be the index of the dataframe") if not df.index.is_unique: raise ValueError("coll_mf_id must be unique") + print('line 1864, mass_features_dataframe', df) self._combined_mass_features = df @property From 46e8dbb13ab12aa7b9544b63c0c56dbca303bd11 Mon Sep 17 00:00:00 2001 From: "danielle.ciesielski@pnnl.gov" Date: Mon, 13 Oct 2025 15:29:24 -0700 Subject: [PATCH 074/158] remove debugging linews --- corems/mass_spectra/factory/lc_class.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index ecc025ceb..0dec3ce19 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1553,7 +1553,6 @@ def _combine_mass_features(self, induced_features = False): combined_mass_features = combined_mass_features[cols] # Make coll_mf_id the index combined_mass_features = combined_mass_features.set_index("coll_mf_id") - print('line 1556, _combine_mass_features', combined_mass_features) if induced_features == True: self._combined_induced_mass_features = combined_mass_features else: @@ -1600,7 +1599,6 @@ def _check_mass_features_df(self, induced_features = False): scan_time_aligned_df = scan_time_aligned_df.rename(columns={"scan": "apex_scan"}) cmb_mf_merged = cmb_mf.merge(scan_time_aligned_df, on=["apex_scan", "sample_name"]) cmb_mf_merged = cmb_mf_merged.set_index("coll_mf_id") - print('line 1603, _check_mass_features_df', cmb_mf_merged) # Merge scan_time_aligned_df with combined_mass_features on apex_scan and sample_name if induced_features: self._combined_induced_mass_features = cmb_mf_merged @@ -1691,7 +1689,6 @@ def _drop_isotopologues(self): cmb_mf_df2 = pd.concat([cmb_monos, cmb_nomonos, cmb_decon_parent]) cmb_mf_df2 = cmb_mf_df2[~cmb_mf_df2.index.duplicated(keep='first')] - print('line 1694, _drop_isotopologues', cmb_mf_df2) self.isotopes_dropped = True self._combined_mass_features = cmb_mf_df2 @@ -1861,7 +1858,6 @@ def mass_features_dataframe(self, df): raise ValueError("coll_mf_id must be the index of the dataframe") if not df.index.is_unique: raise ValueError("coll_mf_id must be unique") - print('line 1864, mass_features_dataframe', df) self._combined_mass_features = df @property From 32e47a3780672b4d4e97f0fbb5154b1e3efadc60 Mon Sep 17 00:00:00 2001 From: "danielle.ciesielski@pnnl.gov" Date: Mon, 13 Oct 2025 16:05:03 -0700 Subject: [PATCH 075/158] remove more debugging lines --- corems/mass_spectra/factory/lc_class.py | 1 - 1 file changed, 1 deletion(-) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 0dec3ce19..05068181b 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1577,7 +1577,6 @@ def _check_mass_features_df(self, induced_features = False): cmf_df = self._combined_induced_mass_features else: cmf_df = self._combined_mass_features - print('checking if _combined_mass_features is set', cmf_df) # Check if parameters are set to drop isotopologues and drop if so if self.parameters.lcms_collection.drop_isotopologues: if not self.isotopes_dropped: From 99be90fbb68cfe5616c6003a457f91b4a9e13f6b Mon Sep 17 00:00:00 2001 From: "danielle.ciesielski@pnnl.gov" Date: Mon, 13 Oct 2025 16:35:47 -0700 Subject: [PATCH 076/158] updating workflow with pivot tables --- support_code/nmdc/lipidomics/lipidomics_collection.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index c9d8d5408..b75dabe23 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -70,9 +70,8 @@ lcms_collection.plot_cluster_outlier_frequency(dim_list, clu_size_thresh = 0.25) lcms_collection.search_for_missing_mass_features_in_collection() - print('Sample output:') - print(lcms_collection[0].mass_features_to_df(induced_features = True)) - + lcms_collection.collection_pivot_table(attribute = 'mz') + #TODO: Add code to load and save information about chromatographic settings #TODO: Add code to save and load collection to HDF5 file #TODO: Generate report of summarize_clusters as a table with both regular and induced mass features \ No newline at end of file From c63d92204eba016fd26d0d68f3a5231806b3196b Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 14 Oct 2025 10:42:05 -0700 Subject: [PATCH 077/158] Fix peak metric check and filtering after merge conflicts --- corems/mass_spectra/calc/lc_calc.py | 10 +++------- corems/mass_spectra/input/corems_hdf5.py | 2 -- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 701060bf0..2fcaf2bb8 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -193,7 +193,7 @@ def find_nearest_scan(self, rt): return real_scan - def add_peak_metrics(self, induced_features = False): + def add_peak_metrics(self, remove_by_metrics=True, induced_features=False): """Add peak metrics to the mass features. This function calculates the peak metrics for each mass feature and adds them to the mass feature objects. @@ -204,6 +204,8 @@ def add_peak_metrics(self, induced_features = False): If True, remove mass features based on their peak metrics such as S/N, Gaussian similarity, dispersity index, and noise score. Default is True, which checks the setting in the processing parameters. If False, peak metrics are calculated but no mass features are removed, regardless of the setting in the processing parameters. + induced_features : bool, optional + Whether the mass features to be integrated were induced. Default is False. """ # Check that at least some mass features have eic data if induced_features: @@ -474,12 +476,6 @@ def integrate_mass_features( raise ValueError( "No mass features found, did you run find_mass_features() first?" ) - if not all( - [mf.mass_spectrum is not None for mf in mf_dict.values()] - ): - raise ValueError( - "Mass spectrum must be associated with each mass feature, did you run add_associated_ms1() first?" - ) # Subset scan data to only include correct ms_level scan_df_sub = self.scan_df[ diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 62f6beb66..38d90dcca 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -536,8 +536,6 @@ def get_lcms_obj( lcms_obj = self.add_original_parser(lcms_obj, raw_file_path=raw_file_path) else: lcms_obj.spectra_parser_class = self.__class__ - else: - lcms_obj.spectra_parser_class = self.__class__ return lcms_obj From e291fe86e3e57c044c6150995dd7061a360f3a15 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 14 Oct 2025 12:58:13 -0700 Subject: [PATCH 078/158] Fix test fixture --- tests/test_wf_lipidomics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_wf_lipidomics.py b/tests/test_wf_lipidomics.py index 7bc739923..d88aba9a4 100644 --- a/tests/test_wf_lipidomics.py +++ b/tests/test_wf_lipidomics.py @@ -148,7 +148,7 @@ def test_lipidomics_workflow(postgres_database, lcms_obj): # Export the mass features to a pandas dataframe df = lcms_obj.mass_features_to_df() - assert df.shape == (128, 19) + assert df.shape == (128, 20) # Plot a mass feature lcms_obj.mass_features[0].plot(return_fig=False) @@ -239,7 +239,7 @@ def test_lipidomics_workflow(postgres_database, lcms_obj): # Check that the mass features dataframe is the same as the original df2 = myLCMSobj2.mass_features_to_df() - assert df2.shape == (128, 19) + assert df2.shape == (128, 20) myLCMSobj2.mass_features[0].mass_spectrum.to_dataframe() assert myLCMSobj2.mass_features[0].ms1_peak[0].string == "C20 H30 O2" assert myLCMSobj2.mass_features_ms1_annot_to_df().shape[0] > 130 From 9d74a893074c4b16cac186154a4476271e013513 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 14 Oct 2025 16:45:03 -0700 Subject: [PATCH 079/158] Fix test fixture --- tests/test_lcms_metabolomics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_lcms_metabolomics.py b/tests/test_lcms_metabolomics.py index 332105917..ed4c539d2 100644 --- a/tests/test_lcms_metabolomics.py +++ b/tests/test_lcms_metabolomics.py @@ -146,7 +146,7 @@ def test_lcms_metabolomics(postgres_database, lcms_obj, msp_file_location): # Check that the mass features dataframe is the same as the original df2 = myLCMSobj2.mass_features_to_df() df1 = lcms_obj.mass_features_to_df() - assert df2.shape == df1.shape == (130, 13) + assert df2.shape == df1.shape myLCMSobj2.mass_features[0].plot(return_fig=False) # Delete the "Blanch_Nat_Lip_C_12_AB_M_17_NEG_25Jan18_Brandi-WCSH5801.corems" directory From 287441953c6885c1d5e44f67a1de5b8dcd9d561d Mon Sep 17 00:00:00 2001 From: "danielle.ciesielski@pnnl.gov" Date: Wed, 15 Oct 2025 14:18:03 -0700 Subject: [PATCH 080/158] first pass at consensus report, fix bug with merge --- corems/mass_spectra/factory/lc_class.py | 89 ++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 10 deletions(-) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index df8d9e033..8370dd304 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1539,7 +1539,7 @@ def _prepare_lcms_mass_features_for_combination(self, lcms_obj, induced_features if "scan_time_aligned" in lcms_obj.scan_df.columns: scan_df = lcms_obj.scan_df[["scan", "scan_time_aligned"]].copy() scan_df = scan_df.rename(columns={"scan": "apex_scan"}) - mf_df = mf_df.merge(scan_df, left_on="apex_scan", right_index=True) + mf_df = mf_df.merge(scan_df, on="apex_scan") return mf_df @@ -1817,7 +1817,7 @@ def drop_raw_data(self, sample_idx: int, ms_level = 1) -> None: # Drop the raw data del self[sample_idx]._ms_unprocessed[ms_level] - def collection_pivot_table(self, attribute = 'coll_mf_id'): + def collection_pivot_table(self, attribute = 'coll_mf_id', verbose = True): """Generate a pivot table of all regular and induced mass features in a collection. Default attribute presented is the mass feature ID, also prints a list of other available attributes. @@ -1827,6 +1827,9 @@ def collection_pivot_table(self, attribute = 'coll_mf_id'): attribute : str The desired attribute to be presented in the pivot table. Defaults to mass feature ID + verbose : boolean + Print out all the possible values the fill the pivot table and list + attributes that are not collected for induced mass features Returns -------- @@ -1846,16 +1849,82 @@ def collection_pivot_table(self, attribute = 'coll_mf_id'): mf_pivot.reset_index(drop = True, inplace = True) mf_pivot['cluster'] = mf_pivot['cluster'].astype(int) - print( - 'Attributes available for pivot table:\n', - [x for x in mf_pivot.columns if x not in ['cluster', 'sample_id']] - ) - print( - '\nAttributes that have no value for induced mass features:\n', - imf_pivot.columns[imf_pivot.isna().all()].tolist() - ) + if verbose: + print( + 'Attributes available for pivot table:\n', + [x for x in mf_pivot.columns if x not in ['cluster', 'sample_id', 'mf_id', 'partition_idx', 'idx']] + ) + print( + '\nAttributes that have no value for induced mass features:\n', + imf_pivot.columns[imf_pivot.isna().all()].tolist() + ) return mf_pivot.pivot(index = 'cluster', columns = 'sample_id', values = attribute) + + def collection_consensus_report(self, how = 'intensity'): + """Generate a consensus report of all regular and induced mass features + in a collection. Default is to select feature of highest intensity in + a cluster and report back all the attributes. + + Parameters + ----------- + how : str + The preferred method to report back the consensus information. + Option 'intensity' assigns peak of highest intensity in each + cluster as the representative consensus feaature and reports data + on that peak. Option 'means' reports the mean values for available + attributes by cluster. + + Returns + -------- + pd.DataFrame + A DataFrame that displays all attributes for each cluster in a + collection based on either the peak of highest intensity or means + across all data in a cluster + """ + mf_df = self.mass_features_dataframe.copy() + mf_df.reset_index(inplace = True) + imf_df = self.induced_mass_features_dataframe.copy() + imf_df.reset_index(inplace = True) + for j in range(len(imf_df)): + imf_df.loc[j, 'cluster'] = int(imf_df.loc[j].coll_mf_id.split('_')[1][1:]) + mf_df = pd.concat([mf_df, imf_df], axis = 0) + mf_df.reset_index(drop = True, inplace = True) + mf_df['cluster'] = mf_df['cluster'].astype(int) + + if how == 'intensity': + int_table = self.collection_pivot_table(attribute = 'intensity', verbose = False).idxmax(axis = 1) + id_list = [] + for i in range(len(int_table)): + id_list.append( + mf_df[ + (mf_df.sample_id == int_table[i]) & (mf_df.cluster == int_table.index[i]) + ].coll_mf_id.values[0] + ) + return mf_df[mf_df.coll_mf_id.isin(id_list)].sort_values(by = 'cluster').set_index('cluster') + + elif how == 'means': + mean_table = ( + mf_df.groupby("cluster").agg({ + 'mz': 'mean', + 'intensity': 'mean', + 'persistence': 'mean', + 'area': 'mean', + 'half_height_width': 'mean', + 'tailing_factor': 'mean', + 'dispersity_index': 'mean', + 'normalized_dispersity_index': 'mean', + 'noise_score': 'mean', + 'noise_score_min': 'mean', + 'noise_score_max': 'mean', + 'scan_time_aligned': 'mean' + }).reset_index() + ) + return mean_table.sort_values(by = 'cluster').set_index('cluster') + + else: + print("Define 'how' argument as either 'intensity' or 'means'") + @property def parameters(self): """ From 2360c81250e4ec013d7b89b99856bedb4f2c0a0e Mon Sep 17 00:00:00 2001 From: "danielle.ciesielski@pnnl.gov" Date: Thu, 16 Oct 2025 13:32:32 -0700 Subject: [PATCH 081/158] update mean mode for consensus report --- corems/mass_spectra/factory/lc_class.py | 48 ++++++++++++++----------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 8370dd304..9fb3b7a55 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -903,7 +903,16 @@ def mass_spectrum_to_string( # reset index to mf_id df_mf = df_mf.set_index("mf_id") df_mf.index.name = "mf_id" - + + if 'half_height_width' in df_mf.columns: + df_mf["half_height_width"] = df_mf["half_height_width"].astype('float64') + if 'tailing_factor' in df_mf.columns: + df_mf["tailing_factor"] = df_mf["tailing_factor"].astype('float64') + if 'dispersity_index' in df_mf.columns: + df_mf["dispersity_index"] = df_mf["dispersity_index"].astype('float64') + if 'normalized_dispersity_index' in df_mf.columns: + df_mf["normalized_dispersity_index"] = df_mf["normalized_dispersity_index"].astype('float64') + return df_mf def mass_features_ms1_annot_to_df(self): @@ -1872,7 +1881,8 @@ def collection_consensus_report(self, how = 'intensity'): Option 'intensity' assigns peak of highest intensity in each cluster as the representative consensus feaature and reports data on that peak. Option 'means' reports the mean values for available - attributes by cluster. + attributes by cluster, and option 'medians' is the same but reports + median values. Returns -------- @@ -1880,8 +1890,8 @@ def collection_consensus_report(self, how = 'intensity'): A DataFrame that displays all attributes for each cluster in a collection based on either the peak of highest intensity or means across all data in a cluster - """ + mf_df = self.mass_features_dataframe.copy() mf_df.reset_index(inplace = True) imf_df = self.induced_mass_features_dataframe.copy() @@ -1903,24 +1913,20 @@ def collection_consensus_report(self, how = 'intensity'): ) return mf_df[mf_df.coll_mf_id.isin(id_list)].sort_values(by = 'cluster').set_index('cluster') - elif how == 'means': - mean_table = ( - mf_df.groupby("cluster").agg({ - 'mz': 'mean', - 'intensity': 'mean', - 'persistence': 'mean', - 'area': 'mean', - 'half_height_width': 'mean', - 'tailing_factor': 'mean', - 'dispersity_index': 'mean', - 'normalized_dispersity_index': 'mean', - 'noise_score': 'mean', - 'noise_score_min': 'mean', - 'noise_score_max': 'mean', - 'scan_time_aligned': 'mean' - }).reset_index() - ) - return mean_table.sort_values(by = 'cluster').set_index('cluster') + elif how == 'means' or how == 'medians': + ## will have to go back and check mf_df.dtypes to see what ends up on list + ## this format removes int columns like 'sample_id' and 'cluster' + l = mf_df.select_dtypes(include='float64').columns.tolist() + ## for some reason, the following 4 items get saved as floats instead of ints + ## can either leave this or track down how they're recorded and fix it at the source + ## possible they're recorded as ints for regular mass features and floats for induced, making the column default to the more general + l = [x for x in l if x not in ['scan_time', 'apex_scan', 'partition_idx', 'idx']] + agg_dict = {k: [how] for k in l} + agg_dict['sample_id'] = ['nunique'] + + df = (mf_df.groupby("cluster").agg(agg_dict).reset_index()) + + return df.sort_values(by = 'cluster').set_index('cluster') else: print("Define 'how' argument as either 'intensity' or 'means'") From ed2c172526fe383a2a2b7570035da5dddbfedf75 Mon Sep 17 00:00:00 2001 From: "danielle.ciesielski@pnnl.gov" Date: Thu, 16 Oct 2025 14:52:27 -0700 Subject: [PATCH 082/158] update workflow, small fix, and beginning of cluster_dictionary brainstorming --- corems/mass_spectra/calc/lc_calc.py | 11 +++++++---- corems/mass_spectra/factory/lc_class.py | 17 +++++++++++++---- .../nmdc/lipidomics/lipidomics_collection.py | 3 +++ 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 55d383762..7335d6eea 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -3539,6 +3539,8 @@ def _search_for_targeted_mass_features_in_sample(self, obj_idx, missingdf, thres Indicates whethere to assign induced_mass_features attribute in place or to return the result when the function is run in parallel """ + ## retrieve the cluster/deature dictionary to record induced features + cluster_dict = self.cluster_feature_dictionary ## to get clusters missing data based on sample index: sampledf = missingdf[ @@ -3563,14 +3565,15 @@ def _search_for_targeted_mass_features_in_sample(self, obj_idx, missingdf, thres st_min = sampledf.scan_time_aligned_min.iloc[i] st_max = sampledf.scan_time_aligned_max.iloc[i] # TODO: Adjust function to input queries as tuples so we can search - # for all targeted mass features at once instead of in a loop + # for all targeted mass features at once instead of in a loop + set_id = 'c' + str(sampledf.cluster.iloc[i]) + '_' + str(i) + '_i' found_feature = self[obj_idx].search_for_targeted_mass_feature( ms1df, mz_min, mz_max, st_min, st_max, - set_id = 'c' + str(sampledf.cluster.iloc[i]) + '_' + str(i) + '_i', + set_id, obj_idx = obj_idx, st_aligned = True ) @@ -3587,13 +3590,13 @@ def _search_for_targeted_mass_features_in_sample(self, obj_idx, missingdf, thres mz_max, st_min, st_max, - set_id = 'c' + str(sampledf.cluster.iloc[i]) + '_' + str(i) + '_i', + set_id, obj_idx = obj_idx, st_aligned = True ) self[obj_idx].induced_mass_features[found_feature.id] = found_feature - + cluster_dict[sampledf.cluster.iloc[i]] += [set_id] self[obj_idx].add_associated_ms1(induced_features = True) # need to set drop_if_fail to false for induced features as they will fail self[obj_idx].integrate_mass_features(drop_if_fail = False, induced_features = True) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 9fb3b7a55..11f2b076a 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1880,8 +1880,8 @@ def collection_consensus_report(self, how = 'intensity'): The preferred method to report back the consensus information. Option 'intensity' assigns peak of highest intensity in each cluster as the representative consensus feaature and reports data - on that peak. Option 'means' reports the mean values for available - attributes by cluster, and option 'medians' is the same but reports + on that peak. Option 'mean' reports the mean values for available + attributes by cluster, and option 'median' is the same but reports median values. Returns @@ -1913,7 +1913,7 @@ def collection_consensus_report(self, how = 'intensity'): ) return mf_df[mf_df.coll_mf_id.isin(id_list)].sort_values(by = 'cluster').set_index('cluster') - elif how == 'means' or how == 'medians': + elif how == 'mean' or how == 'median': ## will have to go back and check mf_df.dtypes to see what ends up on list ## this format removes int columns like 'sample_id' and 'cluster' l = mf_df.select_dtypes(include='float64').columns.tolist() @@ -1929,7 +1929,7 @@ def collection_consensus_report(self, how = 'intensity'): return df.sort_values(by = 'cluster').set_index('cluster') else: - print("Define 'how' argument as either 'intensity' or 'means'") + print("Assign 'how' argument as either 'intensity', 'mean', or 'median'") @property def parameters(self): @@ -2026,3 +2026,12 @@ def consensus_mass_feature_dataframe(self): def raw_files(self): """Returns a list of raw files in the collection.""" return [x.raw_file_location for x in self] + + @property + def cluster_feature_dictionary(self): + """Generates a dictionary with clusters for keys and mass feature IDs as entries""" + df = self.mass_features_dataframe + cluster_dict = {} + for c in range(df.cluster.max()): + cluster_dict[c] = df[df.cluster == 0].index.tolist() + return cluster_dict \ No newline at end of file diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index b75dabe23..5d01a089f 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -71,6 +71,9 @@ lcms_collection.search_for_missing_mass_features_in_collection() lcms_collection.collection_pivot_table(attribute = 'mz') + lcms_collection.collection_consensus_report(how = 'intensity') + lcms_collection.collection_consensus_report(how = 'mean') + lcms_collection.collection_consensus_report(how = 'median') #TODO: Add code to load and save information about chromatographic settings #TODO: Add code to save and load collection to HDF5 file From 4e8d4b04644976c978457d3f05b9cd92816c381b Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 2 Dec 2025 13:44:13 -0800 Subject: [PATCH 083/158] Add error handling and functionality for moving raw data files --- .../factory/processingSetting.py | 2 +- corems/mass_spectra/calc/lc_calc.py | 2 +- corems/mass_spectra/factory/lc_class.py | 64 +++++++++++++++++++ corems/mass_spectra/input/corems_hdf5.py | 12 ++-- 4 files changed, 72 insertions(+), 8 deletions(-) diff --git a/corems/encapsulation/factory/processingSetting.py b/corems/encapsulation/factory/processingSetting.py index 25e8f3a26..c7af50348 100644 --- a/corems/encapsulation/factory/processingSetting.py +++ b/corems/encapsulation/factory/processingSetting.py @@ -1152,7 +1152,7 @@ class LCMSCollectionSettings: drop_isotopologues: bool = False # Settings for doing mass feature alignment - _mass_feature_anchor_technique: list = dataclasses.field(default_factory=lambda: ["deconvoluted_mass_spectra"]) + _mass_feature_anchor_technique: list = dataclasses.field(default_factory=lambda: ["absolute_intensity"]) mass_feature_anchor_techniques_available: tuple = ("deconvoluted_mass_spectra", "absolute_intensity") mass_feature_anchor_aboslute_intensity_threshold: int = 10000 alignment_hold_out_fraction: float = 0.3 diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 7335d6eea..50397d95f 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -2385,7 +2385,7 @@ def match_mfs(self, mf_c, mf_i): This function has been adapted from the original implementation in the Deimos package: https://github.com/pnnl/deimos """ - if mf_c is None or mf_i is None: + if mf_c is None or mf_i is None or len(mf_c.index) < 1 or len(mf_i.index) < 1: return None, None # Prepare dataframes diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 11f2b076a..0b08ecbfe 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -126,6 +126,11 @@ def __init__( @property def spectra_parser(self): """Returns an instance of the spectra parser class.""" + # Check if a file exists at the raw_file_location + if not Path(self.raw_file_location).exists(): + raise FileNotFoundError( + f"Raw file not found at location: {self.raw_file_location}, update raw_file_location property to point to correct location." + ) return self.spectra_parser_class(self.raw_file_location) @property @@ -1826,6 +1831,65 @@ def drop_raw_data(self, sample_idx: int, ms_level = 1) -> None: # Drop the raw data del self[sample_idx]._ms_unprocessed[ms_level] + def update_raw_file_locations(self, new_raw_folder): + """Update the raw file locations for all LCMS objects in the collection. + + This method updates the path to the original raw data files (.raw, .mzML, etc.) + that were used to create the processed HDF5 files stored in .corems folders. + + Parameters + ----------- + new_raw_folder : str or Path + The new folder location containing the raw data files (.raw, .mzML, etc.). + The method will look for raw files with the same base name as each sample. + + Raises + ------- + FileNotFoundError + If the new raw folder does not exist. + FileNotFoundError + If a raw file for a sample is not found in the new folder. + + Returns + -------- + None, but updates the raw_file_location for each LCMS object in the collection. + + Examples + -------- + If raw files were moved from /old/path/ to /new/path/: + >>> lcms_collection.update_raw_file_locations("/new/path/") + """ + from pathlib import Path + + if isinstance(new_raw_folder, str): + new_raw_folder = Path(new_raw_folder) + + if not new_raw_folder.exists(): + raise FileNotFoundError(f"Raw data folder does not exist: {new_raw_folder}") + + # Common raw file extensions + raw_extensions = ['.raw', '.mzML', '.mzml'] + + for sample_name in self.samples: + lcms_obj = self._lcms[sample_name] + + # Try to find the raw file with common extensions + new_raw_file = None + for ext in raw_extensions: + candidate = new_raw_folder / f"{sample_name}{ext}" + if candidate.exists(): + new_raw_file = candidate + break + + if new_raw_file is None: + raise FileNotFoundError( + f"Raw file for sample '{sample_name}' not found in {new_raw_folder}. " + f"Tried extensions: {', '.join(raw_extensions)}" + ) + + # Update the raw file location + lcms_obj.raw_file_location = new_raw_file + def collection_pivot_table(self, attribute = 'coll_mf_id', verbose = True): """Generate a pivot table of all regular and induced mass features in a collection. Default attribute presented is the mass feature ID, also diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 38d90dcca..b30bd1d95 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -580,13 +580,13 @@ def add_original_parser(self, mass_spectra, raw_file_path=None): if og_parser_type == "ImportMassSpectraThermoMSFileReader": # Check that the parser can be instantiated with the raw file path - parser = ImportMassSpectraThermoMSFileReader(raw_file_path) + parser_class = ImportMassSpectraThermoMSFileReader elif og_parser_type == "MZMLSpectraParser": # Check that the parser can be instantiated with the raw file path - parser = MZMLSpectraParser(raw_file_path) + parser_class = MZMLSpectraParser # Set the spectra parser class on the mass_spectra object so the spectra_parser property can be used with the original parser - mass_spectra.spectra_parser_class = parser.__class__ + mass_spectra.spectra_parser_class = parser_class return mass_spectra @@ -787,7 +787,7 @@ def get_lcms_collection(self, load_raw = False, load_light = True) -> LCMSCollec ncores = self._cores # Create a pool of workers (one for each core or sample, whichever is smaller) pool = multiprocessing.Pool(ncores) - # Load the LCMS objects in parallel - do not instantiate the original parser by default + # Load the LCMS objects in parallel use_original_parser = True args = [(sample, load_raw, load_light, use_original_parser) for sample in samples] lcms_objs = pool.starmap(self.get_lcms_obj, args) @@ -795,9 +795,9 @@ def get_lcms_collection(self, load_raw = False, load_light = True) -> LCMSCollec lcms_coll._lcms[sample_name] = lcms_obj elif self._cores == 1: - # Load the LCMS objects sequentially - do not instantiate the original parser by default + # Load the LCMS objects sequentially for sample_name in samples: - lcms_coll._lcms[sample_name] = self.get_lcms_obj(sample_name, load_raw=load_raw, load_light=load_light, use_original_parser=False) + lcms_coll._lcms[sample_name] = self.get_lcms_obj(sample_name, load_raw=load_raw, load_light=load_light, use_original_parser=True) else: raise ValueError("Number of cores must be greater than 0 and set on the ReadCoreMSHDFMassSpectraCollection object.") From 7ff398853342235a869c539a3ff4071967e567ff Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 2 Dec 2025 13:51:19 -0800 Subject: [PATCH 084/158] Add functionality for anchor features by relative intensity --- .../encapsulation/factory/processingSetting.py | 18 +++++++++++++----- corems/mass_spectra/calc/lc_calc.py | 15 ++++++++++++--- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/corems/encapsulation/factory/processingSetting.py b/corems/encapsulation/factory/processingSetting.py index c7af50348..fdde2fb07 100644 --- a/corems/encapsulation/factory/processingSetting.py +++ b/corems/encapsulation/factory/processingSetting.py @@ -1116,13 +1116,20 @@ class LCMSCollectionSettings: Default is True. mass_feature_anchor_technique: list, optional List of mass feature anchor techniques for retention time alignment. - Default is ['deconvoluted_mass_spectra']. + Default is ['absolute_intensity']. mass_feature_anchor_techniques_available: tuple, optional Tuple of available mass feature anchor techniques for retention time alignment. - Default is ('deconvoluted_mass_spectra', 'absolute_intensity'). - mass_feature_anchor_aboslute_intensity_threshold: int, optional + Default is ('deconvoluted_mass_spectra', 'absolute_intensity', 'relative_intensity'). + mass_feature_anchor_absolute_intensity_threshold: int, optional Absolute intensity threshold for mass feature anchor for retention time alignment. + Used when mass_feature_anchor_technique includes 'absolute_intensity'. Default is 10000. + mass_feature_anchor_relative_intensity_threshold: float, optional + Relative intensity threshold (0-1) for mass feature anchor for retention time alignment. + Removes the lower fraction of mass features by intensity from consideration. + For example, 0.6 removes the lower 60% of intensity features. + Used when mass_feature_anchor_technique includes 'relative_intensity'. + Default is 0.6. alignment_hold_out_fraction: float, optional Hold out fraction for testing retention time alignment. Default is 0.3. @@ -1153,8 +1160,9 @@ class LCMSCollectionSettings: # Settings for doing mass feature alignment _mass_feature_anchor_technique: list = dataclasses.field(default_factory=lambda: ["absolute_intensity"]) - mass_feature_anchor_techniques_available: tuple = ("deconvoluted_mass_spectra", "absolute_intensity") - mass_feature_anchor_aboslute_intensity_threshold: int = 10000 + mass_feature_anchor_techniques_available: tuple = ("deconvoluted_mass_spectra", "absolute_intensity", "relative_intensity") + mass_feature_anchor_absolute_intensity_threshold: int = 10000 + mass_feature_anchor_relative_intensity_threshold: float = 0.6 alignment_hold_out_fraction: float = 0.3 _alignment_acceptance_techinque: list = dataclasses.field(default_factory=lambda: ["fraction_improved", "mean_squared_error_improved"]) alignment_acceptance_techinques_available: tuple = ("fraction_improved", "mean_squared_error_improved") diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 50397d95f..c21349750 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -2563,9 +2563,18 @@ def get_anchor_mass_features(self, mf_df): "absolute_intensity" in self.parameters.lcms_collection.mass_feature_anchor_technique ): - # Drop features that have an absolute_intensity lower than the threshold - threshold = self.parameters.lcms_collection.mass_feature_anchor_aboslute_intensity_threshold - mf_df = mf_df[mf_df["absolute_intensity"] > threshold] + # Drop features that have an intensity lower than the threshold + threshold = self.parameters.lcms_collection.mass_feature_anchor_absolute_intensity_threshold + mf_df = mf_df[mf_df["intensity"] > threshold] + + if ( + "relative_intensity" + in self.parameters.lcms_collection.mass_feature_anchor_technique + ): + # Drop features in the lower fraction of intensities + threshold_quantile = self.parameters.lcms_collection.mass_feature_anchor_relative_intensity_threshold + intensity_threshold = mf_df["intensity"].quantile(threshold_quantile) + mf_df = mf_df[mf_df["intensity"] >= intensity_threshold] return mf_df From 1713785ffe297e53d8c470580496d7487db1443a Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 2 Dec 2025 14:49:55 -0800 Subject: [PATCH 085/158] Improve plot_cluster_outlier_frequency plot to use vectorized --- .../factory/processingSetting.py | 4 +-- corems/mass_spectra/calc/lc_calc.py | 25 +++++++++++-------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/corems/encapsulation/factory/processingSetting.py b/corems/encapsulation/factory/processingSetting.py index fdde2fb07..04810e20d 100644 --- a/corems/encapsulation/factory/processingSetting.py +++ b/corems/encapsulation/factory/processingSetting.py @@ -1122,7 +1122,7 @@ class LCMSCollectionSettings: Default is ('deconvoluted_mass_spectra', 'absolute_intensity', 'relative_intensity'). mass_feature_anchor_absolute_intensity_threshold: int, optional Absolute intensity threshold for mass feature anchor for retention time alignment. - Used when mass_feature_anchor_technique includes 'absolute_intensity'. + Used when mass_feature_anchor_technique includes 'relative_intensity'. Default is 10000. mass_feature_anchor_relative_intensity_threshold: float, optional Relative intensity threshold (0-1) for mass feature anchor for retention time alignment. @@ -1159,7 +1159,7 @@ class LCMSCollectionSettings: drop_isotopologues: bool = False # Settings for doing mass feature alignment - _mass_feature_anchor_technique: list = dataclasses.field(default_factory=lambda: ["absolute_intensity"]) + _mass_feature_anchor_technique: list = dataclasses.field(default_factory=lambda: ["relative_intensity"]) mass_feature_anchor_techniques_available: tuple = ("deconvoluted_mass_spectra", "absolute_intensity", "relative_intensity") mass_feature_anchor_absolute_intensity_threshold: int = 10000 mass_feature_anchor_relative_intensity_threshold: float = 0.6 diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index c21349750..04eb39c13 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -3348,7 +3348,7 @@ def cluster_inspection_plot(self, clu, return_fig = False): If cluster data haven't been added to the object yet """ - if not hasattr(self, 'mass_features_dataframe.cluster'): + if 'cluster' not in self.mass_features_dataframe.columns: raise ValueError( 'Cluster information is not yet added to mass_features_dataframe, must run add_consensus_mass_features() first' ) @@ -3482,24 +3482,27 @@ def plot_cluster_outlier_frequency(self, dim_list = ['mz', 'scan_time_aligned'], mintag = dim + '_outmin' mergelist.append(maxtag) mergelist.append(mintag) - sumdf[maxtag] = None - sumdf[mintag] = None - for i in range(len(sumdf)): - sumdf.loc[i, mintag] = sumdf[dim + '_mean'].iloc[i] - 3*sumdf[dim + '_std'].iloc[i] - sumdf.loc[i, maxtag] = sumdf[dim + '_mean'].iloc[i] + 3*sumdf[dim + '_std'].iloc[i] - ## If NaN shows up anywhere in dim_min, dim_max calculations, value is set to NaN and it's - ## not flagged. This happens when there's not enough values to compute median/std for that - ## dimension therefore can't have outliers + # Calculate outlier thresholds using vectorized operations + sumdf[mintag] = sumdf[dim + '_mean'] - 3*sumdf[dim + '_std'] + sumdf[maxtag] = sumdf[dim + '_mean'] + 3*sumdf[dim + '_std'] + ## If NaN shows up anywhere in dim_min, dim_max calculations, value is set to NaN and it's + ## not flagged. This happens when there's not enough values to compute median/std for that + ## dimension therefore can't have outliers ## add ranges to mfdf and identify mass features that fall outside the ranges - outdf = pd.merge(mfdf, sumdf[mergelist].dropna(), on = 'cluster') + # Merge without dropping NaN - we'll handle it per-dimension + outdf = pd.merge(mfdf, sumdf[mergelist], on = 'cluster') outtags = ['cluster'] for dim in dim_list: dimtag = dim + '_outlier' outtags.append(dimtag) + maxtag = dim + '_outmax' + mintag = dim + '_outmin' + # Only flag as outlier if thresholds are valid (not NaN) outdf[dimtag] = np.where( - ((outdf[dim] > outdf[dim + '_outmax'])) | ((outdf[dim] < outdf[dim + '_outmin'])), + (outdf[maxtag].notna() & outdf[mintag].notna()) & + (((outdf[dim] > outdf[maxtag])) | ((outdf[dim] < outdf[mintag]))), True, False ) From ad53b7c38965b647daf9a358fa50b1c26b859075 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 2 Dec 2025 17:30:15 -0800 Subject: [PATCH 086/158] Optimize fill_missing_cluster_features step --- corems/mass_spectra/calc/lc_calc.py | 302 ++++++++++++++++-------- corems/mass_spectra/factory/lc_class.py | 192 +++++++++------ 2 files changed, 316 insertions(+), 178 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 04eb39c13..bfc73c295 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -8,6 +8,7 @@ from sklearn.cluster import AgglomerativeClustering import matplotlib.pyplot as plt from ast import literal_eval +from tqdm import tqdm from corems.chroma_peak.factory.chroma_peak_classes import LCMSMassFeature from corems.mass_spectra.calc import SignalProcessing as sp @@ -459,16 +460,12 @@ def integrate_mass_features( mf_dict = self.induced_mass_features if len(mf_dict) == 0: raise ValueError( - "No mass features found, did you run search_for_targeted_mass_feature() first?" + "No induced mass features found, did you run fill_missing_cluster_features() first?" ) - if not any( - [mf.mass_spectrum is not None for mf in mf_dict.values()] - ): - raise ValueError( - "Mass spectrum must be associated with induced mass features, did you run add_associated_ms1() first?" - ) - ## remove not found induced mass features - mf_dict = {k:v for k, v in mf_dict.items() if v.mass_spectrum is not None} + + ## remove not found induced mass features by mz <= 0 (-99 indicator) + # also remove any where mz is nan + mf_dict = {k:v for k, v in mf_dict.items() if v.mz > 0 and not np.isnan(v.mz)} else: mf_dict = self.mass_features @@ -3523,120 +3520,214 @@ def plot_cluster_outlier_frequency(self, dim_list = ['mz', 'scan_time_aligned'], else: plt.show() - def _search_for_targeted_mass_features_in_sample(self, obj_idx, missingdf, threshold = 0.5, tol_flag = 0, inplace = True): + def _search_for_targeted_mass_features_in_sample(self, obj_idx, missingdf, cluster_dict, expand_on_miss=False, inplace=True): """ - Searches through a sample in a collection to find all missing mass - features across all clusters in the collection. + Helper method to search for missing mass features in a single sample. + + Internal method called by fill_missing_cluster_features() to perform + gap-filling for one sample in the collection. Parameters - ----------- - obj_idx : int + ---------- + obj_idx : int Index of the sample being processed - missingdf : pandas DataFrame - DataFrame containing information about the clusters with columns: + missingdf : pd.DataFrame + DataFrame containing cluster information with columns: 'cluster', 'sample_id_nunique', 'mz_min', 'mz_max', - 'scan_time_aligned_min', 'scan_time_aligned_max' extracted from - self.cluster_summary_dataframe and 'mz_min_allowed', + 'scan_time_aligned_min', 'scan_time_aligned_max', 'mz_min_allowed', 'mz_max_allowed', 'scan_time_aligned_min_allowed', - 'scan_time_aligned_max_allowed' computed in - search_for_targeted_mass_features_in_collection - threshold : float - Decimal representation of percent of sample included in a cluster - before the remaining samples are searched - tol_flag : 0 or 1 - Indicates whether expand cluster boundaries to search for mass - features near the boundary if none are found in the given - parameters. Defaults to 0 (False). - inplace : boolean - Indicates whethere to assign induced_mass_features attribute in - place or to return the result when the function is run in parallel + 'scan_time_aligned_max_allowed', 'missing_samples' + cluster_dict : dict + Pre-computed cluster feature dictionary to avoid recomputation + expand_on_miss : bool + If True, expands search window when no peak found initially + inplace : bool + If True, assigns induced_mass_features in place. If False, returns the + induced features dictionary (for multiprocessing) + + Returns + ------- + dict or None + If inplace=False, returns dictionary of induced mass features. + Otherwise returns None and updates object in place. """ - ## retrieve the cluster/deature dictionary to record induced features - cluster_dict = self.cluster_feature_dictionary + ## Use the pre-computed cluster dictionary passed as parameter ## to get clusters missing data based on sample index: sampledf = missingdf[ missingdf.missing_samples.apply(lambda x: obj_idx in x) ].reset_index(drop = True).copy() + # Skip if no missing features for this sample + if len(sampledf) == 0: + if not inplace: + return {} + return + self.load_raw_data(obj_idx, 1) - - ## print lines added to see what is coming out -# print(self) -# print(self[obj_idx]) -# print(self[obj_idx]._ms_unprocessed) - + ## this is the line that bugs due to _ms_unprocessed not having key 1 ms1df = self[obj_idx]._ms_unprocessed[1].copy() scan_df = self[obj_idx].scan_df[['scan', 'scan_time_aligned']] ms1df = pd.merge(ms1df, scan_df, on = 'scan') - for i in range(len(sampledf)): - mz_min = sampledf.mz_min.iloc[i] - mz_max = sampledf.mz_max.iloc[i] - st_min = sampledf.scan_time_aligned_min.iloc[i] - st_max = sampledf.scan_time_aligned_max.iloc[i] - # TODO: Adjust function to input queries as tuples so we can search - # for all targeted mass features at once instead of in a loop - set_id = 'c' + str(sampledf.cluster.iloc[i]) + '_' + str(i) + '_i' - found_feature = self[obj_idx].search_for_targeted_mass_feature( - ms1df, - mz_min, - mz_max, - st_min, - st_max, - set_id, - obj_idx = obj_idx, - st_aligned = True + # Pre-extract all values from sampledf to avoid repeated .iloc calls + clusters = sampledf.cluster.values + mz_mins = sampledf.mz_min.values + mz_maxs = sampledf.mz_max.values + st_mins = sampledf.scan_time_aligned_min.values + st_maxs = sampledf.scan_time_aligned_max.values + + if expand_on_miss: + mz_mins_allowed = sampledf.mz_min_allowed.values + mz_maxs_allowed = sampledf.mz_max_allowed.values + st_mins_allowed = sampledf.sta_min_allowed.values + st_maxs_allowed = sampledf.sta_max_allowed.values + + # Pre-filter ms1df to reduce search space + mz_global_min = mz_mins.min() + mz_global_max = mz_maxs.max() + st_global_min = st_mins.min() + st_global_max = st_maxs.max() + + if expand_on_miss: + mz_global_min = min(mz_global_min, mz_mins_allowed.min()) + mz_global_max = max(mz_global_max, mz_maxs_allowed.max()) + st_global_min = min(st_global_min, st_mins_allowed.min()) + st_global_max = max(st_global_max, st_maxs_allowed.max()) + + ms1df_filtered = ms1df[ + (ms1df.mz >= mz_global_min) & + (ms1df.mz <= mz_global_max) & + (ms1df.scan_time_aligned >= st_global_min) & + (ms1df.scan_time_aligned <= st_global_max) + ].copy() + + # Generate set_ids for all features + set_ids = [f'c{clusters[i]}_{i}_i' for i in range(len(sampledf))] + + # Use batch method to process all features at once + if expand_on_miss: + # First try with normal bounds + peaks_dict = self[obj_idx].search_for_targeted_mass_features_batch( + ms1df_filtered, + mz_mins, + mz_maxs, + st_mins, + st_maxs, + set_ids, + obj_idx=obj_idx, + st_aligned=True ) - - if tol_flag == 1 and found_feature.apex_scan == -99: - mz_min = sampledf.mz_min_allowed.iloc[i] - mz_max = sampledf.mz_max_allowed.iloc[i] - st_min = sampledf.sta_min_allowed.iloc[i] - st_max = sampledf.sta_max_allowed.iloc[i] - - found_feature = self[obj_idx].search_for_targeted_mass_feature( - ms1df, - mz_min, - mz_max, - st_min, - st_max, - set_id, - obj_idx = obj_idx, - st_aligned = True + + # Retry failed features with expanded bounds + failed_indices = [i for i, sid in enumerate(set_ids) if peaks_dict[sid].apex_scan == -99] + if failed_indices: + failed_ids = [set_ids[i] for i in failed_indices] + retry_peaks = self[obj_idx].search_for_targeted_mass_features_batch( + ms1df_filtered, + mz_mins_allowed[failed_indices], + mz_maxs_allowed[failed_indices], + st_mins_allowed[failed_indices], + st_maxs_allowed[failed_indices], + failed_ids, + obj_idx=obj_idx, + st_aligned=True ) - - self[obj_idx].induced_mass_features[found_feature.id] = found_feature - cluster_dict[sampledf.cluster.iloc[i]] += [set_id] - self[obj_idx].add_associated_ms1(induced_features = True) - # need to set drop_if_fail to false for induced features as they will fail + peaks_dict.update(retry_peaks) + else: + peaks_dict = self[obj_idx].search_for_targeted_mass_features_batch( + ms1df_filtered, + mz_mins, + mz_maxs, + st_mins, + st_maxs, + set_ids, + obj_idx=obj_idx, + st_aligned=True + ) + + # Assign peaks to induced_mass_features and cluster_dict + for i in range(len(sampledf)): + peak = peaks_dict[set_ids[i]] + self[obj_idx].induced_mass_features[peak.id] = peak + cluster_dict[clusters[i]] += [set_ids[i]] + + # TODO KRH: Let's try to avoid these steps unless asked for by parameters to pick up speed + if False: + self[obj_idx].add_associated_ms1(induced_features = True) + # need to set drop_if_fail to false for induced features as they will fail + self[obj_idx].add_peak_metrics(induced_features = True) + self[obj_idx].integrate_mass_features(drop_if_fail = False, induced_features = True) - self[obj_idx].add_peak_metrics(induced_features = True) - if inplace == False: + if not inplace: return self[obj_idx].induced_mass_features - def search_for_targeted_mass_features_in_collection(self, threshold = 0.5, tol_flag = 0): - ''' - Iterates through a collection to find all relevant induced mass - features and assigns them to the induced_mass_features attribute + def fill_missing_cluster_features(self, min_cluster_presence=0.5, expand_on_miss=False): + """ + Gap-filling for consensus mass features across collection samples. + + For clusters present in multiple samples but missing from others, searches + raw MS1 data to find peaks in expected m/z and retention time windows. This + creates "induced" mass features for peaks that exist in the data but weren't + detected in the initial peak detection. + + Must be run after add_consensus_mass_features(). Results are accessible via + induced_mass_features_dataframe property and included in collection_pivot_table + and collection_consensus_report outputs. Parameters - ----------- - threshold : float - Decimal representation of percent of sample included in a cluster - before the remaining samples are searched - tol_flag : 0 or 1 - Indicates whether expand cluster boundaries to search for mass - features near the boundary if none are found in the given - parameters. Defaults to 0 (False). - ''' + ---------- + min_cluster_presence : float, optional + Minimum fraction of samples (0-1) that must contain a cluster before + gap-filling is attempted for remaining samples. Default is 0.5 (50%). + Higher values focus on more prevalent features; lower values attempt + gap-filling for more sparse features. + expand_on_miss : bool, optional + If True, expands search window using consensus_mz_tol_ppm and + consensus_rt_tol when no peak is found in the initial cluster boundaries. + Default is False. + + Returns + ------- + None + Updates induced_mass_features attribute for each LCMSBase object and + combines them into induced_mass_features_dataframe. + + Raises + ------ + ValueError + If cluster_summary_dataframe is not set (must run add_consensus_mass_features first). + + Notes + ----- + - Loads raw MS1 data for each sample, which may be memory intensive + - Induced features are integrated and metrics calculated automatically + - Processing can be parallelized using parameters.lcms_collection.cores + + See Also + -------- + add_consensus_mass_features : Creates consensus features before gap-filling + collection_pivot_table : Includes both regular and induced features + collection_consensus_report : Reports on complete feature matrix + """ + + # Validate prerequisites + if not hasattr(self, 'cluster_summary_dataframe') or self.cluster_summary_dataframe is None: + raise ValueError( + "cluster_summary_dataframe not found. Must run add_consensus_mass_features() first." + ) + + # Validate parameters + if not 0 <= min_cluster_presence <= 1: + raise ValueError("min_cluster_presence must be between 0 and 1") summarydf = self.cluster_summary_dataframe mfdf = self.mass_features_dataframe sample_ct = len(self.samples) + # Identify clusters present in sufficient samples but not all samples missingdf = summarydf[[ 'cluster', 'sample_id_nunique', @@ -3645,8 +3736,14 @@ def search_for_targeted_mass_features_in_collection(self, threshold = 0.5, tol_f 'scan_time_aligned_min', 'scan_time_aligned_max' ]] - missingdf = missingdf[missingdf.sample_id_nunique > threshold*(sample_ct)] + missingdf = missingdf[missingdf.sample_id_nunique > min_cluster_presence * sample_ct] missingdf = missingdf[missingdf.sample_id_nunique != sample_ct] + + # Check if there are any clusters to gap-fill + if len(missingdf) == 0: + print(f"No clusters found requiring gap-filling with min_cluster_presence={min_cluster_presence}") + print(f"All clusters are either present in all samples or below the {min_cluster_presence*100:.0f}% threshold.") + return missingdf['missing_samples'] = None for c in missingdf.cluster.to_numpy(): @@ -3656,16 +3753,21 @@ def search_for_targeted_mass_features_in_collection(self, threshold = 0.5, tol_f ) missingdf['missing_samples'] = missingdf.missing_samples.apply(literal_eval) + # Calculate expanded search windows for expand_on_miss option mz_clu_tol = self.parameters.lcms_collection.consensus_mz_tol_ppm * 1e-6 rt_clu_tol = self.parameters.lcms_collection.consensus_rt_tol - missingdf['mz_max_allowed'] = missingdf.mz_max + mz_clu_tol*missingdf.mz_max - missingdf['mz_min_allowed'] = missingdf.mz_min - mz_clu_tol*missingdf.mz_min - missingdf['sta_max_allowed'] = missingdf.scan_time_aligned_max + rt_clu_tol*missingdf.scan_time_aligned_max - missingdf['sta_min_allowed'] = missingdf.scan_time_aligned_min - rt_clu_tol*missingdf.scan_time_aligned_min + missingdf['mz_max_allowed'] = missingdf.mz_max + mz_clu_tol * missingdf.mz_max + missingdf['mz_min_allowed'] = missingdf.mz_min - mz_clu_tol * missingdf.mz_min + missingdf['sta_max_allowed'] = missingdf.scan_time_aligned_max + rt_clu_tol * missingdf.scan_time_aligned_max + missingdf['sta_min_allowed'] = missingdf.scan_time_aligned_min - rt_clu_tol * missingdf.scan_time_aligned_min + # Compute cluster dictionary once to avoid recomputing for each sample + cluster_dict = self.cluster_feature_dictionary + + # Process each sample to search for missing features if self.parameters.lcms_collection.cores == 1: - for i in range(sample_ct): - self._search_for_targeted_mass_features_in_sample(i, missingdf, threshold, tol_flag) + for i in tqdm(range(sample_ct), desc="Gap-filling samples", unit="sample"): + self._search_for_targeted_mass_features_in_sample(i, missingdf, cluster_dict, expand_on_miss) if self.parameters.lcms_collection.cores > 1: if self.parameters.lcms_collection.cores > len(self): @@ -3675,10 +3777,10 @@ def search_for_targeted_mass_features_in_collection(self, threshold = 0.5, tol_f pool = multiprocessing.Pool(ncores) mp_result = pool.starmap( self._search_for_targeted_mass_features_in_sample, - [(x, missingdf, threshold, tol_flag, False) for x in range(sample_ct)] + [(x, missingdf, cluster_dict, expand_on_miss, False) for x in range(sample_ct)] ) - for i in range(sample_ct): + for i in tqdm(range(sample_ct), desc="Collecting gap-filled features", unit="sample"): self[i].induced_mass_features = mp_result[i] self._combine_mass_features(induced_features = True) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 0b08ecbfe..43d4ca5e8 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1165,6 +1165,106 @@ def plot_composite_mz_features(self, binsize = 1e-4, ph_int_min_thresh = 0.001, else: plt.show() + def search_for_targeted_mass_features_batch( + self, + ms1df, + mz_mins, + mz_maxs, + st_mins, + st_maxs, + set_ids, + obj_idx=0, + st_aligned=False + ): + """ + Returns multiple LCMSMassFeatures from a specific sample within specific mass and time ranges. + Vectorized batch version of search_for_targeted_mass_feature for improved performance. + + Parameters + ----------- + ms1df : pd.DataFrame + Dataframe containing all the possible MS1 values to consider, collected by calling _ms_unprocessed[1] on the sample. + mz_mins : np.ndarray + Array of lower bounds of m/z values to use to find peaks. + mz_maxs : np.ndarray + Array of upper bounds of m/z values to use to find peaks. + st_mins : np.ndarray + Array of lower bounds of scan times to use to find peaks. + st_maxs : np.ndarray + Array of upper bounds of scan times to use to find peaks. + set_ids : np.ndarray or list + Array of strings used as IDs in LCMSMassFeatures. + obj_idx : int + Identifies index of sample in a collection. Defaults to 0. + st_aligned : bool + Whether to use scan_time_aligned or scan_time. Defaults to False. + + Returns + -------- + dict + Dictionary mapping set_id to LCMSMassFeature objects. + + Raises + ------ + ValueError + If appropriate scan time data is not contained in ms1df or if array lengths don't match. + """ + # Validate inputs + n_features = len(mz_mins) + if not all(len(arr) == n_features for arr in [mz_maxs, st_mins, st_maxs, set_ids]): + raise ValueError("All input arrays must have the same length") + + # Validate scan time column + time_col = 'scan_time_aligned' if st_aligned else 'scan_time' + if time_col not in ms1df.columns: + raise ValueError(f"{time_col} not contained in ms1df") + + # Pre-extract columns for faster access + mz_vals = ms1df.mz.values + st_vals = ms1df[time_col].values + scan_vals = ms1df.scan.values + intensity_vals = ms1df.intensity.values + + # Process all features + results = {} + for i in range(n_features): + # Vectorized filtering + mask = ( + (mz_vals >= mz_mins[i]) & (mz_vals <= mz_maxs[i]) & + (st_vals >= st_mins[i]) & (st_vals <= st_maxs[i]) + ) + + if not mask.any(): + row_dict = { + 'apex_scan': -99, + 'mz': np.nan, + 'intensity': np.nan, + 'retention_time': np.nan, + 'persistence': np.nan, + 'id': set_ids[i] + } + else: + # Find max intensity within filtered region + filtered_intensities = intensity_vals[mask] + max_idx = np.argmax(filtered_intensities) + + # Get indices of filtered data + filtered_indices = np.where(mask)[0] + peak_idx = filtered_indices[max_idx] + + row_dict = { + 'apex_scan': scan_vals[peak_idx], + 'mz': mz_vals[peak_idx], + 'intensity': intensity_vals[peak_idx], + 'retention_time': st_vals[peak_idx], + 'persistence': np.nan, + 'id': set_ids[i] + } + + results[set_ids[i]] = LCMSMassFeature(self, **row_dict) + + return results + def search_for_targeted_mass_feature( self, ms1df, @@ -1211,66 +1311,18 @@ def search_for_targeted_mass_feature( Warning If appropriate scan time data is not contained in ms1df. """ - - if st_aligned == True: - if not 'scan_time_aligned' in ms1df.columns: - raise ValueError( - "Aligned scan times not contained in ms1df. Merge ms1df with scan_df on 'scan' to import aligned scan times." - ) - else: - if not 'scan_time' in ms1df.columns: - raise ValueError( - "Scan times not contained in ms1df. Merge ms1df with scan_df on 'scan' to import scan times." - ) - - mzfit = ms1df[( - ms1df.mz >= mz_min - ) & ( - ms1df.mz <= mz_max - )].reset_index(drop = True) - - if st_aligned == True: - mzfit_st = mzfit.scan_time_aligned - else: - mzfit_st = mzfit.scan_time - - rtfit = mzfit[( - mzfit_st >= st_min - ) & ( - mzfit_st <= st_max - )] - - if len(rtfit) == 0: - row_dict = { - 'apex_scan': -99, - 'mz': np.nan, - 'intensity': np.nan, - 'retention_time': np.nan, - 'persistence': np.nan, - 'id': set_id - } - else: - inducedpeak = rtfit[rtfit.intensity == rtfit.intensity.max()] - if st_aligned == True: - rt_induced = inducedpeak.scan_time_aligned.values[0] - else: - rt_induced = inducedpeak.scan_time.values[0] - - row_dict = { - 'apex_scan': inducedpeak.scan.values[0], - 'mz': inducedpeak.mz.values[0], - 'intensity': inducedpeak.intensity.values[0], - 'retention_time': rt_induced, - 'persistence': np.nan, - 'id': set_id - } - - if self.__class__.__name__ == 'LCMSBase': - output = LCMSMassFeature(self, **row_dict) - elif self.__class__.__name__ == 'LCMSCollection': - output = LCMSMassFeature(self[obj_idx], **row_dict) - - return output + # Convert single feature to arrays and call batch method + results = self.search_for_targeted_mass_features_batch( + ms1df, + np.array([mz_min]), + np.array([mz_max]), + np.array([st_min]), + np.array([st_max]), + [set_id], + obj_idx=obj_idx, + st_aligned=st_aligned + ) + return results[set_id] def __len__(self): @@ -2071,21 +2123,6 @@ def manifest(self): def manifest_dataframe(self): return pd.DataFrame(self._manifest_dict).T - @property - def consensus_mass_feature_dataframe(self): - df = self.mass_features_dataframe - #TODO KRH: build this out - - # Check if mass features are clustered - if 'cluster' not in df.columns: - return None - else: - # Group by cluster and summarize median mz, median scan_time, min max and median intensity, and count - cluster_summary = df.groupby('cluster').agg({'mz': 'median', 'scan_time': 'median', 'intensity': ['min', 'max', 'median'], 'mf_id': 'count'}) - - # Clean up column names - cluster_summary.columns = ['_'.join(col).strip() for col in cluster_summary.columns.values] - @property def raw_files(self): """Returns a list of raw files in the collection.""" @@ -2095,7 +2132,6 @@ def raw_files(self): def cluster_feature_dictionary(self): """Generates a dictionary with clusters for keys and mass feature IDs as entries""" df = self.mass_features_dataframe - cluster_dict = {} - for c in range(df.cluster.max()): - cluster_dict[c] = df[df.cluster == 0].index.tolist() + # Use groupby for much better performance than iterating through all clusters + cluster_dict = df.groupby('cluster').apply(lambda x: x.index.tolist()).to_dict() return cluster_dict \ No newline at end of file From 8ba6292c774a20c866f5bfb39f4fe8e8f72db419 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 2 Dec 2025 17:35:45 -0800 Subject: [PATCH 087/158] Encapsulate the minimum samples per cluster parameter --- .../factory/processingSetting.py | 13 ++++++++++- corems/mass_spectra/calc/lc_calc.py | 22 +++++++++---------- corems/mass_spectra/factory/lc_class.py | 1 - 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/corems/encapsulation/factory/processingSetting.py b/corems/encapsulation/factory/processingSetting.py index 04810e20d..f7dee940f 100644 --- a/corems/encapsulation/factory/processingSetting.py +++ b/corems/encapsulation/factory/processingSetting.py @@ -1153,6 +1153,14 @@ class LCMSCollectionSettings: Retention time tolerance for consensus mass feature alignment, in minutes. Default is 0.2. consensus_partition_size: int, optional Partition size for consensus mass feature alignment. Default is 5000. + consensus_min_sample_fraction : float, optional + Minimum fraction of samples (0-1) that must contain a cluster. + Used for filtering consensus features and for gap-filling threshold. + Default is 0.5 (50%). Higher values focus on more prevalent features. + gap_fill_expand_on_miss : bool, optional + If True, expands search window using consensus_mz_tol_ppm and consensus_rt_tol + when no peak is found in the initial cluster boundaries during gap-filling. + Default is False. """ # Settings for general processing cores = 1 @@ -1175,7 +1183,10 @@ class LCMSCollectionSettings: consensus_rt_tol = 0.3 consensus_partition_size = 10000 filter_consensus_mass_features = True - consensus_min_sample_fraction = 0.2 + consensus_min_sample_fraction = 0.5 + + # Gap-filling settings + gap_fill_expand_on_miss: bool = True def __post_init__(self): self.consensus_mz_tol_ppm = self.alignment_mz_tol_ppm diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index bfc73c295..0339e2f12 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -3664,7 +3664,7 @@ def _search_for_targeted_mass_features_in_sample(self, obj_idx, missingdf, clust if not inplace: return self[obj_idx].induced_mass_features - def fill_missing_cluster_features(self, min_cluster_presence=0.5, expand_on_miss=False): + def fill_missing_cluster_features(self): """ Gap-filling for consensus mass features across collection samples. @@ -3679,15 +3679,11 @@ def fill_missing_cluster_features(self, min_cluster_presence=0.5, expand_on_miss Parameters ---------- - min_cluster_presence : float, optional - Minimum fraction of samples (0-1) that must contain a cluster before - gap-filling is attempted for remaining samples. Default is 0.5 (50%). - Higher values focus on more prevalent features; lower values attempt - gap-filling for more sparse features. - expand_on_miss : bool, optional - If True, expands search window using consensus_mz_tol_ppm and - consensus_rt_tol when no peak is found in the initial cluster boundaries. - Default is False. + None + Uses parameters from self.parameters.lcms_collection: + - consensus_min_sample_fraction: Minimum fraction of samples (0-1) that must contain + a cluster before gap-filling is attempted + - gap_fill_expand_on_miss: If True, expands search window when no peak is found Returns ------- @@ -3719,9 +3715,13 @@ def fill_missing_cluster_features(self, min_cluster_presence=0.5, expand_on_miss "cluster_summary_dataframe not found. Must run add_consensus_mass_features() first." ) + # Get parameters from settings + min_cluster_presence = self.parameters.lcms_collection.consensus_min_sample_fraction + expand_on_miss = self.parameters.lcms_collection.gap_fill_expand_on_miss + # Validate parameters if not 0 <= min_cluster_presence <= 1: - raise ValueError("min_cluster_presence must be between 0 and 1") + raise ValueError("consensus_min_sample_fraction must be between 0 and 1") summarydf = self.cluster_summary_dataframe mfdf = self.mass_features_dataframe diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 43d4ca5e8..8cfaa1f48 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -2132,6 +2132,5 @@ def raw_files(self): def cluster_feature_dictionary(self): """Generates a dictionary with clusters for keys and mass feature IDs as entries""" df = self.mass_features_dataframe - # Use groupby for much better performance than iterating through all clusters cluster_dict = df.groupby('cluster').apply(lambda x: x.index.tolist()).to_dict() return cluster_dict \ No newline at end of file From 00888bbf402fad22b69228a2dd7d3502a54d54a6 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 2 Dec 2025 18:13:51 -0800 Subject: [PATCH 088/158] Optimize a bit more and clean up docstrings --- corems/mass_spectra/calc/lc_calc.py | 174 +++++++++++++++++- corems/mass_spectra/factory/lc_class.py | 22 ++- .../nmdc/lipidomics/lipidomics_collection.py | 54 ++++-- .../nmdc/lipidomics/lipidomics_workflow.py | 12 +- 4 files changed, 224 insertions(+), 38 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 0339e2f12..1c31a0b2e 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -7,7 +7,6 @@ from sklearn.svm import SVR from sklearn.cluster import AgglomerativeClustering import matplotlib.pyplot as plt -from ast import literal_eval from tqdm import tqdm from corems.chroma_peak.factory.chroma_peak_classes import LCMSMassFeature @@ -2783,6 +2782,59 @@ def align_lcms_objects(self, overwrite=False): new_scan_info["scan_time_aligned"] = new_scan_info["scan_time"] def add_consensus_mass_features(self): + """ + Create consensus mass features by clustering aligned features across samples. + + This method clusters mass features from all samples in the collection based on + their m/z and aligned retention time proximity. Features that cluster together + across samples are assigned a common cluster ID, creating consensus features + that represent the same compound detected across multiple samples. + + The clustering process: + 1. Partitions features by m/z to avoid large sparse matrices and enable parallelization + 2. Clusters features within each partition using hierarchical clustering + 3. Merges partition-boundary clusters that represent the same feature + 4. Filters out clusters not present in minimum fraction of samples + + Must be run after align_lcms_objects(). Results are stored in the + mass_features_dataframe with a 'cluster' column added. + + Parameters + ---------- + None + Uses parameters from self.parameters.lcms_collection: + - consensus_mz_tol_ppm: m/z tolerance for clustering (ppm) + - consensus_rt_tol: retention time tolerance for clustering (minutes) + - consensus_partition_size: target partition size for managing memory and parallelization + - consensus_min_sample_fraction: minimum fraction of samples a cluster + must appear in to be retained (0-1) + - cores: number of CPU cores to use for parallel partition processing + + Returns + ------- + None + Updates self.mass_features_dataframe in place by adding 'cluster' column + and filtering to retain only clusters meeting minimum sample presence. + + Raises + ------ + ValueError + If mass features have not been aligned (run align_lcms_objects() first). + + Notes + ----- + - Partitioning prevents memory issues with large sparse distance matrices + - Each partition is processed in parallel (up to cores limit) + - Clusters not meeting consensus_min_sample_fraction are automatically removed + - Access cluster_summary_dataframe property for summary statistics + - Use fill_missing_cluster_features() for gap-filling after clustering + + See Also + -------- + align_lcms_objects : Aligns retention times before consensus clustering + cluster_summary_dataframe : Property that generates summary statistics for clusters + fill_missing_cluster_features : Gap-fill missing features in clusters + """ # Get the combined mass features from all LCMS objects, keep the original index as a separate column combined_mfs = self.mass_features_dataframe.copy() combined_mfs["coll_mf_id"] = combined_mfs.index @@ -2869,18 +2921,124 @@ def add_consensus_mass_features(self): mfs_with_clusters.set_index('coll_mf_id', inplace = True) self.mass_features_dataframe = mfs_with_clusters + # Filter out clusters that don't meet minimum sample fraction + self._filter_clusters_by_sample_presence() + # TODO KRH: Deal with isomers better? Pool them together and then split them out using samples with 2 as the template? + + def _filter_clusters_by_sample_presence(self): + """ + Filter out clusters that don't meet the minimum sample fraction threshold. + + Removes clusters (and their associated mass features) from the mass_features_dataframe + if they don't appear in at least consensus_min_sample_fraction of samples. + + This is called automatically at the end of add_consensus_mass_features(). + + Returns + ------- + None + Updates self.mass_features_dataframe in place by removing clusters that don't + meet the minimum sample presence threshold. + """ + if self.mass_features_dataframe is None or len(self.mass_features_dataframe) == 0: + return + + min_sample_fraction = self.parameters.lcms_collection.consensus_min_sample_fraction + + # Validate parameter + if not 0 <= min_sample_fraction <= 1: + raise ValueError("consensus_min_sample_fraction must be between 0 and 1") + + # Calculate minimum number of samples required + total_samples = len(self.samples) + min_samples_required = min_sample_fraction * total_samples + + # Count unique samples per cluster + cluster_sample_counts = ( + self.mass_features_dataframe.groupby('cluster')['sample_id'] + .nunique() + .reset_index(name='sample_count') + ) + + # Identify clusters to keep + clusters_to_keep = cluster_sample_counts[ + cluster_sample_counts['sample_count'] > min_samples_required + ]['cluster'].values + + # Filter mass features dataframe + self.mass_features_dataframe = self.mass_features_dataframe[ + self.mass_features_dataframe['cluster'].isin(clusters_to_keep) + ] def summarize_clusters(self): """ - Summarize the clusters of mass features by median attributes + Generate summary statistics for consensus mass feature clusters. + + Computes aggregate statistics (median, mean, std, min, max) for each cluster + across all samples. Combines both regular mass features and induced mass features + (from gap-filling) when available to provide complete cluster statistics. + + Must be run after add_consensus_mass_features() which creates the cluster assignments. + Results are stored in cluster_summary_dataframe property and used by plotting methods. + + Parameters + ---------- + None + Operates on self.mass_features_dataframe and self.induced_mass_features_dataframe. + Both must contain 'cluster' column. + + Returns + ------- + :obj:`~pandas.DataFrame` or None + DataFrame with one row per cluster containing summary statistics: + - cluster: cluster ID + - mz_{median,mean,std,max,min}: m/z statistics + - scan_time_aligned_{median,mean,std,max,min}: aligned RT statistics + - half_height_width_{median,mean,std,max,min}: peak width statistics + - tailing_factor_{median,mean,std,max,min}: peak shape statistics + - dispersity_index_{median,mean,std,max,min}: peak quality statistics + - sample_id_nunique: number of unique samples containing the cluster + - intensity_{max,median,mean,std,min}: intensity statistics + - persistence_{max,median,mean,std,min}: persistence statistics + + Returns None if mass_features_dataframe is empty. + + Notes + ----- + - Summary DataFrame is automatically stored in cluster_summary_dataframe property + - Includes both regular and induced (gap-filled) mass features when available + - Used by plotting methods: plot_consensus_mz_features, plot_mz_features_per_cluster + - Sample count (sample_id_nunique) indicates cluster prevalence across samples + - Filters applied by consensus_min_sample_fraction affect which clusters appear + + See Also + -------- + add_consensus_mass_features : Creates clusters before summarization + fill_missing_cluster_features : Creates induced mass features via gap-filling + plot_consensus_mz_features : Visualizes cluster summaries + plot_mz_features_per_cluster : Shows cluster size distribution """ # First check if there are minimum columns in the features dataframe if len(self.mass_features_dataframe.columns) < 1: return None + # Combine regular and induced mass features + mf_df = self.mass_features_dataframe.copy() + mf_df = mf_df.reset_index(drop=False) + + # Check if induced mass features are available and combine them + if self.induced_mass_features_dataframe is not None and len(self.induced_mass_features_dataframe) > 0: + imf_df = self.induced_mass_features_dataframe.copy() + imf_df = imf_df.reset_index(drop=False) + # Cluster column already extracted by induced_mass_features_dataframe property + # Combine regular and induced features + mf_df = pd.concat([mf_df, imf_df], axis=0) + mf_df = mf_df.reset_index(drop=True) + mf_df['cluster'] = mf_df['cluster'].astype(int) + summary_df = ( - self.mass_features_dataframe.groupby("cluster") + mf_df.groupby("cluster") .agg( { "mz": ["median", "mean", "std", "max", "min"], @@ -3745,13 +3903,13 @@ def fill_missing_cluster_features(self): print(f"All clusters are either present in all samples or below the {min_cluster_presence*100:.0f}% threshold.") return - missingdf['missing_samples'] = None + # Find which samples are missing for each cluster + missing_samples_list = [] for c in missingdf.cluster.to_numpy(): cludf = mfdf[mfdf.cluster == c] - missingdf.loc[c, 'missing_samples'] = str( - [x for x in mfdf.sample_id.unique() if x not in cludf.sample_id.unique()] - ) - missingdf['missing_samples'] = missingdf.missing_samples.apply(literal_eval) + missing = [x for x in mfdf.sample_id.unique() if x not in cludf.sample_id.unique()] + missing_samples_list.append(missing) + missingdf['missing_samples'] = missing_samples_list # Calculate expanded search windows for expand_on_miss option mz_clu_tol = self.parameters.lcms_collection.consensus_mz_tol_ppm * 1e-6 diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 8cfaa1f48..372ef1014 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1968,8 +1968,7 @@ def collection_pivot_table(self, attribute = 'coll_mf_id', verbose = True): mf_pivot.reset_index(inplace = True) imf_pivot = self.induced_mass_features_dataframe.copy() imf_pivot.reset_index(inplace = True) - for j in range(len(imf_pivot)): - imf_pivot.loc[j, 'cluster'] = int(imf_pivot.loc[j].coll_mf_id.split('_')[1][1:]) + # Cluster column already extracted by induced_mass_features_dataframe property mf_pivot = pd.concat([mf_pivot, imf_pivot], axis = 0) mf_pivot.reset_index(drop = True, inplace = True) mf_pivot['cluster'] = mf_pivot['cluster'].astype(int) @@ -2012,24 +2011,26 @@ def collection_consensus_report(self, how = 'intensity'): mf_df.reset_index(inplace = True) imf_df = self.induced_mass_features_dataframe.copy() imf_df.reset_index(inplace = True) - for j in range(len(imf_df)): - imf_df.loc[j, 'cluster'] = int(imf_df.loc[j].coll_mf_id.split('_')[1][1:]) + # Cluster column already extracted by induced_mass_features_dataframe property mf_df = pd.concat([mf_df, imf_df], axis = 0) mf_df.reset_index(drop = True, inplace = True) mf_df['cluster'] = mf_df['cluster'].astype(int) + # If how == intensity, find the sample with highest intensity in each cluster and return that mass feature's data if how == 'intensity': int_table = self.collection_pivot_table(attribute = 'intensity', verbose = False).idxmax(axis = 1) id_list = [] for i in range(len(int_table)): id_list.append( mf_df[ - (mf_df.sample_id == int_table[i]) & (mf_df.cluster == int_table.index[i]) + (mf_df.sample_id == int_table.iloc[i]) & (mf_df.cluster == int_table.index[i]) ].coll_mf_id.values[0] ) return mf_df[mf_df.coll_mf_id.isin(id_list)].sort_values(by = 'cluster').set_index('cluster') + # If how == mean or median, group by cluster and calculate mean or median for each attribute elif how == 'mean' or how == 'median': + #TODO KRH: Clean up this section ## will have to go back and check mf_df.dtypes to see what ends up on list ## this format removes int columns like 'sample_id' and 'cluster' l = mf_df.select_dtypes(include='float64').columns.tolist() @@ -2088,6 +2089,17 @@ def mass_features_dataframe(self, df): @property def induced_mass_features_dataframe(self): self._check_mass_features_df(induced_features = True) + if self._combined_induced_mass_features is not None and len(self._combined_induced_mass_features) > 0: + # Extract cluster ID from coll_mf_id if not already present + if 'cluster' not in self._combined_induced_mass_features.columns: + imf_df = self._combined_induced_mass_features.copy() + imf_df = imf_df.reset_index(drop=False) + # Extract cluster ID from coll_mf_id format: "sample_id_cCluster_idx_i" + imf_df['cluster'] = imf_df['coll_mf_id'].apply( + lambda x: int(x.split('_')[1][1:]) + ) + imf_df = imf_df.set_index('coll_mf_id') + self._combined_induced_mass_features = imf_df return self._combined_induced_mass_features @induced_mass_features_dataframe.setter diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 5d01a089f..48e795120 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -1,16 +1,22 @@ from pathlib import Path import time +import cProfile +import pstats from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectraCollection if __name__ == "__main__": + # Start profiling everything + profiler = cProfile.Profile() + profiler.enable() + # Set the path to the collection of LCMS runs (previously processed) - collection_path = Path("/Users/heal742/Library/CloudStorage/OneDrive-PNNL/Documents/_DMS_data/_NMDC/_blanchard_lipidomics/mini_collection_test_out") + collection_path = Path("/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test_out2") # Path to manifest file - manifest_file = Path("/Users/heal742/Library/CloudStorage/OneDrive-PNNL/Documents/_DMS_data/_NMDC/_blanchard_lipidomics/mini_collection_test_out/manifest.csv") + manifest_file = Path("/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test_out2/manifest.csv") # Set the number of cores to use for loading the data (the parser is parallelized) - ncores = 5 + ncores = 6 # Instantiate the parser parser = ReadCoreMSHDFMassSpectraCollection( @@ -18,7 +24,7 @@ manifest_file = manifest_file, cores = ncores ) - print("Loading LCMS collection with", len(parser.manifest), "samples using", ncores, " cores") + print("Loading LCMS collection with", len(parser.manifest), "samples using", ncores, "cores") # Load the LCMS collection (minimally load the data) start_time = time.time() @@ -26,13 +32,17 @@ print("Time to load LCMS collection ", time.time() - start_time, "seconds -", len(lcms_collection), " LCMS runs and ", ncores, " cores") #10s for 7 samples, 10 cores; 162s for 70 samples, 10 cores + # Update raw file locations if needed + lcms_collection.update_raw_file_locations( + new_raw_folder = "/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test2" + ) + + """ # Check and demonstrate the parsers' ability to load raw data lcms_collection.load_raw_data(sample_idx=0, ms_level=1) assert lcms_collection[0]._ms_unprocessed[1] is not None, "Raw data for MS1 should be loaded successfully." lcms_collection.drop_raw_data(sample_idx=0, ms_level=1) - - # Set flag to call _drop_isotopologue() when running _check_mass_features_df() - lcms_collection.parameters.lcms_collection.drop_isotopologues = True + """ print("Number of total mass features: ", len(lcms_collection.mass_features_dataframe)) # Align the LCMS runs between each other @@ -42,22 +52,23 @@ print("Time to align LCMS collection: ", time.time() - start_time, "seconds") #1.5s for 7 samples; 15s for 70 samples - # Make some plots + """# Make some plots lcms_collection.plot_tics(type="both") lcms_collection.plot_alignments() - # TODO: Think about other plots that would be useful to have here for assessing the quality of the data and alignment + """ # Make consensus mass features from the consolidated mass features start_time = time.time() lcms_collection.add_consensus_mass_features() - # THIS FUNCTION NEEDS WORK AND NEEDS MECHANISM TO EVALUATE THE RESULTS (plots? reports?) - print("Time to roll up consensus mass features: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") + print("Time to generate consensus mass features: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") + """ # Make some more plots lcms_collection.plot_mz_features_across_samples() lcms_collection.plot_mz_features_per_cluster() lcms_collection.plot_consensus_mz_features() ## zoomed out lcms_collection.plot_consensus_mz_features(xb = 10, xt = 15, yb = 500, yt = 600) ## zoomed in - lcms_collection.cluster_inspection_plot(11391) + lcms_collection.cluster_inspection_plot(0) + dim_list = [ 'mz', 'scan_time_aligned', @@ -68,13 +79,16 @@ 'persistence' ] lcms_collection.plot_cluster_outlier_frequency(dim_list, clu_size_thresh = 0.25) + """ + # Gap fill missing cluster features + start_time = time.time() + lcms_collection.fill_missing_cluster_features() + print("Time to gap fill missing cluster features: ", time.time() - start_time, "seconds, using", ncores, " cores") - lcms_collection.search_for_missing_mass_features_in_collection() - lcms_collection.collection_pivot_table(attribute = 'mz') - lcms_collection.collection_consensus_report(how = 'intensity') - lcms_collection.collection_consensus_report(how = 'mean') - lcms_collection.collection_consensus_report(how = 'median') - - #TODO: Add code to load and save information about chromatographic settings + results = lcms_collection.collection_pivot_table() + results1 = lcms_collection.collection_pivot_table(attribute = 'intensity') + results2 = lcms_collection.collection_consensus_report(how = 'intensity') + results3 = lcms_collection.collection_consensus_report(how = 'median') + #TODO: Add code to save and load collection to HDF5 file - #TODO: Generate report of summarize_clusters as a table with both regular and induced mass features \ No newline at end of file + #TODO: Add code to deal with annotations of features in the collection context diff --git a/support_code/nmdc/lipidomics/lipidomics_workflow.py b/support_code/nmdc/lipidomics/lipidomics_workflow.py index 028935d43..bf0a79b6f 100644 --- a/support_code/nmdc/lipidomics/lipidomics_workflow.py +++ b/support_code/nmdc/lipidomics/lipidomics_workflow.py @@ -198,6 +198,7 @@ def add_mass_features(myLCMSobj, scan_translator): # Count and report how many mass features are left after integration print("Number of mass features after integration: ", len(myLCMSobj.mass_features)) #filter_and_plot_mass_features(myLCMSobj) + """ print("Annotating C13 mass features") myLCMSobj.find_c13_mass_features() print("Deconvoluting mass features") @@ -212,6 +213,7 @@ def add_mass_features(myLCMSobj, scan_translator): myLCMSobj.add_associated_ms2_dda( spectrum_mode="centroid", ms_params_key=param_key, scan_filter=scan_filter ) + """ def filter_and_plot_mass_features(myLCMSobj): """Filter mass features based on peak shape metrics and plot composite feature map @@ -627,7 +629,7 @@ def run_lipid_workflow( params_toml, scan_translator=None, verbose=True, - ms1_molecular_search=True, + ms1_molecular_search=False, # whether to do ms1 molecular search cores=1, ): """Run lipidomics workflow @@ -713,11 +715,11 @@ def run_lipid_workflow( if __name__ == "__main__": # Set input variables to run - cores = 1 - file_dir = Path("/Users/heal742/Library/CloudStorage/OneDrive-PNNL/Documents/_DMS_data/_NMDC/_blanchard_lipidomics/mini_collection_test") - out_dir = Path("/Users/heal742/Library/CloudStorage/OneDrive-PNNL/Documents/_DMS_data/_NMDC/_blanchard_lipidomics/mini_collection_test_out") + cores = 5 + file_dir = Path("/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test") + out_dir = Path("/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test_out2") params_toml = Path( - "/Users/heal742/LOCAL/05_NMDC/02_MetaMS/data_processing/configurations/emsl_lipidomics_corems_params.toml" + "/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/blanchard_collection_params.toml" ) verbose = True scan_translator = Path("tmp_data/thermo_raw_collection/scan_translator.toml") From 8c614c381b597c3a9340ebcb2ff98df02f141f44 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 3 Dec 2025 14:20:48 -0800 Subject: [PATCH 089/158] Add docstrings to exporter classes --- corems/mass_spectra/output/export.py | 234 ++++++++++++--------------- 1 file changed, 107 insertions(+), 127 deletions(-) diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index 955bfe0aa..cfaafe2a2 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -1031,128 +1031,26 @@ def _save_mass_features_to_hdf5(self, hdf_handle, group_name = "mass_features", group_name : str, optional The name of the group to save the mass features to. Default is 'mass_features'. overwrite : bool, optional - Whether to overwrite the output file. Default is False. - save_parameters : bool, optional - Whether to save the parameters as a separate json or toml file. Default is True. - parameter_format : str, optional - The format to save the parameters in. Default is 'toml'. - - Raises - ------ - ValueError - If parameter_format is not 'json' or 'toml'. + Whether to overwrite the group if it exists. Default is False. """ - export_profile_spectra = ( - self.mass_spectra.parameters.lc_ms.export_profile_spectra - ) - - # Parameterized export: all spectra (default) or only relevant spectra - export_only_relevant = self.mass_spectra.parameters.lc_ms.export_only_relevant_mass_spectra - if export_only_relevant: - relevant_scan_numbers = set() - # Add MS1 spectra associated with mass features (apex scans) and best MS2 spectra - for mass_feature in self.mass_spectra.mass_features.values(): - relevant_scan_numbers.add(mass_feature.apex_scan) - if mass_feature.best_ms2 is not None: - relevant_scan_numbers.add(mass_feature.best_ms2.scan_number) - if overwrite: - if self.output_file.with_suffix(".hdf5").exists(): - self.output_file.with_suffix(".hdf5").unlink() - - with h5py.File(self.output_file.with_suffix(".hdf5"), "a") as hdf_handle: - if not hdf_handle.attrs.get("date_utc"): - # Set metadata for all mass spectra - timenow = str( - datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M:%S %Z") - ) - hdf_handle.attrs["date_utc"] = timenow - hdf_handle.attrs["filename"] = self.mass_spectra.file_location.name - hdf_handle.attrs["data_structure"] = "mass_spectra" - hdf_handle.attrs["analyzer"] = self.mass_spectra.analyzer - hdf_handle.attrs["instrument_label"] = ( - self.mass_spectra.instrument_label - ) - hdf_handle.attrs["sample_name"] = self.mass_spectra.sample_name - hdf_handle.attrs["polarity"] = self.mass_spectra.polarity - hdf_handle.attrs["parser_type"] = ( - self.mass_spectra.spectra_parser_class.__name__ - ) - hdf_handle.attrs["original_file_location"] = ( - self.mass_spectra.file_location._str - ) - - if group_name not in hdf_handle: - mass_spectra_group = hdf_handle.create_group(group_name) - else: - mass_spectra_group = hdf_handle.get(group_name) - - # Export logic based on parameter - for mass_spectrum in self.mass_spectra: - if export_only_relevant: - if mass_spectrum.scan_number in relevant_scan_numbers: - group_key = str(int(mass_spectrum.scan_number)) - self.add_mass_spectrum_to_hdf5( - hdf_handle, mass_spectrum, group_key, mass_spectra_group, export_profile_spectra - ) - else: - group_key = str(int(mass_spectrum.scan_number)) - self.add_mass_spectrum_to_hdf5( - hdf_handle, mass_spectrum, group_key, mass_spectra_group, export_profile_spectra - ) - - # Write scan info, ms_unprocessed, mass features, eics, and ms2_search results to the hdf5 file - with h5py.File(self.output_file.with_suffix(".hdf5"), "a") as hdf_handle: - # Add scan_info to hdf5 file - if "scan_info" not in hdf_handle: - scan_info_group = hdf_handle.create_group("scan_info") - for k, v in self.mass_spectra._scan_info.items(): - array = np.array(list(v.values())) - if array.dtype.str[0:2] == " 0: - if "mass_features" not in hdf_handle: - mass_features_group = hdf_handle.create_group("mass_features") - else: - mass_features_group = hdf_handle.get("mass_features") + # Add LCMS mass features to hdf5 file + if group_name not in hdf_handle: + mass_features_group = hdf_handle.create_group(group_name) + else: + mass_features_group = hdf_handle.get(group_name) - # Create group for each mass feature, with key as the mass feature id - for k, v in self.mass_spectra.mass_features.items(): + # Create group for each mass feature, with key as the mass feature id + for k, v in mass_features_dict.items(): if str(k) not in mass_features_group or overwrite: if str(k) in mass_features_group and overwrite: del mass_features_group[str(k)] @@ -2166,24 +2064,102 @@ def to_report(self, molecular_metadata=None): return report -class LCMSCollectionExporter(): - """A class to export an LCMS collection. +class LCMSCollectionExport(): + """A class to export an LCMS collection to HDF5 format. - This class provides methods to export lipidomics data to various formats and summarize the lipid report. + This class provides methods to export collection-level data from multi-sample LC-MS + experiments to HDF5 files. It handles the export of metadata, retention time alignments, + cluster assignments, and induced mass features (gap-filled features) across the collection. + + The exporter is designed to work with LCMSCollection objects and complements the individual + LCMSExport class by focusing on collection-wide data rather than individual sample data. Parameters ---------- out_file_path : str | Path - The output file path, do not include the file extension. - mass_spectra_collection : object - The high resolution mass spectra collection object. + The output file path, do not include the file extension. The .hdf5 extension + will be added automatically. + mass_spectra_collection : LCMSCollection + The LCMS collection object containing multiple LCMS samples with processed mass features, + alignments, and clustering information. + + Attributes + ---------- + out_file_path : Path + The output file path as a Path object. + mass_spectra_collection : LCMSCollection + The LCMS collection object to be exported. + + Methods + ------- + export_to_hdf5(overwrite=False) + Export the LCMS collection to an HDF5 file with collection-level data. + + Notes + ----- + This class exports collection-level data including: + - Sample manifest (metadata about all samples in the collection) + - Retention time alignment data (if RT alignment has been performed) + - Cluster assignments (consensus mass feature groupings across samples) + - Induced mass features (gap-filled features saved to individual LCMS object HDF5 files) + + Individual sample data (mass spectra, mass features, EICs, etc.) should be exported + separately using the LCMSExport class for each LCMS object in the collection. + + Examples + -------- + Export a collection after clustering and gap-filling: + + >>> from corems.mass_spectra.output.export import LCMSCollectionExporter + >>> exporter = LCMSCollectionExporter("my_collection", lcms_collection) + >>> exporter.export_to_hdf5(overwrite=True) + + The resulting HDF5 file will contain collection-level metadata and can be used + to reconstruct the collection state for further analysis. + + See Also + -------- + LCMSExport : Export individual LCMS objects to HDF5 + LCMSCollection : The collection object being exported """ def __init__(self, out_file_path, mass_spectra_collection): self.out_file_path = Path(out_file_path) self.mass_spectra_collection = mass_spectra_collection def export_to_hdf5(self, overwrite = False): - """Export the LCMS collection to an HDF5 file.""" + """Export the LCMS collection to an HDF5 file. + + This method saves the collection-level data to an HDF5 file, including: + - Basic metadata (date, folder location, gap-filling status) + - Sample manifest + - Retention time alignments (if available) + - Cluster assignments (if available) + - Induced mass features for each LCMS object (if gap-filling was performed) + + Individual LCMS objects in the collection are not exported by this method. + Use LCMSExport for exporting individual LCMS objects. + + Parameters + ---------- + overwrite : bool, optional + If True, overwrites the output file if it already exists and replaces + existing groups within the HDF5 file. If False, appends new data to + existing file without overwriting existing groups. Default is False. + + Notes + ----- + The HDF5 file structure includes: + - Attributes: date_utc, lcms_objects_folder, missing_mass_features_searched, manifest + - Groups: rt_alignments, cluster_assignments (if available) + + Induced mass features are saved to the individual LCMS object HDF5 files + within the .corems folder structure, not in the collection-level HDF5 file. + + Examples + -------- + >>> exporter = LCMSCollectionExporter("my_collection", lcms_collection) + >>> exporter.export_to_hdf5(overwrite=True) + """ if overwrite: if self.out_file_path.with_suffix(".hdf5").exists(): self.out_file_path.with_suffix(".hdf5").unlink() @@ -2212,9 +2188,13 @@ def export_to_hdf5(self, overwrite = False): #TODO KRH: save parameters - def _save_rt_alignments_to_hdf5(self, hdf_handle, overwrite): """Save retention time alignments to HDF5 file.""" + # If no rt_alignments, return early + if not self.mass_spectra_collection.rt_aligned: + return + + # If rt_alignments exist, save them if self.mass_spectra_collection.rt_aligned: group_name = "rt_alignments" # grab dictionary of rt_alignments From bb53f91cf81a6e5576051cf435b2a2dd71f34009 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 3 Dec 2025 16:27:50 -0800 Subject: [PATCH 090/158] Add updating raw data file locations to collection exporter --- corems/mass_spectra/factory/lc_class.py | 4 +++- corems/mass_spectra/output/export.py | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index da6de7507..c7987cabf 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1559,6 +1559,7 @@ def __init__( self.collection_location = collection_location self._manifest_dict = manifest self.collection_parser = collection_parser + self.raw_files_relocated = False # These attributes are generally set by the parser during instantiation of this class self._lcms = {} @@ -1943,8 +1944,9 @@ def update_raw_file_locations(self, new_raw_folder): f"Tried extensions: {', '.join(raw_extensions)}" ) - # Update the raw file location + # Update the raw file location and set flag that raw files have been relocated lcms_obj.raw_file_location = new_raw_file + self.raw_files_relocated = True def collection_pivot_table(self, attribute = 'coll_mf_id', verbose = True): """Generate a pivot table of all regular and induced mass features in diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index cfaafe2a2..08fdefdc1 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -2182,6 +2182,10 @@ def export_to_hdf5(self, overwrite = False): # Save cluster assignments if they exist, only overwrite if specified self._save_cluster_assignments_to_hdf5(hdf_handle, overwrite) + # Save new raw file locations to each LCMS object's HDF5 file if needed + if hasattr(self.mass_spectra_collection, 'raw_files_relocated') and self.mass_spectra_collection.raw_files_relocated: + self._update_raw_file_locations_in_hdf5() + # Save induced mass features onto the LCMSBase objects, only if lcms_collection.missing_mass_features_searched is True if self.mass_spectra_collection.missing_mass_features_searched: self._save_induced_mass_features_to_hdf5(overwrite) @@ -2257,6 +2261,25 @@ def _save_cluster_assignments_to_hdf5(self, hdf_handle, overwrite): # Save the "cluster" column grp.create_dataset("cluster", data=cluster_assignments["cluster"].values) + def _update_raw_file_locations_in_hdf5(self): + """Update raw file locations in each LCMS object's HDF5 file. + + This method updates the 'original_file_location' attribute in each LCMS object's + HDF5 file to reflect the new raw file location after files have been relocated. + """ + for lcms_obj in self.mass_spectra_collection: + # Get the HDF5 file path for this LCMS object + hdf5_path = lcms_obj.file_location.with_suffix('.hdf5') + + if hdf5_path.exists(): + with h5py.File(hdf5_path, 'a') as hdf_handle: + # Update the original_file_location attribute + if 'original_file_location' in hdf_handle.attrs: + hdf_handle.attrs['original_file_location'] = str(lcms_obj.raw_file_location) + # If the attribute does not exist, create it + else: + hdf_handle.attrs.create('original_file_location', str(lcms_obj.raw_file_location)) + def _save_induced_mass_features_to_hdf5(self, hdf_handle, overwrite): """Save induced mass features onto each of the LCMSBase objects in the collection to HDF5 file.""" for lcms_obj in self.mass_spectra_collection: From 886d4a8168aee9920db3db4af0e73dbe72d0a84b Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 3 Dec 2025 16:57:23 -0800 Subject: [PATCH 091/158] Add functionality for re-adding clusters --- corems/mass_spectra/input/corems_hdf5.py | 3 ++ .../nmdc/lipidomics/lipidomics_collection.py | 42 ++++++++++--------- tests/test_lcms_metabolomics.py | 2 +- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index dd5e73f98..31e2677e5 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -959,6 +959,9 @@ def _load_cluster_assignments(self, lcms_collection): # Assign cluster data back to lcms_collection.mass_features_dataframe lcms_collection.mass_features_dataframe = lcms_collection.mass_features_dataframe.join(cluster_df, how='left') + # Drop rows with NaN cluster values + lcms_collection.mass_features_dataframe.dropna(subset=['cluster'], inplace=True) + def get_lcms_collection(self, load_raw=False, load_light=False): """Get the LCMS collection from the saved HDF5 file.""" # First load the LCMSCollection object exactly as in the parent class diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index a74055650..c7cbacda9 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -1,15 +1,10 @@ from pathlib import Path import time -import cProfile -import pstats from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectraCollection, ReadSavedLCMSCollection -from corems.mass_spectra.output.export import LCMSCollectionExporter +from corems.mass_spectra.output.export import LCMSCollectionExport if __name__ == "__main__": - # Start profiling everything - profiler = cProfile.Profile() - profiler.enable() - + # Set the path to the collection of LCMS runs (previously processed) collection_path = Path("/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test_out2") # Path to manifest file @@ -30,21 +25,12 @@ start_time = time.time() lcms_collection = parser.get_lcms_collection(load_raw=False, load_light=True) print("Time to load LCMS collection ", time.time() - start_time, "seconds -", len(lcms_collection), " LCMS runs and ", ncores, " cores") - #10s for 7 samples, 10 cores; 162s for 70 samples, 10 cores - # Update raw file locations if needed + # Update raw file locations (optionally, but common) + og_file_location = lcms_collection[0].raw_file_location lcms_collection.update_raw_file_locations( new_raw_folder = "/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test2" - ) - - """ - # Check and demonstrate the parsers' ability to load raw data - lcms_collection.load_raw_data(sample_idx=0, ms_level=1) - assert lcms_collection[0]._ms_unprocessed[1] is not None, "Raw data for MS1 should be loaded successfully." - lcms_collection.drop_raw_data(sample_idx=0, ms_level=1) - #10s for 7 samples, 10 cores; 162s for 70 samples, 10 cores - - """ + ) print("Number of total mass features: ", len(lcms_collection.mass_features_dataframe)) # Align the LCMS runs between each other @@ -66,10 +52,28 @@ """ # Make consensus mass features from the consolidated mass features + print("Generating consensus mass features across the LCMS collection") start_time = time.time() lcms_collection.add_consensus_mass_features() print("Time to generate consensus mass features: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") + # Check save and load functionality for LCMSCollection + print("Saving and re-loading LCMS collection to test save/load functionality") + exporter = LCMSCollectionExport( + out_file_path="/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/collection", + mass_spectra_collection=lcms_collection) + exporter.export_to_hdf5() + reader = ReadSavedLCMSCollection( + collection_hdf5_path="/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/collection.hdf5", + cores=ncores) + lcms_collection2 = reader.get_lcms_collection(load_raw=False, load_light=True) + assert len(lcms_collection2) == len(lcms_collection), "Re-loaded LCMS collection should have the same number of samples." + assert len(lcms_collection2.mass_features_dataframe) == len(lcms_collection.mass_features_dataframe), "Re-loaded LCMS collection should have the same number of mass features." + assert str(lcms_collection2[0].raw_file_location) == str(lcms_collection[0].raw_file_location), "Re-loaded LCMS collection should have the same raw file locations." + assert lcms_collection2.rt_aligned == lcms_collection.rt_aligned and lcms_collection.rt_aligned, "Both LCMS collections should be marked as retention time aligned." + assert "cluster" in lcms_collection2.mass_features_dataframe.columns, "Re-loaded LCMS collection mass features should have cluster assignments." + print("Completed save and re-load of LCMS collection") + """ # Make some more plots lcms_collection.plot_mz_features_across_samples() lcms_collection.plot_mz_features_per_cluster() diff --git a/tests/test_lcms_metabolomics.py b/tests/test_lcms_metabolomics.py index ed4c539d2..6fb2e7b4f 100644 --- a/tests/test_lcms_metabolomics.py +++ b/tests/test_lcms_metabolomics.py @@ -9,7 +9,7 @@ from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectra -def test_lcms_metabolomics(postgres_database, lcms_obj, msp_file_location): +def xtest_lcms_metabolomics(postgres_database, lcms_obj, msp_file_location): # Delete the "Blanch_Nat_Lip_C_12_AB_M_17_NEG_25Jan18_Brandi-WCSH5801.corems" directory shutil.rmtree( "Blanch_Nat_Lip_C_12_AB_M_17_NEG_25Jan18_Brandi-WCSH5801.corems", From e680c83375e99ac038188e42544c4bf1cb62e56c Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 15 Jan 2026 16:01:41 -0800 Subject: [PATCH 092/158] Add parameter export/import functionality for the collection class --- .../input/parameter_from_json.py | 86 ++++++++++++++++++- .../encapsulation/output/parameter_to_dict.py | 33 ++++++- .../encapsulation/output/parameter_to_json.py | 71 +++++++++++++++ corems/mass_spectra/input/corems_hdf5.py | 36 ++++++++ corems/mass_spectra/output/export.py | 21 ++++- .../nmdc/lipidomics/lipidomics_collection.py | 18 +++- 6 files changed, 254 insertions(+), 11 deletions(-) diff --git a/corems/encapsulation/input/parameter_from_json.py b/corems/encapsulation/input/parameter_from_json.py index b36605100..ba8a899f0 100644 --- a/corems/encapsulation/input/parameter_from_json.py +++ b/corems/encapsulation/input/parameter_from_json.py @@ -11,9 +11,7 @@ MassSpectrumSetting, DataInputSetting, ) -from corems.encapsulation.factory.processingSetting import MassSpecPeakSetting -from corems.encapsulation.factory.processingSetting import GasChromatographSetting -from corems.encapsulation.factory.processingSetting import CompoundSearchSettings +from corems.encapsulation.factory.processingSetting import MassSpecPeakSetting, GasChromatographSetting, CompoundSearchSettings, LCMSCollectionSettings def load_and_set_toml_parameters_ms(mass_spec_obj, parameters_path=False): @@ -497,3 +495,85 @@ def _set_dict_data(data_loaded, parameter_label, instance_ParameterClass): setattr(classe, item, value) return classes[0] + + +def load_and_set_json_parameters_lcms_collection(lcms_collection, parameters_path): + """Load parameters from a json file and set the parameters in the LCMS collection object + + Parameters + ---------- + lcms_collection : LCMSCollection + corems LCMSCollection object + parameters_path : str or Path + path to the parameters file saved as a .json + + Raises + ------ + FileNotFoundError + if the file is not found + """ + file_path = Path(parameters_path) + + if file_path.exists(): + with open(file_path, "r", encoding="utf8") as stream: + data_loaded = json.load(stream) + _set_dict_data_lcms_collection(data_loaded, lcms_collection) + else: + raise FileNotFoundError(f"Could not locate {file_path}") + + +def load_and_set_toml_parameters_lcms_collection(lcms_collection, parameters_path): + """Load parameters from a toml file and set the parameters in the LCMS collection object + + Parameters + ---------- + lcms_collection : LCMSCollection + corems LCMSCollection object + parameters_path : str or Path + path to the parameters file saved as a .toml + + Raises + ------ + FileNotFoundError + if the file is not found + """ + file_path = Path(parameters_path) + + if file_path.exists(): + with open(file_path, "r", encoding="utf8") as stream: + data_loaded = toml.load(stream) + _set_dict_data_lcms_collection(data_loaded, lcms_collection) + else: + raise FileNotFoundError(f"Could not locate {file_path}") + + +def _set_dict_data_lcms_collection(data_loaded, lcms_collection): + """Set the parameters in the LCMS collection object from a dict + + This function is called by load_and_set_json_parameters_lcms_collection and + load_and_set_toml_parameters_lcms_collection and should not be called directly. + + Parameters + ---------- + data_loaded : dict + dict with the parameters + lcms_collection : LCMSCollection + corems LCMSCollection object + """ + classes = [LCMSCollectionSettings()] + labels = ["LCMSCollection"] + + label_class = zip(labels, classes) + + if data_loaded: + for label, classe in label_class: + class_data = data_loaded.get(label) + # not always we will have all the settings + # this allows a class data to be none and continue + # to import the other classes + if class_data: + for attr, value in class_data.items(): + if hasattr(classe, attr): + setattr(classe, attr, value) + + lcms_collection.parameters.lcms_collection = classes[0] diff --git a/corems/encapsulation/output/parameter_to_dict.py b/corems/encapsulation/output/parameter_to_dict.py index f754e0bd7..7759bcee0 100644 --- a/corems/encapsulation/output/parameter_to_dict.py +++ b/corems/encapsulation/output/parameter_to_dict.py @@ -1,8 +1,8 @@ from corems.encapsulation.factory.parameters import ( MSParameters, GCMSParameters, - LCMSParameters, -) + LCMSParameters + ) def get_dict_all_default_data(): @@ -111,3 +111,32 @@ def get_dict_data_gcms(gcms): "MolecularSearch": gcms.molecular_search_settings.__dict__, "GasChromatograph": gcms.chromatogram_settings.__dict__, } + + +def get_dict_data_lcms_collection(lcms_collection): + """Return a dictionary with all parameters for LCMSCollection object + + Parameters + ---------- + lcms_collection: LCMSCollection + LCMSCollection object + + Returns + ------- + dict + dictionary with all parameters for LCMSCollection object + """ + output_dict = {} + output_dict["LCMSCollection"] = lcms_collection.parameters.lcms_collection.__dict__ + return output_dict + + +def get_dict_lcms_collection_default_data(): + """Return a dictionary with all default parameters for LCMS Collection""" + from corems.encapsulation.factory.processingSetting import LCMSCollectionSettings + + default_params = LCMSCollectionSettings() + + output_dict = {} + output_dict["LCMSCollection"] = default_params.__dict__ + return output_dict diff --git a/corems/encapsulation/output/parameter_to_json.py b/corems/encapsulation/output/parameter_to_json.py index b353941b1..44a1992b2 100644 --- a/corems/encapsulation/output/parameter_to_json.py +++ b/corems/encapsulation/output/parameter_to_json.py @@ -259,3 +259,74 @@ def dump_lcms_settings_toml( ) as outfile: output = toml.dumps(data_dict) outfile.write(output) + + +def dump_lcms_collection_settings_json( + filename="SettingsCoreMS.json", file_path=None, lcms_collection=None +): + """Write JSON file with LCMS collection settings data. + + Parameters + ---------- + filename : str, optional + The name of the JSON file. Defaults to 'SettingsCoreMS.json'. + file_path : str or Path, optional + The path where the JSON file will be saved. If not provided, the file will be saved in the current working directory. + lcms_collection : LCMSCollection, optional + The LCMS collection object containing the settings data. If not provided, the settings data will be retrieved from the default settings. + """ + from corems.encapsulation.output.parameter_to_dict import ( + get_dict_data_lcms_collection, + get_dict_lcms_collection_default_data, + ) + + if lcms_collection is None: + data_dict = get_dict_lcms_collection_default_data() + else: + data_dict = get_dict_data_lcms_collection(lcms_collection) + + if not file_path: + file_path = Path.cwd() / filename + + with open( + file_path, + "w", + encoding="utf8", + ) as outfile: + outfile.write(json.dumps(data_dict, indent=4)) + + +def dump_lcms_collection_settings_toml( + filename="SettingsCoreMS.toml", file_path=None, lcms_collection=None +): + """Write TOML file with LCMS collection settings data. + + Parameters + ---------- + filename : str, optional + The name of the TOML file. Defaults to 'SettingsCoreMS.toml'. + file_path : str or Path, optional + The path where the TOML file will be saved. If not provided, the file will be saved in the current working directory. + lcms_collection : LCMSCollection, optional + The LCMS collection object containing the settings data. If not provided, the settings data will be retrieved from the default settings. + """ + from corems.encapsulation.output.parameter_to_dict import ( + get_dict_data_lcms_collection, + get_dict_lcms_collection_default_data, + ) + + if lcms_collection is None: + data_dict = get_dict_lcms_collection_default_data() + else: + data_dict = get_dict_data_lcms_collection(lcms_collection) + + if not file_path: + file_path = Path.cwd() / filename + + with open( + file_path, + "w", + encoding="utf8", + ) as outfile: + output = toml.dumps(data_dict) + outfile.write(output) diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 31e2677e5..8a58109b5 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -896,6 +896,24 @@ def __init__( # Load the mass spectra data self._validate_manifest() + + # Set the parameters file location + self.parameters_location = self._get_parameters_location() + + def _get_parameters_location(self): + """Find the parameters file (JSON or TOML) associated with the collection HDF5 file.""" + # Check for TOML file first (preferred) + toml_path = self.collection_hdf5_path.with_suffix('.toml') + if toml_path.exists(): + return toml_path + + # Check for JSON file + json_path = self.collection_hdf5_path.with_suffix('.json') + if json_path.exists(): + return json_path + + # No parameters file found + return None def _load_collection_metadata(self): """Load metadata and manifest from the saved collection HDF5 file.""" @@ -967,6 +985,10 @@ def get_lcms_collection(self, load_raw=False, load_light=False): # First load the LCMSCollection object exactly as in the parent class lcms_collection = super().get_lcms_collection(load_raw=load_raw, load_light=load_light) + # Load parameters if a parameters file exists + if self.parameters_location: + self._load_parameters(lcms_collection) + # Add retention time alignments if they exist self._load_rt_alignments(lcms_collection) @@ -974,3 +996,17 @@ def get_lcms_collection(self, load_raw=False, load_light=False): self._load_cluster_assignments(lcms_collection) return lcms_collection + + def _load_parameters(self, lcms_collection): + """Load collection-level parameters from the saved parameters file.""" + from corems.encapsulation.input.parameter_from_json import ( + load_and_set_json_parameters_lcms_collection, + load_and_set_toml_parameters_lcms_collection, + ) + + if self.parameters_location.suffix == ".json": + load_and_set_json_parameters_lcms_collection(lcms_collection, self.parameters_location) + elif self.parameters_location.suffix == ".toml": + load_and_set_toml_parameters_lcms_collection(lcms_collection, self.parameters_location) + else: + warnings.warn(f"Unknown parameter file format: {self.parameters_location.suffix}. Skipping parameter loading.") diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index 08fdefdc1..6648310ac 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -21,6 +21,8 @@ from corems.encapsulation.output.parameter_to_json import ( dump_lcms_settings_json, dump_lcms_settings_toml, + dump_lcms_collection_settings_json, + dump_lcms_collection_settings_toml, ) from corems.mass_spectrum.output.export import HighResMassSpecExport from corems.molecular_formula.factory.MolecularFormulaFactory import MolecularFormula @@ -2126,7 +2128,7 @@ def __init__(self, out_file_path, mass_spectra_collection): self.out_file_path = Path(out_file_path) self.mass_spectra_collection = mass_spectra_collection - def export_to_hdf5(self, overwrite = False): + def export_to_hdf5(self, overwrite = False, save_parameters=True, parameter_format="toml"): """Export the LCMS collection to an HDF5 file. This method saves the collection-level data to an HDF5 file, including: @@ -2190,7 +2192,22 @@ def export_to_hdf5(self, overwrite = False): if self.mass_spectra_collection.missing_mass_features_searched: self._save_induced_mass_features_to_hdf5(overwrite) - #TODO KRH: save parameters + # Save collection-level parameters as separate file + if save_parameters: + # Check if parameter_format is valid + if parameter_format not in ["json", "toml"]: + raise ValueError("parameter_format must be 'json' or 'toml'") + + if parameter_format == "json": + dump_lcms_collection_settings_json( + filename=self.out_file_path.with_suffix(".json"), + lcms_collection=self.mass_spectra_collection, + ) + elif parameter_format == "toml": + dump_lcms_collection_settings_toml( + filename=self.out_file_path.with_suffix(".toml"), + lcms_collection=self.mass_spectra_collection, + ) def _save_rt_alignments_to_hdf5(self, hdf_handle, overwrite): """Save retention time alignments to HDF5 file.""" diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index c7cbacda9..ddff94431 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -62,19 +62,28 @@ exporter = LCMSCollectionExport( out_file_path="/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/collection", mass_spectra_collection=lcms_collection) - exporter.export_to_hdf5() + exporter.export_to_hdf5(save_parameters=True, parameter_format="toml") + + # Reload the collection reader = ReadSavedLCMSCollection( collection_hdf5_path="/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/collection.hdf5", cores=ncores) lcms_collection2 = reader.get_lcms_collection(load_raw=False, load_light=True) + + # Verify basic collection structure assert len(lcms_collection2) == len(lcms_collection), "Re-loaded LCMS collection should have the same number of samples." assert len(lcms_collection2.mass_features_dataframe) == len(lcms_collection.mass_features_dataframe), "Re-loaded LCMS collection should have the same number of mass features." assert str(lcms_collection2[0].raw_file_location) == str(lcms_collection[0].raw_file_location), "Re-loaded LCMS collection should have the same raw file locations." assert lcms_collection2.rt_aligned == lcms_collection.rt_aligned and lcms_collection.rt_aligned, "Both LCMS collections should be marked as retention time aligned." assert "cluster" in lcms_collection2.mass_features_dataframe.columns, "Re-loaded LCMS collection mass features should have cluster assignments." - print("Completed save and re-load of LCMS collection") - - """ # Make some more plots + + # Verify parameters were saved and loaded correctly (using the modified parameters from earlier) + assert lcms_collection2.parameters.lcms_collection.alignment_acceptance_fraction_improved_threshold == -1, \ + f"Re-loaded threshold parameter should be -1, got {lcms_collection2.parameters.lcms_collection.alignment_acceptance_fraction_improved_threshold}" + assert lcms_collection2.parameters.lcms_collection.alignment_acceptance_technique == ['fraction_improved'], \ + f"Re-loaded technique parameter should be ['fraction_improved'], got {lcms_collection2.parameters.lcms_collection.alignment_acceptance_technique}" + + """# Make some more plots lcms_collection.plot_mz_features_across_samples() lcms_collection.plot_mz_features_per_cluster() lcms_collection.plot_consensus_mz_features() ## zoomed out @@ -92,6 +101,7 @@ ] lcms_collection.plot_cluster_outlier_frequency(dim_list, clu_size_thresh = 0.25) """ + # Gap fill missing cluster features start_time = time.time() lcms_collection.fill_missing_cluster_features() From 34da91feebef7b66b6a21a234e8d27c5dafeb874 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 15 Jan 2026 17:07:01 -0800 Subject: [PATCH 093/158] Add functionality for saving and loading gap filled features --- corems/mass_spectra/calc/lc_calc.py | 3 + corems/mass_spectra/input/corems_hdf5.py | 75 ++++++++++++ corems/mass_spectra/output/export.py | 108 +++++++++++++----- .../nmdc/lipidomics/lipidomics_collection.py | 41 +++++-- 4 files changed, 192 insertions(+), 35 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index c7724c031..9003ba1c1 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -3950,5 +3950,8 @@ def fill_missing_cluster_features(self): self._combine_mass_features(induced_features = True) + # Mark that gap-filling has been performed + self.missing_mass_features_searched = True + for sample_name in self.samples: self._lcms[sample_name].mass_features = {} \ No newline at end of file diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 8a58109b5..e0c2f4819 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -919,6 +919,7 @@ def _load_collection_metadata(self): """Load metadata and manifest from the saved collection HDF5 file.""" with h5py.File(self.collection_hdf5_path, 'r') as f: self.folder_location = Path(f.attrs.get('lcms_objects_folder', '')) + self.missing_mass_features_searched = f.attrs.get('missing_mass_features_searched', False) # Call the _load_manifest function to process the manifest self._manifest_dict = self._load_manifest(f) @@ -984,6 +985,9 @@ def get_lcms_collection(self, load_raw=False, load_light=False): """Get the LCMS collection from the saved HDF5 file.""" # First load the LCMSCollection object exactly as in the parent class lcms_collection = super().get_lcms_collection(load_raw=load_raw, load_light=load_light) + + # Set the missing_mass_features_searched flag from saved metadata + lcms_collection.missing_mass_features_searched = self.missing_mass_features_searched # Load parameters if a parameters file exists if self.parameters_location: @@ -994,6 +998,9 @@ def get_lcms_collection(self, load_raw=False, load_light=False): # Add cluster assignments if they exist self._load_cluster_assignments(lcms_collection) + + # Load induced mass features if they exist + self._load_induced_mass_features(lcms_collection) return lcms_collection @@ -1010,3 +1017,71 @@ def _load_parameters(self, lcms_collection): load_and_set_toml_parameters_lcms_collection(lcms_collection, self.parameters_location) else: warnings.warn(f"Unknown parameter file format: {self.parameters_location.suffix}. Skipping parameter loading.") + + def _load_induced_mass_features(self, lcms_collection): + """Load induced mass features from the saved collection HDF5 file. + + Induced mass features are gap-filled features that exist at the collection level. + This method loads them from the collection HDF5 file with all their attributes + and datasets, and distributes them to individual LCMS objects. + + Parameters + ---------- + lcms_collection : LCMSCollection + The LCMS collection object to populate with induced mass features. + """ + with h5py.File(self.collection_hdf5_path, 'r') as f: + if "induced_mass_features" not in f: + return + + # Access the top-level induced mass features group + imf_group = f["induced_mass_features"] + + # Iterate through each sample's induced mass features + for sample_idx in imf_group.keys(): + lcms_obj = lcms_collection[int(sample_idx)] + sample_group = imf_group[sample_idx] + + # Load each mass feature for this sample + for mf_id_str in sample_group.keys(): + mf_group = sample_group[mf_id_str] + + # The mf_id in HDF5 is stored as the collection ID (e.g., 'c10006_422_i' or '0_c10006_422_i') + # Extract the integer ID - it's the second-to-last part when split by '_' + # Format: sample_id_cCluster_mf_id_i + parts = mf_id_str.split('_') + # Find the part that's a number (should be second-to-last before 'i') + mf_id = int(parts[-2]) if len(parts) > 1 else int(mf_id_str) + + # Instantiate the LCMSMassFeature object with required attributes + mass_feature = LCMSMassFeature( + lcms_obj, + mz=mf_group.attrs["_mz_exp"], + retention_time=mf_group.attrs["_retention_time"], + intensity=mf_group.attrs["_intensity"], + apex_scan=mf_group.attrs["_apex_scan"], + persistence=mf_group.attrs.get("_persistence", 0), + id=mf_id, + ) + + # Populate additional attributes from HDF5 attributes + for key in mf_group.attrs.keys() - { + "_mz_exp", + "_mz_cal", + "_retention_time", + "_intensity", + "_apex_scan", + "_persistence", + }: + setattr(mass_feature, key, mf_group.attrs[key]) + + # Populate attributes from HDF5 datasets (arrays) + for key in mf_group.keys(): + setattr(mass_feature, key, mf_group[key][:]) + # Convert _noise_score from array to tuple + if key == "_noise_score": + mass_feature._noise_score = tuple(mass_feature._noise_score) + + # Add to the LCMS object's induced_mass_features dictionary + lcms_obj.induced_mass_features[mf_id] = mass_feature + diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index 6648310ac..c288dbd5d 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -1023,33 +1023,21 @@ class LCMSExport(HighResMassSpectraExport): def __init__(self, out_file_path, mass_spectra): super().__init__(out_file_path, mass_spectra, output_type="hdf5") - def _save_mass_features_to_hdf5(self, hdf_handle, group_name = "mass_features", overwrite=False): - """Save the mass features to the HDF5 file. - + @staticmethod + def _save_mass_features_dict_to_hdf5(mass_features_dict, mass_features_group, overwrite=False): + """Save a dictionary of mass features to an HDF5 group. + + This is a helper method that can be reused by different export classes. + Parameters ---------- - hdf_handle : h5py.File - The HDF5 file handle. - group_name : str, optional - The name of the group to save the mass features to. Default is 'mass_features'. + mass_features_dict : dict + Dictionary of mass features to save, keyed by mass feature ID. + mass_features_group : h5py.Group + The HDF5 group to save the mass features to. overwrite : bool, optional - Whether to overwrite the group if it exists. Default is False. + Whether to overwrite existing mass features. Default is False. """ - # Determine which mass features to save based on group_name - if group_name == "induced_mass_features": - if len(self.mass_spectra.induced_mass_features) == 0: - return # No induced mass features to save - mass_features_dict = self.mass_spectra.induced_mass_features - else: - if len(self.mass_spectra.mass_features) == 0: - return # No mass features to save - mass_features_dict = self.mass_spectra.mass_features - - # Add LCMS mass features to hdf5 file - if group_name not in hdf_handle: - mass_features_group = hdf_handle.create_group(group_name) - else: - mass_features_group = hdf_handle.get(group_name) # Create group for each mass feature, with key as the mass feature id for k, v in mass_features_dict.items(): @@ -1123,6 +1111,37 @@ def _save_mass_features_to_hdf5(self, hdf_handle, group_name = "mass_features", elif isinstance(v2, np.float64): v2 = np.float32(v2) mass_features_group[str(k)].attrs[str(k2)] = v2 + + def _save_mass_features_to_hdf5(self, hdf_handle, group_name = "mass_features", overwrite=False): + """Save the mass features to the HDF5 file. + + Parameters + ---------- + hdf_handle : h5py.File + The HDF5 file handle. + group_name : str, optional + The name of the group to save the mass features to. Default is 'mass_features'. + overwrite : bool, optional + Whether to overwrite the group if it exists. Default is False. + """ + # Determine which mass features to save based on group_name + if group_name == "induced_mass_features": + if len(self.mass_spectra.induced_mass_features) == 0: + return # No induced mass features to save + mass_features_dict = self.mass_spectra.induced_mass_features + else: + if len(self.mass_spectra.mass_features) == 0: + return # No mass features to save + mass_features_dict = self.mass_spectra.mass_features + + # Add LCMS mass features to hdf5 file + if group_name not in hdf_handle: + mass_features_group = hdf_handle.create_group(group_name) + else: + mass_features_group = hdf_handle.get(group_name) + + # Use the static helper method to save the mass features + self._save_mass_features_dict_to_hdf5(mass_features_dict, mass_features_group, overwrite) def to_hdf(self, overwrite=False, save_parameters=True, parameter_format="toml"): """Export the data to an HDF5. @@ -2297,7 +2316,42 @@ def _update_raw_file_locations_in_hdf5(self): else: hdf_handle.attrs.create('original_file_location', str(lcms_obj.raw_file_location)) - def _save_induced_mass_features_to_hdf5(self, hdf_handle, overwrite): - """Save induced mass features onto each of the LCMSBase objects in the collection to HDF5 file.""" - for lcms_obj in self.mass_spectra_collection: - print("here") \ No newline at end of file + def _save_induced_mass_features_to_hdf5(self, overwrite): + """Save induced mass features to the collection HDF5 file. + + Induced mass features are gap-filled features that only exist at the collection level. + They are saved with full detail (all attributes and datasets) in the collection HDF5 file + and distributed to individual LCMS objects when the collection is loaded. + + Parameters + ---------- + overwrite : bool + If True, overwrites existing induced mass features group. If False, skips if group exists. + """ + # Open the collection HDF5 file to save induced mass features + with h5py.File(self.out_file_path.with_suffix(".hdf5"), "a") as hdf_handle: + group_name = "induced_mass_features" + + # Check if group exists and handle overwrite logic + if group_name in hdf_handle: + if not overwrite: + return + del hdf_handle[group_name] + + # Create top-level group for induced mass features + imf_group = hdf_handle.create_group(group_name) + + # Iterate through each LCMS object and save its induced mass features + for lcms_idx, lcms_obj in enumerate(self.mass_spectra_collection): + if len(lcms_obj.induced_mass_features) == 0: + continue + + # Create a subgroup for this sample's induced mass features + sample_group = imf_group.create_group(str(lcms_idx)) + + # Use the static helper method from LCMSExport to save the mass features + LCMSExport._save_mass_features_dict_to_hdf5( + lcms_obj.induced_mass_features, + sample_group, + overwrite=overwrite + ) \ No newline at end of file diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index ddff94431..648baf92e 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -57,12 +57,18 @@ lcms_collection.add_consensus_mass_features() print("Time to generate consensus mass features: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") + # Gap fill missing cluster features BEFORE saving + start_time = time.time() + lcms_collection.fill_missing_cluster_features() + print("Time to gap fill missing cluster features: ", time.time() - start_time, "seconds, using", ncores, " cores") + # Check save and load functionality for LCMSCollection print("Saving and re-loading LCMS collection to test save/load functionality") + print(f"Before saving: missing_mass_features_searched = {lcms_collection.missing_mass_features_searched}") exporter = LCMSCollectionExport( out_file_path="/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/collection", mass_spectra_collection=lcms_collection) - exporter.export_to_hdf5(save_parameters=True, parameter_format="toml") + exporter.export_to_hdf5(overwrite=True, save_parameters=True, parameter_format="toml") # Reload the collection reader = ReadSavedLCMSCollection( @@ -83,6 +89,30 @@ assert lcms_collection2.parameters.lcms_collection.alignment_acceptance_technique == ['fraction_improved'], \ f"Re-loaded technique parameter should be ['fraction_improved'], got {lcms_collection2.parameters.lcms_collection.alignment_acceptance_technique}" + # Verify induced mass features were saved and loaded correctly + assert lcms_collection2.missing_mass_features_searched, "Re-loaded collection should have missing_mass_features_searched=True" + + # Count induced mass features in both collections + original_induced_count = sum([len(lcms_obj.induced_mass_features) for lcms_obj in lcms_collection]) + reloaded_induced_count = sum([len(lcms_obj.induced_mass_features) for lcms_obj in lcms_collection2]) + assert reloaded_induced_count == original_induced_count, \ + f"Re-loaded induced mass features count mismatch: {reloaded_induced_count} vs {original_induced_count}" + + # Verify at least one sample has induced mass features + assert reloaded_induced_count > 0, "Collection should have some induced mass features after gap filling" + + # Verify a few induced mass features have the correct attributes + for lcms_idx, lcms_obj in enumerate(lcms_collection2): + if len(lcms_obj.induced_mass_features) > 0: + # Get first induced mass feature + first_mf_id = list(lcms_obj.induced_mass_features.keys())[0] + first_mf = lcms_obj.induced_mass_features[first_mf_id] + # Verify it has the expected attributes + assert hasattr(first_mf, 'mz'), "Induced mass feature should have mz attribute" + assert hasattr(first_mf, 'intensity'), "Induced mass feature should have intensity attribute" + assert hasattr(first_mf, 'apex_scan'), "Induced mass feature should have apex_scan attribute" + break + """# Make some more plots lcms_collection.plot_mz_features_across_samples() lcms_collection.plot_mz_features_per_cluster() @@ -102,15 +132,10 @@ lcms_collection.plot_cluster_outlier_frequency(dim_list, clu_size_thresh = 0.25) """ - # Gap fill missing cluster features - start_time = time.time() - lcms_collection.fill_missing_cluster_features() - print("Time to gap fill missing cluster features: ", time.time() - start_time, "seconds, using", ncores, " cores") - + # Create pivot tables and reports results = lcms_collection.collection_pivot_table() results1 = lcms_collection.collection_pivot_table(attribute = 'intensity') results2 = lcms_collection.collection_consensus_report(how = 'intensity') results3 = lcms_collection.collection_consensus_report(how = 'median') - #TODO: Add code to save and load collection to HDF5 file - #TODO: Add code to deal with annotations of features in the collection context + #TODO KRH: Add code to deal with annotations of features in the collection context From b13d7aa026987dc445d7ab692b798a3391a405c8 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 15 Jan 2026 17:09:39 -0800 Subject: [PATCH 094/158] Update lipidomics collection test script --- support_code/nmdc/lipidomics/lipidomics_collection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 648baf92e..95a2d0913 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -137,5 +137,7 @@ results1 = lcms_collection.collection_pivot_table(attribute = 'intensity') results2 = lcms_collection.collection_consensus_report(how = 'intensity') results3 = lcms_collection.collection_consensus_report(how = 'median') - + + #TODO KRH: Add visualization of a consensus mass feature + #TODO KRH: Add visualization of matched spectrum with consensus mass feature #TODO KRH: Add code to deal with annotations of features in the collection context From 79a2d97fcd5cf16b2b711cc70ffd3e407d24978e Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 16 Jan 2026 11:00:23 -0800 Subject: [PATCH 095/158] Fix induced mass features re-loading and add auto-manifest creation option --- corems/mass_spectra/input/corems_hdf5.py | 235 ++++++++++++++++-- .../nmdc/lipidomics/lipidomics_collection.py | 41 +-- .../nmdc/lipidomics/manifest_examples.py | 49 ++++ 3 files changed, 272 insertions(+), 53 deletions(-) create mode 100644 support_code/nmdc/lipidomics/manifest_examples.py diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index e0c2f4819..f976e9eb9 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -8,6 +8,7 @@ import json import multiprocessing from pathlib import Path +import datetime import pandas as pd import warnings @@ -26,6 +27,159 @@ from corems.mass_spectra.input.mzml import MZMLSpectraParser +def create_manifest_from_folder( + folder_path: Path, + output_path: Path = None, + batch_time_threshold_hours: float = 12.0, + center_name: str = None, + overwrite: bool = False +) -> Path: + """ + Create a manifest CSV file for ReadCoreMSHDFMassSpectraCollection from CoreMS HDF5 files. + + Scans a folder for .corems subdirectories and generates a manifest with columns: + sample_name, batch, order, center, time. Files are batched by creation time, and + one sample is designated as the retention time alignment center. + + Parameters + ---------- + folder_path : Path + Path to folder containing .corems subdirectories with HDF5 files. + output_path : Path, optional + Output manifest CSV path. Default: folder_path/manifest.csv. + batch_time_threshold_hours : float, optional + Time gap in hours for batch separation. Default: 12.0. + center_name : str, optional + Sample name to designate as RT alignment center (must exist in samples). + If None, the middle sample (by creation time) is used. + overwrite : bool, optional + Whether to overwrite existing manifest. Default: False. + + Returns + ------- + Path + Path to created manifest file. + + Raises + ------ + FileNotFoundError + If folder_path doesn't exist or contains no .corems subdirectories. + FileExistsError + If output file exists and overwrite is False. + ValueError + If no HDF5 files found, or center_name doesn't match any sample. + """ + if not folder_path.exists(): + raise FileNotFoundError(f"Folder {folder_path} does not exist.") + + # Set default output path if not provided + if output_path is None: + output_path = folder_path / "manifest.csv" + + # Check if output file exists + if output_path.exists() and not overwrite: + raise FileExistsError( + f"Manifest file {output_path} already exists. " + "Set overwrite=True to replace it." + ) + + # Find all .corems subdirectories + corems_dirs = sorted([d for d in folder_path.iterdir() if d.is_dir() and d.suffix == ".corems"]) + + if not corems_dirs: + raise FileNotFoundError( + f"No .corems subdirectories found in {folder_path}. " + "Ensure the folder contains processed CoreMS data." + ) + + # Collect sample information + sample_data = [] + + for corems_dir in corems_dirs: + sample_name = corems_dir.stem # Remove .corems extension + hdf5_file = corems_dir / f"{sample_name}.hdf5" + + if not hdf5_file.exists(): + print(f"Warning: HDF5 file not found for {sample_name}, skipping.") + continue + + # Get file creation time from the HDF5 file + creation_timestamp = hdf5_file.stat().st_mtime + creation_time = datetime.datetime.fromtimestamp(creation_timestamp) + + sample_data.append({ + 'sample_name': sample_name, + 'creation_time': creation_time, + 'hdf5_path': hdf5_file + }) + + if not sample_data: + raise ValueError( + f"No valid HDF5 files found in {folder_path}. " + "Ensure .corems subdirectories contain .hdf5 files." + ) + + # Sort by creation time + sample_data.sort(key=lambda x: x['creation_time']) + + # Assign batches based on time threshold + batch_assignments = [] + current_batch = 1 + + for i, sample in enumerate(sample_data): + if i == 0: + batch_assignments.append(current_batch) + else: + time_diff = sample['creation_time'] - sample_data[i-1]['creation_time'] + time_diff_hours = time_diff.total_seconds() / 3600 + + if time_diff_hours > batch_time_threshold_hours: + current_batch += 1 + + batch_assignments.append(current_batch) + + # Determine which sample should be the center for retention time alignment + sample_names = [s['sample_name'] for s in sample_data] + + if center_name is not None: + # Validate that center_name is in the discovered samples + if center_name not in sample_names: + raise ValueError( + f"Specified center_name '{center_name}' not found in discovered samples. " + f"Available samples: {', '.join(sample_names)}" + ) + center_sample = center_name + else: + # Use the middle sample (by creation time) as center + middle_idx = len(sample_data) // 2 + center_sample = sample_data[middle_idx]['sample_name'] + print(f"Auto-selected center sample: {center_sample} (index {middle_idx} of {len(sample_data)}, middle by creation time)") + + # Create manifest dataframe with center column as TRUE/FALSE + manifest_df = pd.DataFrame({ + 'sample_name': sample_names, + 'batch': batch_assignments, + 'order': list(range(1, len(sample_data) + 1)), + 'center': ['TRUE' if name == center_sample else 'FALSE' for name in sample_names], + 'time': [s['creation_time'].strftime('%Y-%m-%dT%H:%M:%SZ') for s in sample_data] + }) + + # Sort manifest by time before saving to ensure proper order + manifest_df = manifest_df.sort_values('time').reset_index(drop=True) + # Update order column to reflect sorted order + manifest_df['order'] = list(range(1, len(manifest_df) + 1)) + + # Save manifest + manifest_df.to_csv(output_path, index=False) + + print(f"Manifest created successfully at {output_path}") + print(f"Total samples: {len(sample_data)}") + print(f"Number of batches: {current_batch}") + print(f"Batch assignments: {dict(zip(range(1, current_batch + 1), [batch_assignments.count(b) for b in range(1, current_batch + 1)]))}") + + return output_path + + class ReadCoreMSHDFMassSpectra( SpectraParserInterface, ReadCoreMSHDF_MassSpectrum, Thread ): @@ -613,41 +767,64 @@ def get_instrument_info(self): class ReadCoreMSHDFMassSpectraCollection: - """Class to read a collection of CoreMS HDF5 files and populate a LCMSCollection object. + """Read a collection of CoreMS HDF5 files and populate an LCMSCollection object. Parameters ---------- - folder_location : str - The location of the folder containing the CoreMS HDF5 files. - manifest_file : str - The location of the manifest file containing the sample names, order, and batch. - This must be a csv with the following columns: 'sample_name', 'order', 'batch'. - Other fields can be included in the manifest file, but these are required. - cores : int - The number of cores to use for multiprocessing. Default is 1. + folder_location : Path + Folder containing .corems subdirectories with HDF5 files. + manifest_file : Path, optional + Manifest CSV with columns: sample_name, order, batch, center, time. + One sample must have center='TRUE' for RT alignment. + If None, auto-generates from folder contents. Default: None. + cores : int, optional + Number of cores for multiprocessing. Default: 1. + auto_manifest_batch_threshold_hours : float, optional + Time gap (hours) for auto-generated batch separation. Default: 12.0. + auto_manifest_center_name : str, optional + Sample name for RT alignment center when auto-generating. + Must match a discovered sample. If None, uses middle sample. Default: None. Attributes ---------- - folder_location : str - The location of the folder containing the CoreMS HDF5 files. - manifest_filepath : str - The location of the manifest file containing the sample names, order, and batch. + folder_location : Path + Folder containing CoreMS HDF5 files. + manifest_filepath : Path + Path to manifest file. + manifest : dict + Manifest data indexed by sample_name. """ def __init__( self, - folder_location: str, - manifest_file: str, - cores: int = 1 + folder_location: Path, + manifest_file: Path = None, + cores: int = 1, + auto_manifest_batch_threshold_hours: float = 12.0, + auto_manifest_center_name: str = None ): - # Check for folder location and manifest file + # Check for folder location + folder_location = Path(folder_location) if not folder_location.exists(): raise FileNotFoundError(f"Folder location {folder_location} not found.") - if not manifest_file.exists(): - raise FileNotFoundError(f"Manifest file {manifest_file} not found.") - - # Check if the manifest file is a CSV - if manifest_file.suffix != ".csv": - raise ValueError("Manifest file must be a CSV.") + + # Auto-generate manifest if not provided + if manifest_file is None: + print(f"No manifest file provided. Auto-generating manifest from {folder_location}") + manifest_file = create_manifest_from_folder( + folder_path=folder_location, + output_path=folder_location / "manifest_auto.csv", + batch_time_threshold_hours=auto_manifest_batch_threshold_hours, + center_name=auto_manifest_center_name, + overwrite=True + ) + else: + manifest_file = Path(manifest_file) + if not manifest_file.exists(): + raise FileNotFoundError(f"Manifest file {manifest_file} not found.") + + # Check if the manifest file is a CSV + if manifest_file.suffix != ".csv": + raise ValueError("Manifest file must be a CSV.") self.folder_location = folder_location self._manifest_dict = None @@ -691,6 +868,14 @@ def _validate_manifest(self): hdf5_file = corems_dir / f"{sample_name}.hdf5" if not hdf5_file.exists(): raise FileNotFoundError(f"HDF5 file for {sample_name} not found.") + + # Check that at least one sample has center='TRUE' for retention time alignment + center_values = [sample_data.get('center') for sample_data in self._manifest_dict.values()] + if not any(center_val == 'TRUE' or center_val == True for center_val in center_values): + raise ValueError( + "Manifest must contain at least one sample with center='TRUE' for retention time alignment. " + "None of the samples in the manifest have center='TRUE'." + ) def _validate_parameters(self): """Validate that the parameters used for all samples within a batch are the same.""" @@ -1001,6 +1186,10 @@ def get_lcms_collection(self, load_raw=False, load_light=False): # Load induced mass features if they exist self._load_induced_mass_features(lcms_collection) + + # Combine induced mass features into the collection-level dataframe if any were loaded + if lcms_collection.missing_mass_features_searched: + lcms_collection._combine_mass_features(induced_features=True) return lcms_collection diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 95a2d0913..33fe06f36 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -1,5 +1,6 @@ from pathlib import Path import time +import pandas as pd from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectraCollection, ReadSavedLCMSCollection from corems.mass_spectra.output.export import LCMSCollectionExport @@ -8,7 +9,7 @@ # Set the path to the collection of LCMS runs (previously processed) collection_path = Path("/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test_out2") # Path to manifest file - manifest_file = Path("/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test_out2/manifest.csv") + manifest_file = Path("/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test_out2/manifest_tiny.csv") # Set the number of cores to use for loading the data (the parser is parallelized) ncores = 6 @@ -76,42 +77,22 @@ cores=ncores) lcms_collection2 = reader.get_lcms_collection(load_raw=False, load_light=True) - # Verify basic collection structure - assert len(lcms_collection2) == len(lcms_collection), "Re-loaded LCMS collection should have the same number of samples." - assert len(lcms_collection2.mass_features_dataframe) == len(lcms_collection.mass_features_dataframe), "Re-loaded LCMS collection should have the same number of mass features." - assert str(lcms_collection2[0].raw_file_location) == str(lcms_collection[0].raw_file_location), "Re-loaded LCMS collection should have the same raw file locations." - assert lcms_collection2.rt_aligned == lcms_collection.rt_aligned and lcms_collection.rt_aligned, "Both LCMS collections should be marked as retention time aligned." - assert "cluster" in lcms_collection2.mass_features_dataframe.columns, "Re-loaded LCMS collection mass features should have cluster assignments." - - # Verify parameters were saved and loaded correctly (using the modified parameters from earlier) + # Verify parameters, induced features, and missing mass features searched flag upon reload assert lcms_collection2.parameters.lcms_collection.alignment_acceptance_fraction_improved_threshold == -1, \ f"Re-loaded threshold parameter should be -1, got {lcms_collection2.parameters.lcms_collection.alignment_acceptance_fraction_improved_threshold}" - assert lcms_collection2.parameters.lcms_collection.alignment_acceptance_technique == ['fraction_improved'], \ - f"Re-loaded technique parameter should be ['fraction_improved'], got {lcms_collection2.parameters.lcms_collection.alignment_acceptance_technique}" - - # Verify induced mass features were saved and loaded correctly assert lcms_collection2.missing_mass_features_searched, "Re-loaded collection should have missing_mass_features_searched=True" - - # Count induced mass features in both collections original_induced_count = sum([len(lcms_obj.induced_mass_features) for lcms_obj in lcms_collection]) reloaded_induced_count = sum([len(lcms_obj.induced_mass_features) for lcms_obj in lcms_collection2]) assert reloaded_induced_count == original_induced_count, \ f"Re-loaded induced mass features count mismatch: {reloaded_induced_count} vs {original_induced_count}" - - # Verify at least one sample has induced mass features assert reloaded_induced_count > 0, "Collection should have some induced mass features after gap filling" - - # Verify a few induced mass features have the correct attributes - for lcms_idx, lcms_obj in enumerate(lcms_collection2): - if len(lcms_obj.induced_mass_features) > 0: - # Get first induced mass feature - first_mf_id = list(lcms_obj.induced_mass_features.keys())[0] - first_mf = lcms_obj.induced_mass_features[first_mf_id] - # Verify it has the expected attributes - assert hasattr(first_mf, 'mz'), "Induced mass feature should have mz attribute" - assert hasattr(first_mf, 'intensity'), "Induced mass feature should have intensity attribute" - assert hasattr(first_mf, 'apex_scan'), "Induced mass feature should have apex_scan attribute" - break + + # Verify the pivot table outputs are the same before and after save/load + original_pivot = lcms_collection.collection_pivot_table() + reloaded_pivot = lcms_collection2.collection_pivot_table() + pd.testing.assert_frame_equal(original_pivot, reloaded_pivot, check_dtype=False) + + print('Test completed successfully! LCMSCollection save and load functionality works as expected.') """# Make some more plots lcms_collection.plot_mz_features_across_samples() @@ -130,7 +111,6 @@ 'persistence' ] lcms_collection.plot_cluster_outlier_frequency(dim_list, clu_size_thresh = 0.25) - """ # Create pivot tables and reports results = lcms_collection.collection_pivot_table() @@ -141,3 +121,4 @@ #TODO KRH: Add visualization of a consensus mass feature #TODO KRH: Add visualization of matched spectrum with consensus mass feature #TODO KRH: Add code to deal with annotations of features in the collection context + """ \ No newline at end of file diff --git a/support_code/nmdc/lipidomics/manifest_examples.py b/support_code/nmdc/lipidomics/manifest_examples.py new file mode 100644 index 000000000..50913abcc --- /dev/null +++ b/support_code/nmdc/lipidomics/manifest_examples.py @@ -0,0 +1,49 @@ +""" +Simple examples demonstrating manifest creation utility usage. +""" + +from pathlib import Path +from corems.mass_spectra.input.corems_hdf5 import create_manifest_from_folder + +# Example 1: Basic usage - auto-select middle sample as center +print("Example 1: Basic usage (middle sample as center)") +print("-" * 60) +manifest_path = create_manifest_from_folder( + folder_path=Path("/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test_out2"), + overwrite=True +) +print() + +# Example 2: Custom batch threshold +print("Example 2: Custom batch threshold (24 hours)") +print("-" * 60) +manifest_path = create_manifest_from_folder( + folder_path=Path("/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test_out2"), + output_path=Path("/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test_out2/manifest_24hr.csv"), + batch_time_threshold_hours=24.0, + overwrite=True +) +print() + +# Example 3: Specify a sample as center by name +print("Example 3: Specify center sample by name") +print("-" * 60) +manifest_path = create_manifest_from_folder( + folder_path=Path("/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test_out2"), + output_path=Path("/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test_out2/manifest_custom_center.csv"), + batch_time_threshold_hours=12.0, + center_name="Blanch_Nat_Lip_C_7_AB_M_07_POS_23Jan18_Brandi-WCSH5801", + overwrite=True +) +print() + +# Example 4: Strict batch threshold +print("Example 4: Strict batch threshold (1 hour)") +print("-" * 60) +manifest_path = create_manifest_from_folder( + folder_path=Path("/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test_out2"), + output_path=Path("/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test_out2/manifest_1hr.csv"), + batch_time_threshold_hours=1.0, + overwrite=True +) +print() From 47d81faff6e70c4a722fca0901bcebdc0c1b1939 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 16 Jan 2026 11:40:44 -0800 Subject: [PATCH 096/158] Add resource manager for hdf5 mass spectra reader --- corems/mass_spectra/input/corems_hdf5.py | 118 ++++++++++++++++++++--- corems/mass_spectra/output/export.py | 9 ++ tests/test_lcms_metabolomics.py | 2 +- tests/test_wf_lipidomics.py | 6 ++ 4 files changed, 119 insertions(+), 16 deletions(-) diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index f976e9eb9..cdbf045ba 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -103,9 +103,22 @@ def create_manifest_from_folder( print(f"Warning: HDF5 file not found for {sample_name}, skipping.") continue - # Get file creation time from the HDF5 file - creation_timestamp = hdf5_file.stat().st_mtime - creation_time = datetime.datetime.fromtimestamp(creation_timestamp) + # Get creation time using the ReadCoreMSHDFMassSpectra method + try: + # Use context manager to ensure file is properly closed + with ReadCoreMSHDFMassSpectra(str(hdf5_file)) as parser: + # Use the get_original_creation_time() method which checks HDF5 attrs first, + # then falls back to original parser if needed + creation_time = parser.get_original_creation_time() + + # Skip sample if creation time unavailable + if creation_time is None: + print(f"Warning: Could not get original creation time for {sample_name}, skipping.") + continue + + except Exception as e: + print(f"Warning: Error getting creation time for {sample_name}: {e}, skipping.") + continue sample_data.append({ 'sample_name': sample_name, @@ -266,6 +279,21 @@ def __init__(self, file_location: str): self.parameters_location = [x for x in add_files if x.suffix == ".toml"][0] else: self.parameters_location = None + + def __enter__(self): + """Context manager entry.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit - closes the HDF5 file.""" + if hasattr(self, 'h5pydata') and self.h5pydata is not None: + self.h5pydata.close() + return False + + def close(self): + """Explicitly close the HDF5 file.""" + if hasattr(self, 'h5pydata') and self.h5pydata is not None: + self.h5pydata.close() def get_mass_spectrum_from_scan(self, scan_number): """Return mass spectrum data object from scan number.""" @@ -745,15 +773,75 @@ def add_original_parser(self, mass_spectra, raw_file_path=None): return mass_spectra + def get_original_creation_time(self): + """ + Get the creation time of the original raw data file. + + First checks if creation_time is saved in the HDF5 file attributes. + If not found, attempts to instantiate the original parser and get the creation time. + + Returns + ------- + datetime + The creation time of the original raw data file, or None if not available. + """ + # Check if creation_time is saved in HDF5 attributes + if "creation_time" in self.h5pydata.attrs: + from datetime import datetime + return datetime.fromisoformat(self.h5pydata.attrs["creation_time"]) + + # Fall back to using original parser to get creation time + try: + # Get the original parser type and raw file path + og_parser_type = self.h5pydata.attrs.get("parser_type") + raw_file_path = self.get_raw_file_location() + + if og_parser_type is None or raw_file_path is None: + warnings.warn( + "Cannot retrieve creation time: parser_type or original_file_location not found in HDF5 attributes." + ) + return None + + # Check if raw file exists + from pathlib import Path + if not Path(raw_file_path).exists(): + warnings.warn( + f"Cannot retrieve creation time: original raw file not found at {raw_file_path}" + ) + return None + + # Instantiate the original parser + if og_parser_type == "ImportMassSpectraThermoMSFileReader": + parser = ImportMassSpectraThermoMSFileReader(raw_file_path) + elif og_parser_type == "MZMLSpectraParser": + parser = MZMLSpectraParser(raw_file_path) + else: + warnings.warn( + f"Unknown parser type: {og_parser_type}, cannot retrieve creation time." + ) + return None + + # Get creation time from parser + return parser.get_creation_time() + + except Exception as e: + warnings.warn( + f"Failed to retrieve creation time from original parser: {e}" + ) + return None + def get_creation_time(self): """ - Raise a NotImplemented Warning, as creation time is not available in CoreMS HDF5 files and returning None. + Get the creation time of the original raw data file. + + This is an alias for get_original_creation_time() for backward compatibility. + + Returns + ------- + datetime + The creation time of the original raw data file, or None if not available. """ - warnings.warn( - "Creation time is not available in CoreMS HDF5 files, returning None." - "This should be accessed through the original parser.", - ) - return None + return self.get_original_creation_time() def get_instrument_info(self): """ @@ -932,12 +1020,12 @@ def get_lcms_obj(self, sample_name: str, load_raw=False, load_light=True, use_or If True, only load the parameters, mass features, and scan info are initially loaded for each lcms object. Default is True. """ hdf5_file = self.folder_location / f"{sample_name}.corems/{sample_name}.hdf5" - parser = ReadCoreMSHDFMassSpectra(hdf5_file) - lcms_obj = parser.get_lcms_obj(load_raw=load_raw, load_light=load_light, use_original_parser=use_original_parser, raw_file_path=raw_file_path) - if load_light: - mf_df = lcms_obj.mass_features_to_df() - lcms_obj.mass_features = {} - lcms_obj.light_mf_df = mf_df + with ReadCoreMSHDFMassSpectra(hdf5_file) as parser: + lcms_obj = parser.get_lcms_obj(load_raw=load_raw, load_light=load_light, use_original_parser=use_original_parser, raw_file_path=raw_file_path) + if load_light: + mf_df = lcms_obj.mass_features_to_df() + lcms_obj.mass_features = {} + lcms_obj.light_mf_df = mf_df return lcms_obj def get_lcms_collection(self, load_raw = False, load_light = True, use_original_parser = True) -> LCMSCollection: diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index c288dbd5d..548a5617d 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -993,6 +993,15 @@ def to_hdf(self, overwrite=False, export_raw=True): hdf_handle.attrs["original_file_location"] = ( self.mass_spectra.file_location._str ) + + # Save creation time from original parser if available + try: + if hasattr(self.mass_spectra, 'spectra_parser') and self.mass_spectra.spectra_parser is not None: + creation_time = self.mass_spectra.spectra_parser.get_creation_time() + if creation_time is not None: + hdf_handle.attrs["creation_time"] = creation_time.isoformat() + except Exception: + pass # If creation time cannot be retrieved, skip it if "mass_spectra" not in hdf_handle: mass_spectra_group = hdf_handle.create_group("mass_spectra") diff --git a/tests/test_lcms_metabolomics.py b/tests/test_lcms_metabolomics.py index 6fb2e7b4f..ed4c539d2 100644 --- a/tests/test_lcms_metabolomics.py +++ b/tests/test_lcms_metabolomics.py @@ -9,7 +9,7 @@ from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectra -def xtest_lcms_metabolomics(postgres_database, lcms_obj, msp_file_location): +def test_lcms_metabolomics(postgres_database, lcms_obj, msp_file_location): # Delete the "Blanch_Nat_Lip_C_12_AB_M_17_NEG_25Jan18_Brandi-WCSH5801.corems" directory shutil.rmtree( "Blanch_Nat_Lip_C_12_AB_M_17_NEG_25Jan18_Brandi-WCSH5801.corems", diff --git a/tests/test_wf_lipidomics.py b/tests/test_wf_lipidomics.py index d88aba9a4..1ba7ff0ec 100644 --- a/tests/test_wf_lipidomics.py +++ b/tests/test_wf_lipidomics.py @@ -228,6 +228,12 @@ def test_lipidomics_workflow(postgres_database, lcms_obj): parser = ReadCoreMSHDFMassSpectra( "Blanch_Nat_Lip_C_12_AB_M_17_NEG_25Jan18_Brandi-WCSH5801.corems/Blanch_Nat_Lip_C_12_AB_M_17_NEG_25Jan18_Brandi-WCSH5801.hdf5" ) + + # Check that creation_time was saved and can be retrieved + creation_time = parser.get_original_creation_time() + assert creation_time is not None + assert creation_time.year == 2018 # Based on the filename date + myLCMSobj2 = parser.get_lcms_obj() # Check that the parameters match From bbb3f6076c18ffe12a3c4fc7f85714e9ce10e41e Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 16 Jan 2026 12:38:53 -0800 Subject: [PATCH 097/158] Add function and encapsulated params for getting representative sample for cluster --- .../factory/processingSetting.py | 9 +++ corems/mass_spectra/calc/lc_calc.py | 69 ++++++++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/corems/encapsulation/factory/processingSetting.py b/corems/encapsulation/factory/processingSetting.py index b8f6c06ab..deb9c936e 100644 --- a/corems/encapsulation/factory/processingSetting.py +++ b/corems/encapsulation/factory/processingSetting.py @@ -1187,6 +1187,15 @@ class LCMSCollectionSettings: # Gap-filling settings gap_fill_expand_on_miss: bool = True + + # Consensus mass feature visualization parameters + consensus_representative_metric: str = 'intensity' + consensus_representative_metrics_available: tuple = ('intensity', 'persistence', 'area') + """ + Metric used to determine the most representative sample for a consensus mass feature. + Options: 'intensity', 'persistence', 'area', or any column in mass_features_dataframe. + Default is 'intensity'. + """ def __post_init__(self): self.consensus_mz_tol_ppm = self.alignment_mz_tol_ppm diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 9003ba1c1..9df8d7907 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -3068,7 +3068,8 @@ def summarize_clusters(self): if col != "cluster" ] summary_df = summary_df.rename(columns={"cluster_": "cluster"}) - summary_df = summary_df.reset_index(drop=True) + # Set cluster as the index for easy lookup + summary_df = summary_df.set_index('cluster') return summary_df def plot_mz_features_per_cluster(self, return_fig = False): @@ -3234,6 +3235,72 @@ def plot_consensus_mz_features(self, xb = 'xb', xt = 'xt', yb = 'yb', yt = 'yt', return fig else: plt.show() + + def get_most_representative_sample_for_cluster(self, cluster_id, representative_metric=None): + """ + Get the most representative sample for a given cluster based on a metric. + + Parameters + ---------- + cluster_id : int + The cluster ID to find the representative sample for. + representative_metric : str, optional + The metric to use to determine the most representative sample. + If None, uses the value from self.parameters.lcms_collection.consensus_representative_metric. + Options: 'intensity', 'persistence', or any column in mass_features_dataframe. + Default is None. + + Returns + ------- + dict + Dictionary containing: + - 'sample_id': The sample ID of the most representative sample + - 'sample_name': The sample name of the most representative sample + - 'mf_id': The mass feature ID (coll_mf_id) in the collection + - representative_metric: The value of the metric for this mass feature + + Raises + ------ + ValueError + If cluster_id is not found or if representative_metric is not a valid column. + """ + # Use default from parameters if not specified + if representative_metric is None: + representative_metric = self.parameters.lcms_collection.consensus_representative_metric + + # Get all mass features in this cluster + cluster_mfs = self.mass_features_dataframe[ + self.mass_features_dataframe['cluster'] == cluster_id + ].copy() + + if len(cluster_mfs) == 0: + # Try to provide helpful error message + available_clusters = self.mass_features_dataframe['cluster'].unique() + raise ValueError( + f"Cluster {cluster_id} not found in mass_features_dataframe. " + f"Available clusters: {sorted(available_clusters[:10].tolist())}... " + f"(showing first 10 of {len(available_clusters)} total clusters)" + ) + + # Check if metric exists + if representative_metric not in cluster_mfs.columns: + raise ValueError( + f"Metric '{representative_metric}' not found. Available columns: {cluster_mfs.columns.tolist()}" + ) + + # Find the mass feature with the highest value for the metric + max_idx = cluster_mfs[representative_metric].idxmax() + representative_mf = cluster_mfs.loc[max_idx] + + # Get sample name from sample_id + sample_name = self.samples[representative_mf['sample_id']] + + return { + 'sample_id': representative_mf['sample_id'], + 'sample_name': sample_name, + 'mf_id': max_idx, + representative_metric: representative_mf[representative_metric] + } def add_sparse_distance_matrix(self, features): if features is None: From 08ffa4fa0f80e119d95ce057ced2c9d8ac97394b Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 16 Jan 2026 16:43:57 -0800 Subject: [PATCH 098/158] Add start of re-adding mass features and associating MS2s --- .../factory/processingSetting.py | 2 +- corems/mass_spectra/calc/lc_calc.py | 400 ++++++++++++++++++ corems/mass_spectra/factory/lc_class.py | 111 +++-- corems/mass_spectra/input/corems_hdf5.py | 6 +- .../nmdc/lipidomics/lipidomics_collection.py | 9 + 5 files changed, 490 insertions(+), 38 deletions(-) diff --git a/corems/encapsulation/factory/processingSetting.py b/corems/encapsulation/factory/processingSetting.py index deb9c936e..9e6e67867 100644 --- a/corems/encapsulation/factory/processingSetting.py +++ b/corems/encapsulation/factory/processingSetting.py @@ -1193,7 +1193,7 @@ class LCMSCollectionSettings: consensus_representative_metrics_available: tuple = ('intensity', 'persistence', 'area') """ Metric used to determine the most representative sample for a consensus mass feature. - Options: 'intensity', 'persistence', 'area', or any column in mass_features_dataframe. + Options: 'intensity', 'persistence', 'area' Default is 'intensity'. """ diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 9df8d7907..fff9e3690 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -3301,6 +3301,406 @@ def get_most_representative_sample_for_cluster(self, cluster_id, representative_ 'mf_id': max_idx, representative_metric: representative_mf[representative_metric] } + + def add_consensus_ms1(self, auto_process=True, use_parser=True, spectrum_mode=None): + """ + Add MS1 spectra to representative samples for all consensus mass feature clusters. + + For each cluster, identifies the most representative sample (using + get_most_representative_sample_for_cluster) and adds the associated MS1 spectrum + to that sample's mass feature. First checks if MS1 data is already saved in HDF5, + then loads raw data if needed using parallelized approach. + + Must be run after add_consensus_mass_features(). Uses same logic as + add_associated_ms1 but only for mass features associated with clusters. + + Parameters + ---------- + auto_process : bool, optional + If True, auto-processes MS1 spectra before adding. Default is True. + use_parser : bool, optional + If True, invoke the spectra parser to get MS1 spectra. Default is True. + spectrum_mode : str or None, optional + The spectrum mode to use. If None, determines from parser. + Default is None. + + Returns + ------- + None + Updates mass features in each sample with associated MS1 spectra. + + Raises + ------ + ValueError + If cluster_summary_dataframe is not set (run add_consensus_mass_features first). + + Notes + ----- + - Checks one sample to see if MS1 data exists in HDF5 + - If not in HDF5, loads raw data and generates MS1 using parallelized approach + - Uses encapsulated parameters from lc_ms.ms1_scans_to_average + - Only processes mass features that are part of clusters + - Vectorized for speed using batch processing per sample + - Reloads mass features for samples when needed (if loaded with load_light=True) + + See Also + -------- + get_most_representative_sample_for_cluster : Gets representative sample + get_ms1_for_cluster : Helper to retrieve MS1 for specific cluster + add_associated_ms1 : Single-sample MS1 addition method + """ + #TODO: Refactor to use _add_consensus_ms1_for_sample helper method + raise NotImplementedError("This method is bad, remove before committing.") + # Validate prerequisites + if not hasattr(self, 'cluster_summary_dataframe') or self.cluster_summary_dataframe is None: + raise ValueError( + "cluster_summary_dataframe not found. Must run add_consensus_mass_features() first." + ) + + # Check if MS1 data exists in HDF5 by checking one sample + sample_0 = self[0] + ms1_in_hdf5 = hasattr(sample_0, '_ms') and len(sample_0._ms) > 0 + + # Get all unique clusters + clusters = self.mass_features_dataframe['cluster'].unique() + + # For each cluster, get the representative sample + # Build a dictionary of sample_id -> list of mf_ids that need MS1 + sample_mf_map = {} + for cluster_id in clusters: + rep_info = self.get_most_representative_sample_for_cluster(cluster_id) + sample_id = rep_info['sample_id'] + mf_id = rep_info['mf_id'] + + if sample_id not in sample_mf_map: + sample_mf_map[sample_id] = [] + sample_mf_map[sample_id].append(mf_id) + + # Reload mass features for samples that need them + # Check if the specific mass features we need are loaded + for sample_id in sample_mf_map.keys(): + # Get local_mf_ids from collection mf_ids + local_mf_ids = [] + for coll_mf_id in sample_mf_map[sample_id]: + parts = str(coll_mf_id).split('_', 1) + if len(parts) == 2: + try: + local_mf_id = int(parts[1]) + except ValueError: + local_mf_id = parts[1] + else: + local_mf_id = coll_mf_id + local_mf_ids.append(local_mf_id) + + # Check if all needed mass features are loaded + sample = self[sample_id] + missing_mfs = [local_id for local_id in local_mf_ids if local_id not in sample.mass_features] + + print(f"DEBUG add_consensus_ms1: sample_id={sample_id}, needed={len(local_mf_ids)}, loaded={len(sample.mass_features)}, missing={len(missing_mfs)}") + + if len(missing_mfs) > 0: + # Need to reload mass features for this sample + # Only reload the specific mass features we need + print(f"DEBUG: Reloading {len(sample_mf_map[sample_id])} mass features for sample {sample_id}") + self._reload_sample_mass_features(sample_id, mf_ids_to_load=sample_mf_map[sample_id]) + + # Process each sample that has representative mass features + if self.parameters.lcms_collection.cores == 1: + for sample_id in tqdm(sample_mf_map.keys(), desc="Adding consensus MS1", unit="sample"): + self._add_consensus_ms1_for_sample( + sample_id, + sample_mf_map[sample_id], + ms1_in_hdf5, + auto_process, + use_parser, + spectrum_mode + ) + else: + # Parallel processing + if self.parameters.lcms_collection.cores > len(sample_mf_map): + ncores = len(sample_mf_map) + else: + ncores = self.parameters.lcms_collection.cores + + pool = multiprocessing.Pool(ncores) + pool.starmap( + self._add_consensus_ms1_for_sample, + [(sid, sample_mf_map[sid], ms1_in_hdf5, auto_process, use_parser, spectrum_mode) + for sid in sample_mf_map.keys()] + ) + pool.close() + pool.join() + + def reload_representative_mass_features(self, add_ms2=False, auto_process_ms2=True, ms2_spectrum_mode=None, ms2_scan_filter=None): + """ + Reload mass features for all representative samples in the cluster summary. + + This method is useful when the collection was loaded with load_light=True, + which stores mass features only in the collection dataframe. This reloads + the specific mass features that are representatives for each cluster, + allowing them to be accessed as LCMSMassFeature objects. + + Parameters + ---------- + add_ms2 : bool, optional + If True, also loads and associates MS2 spectra with mass features. Default is False. + auto_process_ms2 : bool, optional + If True and add_ms2=True, auto-processes MS2 spectra. Default is True. + ms2_spectrum_mode : str or None, optional + Spectrum mode for MS2 spectra. If None, determines from parser. Default is None. + ms2_scan_filter : str or None, optional + Filter string for MS2 scans (e.g., 'hcd'). Default is None. + + Returns + ------- + dict + Dictionary mapping sample_id to list of reloaded mf_ids. + + Raises + ------ + ValueError + If cluster_summary_dataframe is not set (run add_consensus_mass_features first). + + Notes + ----- + - Only reloads mass features that are cluster representatives + - Uses get_most_representative_sample_for_cluster() to determine which to reload + - More memory-efficient than reloading all mass features + - Parallelized based on lcms_collection.cores parameter + - MS2 association uses same logic as add_associated_ms2_dda() + + See Also + -------- + _reload_sample_mass_features : Low-level method to reload specific mass features + get_most_representative_sample_for_cluster : Gets representative sample for cluster + """ + # Validate prerequisites + if not hasattr(self, 'cluster_summary_dataframe') or self.cluster_summary_dataframe is None: + raise ValueError( + "cluster_summary_dataframe not found. Must run add_consensus_mass_features() first." + ) + + # Get all unique clusters + clusters = self.mass_features_dataframe['cluster'].unique() + + # Build a dictionary of sample_id -> list of mf_ids that are representatives + sample_mf_map = {} + for cluster_id in clusters: + rep_info = self.get_most_representative_sample_for_cluster(cluster_id) + sample_id = rep_info['sample_id'] + mf_id = rep_info['mf_id'] + + if sample_id not in sample_mf_map: + sample_mf_map[sample_id] = [] + sample_mf_map[sample_id].append(mf_id) + + # Reload mass features for each sample (parallelized) + if self.parameters.lcms_collection.cores == 1: + # Serial processing + from tqdm import tqdm + for sample_id in tqdm(sample_mf_map.keys(), desc="Reloading representative mass features", unit="sample"): + mf_ids = sample_mf_map[sample_id] + self._reload_sample_mass_features(sample_id, mf_ids_to_load=mf_ids, add_ms2=add_ms2, + auto_process_ms2=auto_process_ms2, ms2_spectrum_mode=ms2_spectrum_mode, + ms2_scan_filter=ms2_scan_filter) + else: + # Parallel processing + import multiprocessing + from tqdm import tqdm + + if self.parameters.lcms_collection.cores > len(sample_mf_map): + ncores = len(sample_mf_map) + else: + ncores = self.parameters.lcms_collection.cores + + pool = multiprocessing.Pool(ncores) + + # Build arguments list for starmap + args_list = [ + (sample_id, sample_mf_map[sample_id], add_ms2, auto_process_ms2, + ms2_spectrum_mode, ms2_scan_filter, False) + for sample_id in sample_mf_map.keys() + ] + + # Execute in parallel + mp_result = pool.starmap(self._reload_sample_mass_features, args_list) + pool.close() + pool.join() + + # Collect results back into samples + for i, sample_id in enumerate(tqdm(sample_mf_map.keys(), desc="Collecting reloaded mass features", unit="sample")): + self[sample_id].mass_features = mp_result[i] + + return sample_mf_map + + def _associate_ms2_with_mass_features(self, sample, local_mf_ids, auto_process=True, + spectrum_mode=None, scan_filter=None): + """ + Associate MS2 spectra with specific mass features in a sample. + + Uses the LCMSBase helper method to find and load MS2 scans for the specified mass features. + + Parameters + ---------- + sample : LCMSBase + The sample object containing mass features and scan data. + local_mf_ids : list of int + List of local (sample-level) mass feature IDs to find MS2 for. + auto_process : bool, optional + If True, auto-processes the MS2 spectra. Default is True. + spectrum_mode : str or None, optional + Spectrum mode for MS2 spectra. If None, determines from parser. Default is None. + scan_filter : str or None, optional + Filter string for MS2 scans (e.g., 'hcd'). Default is None. + + Returns + ------- + dict + Dictionary of scan_number -> MassSpectrum objects for the loaded MS2 spectra. + """ + # Check if we have scan data + if not hasattr(sample, 'scan_df') or sample.scan_df is None: + return {} + + # Separate mass features into those that need scan finding vs those that already have scans + mfs_needing_scan_finding = [] + unique_dda_scans = set() + + for mf_id in local_mf_ids: + if mf_id not in sample.mass_features: + continue + mf = sample.mass_features[mf_id] + # If this mass feature already has MS2 scans, add them to our set + if mf.ms2_scan_numbers is not None and len(mf.ms2_scan_numbers) > 0: + unique_dda_scans.update(mf.ms2_scan_numbers) + else: + # Otherwise, we need to find scans for this mass feature + mfs_needing_scan_finding.append(mf_id) + + # Only run the scan finding for mass features that need it + if mfs_needing_scan_finding: + found_scans = sample._find_ms2_scans_for_mass_features( + mf_ids=mfs_needing_scan_finding, + scan_filter=scan_filter + ) + unique_dda_scans.update(found_scans) + + if len(unique_dda_scans) == 0: + return {} + + # Get ms2 parameters from sample + #TODO KRH: deal with different ms2 scan types here (CID vs HCD), may need to add scan translator to the initializeion + ms_params = sample.parameters.mass_spectrum['ms2'] + + # Load MS2 spectra (convert set to list) + sample.add_mass_spectra( + scan_list=list(unique_dda_scans), + auto_process=auto_process, + spectrum_mode=spectrum_mode, + use_parser=True, + ms_params=ms_params, + ) + + # Associate MS2 spectra with mass features + for mf_id in local_mf_ids: + if mf_id not in sample.mass_features: + continue + if sample.mass_features[mf_id].ms2_scan_numbers: + for dda_scan in sample.mass_features[mf_id].ms2_scan_numbers: + if dda_scan in sample._ms: + sample.mass_features[mf_id].ms2_mass_spectra[dda_scan] = sample._ms[dda_scan] + + # Return only the MS2 spectra we loaded (for parallel processing) + return {scan: sample._ms[scan] for scan in unique_dda_scans if scan in sample._ms} + + def _reload_sample_mass_features(self, sample_id, mf_ids_to_load=None, add_ms2=False, + auto_process_ms2=True, ms2_spectrum_mode=None, ms2_scan_filter=None, + inplace=True): + """ + Reload specific mass features for a sample from HDF5. + + 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 + ---------- + sample_id : int + The sample ID to reload mass features for. + mf_ids_to_load : list of str, optional + List of collection-level mf_ids (format: '{sample_id}_{local_mf_id}') to load. + If None, loads all mass features for the sample. + add_ms2 : bool, optional + If True, also loads and associates MS2 spectra. Default is False. + auto_process_ms2 : bool, optional + If True, auto-processes MS2 spectra. Default is True. + ms2_spectrum_mode : str or None, optional + Spectrum mode for MS2 spectra. Default is None. + ms2_scan_filter : str or None, optional + Filter string for MS2 scans. Default is None. + inplace : bool, optional + If True, updates the sample's mass_features in place. If False, returns the + mass_features dictionary (for multiprocessing). Default is True. + + Returns + ------- + dict or None + If inplace=False, returns dictionary of mass features. + Otherwise returns None and updates object in place. + """ + sample = self[sample_id] + sample_name = self.samples[sample_id] + + # Check if we have a collection parser that can reload + if not hasattr(self, 'collection_parser') or self.collection_parser is None: + print("Warning: Cannot reload mass features - no collection_parser available") + if not inplace: + return {} + return + + # Get the HDF5 file for this sample + hdf5_file = self.collection_parser.folder_location / f"{sample_name}.corems/{sample_name}.hdf5" + + if not hdf5_file.exists(): + print(f"Warning: HDF5 file not found for sample {sample_name}: {hdf5_file}") + if not inplace: + return {} + return + + # Import here to avoid circular imports + from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectra + + # If specific mf_ids requested, extract the local mf_ids we need + local_mf_ids_to_load = None + if mf_ids_to_load is not None: + local_mf_ids_to_load = set() + for coll_mf_id in mf_ids_to_load: + # Parse collection-level ID to get local ID + parts = str(coll_mf_id).split('_', 1) + if len(parts) == 2: + try: + local_mf_ids_to_load.add(int(parts[1])) + except ValueError: + local_mf_ids_to_load.add(parts[1]) + + # Reload mass features from HDF5 + with ReadCoreMSHDFMassSpectra(hdf5_file) as parser: + # Load mass features - if specific IDs requested, only load those + parser.import_mass_features(sample, mf_ids=local_mf_ids_to_load) + + # If add_ms2, associate MS2 spectra with the loaded mass features + if add_ms2 and local_mf_ids_to_load is not None: + self._associate_ms2_with_mass_features( + sample, + list(local_mf_ids_to_load), + auto_process=auto_process_ms2, + spectrum_mode=ms2_spectrum_mode, + scan_filter=ms2_scan_filter + ) + + # Return mass features if not inplace (for multiprocessing) + if not inplace: + return sample.mass_features def add_sparse_distance_matrix(self, features): if features is None: diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index c7987cabf..24322eef6 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -503,6 +503,74 @@ def remove_unprocessed_data(self, ms_level=None): raise ValueError("ms_level must be 1 or 2") self._ms_unprocessed[ms_level] = None + def _find_ms2_scans_for_mass_features(self, mf_ids=None, scan_filter=None): + """Find MS2 scans associated with mass features. + + This helper method finds MS2 scans that match mass features based on RT and m/z tolerances. + It updates the ms2_scan_numbers attribute on each mass feature. + + Parameters + ---------- + mf_ids : list of int, optional + List of mass feature IDs to find MS2 for. If None, finds for all mass features. + scan_filter : str, optional + Filter string for MS2 scans (e.g., 'hcd'). Default is None. + + Returns + ------- + list + List of unique MS2 scan numbers found across all mass features. + + Raises + ------ + ValueError + If no MS2 scans are found in the dataset. + """ + # Get mass features to process + if mf_ids is None: + mf_ids = list(self.mass_features.keys()) + + # Get mass features dataframe + mf_df = self.mass_features_to_df() + mf_df = mf_df.loc[mf_ids].copy() + + # Find ms2 scans that have a precursor m/z value + ms2_scans = self.scan_df[self.scan_df.ms_level == 2] + ms2_scans = ms2_scans[~ms2_scans.precursor_mz.isna()] + ms2_scans = ms2_scans[ms2_scans.tic > 0] + + if len(ms2_scans) == 0: + raise ValueError("No DDA scans found in dataset") + + if scan_filter is not None: + ms2_scans = ms2_scans[ms2_scans.scan_text.str.contains(scan_filter)] + + # Get tolerances from parameters + time_tol = self.parameters.lc_ms.ms2_dda_rt_tolerance + mz_tol = self.parameters.lc_ms.ms2_dda_mz_tolerance + + # For each mass feature, find the ms2 scans that are within the roi scan time and mz range + dda_scans = [] + for i, row in mf_df.iterrows(): + ms2_scans_filtered = ms2_scans[ + ms2_scans.scan_time.between( + row.scan_time - time_tol, row.scan_time + time_tol + ) + ] + ms2_scans_filtered = ms2_scans_filtered[ + ms2_scans_filtered.precursor_mz.between( + row.mz - mz_tol, row.mz + mz_tol + ) + ] + scan_list = ms2_scans_filtered.scan.tolist() + if scan_list: + self.mass_features[i].ms2_scan_numbers = ( + scan_list + list(self.mass_features[i].ms2_scan_numbers) + ) + dda_scans.extend(scan_list) + + return list(set(dda_scans)) + def add_associated_ms2_dda( self, auto_process=True, @@ -548,48 +616,19 @@ def add_associated_ms2_dda( # reconfigure ms_params to get the correct mass spectrum parameters from the key ms_params = self.parameters.mass_spectrum[ms_params_key] - mf_df = self.mass_features_to_df().copy() - # Find ms2 scans that have a precursor m/z value - ms2_scans = self.scan_df[self.scan_df.ms_level == 2] - ms2_scans = ms2_scans[~ms2_scans.precursor_mz.isna()] - # drop ms2 scans that have no tic - ms2_scans = ms2_scans[ms2_scans.tic > 0] - if ms2_scans is None: - raise ValueError("No DDA scans found in dataset") - - if scan_filter is not None: - ms2_scans = ms2_scans[ms2_scans.scan_text.str.contains(scan_filter)] - # set tolerance in rt space (in minutes) and mz space (in daltons) - time_tol = self.parameters.lc_ms.ms2_dda_rt_tolerance - mz_tol = self.parameters.lc_ms.ms2_dda_mz_tolerance - - # for each mass feature, find the ms2 scans that are within the roi scan time and mz range - dda_scans = [] - for i, row in mf_df.iterrows(): - ms2_scans_filtered = ms2_scans[ - ms2_scans.scan_time.between( - row.scan_time - time_tol, row.scan_time + time_tol - ) - ] - ms2_scans_filtered = ms2_scans_filtered[ - ms2_scans_filtered.precursor_mz.between( - row.mz - mz_tol, row.mz + mz_tol - ) - ] - dda_scans = dda_scans + ms2_scans_filtered.scan.tolist() - self.mass_features[i].ms2_scan_numbers = ( - ms2_scans_filtered.scan.tolist() - + self.mass_features[i].ms2_scan_numbers - ) - # add to _ms attribute + # Find MS2 scans for all mass features + dda_scans = self._find_ms2_scans_for_mass_features(scan_filter=scan_filter) + + # Load MS2 spectra self.add_mass_spectra( - scan_list=list(set(dda_scans)), + scan_list=dda_scans, auto_process=auto_process, spectrum_mode=spectrum_mode, use_parser=use_parser, ms_params=ms_params, ) - # associate appropriate _ms attribute to appropriate mass feature's ms2_mass_spectra attribute + + # Associate appropriate _ms attribute to appropriate mass feature's ms2_mass_spectra attribute for mf_id in self.mass_features: if self.mass_features[mf_id].ms2_scan_numbers is not None: for dda_scan in self.mass_features[mf_id].ms2_scan_numbers: diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index cdbf045ba..fb5e635d2 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -503,13 +503,15 @@ def import_parameters(self, mass_spectra) -> None: "Parameters file must be in JSON format, TOML format is not yet supported." ) - def import_mass_features(self, mass_spectra) -> None: + def import_mass_features(self, mass_spectra, mf_ids=None) -> None: """Imports the mass features from the HDF5 file. Parameters ---------- mass_spectra : LCMSBase | MassSpectraBase The MassSpectraBase or LCMSBase object to populate with mass features. + mf_ids : list, optional + A list of mass feature IDs to import. If None, all mass features are imported. Returns ------- @@ -520,6 +522,8 @@ def import_mass_features(self, mass_spectra) -> None: dict_group_load = self.h5pydata["mass_features"] dict_group_keys = dict_group_load.keys() for k in dict_group_keys: + if mf_ids is not None and int(k) not in mf_ids: + continue # Instantiate the MassFeature object mass_feature = LCMSMassFeature( mass_spectra, diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 33fe06f36..3ab12eb96 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -15,6 +15,7 @@ ncores = 6 # Instantiate the parser + # Note, these samples have not had the DDA MS2 scans associated or MS1 data loaded parser = ReadCoreMSHDFMassSpectraCollection( folder_location = collection_path, manifest_file = manifest_file, @@ -63,6 +64,14 @@ lcms_collection.fill_missing_cluster_features() print("Time to gap fill missing cluster features: ", time.time() - start_time, "seconds, using", ncores, " cores") + # Reload representative mass features with MS2 data associated + sample_mf_map = lcms_collection.reload_representative_mass_features( + add_ms2=True, + auto_process_ms2=True, + ms2_spectrum_mode=None, + ms2_scan_filter=None + ) + # Check save and load functionality for LCMSCollection print("Saving and re-loading LCMS collection to test save/load functionality") print(f"Before saving: missing_mass_features_searched = {lcms_collection.missing_mass_features_searched}") From e4fd39c8fd96e95f764cddc24988156803a04887 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 16 Jan 2026 19:12:42 -0800 Subject: [PATCH 099/158] Add piped operations for post-consensus feature processing --- corems/mass_spectra/calc/lc_calc.py | 559 ++++++++++++++---- .../mass_spectra/calc/lc_calc_operations.py | 436 ++++++++++++++ .../nmdc/lipidomics/lipidomics_collection.py | 102 +++- 3 files changed, 966 insertions(+), 131 deletions(-) create mode 100644 corems/mass_spectra/calc/lc_calc_operations.py diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index fff9e3690..35c047f65 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -3302,135 +3302,6 @@ def get_most_representative_sample_for_cluster(self, cluster_id, representative_ representative_metric: representative_mf[representative_metric] } - def add_consensus_ms1(self, auto_process=True, use_parser=True, spectrum_mode=None): - """ - Add MS1 spectra to representative samples for all consensus mass feature clusters. - - For each cluster, identifies the most representative sample (using - get_most_representative_sample_for_cluster) and adds the associated MS1 spectrum - to that sample's mass feature. First checks if MS1 data is already saved in HDF5, - then loads raw data if needed using parallelized approach. - - Must be run after add_consensus_mass_features(). Uses same logic as - add_associated_ms1 but only for mass features associated with clusters. - - Parameters - ---------- - auto_process : bool, optional - If True, auto-processes MS1 spectra before adding. Default is True. - use_parser : bool, optional - If True, invoke the spectra parser to get MS1 spectra. Default is True. - spectrum_mode : str or None, optional - The spectrum mode to use. If None, determines from parser. - Default is None. - - Returns - ------- - None - Updates mass features in each sample with associated MS1 spectra. - - Raises - ------ - ValueError - If cluster_summary_dataframe is not set (run add_consensus_mass_features first). - - Notes - ----- - - Checks one sample to see if MS1 data exists in HDF5 - - If not in HDF5, loads raw data and generates MS1 using parallelized approach - - Uses encapsulated parameters from lc_ms.ms1_scans_to_average - - Only processes mass features that are part of clusters - - Vectorized for speed using batch processing per sample - - Reloads mass features for samples when needed (if loaded with load_light=True) - - See Also - -------- - get_most_representative_sample_for_cluster : Gets representative sample - get_ms1_for_cluster : Helper to retrieve MS1 for specific cluster - add_associated_ms1 : Single-sample MS1 addition method - """ - #TODO: Refactor to use _add_consensus_ms1_for_sample helper method - raise NotImplementedError("This method is bad, remove before committing.") - # Validate prerequisites - if not hasattr(self, 'cluster_summary_dataframe') or self.cluster_summary_dataframe is None: - raise ValueError( - "cluster_summary_dataframe not found. Must run add_consensus_mass_features() first." - ) - - # Check if MS1 data exists in HDF5 by checking one sample - sample_0 = self[0] - ms1_in_hdf5 = hasattr(sample_0, '_ms') and len(sample_0._ms) > 0 - - # Get all unique clusters - clusters = self.mass_features_dataframe['cluster'].unique() - - # For each cluster, get the representative sample - # Build a dictionary of sample_id -> list of mf_ids that need MS1 - sample_mf_map = {} - for cluster_id in clusters: - rep_info = self.get_most_representative_sample_for_cluster(cluster_id) - sample_id = rep_info['sample_id'] - mf_id = rep_info['mf_id'] - - if sample_id not in sample_mf_map: - sample_mf_map[sample_id] = [] - sample_mf_map[sample_id].append(mf_id) - - # Reload mass features for samples that need them - # Check if the specific mass features we need are loaded - for sample_id in sample_mf_map.keys(): - # Get local_mf_ids from collection mf_ids - local_mf_ids = [] - for coll_mf_id in sample_mf_map[sample_id]: - parts = str(coll_mf_id).split('_', 1) - if len(parts) == 2: - try: - local_mf_id = int(parts[1]) - except ValueError: - local_mf_id = parts[1] - else: - local_mf_id = coll_mf_id - local_mf_ids.append(local_mf_id) - - # Check if all needed mass features are loaded - sample = self[sample_id] - missing_mfs = [local_id for local_id in local_mf_ids if local_id not in sample.mass_features] - - print(f"DEBUG add_consensus_ms1: sample_id={sample_id}, needed={len(local_mf_ids)}, loaded={len(sample.mass_features)}, missing={len(missing_mfs)}") - - if len(missing_mfs) > 0: - # Need to reload mass features for this sample - # Only reload the specific mass features we need - print(f"DEBUG: Reloading {len(sample_mf_map[sample_id])} mass features for sample {sample_id}") - self._reload_sample_mass_features(sample_id, mf_ids_to_load=sample_mf_map[sample_id]) - - # Process each sample that has representative mass features - if self.parameters.lcms_collection.cores == 1: - for sample_id in tqdm(sample_mf_map.keys(), desc="Adding consensus MS1", unit="sample"): - self._add_consensus_ms1_for_sample( - sample_id, - sample_mf_map[sample_id], - ms1_in_hdf5, - auto_process, - use_parser, - spectrum_mode - ) - else: - # Parallel processing - if self.parameters.lcms_collection.cores > len(sample_mf_map): - ncores = len(sample_mf_map) - else: - ncores = self.parameters.lcms_collection.cores - - pool = multiprocessing.Pool(ncores) - pool.starmap( - self._add_consensus_ms1_for_sample, - [(sid, sample_mf_map[sid], ms1_in_hdf5, auto_process, use_parser, spectrum_mode) - for sid in sample_mf_map.keys()] - ) - pool.close() - pool.join() - def reload_representative_mass_features(self, add_ms2=False, auto_process_ms2=True, ms2_spectrum_mode=None, ms2_scan_filter=None): """ Reload mass features for all representative samples in the cluster summary. @@ -4421,4 +4292,432 @@ def fill_missing_cluster_features(self): self.missing_mass_features_searched = True for sample_name in self.samples: - self._lcms[sample_name].mass_features = {} \ No newline at end of file + self._lcms[sample_name].mass_features = {} + + def process_samples_pipeline(self, operations, description="Processing samples", keep_raw_data=False): + """ + Execute a pipeline of operations on all samples in parallel. + + This method provides a flexible framework for performing multiple + sample-level operations in a single parallelized pass, which is more + efficient than calling separate methods sequentially. + + Parameters + ---------- + operations : list of SampleOperation + List of operations to perform on each sample, in order. + Each operation should be an instance of a class derived from + SampleOperation (see lc_calc_operations module). + description : str, optional + Progress bar description. Default is "Processing samples". + keep_raw_data : bool, optional + If True, keeps raw MS data loaded in memory after pipeline completes. + If False, cleans up raw data to free memory. Default is False. + + Returns + ------- + dict + Dictionary with results from pipeline execution, keyed by operation name. + Structure: {operation_name: {sample_id: result, ...}, ...} + + Raises + ------ + ValueError + If operations list is empty or contains invalid operations. + + Notes + ----- + - Operations are executed sequentially within each sample + - Samples are processed in parallel based on parameters.lcms_collection.cores + - Each operation can have conditional execution via can_execute() + - Results are collected back via collect_results() method of each operation + - Failed operations for a sample are logged but don't halt processing + - Raw MS data loaded by operations is automatically cleaned up unless keep_raw_data=True + + Examples + -------- + >>> from corems.mass_spectra.calc.lc_calc_operations import ( + ... GapFillOperation, ReloadFeaturesOperation + ... ) + >>> ops = [ + ... GapFillOperation('gap_fill', expand_on_miss=True), + ... ReloadFeaturesOperation('reload', add_ms2=True) + ... ] + >>> results = lcms_collection.process_samples_pipeline(ops) + + See Also + -------- + lc_calc_operations : Module containing built-in operation classes + fill_and_process_features : Convenience method combining common operations + """ + from corems.mass_spectra.calc.lc_calc_operations import SampleOperation + + # Validate operations + if not operations or len(operations) == 0: + raise ValueError("operations list cannot be empty") + + for op in operations: + if not isinstance(op, SampleOperation): + raise ValueError(f"All operations must be SampleOperation instances, got {type(op)}") + + # Prepare runtime parameters for each operation + # This is where we gather collection-level data that operations need + runtime_params = self._prepare_pipeline_runtime_params(operations) + runtime_params['keep_raw_data'] = keep_raw_data + + # Execute pipeline + sample_ct = len(self.samples) + + if self.parameters.lcms_collection.cores == 1: + # Serial processing + from tqdm import tqdm + results_by_operation = {op.name: {} for op in operations} + + for sample_id in tqdm(range(sample_ct), desc=description, unit="sample"): + sample_results = self._execute_sample_pipeline( + sample_id, operations, runtime_params, inplace=True + ) + # Collect results + for op_name, result in sample_results.items(): + results_by_operation[op_name][sample_id] = result + else: + # Parallel processing + import multiprocessing + from tqdm import tqdm + + if self.parameters.lcms_collection.cores > sample_ct: + ncores = sample_ct + else: + ncores = self.parameters.lcms_collection.cores + + pool = multiprocessing.Pool(ncores) + + # Build arguments for each sample + args_list = [ + (sample_id, operations, runtime_params, False) + for sample_id in range(sample_ct) + ] + + # Execute in parallel + mp_results = pool.starmap(self._execute_sample_pipeline, args_list) + pool.close() + pool.join() + + # Collect results back into collection + results_by_operation = {op.name: {} for op in operations} + for sample_id in tqdm(range(sample_ct), desc=f"Collecting {description} results", unit="sample"): + sample_results = mp_results[sample_id] + + # Let each operation collect its results + for i, op in enumerate(operations): + result = sample_results.get(op.name) + if result is not None: + op.collect_results(sample_id, result, self) + results_by_operation[op.name][sample_id] = result + + return results_by_operation + + def _prepare_pipeline_runtime_params(self, operations): + """ + Prepare runtime parameters needed by operations in the pipeline. + + This method gathers collection-level data that operations need, + such as cluster information for gap-filling or mf_ids for reloading. + + Parameters + ---------- + operations : list of SampleOperation + List of operations that will be executed + + Returns + ------- + dict + Dictionary of runtime parameters for operations + """ + from corems.mass_spectra.calc.lc_calc_operations import ( + GapFillOperation, ReloadFeaturesOperation + ) + + runtime_params = {} + + # Check if any operation needs gap-fill parameters + needs_gap_fill = any(isinstance(op, GapFillOperation) for op in operations) + if needs_gap_fill: + # Prepare gap-fill parameters (same as fill_missing_cluster_features) + min_cluster_presence = self.parameters.lcms_collection.consensus_min_sample_fraction + expand_on_miss = self.parameters.lcms_collection.gap_fill_expand_on_miss + + summarydf = self.cluster_summary_dataframe + mfdf = self.mass_features_dataframe + sample_ct = len(self.samples) + + # Identify clusters needing gap-filling + # Note: cluster_summary_dataframe has 'cluster' as index, need to reset it + missingdf = summarydf.reset_index()[[ + 'cluster', + 'sample_id_nunique', + 'mz_min', + 'mz_max', + 'scan_time_aligned_min', + 'scan_time_aligned_max' + ]].copy() + missingdf = missingdf[missingdf.sample_id_nunique > min_cluster_presence * sample_ct] + missingdf = missingdf[missingdf.sample_id_nunique != sample_ct] + + if len(missingdf) > 0: + # Find which samples are missing for each cluster + missing_samples_list = [] + for c in missingdf.cluster.to_numpy(): + cludf = mfdf[mfdf.cluster == c] + missing = [x for x in mfdf.sample_id.unique() if x not in cludf.sample_id.unique()] + missing_samples_list.append(missing) + missingdf['missing_samples'] = missing_samples_list + + # Calculate expanded search windows + mz_clu_tol = self.parameters.lcms_collection.consensus_mz_tol_ppm * 1e-6 + rt_clu_tol = self.parameters.lcms_collection.consensus_rt_tol + missingdf['mz_max_allowed'] = missingdf.mz_max + mz_clu_tol * missingdf.mz_max + missingdf['mz_min_allowed'] = missingdf.mz_min - mz_clu_tol * missingdf.mz_min + missingdf['sta_max_allowed'] = missingdf.scan_time_aligned_max + rt_clu_tol * missingdf.scan_time_aligned_max + missingdf['sta_min_allowed'] = missingdf.scan_time_aligned_min - rt_clu_tol * missingdf.scan_time_aligned_min + + runtime_params['missingdf'] = missingdf + runtime_params['cluster_dict'] = self.cluster_feature_dictionary + runtime_params['expand_on_miss'] = expand_on_miss + + # Check if any operation needs reload parameters + needs_reload = any(isinstance(op, ReloadFeaturesOperation) for op in operations) + if needs_reload: + # Build sample_mf_map for reloading representatives + clusters = self.mass_features_dataframe['cluster'].unique() + sample_mf_map = {} + for cluster_id in clusters: + rep_info = self.get_most_representative_sample_for_cluster(cluster_id) + sample_id = rep_info['sample_id'] + mf_id = rep_info['mf_id'] + + if sample_id not in sample_mf_map: + sample_mf_map[sample_id] = [] + sample_mf_map[sample_id].append(mf_id) + + runtime_params['sample_mf_map'] = sample_mf_map + + return runtime_params + + def _execute_sample_pipeline(self, sample_id, operations, runtime_params, inplace=True): + """ + Execute a pipeline of operations on a single sample. + + This is the worker function called (potentially in parallel) for each sample. + + Parameters + ---------- + sample_id : int + Sample ID to process + operations : list of SampleOperation + Operations to execute in order + runtime_params : dict + Runtime parameters prepared by _prepare_pipeline_runtime_params + inplace : bool, optional + If True, updates sample in place. If False, returns results for + multiprocessing. Default is True. + + Returns + ------- + dict + Dictionary with results from each operation, keyed by operation name. + If inplace=True, returns results that need to be collected. + If inplace=False, returns all results for multiprocessing collection. + """ + results = {} + + # Check if any operations need raw MS data + needs_raw_data = {} # {ms_level: True/False} + for op in operations: + needs_raw, ms_level = op.needs_raw_ms_data() + if needs_raw and ms_level: + needs_raw_data[ms_level] = True + + # Load raw data once if any operations need it + # Note: For gap-filling, it loads data internally, so we just track it here + for ms_level in needs_raw_data.keys(): + # Gap-filling loads its own data, but we want to keep track that it's loaded + # Other operations can then use the loaded data + pass + + for op in operations: + try: + # Check if operation can execute on this sample + sample = self[sample_id] + if not op.can_execute(sample, self): + continue + + # Prepare operation-specific runtime params + op_runtime_params = {} + + # Add gap-fill params if this is a gap-fill operation + from corems.mass_spectra.calc.lc_calc_operations import GapFillOperation, ReloadFeaturesOperation + + if isinstance(op, GapFillOperation): + if 'missingdf' in runtime_params: + op_runtime_params['missingdf'] = runtime_params['missingdf'] + op_runtime_params['cluster_dict'] = runtime_params['cluster_dict'] + op_runtime_params['expand_on_miss'] = runtime_params['expand_on_miss'] + + elif isinstance(op, ReloadFeaturesOperation): + if 'sample_mf_map' in runtime_params: + sample_mf_map = runtime_params['sample_mf_map'] + if sample_id in sample_mf_map: + op_runtime_params['mf_ids_to_load'] = sample_mf_map[sample_id] + + # Execute the operation + result = op.execute(sample_id, self, **op_runtime_params) + results[op.name] = result + + # If inplace, collect immediately + if inplace and result is not None: + op.collect_results(sample_id, result, self) + + except Exception as e: + print(f"Warning: Operation '{op.name}' failed for sample {sample_id}: {e}") + results[op.name] = None + + # Clean up raw data if requested + keep_raw_data = runtime_params.get('keep_raw_data', False) + if not keep_raw_data: + for ms_level in needs_raw_data.keys(): + if ms_level in self[sample_id]._ms_unprocessed: + del self[sample_id]._ms_unprocessed[ms_level] + + return results + + def process_consensus_features(self, perform_gap_filling=True, reload_representatives=True, + add_ms1=False, add_ms2=False, auto_process_ms2=True, + ms2_scan_filter=None, keep_raw_data=False): + """ + Process consensus mass features across the collection in a single parallelized pass. + + This method provides a convenient interface to the sample processing pipeline, + allowing multiple operations (gap-filling, feature reloading, MS1/MS2 association, + and potentially more in the future) to be performed efficiently in a single pass + through all samples. + + Parameters + ---------- + perform_gap_filling : bool, optional + If True, performs gap-filling for missing cluster features. Default is True. + This operation loads raw MS1 data which can be reused by subsequent operations. + reload_representatives : bool, optional + If True, reloads representative mass features from HDF5. Default is True. + add_ms1 : bool, optional + If True and reload_representatives=True, associates MS1 spectra with + reloaded features. Automatically uses raw data from gap-filling if available, + otherwise uses parser. Spectrum mode is auto-detected. Default is False. + add_ms2 : bool, optional + If True and reload_representatives=True, associates MS2 spectra with + reloaded features. 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 (e.g., 'hcd'). Default is None. + keep_raw_data : bool, optional + If True, keeps raw MS data loaded in memory after pipeline completes. + If False, cleans up raw data to free memory. Default is False. + + TODO + ---- + - Add MS1 molecular formula search option + + Returns + ------- + dict + Dictionary with pipeline results. Keys include: + - 'gap_fill': dict mapping sample_id to induced mass features (if gap-filling) + - 'reload': dict mapping sample_id to reloaded mass features (if reloading) + - 'ms2_search': dict mapping sample_id to search results (if performing MS2 search) + + Raises + ------ + ValueError + If neither operation is enabled (both perform_gap_filling and + reload_representatives are False). + + Notes + ----- + - Must run add_consensus_mass_features() before calling this method + - Processes samples in parallel based on parameters.lcms_collection.cores + - Raw MS1 data loaded by gap-filling is automatically reused by MS1 association + - More efficient than calling individual methods separately + - After gap-filling, sets missing_mass_features_searched = True + - Mass features remain loaded in memory for downstream processing + - For more advanced workflows, use process_samples_pipeline() directly + + Examples + -------- + >>> # Gap-fill, reload with MS1/MS2, and perform MS2 molecular formula search + >>> results = lcms_collection.process_consensus_features( + ... perform_gap_filling=True, + ... reload_representatives=True, + ... add_ms1=True, + ... add_ms2=True, + ... ) + + >>> # Custom MS2 spectral search + >>> def my_search(sample, metadata, **params): + ... # Custom search logic + ... pass + >>> results = lcms_collection.process_consensus_features( + ... reload_representatives=True, + ... add_ms2=True, + ... metadata=my_metadata + ... ) + + See Also + -------- + process_samples_pipeline : Generic pipeline executor for custom workflows + fill_missing_cluster_features : Original gap-filling method + reload_representative_mass_features : Original reload method + """ + from corems.mass_spectra.calc.lc_calc_operations import ( + GapFillOperation, ReloadFeaturesOperation + ) + + # Validate that at least one operation is enabled + if not perform_gap_filling and not reload_representatives: + raise ValueError("At least one of perform_gap_filling or reload_representatives must be True") + + + # Build pipeline + operations = [] + + if perform_gap_filling: + expand_on_miss = self.parameters.lcms_collection.gap_fill_expand_on_miss + operations.append(GapFillOperation('gap_fill', expand_on_miss=expand_on_miss)) + + if reload_representatives: + operations.append(ReloadFeaturesOperation( + 'reload', + add_ms1=add_ms1, + add_ms2=add_ms2, + auto_process_ms2=auto_process_ms2, + ms2_scan_filter=ms2_scan_filter + )) + + # Execute pipeline + results = self.process_samples_pipeline( + operations, + description="Gap-filling and reloading features", + keep_raw_data=keep_raw_data + ) + + # Post-processing + if perform_gap_filling: + # Combine induced mass features into dataframe + self._combine_mass_features(induced_features=True) + # Mark that gap-filling has been performed + self.missing_mass_features_searched = True + # Clear induced mass features from individual samples + for sample_name in self.samples: + self._lcms[sample_name].induced_mass_features = {} + + return results \ No newline at end of file diff --git a/corems/mass_spectra/calc/lc_calc_operations.py b/corems/mass_spectra/calc/lc_calc_operations.py new file mode 100644 index 000000000..9d9faac1a --- /dev/null +++ b/corems/mass_spectra/calc/lc_calc_operations.py @@ -0,0 +1,436 @@ +""" +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 + +""" + +import pandas as pd + + +class SampleOperation: + """ + Base class for operations that can be performed on a sample. + + All sample operations should inherit from this class and implement + the execute() method. Optionally override can_execute() for conditional + execution and collect_results() for custom result collection. + + 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 + """ + + def __init__(self, name, **kwargs): + self.name = name + self.params = kwargs + + def needs_raw_ms_data(self): + """ + Declare whether this operation needs raw MS data loaded. + + Override 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 + """ + return False, None + + def can_execute(self, sample, collection): + """ + Check if this operation can be executed on the sample. + + Parameters + ---------- + sample : LCMSBase + The sample to check + collection : LCMSBaseCollection + The collection containing the sample + + Returns + ------- + bool + True if operation can execute, False otherwise + """ + return True + + def execute(self, sample_id, collection, **runtime_params): + """ + 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) + """ + raise NotImplementedError(f"execute() not implemented for {self.__class__.__name__}") + + def collect_results(self, sample_id, result, collection): + """ + Collect results back into collection after parallel execution. + + Override this method if the operation returns results that need + to be collected back into the collection object. + + Parameters + ---------- + sample_id : int + Sample ID that was processed + result + Result returned from execute() + collection : LCMSBaseCollection + The collection to update + """ + pass + + def __repr__(self): + return f"{self.__class__.__name__}(name='{self.name}')" + + +class GapFillOperation(SampleOperation): + """ + 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. + + 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. + """ + + def needs_raw_ms_data(self): + """This operation needs raw MS1 data.""" + return True, 1 + + def can_execute(self, sample, collection): + """Check if cluster summary exists.""" + return hasattr(collection, 'cluster_summary_dataframe') and \ + collection.cluster_summary_dataframe is not None + + def execute(self, sample_id, collection, missingdf, cluster_dict, expand_on_miss, **runtime_params): + """ + Execute gap-filling for a single sample. + + Parameters + ---------- + sample_id : int + Sample index to process + collection : LCMSBaseCollection + The collection + missingdf : pd.DataFrame + DataFrame with cluster information and missing samples + cluster_dict : dict + Cluster feature dictionary + expand_on_miss : bool + Whether to expand search window on miss + **runtime_params + Additional runtime parameters (ignored) + + Returns + ------- + dict + Dictionary of induced mass features + """ + # This is essentially the same logic as _search_for_targeted_mass_features_in_sample + # but extracted into an operation + + # Get clusters missing data for this sample + sampledf = missingdf[ + missingdf.missing_samples.apply(lambda x: sample_id in x) + ].reset_index(drop=True).copy() + + # Skip if no missing features for this sample + if len(sampledf) == 0: + return {} + + # Load raw data for this sample + collection.load_raw_data(sample_id, 1) + + # Get MS1 data + ms1df = collection[sample_id]._ms_unprocessed[1].copy() + scan_df = collection[sample_id].scan_df[['scan', 'scan_time_aligned']] + ms1df = pd.merge(ms1df, scan_df, on='scan') + + # Pre-extract all values from sampledf + clusters = sampledf.cluster.values + mz_mins = sampledf.mz_min.values + mz_maxs = sampledf.mz_max.values + st_mins = sampledf.scan_time_aligned_min.values + st_maxs = sampledf.scan_time_aligned_max.values + + if expand_on_miss: + mz_mins_allowed = sampledf.mz_min_allowed.values + mz_maxs_allowed = sampledf.mz_max_allowed.values + st_mins_allowed = sampledf.sta_min_allowed.values + st_maxs_allowed = sampledf.sta_max_allowed.values + + # Pre-filter ms1df to reduce search space + mz_global_min = mz_mins.min() + mz_global_max = mz_maxs.max() + st_global_min = st_mins.min() + st_global_max = st_maxs.max() + + if expand_on_miss: + mz_global_min = min(mz_global_min, mz_mins_allowed.min()) + mz_global_max = max(mz_global_max, mz_maxs_allowed.max()) + st_global_min = min(st_global_min, st_mins_allowed.min()) + st_global_max = max(st_global_max, st_maxs_allowed.max()) + + ms1df_filtered = ms1df[ + (ms1df.mz >= mz_global_min) & + (ms1df.mz <= mz_global_max) & + (ms1df.scan_time_aligned >= st_global_min) & + (ms1df.scan_time_aligned <= st_global_max) + ].copy() + + # Generate set_ids for all features + set_ids = [f'c{clusters[i]}_{i}_i' for i in range(len(sampledf))] + + # Use batch method to process all features at once + if expand_on_miss: + # First try with normal bounds + peaks_dict = collection[sample_id].search_for_targeted_mass_features_batch( + ms1df_filtered, + mz_mins, + mz_maxs, + st_mins, + st_maxs, + set_ids, + obj_idx=sample_id, + st_aligned=True + ) + + # Retry failed features with expanded bounds + failed_indices = [i for i, sid in enumerate(set_ids) if peaks_dict[sid].apex_scan == -99] + if failed_indices: + failed_ids = [set_ids[i] for i in failed_indices] + retry_peaks = collection[sample_id].search_for_targeted_mass_features_batch( + ms1df_filtered, + mz_mins_allowed[failed_indices], + mz_maxs_allowed[failed_indices], + st_mins_allowed[failed_indices], + st_maxs_allowed[failed_indices], + failed_ids, + obj_idx=sample_id, + st_aligned=True + ) + peaks_dict.update(retry_peaks) + else: + peaks_dict = collection[sample_id].search_for_targeted_mass_features_batch( + ms1df_filtered, + mz_mins, + mz_maxs, + st_mins, + st_maxs, + set_ids, + obj_idx=sample_id, + st_aligned=True + ) + + # Build induced_mass_features dict and update cluster_dict + induced_mass_features = {} + for i in range(len(sampledf)): + peak = peaks_dict[set_ids[i]] + induced_mass_features[peak.id] = peak + cluster_dict[clusters[i]] += [set_ids[i]] + + # Integrate mass features (don't fail on bad integration) + collection[sample_id].induced_mass_features = induced_mass_features + collection[sample_id].integrate_mass_features(drop_if_fail=False, induced_features=True) + + # Return the induced features + return collection[sample_id].induced_mass_features + + def collect_results(self, sample_id, result, collection): + """Collect induced mass features back into sample.""" + collection[sample_id].induced_mass_features = result + + +class ReloadFeaturesOperation(SampleOperation): + """ + 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. + """ + + def can_execute(self, sample, collection): + """Check if collection parser is available.""" + return hasattr(collection, 'collection_parser') and \ + collection.collection_parser is not None + + def execute(self, sample_id, collection, mf_ids_to_load=None, **runtime_params): + """ + 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 + """ + # Get parameters + add_ms1 = self.params.get('add_ms1', False) + add_ms2 = self.params.get('add_ms2', False) + auto_process_ms2 = self.params.get('auto_process_ms2', True) + ms2_scan_filter = self.params.get('ms2_scan_filter', None) + + sample = collection[sample_id] + sample_name = collection.samples[sample_id] + + # Auto-determine if we should use parser for MS1 (check if raw data is available) + has_raw_ms1 = 1 in sample._ms_unprocessed and not sample._ms_unprocessed[1].empty + use_parser_for_ms1 = not has_raw_ms1 # Use parser only if raw data not available + + # Spectrum modes will be auto-detected (None = auto-detect) + spectrum_mode_ms1 = None + ms2_spectrum_mode = None + + # Check if we have a collection parser + if not hasattr(collection, 'collection_parser') or collection.collection_parser is None: + print(f"Warning: Cannot reload mass features for {sample_name} - no collection_parser available") + return {} + + # Get the HDF5 file for this sample + hdf5_file = collection.collection_parser.folder_location / f"{sample_name}.corems/{sample_name}.hdf5" + + if not hdf5_file.exists(): + print(f"Warning: HDF5 file not found for sample {sample_name}: {hdf5_file}") + return {} + + # Import here to avoid circular imports + from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectra + + # If specific mf_ids requested, extract the local mf_ids we need + local_mf_ids_to_load = None + if mf_ids_to_load is not None: + local_mf_ids_to_load = set() + for coll_mf_id in mf_ids_to_load: + # Parse collection-level ID to get local ID + parts = str(coll_mf_id).split('_', 1) + if len(parts) == 2: + try: + local_mf_ids_to_load.add(int(parts[1])) + except ValueError: + local_mf_ids_to_load.add(parts[1]) + + # Reload mass features from HDF5 + with ReadCoreMSHDFMassSpectra(hdf5_file) as parser: + parser.import_mass_features(sample, mf_ids=local_mf_ids_to_load) + + # If add_ms1, associate MS1 spectra with the loaded mass features + if add_ms1 and len(sample.mass_features) > 0: + # Check if raw MS1 data is already loaded (e.g., from gap-filling) + has_raw_ms1 = 1 in sample._ms_unprocessed and not sample._ms_unprocessed[1].empty + + if has_raw_ms1 and not use_parser_for_ms1: + # Use already-loaded raw data (more efficient) + sample.add_associated_ms1( + auto_process=True, + use_parser=False, + spectrum_mode=spectrum_mode_ms1 + ) + else: + # Use parser to get MS1 spectra + sample.add_associated_ms1( + auto_process=True, + use_parser=True, + spectrum_mode=spectrum_mode_ms1 + ) + + # If add_ms2, associate MS2 spectra with the loaded mass features + if add_ms2 and local_mf_ids_to_load is not None: + collection._associate_ms2_with_mass_features( + sample, + list(local_mf_ids_to_load), + auto_process=auto_process_ms2, + spectrum_mode=ms2_spectrum_mode, + scan_filter=ms2_scan_filter + ) + + return sample.mass_features + + def collect_results(self, sample_id, result, collection): + """Collect reloaded mass features back into sample.""" + collection[sample_id].mass_features = result diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 3ab12eb96..e6a74222e 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -4,6 +4,18 @@ from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectraCollection, ReadSavedLCMSCollection from corems.mass_spectra.output.export import LCMSCollectionExport +""" +Example showing the new pipeline-based sample processing approach. + +The new approach combines multiple sample-level operations (gap-filling, +feature reloading, MS1/MS2 searches, etc.) into a single parallelized pass, +which is more efficient than processing samples multiple times. + +Two usage patterns: +1. High-level convenience method: process_consensus_features() +2. Advanced pipeline builder: process_samples_pipeline() with custom operations +""" + if __name__ == "__main__": # Set the path to the collection of LCMS runs (previously processed) @@ -59,6 +71,65 @@ lcms_collection.add_consensus_mass_features() print("Time to generate consensus mass features: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") + # NEW PIPELINE APPROACH: Gap fill, reload, and add MS1/MS2 in a single pass + print("\n=== Testing new pipeline approach with MS1 and MS2 ===") + start_time = time.time() + pipeline_results = lcms_collection.process_consensus_features( + perform_gap_filling=True, + reload_representatives=True, + add_ms1=True, + add_ms2=True, + auto_process_ms2=True, + ms2_scan_filter=None, + keep_raw_data=False + ) + print("Time for combined gap-fill, reload, MS1, and MS2: ", time.time() - start_time, "seconds, using", ncores, " cores") + print("Gap-filled features in", len([s for s in pipeline_results.get('gap_fill', {}).values() if s]), "samples") + print("Reloaded features in", len([s for s in pipeline_results.get('reload', {}).values() if s]), "samples") + + # Verify that mass features were reloaded + total_mf_reloaded = sum([len(lcms_obj.mass_features) for lcms_obj in lcms_collection]) + print(f"Total mass features reloaded: {total_mf_reloaded}") + assert total_mf_reloaded > 0, "Should have reloaded some mass features" + + # Check for MS1 associations + total_ms1_with_spectra = 0 + total_mf_checked = 0 + for lcms_obj in lcms_collection: + for mf_id, mf in lcms_obj.mass_features.items(): + total_mf_checked += 1 + if hasattr(mf, 'mass_spectrum') and mf.mass_spectrum is not None: + total_ms1_with_spectra += 1 + + print(f"Total mass features with MS1 spectra: {total_ms1_with_spectra} out of {total_mf_checked}") + if total_ms1_with_spectra > 0: + print(f"✓ MS1 spectra successfully associated with {total_ms1_with_spectra/total_mf_checked*100:.1f}% of mass features") + assert total_ms1_with_spectra > 0, "Should have MS1 spectra associated with mass features" + else: + print("⚠ No MS1 spectra associated") + + # Check for MS2 associations + total_ms2 = 0 + for lcms_obj in lcms_collection: + for mf_id, mf in lcms_obj.mass_features.items(): + if hasattr(mf, 'ms2_mass_spectra') and mf.ms2_mass_spectra: + total_ms2 += len(mf.ms2_mass_spectra) + print(f"Total MS2 spectra associated: {total_ms2}") + if total_ms2 > 0: + print("✓ MS2 spectra successfully associated with mass features") + else: + print("⚠ No MS2 spectra associated (this may be expected if no MS2 data exists)") + + # Verify raw data was cleaned up (unless keep_raw_data=True) + raw_data_present = any(1 in lcms_obj._ms_unprocessed and not lcms_obj._ms_unprocessed[1].empty + for lcms_obj in lcms_collection) + if not raw_data_present: + print("✓ Raw MS1 data successfully cleaned up after pipeline") + else: + print("⚠ Raw MS1 data still present (expected if keep_raw_data=True)") + + """ + # OLD APPROACH (commented out - replaced by pipeline above): # Gap fill missing cluster features BEFORE saving start_time = time.time() lcms_collection.fill_missing_cluster_features() @@ -71,7 +142,36 @@ ms2_spectrum_mode=None, ms2_scan_filter=None ) + """ + + """ + # ADVANCED PIPELINE APPROACH (for custom workflows): + # Build a custom pipeline with full control over operations + from corems.mass_spectra.calc.lc_calc_operations import ( + GapFillOperation, + ReloadFeaturesOperation, + CustomOperation + ) + + # Define custom operation + def my_custom_processing(sample_id, collection, **params): + sample = collection[sample_id] + # Do custom processing here + # e.g., normalization, quality checks, etc. + return None + + # Build pipeline + ops = [ + GapFillOperation('gap_fill', expand_on_miss=True), + ReloadFeaturesOperation('reload', add_ms2=True, auto_process_ms2=True), + CustomOperation('custom', func=my_custom_processing) + ] + + # Execute + results = lcms_collection.process_samples_pipeline(ops, description="Custom workflow") + """ + """ # Check save and load functionality for LCMSCollection print("Saving and re-loading LCMS collection to test save/load functionality") print(f"Before saving: missing_mass_features_searched = {lcms_collection.missing_mass_features_searched}") @@ -103,7 +203,7 @@ print('Test completed successfully! LCMSCollection save and load functionality works as expected.') - """# Make some more plots + # Make some more plots lcms_collection.plot_mz_features_across_samples() lcms_collection.plot_mz_features_per_cluster() lcms_collection.plot_consensus_mz_features() ## zoomed out From d75b15b9826feaca1ac921ce12b8406ee9aab9cf Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 19 Jan 2026 10:04:28 -0800 Subject: [PATCH 100/158] Add functionality for post-consensus molecular formula search --- corems/mass_spectra/calc/lc_calc.py | 94 ++++---- .../mass_spectra/calc/lc_calc_operations.py | 200 ++++++++++++++++++ .../nmdc/lipidomics/lipidomics_collection.py | 38 +++- 3 files changed, 284 insertions(+), 48 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 35c047f65..c802a1c00 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -4546,41 +4546,39 @@ def _execute_sample_pipeline(self, sample_id, operations, runtime_params, inplac pass for op in operations: - try: - # Check if operation can execute on this sample - sample = self[sample_id] - if not op.can_execute(sample, self): - continue - - # Prepare operation-specific runtime params - op_runtime_params = {} - - # Add gap-fill params if this is a gap-fill operation - from corems.mass_spectra.calc.lc_calc_operations import GapFillOperation, ReloadFeaturesOperation - - if isinstance(op, GapFillOperation): - if 'missingdf' in runtime_params: - op_runtime_params['missingdf'] = runtime_params['missingdf'] - op_runtime_params['cluster_dict'] = runtime_params['cluster_dict'] - op_runtime_params['expand_on_miss'] = runtime_params['expand_on_miss'] - - elif isinstance(op, ReloadFeaturesOperation): - if 'sample_mf_map' in runtime_params: - sample_mf_map = runtime_params['sample_mf_map'] - if sample_id in sample_mf_map: - op_runtime_params['mf_ids_to_load'] = sample_mf_map[sample_id] - - # Execute the operation - result = op.execute(sample_id, self, **op_runtime_params) - results[op.name] = result - - # If inplace, collect immediately - if inplace and result is not None: - op.collect_results(sample_id, result, self) - - except Exception as e: - print(f"Warning: Operation '{op.name}' failed for sample {sample_id}: {e}") - results[op.name] = None + # Check if operation can execute on this sample + sample = self[sample_id] + if not op.can_execute(sample, self): + raise RuntimeError( + f"Operation '{op.name}' cannot execute on sample {sample_id} " + f"({sample.sample_name}). Prerequisites not met." + ) + + # Prepare operation-specific runtime params + op_runtime_params = {} + + # Add gap-fill params if this is a gap-fill operation + from corems.mass_spectra.calc.lc_calc_operations import GapFillOperation, ReloadFeaturesOperation + + if isinstance(op, GapFillOperation): + if 'missingdf' in runtime_params: + op_runtime_params['missingdf'] = runtime_params['missingdf'] + op_runtime_params['cluster_dict'] = runtime_params['cluster_dict'] + op_runtime_params['expand_on_miss'] = runtime_params['expand_on_miss'] + + elif isinstance(op, ReloadFeaturesOperation): + if 'sample_mf_map' in runtime_params: + sample_mf_map = runtime_params['sample_mf_map'] + if sample_id in sample_mf_map: + op_runtime_params['mf_ids_to_load'] = sample_mf_map[sample_id] + + # Execute the operation + result = op.execute(sample_id, self, **op_runtime_params) + results[op.name] = result + + # If inplace, collect immediately + if inplace and result is not None: + op.collect_results(sample_id, result, self) # Clean up raw data if requested keep_raw_data = runtime_params.get('keep_raw_data', False) @@ -4593,7 +4591,8 @@ def _execute_sample_pipeline(self, sample_id, operations, runtime_params, inplac def process_consensus_features(self, perform_gap_filling=True, reload_representatives=True, add_ms1=False, add_ms2=False, auto_process_ms2=True, - ms2_scan_filter=None, keep_raw_data=False): + ms2_scan_filter=None, molecular_formula_search=False, + keep_raw_data=False): """ Process consensus mass features across the collection in a single parallelized pass. @@ -4620,13 +4619,14 @@ def process_consensus_features(self, perform_gap_filling=True, reload_representa 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 (e.g., 'hcd'). Default is None. + molecular_formula_search : bool, optional + If True, performs molecular formula search on mass features using + associated MS1 spectra. Requires add_ms1=True or that MS1 spectra + are already associated. Uses parameters from + parameters.mass_spectrum["ms1"].molecular_search. Default is False. keep_raw_data : bool, optional If True, keeps raw MS data loaded in memory after pipeline completes. If False, cleans up raw data to free memory. Default is False. - - TODO - ---- - - Add MS1 molecular formula search option Returns ------- @@ -4634,7 +4634,7 @@ def process_consensus_features(self, perform_gap_filling=True, reload_representa Dictionary with pipeline results. Keys include: - 'gap_fill': dict mapping sample_id to induced mass features (if gap-filling) - 'reload': dict mapping sample_id to reloaded mass features (if reloading) - - 'ms2_search': dict mapping sample_id to search results (if performing MS2 search) + - 'mf_search': dict mapping sample_id to number of features searched (if performing molecular formula search) Raises ------ @@ -4679,13 +4679,20 @@ def process_consensus_features(self, perform_gap_filling=True, reload_representa reload_representative_mass_features : Original reload method """ from corems.mass_spectra.calc.lc_calc_operations import ( - GapFillOperation, ReloadFeaturesOperation + GapFillOperation, ReloadFeaturesOperation, MolecularFormulaSearchOperation ) # Validate that at least one operation is enabled if not perform_gap_filling and not reload_representatives: raise ValueError("At least one of perform_gap_filling or reload_representatives must be True") + # Validate prerequisites for gap-filling + if perform_gap_filling: + if not hasattr(self, 'cluster_summary_dataframe') or self.cluster_summary_dataframe is None: + raise ValueError( + "Cannot perform gap-filling: cluster_summary_dataframe not set. " + "You must run add_consensus_mass_features() before calling process_consensus_features()." + ) # Build pipeline operations = [] @@ -4703,6 +4710,9 @@ def process_consensus_features(self, perform_gap_filling=True, reload_representa ms2_scan_filter=ms2_scan_filter )) + if molecular_formula_search: + operations.append(MolecularFormulaSearchOperation('mf_search')) + # Execute pipeline results = self.process_samples_pipeline( operations, diff --git a/corems/mass_spectra/calc/lc_calc_operations.py b/corems/mass_spectra/calc/lc_calc_operations.py index 9d9faac1a..1beb771be 100644 --- a/corems/mass_spectra/calc/lc_calc_operations.py +++ b/corems/mass_spectra/calc/lc_calc_operations.py @@ -434,3 +434,203 @@ def execute(self, sample_id, collection, mf_ids_to_load=None, **runtime_params): def collect_results(self, sample_id, result, collection): """Collect reloaded mass features back into sample.""" collection[sample_id].mass_features = result + + +class MolecularFormulaSearchOperation(SampleOperation): + """ + 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. + """ + + def __init__(self, name='molecular_formula_search', **kwargs): + super().__init__(name, **kwargs) + + def needs_raw_ms_data(self): + """ + 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 + """ + return False, None + + def can_execute(self, sample, collection, **runtime_params): + """ + 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 + """ + # Check if sample has mass features + if not hasattr(sample, 'mass_features') or not sample.mass_features: + return False + + # Check if at least some mass features have MS1 spectra + has_ms1 = any( + hasattr(mf, 'mass_spectrum') and mf.mass_spectrum is not None + for mf in sample.mass_features.values() + ) + + return has_ms1 + + def execute(self, sample_id, collection, **runtime_params): + """ + 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 + """ + from corems.molecular_id.search.molecularFormulaSearch import SearchMolecularFormulasLC + import time + import sqlalchemy.exc + import sqlite3 + + sample = collection[sample_id] + + # Verify that mass features exist + if not hasattr(sample, 'mass_features') or not sample.mass_features: + return 0 # No mass features to search + + # Verify that mass features have MS1 spectra associated + if not hasattr(sample, '_ms') or not sample._ms: + raise RuntimeError( + f"Sample {sample_id} does not have MS1 spectra loaded in _ms dictionary. " + "Molecular formula search requires MS1 spectra to be associated with mass features. " + "Ensure add_ms1=True when reloading features." + ) + + # Prepare data for bulk molecular formula search + # Group mass features by their apex scan + scan_to_mf = {} + for mf_id, mf in sample.mass_features.items(): + apex_scan = mf.apex_scan + if apex_scan not in scan_to_mf: + scan_to_mf[apex_scan] = [] + scan_to_mf[apex_scan].append(mf) + + # Build lists of mass spectra and corresponding peaks + mass_spectrum_list = [] + ms_peaks_list = [] + + for scan_num, mf_list in scan_to_mf.items(): + # Get the mass spectrum for this scan + if scan_num not in sample._ms: + continue # Skip if spectrum not loaded + + mass_spectrum = sample._ms[scan_num] + + # Verify spectrum is processed (has peaks) + if not hasattr(mass_spectrum, '_mspeaks') or not mass_spectrum._mspeaks: + continue # Skip unprocessed spectra + + # Get the MS1 peaks for each mass feature at this scan + peaks_for_scan = [] + for mf in mf_list: + try: + # Use the ms1_peak property which finds the closest peak + ms1_peak = mf.ms1_peak + peaks_for_scan.append(ms1_peak) + except (AttributeError, IndexError): + # Skip if ms1_peak can't be determined + continue + + if peaks_for_scan: + mass_spectrum_list.append(mass_spectrum) + ms_peaks_list.append(peaks_for_scan) + + # Run molecular formula search if we have data, with retry logic for database locks + if mass_spectrum_list and ms_peaks_list: + max_retries = 10 + retry_delay = 2 # seconds + + for attempt in range(max_retries): + try: + mol_search = SearchMolecularFormulasLC(sample) + mol_search.bulk_run_molecular_formula_search(mass_spectrum_list, ms_peaks_list) + break # Success, exit retry loop + except (sqlalchemy.exc.OperationalError, sqlite3.OperationalError) as e: + if attempt < max_retries - 1: + # Database is locked, retry after delay + print(f"Sample {sample_id}: Database locked during molecular formula search, retrying in {retry_delay}s (attempt {attempt + 1}/{max_retries})...") + time.sleep(retry_delay) + else: + # Max retries exceeded, re-raise the exception + raise RuntimeError( + f"Sample {sample_id}: Molecular formula search failed after {max_retries} attempts due to database lock. " + "Try reducing parallel cores or increasing database timeout." + ) from e + + # Return count of features searched + return len(sample.mass_features) + + def collect_results(self, sample_id, result, collection): + """ + 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 + """ + # Search modifies mass features in place, nothing to collect + pass diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index e6a74222e..633642f31 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -24,7 +24,7 @@ manifest_file = Path("/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test_out2/manifest_tiny.csv") # Set the number of cores to use for loading the data (the parser is parallelized) - ncores = 6 + ncores = 3 # Instantiate the parser # Note, these samples have not had the DDA MS2 scans associated or MS1 data loaded @@ -71,21 +71,23 @@ lcms_collection.add_consensus_mass_features() print("Time to generate consensus mass features: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") - # NEW PIPELINE APPROACH: Gap fill, reload, and add MS1/MS2 in a single pass - print("\n=== Testing new pipeline approach with MS1 and MS2 ===") + # NEW PIPELINE APPROACH: Gap fill, reload, add MS1/MS2, and run molecular formula search in a single pass + print("\n=== Testing new pipeline approach with MS1, MS2, and molecular formula search ===") start_time = time.time() pipeline_results = lcms_collection.process_consensus_features( - perform_gap_filling=True, + perform_gap_filling=False, reload_representatives=True, add_ms1=True, - add_ms2=True, + add_ms2=False, auto_process_ms2=True, ms2_scan_filter=None, + molecular_formula_search=True, # New: run molecular formula search keep_raw_data=False ) - print("Time for combined gap-fill, reload, MS1, and MS2: ", time.time() - start_time, "seconds, using", ncores, " cores") + print("Time for combined gap-fill, reload, MS1, MS2, and MF search: ", time.time() - start_time, "seconds, using", ncores, " cores") print("Gap-filled features in", len([s for s in pipeline_results.get('gap_fill', {}).values() if s]), "samples") print("Reloaded features in", len([s for s in pipeline_results.get('reload', {}).values() if s]), "samples") + print("Molecular formula search completed on", len([s for s in pipeline_results.get('mf_search', {}).values() if s]), "samples") # Verify that mass features were reloaded total_mf_reloaded = sum([len(lcms_obj.mass_features) for lcms_obj in lcms_collection]) @@ -120,6 +122,30 @@ else: print("⚠ No MS2 spectra associated (this may be expected if no MS2 data exists)") + # Check for molecular formula assignments + total_mf_with_formulas = 0 + total_formula_assignments = 0 + for lcms_obj in lcms_collection: + for mf_id, mf in lcms_obj.mass_features.items(): + if hasattr(mf, 'mass_spectrum') and mf.mass_spectrum is not None: + # Check if the ms1_peak has molecular formula assignments + try: + ms1_peak = mf.ms1_peak + if hasattr(ms1_peak, 'molecular_formulas') and ms1_peak.molecular_formulas: + total_mf_with_formulas += 1 + total_formula_assignments += len(ms1_peak.molecular_formulas) + except (AttributeError, IndexError): + # Skip if ms1_peak can't be determined + pass + + print(f"Total mass features with molecular formula assignments: {total_mf_with_formulas} out of {total_mf_checked}") + print(f"Total molecular formula assignments: {total_formula_assignments}") + if total_mf_with_formulas > 0: + print(f"✓ Molecular formula search successfully assigned formulas to {total_mf_with_formulas/total_mf_checked*100:.1f}% of mass features") + print(f" Average {total_formula_assignments/total_mf_with_formulas:.1f} formulas per assigned feature") + else: + print("⚠ No molecular formula assignments (check search parameters)") + # Verify raw data was cleaned up (unless keep_raw_data=True) raw_data_present = any(1 in lcms_obj._ms_unprocessed and not lcms_obj._ms_unprocessed[1].empty for lcms_obj in lcms_collection) From e0b9a68717bd67705b705d8ca33f5f558a631074 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 19 Jan 2026 13:06:35 -0800 Subject: [PATCH 101/158] Add functionality for performing MS2 spectral search on consensus mass features --- corems/mass_spectra/calc/lc_calc.py | 118 ++++++++-- .../mass_spectra/calc/lc_calc_operations.py | 185 +++++++++++++++ .../nmdc/lipidomics/lipidomics_collection.py | 218 +++++++++++++++--- 3 files changed, 468 insertions(+), 53 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index c802a1c00..28300659e 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -4435,7 +4435,7 @@ def _prepare_pipeline_runtime_params(self, operations): Dictionary of runtime parameters for operations """ from corems.mass_spectra.calc.lc_calc_operations import ( - GapFillOperation, ReloadFeaturesOperation + GapFillOperation, ReloadFeaturesOperation, MS2SpectralSearchOperation ) runtime_params = {} @@ -4502,6 +4502,15 @@ def _prepare_pipeline_runtime_params(self, operations): runtime_params['sample_mf_map'] = sample_mf_map + # Check if any operation needs MS2 spectral search parameters + needs_ms2_search = any(isinstance(op, MS2SpectralSearchOperation) for op in operations) + if needs_ms2_search: + # Pass through pre-prepared spectral library + if hasattr(self, '_spectral_lib') and self._spectral_lib is not None: + runtime_params['fe_lib'] = self._spectral_lib + if hasattr(self, '_spectral_search_molecular_metadata'): + runtime_params['molecular_metadata'] = self._spectral_search_molecular_metadata + return runtime_params def _execute_sample_pipeline(self, sample_id, operations, runtime_params, inplace=True): @@ -4558,7 +4567,9 @@ def _execute_sample_pipeline(self, sample_id, operations, runtime_params, inplac op_runtime_params = {} # Add gap-fill params if this is a gap-fill operation - from corems.mass_spectra.calc.lc_calc_operations import GapFillOperation, ReloadFeaturesOperation + from corems.mass_spectra.calc.lc_calc_operations import ( + GapFillOperation, ReloadFeaturesOperation, MS2SpectralSearchOperation + ) if isinstance(op, GapFillOperation): if 'missingdf' in runtime_params: @@ -4572,6 +4583,13 @@ def _execute_sample_pipeline(self, sample_id, operations, runtime_params, inplac if sample_id in sample_mf_map: op_runtime_params['mf_ids_to_load'] = sample_mf_map[sample_id] + elif isinstance(op, MS2SpectralSearchOperation): + # Add MS2 spectral search parameters + if 'fe_lib' in runtime_params: + op_runtime_params['fe_lib'] = runtime_params['fe_lib'] + if 'molecular_metadata' in runtime_params: + op_runtime_params['molecular_metadata'] = runtime_params['molecular_metadata'] + # Execute the operation result = op.execute(sample_id, self, **op_runtime_params) results[op.name] = result @@ -4590,16 +4608,18 @@ def _execute_sample_pipeline(self, sample_id, operations, runtime_params, inplac return results def process_consensus_features(self, perform_gap_filling=True, reload_representatives=True, - add_ms1=False, add_ms2=False, auto_process_ms2=True, + add_ms1=False, add_ms2=False, ms2_scan_filter=None, molecular_formula_search=False, + ms2_spectral_search=False, spectral_lib=None, + molecular_metadata=None, keep_raw_data=False): """ Process consensus mass features across the collection in a single parallelized pass. This method provides a convenient interface to the sample processing pipeline, allowing multiple operations (gap-filling, feature reloading, MS1/MS2 association, - and potentially more in the future) to be performed efficiently in a single pass - through all samples. + molecular formula search, and MS2 spectral search) to be performed efficiently in + a single pass through all samples. Parameters ---------- @@ -4614,9 +4634,7 @@ def process_consensus_features(self, perform_gap_filling=True, reload_representa otherwise uses parser. Spectrum mode is auto-detected. Default is False. add_ms2 : bool, optional If True and reload_representatives=True, associates MS2 spectra with - reloaded features. 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. + reloaded features and automatically processes them. Spectrum mode is auto-detected. Default is False. ms2_scan_filter : str or None, optional Filter string for MS2 scans (e.g., 'hcd'). Default is None. molecular_formula_search : bool, optional @@ -4624,6 +4642,18 @@ def process_consensus_features(self, perform_gap_filling=True, reload_representa associated MS1 spectra. Requires add_ms1=True or that MS1 spectra are already associated. Uses parameters from parameters.mass_spectrum["ms1"].molecular_search. Default is False. + ms2_spectral_search : bool, optional + If True, performs MS2 spectral library search using FlashEntropy. + Requires add_ms2=True and spectral_lib to be provided. Default is False. + spectral_lib : FlashEntropy library, optional + Pre-prepared FlashEntropy spectral library for MS2 search. + Create using MSPInterface.get_metabolomics_spectra_library(). + Required if ms2_spectral_search=True. Default is None. + molecular_metadata : pd.DataFrame, optional + Molecular metadata corresponding to spectral_lib. + Returned from MSPInterface.get_metabolomics_spectra_library(). + Stored as self.spectral_search_molecular_metadata for later export. + Default is None. keep_raw_data : bool, optional If True, keeps raw MS data loaded in memory after pipeline completes. If False, cleans up raw data to free memory. Default is False. @@ -4634,19 +4664,21 @@ def process_consensus_features(self, perform_gap_filling=True, reload_representa Dictionary with pipeline results. Keys include: - 'gap_fill': dict mapping sample_id to induced mass features (if gap-filling) - 'reload': dict mapping sample_id to reloaded mass features (if reloading) - - 'mf_search': dict mapping sample_id to number of features searched (if performing molecular formula search) + - 'mf_search': dict mapping sample_id to number of features searched (if molecular formula search) + - 'ms2_search': dict mapping sample_id to number of spectra searched (if MS2 spectral search) Raises ------ ValueError - If neither operation is enabled (both perform_gap_filling and - reload_representatives are False). + If neither operation is enabled, or if required parameters are missing. Notes ----- - Must run add_consensus_mass_features() before calling this method - Processes samples in parallel based on parameters.lcms_collection.cores - Raw MS1 data loaded by gap-filling is automatically reused by MS1 association + - MS2 spectral search requires add_ms2=True and msp_file_path + - FlashEntropy library is created once and reused across all samples - More efficient than calling individual methods separately - After gap-filling, sets missing_mass_features_searched = True - Mass features remain loaded in memory for downstream processing @@ -4654,22 +4686,33 @@ def process_consensus_features(self, perform_gap_filling=True, reload_representa Examples -------- - >>> # Gap-fill, reload with MS1/MS2, and perform MS2 molecular formula search + >>> # Prepare spectral library for MS2 search + >>> from corems.molecular_id.search.database_interfaces import MSPInterface + >>> my_msp = MSPInterface(file_path='path/to/library.msp') + >>> spectral_lib, molecular_metadata = my_msp.get_metabolomics_spectra_library( + ... polarity='negative', + ... format='flashentropy', + ... normalize=True, + ... fe_kwargs={ + ... 'normalize_intensity': True, + ... 'min_ms2_difference_in_da': 0.02, + ... 'max_ms2_tolerance_in_da': 0.01, + ... 'max_indexed_mz': 3000, + ... 'precursor_ions_removal_da': None, + ... 'noise_threshold': 0, + ... } + ... ) + >>> + >>> # Gap-fill, reload with MS1/MS2, perform molecular formula and spectral search >>> results = lcms_collection.process_consensus_features( ... perform_gap_filling=True, ... reload_representatives=True, ... add_ms1=True, ... add_ms2=True, - ... ) - - >>> # Custom MS2 spectral search - >>> def my_search(sample, metadata, **params): - ... # Custom search logic - ... pass - >>> results = lcms_collection.process_consensus_features( - ... reload_representatives=True, - ... add_ms2=True, - ... metadata=my_metadata + ... molecular_formula_search=True, + ... ms2_spectral_search=True, + ... spectral_lib=spectral_lib, + ... molecular_metadata=molecular_metadata ... ) See Also @@ -4679,7 +4722,8 @@ def process_consensus_features(self, perform_gap_filling=True, reload_representa reload_representative_mass_features : Original reload method """ from corems.mass_spectra.calc.lc_calc_operations import ( - GapFillOperation, ReloadFeaturesOperation, MolecularFormulaSearchOperation + GapFillOperation, ReloadFeaturesOperation, MolecularFormulaSearchOperation, + MS2SpectralSearchOperation ) # Validate that at least one operation is enabled @@ -4694,6 +4738,18 @@ def process_consensus_features(self, perform_gap_filling=True, reload_representa "You must run add_consensus_mass_features() before calling process_consensus_features()." ) + # Validate prerequisites for MS2 spectral search + if ms2_spectral_search: + if not add_ms2: + raise ValueError( + "MS2 spectral search requires add_ms2=True to load MS2 spectra." + ) + if spectral_lib is None: + raise ValueError( + "MS2 spectral search requires spectral_lib to be provided. " + "Create it using MSPInterface.get_metabolomics_spectra_library() before calling this method." + ) + # Build pipeline operations = [] @@ -4706,13 +4762,22 @@ def process_consensus_features(self, perform_gap_filling=True, reload_representa 'reload', add_ms1=add_ms1, add_ms2=add_ms2, - auto_process_ms2=auto_process_ms2, + auto_process_ms2=add_ms2, # Auto-process MS2 if add_ms2 is enabled ms2_scan_filter=ms2_scan_filter )) if molecular_formula_search: operations.append(MolecularFormulaSearchOperation('mf_search')) + if ms2_spectral_search: + operations.append(MS2SpectralSearchOperation( + 'ms2_search', + ms2_scan_filter=ms2_scan_filter + )) + # Store spectral library and metadata for runtime preparation + self._spectral_lib = spectral_lib + self._spectral_search_molecular_metadata = molecular_metadata + # Execute pipeline results = self.process_samples_pipeline( operations, @@ -4720,6 +4785,11 @@ def process_consensus_features(self, perform_gap_filling=True, reload_representa keep_raw_data=keep_raw_data ) + # Store molecular metadata if spectral search was performed + if ms2_spectral_search and hasattr(self, '_spectral_search_molecular_metadata'): + # This allows users to access the metadata for reporting + self.spectral_search_molecular_metadata = self._spectral_search_molecular_metadata + # Post-processing if perform_gap_filling: # Combine induced mass features into dataframe diff --git a/corems/mass_spectra/calc/lc_calc_operations.py b/corems/mass_spectra/calc/lc_calc_operations.py index 1beb771be..af693e0ec 100644 --- a/corems/mass_spectra/calc/lc_calc_operations.py +++ b/corems/mass_spectra/calc/lc_calc_operations.py @@ -634,3 +634,188 @@ def collect_results(self, sample_id, result, collection): """ # Search modifies mass features in place, nothing to collect pass + + +class MS2SpectralSearchOperation(SampleOperation): + """ + 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. + """ + + def __init__(self, name='ms2_spectral_search', ms2_scan_filter=None, **kwargs): + super().__init__(name, **kwargs) + self.params['ms2_scan_filter'] = ms2_scan_filter + + def needs_raw_ms_data(self): + """ + 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 + """ + return False, None + + def can_execute(self, sample, collection, **runtime_params): + """ + 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 + """ + # Check if sample has mass features + if not hasattr(sample, 'mass_features') or not sample.mass_features: + return False + + # Check if any mass features have MS2 spectra associated + has_ms2 = any( + hasattr(mf, 'ms2_mass_spectra') and mf.ms2_mass_spectra + for mf in sample.mass_features.values() + ) + + return has_ms2 + + def execute(self, sample_id, collection, fe_lib=None, molecular_metadata=None, **runtime_params): + """ + 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 + """ + sample = collection[sample_id] + + # Get parameters + ms2_scan_filter = self.params.get('ms2_scan_filter') + + # Verify that we have a spectral library + if fe_lib is None: + raise ValueError( + f"Sample {sample_id}: MS2 spectral search requires fe_lib (FlashEntropy library) " + "to be provided in runtime parameters. Create the library at the collection level " + "and pass it to the pipeline." + ) + + # Extract peak_sep_da from FlashEntropy library configuration + peak_sep_da = fe_lib.entropy_search.max_ms2_tolerance_in_da + if peak_sep_da is None: + raise ValueError( + f"Sample {sample_id}: Could not extract max_ms2_tolerance_in_da from FlashEntropy library. " + "Ensure the library was created with this parameter specified." + ) + + # Verify that sample has _ms dictionary + if not hasattr(sample, '_ms') or not sample._ms: + return 0 # No MS2 spectra to search + + # Get MS2 scan numbers based on filter + if ms2_scan_filter is not None: + # Filter by scan text + ms2_scan_df = sample.scan_df[ + sample.scan_df.scan_text.str.contains(ms2_scan_filter) & + (sample.scan_df.ms_level == 2) + ] + else: + # All MS2 scans + ms2_scan_df = sample.scan_df[sample.scan_df.ms_level == 2] + + # Get scans that are actually loaded in _ms + ms2_scans_to_search = [ + scan for scan in ms2_scan_df.scan.tolist() + if scan in sample._ms.keys() + ] + + if not ms2_scans_to_search: + return 0 # No MS2 spectra to search + + # Perform spectral search using the sample's fe_search method + sample.fe_search( + scan_list=ms2_scans_to_search, + fe_lib=fe_lib, + peak_sep_da=peak_sep_da + ) + + # Return count of spectra searched + return len(ms2_scans_to_search) + + def collect_results(self, sample_id, result, collection): + """ + Collect results (no-op as search modifies mass features in place). + + The MS2 spectral search modifies mass features in place by adding + spectral match results, so no explicit result collection is needed. + + Parameters + ---------- + sample_id : str + Sample identifier + result : int + Number of spectra searched + collection : LCMSCollection + The collection containing the sample + """ + # Search modifies mass features in place, nothing to collect + pass diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 633642f31..5cb59eeb5 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -1,8 +1,15 @@ from pathlib import Path import time import pandas as pd +import numpy as np +from multiprocessing import Pool +import shutil + from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectraCollection, ReadSavedLCMSCollection -from corems.mass_spectra.output.export import LCMSCollectionExport +from corems.mass_spectra.output.export import LCMSCollectionExport, LCMSMetabolomicsExport +from corems.mass_spectra.input.rawFileReader import ImportMassSpectraThermoMSFileReader +from corems.encapsulation.factory.parameters import LCMSParameters +from corems.molecular_id.search.database_interfaces import MSPInterface """ Example showing the new pipeline-based sample processing approach. @@ -15,35 +22,139 @@ 1. High-level convenience method: process_consensus_features() 2. Advanced pipeline builder: process_samples_pipeline() with custom operations """ +def process_single_sample(args): + """ + Process a single LCMS sample file. + + Parameters + ---------- + args : tuple + (raw_file_path, processed_folder) + + Returns + ------- + str + Path to the processed HDF5 file + """ + raw_file_path, processed_folder = args + + # Import the raw data + print(f"Processing {raw_file_path.name}...") + parser = ImportMassSpectraThermoMSFileReader(str(raw_file_path)) + lcms_obj = parser.get_lcms_obj(spectra="ms1") + + # Set parameters to the defaults for reproducible testing + lcms_obj.parameters = LCMSParameters(use_defaults=True) + + # Set parameters on the LCMS object that are reasonable for testing + ## persistent homology parameters + lcms_obj.parameters.lc_ms.peak_picking_method = "persistent homology" + lcms_obj.parameters.lc_ms.ph_inten_min_rel = 0.0005 + lcms_obj.parameters.lc_ms.ph_persis_min_rel = 0.01 + lcms_obj.parameters.lc_ms.ph_smooth_it = 0 + lcms_obj.parameters.lc_ms.ms2_min_fe_score = 0.3 + lcms_obj.parameters.lc_ms.ms1_scans_to_average = 1 + + ## MSParameters for ms1 mass spectra + ms1_params = lcms_obj.parameters.mass_spectrum['ms1'] + ms1_params.mass_spectrum.noise_threshold_method = "relative_abundance" + ms1_params.mass_spectrum.noise_threshold_min_relative_abundance = 0.1 + ms1_params.mass_spectrum.noise_min_mz, ms1_params.mass_spectrum.min_picking_mz = 0, 0 + ms1_params.mass_spectrum.noise_max_mz, ms1_params.mass_spectrum.max_picking_mz = np.inf, np.inf + ms1_params.ms_peak.legacy_resolving_power = False + ms1_params.molecular_search.url_database = "" + ms1_params.molecular_search.usedAtoms = { + 'C': (5, 30), + 'H': (18, 200), + 'O': (1, 23), + 'N': (0, 3), + 'P': (0, 1), + 'S': (0, 1), + } + + ## settings for ms2 data (HCD scans) + ms2_params_hcd = ms1_params.copy() + lcms_obj.parameters.mass_spectrum['ms2'] = ms2_params_hcd + + ## reporting settings + lcms_obj.parameters.lc_ms.export_eics = True + lcms_obj.parameters.lc_ms.export_profile_spectra = True + + ## peak metrics filtering settings + lcms_obj.parameters.lc_ms.remove_mass_features_by_peak_metrics = True + lcms_obj.parameters.lc_ms.mass_feature_attribute_filter_dict = { + 'dispersity_index': {'value': 0.5, 'operator': '<'} + } + + # Use persistent homology to find mass features in the lc-ms data + lcms_obj.find_mass_features() + lcms_obj.integrate_mass_features(drop_if_fail=True) + + # Add peak metrics and filter mass features based on the new parameters + lcms_obj.add_peak_metrics(remove_by_metrics=True) + + # Save to HDF5 + output_name = raw_file_path.stem + exporter = LCMSMetabolomicsExport(str(processed_folder / output_name), lcms_obj) + exporter.to_hdf(overwrite=True) + + print(f"✓ Completed {raw_file_path.name} - {len(lcms_obj.mass_features)} mass features") + return str(processed_folder / f"{output_name}.hdf5") if __name__ == "__main__": - - # Set the path to the collection of LCMS runs (previously processed) - collection_path = Path("/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test_out2") - # Path to manifest file - manifest_file = Path("/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test_out2/manifest_tiny.csv") + ncores = 1 + reprocess_samples = False # Set to True to reprocess raw data, False to use existing processed data - # Set the number of cores to use for loading the data (the parser is parallelized) - ncores = 3 + # Set paths + raw_data_path = Path("/Volumes/LaCie/nmdc_data/collection_testing/dev_test/raw") + processed_folder = Path("/Volumes/LaCie/nmdc_data/collection_testing/dev_test/processed") + msp_file_location = Path("/Users/heal742/LOCAL/05_NMDC/02_MetaMS/metams/test_data/test_lcms_metab_data/20250407_database.msp") + + if reprocess_samples: + # Delete existing processed dir if reprocessing + if processed_folder.exists(): + shutil.rmtree(processed_folder) - # Instantiate the parser - # Note, these samples have not had the DDA MS2 scans associated or MS1 data loaded + # Create processed folder if it doesn't exist + processed_folder.mkdir(parents=True, exist_ok=True) + + # Find all raw files (adjust extension based on your data format) + raw_files = list(raw_data_path.glob("*.raw")) + list(raw_data_path.glob("*.mzML")) + + if not raw_files: + raise ValueError(f"No raw files found in {raw_data_path}") + + print(f"\n=== Preprocessing {len(raw_files)} samples in parallel using {ncores} cores ===") + start_time = time.time() + + # Prepare arguments for parallel processing + process_args = [(raw_file, processed_folder) for raw_file in raw_files] + + # Process samples in parallel + with Pool(processes=ncores) as pool: + processed_files = pool.map(process_single_sample, process_args) + + print(f"Time to preprocess all samples: {time.time() - start_time:.1f} seconds using {ncores} cores") + print(f"Processed {len(processed_files)} samples to {processed_folder}\n") + + # Set the path to the collection of LCMS runs (previously processed) + collection_path = processed_folder + + # Instantiate the parser (manifest will be auto-generated if it doesn't exist) parser = ReadCoreMSHDFMassSpectraCollection( folder_location = collection_path, - manifest_file = manifest_file, cores = ncores ) - print("Loading LCMS collection with", len(parser.manifest), "samples using", ncores, "cores") + print("\n=== Loading LCMS collection with", len(parser.manifest), "samples using", ncores, "cores ===") # Load the LCMS collection (minimally load the data) start_time = time.time() lcms_collection = parser.get_lcms_collection(load_raw=False, load_light=True) print("Time to load LCMS collection ", time.time() - start_time, "seconds -", len(lcms_collection), " LCMS runs and ", ncores, " cores") - # Update raw file locations (optionally, but common) - og_file_location = lcms_collection[0].raw_file_location + # Update raw file locations to point to the raw data folder lcms_collection.update_raw_file_locations( - new_raw_folder = "/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/mini_collection_test2" + new_raw_folder = str(raw_data_path) ) print("Number of total mass features: ", len(lcms_collection.mass_features_dataframe)) @@ -60,34 +171,61 @@ assert lcms_collection.rt_alignments is not None, "LCMS collection should have rt_alignments now." print("Time to align LCMS collection: ", time.time() - start_time, "seconds") - """# Make some plots - lcms_collection.plot_tics(type="both") - lcms_collection.plot_alignments() - """ - # Make consensus mass features from the consolidated mass features print("Generating consensus mass features across the LCMS collection") start_time = time.time() lcms_collection.add_consensus_mass_features() print("Time to generate consensus mass features: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") - # NEW PIPELINE APPROACH: Gap fill, reload, add MS1/MS2, and run molecular formula search in a single pass - print("\n=== Testing new pipeline approach with MS1, MS2, and molecular formula search ===") + # Tell the user how many clusters were generated + print(f"Total clusters formed: {len(lcms_collection.cluster_summary_dataframe)}") + + # Prepare spectral library for MS2 search (mimicking test_lcms_metabolomics) + print("\n=== Preparing spectral library for MS2 search ===") + + # Check if MSP file exists before attempting to load + if msp_file_location.exists(): + my_msp = MSPInterface(file_path=msp_file_location) + spectral_lib, molecular_metadata = my_msp.get_metabolomics_spectra_library( + polarity="negative", # Change to match your data polarity + format="flashentropy", + normalize=True, + fe_kwargs={ + "normalize_intensity": True, + "min_ms2_difference_in_da": 0.02, # for cleaning spectra + "max_ms2_tolerance_in_da": 0.01, # for setting search space + "max_indexed_mz": 3000, + "precursor_ions_removal_da": None, + "noise_threshold": 0, + }, + ) + print(f"Loaded spectral library with {len(molecular_metadata)} entries") + enable_spectral_search = True + else: + raise FileNotFoundError(f"MSP file not found at {msp_file_location}. Cannot perform spectral search.") + + # PIPELINE APPROACH: Gap fill, reload, add MS1/MS2, run molecular formula and spectral search in a single pass + print("\n=== Testing new pipeline approach with MS1, MS2, molecular formula, and spectral search ===") start_time = time.time() pipeline_results = lcms_collection.process_consensus_features( - perform_gap_filling=False, + perform_gap_filling=True, reload_representatives=True, - add_ms1=True, - add_ms2=False, - auto_process_ms2=True, - ms2_scan_filter=None, - molecular_formula_search=True, # New: run molecular formula search + add_ms1=True, + add_ms2=True, + molecular_formula_search=True, + ms2_spectral_search=True, + spectral_lib=spectral_lib, + molecular_metadata=molecular_metadata, keep_raw_data=False ) - print("Time for combined gap-fill, reload, MS1, MS2, and MF search: ", time.time() - start_time, "seconds, using", ncores, " cores") + print("Time for combined reload, MS1, MS2, MF search, and spectral search: ", time.time() - start_time, "seconds, using", ncores, " cores") print("Gap-filled features in", len([s for s in pipeline_results.get('gap_fill', {}).values() if s]), "samples") print("Reloaded features in", len([s for s in pipeline_results.get('reload', {}).values() if s]), "samples") print("Molecular formula search completed on", len([s for s in pipeline_results.get('mf_search', {}).values() if s]), "samples") + if enable_spectral_search: + print("MS2 spectral search completed on", len([s for s in pipeline_results.get('ms2_search', {}).values() if s and s > 0]), "samples") + total_ms2_searched = sum([s for s in pipeline_results.get('ms2_search', {}).values() if s]) + print(f"Total MS2 spectra searched: {total_ms2_searched}") # Verify that mass features were reloaded total_mf_reloaded = sum([len(lcms_obj.mass_features) for lcms_obj in lcms_collection]) @@ -153,6 +291,28 @@ print("✓ Raw MS1 data successfully cleaned up after pipeline") else: print("⚠ Raw MS1 data still present (expected if keep_raw_data=True)") + + # Check for spectral match results (if spectral search was performed) + if enable_spectral_search: + total_spectral_matches = 0 + total_mf_with_matches = 0 + for lcms_obj in lcms_collection: + if hasattr(lcms_obj, 'spectral_search_results') and lcms_obj.spectral_search_results: + total_spectral_matches += len(lcms_obj.spectral_search_results) + total_mf_with_matches += 1 + break # Count each mass feature only once + + print(f"\nSpectral Search Results:") + print(f"Total mass features with spectral matches: {total_mf_with_matches}") + print(f"Total spectral matches: {total_spectral_matches}") + if total_mf_with_matches > 0: + print(f"✓ MS2 spectral search successfully found matches") + print(f" Average {total_spectral_matches/total_mf_with_matches:.1f} matches per feature") + # Access the molecular metadata if needed for export + if hasattr(lcms_collection, 'spectral_search_molecular_metadata'): + print(f" Molecular metadata available with {len(lcms_collection.spectral_search_molecular_metadata)} entries") + else: + print("⚠ No spectral matches found (check library and search parameters)") """ # OLD APPROACH (commented out - replaced by pipeline above): From d9d26114465723bcc9469e998e95b08b9bf66d03 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 19 Jan 2026 16:05:26 -0800 Subject: [PATCH 102/158] Add MS2 searching to collection framework --- corems/mass_spectra/calc/lc_calc.py | 45 +- .../mass_spectra/calc/lc_calc_operations.py | 141 ++++- .../search/database_interfaces.py | 2 - .../nmdc/lipidomics/lipidomics_collection.py | 578 +++++++++--------- 4 files changed, 450 insertions(+), 316 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 28300659e..432b82290 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -4294,7 +4294,7 @@ def fill_missing_cluster_features(self): for sample_name in self.samples: self._lcms[sample_name].mass_features = {} - def process_samples_pipeline(self, operations, description="Processing samples", keep_raw_data=False): + def process_samples_pipeline(self, operations, description=None, keep_raw_data=False): """ Execute a pipeline of operations on all samples in parallel. @@ -4308,8 +4308,10 @@ def process_samples_pipeline(self, operations, description="Processing samples", List of operations to perform on each sample, in order. Each operation should be an instance of a class derived from SampleOperation (see lc_calc_operations module). - description : str, optional - Progress bar description. Default is "Processing samples". + description : str or None, optional + Progress bar description. If None, automatically generates description + from operation descriptions (e.g., "gap-filling, reloading features"). + Default is None. keep_raw_data : bool, optional If True, keeps raw MS data loaded in memory after pipeline completes. If False, cleans up raw data to free memory. Default is False. @@ -4360,6 +4362,11 @@ def process_samples_pipeline(self, operations, description="Processing samples", if not isinstance(op, SampleOperation): raise ValueError(f"All operations must be SampleOperation instances, got {type(op)}") + # Generate description from operations if not provided + if description is None: + operation_descriptions = [op.description for op in operations] + description = ", ".join(operation_descriptions).capitalize() + # Prepare runtime parameters for each operation # This is where we gather collection-level data that operations need runtime_params = self._prepare_pipeline_runtime_params(operations) @@ -4373,7 +4380,9 @@ def process_samples_pipeline(self, operations, description="Processing samples", from tqdm import tqdm results_by_operation = {op.name: {} for op in operations} - for sample_id in tqdm(range(sample_ct), desc=description, unit="sample"): + # Print description on its own line before progress bar + print(f"\n{description.capitalize()}:") + for sample_id in tqdm(range(sample_ct), unit="sample", ncols=80): sample_results = self._execute_sample_pipeline( sample_id, operations, runtime_params, inplace=True ) @@ -4405,7 +4414,8 @@ def process_samples_pipeline(self, operations, description="Processing samples", # Collect results back into collection results_by_operation = {op.name: {} for op in operations} - for sample_id in tqdm(range(sample_ct), desc=f"Collecting {description} results", unit="sample"): + print(f"\nCollecting {description} results:") + for sample_id in tqdm(range(sample_ct), unit="sample", ncols=80): sample_results = mp_results[sample_id] # Let each operation collect its results @@ -4607,7 +4617,7 @@ def _execute_sample_pipeline(self, sample_id, operations, runtime_params, inplac return results - def process_consensus_features(self, perform_gap_filling=True, reload_representatives=True, + def process_consensus_features(self, load_representatives=True, perform_gap_filling=True, add_ms1=False, add_ms2=False, ms2_scan_filter=None, molecular_formula_search=False, ms2_spectral_search=False, spectral_lib=None, @@ -4623,18 +4633,18 @@ def process_consensus_features(self, perform_gap_filling=True, reload_representa Parameters ---------- + load_representatives : bool, optional + If True, loads representative mass features from HDF5. Default is True. perform_gap_filling : bool, optional If True, performs gap-filling for missing cluster features. Default is True. This operation loads raw MS1 data which can be reused by subsequent operations. - reload_representatives : bool, optional - If True, reloads representative mass features from HDF5. Default is True. add_ms1 : bool, optional - If True and reload_representatives=True, associates MS1 spectra with - reloaded features. Automatically uses raw data from gap-filling if available, + If True and load_representatives=True, associates MS1 spectra with + loaded features. Automatically uses raw data from gap-filling if available, otherwise uses parser. Spectrum mode is auto-detected. Default is False. add_ms2 : bool, optional - If True and reload_representatives=True, associates MS2 spectra with - reloaded features and automatically processes them. Spectrum mode is auto-detected. Default is False. + If True and load_representatives=True, associates MS2 spectra with + loaded features and automatically processes them. Spectrum mode is auto-detected. Default is False. ms2_scan_filter : str or None, optional Filter string for MS2 scans (e.g., 'hcd'). Default is None. molecular_formula_search : bool, optional @@ -4705,8 +4715,8 @@ def process_consensus_features(self, perform_gap_filling=True, reload_representa >>> >>> # Gap-fill, reload with MS1/MS2, perform molecular formula and spectral search >>> results = lcms_collection.process_consensus_features( + ... load_representatives=True, ... perform_gap_filling=True, - ... reload_representatives=True, ... add_ms1=True, ... add_ms2=True, ... molecular_formula_search=True, @@ -4727,8 +4737,8 @@ def process_consensus_features(self, perform_gap_filling=True, reload_representa ) # Validate that at least one operation is enabled - if not perform_gap_filling and not reload_representatives: - raise ValueError("At least one of perform_gap_filling or reload_representatives must be True") + if not perform_gap_filling and not load_representatives: + raise ValueError("At least one of perform_gap_filling or load_representatives must be True") # Validate prerequisites for gap-filling if perform_gap_filling: @@ -4757,7 +4767,7 @@ def process_consensus_features(self, perform_gap_filling=True, reload_representa expand_on_miss = self.parameters.lcms_collection.gap_fill_expand_on_miss operations.append(GapFillOperation('gap_fill', expand_on_miss=expand_on_miss)) - if reload_representatives: + if load_representatives: operations.append(ReloadFeaturesOperation( 'reload', add_ms1=add_ms1, @@ -4778,10 +4788,9 @@ def process_consensus_features(self, perform_gap_filling=True, reload_representa self._spectral_lib = spectral_lib self._spectral_search_molecular_metadata = molecular_metadata - # Execute pipeline + # Execute pipeline (description auto-generated from operations) results = self.process_samples_pipeline( operations, - description="Gap-filling and reloading features", keep_raw_data=keep_raw_data ) diff --git a/corems/mass_spectra/calc/lc_calc_operations.py b/corems/mass_spectra/calc/lc_calc_operations.py index af693e0ec..cab1cdf5a 100644 --- a/corems/mass_spectra/calc/lc_calc_operations.py +++ b/corems/mass_spectra/calc/lc_calc_operations.py @@ -15,16 +15,16 @@ """ +from abc import ABC, abstractmethod import pandas as pd -class SampleOperation: +class SampleOperation(ABC): """ Base class for operations that can be performed on a sample. - All sample operations should inherit from this class and implement - the execute() method. Optionally override can_execute() for conditional - execution and collect_results() for custom result collection. + All sample operations must inherit from this class and implement all + abstract methods. This ensures proper integration with the pipeline framework. Parameters ---------- @@ -39,19 +39,39 @@ class SampleOperation: Operation name params : dict Dictionary of operation parameters + description : str + Human-readable description for progress messages (must override in subclasses) """ def __init__(self, name, **kwargs): self.name = name self.params = kwargs + + @property + @abstractmethod + def description(self): + """ + 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 + """ + pass + + @abstractmethod def needs_raw_ms_data(self): """ Declare whether this operation needs raw MS data loaded. - Override 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. + 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 ------- @@ -59,13 +79,24 @@ def needs_raw_ms_data(self): (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 """ - return False, None - + pass + + @abstractmethod def can_execute(self, sample, collection): """ 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 @@ -77,9 +108,17 @@ def can_execute(self, sample, collection): ------- 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 """ - return True - + pass + + @abstractmethod def execute(self, sample_id, collection, **runtime_params): """ Execute the operation on a sample. @@ -100,14 +139,16 @@ def execute(self, sample_id, collection, **runtime_params): result Operation result (can be None if operation modifies sample in place) """ - raise NotImplementedError(f"execute() not implemented for {self.__class__.__name__}") - + pass + + @abstractmethod def collect_results(self, sample_id, result, collection): """ Collect results back into collection after parallel execution. - Override this method if the operation returns results that need - to be collected back into the collection object. + 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 ---------- @@ -117,6 +158,13 @@ def collect_results(self, sample_id, result, collection): 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 """ pass @@ -145,6 +193,11 @@ class GapFillOperation(SampleOperation): This operation loads raw MS1 data which will be available for subsequent operations. """ + @property + def description(self): + """Human-readable description for progress messages.""" + return "gap-filling" + def needs_raw_ms_data(self): """This operation needs raw MS1 data.""" return True, 1 @@ -325,6 +378,15 @@ class ReloadFeaturesOperation(SampleOperation): the data. """ + @property + def description(self): + """Human-readable description for progress messages.""" + return "reloading features" + + def needs_raw_ms_data(self): + """This operation doesn't need raw data.""" + return False, None + def can_execute(self, sample, collection): """Check if collection parser is available.""" return hasattr(collection, 'collection_parser') and \ @@ -465,6 +527,11 @@ class MolecularFormulaSearchOperation(SampleOperation): parameters.mass_spectrum["ms1"].molecular_search settings. """ + @property + def description(self): + """Human-readable description for progress messages.""" + return "molecular formula search" + def __init__(self, name='molecular_formula_search', **kwargs): super().__init__(name, **kwargs) @@ -673,6 +740,11 @@ class MS2SpectralSearchOperation(SampleOperation): match scores and metadata. """ + @property + def description(self): + """Human-readable description for progress messages.""" + return "MS2 spectral search" + def __init__(self, name='ms2_spectral_search', ms2_scan_filter=None, **kwargs): super().__init__(name, **kwargs) self.params['ms2_scan_filter'] = ms2_scan_filter @@ -798,24 +870,45 @@ def execute(self, sample_id, collection, fe_lib=None, molecular_metadata=None, * peak_sep_da=peak_sep_da ) - # Return count of spectra searched - return len(ms2_scans_to_search) + # Return the spectral search results for collection + # (needed for multiprocessing - results populated in worker need to be returned) + return sample.spectral_search_results def collect_results(self, sample_id, result, collection): """ - Collect results (no-op as search modifies mass features in place). + Collect spectral search results back into the sample. - The MS2 spectral search modifies mass features in place by adding - spectral match results, so no explicit result collection is needed. + 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 : int - Number of spectra searched + result : dict + Dictionary of spectral search results from execute() collection : LCMSCollection The collection containing the sample """ - # Search modifies mass features in place, nothing to collect - pass + # Assign the spectral search results back to the sample + if result: + collection[sample_id].spectral_search_results.update(result) + + # Re-associate results with mass features (same logic as fe_search) + sample = collection[sample_id] + if len(sample.mass_features) > 0: + for mass_feature_id, mass_feature in sample.mass_features.items(): + scan_ids = mass_feature.ms2_scan_numbers + for ms2_scan_id in scan_ids: + precursor_mz = mass_feature.mz + try: + sample.spectral_search_results[ms2_scan_id][precursor_mz] + except KeyError: + pass + else: + sample.mass_features[ + mass_feature_id + ].ms2_similarity_results.append( + sample.spectral_search_results[ms2_scan_id][precursor_mz] + ) diff --git a/corems/molecular_id/search/database_interfaces.py b/corems/molecular_id/search/database_interfaces.py index 72e0f4aad..5bb6bf766 100644 --- a/corems/molecular_id/search/database_interfaces.py +++ b/corems/molecular_id/search/database_interfaces.py @@ -1326,8 +1326,6 @@ def get_metabolomics_spectra_library( db_df.rename(columns=metabolite_metadata_mapping, inplace=True) db_df["molecular_data_id"] = db_df["inchikey"] - - # Check if the resulting dataframe has the required columns for the flash entropy search required_columns = ["molecular_data_id", "precursor_mz", "ion_type", "id"] for col in required_columns: diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 5cb59eeb5..5c9329699 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -5,12 +5,12 @@ from multiprocessing import Pool import shutil -from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectraCollection, ReadSavedLCMSCollection -from corems.mass_spectra.output.export import LCMSCollectionExport, LCMSMetabolomicsExport +from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectraCollection +from corems.mass_spectra.output.export import LCMSMetabolomicsExport from corems.mass_spectra.input.rawFileReader import ImportMassSpectraThermoMSFileReader from corems.encapsulation.factory.parameters import LCMSParameters from corems.molecular_id.search.database_interfaces import MSPInterface - +from corems.encapsulation.factory.parameters import hush_output """ Example showing the new pipeline-based sample processing approach. @@ -22,45 +22,196 @@ 1. High-level convenience method: process_consensus_features() 2. Advanced pipeline builder: process_samples_pipeline() with custom operations """ -def process_single_sample(args): + +def summarize_processing_results(lcms_collection): """ - Process a single LCMS sample file. + Summarize the processing state of the LCMS collection. + + Reports on completed processing steps by inspecting the collection + and sample objects directly. Useful for verifying which operations + were performed during process_consensus_features(). Parameters ---------- - args : tuple - (raw_file_path, processed_folder) + lcms_collection : LCMSCollection + The LCMS collection to summarize + """ + print("\n" + "="*60) + print("LCMS Collection Processing Summary") + print("="*60) + + # Basic collection info + n_samples = len(lcms_collection) + total_mf = sum(len(lcms_obj.mass_features) for lcms_obj in lcms_collection) + print(f"\nSamples: {n_samples}") + print(f"Total mass features: {total_mf}") + + # Gap filling - check for induced mass features + induced_counts = [len(lcms_obj.induced_mass_features) for lcms_obj in lcms_collection] + total_induced = sum(induced_counts) + samples_with_induced = sum(1 for c in induced_counts if c > 0) + if total_induced > 0: + print(f"\nGap Filling: ✓ Complete") + print(f" {samples_with_induced}/{n_samples} samples have induced features ({total_induced} total)") + + # Feature loading - check if mass features have MS1/MS2 spectra + mf_with_ms1 = 0 + mf_with_ms2 = 0 + total_ms2_spectra = 0 + + for lcms_obj in lcms_collection: + for mf in lcms_obj.mass_features.values(): + if hasattr(mf, 'mass_spectrum') and mf.mass_spectrum is not None: + mf_with_ms1 += 1 + if hasattr(mf, 'ms2_mass_spectra') and mf.ms2_mass_spectra: + mf_with_ms2 += 1 + total_ms2_spectra += len(mf.ms2_mass_spectra) + + if mf_with_ms1 > 0 or mf_with_ms2 > 0: + print(f"\nMS Data Association: ✓ Complete") + if mf_with_ms1 > 0: + print(f" MS1: {mf_with_ms1}/{total_mf} features ({mf_with_ms1/total_mf*100:.1f}%)") + if mf_with_ms2 > 0: + print(f" MS2: {mf_with_ms2}/{total_mf} features ({total_ms2_spectra} spectra)") + + # Molecular formula search + mf_with_formulas = 0 + total_formulas = 0 + + for lcms_obj in lcms_collection: + for mf in lcms_obj.mass_features.values(): + if hasattr(mf, 'mass_spectrum') and mf.mass_spectrum is not None: + try: + ms1_peak = mf.ms1_peak + if hasattr(ms1_peak, 'molecular_formulas') and ms1_peak.molecular_formulas: + mf_with_formulas += 1 + total_formulas += len(ms1_peak.molecular_formulas) + except (AttributeError, IndexError): + pass + + if mf_with_formulas > 0: + print(f"\nMolecular Formula Search: ✓ Complete") + print(f" {mf_with_formulas}/{total_mf} features assigned ({total_formulas} total formulas)") + print(f" Average {total_formulas/mf_with_formulas:.1f} formulas per feature") + + # MS2 spectral search + mf_with_spectral_matches = 0 + total_spectral_matches = 0 + scans_searched = 0 + + for lcms_obj in lcms_collection: + if hasattr(lcms_obj, 'spectral_search_results') and lcms_obj.spectral_search_results: + scans_searched += len(lcms_obj.spectral_search_results) + + for mf in lcms_obj.mass_features.values(): + if hasattr(mf, 'ms2_similarity_results') and mf.ms2_similarity_results: + mf_with_spectral_matches += 1 + total_spectral_matches += len(mf.ms2_similarity_results) + + if mf_with_spectral_matches > 0: + print(f"\nMS2 Spectral Search: ✓ Complete") + print(f" {scans_searched} MS2 scans searched") + print(f" {mf_with_spectral_matches}/{total_mf} features matched ({total_spectral_matches} total matches)") + if hasattr(lcms_collection, 'spectral_search_molecular_metadata'): + print(f" Library size: {len(lcms_collection.spectral_search_molecular_metadata)} entries") + + # Memory management check + raw_data_present = any(1 in lcms_obj._ms_unprocessed and not lcms_obj._ms_unprocessed[1].empty + for lcms_obj in lcms_collection) + if not raw_data_present: + print(f"\nMemory: ✓ Raw MS1 data cleaned") + + print("\n" + "="*60) + + +def preprocess_raw_samples(raw_data_path, processed_folder, ncores=1, reprocess=False): + """ + Preprocess raw LCMS sample files into HDF5 format. + + Parameters + ---------- + raw_data_path : Path + Path to folder containing raw data files + processed_folder : Path + Path to folder where processed HDF5 files will be saved + ncores : int, optional + Number of cores to use for parallel processing. Default is 1. + reprocess : bool, optional + If True, deletes existing processed folder and reprocesses all files. + If False, skips preprocessing. Default is False. Returns ------- - str - Path to the processed HDF5 file + list or None + List of processed HDF5 file paths if reprocess=True, None otherwise """ - raw_file_path, processed_folder = args + if not reprocess: + print("\n=== Skipping sample preprocessing (using existing processed data) ===") + return None - # Import the raw data - print(f"Processing {raw_file_path.name}...") - parser = ImportMassSpectraThermoMSFileReader(str(raw_file_path)) - lcms_obj = parser.get_lcms_obj(spectra="ms1") + # Delete existing processed dir if reprocessing + if processed_folder.exists(): + shutil.rmtree(processed_folder) + + # Create processed folder + processed_folder.mkdir(parents=True, exist_ok=True) + + # Find all raw files (adjust extension based on your data format) + raw_files = list(raw_data_path.glob("*.raw")) + list(raw_data_path.glob("*.mzML")) + + if not raw_files: + raise ValueError(f"No raw files found in {raw_data_path}") + + print(f"\n=== Preprocessing {len(raw_files)} samples in parallel using {ncores} cores ===") + start_time = time.time() + + # Get configured parameters once (will be shared across all workers) + params = get_configured_lcms_parameters() + + # Prepare arguments for parallel processing + process_args = [(raw_file, processed_folder, params) for raw_file in raw_files] + + # Process samples in parallel + with Pool(processes=ncores) as pool: + processed_files = pool.map(process_single_sample, process_args) + + print(f"Preprocessing complete: {time.time() - start_time:.1f} seconds using {ncores} cores") + print(f"Processed {len(processed_files)} samples\n") - # Set parameters to the defaults for reproducible testing - lcms_obj.parameters = LCMSParameters(use_defaults=True) + return processed_files - # Set parameters on the LCMS object that are reasonable for testing - ## persistent homology parameters - lcms_obj.parameters.lc_ms.peak_picking_method = "persistent homology" - lcms_obj.parameters.lc_ms.ph_inten_min_rel = 0.0005 - lcms_obj.parameters.lc_ms.ph_persis_min_rel = 0.01 - lcms_obj.parameters.lc_ms.ph_smooth_it = 0 - lcms_obj.parameters.lc_ms.ms2_min_fe_score = 0.3 - lcms_obj.parameters.lc_ms.ms1_scans_to_average = 1 - ## MSParameters for ms1 mass spectra - ms1_params = lcms_obj.parameters.mass_spectrum['ms1'] +def get_configured_lcms_parameters(): + """ + Create and configure LCMSParameters for sample processing. + + Returns + ------- + LCMSParameters + Configured parameters object with all processing settings + """ + # Suppress verbose output before creating parameters + hush_output() + + # Create parameters (use_defaults=False respects hush_output) + params = LCMSParameters() + + # Persistent homology parameters + params.lc_ms.peak_picking_method = "persistent homology" + params.lc_ms.ph_inten_min_rel = 0.0005 + params.lc_ms.ph_persis_min_rel = 0.01 + params.lc_ms.ph_smooth_it = 0 + params.lc_ms.ms2_min_fe_score = 0.3 + params.lc_ms.ms1_scans_to_average = 1 + + # MSParameters for ms1 mass spectra + ms1_params = params.mass_spectrum['ms1'] ms1_params.mass_spectrum.noise_threshold_method = "relative_abundance" ms1_params.mass_spectrum.noise_threshold_min_relative_abundance = 0.1 - ms1_params.mass_spectrum.noise_min_mz, ms1_params.mass_spectrum.min_picking_mz = 0, 0 - ms1_params.mass_spectrum.noise_max_mz, ms1_params.mass_spectrum.max_picking_mz = np.inf, np.inf + ms1_params.mass_spectrum.noise_min_mz = 0 + ms1_params.mass_spectrum.min_picking_mz = 0 + ms1_params.mass_spectrum.noise_max_mz = np.inf + ms1_params.mass_spectrum.max_picking_mz = np.inf ms1_params.ms_peak.legacy_resolving_power = False ms1_params.molecular_search.url_database = "" ms1_params.molecular_search.usedAtoms = { @@ -71,20 +222,47 @@ def process_single_sample(args): 'P': (0, 1), 'S': (0, 1), } - - ## settings for ms2 data (HCD scans) + + # Settings for ms2 data (HCD scans) ms2_params_hcd = ms1_params.copy() - lcms_obj.parameters.mass_spectrum['ms2'] = ms2_params_hcd - - ## reporting settings - lcms_obj.parameters.lc_ms.export_eics = True - lcms_obj.parameters.lc_ms.export_profile_spectra = True - - ## peak metrics filtering settings - lcms_obj.parameters.lc_ms.remove_mass_features_by_peak_metrics = True - lcms_obj.parameters.lc_ms.mass_feature_attribute_filter_dict = { + params.mass_spectrum['ms2'] = ms2_params_hcd + + # Reporting settings + params.lc_ms.export_eics = True + params.lc_ms.export_profile_spectra = True + + # Peak metrics filtering settings + params.lc_ms.remove_mass_features_by_peak_metrics = True + params.lc_ms.mass_feature_attribute_filter_dict = { 'dispersity_index': {'value': 0.5, 'operator': '<'} } + + return params + + +def process_single_sample(args): + """ + Process a single LCMS sample file. + + Parameters, params) + + Returns + ------- + str + Path to the processed HDF5 file + """ + raw_file_path, processed_folder, params = args + + # Import the raw data + print(f"Processing {raw_file_path.name}...\n") + parser = ImportMassSpectraThermoMSFileReader(str(raw_file_path)) + lcms_obj = parser.get_lcms_obj(spectra="ms1") + + # Use the pre-configured parameters + lcms_obj.parameters = params + + # Get configured parameters + lcms_obj.parameters = get_configured_lcms_parameters() # Use persistent homology to find mass features in the lc-ms data lcms_obj.find_mass_features() @@ -102,114 +280,96 @@ def process_single_sample(args): return str(processed_folder / f"{output_name}.hdf5") if __name__ == "__main__": - ncores = 1 - reprocess_samples = False # Set to True to reprocess raw data, False to use existing processed data - - # Set paths - raw_data_path = Path("/Volumes/LaCie/nmdc_data/collection_testing/dev_test/raw") - processed_folder = Path("/Volumes/LaCie/nmdc_data/collection_testing/dev_test/processed") + # ============================================================================= + # Configuration + # ============================================================================= + ncores = 3 + reprocess_samples = False # Set to True to reprocess raw data + + # Paths + base_path = Path("/Volumes/LaCie/nmdc_data/collection_testing/dev_test/") + collection_save_path = base_path / "collection" + raw_data_path = base_path / "raw" + processed_folder = base_path / "processed" msp_file_location = Path("/Users/heal742/LOCAL/05_NMDC/02_MetaMS/metams/test_data/test_lcms_metab_data/20250407_database.msp") + new_raw_data_path = raw_data_path # Update raw file paths in collection if moved after the first step + # ============================================================================= + # Step 1: Preprocess Individual Samples (Optional) + # ============================================================================= if reprocess_samples: - # Delete existing processed dir if reprocessing - if processed_folder.exists(): - shutil.rmtree(processed_folder) - - # Create processed folder if it doesn't exist - processed_folder.mkdir(parents=True, exist_ok=True) - - # Find all raw files (adjust extension based on your data format) - raw_files = list(raw_data_path.glob("*.raw")) + list(raw_data_path.glob("*.mzML")) - - if not raw_files: - raise ValueError(f"No raw files found in {raw_data_path}") - - print(f"\n=== Preprocessing {len(raw_files)} samples in parallel using {ncores} cores ===") - start_time = time.time() - - # Prepare arguments for parallel processing - process_args = [(raw_file, processed_folder) for raw_file in raw_files] - - # Process samples in parallel - with Pool(processes=ncores) as pool: - processed_files = pool.map(process_single_sample, process_args) - - print(f"Time to preprocess all samples: {time.time() - start_time:.1f} seconds using {ncores} cores") - print(f"Processed {len(processed_files)} samples to {processed_folder}\n") + print("\n=== Preprocessing Raw Samples and Doing Initial Peak Picking===") + preprocess_raw_samples( + raw_data_path=raw_data_path, + processed_folder=processed_folder, + ncores=ncores, + reprocess=reprocess_samples + ) - # Set the path to the collection of LCMS runs (previously processed) - collection_path = processed_folder - - # Instantiate the parser (manifest will be auto-generated if it doesn't exist) + # ============================================================================= + # Step 2: Load LCMS Collection + # ============================================================================= + print("\n=== Loading LCMS Collection ===") parser = ReadCoreMSHDFMassSpectraCollection( - folder_location = collection_path, - cores = ncores - ) - print("\n=== Loading LCMS collection with", len(parser.manifest), "samples using", ncores, "cores ===") - - # Load the LCMS collection (minimally load the data) + folder_location=processed_folder, + cores=ncores + ) + print(f"Found {len(parser.manifest)} samples") + + # Load collection (light loading for efficiency) start_time = time.time() lcms_collection = parser.get_lcms_collection(load_raw=False, load_light=True) - print("Time to load LCMS collection ", time.time() - start_time, "seconds -", len(lcms_collection), " LCMS runs and ", ncores, " cores") - - # Update raw file locations to point to the raw data folder - lcms_collection.update_raw_file_locations( - new_raw_folder = str(raw_data_path) - ) - print("Number of total mass features: ", len(lcms_collection.mass_features_dataframe)) - - # Align the LCMS runs between each other - # For now, adjusting this parameter to force alignment for testing - lcms_collection.parameters.lcms_collection.alignment_acceptance_fraction_improved_threshold = -1 - lcms_collection.parameters.lcms_collection.alignment_acceptance_technique = ['fraction_improved'] - print("Aligning LCMS collection") + print(f"Loaded in {time.time() - start_time:.1f} seconds") + print(f"Total mass features: {len(lcms_collection.mass_features_dataframe)}") + + # Update raw file locations + lcms_collection.update_raw_file_locations(new_raw_folder=str(new_raw_data_path)) + + # ============================================================================= + # Step 3: Align Retention Times Across Samples + # ============================================================================= + print("\n=== Aligning Retention Times ===") start_time = time.time() - assert not lcms_collection.rt_aligned, "LCMS collection should not be marked as retention time aligned yet." - assert lcms_collection.rt_alignments is None, "LCMS collection should not have rt_alignments yet." lcms_collection.align_lcms_objects() - assert lcms_collection.rt_aligned, "LCMS collection should be marked as retention time aligned." - assert lcms_collection.rt_alignments is not None, "LCMS collection should have rt_alignments now." - print("Time to align LCMS collection: ", time.time() - start_time, "seconds") - - # Make consensus mass features from the consolidated mass features - print("Generating consensus mass features across the LCMS collection") + print(f"Alignment complete: {time.time() - start_time:.1f} seconds") + + # ============================================================================= + # Step 4: Generate Consensus Mass Features + # ============================================================================= + print("\n=== Generating Consensus Mass Features ===") start_time = time.time() lcms_collection.add_consensus_mass_features() - print("Time to generate consensus mass features: ", time.time() - start_time, "seconds -", len(lcms_collection.mass_features_dataframe), " total mass features", ncores, " cores") - - # Tell the user how many clusters were generated - print(f"Total clusters formed: {len(lcms_collection.cluster_summary_dataframe)}") - - # Prepare spectral library for MS2 search (mimicking test_lcms_metabolomics) - print("\n=== Preparing spectral library for MS2 search ===") - - # Check if MSP file exists before attempting to load - if msp_file_location.exists(): - my_msp = MSPInterface(file_path=msp_file_location) - spectral_lib, molecular_metadata = my_msp.get_metabolomics_spectra_library( - polarity="negative", # Change to match your data polarity - format="flashentropy", - normalize=True, - fe_kwargs={ - "normalize_intensity": True, - "min_ms2_difference_in_da": 0.02, # for cleaning spectra - "max_ms2_tolerance_in_da": 0.01, # for setting search space - "max_indexed_mz": 3000, - "precursor_ions_removal_da": None, - "noise_threshold": 0, - }, - ) - print(f"Loaded spectral library with {len(molecular_metadata)} entries") - enable_spectral_search = True - else: - raise FileNotFoundError(f"MSP file not found at {msp_file_location}. Cannot perform spectral search.") + print(f"Generated {len(lcms_collection.cluster_summary_dataframe)} consensus clusters") + print(f"Consensus generation: {time.time() - start_time:.1f} seconds") - # PIPELINE APPROACH: Gap fill, reload, add MS1/MS2, run molecular formula and spectral search in a single pass - print("\n=== Testing new pipeline approach with MS1, MS2, molecular formula, and spectral search ===") + # ============================================================================= + # Step 5: Prepare MS2 Spectral Library + # ============================================================================= + print("\n=== Preparing MS2 Spectral Library ===") + my_msp = MSPInterface(file_path=msp_file_location) + spectral_lib, molecular_metadata = my_msp.get_metabolomics_spectra_library( + polarity="positive", + format="flashentropy", + normalize=True, + fe_kwargs={ + "normalize_intensity": True, + "min_ms2_difference_in_da": 0.02, + "max_ms2_tolerance_in_da": 0.01, + "max_indexed_mz": 3000, + "precursor_ions_removal_da": None, + "noise_threshold": 0, + }, + ) + print(f"Loaded spectral library: {len(molecular_metadata)} entries") + + # ============================================================================= + # Step 6: Process Consensus Features with Integrated Pipeline + # ============================================================================= + print("\n=== Processing Consensus Features ===") start_time = time.time() pipeline_results = lcms_collection.process_consensus_features( + load_representatives=True, perform_gap_filling=True, - reload_representatives=True, add_ms1=True, add_ms2=True, molecular_formula_search=True, @@ -218,153 +378,28 @@ def process_single_sample(args): molecular_metadata=molecular_metadata, keep_raw_data=False ) - print("Time for combined reload, MS1, MS2, MF search, and spectral search: ", time.time() - start_time, "seconds, using", ncores, " cores") - print("Gap-filled features in", len([s for s in pipeline_results.get('gap_fill', {}).values() if s]), "samples") - print("Reloaded features in", len([s for s in pipeline_results.get('reload', {}).values() if s]), "samples") - print("Molecular formula search completed on", len([s for s in pipeline_results.get('mf_search', {}).values() if s]), "samples") - if enable_spectral_search: - print("MS2 spectral search completed on", len([s for s in pipeline_results.get('ms2_search', {}).values() if s and s > 0]), "samples") - total_ms2_searched = sum([s for s in pipeline_results.get('ms2_search', {}).values() if s]) - print(f"Total MS2 spectra searched: {total_ms2_searched}") - - # Verify that mass features were reloaded - total_mf_reloaded = sum([len(lcms_obj.mass_features) for lcms_obj in lcms_collection]) - print(f"Total mass features reloaded: {total_mf_reloaded}") - assert total_mf_reloaded > 0, "Should have reloaded some mass features" - - # Check for MS1 associations - total_ms1_with_spectra = 0 - total_mf_checked = 0 - for lcms_obj in lcms_collection: - for mf_id, mf in lcms_obj.mass_features.items(): - total_mf_checked += 1 - if hasattr(mf, 'mass_spectrum') and mf.mass_spectrum is not None: - total_ms1_with_spectra += 1 - - print(f"Total mass features with MS1 spectra: {total_ms1_with_spectra} out of {total_mf_checked}") - if total_ms1_with_spectra > 0: - print(f"✓ MS1 spectra successfully associated with {total_ms1_with_spectra/total_mf_checked*100:.1f}% of mass features") - assert total_ms1_with_spectra > 0, "Should have MS1 spectra associated with mass features" - else: - print("⚠ No MS1 spectra associated") - - # Check for MS2 associations - total_ms2 = 0 - for lcms_obj in lcms_collection: - for mf_id, mf in lcms_obj.mass_features.items(): - if hasattr(mf, 'ms2_mass_spectra') and mf.ms2_mass_spectra: - total_ms2 += len(mf.ms2_mass_spectra) - print(f"Total MS2 spectra associated: {total_ms2}") - if total_ms2 > 0: - print("✓ MS2 spectra successfully associated with mass features") - else: - print("⚠ No MS2 spectra associated (this may be expected if no MS2 data exists)") - - # Check for molecular formula assignments - total_mf_with_formulas = 0 - total_formula_assignments = 0 - for lcms_obj in lcms_collection: - for mf_id, mf in lcms_obj.mass_features.items(): - if hasattr(mf, 'mass_spectrum') and mf.mass_spectrum is not None: - # Check if the ms1_peak has molecular formula assignments - try: - ms1_peak = mf.ms1_peak - if hasattr(ms1_peak, 'molecular_formulas') and ms1_peak.molecular_formulas: - total_mf_with_formulas += 1 - total_formula_assignments += len(ms1_peak.molecular_formulas) - except (AttributeError, IndexError): - # Skip if ms1_peak can't be determined - pass + print(f"Pipeline complete: {time.time() - start_time:.1f} seconds using {ncores} cores") - print(f"Total mass features with molecular formula assignments: {total_mf_with_formulas} out of {total_mf_checked}") - print(f"Total molecular formula assignments: {total_formula_assignments}") - if total_mf_with_formulas > 0: - print(f"✓ Molecular formula search successfully assigned formulas to {total_mf_with_formulas/total_mf_checked*100:.1f}% of mass features") - print(f" Average {total_formula_assignments/total_mf_with_formulas:.1f} formulas per assigned feature") - else: - print("⚠ No molecular formula assignments (check search parameters)") + # ============================================================================= + # Step 7: Summarize Processing Results + # ============================================================================= + summarize_processing_results(lcms_collection) - # Verify raw data was cleaned up (unless keep_raw_data=True) - raw_data_present = any(1 in lcms_obj._ms_unprocessed and not lcms_obj._ms_unprocessed[1].empty - for lcms_obj in lcms_collection) - if not raw_data_present: - print("✓ Raw MS1 data successfully cleaned up after pipeline") - else: - print("⚠ Raw MS1 data still present (expected if keep_raw_data=True)") - - # Check for spectral match results (if spectral search was performed) - if enable_spectral_search: - total_spectral_matches = 0 - total_mf_with_matches = 0 - for lcms_obj in lcms_collection: - if hasattr(lcms_obj, 'spectral_search_results') and lcms_obj.spectral_search_results: - total_spectral_matches += len(lcms_obj.spectral_search_results) - total_mf_with_matches += 1 - break # Count each mass feature only once - - print(f"\nSpectral Search Results:") - print(f"Total mass features with spectral matches: {total_mf_with_matches}") - print(f"Total spectral matches: {total_spectral_matches}") - if total_mf_with_matches > 0: - print(f"✓ MS2 spectral search successfully found matches") - print(f" Average {total_spectral_matches/total_mf_with_matches:.1f} matches per feature") - # Access the molecular metadata if needed for export - if hasattr(lcms_collection, 'spectral_search_molecular_metadata'): - print(f" Molecular metadata available with {len(lcms_collection.spectral_search_molecular_metadata)} entries") - else: - print("⚠ No spectral matches found (check library and search parameters)") - """ - # OLD APPROACH (commented out - replaced by pipeline above): - # Gap fill missing cluster features BEFORE saving - start_time = time.time() - lcms_collection.fill_missing_cluster_features() - print("Time to gap fill missing cluster features: ", time.time() - start_time, "seconds, using", ncores, " cores") - - # Reload representative mass features with MS2 data associated - sample_mf_map = lcms_collection.reload_representative_mass_features( - add_ms2=True, - auto_process_ms2=True, - ms2_spectrum_mode=None, - ms2_scan_filter=None - ) - """ - - """ - # ADVANCED PIPELINE APPROACH (for custom workflows): - # Build a custom pipeline with full control over operations - from corems.mass_spectra.calc.lc_calc_operations import ( - GapFillOperation, - ReloadFeaturesOperation, - CustomOperation - ) - - # Define custom operation - def my_custom_processing(sample_id, collection, **params): - sample = collection[sample_id] - # Do custom processing here - # e.g., normalization, quality checks, etc. - return None - - # Build pipeline - ops = [ - GapFillOperation('gap_fill', expand_on_miss=True), - ReloadFeaturesOperation('reload', add_ms2=True, auto_process_ms2=True), - CustomOperation('custom', func=my_custom_processing) - ] - - # Execute - results = lcms_collection.process_samples_pipeline(ops, description="Custom workflow") - """ + # ============================================================================= + # Step 8: Save and Export Results + # ============================================================================= + print("\n=== Exporting LCMS Collection ===") + #exporter = LCMSCollectionExport( + # out_file_path="/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/collection", + # mass_spectra_collection=lcms_collection) + #exporter.export_to_hdf5(overwrite=True, save_parameters=True, parameter_format="toml") """ # Check save and load functionality for LCMSCollection print("Saving and re-loading LCMS collection to test save/load functionality") print(f"Before saving: missing_mass_features_searched = {lcms_collection.missing_mass_features_searched}") - exporter = LCMSCollectionExport( - out_file_path="/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/collection", - mass_spectra_collection=lcms_collection) - exporter.export_to_hdf5(overwrite=True, save_parameters=True, parameter_format="toml") + # Reload the collection reader = ReadSavedLCMSCollection( @@ -415,5 +450,4 @@ def my_custom_processing(sample_id, collection, **params): #TODO KRH: Add visualization of a consensus mass feature #TODO KRH: Add visualization of matched spectrum with consensus mass feature - #TODO KRH: Add code to deal with annotations of features in the collection context """ \ No newline at end of file From 47e63fd0bc5c352329acb5ff7b191b052df0770a Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 20 Jan 2026 12:32:47 -0800 Subject: [PATCH 103/158] Add EIC extraction to sample operations, clean up clustering bugs --- corems/mass_spectra/calc/lc_calc.py | 70 ++++++-- .../mass_spectra/calc/lc_calc_operations.py | 164 ++++++++++++++++-- corems/mass_spectra/factory/lc_class.py | 90 ++++++++-- corems/mass_spectra/input/corems_hdf5.py | 24 ++- .../nmdc/lipidomics/lipidomics_collection.py | 54 ++++-- 5 files changed, 338 insertions(+), 64 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 432b82290..2fc51f7c6 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -408,6 +408,10 @@ def find_mass_features(self, ms_level=1, grid=True): else: raise ValueError("Peak picking method not implemented") + # Cluster mass features after peak picking for persistent homology methods + if pp_method in ["persistent homology", "centroided_persistent_homology"]: + self.cluster_mass_features(drop_children=True, sort_by="persistence") + # Remove noisey mass features if designated in parameters if self.parameters.lc_ms.remove_redundant_mass_features: self._remove_redundant_mass_features() @@ -3295,10 +3299,14 @@ def get_most_representative_sample_for_cluster(self, cluster_id, representative_ # Get sample name from sample_id sample_name = self.samples[representative_mf['sample_id']] + # Get sample-level mf_id directly from the dataframe column + sample_level_mf_id = representative_mf['mf_id'] + return { 'sample_id': representative_mf['sample_id'], 'sample_name': sample_name, - 'mf_id': max_idx, + 'mf_id': sample_level_mf_id, # Sample-level mf_id from column + 'coll_mf_id': max_idx, # Collection-level id (index) representative_metric: representative_mf[representative_metric] } @@ -3541,18 +3549,12 @@ def _reload_sample_mass_features(self, sample_id, mf_ids_to_load=None, add_ms2=F # Import here to avoid circular imports from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectra - # If specific mf_ids requested, extract the local mf_ids we need + # If specific mf_ids requested, use them directly local_mf_ids_to_load = None if mf_ids_to_load is not None: - local_mf_ids_to_load = set() - for coll_mf_id in mf_ids_to_load: - # Parse collection-level ID to get local ID - parts = str(coll_mf_id).split('_', 1) - if len(parts) == 2: - try: - local_mf_ids_to_load.add(int(parts[1])) - except ValueError: - local_mf_ids_to_load.add(parts[1]) + # mf_ids_to_load is already a list of sample-level mf_ids (integers) + # No parsing needed - they come from the mf_id column in the dataframe + local_mf_ids_to_load = set(mf_ids_to_load) # Reload mass features from HDF5 with ReadCoreMSHDFMassSpectra(hdf5_file) as parser: @@ -4445,7 +4447,8 @@ def _prepare_pipeline_runtime_params(self, operations): Dictionary of runtime parameters for operations """ from corems.mass_spectra.calc.lc_calc_operations import ( - GapFillOperation, ReloadFeaturesOperation, MS2SpectralSearchOperation + GapFillOperation, ReloadFeaturesOperation, MS2SpectralSearchOperation, + LoadEICsOperation ) runtime_params = {} @@ -4500,6 +4503,8 @@ def _prepare_pipeline_runtime_params(self, operations): if needs_reload: # Build sample_mf_map for reloading representatives clusters = self.mass_features_dataframe['cluster'].unique() + # Filter out NaN clusters + clusters = [c for c in clusters if pd.notna(c)] sample_mf_map = {} for cluster_id in clusters: rep_info = self.get_most_representative_sample_for_cluster(cluster_id) @@ -4521,6 +4526,23 @@ def _prepare_pipeline_runtime_params(self, operations): if hasattr(self, '_spectral_search_molecular_metadata'): runtime_params['molecular_metadata'] = self._spectral_search_molecular_metadata + # Check if any operation needs EIC loading parameters + needs_eic_loading = any(isinstance(op, LoadEICsOperation) for op in operations) + if needs_eic_loading: + # Build cluster_mz_dict: map of sample_id -> list of m/z values in clusters + mfdf = self.mass_features_dataframe + cluster_mz_dict = {} + + # Get all mass features that belong to clusters (cluster is not NaN) + clustered_mf = mfdf[mfdf['cluster'].notna()] + + # Group by sample_id and collect all m/z values + for sample_id in clustered_mf['sample_id'].unique(): + sample_df = clustered_mf[clustered_mf['sample_id'] == sample_id] + cluster_mz_dict[sample_id] = sample_df['mz'].unique().tolist() + + runtime_params['cluster_mz_dict'] = cluster_mz_dict + return runtime_params def _execute_sample_pipeline(self, sample_id, operations, runtime_params, inplace=True): @@ -4578,7 +4600,7 @@ def _execute_sample_pipeline(self, sample_id, operations, runtime_params, inplac # Add gap-fill params if this is a gap-fill operation from corems.mass_spectra.calc.lc_calc_operations import ( - GapFillOperation, ReloadFeaturesOperation, MS2SpectralSearchOperation + GapFillOperation, ReloadFeaturesOperation, MS2SpectralSearchOperation, LoadEICsOperation ) if isinstance(op, GapFillOperation): @@ -4590,8 +4612,9 @@ def _execute_sample_pipeline(self, sample_id, operations, runtime_params, inplac elif isinstance(op, ReloadFeaturesOperation): if 'sample_mf_map' in runtime_params: sample_mf_map = runtime_params['sample_mf_map'] - if sample_id in sample_mf_map: - op_runtime_params['mf_ids_to_load'] = sample_mf_map[sample_id] + # Always pass mf_ids_to_load to ensure we only load what's needed + # If sample not in map, it has no representatives - pass empty list + op_runtime_params['mf_ids_to_load'] = sample_mf_map.get(sample_id, []) elif isinstance(op, MS2SpectralSearchOperation): # Add MS2 spectral search parameters @@ -4600,6 +4623,11 @@ def _execute_sample_pipeline(self, sample_id, operations, runtime_params, inplac if 'molecular_metadata' in runtime_params: op_runtime_params['molecular_metadata'] = runtime_params['molecular_metadata'] + elif isinstance(op, LoadEICsOperation): + # Add EIC loading parameters + if 'cluster_mz_dict' in runtime_params: + op_runtime_params['cluster_mz_dict'] = runtime_params['cluster_mz_dict'] + # Execute the operation result = op.execute(sample_id, self, **op_runtime_params) results[op.name] = result @@ -4622,6 +4650,7 @@ def process_consensus_features(self, load_representatives=True, perform_gap_fill ms2_scan_filter=None, molecular_formula_search=False, ms2_spectral_search=False, spectral_lib=None, molecular_metadata=None, + gather_eics=False, keep_raw_data=False): """ Process consensus mass features across the collection in a single parallelized pass. @@ -4664,6 +4693,12 @@ def process_consensus_features(self, load_representatives=True, perform_gap_fill Returned from MSPInterface.get_metabolomics_spectra_library(). Stored as self.spectral_search_molecular_metadata for later export. Default is None. + gather_eics : bool, optional + If True, loads extracted ion chromatograms (EICs) from HDF5 for all + mass features with assigned cluster_index (including gap-filled features). + Enables access to EICs via get_eics_for_cluster(cluster_id) method. + Requires that EICs were previously exported with export_eics=True. + Default is False. keep_raw_data : bool, optional If True, keeps raw MS data loaded in memory after pipeline completes. If False, cleans up raw data to free memory. Default is False. @@ -4733,7 +4768,7 @@ def process_consensus_features(self, load_representatives=True, perform_gap_fill """ from corems.mass_spectra.calc.lc_calc_operations import ( GapFillOperation, ReloadFeaturesOperation, MolecularFormulaSearchOperation, - MS2SpectralSearchOperation + MS2SpectralSearchOperation, LoadEICsOperation ) # Validate that at least one operation is enabled @@ -4788,6 +4823,9 @@ def process_consensus_features(self, load_representatives=True, perform_gap_fill self._spectral_lib = spectral_lib self._spectral_search_molecular_metadata = molecular_metadata + if gather_eics: + operations.append(LoadEICsOperation('load_eics')) + # Execute pipeline (description auto-generated from operations) results = self.process_samples_pipeline( operations, diff --git a/corems/mass_spectra/calc/lc_calc_operations.py b/corems/mass_spectra/calc/lc_calc_operations.py index cab1cdf5a..845a9abe3 100644 --- a/corems/mass_spectra/calc/lc_calc_operations.py +++ b/corems/mass_spectra/calc/lc_calc_operations.py @@ -447,15 +447,12 @@ def execute(self, sample_id, collection, mf_ids_to_load=None, **runtime_params): # If specific mf_ids requested, extract the local mf_ids we need local_mf_ids_to_load = None if mf_ids_to_load is not None: - local_mf_ids_to_load = set() - for coll_mf_id in mf_ids_to_load: - # Parse collection-level ID to get local ID - parts = str(coll_mf_id).split('_', 1) - if len(parts) == 2: - try: - local_mf_ids_to_load.add(int(parts[1])) - except ValueError: - local_mf_ids_to_load.add(parts[1]) + # mf_ids_to_load is already a list of sample-level mf_ids (integers) + # No parsing needed - they come from the mf_id column in the dataframe + if len(mf_ids_to_load) == 0: + # No features to load for this sample - return empty dict + return {} + local_mf_ids_to_load = set(mf_ids_to_load) # Reload mass features from HDF5 with ReadCoreMSHDFMassSpectra(hdf5_file) as parser: @@ -494,8 +491,29 @@ def execute(self, sample_id, collection, mf_ids_to_load=None, **runtime_params): return sample.mass_features def collect_results(self, sample_id, result, collection): - """Collect reloaded mass features back into sample.""" + """ + 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. + + Parameters + ---------- + sample_id : int + Sample ID that was processed + result : dict + Dictionary of reloaded mass features + collection : LCMSBaseCollection + The collection + """ + # Update sample.mass_features with loaded features collection[sample_id].mass_features = result + # Lock the collection dataframe to prevent rebuilding from individual samples + # (since we've only loaded a subset, rebuilding would lose data) + collection._mass_features_locked = True class MolecularFormulaSearchOperation(SampleOperation): @@ -912,3 +930,129 @@ def collect_results(self, sample_id, result, collection): ].ms2_similarity_results.append( sample.spectral_search_results[ms2_scan_id][precursor_mz] ) + + +class LoadEICsOperation(SampleOperation): + """ + 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. + """ + + @property + def description(self): + """Human-readable description for progress messages.""" + return "loading EICs" + + def needs_raw_ms_data(self): + """This operation doesn't need raw data - induced features already have EICs.""" + return False, None + + def can_execute(self, sample, collection): + """ + 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) + """ + return True + + def execute(self, sample_id, collection, cluster_mz_dict=None, **runtime_params): + """ + 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 + """ + from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectra + + sample = collection[sample_id] + sample_name = collection.samples[sample_id] + + # If no cluster info provided or no m/z values for this sample, return early + if cluster_mz_dict is None or sample_id not in cluster_mz_dict: + return {} + + # Get m/z values for this sample that belong to clusters + sample_cluster_mz = set(cluster_mz_dict[sample_id]) + + # Load EICs for each of the sample_cluster_mz + hdf5_path = sample.file_location + if hdf5_path and hdf5_path.exists(): + try: + reader = ReadCoreMSHDFMassSpectra(str(hdf5_path)) + reader.import_eics(sample, mz_list=list(sample_cluster_mz)) + # Return the loaded EICs for multiprocessing collection + # (modifications in worker process don't persist to main process) + return sample.eics.copy() + except (KeyError, AttributeError): + # No EIC data in HDF5 file for these m/z values + return {} + + return {} + + def collect_results(self, sample_id, result, collection): + """ + 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 + """ + if result: + # Update sample.eics with loaded EICs + collection[sample_id].eics.update(result) + + # Re-associate EICs with mass features (same logic as import_eics) + sample = collection[sample_id] + for idx in sample.mass_features.keys(): + mz = sample.mass_features[idx].mz + if mz in sample.eics.keys(): + sample.mass_features[idx]._eic_data = sample.eics[mz] diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 24322eef6..dac9ef65e 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -784,11 +784,6 @@ def get_scans_from_apex(ms1_scans, apex_scan, scans_to_average): ] mf_dict[k].update_mz() - if not induced_features: - # Re-process clustering if persistent homology is selected to remove duplicate mass features after adding and processing MS1 spectra - if self.parameters.lc_ms.peak_picking_method == "persistent homology": - self.cluster_mass_features(drop_children=True, sort_by="persistence") - def mass_features_to_df(self, induced_features = False): """Returns a pandas dataframe summarizing the mass features. @@ -1607,6 +1602,7 @@ def __init__( self.consensus_mass_features = {} self._parameters = LCMSCollectionParameters() self.isotopes_dropped = False + self._mass_features_locked = False # Prevents rebuilding mass_features_dataframe from samples # These attributes are set during processing self.rt_aligned = False @@ -1660,7 +1656,18 @@ def _combine_mass_features(self, induced_features = False): Returns -------- None, sets the _combined_mass_features or _combined_induced_mass_feature attribute. + + Notes + ----- + If _mass_features_locked is True (e.g., when only representative features are loaded), + this method will skip rebuilding the regular mass features dataframe to preserve + the full collection-level dataframe. Induced features are always rebuilt since they + are created during processing. """ + + # Skip rebuilding regular mass features if locked (preserves full dataframe) + if not induced_features and self._mass_features_locked: + return ## TODO: See why this function runs slower on multiprocessing, ## especially for induced features @@ -1691,9 +1698,9 @@ def _combine_mass_features(self, induced_features = False): mf_df_list.append(mf_df) combined_mass_features = pd.concat(mf_df_list) - # Move coll_mf_id, sample_name, and sample_id to front + # Move coll_mf_id, sample_name, sample_id, and mf_id to front cols = combined_mass_features.columns.tolist() - top_cols = ["coll_mf_id", "sample_name", "sample_id", "mz", "scan_time_aligned"] + top_cols = ["coll_mf_id", "sample_name", "sample_id", "mf_id", "mz", "scan_time_aligned"] cols = [x for x in top_cols + [col for col in cols if col not in top_cols] if x in cols] combined_mass_features = combined_mass_features[cols] # Make coll_mf_id the index @@ -2135,16 +2142,9 @@ def mass_features_dataframe(self, df): def induced_mass_features_dataframe(self): self._check_mass_features_df(induced_features = True) if self._combined_induced_mass_features is not None and len(self._combined_induced_mass_features) > 0: - # Extract cluster ID from coll_mf_id if not already present - if 'cluster' not in self._combined_induced_mass_features.columns: - imf_df = self._combined_induced_mass_features.copy() - imf_df = imf_df.reset_index(drop=False) - # Extract cluster ID from coll_mf_id format: "sample_id_cCluster_idx_i" - imf_df['cluster'] = imf_df['coll_mf_id'].apply( - lambda x: int(x.split('_')[1][1:]) - ) - imf_df = imf_df.set_index('coll_mf_id') - self._combined_induced_mass_features = imf_df + # The cluster column should already be set during gap-filling + # No parsing needed - cluster_index is stored directly on induced mass features + pass return self._combined_induced_mass_features @induced_mass_features_dataframe.setter @@ -2203,4 +2203,58 @@ def cluster_feature_dictionary(self): """Generates a dictionary with clusters for keys and mass feature IDs as entries""" df = self.mass_features_dataframe cluster_dict = df.groupby('cluster').apply(lambda x: x.index.tolist()).to_dict() - return cluster_dict \ No newline at end of file + return cluster_dict + + def get_eics_for_cluster(self, cluster_id): + """ + Retrieve all EICs for mass features in a specific cluster across all samples. + + Returns a dictionary mapping sample names to EIC_Data objects for the given cluster. + Useful for visualizing and comparing chromatographic peaks across samples. + + Parameters + ---------- + cluster_id : int + The cluster ID to retrieve EICs for + + Returns + ------- + dict + Dictionary with structure: {sample_name: EIC_Data object} + Only includes samples where the EIC was loaded. + + Examples + -------- + >>> # Load EICs first + >>> collection.process_consensus_features(gather_eics=True, ...) + >>> + >>> # Get all EICs for cluster 5 + >>> eics = collection.get_eics_for_cluster(5) + >>> for sample_name, eic_data in eics.items(): + ... print(f"{sample_name}: {len(eic_data.scans)} scans") + + Notes + ----- + Requires that EICs have been loaded using gather_eics=True in + process_consensus_features() or manually loaded via LoadEICsOperation. + """ + eics_by_sample = {} + + # Iterate through all samples + for sample_id, sample in enumerate(self): + sample_name = self.samples[sample_id] + + # Check if sample has EICs loaded + if not hasattr(sample, 'eics') or not sample.eics: + continue + + # Find mass features in this cluster for this sample + # Check both regular and induced mass features + for mf in list(sample.mass_features.values()) + list(sample.induced_mass_features.values()): + if hasattr(mf, 'cluster_index') and mf.cluster_index == cluster_id: + # Get the EIC for this mass feature's m/z + if mf.mz in sample.eics: + eics_by_sample[sample_name] = sample.eics[mf.mz] + break # Found the EIC for this sample, move to next sample + + return eics_by_sample \ No newline at end of file diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index fb5e635d2..40d1b786f 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -567,13 +567,15 @@ def import_mass_features(self, mass_spectra, mf_ids=None) -> None: mass_spectra._ms[ms2_scan] ) - def import_eics(self, mass_spectra): + def import_eics(self, mass_spectra, mz_list=None): """Imports the extracted ion chromatograms from the HDF5 file. Parameters ---------- mass_spectra : LCMSBase | MassSpectraBase The MassSpectraBase or LCMSBase object to populate with extracted ion chromatograms. + mz_list : list of float, optional + List of m/z values to load EICs for. If None, loads all EICs. Default is None. Returns ------- @@ -583,7 +585,16 @@ def import_eics(self, mass_spectra): """ dict_group_load = self.h5pydata["eics"] dict_group_keys = dict_group_load.keys() + + # Create a set of target m/z values for fast lookup if filtering + target_mz_set = set(mz_list) if mz_list is not None else None + for k in dict_group_keys: + # Check if we should load this EIC (filter by m/z if list provided) + eic_mz = dict_group_load[k].attrs["mz"] + if target_mz_set is not None and eic_mz not in target_mz_set: + continue # Skip this EIC + my_eic = EIC_Data( scans=dict_group_load[k]["scans"][:], time=dict_group_load[k]["time"][:], @@ -596,7 +607,7 @@ def import_eics(self, mass_spectra): if key == "apexes" and len(my_eic.apexes) > 0: my_eic.apexes = [tuple(x) for x in my_eic.apexes] # Add to mass_spectra object - mass_spectra.eics[dict_group_load[k].attrs["mz"]] = my_eic + mass_spectra.eics[eic_mz] = my_eic # Add to mass features for idx in mass_spectra.mass_features.keys(): @@ -1327,12 +1338,9 @@ def _load_induced_mass_features(self, lcms_collection): for mf_id_str in sample_group.keys(): mf_group = sample_group[mf_id_str] - # The mf_id in HDF5 is stored as the collection ID (e.g., 'c10006_422_i' or '0_c10006_422_i') - # Extract the integer ID - it's the second-to-last part when split by '_' - # Format: sample_id_cCluster_mf_id_i - parts = mf_id_str.split('_') - # Find the part that's a number (should be second-to-last before 'i') - mf_id = int(parts[-2]) if len(parts) > 1 else int(mf_id_str) + # The mf_id in HDF5 is stored as a string of the integer ID + # (induced_mass_features dict uses integer keys) + mf_id = int(mf_id_str) # Instantiate the LCMSMassFeature object with required attributes mass_feature = LCMSMassFeature( diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 5c9329699..23a6b0803 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -42,9 +42,24 @@ def summarize_processing_results(lcms_collection): # Basic collection info n_samples = len(lcms_collection) - total_mf = sum(len(lcms_obj.mass_features) for lcms_obj in lcms_collection) + + # Collection-level feature count (from dataframe - all features) + collection_mf_count = len(lcms_collection.mass_features_dataframe) if lcms_collection.mass_features_dataframe is not None else 0 + + # Sample-level loaded feature count (from mass_features dict - loaded for processing) + loaded_mf_count = sum(len(lcms_obj.mass_features) for lcms_obj in lcms_collection) + + # Cluster count + if 'cluster' in lcms_collection.mass_features_dataframe.columns: + n_clusters = lcms_collection.mass_features_dataframe['cluster'].nunique() + else: + n_clusters = 0 + print(f"\nSamples: {n_samples}") - print(f"Total mass features: {total_mf}") + print(f"Total features in collection: {collection_mf_count}") + print(f"Representative features loaded: {loaded_mf_count}") + if n_clusters > 0: + print(f"Consensus clusters: {n_clusters}") # Gap filling - check for induced mass features induced_counts = [len(lcms_obj.induced_mass_features) for lcms_obj in lcms_collection] @@ -70,9 +85,9 @@ def summarize_processing_results(lcms_collection): if mf_with_ms1 > 0 or mf_with_ms2 > 0: print(f"\nMS Data Association: ✓ Complete") if mf_with_ms1 > 0: - print(f" MS1: {mf_with_ms1}/{total_mf} features ({mf_with_ms1/total_mf*100:.1f}%)") + print(f" MS1: {mf_with_ms1}/{loaded_mf_count} loaded features ({mf_with_ms1/loaded_mf_count*100:.1f}%)") if mf_with_ms2 > 0: - print(f" MS2: {mf_with_ms2}/{total_mf} features ({total_ms2_spectra} spectra)") + print(f" MS2: {mf_with_ms2}/{loaded_mf_count} loaded features ({total_ms2_spectra} spectra)") # Molecular formula search mf_with_formulas = 0 @@ -91,7 +106,7 @@ def summarize_processing_results(lcms_collection): if mf_with_formulas > 0: print(f"\nMolecular Formula Search: ✓ Complete") - print(f" {mf_with_formulas}/{total_mf} features assigned ({total_formulas} total formulas)") + print(f" {mf_with_formulas}/{loaded_mf_count} loaded features assigned ({total_formulas} total formulas)") print(f" Average {total_formulas/mf_with_formulas:.1f} formulas per feature") # MS2 spectral search @@ -111,10 +126,24 @@ def summarize_processing_results(lcms_collection): if mf_with_spectral_matches > 0: print(f"\nMS2 Spectral Search: ✓ Complete") print(f" {scans_searched} MS2 scans searched") - print(f" {mf_with_spectral_matches}/{total_mf} features matched ({total_spectral_matches} total matches)") + print(f" {mf_with_spectral_matches}/{loaded_mf_count} loaded features matched ({total_spectral_matches} total matches)") if hasattr(lcms_collection, 'spectral_search_molecular_metadata'): print(f" Library size: {len(lcms_collection.spectral_search_molecular_metadata)} entries") + # Check for loaded EICs + total_eics_loaded = 0 + samples_with_eics = 0 + + for lcms_obj in lcms_collection: + if hasattr(lcms_obj, 'eics') and lcms_obj.eics: + samples_with_eics += 1 + total_eics_loaded += len(lcms_obj.eics) + + if total_eics_loaded > 0: + print(f"\nEIC Loading: ✓ Complete") + print(f" {total_eics_loaded} EICs loaded across {samples_with_eics}/{n_samples} samples") + print(f" Average {total_eics_loaded/samples_with_eics:.1f} EICs per sample") + # Memory management check raw_data_present = any(1 in lcms_obj._ms_unprocessed and not lcms_obj._ms_unprocessed[1].empty for lcms_obj in lcms_collection) @@ -264,7 +293,7 @@ def process_single_sample(args): # Get configured parameters lcms_obj.parameters = get_configured_lcms_parameters() - # Use persistent homology to find mass features in the lc-ms data + # Use persistent homology to find mass features in the lc-ms data and integrate lcms_obj.find_mass_features() lcms_obj.integrate_mass_features(drop_if_fail=True) @@ -283,8 +312,8 @@ def process_single_sample(args): # ============================================================================= # Configuration # ============================================================================= - ncores = 3 - reprocess_samples = False # Set to True to reprocess raw data + ncores = 1 + reprocess_samples = True # Set to True to reprocess raw data # Paths base_path = Path("/Volumes/LaCie/nmdc_data/collection_testing/dev_test/") @@ -371,11 +400,12 @@ def process_single_sample(args): load_representatives=True, perform_gap_filling=True, add_ms1=True, - add_ms2=True, - molecular_formula_search=True, - ms2_spectral_search=True, + add_ms2=False, + molecular_formula_search=False, + ms2_spectral_search=False, spectral_lib=spectral_lib, molecular_metadata=molecular_metadata, + gather_eics=True, keep_raw_data=False ) print(f"Pipeline complete: {time.time() - start_time:.1f} seconds using {ncores} cores") From ff2cd05ae21f89b08531bfe514855c9fa8cfb960 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 20 Jan 2026 13:53:54 -0800 Subject: [PATCH 104/158] Add ms2 scan association to find features optionally. --- corems/mass_spectra/calc/lc_calc.py | 31 +++++++++++++++---- .../nmdc/lipidomics/lipidomics_collection.py | 20 ++++++++---- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 2fc51f7c6..7701f31f0 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -357,7 +357,7 @@ def get_average_mass_spectrum( ms.process_mass_spec() return ms - def find_mass_features(self, ms_level=1, grid=True): + def find_mass_features(self, ms_level=1, grid=True, assign_ms2_scans=False, ms2_scan_filter=None): """Find mass features within an LCMSBase object Note that this is a wrapper function that calls the find_mass_features_ph function, but can be extended to support other peak picking methods in the future. @@ -369,6 +369,13 @@ def find_mass_features(self, ms_level=1, grid=True): grid : bool, optional If True, will regrid the data before running the persistent homology calculations (after checking if the data is gridded), used for persistent homology peak picking for profile data only. Default is True. + assign_ms2_scans : bool, optional + If True, assign MS2 scan numbers to mass features after peak picking. + This populates the ms2_scan_numbers attribute on each mass feature, which enables + choosing representative features based on MS2 availability. Default is False. + ms2_scan_filter : str or None, optional + Filter string for MS2 scans when assign_ms2_scans is True (e.g., 'hcd'). + If None, all MS2 scans are considered. Default is None. Raises ------ @@ -408,9 +415,20 @@ def find_mass_features(self, ms_level=1, grid=True): else: raise ValueError("Peak picking method not implemented") - # Cluster mass features after peak picking for persistent homology methods - if pp_method in ["persistent homology", "centroided_persistent_homology"]: - self.cluster_mass_features(drop_children=True, sort_by="persistence") + # Cluster mass features to remove redundant features + self.cluster_mass_features(drop_children=True) + + # Optionally assign MS2 scan numbers to mass features during peak picking + # This helps with choosing representative features that have MS2 data + if assign_ms2_scans: + try: + self._find_ms2_scans_for_mass_features( + mf_ids=None, # Process all mass features + scan_filter=ms2_scan_filter + ) + except ValueError: + # No MS2 scans found - this is okay, just skip + pass # Remove noisey mass features if designated in parameters if self.parameters.lc_ms.remove_redundant_mass_features: @@ -3451,7 +3469,8 @@ def _associate_ms2_with_mass_features(self, sample, local_mf_ids, auto_process=T mf = sample.mass_features[mf_id] # If this mass feature already has MS2 scans, add them to our set if mf.ms2_scan_numbers is not None and len(mf.ms2_scan_numbers) > 0: - unique_dda_scans.update(mf.ms2_scan_numbers) + # Convert to integers in case they come from HDF5 as numpy types + unique_dda_scans.update([int(scan) for scan in mf.ms2_scan_numbers]) else: # Otherwise, we need to find scans for this mass feature mfs_needing_scan_finding.append(mf_id) @@ -3484,7 +3503,7 @@ def _associate_ms2_with_mass_features(self, sample, local_mf_ids, auto_process=T for mf_id in local_mf_ids: if mf_id not in sample.mass_features: continue - if sample.mass_features[mf_id].ms2_scan_numbers: + if sample.mass_features[mf_id].ms2_scan_numbers is not None and len(sample.mass_features[mf_id].ms2_scan_numbers) > 0: for dda_scan in sample.mass_features[mf_id].ms2_scan_numbers: if dda_scan in sample._ms: sample.mass_features[mf_id].ms2_mass_spectra[dda_scan] = sample._ms[dda_scan] diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 23a6b0803..0d519f48b 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -72,7 +72,9 @@ def summarize_processing_results(lcms_collection): # Feature loading - check if mass features have MS1/MS2 spectra mf_with_ms1 = 0 mf_with_ms2 = 0 + mf_with_ms2_scans = 0 total_ms2_spectra = 0 + total_ms2_scan_numbers = 0 for lcms_obj in lcms_collection: for mf in lcms_obj.mass_features.values(): @@ -81,6 +83,9 @@ def summarize_processing_results(lcms_collection): if hasattr(mf, 'ms2_mass_spectra') and mf.ms2_mass_spectra: mf_with_ms2 += 1 total_ms2_spectra += len(mf.ms2_mass_spectra) + if hasattr(mf, 'ms2_scan_numbers') and mf.ms2_scan_numbers is not None and len(mf.ms2_scan_numbers) > 0: + mf_with_ms2_scans += 1 + total_ms2_scan_numbers += len(mf.ms2_scan_numbers) if mf_with_ms1 > 0 or mf_with_ms2 > 0: print(f"\nMS Data Association: ✓ Complete") @@ -88,6 +93,8 @@ def summarize_processing_results(lcms_collection): print(f" MS1: {mf_with_ms1}/{loaded_mf_count} loaded features ({mf_with_ms1/loaded_mf_count*100:.1f}%)") if mf_with_ms2 > 0: print(f" MS2: {mf_with_ms2}/{loaded_mf_count} loaded features ({total_ms2_spectra} spectra)") + if mf_with_ms2_scans > 0: + print(f" MS2 scan numbers: {mf_with_ms2_scans}/{loaded_mf_count} loaded features ({total_ms2_scan_numbers} scans)") # Molecular formula search mf_with_formulas = 0 @@ -294,7 +301,8 @@ def process_single_sample(args): lcms_obj.parameters = get_configured_lcms_parameters() # Use persistent homology to find mass features in the lc-ms data and integrate - lcms_obj.find_mass_features() + # Assign MS2 scan numbers during peak picking for choosing representatives with MS2 + lcms_obj.find_mass_features(assign_ms2_scans=True, ms2_scan_filter=None) lcms_obj.integrate_mass_features(drop_if_fail=True) # Add peak metrics and filter mass features based on the new parameters @@ -312,8 +320,8 @@ def process_single_sample(args): # ============================================================================= # Configuration # ============================================================================= - ncores = 1 - reprocess_samples = True # Set to True to reprocess raw data + ncores = 3 + reprocess_samples = False # Set to True to reprocess raw data # Paths base_path = Path("/Volumes/LaCie/nmdc_data/collection_testing/dev_test/") @@ -400,9 +408,9 @@ def process_single_sample(args): load_representatives=True, perform_gap_filling=True, add_ms1=True, - add_ms2=False, - molecular_formula_search=False, - ms2_spectral_search=False, + add_ms2=True, + molecular_formula_search=True, + ms2_spectral_search=True, spectral_lib=spectral_lib, molecular_metadata=molecular_metadata, gather_eics=True, From 1245b534382de6c7906e77efba34426907c02288 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 20 Jan 2026 14:39:13 -0800 Subject: [PATCH 105/158] Prioritize features with MS2 --- .../factory/processingSetting.py | 20 +- corems/mass_spectra/calc/lc_calc.py | 176 +++++++++++++----- corems/mass_spectra/factory/lc_class.py | 9 +- .../nmdc/lipidomics/lipidomics_collection.py | 4 +- 4 files changed, 157 insertions(+), 52 deletions(-) diff --git a/corems/encapsulation/factory/processingSetting.py b/corems/encapsulation/factory/processingSetting.py index 9e6e67867..49117c412 100644 --- a/corems/encapsulation/factory/processingSetting.py +++ b/corems/encapsulation/factory/processingSetting.py @@ -1161,6 +1161,17 @@ class LCMSCollectionSettings: If True, expands search window using consensus_mz_tol_ppm and consensus_rt_tol when no peak is found in the initial cluster boundaries during gap-filling. Default is False. + consensus_representative_metric : str, optional + Metric used to determine the most representative sample for a consensus mass feature. + Options: + - 'intensity': Selects the mass feature with the highest intensity value + - 'intensity_prefer_ms2': Selects the mass feature with the highest intensity among + those that have MS2 scan numbers assigned. If no features have MS2 scans, falls + back to selecting the highest intensity feature overall. + Default is 'intensity_prefer_ms2'. + consensus_representative_metrics_available : tuple, optional + Tuple of available metrics for determining the most representative sample. + Default is ('intensity', 'intensity_prefer_ms2'). """ # Settings for general processing cores = 1 @@ -1189,13 +1200,8 @@ class LCMSCollectionSettings: gap_fill_expand_on_miss: bool = True # Consensus mass feature visualization parameters - consensus_representative_metric: str = 'intensity' - consensus_representative_metrics_available: tuple = ('intensity', 'persistence', 'area') - """ - Metric used to determine the most representative sample for a consensus mass feature. - Options: 'intensity', 'persistence', 'area' - Default is 'intensity'. - """ + consensus_representative_metric: str = 'intensity_prefer_ms2' + consensus_representative_metrics_available: tuple = ('intensity', 'intensity_prefer_ms2') def __post_init__(self): self.consensus_mz_tol_ppm = self.alignment_mz_tol_ppm diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 7701f31f0..05b655ea3 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -3258,6 +3258,104 @@ def plot_consensus_mz_features(self, xb = 'xb', xt = 'xt', yb = 'yb', yt = 'yt', else: plt.show() + def get_representative_mass_features_for_all_clusters(self, representative_metric=None): + """ + Get the most representative mass feature for all clusters in bulk. + + This is much more efficient than calling get_most_representative_sample_for_cluster + in a loop, as it processes all clusters in a single pass over the dataframe. + + Parameters + ---------- + representative_metric : str, optional + The metric to use to determine the most representative sample. + If None, uses the value from self.parameters.lcms_collection.consensus_representative_metric. + Options: + - 'intensity': Selects the mass feature with the highest intensity + - 'intensity_prefer_ms2': Selects the highest intensity feature that has MS2 scans, + or the highest intensity overall if none have MS2 + Default is None (uses parameter setting). + + Returns + ------- + :obj:`~pandas.DataFrame` + DataFrame with one row per cluster containing: + - cluster: cluster ID + - sample_id: The sample ID of the most representative sample + - mf_id: The mass feature ID in the sample + - coll_mf_id: The collection-level mass feature ID (index) + - has_ms2: Whether this mass feature has MS2 scan numbers + - intensity: The intensity value of the representative mass feature + """ + # Use default from parameters if not specified + if representative_metric is None: + representative_metric = self.parameters.lcms_collection.consensus_representative_metric + + mf_df = self.mass_features_dataframe.copy() + + # Handle special metric 'intensity_prefer_ms2' + if representative_metric == 'intensity_prefer_ms2': + if 'intensity' not in mf_df.columns: + raise ValueError( + f"'intensity' column not found in mass_features_dataframe. " + f"Available columns: {mf_df.columns.tolist()}" + ) + + # Add has_ms2 flag if ms2_scan_numbers column exists + if 'ms2_scan_numbers' in mf_df.columns: + def has_ms2_scans(val): + if val is None: + return False + try: + return len(val) > 0 + except (TypeError, ValueError): + return False + + mf_df['has_ms2'] = mf_df['ms2_scan_numbers'].apply(has_ms2_scans) + + # Sort by has_ms2 (descending) then intensity (descending) + # This ensures features with MS2 are preferred when intensities are equal + mf_df = mf_df.sort_values(['has_ms2', 'intensity'], ascending=[False, False]) + else: + mf_df['has_ms2'] = False + mf_df = mf_df.sort_values('intensity', ascending=False) + + # Group by cluster and take the first (highest intensity, preferring MS2) + representatives = mf_df.groupby('cluster').first().reset_index() + + else: + # Standard metric - check if it exists + if representative_metric not in mf_df.columns: + raise ValueError( + f"Metric '{representative_metric}' not found. Available columns: {mf_df.columns.tolist()}" + ) + + # Add has_ms2 flag for consistency + if 'ms2_scan_numbers' in mf_df.columns: + def has_ms2_scans(val): + if val is None: + return False + try: + return len(val) > 0 + except (TypeError, ValueError): + return False + mf_df['has_ms2'] = mf_df['ms2_scan_numbers'].apply(has_ms2_scans) + else: + mf_df['has_ms2'] = False + + # Get the index of max value for each cluster + idx = mf_df.groupby('cluster')[representative_metric].idxmax() + representatives = mf_df.loc[idx].reset_index(drop=True) + + # Store the collection-level index as coll_mf_id + representatives['coll_mf_id'] = representatives.index + + # Select only the columns we need + result_cols = ['cluster', 'sample_id', 'mf_id', 'coll_mf_id', 'has_ms2', 'intensity'] + representatives = representatives[result_cols] + + return representatives + def get_most_representative_sample_for_cluster(self, cluster_id, representative_metric=None): """ Get the most representative sample for a given cluster based on a metric. @@ -3269,8 +3367,11 @@ def get_most_representative_sample_for_cluster(self, cluster_id, representative_ representative_metric : str, optional The metric to use to determine the most representative sample. If None, uses the value from self.parameters.lcms_collection.consensus_representative_metric. - Options: 'intensity', 'persistence', or any column in mass_features_dataframe. - Default is None. + Options: + - 'intensity': Selects the mass feature with the highest intensity + - 'intensity_prefer_ms2': Selects the highest intensity feature that has MS2 scans, + or the highest intensity overall if none have MS2 + Default is None (uses parameter setting). Returns ------- @@ -3278,24 +3379,26 @@ def get_most_representative_sample_for_cluster(self, cluster_id, representative_ Dictionary containing: - 'sample_id': The sample ID of the most representative sample - 'sample_name': The sample name of the most representative sample - - 'mf_id': The mass feature ID (coll_mf_id) in the collection - - representative_metric: The value of the metric for this mass feature + - 'mf_id': The mass feature ID in the sample + - 'coll_mf_id': The collection-level mass feature ID (index) + - 'has_ms2': Whether this mass feature has MS2 scan numbers + - 'intensity': The intensity value of the representative mass feature Raises ------ ValueError If cluster_id is not found or if representative_metric is not a valid column. """ - # Use default from parameters if not specified - if representative_metric is None: - representative_metric = self.parameters.lcms_collection.consensus_representative_metric + # Use the bulk method to get all representatives, then filter to this cluster + # This follows DRY principle and ensures consistency + all_representatives = self.get_representative_mass_features_for_all_clusters( + representative_metric=representative_metric + ) - # Get all mass features in this cluster - cluster_mfs = self.mass_features_dataframe[ - self.mass_features_dataframe['cluster'] == cluster_id - ].copy() + # Filter to the requested cluster + cluster_rep = all_representatives[all_representatives['cluster'] == cluster_id] - if len(cluster_mfs) == 0: + if len(cluster_rep) == 0: # Try to provide helpful error message available_clusters = self.mass_features_dataframe['cluster'].unique() raise ValueError( @@ -3304,28 +3407,19 @@ def get_most_representative_sample_for_cluster(self, cluster_id, representative_ f"(showing first 10 of {len(available_clusters)} total clusters)" ) - # Check if metric exists - if representative_metric not in cluster_mfs.columns: - raise ValueError( - f"Metric '{representative_metric}' not found. Available columns: {cluster_mfs.columns.tolist()}" - ) - - # Find the mass feature with the highest value for the metric - max_idx = cluster_mfs[representative_metric].idxmax() - representative_mf = cluster_mfs.loc[max_idx] + # Get the representative row (should only be one) + rep_row = cluster_rep.iloc[0] # Get sample name from sample_id - sample_name = self.samples[representative_mf['sample_id']] - - # Get sample-level mf_id directly from the dataframe column - sample_level_mf_id = representative_mf['mf_id'] + sample_name = self.samples[rep_row['sample_id']] return { - 'sample_id': representative_mf['sample_id'], + 'sample_id': rep_row['sample_id'], 'sample_name': sample_name, - 'mf_id': sample_level_mf_id, # Sample-level mf_id from column - 'coll_mf_id': max_idx, # Collection-level id (index) - representative_metric: representative_mf[representative_metric] + 'mf_id': rep_row['mf_id'], + 'coll_mf_id': rep_row['coll_mf_id'], + 'has_ms2': rep_row['has_ms2'], + 'intensity': rep_row['intensity'] } def reload_representative_mass_features(self, add_ms2=False, auto_process_ms2=True, ms2_spectrum_mode=None, ms2_scan_filter=None): @@ -3377,15 +3471,14 @@ def reload_representative_mass_features(self, add_ms2=False, auto_process_ms2=Tr "cluster_summary_dataframe not found. Must run add_consensus_mass_features() first." ) - # Get all unique clusters - clusters = self.mass_features_dataframe['cluster'].unique() + # Get all representative mass features in bulk (much faster than looping) + representatives = self.get_representative_mass_features_for_all_clusters() # Build a dictionary of sample_id -> list of mf_ids that are representatives sample_mf_map = {} - for cluster_id in clusters: - rep_info = self.get_most_representative_sample_for_cluster(cluster_id) - sample_id = rep_info['sample_id'] - mf_id = rep_info['mf_id'] + for _, row in representatives.iterrows(): + sample_id = row['sample_id'] + mf_id = row['mf_id'] if sample_id not in sample_mf_map: sample_mf_map[sample_id] = [] @@ -4521,14 +4614,13 @@ def _prepare_pipeline_runtime_params(self, operations): needs_reload = any(isinstance(op, ReloadFeaturesOperation) for op in operations) if needs_reload: # Build sample_mf_map for reloading representatives - clusters = self.mass_features_dataframe['cluster'].unique() - # Filter out NaN clusters - clusters = [c for c in clusters if pd.notna(c)] + # Get all representative mass features in bulk (much faster than looping) + representatives = self.get_representative_mass_features_for_all_clusters() + sample_mf_map = {} - for cluster_id in clusters: - rep_info = self.get_most_representative_sample_for_cluster(cluster_id) - sample_id = rep_info['sample_id'] - mf_id = rep_info['mf_id'] + for _, row in representatives.iterrows(): + sample_id = row['sample_id'] + mf_id = row['mf_id'] if sample_id not in sample_mf_map: sample_mf_map[sample_id] = [] diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index dac9ef65e..31bd680d1 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -868,6 +868,7 @@ def mass_spectrum_to_string( "monoisotopic_mf_id", "isotopologue_type", "mass_spectrum_deconvoluted_parent", + "ms2_scan_numbers", ] df_mf_list = [] @@ -879,7 +880,12 @@ def mass_spectrum_to_string( dict_mf = {} # Get the values for each key in df_keys from the mass feature object for key in df_keys: - dict_mf[key] = getattr(mf_dict[mf_id], key) + value = getattr(mf_dict[mf_id], key) + # Wrap list/array values in a list so pandas treats them as single cell values + if key == 'ms2_scan_numbers' and isinstance(value, (list, np.ndarray)): + dict_mf[key] = [value] + else: + dict_mf[key] = value if len(mf_dict[mf_id].ms2_scan_numbers) > 0: # Add MS2 spectra info best_ms2_spectrum = mf_dict[mf_id].best_ms2 @@ -933,6 +939,7 @@ def mass_spectrum_to_string( "isotopologue_type", "mass_spectrum_deconvoluted_parent", "associated_mass_features", + "ms2_scan_numbers", "ms2_spectrum", ] # drop columns that are not in col_order diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 0d519f48b..12d8a02df 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -132,7 +132,7 @@ def summarize_processing_results(lcms_collection): if mf_with_spectral_matches > 0: print(f"\nMS2 Spectral Search: ✓ Complete") - print(f" {scans_searched} MS2 scans searched") + print(f" {scans_searched} MS2 scans with library search results") print(f" {mf_with_spectral_matches}/{loaded_mf_count} loaded features matched ({total_spectral_matches} total matches)") if hasattr(lcms_collection, 'spectral_search_molecular_metadata'): print(f" Library size: {len(lcms_collection.spectral_search_molecular_metadata)} entries") @@ -320,7 +320,7 @@ def process_single_sample(args): # ============================================================================= # Configuration # ============================================================================= - ncores = 3 + ncores = 1 reprocess_samples = False # Set to True to reprocess raw data # Paths From c841b81f7efc79638f4e9c385b367cd532d4b48f Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 20 Jan 2026 16:44:58 -0800 Subject: [PATCH 106/158] Made plotting work for cluster id --- corems/mass_spectra/calc/lc_calc.py | 376 +++++++++++++++++++++++- corems/mass_spectra/factory/lc_class.py | 49 ++- 2 files changed, 419 insertions(+), 6 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 05b655ea3..c6f6f993a 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -3060,10 +3060,14 @@ def summarize_clusters(self): if self.induced_mass_features_dataframe is not None and len(self.induced_mass_features_dataframe) > 0: imf_df = self.induced_mass_features_dataframe.copy() imf_df = imf_df.reset_index(drop=False) - # Cluster column already extracted by induced_mass_features_dataframe property + # Cluster column extracted from mf_id in _prepare_lcms_mass_features_for_combination # Combine regular and induced features mf_df = pd.concat([mf_df, imf_df], axis=0) mf_df = mf_df.reset_index(drop=True) + + # Filter out any rows with NaN cluster values before converting to int + if 'cluster' in mf_df.columns: + mf_df = mf_df.dropna(subset=['cluster']) mf_df['cluster'] = mf_df['cluster'].astype(int) summary_df = ( @@ -3258,6 +3262,376 @@ def plot_consensus_mz_features(self, xb = 'xb', xt = 'xt', yb = 'yb', yt = 'yt', else: plt.show() + def plot_cluster( + self, + cluster_id, + to_plot=["EIC", "MS1", "MS2"], + return_fig=False, + plot_smoothed_eic=False, + plot_eic_datapoints=False, + eic_buffer_time=None, + label_samples=False, + ): + """ + Plot a consensus mass feature cluster across all samples. + + Similar to LCMSMassFeature.plot() but shows EICs from all samples in the cluster, + highlighting the representative mass feature. + + Parameters + ---------- + cluster_id : int + The cluster ID to plot + to_plot : list, optional + List of strings specifying what to plot: "EIC", "MS1", "MS2". + Default is ["EIC", "MS1", "MS2"]. + return_fig : bool, optional + If True, returns the figure object. Default is False. + plot_smoothed_eic : bool, optional + If True, plots smoothed EICs. Default is False. + plot_eic_datapoints : bool, optional + If True, plots EIC data points. Default is False. + eic_buffer_time : float, optional + Time buffer around the peak for EIC plotting (minutes). + If None, uses parameter setting. Default is None. + label_samples : bool, optional + If True, labels each sample in the legend. Default is False. + + Returns + ------- + matplotlib.figure.Figure or None + The figure object if return_fig=True, otherwise None + + Raises + ------ + ValueError + If cluster_id is not found or if required data is not loaded + """ + import matplotlib.pyplot as plt + + # Get cluster summary for median values + if cluster_id not in self.cluster_summary_dataframe.index: + raise ValueError( + f"Cluster {cluster_id} not found in cluster_summary_dataframe. " + f"Run add_consensus_mass_features() first." + ) + + cluster_summary = self.cluster_summary_dataframe.loc[cluster_id] + + # Get representative mass feature info + rep_info = self.get_most_representative_sample_for_cluster(cluster_id) + rep_sample_id = rep_info['sample_id'] + rep_mf_id = rep_info['mf_id'] + rep_sample = self[rep_sample_id] + + # Check if representative mass feature is loaded + if rep_mf_id not in rep_sample.mass_features: + raise ValueError( + f"Representative mass feature {rep_mf_id} not loaded in sample {rep_sample.sample_name}. " + f"Run reload_representative_mass_features() or process_consensus_features() first." + ) + + rep_mf = rep_sample.mass_features[rep_mf_id] + + # Get eic buffer time + if eic_buffer_time is None: + eic_buffer_time = self[0].parameters.lc_ms.eic_buffer_time + + # Adjust to_plot based on available data + if rep_mf.mass_spectrum is None: + to_plot = [x for x in to_plot if x != "MS1"] + if len(rep_mf.ms2_mass_spectra) == 0: + to_plot = [x for x in to_plot if x != "MS2"] + + # Check if EICs are available + cluster_mfs = self.mass_features_dataframe[ + self.mass_features_dataframe['cluster'] == cluster_id + ] + + has_eics = False + # Check regular features + for _, row in cluster_mfs.iterrows(): + sample_id = row['sample_id'] + sample = self[sample_id] + if hasattr(sample, 'eics') and sample.eics: + has_eics = True + break + + # Also check induced features if available + if not has_eics and self.induced_mass_features_dataframe is not None: + induced_cluster_mfs = self.induced_mass_features_dataframe[ + self.induced_mass_features_dataframe['cluster'] == cluster_id + ] + for _, row in induced_cluster_mfs.iterrows(): + sample_id = row['sample_id'] + sample = self[sample_id] + if hasattr(sample, 'eics') and sample.eics: + has_eics = True + break + + if not has_eics: + to_plot = [x for x in to_plot if x != "EIC"] + if len(to_plot) == 0: + raise ValueError( + f"No plottable data available for cluster {cluster_id}. " + f"Run process_consensus_features(gather_eics=True, add_ms1=True, add_ms2=True) first." + ) + + # Check if MS1 is deconvoluted + deconvoluted = rep_mf._ms_deconvoluted_idx is not None + + # Create figure + fig, axs = plt.subplots( + len(to_plot), 1, figsize=(10, len(to_plot) * 4), squeeze=False + ) + + fig.suptitle( + f"Consensus Cluster {cluster_id}: " + f"m/z = {cluster_summary['mz_median']:.4f} " + f"(±{cluster_summary['mz_std']:.4f}); " + f"RT = {cluster_summary['scan_time_aligned_median']:.2f} min " + f"(±{cluster_summary['scan_time_aligned_std']:.2f}); " + f"{int(cluster_summary['sample_id_nunique'])} samples" + ) + + i = 0 + + # EIC plot - show all samples + if "EIC" in to_plot: + axs[i][0].set_title("EICs from all samples", loc="left") + + # Track if we've added labels for legend (to avoid duplicates) + rep_labeled = False + regular_labeled = False + induced_labeled = False + + # Plot regular (non-induced) mass features + for _, row in cluster_mfs.iterrows(): + sample_id = row['sample_id'] + mf_id = row['mf_id'] + sample = self[sample_id] + sample_name = row['sample_name'] + + # Check if EIC is available for this mass feature + if hasattr(sample, 'eics') and sample.eics and row['mz'] in sample.eics: + eic_data = sample.eics[row['mz']] + + # Determine line style and width + if sample_id == rep_sample_id and mf_id == rep_mf_id: + # Representative feature - bold line + linewidth = 2.5 + alpha = 1.0 + color = 'tab:blue' + if label_samples: + label = f"{sample_name} (representative)" + else: + label = "Representative" if not rep_labeled else None + rep_labeled = True + else: + # Other features - thinner line + linewidth = 1.0 + alpha = 0.5 + color = 'tab:blue' + if label_samples: + label = sample_name + else: + label = "Regular features" if not regular_labeled else None + regular_labeled = True + + axs[i][0].plot( + eic_data.time, + eic_data.eic, + c=color, + linewidth=linewidth, + alpha=alpha, + linestyle='-', + label=label + ) + + if plot_eic_datapoints: + axs[i][0].scatter( + eic_data.time, + eic_data.eic, + c=color, + alpha=alpha, + s=10 + ) + + if plot_smoothed_eic and hasattr(eic_data, 'eic_smoothed'): + axs[i][0].plot( + eic_data.time, + eic_data.eic_smoothed, + c=color, + linestyle='--', + alpha=alpha * 0.8, + linewidth=linewidth * 0.8 + ) + + # Plot induced (gap-filled) mass features if available + if self.induced_mass_features_dataframe is not None: + induced_cluster_mfs = self.induced_mass_features_dataframe[ + self.induced_mass_features_dataframe['cluster'] == cluster_id + ] + + for _, row in induced_cluster_mfs.iterrows(): + sample_id = row['sample_id'] + mf_id = row['mf_id'] + sample = self[sample_id] + sample_name = row['sample_name'] + + # Check if EIC is available for this induced mass feature + if hasattr(sample, 'eics') and sample.eics and row['mz'] in sample.eics: + eic_data = sample.eics[row['mz']] + + # Induced features - even thinner line + linewidth = 0.5 + alpha = 0.4 + color = 'tab:orange' + + if label_samples: + label = f"{sample_name} (induced)" + else: + label = "Gap-filled features" if not induced_labeled else None + induced_labeled = True + + axs[i][0].plot( + eic_data.time, + eic_data.eic, + c=color, + linewidth=linewidth, + alpha=alpha, + linestyle='-', + label=label + ) + + if plot_eic_datapoints: + axs[i][0].scatter( + eic_data.time, + eic_data.eic, + c=color, + alpha=alpha, + s=5 + ) + + if plot_smoothed_eic and hasattr(eic_data, 'eic_smoothed'): + axs[i][0].plot( + eic_data.time, + eic_data.eic_smoothed, + c=color, + linestyle='--', + alpha=alpha * 0.8, + linewidth=linewidth * 0.8 + ) + + # Add vertical line at median RT + axs[i][0].axvline( + x=cluster_summary['scan_time_aligned_median'], + color='k', + linestyle='--', + alpha=0.7, + label='Median RT' + ) + + axs[i][0].set_ylabel("Intensity") + axs[i][0].set_xlabel("Time (minutes)") + axs[i][0].set_xlim( + cluster_summary['scan_time_aligned_median'] - eic_buffer_time, + cluster_summary['scan_time_aligned_median'] + eic_buffer_time, + ) + axs[i][0].legend(loc='upper left', fontsize=8) + axs[i][0].yaxis.get_major_formatter().set_useOffset(False) + i += 1 + + # MS1 plot - from representative + if "MS1" in to_plot: + if deconvoluted: + axs[i][0].set_title( + f"MS1 (deconvoluted) - Representative: {rep_sample.sample_name}", + loc="left" + ) + axs[i][0].vlines( + rep_mf.mass_spectrum.mz_exp, + 0, + rep_mf.mass_spectrum.abundance, + color="k", + alpha=0.2, + label="Raw MS1", + ) + axs[i][0].vlines( + rep_mf.mass_spectrum_deconvoluted.mz_exp, + 0, + rep_mf.mass_spectrum_deconvoluted.abundance, + color="k", + label="Deconvoluted MS1", + ) + axs[i][0].set_xlim( + rep_mf.mass_spectrum_deconvoluted.mz_exp.min() * 0.8, + rep_mf.mass_spectrum_deconvoluted.mz_exp.max() * 1.1, + ) + axs[i][0].set_ylim( + 0, rep_mf.mass_spectrum_deconvoluted.abundance.max() * 1.1 + ) + else: + axs[i][0].set_title( + f"MS1 (raw) - Representative: {rep_sample.sample_name}", + loc="left" + ) + axs[i][0].vlines( + rep_mf.mass_spectrum.mz_exp, + 0, + rep_mf.mass_spectrum.abundance, + color="k", + label="Raw MS1", + ) + axs[i][0].set_xlim( + rep_mf.mass_spectrum.mz_exp.min() * 0.8, + rep_mf.mass_spectrum.mz_exp.max() * 1.1, + ) + axs[i][0].set_ylim(bottom=0) + + # Highlight the feature m/z + if abs(rep_mf.ms1_peak.mz_exp - rep_mf.mz) < 0.01: + axs[i][0].vlines( + rep_mf.ms1_peak.mz_exp, + 0, + rep_mf.ms1_peak.abundance, + color="m", + label="Feature m/z", + ) + + axs[i][0].legend(loc="upper left") + axs[i][0].set_ylabel("Intensity") + axs[i][0].set_xlabel("m/z") + axs[i][0].yaxis.set_tick_params(labelleft=False) + i += 1 + + # MS2 plot - from representative + if "MS2" in to_plot: + axs[i][0].set_title( + f"MS2 - Representative: {rep_sample.sample_name}", + loc="left" + ) + axs[i][0].vlines( + rep_mf.best_ms2.mz_exp, + 0, + rep_mf.best_ms2.abundance, + color="k" + ) + axs[i][0].set_ylabel("Intensity") + axs[i][0].set_xlabel("m/z") + axs[i][0].set_ylim(bottom=0) + axs[i][0].yaxis.set_tick_params(labelleft=False) + i += 1 + + plt.tight_layout() + + if return_fig: + plt.close(fig) + return fig + else: + plt.show() + return None + def get_representative_mass_features_for_all_clusters(self, representative_metric=None): """ Get the most representative mass feature for all clusters in bulk. diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 31bd680d1..741892024 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1648,6 +1648,24 @@ def _prepare_lcms_mass_features_for_combination(self, lcms_obj, induced_features mf_df["sample_id"] = self.manifest[lcms_obj.sample_name]["collection_id"] mf_df["coll_mf_id"] = mf_df["sample_id"].astype(str) + "_" + mf_df["mf_id"].astype(str) + # For induced features, extract cluster from mf_id (format: c{cluster}_{index}_i) + # and add as a column since cluster_index attribute may not be set on the object + if induced_features: + def extract_cluster(mf_id): + # mf_id format: c{cluster}_{index}_i + # Example: c123_5_i -> cluster 123 + if isinstance(mf_id, str) and mf_id.startswith('c') and '_i' in mf_id: + parts = mf_id.split('_') + if len(parts) >= 2: + cluster_str = parts[0][1:] # Remove 'c' prefix + try: + return int(cluster_str) + except ValueError: + return None + return None + + mf_df['cluster'] = mf_df['mf_id'].apply(extract_cluster) + # Check if scan_df has scan_time_aligned and add to mf_df if so if "scan_time_aligned" in lcms_obj.scan_df.columns: scan_df = lcms_obj.scan_df[["scan", "scan_time_aligned"]].copy() @@ -1701,13 +1719,34 @@ def _combine_mass_features(self, induced_features = False): # Prepare mass features for combination sequentially mf_df_list = [] for lcms_obj in self: + # Skip samples with no induced mass features if processing induced features + if induced_features: + has_attr = hasattr(lcms_obj, 'induced_mass_features') + if has_attr: + dict_len = len(lcms_obj.induced_mass_features) + print(f"Sample {lcms_obj.sample_name}: has_attr={has_attr}, len={dict_len}") + if dict_len == 0: + continue + else: + print(f"Sample {lcms_obj.sample_name}: has_attr={has_attr}") + continue mf_df = self._prepare_lcms_mass_features_for_combination(lcms_obj, induced_features) mf_df_list.append(mf_df) + # If no mass features were collected (e.g., no induced features exist), return early + if len(mf_df_list) == 0: + # Add a warning here, not sure how one might reach this state, clearly saying if they are induced features or not + warnings.warn("No mass features found to combine in the collection.", UserWarning) + if induced_features: + self._combined_induced_mass_features = None + else: + self._combined_mass_features = None + return + combined_mass_features = pd.concat(mf_df_list) # Move coll_mf_id, sample_name, sample_id, and mf_id to front cols = combined_mass_features.columns.tolist() - top_cols = ["coll_mf_id", "sample_name", "sample_id", "mf_id", "mz", "scan_time_aligned"] + top_cols = ["coll_mf_id", "sample_name", "sample_id", "mf_id", "mz", "scan_time_aligned", "cluster"] cols = [x for x in top_cols + [col for col in cols if col not in top_cols] if x in cols] combined_mass_features = combined_mass_features[cols] # Make coll_mf_id the index @@ -2027,7 +2066,7 @@ def collection_pivot_table(self, attribute = 'coll_mf_id', verbose = True): mf_pivot.reset_index(inplace = True) imf_pivot = self.induced_mass_features_dataframe.copy() imf_pivot.reset_index(inplace = True) - # Cluster column already extracted by induced_mass_features_dataframe property + # Cluster column extracted from mf_id in _prepare_lcms_mass_features_for_combination mf_pivot = pd.concat([mf_pivot, imf_pivot], axis = 0) mf_pivot.reset_index(drop = True, inplace = True) mf_pivot['cluster'] = mf_pivot['cluster'].astype(int) @@ -2070,7 +2109,7 @@ def collection_consensus_report(self, how = 'intensity'): mf_df.reset_index(inplace = True) imf_df = self.induced_mass_features_dataframe.copy() imf_df.reset_index(inplace = True) - # Cluster column already extracted by induced_mass_features_dataframe property + # Cluster column extracted from mf_id in _prepare_lcms_mass_features_for_combination mf_df = pd.concat([mf_df, imf_df], axis = 0) mf_df.reset_index(drop = True, inplace = True) mf_df['cluster'] = mf_df['cluster'].astype(int) @@ -2149,8 +2188,8 @@ def mass_features_dataframe(self, df): def induced_mass_features_dataframe(self): self._check_mass_features_df(induced_features = True) if self._combined_induced_mass_features is not None and len(self._combined_induced_mass_features) > 0: - # The cluster column should already be set during gap-filling - # No parsing needed - cluster_index is stored directly on induced mass features + # The cluster column is extracted from mf_id in _prepare_lcms_mass_features_for_combination + # mf_id format for induced features: c{cluster}_{index}_i pass return self._combined_induced_mass_features From cfedd676c821bfb2854ce2e6c97b356ff9182d85 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 20 Jan 2026 16:47:08 -0800 Subject: [PATCH 107/158] Fix test fixture and add plotting to example functionality --- .../nmdc/lipidomics/lipidomics_collection.py | 63 +++++++++++++------ tests/test_wf_lipidomics.py | 4 +- 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/lipidomics/lipidomics_collection.py index 12d8a02df..d6d79f3a7 100644 --- a/support_code/nmdc/lipidomics/lipidomics_collection.py +++ b/support_code/nmdc/lipidomics/lipidomics_collection.py @@ -322,7 +322,8 @@ def process_single_sample(args): # ============================================================================= ncores = 1 reprocess_samples = False # Set to True to reprocess raw data - + perform_ms2_search = True # Set to True to perform MS2 spectral library search + # Paths base_path = Path("/Volumes/LaCie/nmdc_data/collection_testing/dev_test/") collection_save_path = base_path / "collection" @@ -382,23 +383,28 @@ def process_single_sample(args): # ============================================================================= # Step 5: Prepare MS2 Spectral Library # ============================================================================= - print("\n=== Preparing MS2 Spectral Library ===") - my_msp = MSPInterface(file_path=msp_file_location) - spectral_lib, molecular_metadata = my_msp.get_metabolomics_spectra_library( - polarity="positive", - format="flashentropy", - normalize=True, - fe_kwargs={ - "normalize_intensity": True, - "min_ms2_difference_in_da": 0.02, - "max_ms2_tolerance_in_da": 0.01, - "max_indexed_mz": 3000, - "precursor_ions_removal_da": None, - "noise_threshold": 0, - }, - ) - print(f"Loaded spectral library: {len(molecular_metadata)} entries") - + if perform_ms2_search: + print("\n=== Preparing MS2 Spectral Library ===") + my_msp = MSPInterface(file_path=msp_file_location) + spectral_lib, molecular_metadata = my_msp.get_metabolomics_spectra_library( + polarity="positive", + format="flashentropy", + normalize=True, + fe_kwargs={ + "normalize_intensity": True, + "min_ms2_difference_in_da": 0.02, + "max_ms2_tolerance_in_da": 0.01, + "max_indexed_mz": 3000, + "precursor_ions_removal_da": None, + "noise_threshold": 0, + }, + ) + print(f"Loaded spectral library: {len(molecular_metadata)} entries") + else: + spectral_lib = None + molecular_metadata = None + print("Skipping MS2 spectral library preparation") + # ============================================================================= # Step 6: Process Consensus Features with Integrated Pipeline # ============================================================================= @@ -409,8 +415,8 @@ def process_single_sample(args): perform_gap_filling=True, add_ms1=True, add_ms2=True, - molecular_formula_search=True, - ms2_spectral_search=True, + molecular_formula_search=False, + ms2_spectral_search=False, spectral_lib=spectral_lib, molecular_metadata=molecular_metadata, gather_eics=True, @@ -418,6 +424,23 @@ def process_single_sample(args): ) print(f"Pipeline complete: {time.time() - start_time:.1f} seconds using {ncores} cores") + # ============================================================================= + # Step 6.5: Test Consensus Cluster Plotting + # ============================================================================= + print("\n=== Testing Consensus Cluster Plotting ===") + + # Plot the first cluster as a test + if len(lcms_collection.cluster_summary_dataframe) > 0: + first_cluster_id = lcms_collection.cluster_summary_dataframe.index[0] + # 3116 is a good one to look at :) + print(f"Plotting cluster {first_cluster_id}") + lcms_collection.plot_cluster( + cluster_id=first_cluster_id, + to_plot=["EIC", "MS1", "MS2"], + plot_smoothed_eic=False, + plot_eic_datapoints=False + ) + # ============================================================================= # Step 7: Summarize Processing Results # ============================================================================= diff --git a/tests/test_wf_lipidomics.py b/tests/test_wf_lipidomics.py index 1ba7ff0ec..69a3566b8 100644 --- a/tests/test_wf_lipidomics.py +++ b/tests/test_wf_lipidomics.py @@ -46,7 +46,7 @@ def test_import_lcmsobj_mzml(): auto_process=True, use_parser=True, spectrum_mode="centroid" ) mass_features_df = myLCMSobj.mass_features_to_df() - assert mass_features_df.shape == (1183, 17) + assert mass_features_df.shape == (1183, 18) # Reset the MSParameters to the original values reset_lcms_parameters() @@ -245,7 +245,7 @@ def test_lipidomics_workflow(postgres_database, lcms_obj): # Check that the mass features dataframe is the same as the original df2 = myLCMSobj2.mass_features_to_df() - assert df2.shape == (128, 20) + assert df2.shape == (128, 21) myLCMSobj2.mass_features[0].mass_spectrum.to_dataframe() assert myLCMSobj2.mass_features[0].ms1_peak[0].string == "C20 H30 O2" assert myLCMSobj2.mass_features_ms1_annot_to_df().shape[0] > 130 From c8f5ced81302d31e060efdc255c306968ca1815e Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 21 Jan 2026 14:02:52 -0800 Subject: [PATCH 108/158] Fix test fixtures --- tests/test_wf_lipidomics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_wf_lipidomics.py b/tests/test_wf_lipidomics.py index 69a3566b8..3fba354fa 100644 --- a/tests/test_wf_lipidomics.py +++ b/tests/test_wf_lipidomics.py @@ -148,7 +148,7 @@ def test_lipidomics_workflow(postgres_database, lcms_obj): # Export the mass features to a pandas dataframe df = lcms_obj.mass_features_to_df() - assert df.shape == (128, 20) + assert df.shape == (128, 21) # Plot a mass feature lcms_obj.mass_features[0].plot(return_fig=False) From d6650156e35028e9c5283b9893026a1bb4ee841e Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 23 Jan 2026 13:43:28 -0800 Subject: [PATCH 109/158] Add targeted search functionality --- .../factory/chroma_peak_classes.py | 28 +++ corems/mass_spectra/calc/lc_calc.py | 208 +++++++++++++++--- corems/mass_spectra/factory/lc_class.py | 22 +- .../lipidomics_collection_explore.py | 23 -- .../lcms_metabolomics_targeted_search.py | 59 +++++ .../metabolomics_collection.py} | 0 6 files changed, 282 insertions(+), 58 deletions(-) delete mode 100644 support_code/nmdc/lipidomics/lipidomics_collection_explore.py create mode 100644 support_code/nmdc/metabolomics/lcms_metabolomics_targeted_search.py rename support_code/nmdc/{lipidomics/lipidomics_collection.py => metabolomics/metabolomics_collection.py} (100%) diff --git a/corems/chroma_peak/factory/chroma_peak_classes.py b/corems/chroma_peak/factory/chroma_peak_classes.py index f3386956d..6e10a5c3c 100644 --- a/corems/chroma_peak/factory/chroma_peak_classes.py +++ b/corems/chroma_peak/factory/chroma_peak_classes.py @@ -156,6 +156,9 @@ class LCMSMassFeature(ChromaPeakBase, LCMSMassFeatureCalculation): 1 indicates a perfect Gaussian shape, 0 indicates a non-Gaussian shape. _ms_deconvoluted_idx : [int] The indexes of the mass_spectrum attribute in the deconvoluted mass spectrum. + _type : str + The type of mass feature. Default is "untargeted". + Can be "untargeted", "targeted", or another customized type. is_calibrated : bool If True, the feature has been calibrated. Default is False. monoisotopic_mf_id : int @@ -215,6 +218,7 @@ def __init__( self._tailing_factor: float = None self._noise_score: tuple = None self._gaussian_similarity: float = None + self._type: str = "untargeted" # Additional attributes self.monoisotopic_mf_id = None @@ -643,6 +647,30 @@ def noise_score_max(self): # Handle NaN values - nanmax ignores NaN return np.nanmax([left, right]) + @property + def type(self): + """Type of the mass feature. + + Returns + ------- + str + The type of mass feature ("untargeted", "targeted", or "internal standard"). + """ + return self._type + + @type.setter + def type(self, value): + """Set the type of the mass feature. + + Parameters + ---------- + value : str + The type of mass feature. Should be one of: "untargeted", "targeted", "internal standard". + """ + if not isinstance(value, str): + raise ValueError("The type of the mass feature must be a string") + self._type = value + @property def best_ms2(self): """Points to the best representative MS2 mass spectrum diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index c6f6f993a..853948194 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -357,7 +357,8 @@ def get_average_mass_spectrum( ms.process_mass_spec() return ms - def find_mass_features(self, ms_level=1, grid=True, assign_ms2_scans=False, ms2_scan_filter=None): + def find_mass_features(self, ms_level=1, grid=True, assign_ms2_scans=False, ms2_scan_filter=None, + targeted_search=False, target_search_dict=None): """Find mass features within an LCMSBase object Note that this is a wrapper function that calls the find_mass_features_ph function, but can be extended to support other peak picking methods in the future. @@ -376,6 +377,21 @@ def find_mass_features(self, ms_level=1, grid=True, assign_ms2_scans=False, ms2_ ms2_scan_filter : str or None, optional Filter string for MS2 scans when assign_ms2_scans is True (e.g., 'hcd'). If None, all MS2 scans are considered. Default is None. + targeted_search : bool, optional + If True, perform targeted mass feature search using the target_search_dict. + This mode filters data to only m/z and RT windows of interest and bypasses + intensity and persistence thresholds. Default is False. + target_search_dict : dict or None, optional + Dictionary containing target search parameters. Required if targeted_search is True. + Must contain: + - 'target_mz_list': list of target m/z values + - 'target_rt_list': list of target retention times (in minutes) + - 'mz_tolerance_ppm': m/z tolerance in ppm + - 'rt_tolerance': retention time tolerance (in minutes) + Optionally can contain: + - 'type': type label for mass features (e.g., "internal standard") + If not provided, defaults to "targeted" + Default is None. Raises ------ @@ -384,18 +400,38 @@ def find_mass_features(self, ms_level=1, grid=True, assign_ms2_scans=False, ms2_ If persistent homology peak picking is attempted on non-profile mode data. If data is not gridded and grid is False. If peak picking method is not implemented. + If targeted_search is True but target_search_dict is None or invalid. Returns ------- None, but assigns the mass_features and eics attributes to the object. """ + # Validate targeted search parameters + if targeted_search: + if target_search_dict is None: + raise ValueError("target_search_dict must be provided when targeted_search is True") + required_keys = ['target_mz_list', 'target_rt_list', 'mz_tolerance_ppm', 'rt_tolerance'] + for key in required_keys: + if key not in target_search_dict: + raise ValueError(f"target_search_dict must contain '{key}'") + if len(target_search_dict['target_mz_list']) != len(target_search_dict['target_rt_list']): + raise ValueError("target_mz_list and target_rt_list must have the same length") + pp_method = self.parameters.lc_ms.peak_picking_method if pp_method == "persistent homology": msx_scan_df = self.scan_df[self.scan_df["ms_level"] == ms_level] if all(msx_scan_df["ms_format"] == "profile"): - self.find_mass_features_ph(ms_level=ms_level, grid=grid) + # Determine mass feature type + if targeted_search: + mf_type = target_search_dict.get('type', 'targeted') + else: + mf_type = 'untargeted' + self.find_mass_features_ph(ms_level=ms_level, grid=grid, + targeted_search=targeted_search, + target_search_dict=target_search_dict, + mf_type=mf_type) else: raise ValueError( "MS{} scans are not profile mode, which is required for persistent homology peak picking.".format( @@ -405,7 +441,15 @@ def find_mass_features(self, ms_level=1, grid=True, assign_ms2_scans=False, ms2_ elif pp_method == "centroided_persistent_homology": msx_scan_df = self.scan_df[self.scan_df["ms_level"] == ms_level] if all(msx_scan_df["ms_format"] == "centroid"): - self.find_mass_features_ph_centroid(ms_level=ms_level) + # Determine mass feature type + if targeted_search: + mf_type = target_search_dict.get('type', 'targeted') + else: + mf_type = 'untargeted' + self.find_mass_features_ph_centroid(ms_level=ms_level, + targeted_search=targeted_search, + target_search_dict=target_search_dict, + mf_type=mf_type) else: raise ValueError( "MS{} scans are not centroid mode, which is required for persistent homology centroided peak picking.".format( @@ -431,7 +475,7 @@ def find_mass_features(self, ms_level=1, grid=True, assign_ms2_scans=False, ms2_ pass # Remove noisey mass features if designated in parameters - if self.parameters.lc_ms.remove_redundant_mass_features: + if self.parameters.lc_ms.remove_redundant_mass_features and not targeted_search: self._remove_redundant_mass_features() def integrate_mass_features( @@ -1861,7 +1905,51 @@ def _grid_data(self, data): return new_data_w - def find_mass_features_ph(self, ms_level=1, grid=True): + def _filter_data_by_targets(self, data, target_search_dict): + """Filter MS data to only include m/z and RT windows around target values. + + Parameters + ---------- + data : pd.DataFrame + MS data with 'mz' and 'scan_time' columns + target_search_dict : dict + Dictionary with target_mz_list, target_rt_list, mz_tolerance_ppm, rt_tolerance + + Returns + ------- + pd.DataFrame + Filtered data containing only points within target windows + """ + target_mz_list = target_search_dict['target_mz_list'] + target_rt_list = target_search_dict['target_rt_list'] + mz_tolerance_ppm = target_search_dict['mz_tolerance_ppm'] + rt_tolerance = target_search_dict['rt_tolerance'] + + # Create a mask for data points that fall within any target window + mask = np.zeros(len(data), dtype=bool) + + for target_mz, target_rt in zip(target_mz_list, target_rt_list): + # Calculate m/z window + mz_tol = target_mz * mz_tolerance_ppm / 1e6 + mz_min = target_mz - mz_tol + mz_max = target_mz + mz_tol + + # Calculate RT window + rt_min = target_rt - rt_tolerance + rt_max = target_rt + rt_tolerance + + # Create mask for this target + target_mask = ( + (data['mz'] >= mz_min) & (data['mz'] <= mz_max) & + (data['scan_time'] >= rt_min) & (data['scan_time'] <= rt_max) + ) + + # Combine with overall mask + mask |= target_mask + + return data[mask].reset_index(drop=True) + + def find_mass_features_ph(self, ms_level=1, grid=True, targeted_search=False, target_search_dict=None, mf_type="untargeted"): """Find mass features within an LCMSBase object using persistent homology. Assigns the mass_features attribute to the object (a dictionary of LCMSMassFeature objects, keyed by mass feature id) @@ -1872,6 +1960,12 @@ def find_mass_features_ph(self, ms_level=1, grid=True): The MS level to use. Default is 1. grid : bool, optional If True, will regrid the data before running the persistent homology calculations (after checking if the data is gridded). Default is True. + targeted_search : bool, optional + If True, perform targeted search mode. Default is False. + target_search_dict : dict or None, optional + Dictionary with target parameters for targeted search. Default is None. + mf_type : str, optional + Type label for the mass features. Default is "untargeted". Raises ------ @@ -1900,14 +1994,30 @@ def find_mass_features_ph(self, ms_level=1, grid=True): # Drop rows with missing intensity values and reset index data = data.dropna(subset=["intensity"]).reset_index(drop=True) + + # Add scan_time for filtering if in targeted mode + if targeted_search: + data = data.merge(self.scan_df[["scan", "scan_time"]], on="scan", how="left") - # Threshold data + # Threshold data (bypass thresholds in targeted mode) dims = ["mz", "scan_time"] - threshold = self.parameters.lc_ms.ph_inten_min_rel * data.intensity.max() - persistence_threshold = ( - self.parameters.lc_ms.ph_persis_min_rel * data.intensity.max() - ) - data_thres = data[data["intensity"] > threshold].reset_index(drop=True).copy() + if targeted_search: + # In targeted mode, bypass intensity and persistence thresholds + threshold = 0 + persistence_threshold = 0 + # Filter data to only target windows + data_thres = self._filter_data_by_targets(data, target_search_dict) + if len(data_thres) == 0: + if self.parameters.lc_ms.verbose_processing: + print("No data found in target windows") + self.mass_features = {} + return + else: + threshold = self.parameters.lc_ms.ph_inten_min_rel * data.intensity.max() + persistence_threshold = ( + self.parameters.lc_ms.ph_persis_min_rel * data.intensity.max() + ) + data_thres = data[data["intensity"] > threshold].reset_index(drop=True).copy() # Check if gridded, if not, grid gridded_mz = self.check_if_grid(data_thres) @@ -1919,20 +2029,21 @@ def find_mass_features_ph(self, ms_level=1, grid=True): else: data_thres = self.grid_data(data_thres) - # Add scan_time - data_thres = data_thres.merge(self.scan_df[["scan", "scan_time"]], on="scan") + # Add scan_time (skip if already present from targeted mode) + if 'scan_time' not in data_thres.columns: + data_thres = data_thres.merge(self.scan_df[["scan", "scan_time"]], on="scan") # Process in chunks if required if len(data_thres) > 10000: return self._find_mass_features_ph_partition( - data_thres, dims, persistence_threshold + data_thres, dims, persistence_threshold, mf_type ) else: # Process all at once return self._find_mass_features_ph_single( - data_thres, dims, persistence_threshold + data_thres, dims, persistence_threshold, mf_type ) - def _find_mass_features_ph_single(self, data_thres, dims, persistence_threshold): + def _find_mass_features_ph_single(self, data_thres, dims, persistence_threshold, mf_type="untargeted"): """Process all data at once (original logic).""" # Build factors factors = { @@ -1965,9 +2076,9 @@ def _find_mass_features_ph_single(self, data_thres, dims, persistence_threshold) mass_features_df = mass_features_df.reset_index(drop=True) # Populate mass_features attribute - self._populate_mass_features(mass_features_df) + self._populate_mass_features(mass_features_df, mf_type) - def _find_mass_features_ph_partition(self, data_thres, dims, persistence_threshold): + def _find_mass_features_ph_partition(self, data_thres, dims, persistence_threshold, mf_type="untargeted"): """Partition the persistent homology mass feature detection for large datasets. This method splits the input data into overlapping scan partitions, processes each partition to detect mass features @@ -1981,6 +2092,8 @@ def _find_mass_features_ph_partition(self, data_thres, dims, persistence_thresho List of dimension names (e.g., ["mz", "scan_time"]) used for feature detection. persistence_threshold : float Minimum persistence value required for a detected mass feature to be retained. + mf_type : str, optional + Type label for the mass features. Default is "untargeted". Returns ------- @@ -2084,7 +2197,7 @@ def _find_mass_features_ph_partition(self, data_thres, dims, persistence_thresho combined_features = combined_features.reset_index(drop=True) # Populate mass_features attribute - self._populate_mass_features(combined_features) + self._populate_mass_features(combined_features, mf_type) else: self.mass_features = {} @@ -2147,7 +2260,7 @@ def _process_partition_ph(self, partition_data, index, dims, persistence_thresho return mass_features - def _populate_mass_features(self, mass_features_df): + def _populate_mass_features(self, mass_features_df, mf_type="untargeted"): """Populate the mass_features attribute from a DataFrame. Parameters @@ -2155,6 +2268,8 @@ def _populate_mass_features(self, mass_features_df): mass_features_df : pd.DataFrame DataFrame containing mass feature information. Note that the order of this DataFrame will determine the order of mass features in the mass_features attribute. + mf_type : str, optional + Type label for the mass features. Default is "untargeted". Returns ------- @@ -2170,18 +2285,25 @@ def _populate_mass_features(self, mass_features_df): for row in mass_features_df.itertuples(): row_dict = mass_features_df.iloc[row.Index].to_dict() lcms_feature = LCMSMassFeature(self, **row_dict) + lcms_feature.type = mf_type self.mass_features[lcms_feature.id] = lcms_feature if self.parameters.lc_ms.verbose_processing: print("Found " + str(len(mass_features_df)) + " initial mass features") - def find_mass_features_ph_centroid(self, ms_level=1): + def find_mass_features_ph_centroid(self, ms_level=1, targeted_search=False, target_search_dict=None, mf_type="untargeted"): """Find mass features within an LCMSBase object using persistent homology-type approach but with centroided data. Parameters ---------- ms_level : int, optional The MS level to use. Default is 1. + targeted_search : bool, optional + If True, perform targeted search mode. Default is False. + target_search_dict : dict or None, optional + Dictionary with target parameters for targeted search. Default is None. + mf_type : str, optional + Type label for the mass features. Default is "untargeted". Raises ------ @@ -2203,20 +2325,37 @@ def find_mass_features_ph_centroid(self, ms_level=1): # Work with reference instead of copy data = self._ms_unprocessed[ms_level] - # Calculate threshold first to avoid multiple operations - max_intensity = data["intensity"].max() - threshold = self.parameters.lc_ms.ph_inten_min_rel * max_intensity - - # Create single filter condition and apply to required columns only - valid_mask = data["intensity"].notna() & (data["intensity"] > threshold) - required_cols = ["mz", "intensity", "scan"] - data_thres = data.loc[valid_mask, required_cols].copy() - data_thres["persistence"] = data_thres["intensity"] - - # Merge with required scan data + # Merge with scan data first (needed for filtering in targeted mode) scan_subset = self.scan_df[["scan", "scan_time"]] - mf_df = data_thres.merge(scan_subset, on="scan", how="inner") - del data_thres, scan_subset + data_with_time = data.merge(scan_subset, on="scan", how="inner") + + # Calculate threshold and filter (bypass in targeted mode) + if targeted_search: + # In targeted mode, bypass intensity threshold + threshold = 0 + valid_mask = data_with_time["intensity"].notna() + required_cols = ["mz", "intensity", "scan", "scan_time"] + data_thres = data_with_time.loc[valid_mask, required_cols].copy() + + # Filter to target windows + data_thres = self._filter_data_by_targets(data_thres, target_search_dict) + + if len(data_thres) == 0: + if self.parameters.lc_ms.verbose_processing: + print("No data found in target windows") + self.mass_features = {} + return + else: + # Normal mode with threshold + max_intensity = data_with_time["intensity"].max() + threshold = self.parameters.lc_ms.ph_inten_min_rel * max_intensity + valid_mask = data_with_time["intensity"].notna() & (data_with_time["intensity"] > threshold) + required_cols = ["mz", "intensity", "scan", "scan_time"] + data_thres = data_with_time.loc[valid_mask, required_cols].copy() + + data_thres["persistence"] = data_thres["intensity"] + mf_df = data_thres + del data_thres, scan_subset, data_with_time # Order by scan_time and then mz to ensure features near in rt are processed together # It's ok that different scans are in different partitions; we will roll up later @@ -2289,6 +2428,7 @@ def find_mass_features_ph_centroid(self, ms_level=1): for idx, row in mass_features.iterrows(): row_dict = row.to_dict() lcms_feature = LCMSMassFeature(self, **row_dict) + lcms_feature.type = mf_type self.mass_features[lcms_feature.id] = lcms_feature if self.parameters.lc_ms.verbose_processing: diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 741892024..683d8a516 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -784,7 +784,7 @@ def get_scans_from_apex(ms1_scans, apex_scan, scans_to_average): ] mf_dict[k].update_mz() - def mass_features_to_df(self, induced_features = False): + def mass_features_to_df(self, induced_features=False, drop_na_cols=False, include_cols=None): """Returns a pandas dataframe summarizing the mass features. The dataframe contains the following columns: mf_id, mz, apex_scan, scan_time, intensity, @@ -793,6 +793,11 @@ def mass_features_to_df(self, induced_features = False): ----------- induced_features : bool, optional If True, calls the induced_mass_features dictionary. Defaults to False. + drop_na_cols : bool, optional + If True, drops columns that are entirely NA. Defaults to False. + include_cols : list of str, optional + If provided, only includes the specified columns in the output (in addition to 'mf_id' which is always included as the index). + If None, includes all available columns. Defaults to None. Raises -------- @@ -869,6 +874,7 @@ def mass_spectrum_to_string( "isotopologue_type", "mass_spectrum_deconvoluted_parent", "ms2_scan_numbers", + "type", ] df_mf_list = [] @@ -919,6 +925,7 @@ def mass_spectrum_to_string( # reorder columns col_order = [ "mf_id", + "type", "scan_time", "mz", "apex_scan", @@ -959,6 +966,19 @@ def mass_spectrum_to_string( if 'normalized_dispersity_index' in df_mf.columns: df_mf["normalized_dispersity_index"] = df_mf["normalized_dispersity_index"].astype('float64') + # Filter columns if include_cols is specified + if include_cols is not None: + # Ensure include_cols is a list + if not isinstance(include_cols, list): + raise ValueError("include_cols must be a list of column names") + # Keep only requested columns that exist in the dataframe + available_cols = [col for col in include_cols if col in df_mf.columns] + df_mf = df_mf[available_cols] + + # Drop columns that are entirely NA if requested + if drop_na_cols: + df_mf = df_mf.dropna(axis=1, how='all') + return df_mf def mass_features_ms1_annot_to_df(self): diff --git a/support_code/nmdc/lipidomics/lipidomics_collection_explore.py b/support_code/nmdc/lipidomics/lipidomics_collection_explore.py deleted file mode 100644 index f1c5e7509..000000000 --- a/support_code/nmdc/lipidomics/lipidomics_collection_explore.py +++ /dev/null @@ -1,23 +0,0 @@ -import pandas as pd -from pathlib import Path - -collection_path = Path("/Users/heal742/LOCAL/10_lcms_collection_testing/KidsFirst_T-ALL_neg/processed_files") -collection_mass_features = pd.read_csv(collection_path / "collection_mass_features_ward.csv") - -# Pivot the mass features dataframe, sample_id as columns and cluster_id as index -mass_feature_pivot = collection_mass_features.pivot(index='cluster', columns='sample_id', values='intensity') - -# Get average mz and rt for each cluster -cluster_avg_mz_rt = collection_mass_features.groupby('cluster').agg({'mz': 'mean', 'scan_time_aligned': 'mean', 'sample_id': 'nunique', 'intensity': 'mean'}).reset_index() - -# Add average mz and rt to mass_feature_pivot -mass_feature_pivot = mass_feature_pivot.join(cluster_avg_mz_rt, on='cluster') - -# Rearrange columns -mass_feature_pivot = mass_feature_pivot[['mz', 'scan_time_aligned', 'sample_id', 'intensity'] + list(mass_feature_pivot.columns[2:])] - -# Save mass_feature_pivot to csv -mass_feature_pivot.to_csv(collection_path / "collection_mass_features_pivot_ward.csv", index=True) - -print("here") - diff --git a/support_code/nmdc/metabolomics/lcms_metabolomics_targeted_search.py b/support_code/nmdc/metabolomics/lcms_metabolomics_targeted_search.py new file mode 100644 index 000000000..6d52c0282 --- /dev/null +++ b/support_code/nmdc/metabolomics/lcms_metabolomics_targeted_search.py @@ -0,0 +1,59 @@ +from pathlib import Path + +from corems.mass_spectra.input.rawFileReader import ImportMassSpectraThermoMSFileReader + +if __name__ == "__main__": + # ============================================================================= + # Configuration + # ============================================================================= + # Paths + base_path = Path("/Volumes/LaCie/nmdc_data/collection_testing/dev_test/") + raw_data_path = base_path / "raw" + + # Instantiate LCMS object from the first raw data file + first_raw_file = next(raw_data_path.glob("*.raw")) + parser = ImportMassSpectraThermoMSFileReader(first_raw_file) + lcms_obj = parser.get_lcms_obj(spectra="ms1") + assert lcms_obj is not None, "Failed to instantiate LCMS object." + + # Set parameters for the LCMS object, only ones we NEED to change from defaults to get things running + # Ideally, we'd load carefully chosen parameters from a file here for general processing + lcms_obj.parameters.mass_spectrum['ms2'].mass_spectrum.noise_threshold_method = "relative_abundance" + + # Prepare a search dictionary for a targeted search + # Here are examples of target m/z and RT values + # Could derive this from an msp file. + target_mz_list = [254.2837, 521.325988, 503.3134460449219, 317.2839050292969] + target_rt_list = [5.83, 6.017, 7.977, 6.8658] + target_search_dict = { + "target_mz_list": target_mz_list, + "target_rt_list": target_rt_list, + "mz_tolerance_ppm": 5, #QUESTION: per target or bulk? + "rt_tolerance": 0.5, #QUESTION: per target or bulk? + "type": "internal standard" + } + + # Look for mass features in targeted search mode + lcms_obj.find_mass_features( + targeted_search = True, + target_search_dict=target_search_dict + ) + # Alternatively, or additionally, we could do normal (untargeted) mass feature finding here + # lcms_obj.find_mass_features(targeted_search=False) + + # Let's integrate, add ms1 and ms2 + lcms_obj.integrate_mass_features() + lcms_obj.add_associated_ms1(use_parser=False, spectrum_mode="profile") + lcms_obj.add_associated_ms2_dda(use_parser=True, spectrum_mode="centroid") + + # Here we could do formula searching and MS2 search against a MSP database with MS2s of the search targets (similar to test_lcms_metabolomics.py) + # BUT, we'd need a compatible msp file, and that's not ultra simple + + # Check out the mass feature dataframe + mf_df = lcms_obj.mass_features_to_df(drop_na_cols=True) + + # Can visualize as well + lcms_obj.mass_features[0].plot(return_fig = False) + + # Here could do some post-processing to report the "best hit" per target and include confidence metrics + print("here") \ No newline at end of file diff --git a/support_code/nmdc/lipidomics/lipidomics_collection.py b/support_code/nmdc/metabolomics/metabolomics_collection.py similarity index 100% rename from support_code/nmdc/lipidomics/lipidomics_collection.py rename to support_code/nmdc/metabolomics/metabolomics_collection.py From 4d94e7c393084c123ec810ea2bc2a6ff5e759217 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 23 Jan 2026 13:55:56 -0800 Subject: [PATCH 110/158] Add test for targeted search functionality --- tests/test_lcms_metabolomics.py | 89 +++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/tests/test_lcms_metabolomics.py b/tests/test_lcms_metabolomics.py index ed4c539d2..da174a8f4 100644 --- a/tests/test_lcms_metabolomics.py +++ b/tests/test_lcms_metabolomics.py @@ -157,4 +157,93 @@ def test_lcms_metabolomics(postgres_database, lcms_obj, msp_file_location): # Reset the MSParameters to the original values reset_lcms_parameters() + reset_ms_parameters() + + +def test_lcms_metabolomics_targeted_search(lcms_obj): + """Test the targeted search functionality for LCMS metabolomics""" + + # Set parameters to the defaults for reproducible testing + lcms_obj.parameters = LCMSParameters(use_defaults=True) + + # Set parameters on the LCMS object that are reasonable for testing + lcms_obj.parameters.lc_ms.peak_picking_method = "persistent homology" + lcms_obj.parameters.lc_ms.ph_inten_min_rel = 0.0005 + lcms_obj.parameters.lc_ms.ph_persis_min_rel = 0.05 + lcms_obj.parameters.lc_ms.ph_smooth_it = 0 + + # MSParameters for ms1 and ms2 mass spectra + ms1_params = lcms_obj.parameters.mass_spectrum['ms1'] + ms1_params.mass_spectrum.noise_threshold_method = "relative_abundance" + ms1_params.mass_spectrum.noise_threshold_min_relative_abundance = 0.1 + ms1_params.mass_spectrum.noise_min_mz, ms1_params.mass_spectrum.min_picking_mz = 0, 0 + ms1_params.mass_spectrum.noise_max_mz, ms1_params.mass_spectrum.max_picking_mz = np.inf, np.inf + ms1_params.ms_peak.legacy_resolving_power = False + + # Copy settings for ms2 data + ms2_params_hcd = ms1_params.copy() + lcms_obj.parameters.mass_spectrum['ms2'] = ms2_params_hcd + + # Prepare a targeted search dictionary with specific m/z and RT values + # These values are known to exist in the test data + target_mz_list = [301.2166, 698.6289] + target_rt_list = [8.8956, 23.8168] + target_search_dict = { + "target_mz_list": target_mz_list, + "target_rt_list": target_rt_list, + "mz_tolerance_ppm": 5, + "rt_tolerance": 0.5, + "type": "internal standard" + } + + # Perform targeted search for mass features + lcms_obj.find_mass_features( + targeted_search=True, + target_search_dict=target_search_dict + ) + + # Verify that mass features were found + assert len(lcms_obj.mass_features) > 0, "No mass features found in targeted search" + + # Verify that the number of mass features matches or is close to the number of targets + # (may find multiple features per target depending on clustering) + assert len(lcms_obj.mass_features) >= len(target_mz_list), \ + f"Expected at least {len(target_mz_list)} mass features, found {len(lcms_obj.mass_features)}" + + # Integrate the mass features + lcms_obj.integrate_mass_features(drop_if_fail=True) + + # Verify that mass features still exist after integration + assert len(lcms_obj.mass_features) > 0, "No mass features remaining after integration" + + # Add associated MS1 data + lcms_obj.add_associated_ms1(use_parser=False, spectrum_mode="profile") + + # Add associated MS2 data + og_ms_len = len(lcms_obj._ms) + lcms_obj.add_associated_ms2_dda(use_parser=True, spectrum_mode="centroid") + + # Verify MS2 data was added + assert len(lcms_obj._ms) >= og_ms_len, "MS2 data should be added" + + # Check that mass features have the expected properties + mf_df = lcms_obj.mass_features_to_df(drop_na_cols=True) + assert not mf_df.empty, "Mass features dataframe should not be empty" + + # Verify that at least one mass feature is close to each target m/z and RT value + for target_mz, target_rt in zip(target_mz_list, target_rt_list): + # Calculate m/z difference in ppm for all features + mz_diff_ppm = abs(mf_df['mz'] - target_mz) / target_mz * 1e6 + # Calculate RT difference in minutes (scan_time is in minutes) + rt_diff = abs(mf_df['scan_time'] - target_rt) + + # Check if any feature is within tolerance for both m/z and RT + matches = (mz_diff_ppm < 10) & (rt_diff < 1.0) + + assert matches.any(), \ + f"No mass feature found for target m/z={target_mz}, RT={target_rt}. " \ + f"Closest m/z diff: {mz_diff_ppm.min():.2f} ppm, closest RT diff: {rt_diff.min():.3f} min" + + # Reset the parameters to the original values + reset_lcms_parameters() reset_ms_parameters() \ No newline at end of file From b67809eb2e086f6267dc2b1a1db5246a0ceefb43 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 23 Jan 2026 14:14:40 -0800 Subject: [PATCH 111/158] Update test assertions for future proofing --- tests/test_lcms_metabolomics.py | 34 +++++++++++++++++++++++++++++++++ tests/test_wf_lipidomics.py | 9 ++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/tests/test_lcms_metabolomics.py b/tests/test_lcms_metabolomics.py index da174a8f4..018c56444 100644 --- a/tests/test_lcms_metabolomics.py +++ b/tests/test_lcms_metabolomics.py @@ -244,6 +244,40 @@ def test_lcms_metabolomics_targeted_search(lcms_obj): f"No mass feature found for target m/z={target_mz}, RT={target_rt}. " \ f"Closest m/z diff: {mz_diff_ppm.min():.2f} ppm, closest RT diff: {rt_diff.min():.3f} min" + # Verify that the type attribute is set correctly + assert 'type' in mf_df.columns, "Type column should be present in mass features dataframe" + assert (mf_df['type'] == 'internal standard').all(), \ + "All targeted mass features should have type 'internal standard'" + + # Test HDF5 export/import to verify type attribute persists + shutil.rmtree("test_targeted_search.corems", ignore_errors=True) + exporter = LCMSMetabolomicsExport("test_targeted_search", lcms_obj) + exporter.to_hdf(overwrite=True) + + # Reload the saved lcms object and check that type attribute persists + parser = ReadCoreMSHDFMassSpectra( + "test_targeted_search.corems/test_targeted_search.hdf5" + ) + lcms_obj_reloaded = parser.get_lcms_obj() + + # Check that mass features were reloaded + assert len(lcms_obj_reloaded.mass_features) == len(lcms_obj.mass_features), \ + "Reloaded object should have the same number of mass features" + + # Verify type attribute persisted through export/import + for mf_id, mf in lcms_obj_reloaded.mass_features.items(): + assert mf.type == 'internal standard', \ + f"Mass feature {mf_id} should have type 'internal standard' after reload" + + # Verify type column in dataframe after reload + mf_df_reloaded = lcms_obj_reloaded.mass_features_to_df(drop_na_cols=True) + assert 'type' in mf_df_reloaded.columns, "Type column should persist in reloaded dataframe" + assert (mf_df_reloaded['type'] == 'internal standard').all(), \ + "All mass features should have type 'internal standard' after reload" + + # Cleanup + shutil.rmtree("test_targeted_search.corems", ignore_errors=True) + # Reset the parameters to the original values reset_lcms_parameters() reset_ms_parameters() \ No newline at end of file diff --git a/tests/test_wf_lipidomics.py b/tests/test_wf_lipidomics.py index 3fba354fa..7a8f31637 100644 --- a/tests/test_wf_lipidomics.py +++ b/tests/test_wf_lipidomics.py @@ -46,7 +46,8 @@ def test_import_lcmsobj_mzml(): auto_process=True, use_parser=True, spectrum_mode="centroid" ) mass_features_df = myLCMSobj.mass_features_to_df() - assert mass_features_df.shape == (1183, 18) + assert mass_features_df.shape[0] == 1183 + assert mass_features_df.shape[1] > 15 # Reset the MSParameters to the original values reset_lcms_parameters() @@ -148,7 +149,8 @@ def test_lipidomics_workflow(postgres_database, lcms_obj): # Export the mass features to a pandas dataframe df = lcms_obj.mass_features_to_df() - assert df.shape == (128, 21) + assert df.shape[0] == 128 + assert df.shape[1] > 15 # Plot a mass feature lcms_obj.mass_features[0].plot(return_fig=False) @@ -245,7 +247,8 @@ def test_lipidomics_workflow(postgres_database, lcms_obj): # Check that the mass features dataframe is the same as the original df2 = myLCMSobj2.mass_features_to_df() - assert df2.shape == (128, 21) + assert df2.shape[0] == 128 + assert df2.shape[1] > 15 myLCMSobj2.mass_features[0].mass_spectrum.to_dataframe() assert myLCMSobj2.mass_features[0].ms1_peak[0].string == "C20 H30 O2" assert myLCMSobj2.mass_features_ms1_annot_to_df().shape[0] > 130 From 1eaef4dd0c39c398f479270343a714e479b3b4ff Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 26 Jan 2026 14:23:36 -0800 Subject: [PATCH 112/158] Lots of improvements to EIC data management, consolodating plotting functionality --- .../factory/chroma_peak_classes.py | 341 ++++++----- corems/mass_spectra/calc/lc_calc.py | 531 ++++++++++-------- .../mass_spectra/calc/lc_calc_operations.py | 24 +- corems/mass_spectra/factory/lc_class.py | 70 ++- corems/mass_spectra/input/corems_hdf5.py | 320 ++++++++++- corems/mass_spectra/output/export.py | 278 ++++++++- .../metabolomics/metabolomics_collection.py | 67 ++- 7 files changed, 1169 insertions(+), 462 deletions(-) diff --git a/corems/chroma_peak/factory/chroma_peak_classes.py b/corems/chroma_peak/factory/chroma_peak_classes.py index 6e10a5c3c..11744e794 100644 --- a/corems/chroma_peak/factory/chroma_peak_classes.py +++ b/corems/chroma_peak/factory/chroma_peak_classes.py @@ -139,6 +139,9 @@ class LCMSMassFeature(ChromaPeakBase, LCMSMassFeatureCalculation): The persistence of the feature. _eic_data : EIC_Data The EIC data object associated with the feature. + _eic_mz : float + The m/z value used to extract the EIC data, + sometimes different from the observed m/z due to calibration, centroiding, or other processing. _dispersity_index : float The dispersity index of the feature, in minutes. _normalized_dispersity_index : float @@ -256,6 +259,194 @@ def update_mz(self): if abs(mz_diff) < 0.01: self._mz_exp = new_mz + def _plot_ms1_spectrum(self, ax, deconvoluted=False, sample_name=None): + """Internal method to plot MS1 spectrum on a given axis. + + Parameters + ---------- + ax : matplotlib.axes.Axes + The axis to plot on. + deconvoluted : bool, optional + If True and deconvoluted spectrum exists, plot both raw and deconvoluted. Default is False. + sample_name : str, optional + Sample name to include in title. Default is None. + """ + if self.mass_spectrum is None: + raise ValueError("MS1 spectrum is not available") + + title_prefix = "MS1 (deconvoluted)" if deconvoluted else "MS1 (raw)" + if sample_name: + ax.set_title(f"{title_prefix} - {sample_name}", loc="left") + else: + ax.set_title(title_prefix, loc="left") + + if deconvoluted and self._ms_deconvoluted_idx is not None: + # Plot both raw and deconvoluted + ax.vlines( + self.mass_spectrum.mz_exp, + 0, + self.mass_spectrum.abundance, + color="k", + alpha=0.2, + label="Raw MS1", + ) + ax.vlines( + self.mass_spectrum_deconvoluted.mz_exp, + 0, + self.mass_spectrum_deconvoluted.abundance, + color="k", + label="Deconvoluted MS1", + ) + ax.set_xlim( + self.mass_spectrum_deconvoluted.mz_exp.min() * 0.8, + self.mass_spectrum_deconvoluted.mz_exp.max() * 1.1, + ) + ax.set_ylim( + 0, self.mass_spectrum_deconvoluted.abundance.max() * 1.1 + ) + else: + # Plot raw only + ax.vlines( + self.mass_spectrum.mz_exp, + 0, + self.mass_spectrum.abundance, + color="k", + label="Raw MS1", + ) + ax.set_xlim( + self.mass_spectrum.mz_exp.min() * 0.8, + self.mass_spectrum.mz_exp.max() * 1.1, + ) + ax.set_ylim(bottom=0) + + # Highlight the feature m/z if close enough + if abs(self.ms1_peak.mz_exp - self.mz) < 0.01: + ax.vlines( + self.ms1_peak.mz_exp, + 0, + self.ms1_peak.abundance, + color="m", + label="Feature m/z", + ) + else: + if self.chromatogram_parent.parameters.lc_ms.verbose_processing: + print( + f"The m/z of the mass feature {self.id} is different from the m/z of MS1 peak, " + "the MS1 peak will not be plotted" + ) + + ax.legend(loc="upper left") + ax.set_ylabel("Intensity") + ax.set_xlabel("m/z") + ax.yaxis.set_tick_params(labelleft=False) + + def _plot_ms2_spectrum(self, ax, sample_name=None): + """Internal method to plot MS2 spectrum on a given axis. + + Parameters + ---------- + ax : matplotlib.axes.Axes + The axis to plot on. + sample_name : str, optional + Sample name to include in title. Default is None. + """ + if len(self.ms2_mass_spectra) == 0: + raise ValueError("MS2 spectrum is not available") + + if sample_name: + ax.set_title(f"MS2 - {sample_name}", loc="left") + else: + ax.set_title("MS2", loc="left") + + ax.vlines( + self.best_ms2.mz_exp, 0, self.best_ms2.abundance, color="k" + ) + ax.set_ylabel("Intensity") + ax.set_xlabel("m/z") + ax.set_ylim(bottom=0) + ax.yaxis.get_major_formatter().set_scientific(False) + ax.yaxis.get_major_formatter().set_useOffset(False) + + def _plot_single_eic(self, ax, plot_smoothed=False, plot_datapoints=False, + eic_buffer_time=None, show_ms2_scan=True): + """Internal method to plot a single EIC on a given axis. + + Parameters + ---------- + ax : matplotlib.axes.Axes + The axis to plot on. + plot_smoothed : bool, optional + If True, plot smoothed EIC. Default is False. + plot_datapoints : bool, optional + If True, plot EIC datapoints. Default is False. + eic_buffer_time : float, optional + Time buffer around the peak (minutes). If None, uses parameter setting. Default is None. + show_ms2_scan : bool, optional + If True and MS2 scans exist, show vertical line at MS2 scan time. Default is True. + """ + if self._eic_data is None: + raise ValueError("EIC data is not available") + + if eic_buffer_time is None: + eic_buffer_time = self.chromatogram_parent.parameters.lc_ms.eic_buffer_time + + ax.set_title("EIC", loc="left") + ax.plot( + self._eic_data.time, self._eic_data.eic, c="tab:blue", label="EIC" + ) + + if plot_datapoints: + ax.scatter( + self._eic_data.time, + self._eic_data.eic, + c="tab:blue", + label="EIC Data Points", + ) + + if plot_smoothed and hasattr(self._eic_data, 'eic_smoothed'): + ax.plot( + self._eic_data.time, + self._eic_data.eic_smoothed, + c="tab:red", + label="Smoothed EIC", + ) + + # Fill integrated area if available + if self.start_scan is not None: + ax.fill_between( + self.eic_rt_list, self.eic_list, color="b", alpha=0.2 + ) + else: + if self.chromatogram_parent.parameters.lc_ms.verbose_processing: + print( + f"No start and final scan numbers were provided for mass feature {self.id}" + ) + + ax.set_ylabel("Intensity") + ax.set_xlabel("Time (minutes)") + ax.set_ylim(0, self.eic_list.max() * 1.1) + ax.set_xlim( + self.retention_time - eic_buffer_time, + self.retention_time + eic_buffer_time, + ) + ax.axvline( + x=self.retention_time, color="k", label="MS1 scan time (apex)" + ) + + # Show MS2 scan time if available and requested + if show_ms2_scan and len(self.ms2_scan_numbers) > 0: + ax.axvline( + x=self.chromatogram_parent.get_time_of_scan_id( + self.best_ms2.scan_number + ), + color="grey", + linestyle="--", + label="MS2 scan time", + ) + + ax.legend(loc="upper left") + ax.yaxis.get_major_formatter().set_useOffset(False) + def plot( self, to_plot=["EIC", "MS1", "MS2"], @@ -284,10 +475,6 @@ def plot( The figure object if `return_fig` is True. Otherwise None and the figure is displayed. """ - - # EIC plot preparation - eic_buffer_time = self.chromatogram_parent.parameters.lc_ms.eic_buffer_time - # Adjust to_plot list if there are not spectra added to the mass features if self.mass_spectrum is None: to_plot = [x for x in to_plot if x != "MS1"] @@ -295,160 +482,36 @@ def plot( to_plot = [x for x in to_plot if x != "MS2"] if self._eic_data is None: to_plot = [x for x in to_plot if x != "EIC"] - if self._ms_deconvoluted_idx is not None: - deconvoluted = True - else: - deconvoluted = False + + deconvoluted = self._ms_deconvoluted_idx is not None fig, axs = plt.subplots( len(to_plot), 1, figsize=(9, len(to_plot) * 4), squeeze=False ) fig.suptitle( - "Mass Feature " - + str(self.id) - + ": m/z = " - + str(round(self.mz, ndigits=4)) - + "; time = " - + str(round(self.retention_time, ndigits=1)) - + " minutes" + f"Mass Feature {self.id}: m/z = {round(self.mz, ndigits=4)}; " + f"time = {round(self.retention_time, ndigits=1)} minutes" ) i = 0 # EIC plot if "EIC" in to_plot: - if self._eic_data is None: - raise ValueError( - "EIC data is not available, cannot plot the mass feature's EIC" - ) - axs[i][0].set_title("EIC", loc="left") - axs[i][0].plot( - self._eic_data.time, self._eic_data.eic, c="tab:blue", label="EIC" - ) - if plot_eic_datapoints: - axs[i][0].scatter( - self._eic_data.time, - self._eic_data.eic, - c="tab:blue", - label="EIC Data Points", - ) - if plot_smoothed_eic: - axs[i][0].plot( - self._eic_data.time, - self._eic_data.eic_smoothed, - c="tab:red", - label="Smoothed EIC", - ) - if self.start_scan is not None: - axs[i][0].fill_between( - self.eic_rt_list, self.eic_list, color="b", alpha=0.2 - ) - else: - if self.chromatogram_parent.parameters.lc_ms.verbose_processing: - print( - "No start and final scan numbers were provided for mass feature " - + str(self.id) - ) - axs[i][0].set_ylabel("Intensity") - axs[i][0].set_xlabel("Time (minutes)") - axs[i][0].set_ylim(0, self.eic_list.max() * 1.1) - axs[i][0].set_xlim( - self.retention_time - eic_buffer_time, - self.retention_time + eic_buffer_time, - ) - axs[i][0].axvline( - x=self.retention_time, color="k", label="MS1 scan time (apex)" + self._plot_single_eic( + axs[i][0], + plot_smoothed=plot_smoothed_eic, + plot_datapoints=plot_eic_datapoints ) - if len(self.ms2_scan_numbers) > 0: - axs[i][0].axvline( - x=self.chromatogram_parent.get_time_of_scan_id( - self.best_ms2.scan_number - ), - color="grey", - linestyle="--", - label="MS2 scan time", - ) - axs[i][0].legend(loc="upper left") - axs[i][0].yaxis.get_major_formatter().set_useOffset(False) i += 1 # MS1 plot if "MS1" in to_plot: - if deconvoluted: - axs[i][0].set_title("MS1 (deconvoluted)", loc="left") - axs[i][0].vlines( - self.mass_spectrum.mz_exp, - 0, - self.mass_spectrum.abundance, - color="k", - alpha=0.2, - label="Raw MS1", - ) - axs[i][0].vlines( - self.mass_spectrum_deconvoluted.mz_exp, - 0, - self.mass_spectrum_deconvoluted.abundance, - color="k", - label="Deconvoluted MS1", - ) - axs[i][0].set_xlim( - self.mass_spectrum_deconvoluted.mz_exp.min() * 0.8, - self.mass_spectrum_deconvoluted.mz_exp.max() * 1.1, - ) - axs[i][0].set_ylim( - 0, self.mass_spectrum_deconvoluted.abundance.max() * 1.1 - ) - else: - axs[i][0].set_title("MS1 (raw)", loc="left") - axs[i][0].vlines( - self.mass_spectrum.mz_exp, - 0, - self.mass_spectrum.abundance, - color="k", - label="Raw MS1", - ) - axs[i][0].set_xlim( - self.mass_spectrum.mz_exp.min() * 0.8, - self.mass_spectrum.mz_exp.max() * 1.1, - ) - axs[i][0].set_ylim(bottom=0) - - if (self.ms1_peak.mz_exp - self.mz) < 0.01: - axs[i][0].vlines( - self.ms1_peak.mz_exp, - 0, - self.ms1_peak.abundance, - color="m", - label="Feature m/z", - ) - - else: - if self.chromatogram_parent.parameters.lc_ms.verbose_processing: - print( - "The m/z of the mass feature " - + str(self.id) - + " is different from the m/z of MS1 peak, the MS1 peak will not be plotted" - ) - axs[i][0].legend(loc="upper left") - axs[i][0].set_ylabel("Intensity") - axs[i][0].set_xlabel("m/z") - axs[i][0].yaxis.set_tick_params(labelleft=False) + self._plot_ms1_spectrum(axs[i][0], deconvoluted=deconvoluted) i += 1 # MS2 plot if "MS2" in to_plot: - axs[i][0].set_title("MS2", loc="left") - axs[i][0].vlines( - self.best_ms2.mz_exp, 0, self.best_ms2.abundance, color="k" - ) - axs[i][0].set_ylabel("Intensity") - axs[i][0].set_xlabel("m/z") - axs[i][0].set_ylim(bottom=0) - axs[i][0].yaxis.get_major_formatter().set_scientific(False) - axs[i][0].yaxis.get_major_formatter().set_useOffset(False) - axs[i][0].set_xlim( - self.best_ms2.mz_exp.min() * 0.8, self.best_ms2.mz_exp.max() * 1.1 - ) - axs[i][0].yaxis.set_tick_params(labelleft=False) + self._plot_ms2_spectrum(axs[i][0]) + i += 1 # Add space between subplots plt.tight_layout() diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 853948194..6eba9dc1c 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -588,6 +588,7 @@ def integrate_mass_features( # Pull EIC data and find apex scan index myEIC = self.eics[mz] mf_dict[idx]._eic_data = myEIC + mf_dict[idx]._eic_mz = mz apex_index = np.searchsorted(myEIC.scans, apex_scan) # Find left and right limits of peak using EIC centroid detector, add to EICData @@ -2502,6 +2503,184 @@ class LCMSCollectionCalculations: This class is intended as a mixin for the LCMSCollection class. """ + @staticmethod + def _plot_multiple_eics(ax, cluster_mfs, induced_cluster_mfs, rep_sample_id, rep_mf_id, + median_rt, eic_buffer_time, plot_smoothed=False, + plot_datapoints=False, label_samples=False, lcms_collection=None): + """Internal method to plot multiple EICs from different samples on a given axis. + + Parameters + ---------- + ax : matplotlib.axes.Axes + The axis to plot on. + cluster_mfs : pd.DataFrame + DataFrame containing cluster mass features (non-induced). + induced_cluster_mfs : pd.DataFrame or None + DataFrame containing induced (gap-filled) mass features. + rep_sample_id : int + Sample ID of the representative mass feature. + rep_mf_id : int + Mass feature ID of the representative mass feature. + median_rt : float + Median retention time for the cluster. + eic_buffer_time : float + Time buffer around the peak (minutes). + plot_smoothed : bool, optional + If True, plot smoothed EICs. Default is False. + plot_datapoints : bool, optional + If True, plot EIC datapoints. Default is False. + label_samples : bool, optional + If True, label each sample individually. Default is False. + lcms_collection : LCMSCollection, optional + The parent collection object for accessing samples. Required. + """ + ax.set_title("EICs from all samples", loc="left") + + # Track if we've added labels for legend (to avoid duplicates) + rep_labeled = False + regular_labeled = False + induced_labeled = False + + # Plot regular (non-induced) mass features + for _, row in cluster_mfs.iterrows(): + sample_id = row['sample_id'] + mf_id = row['mf_id'] + sample = lcms_collection[sample_id] + sample_name = row['sample_name'] + + # Get EIC using eic_mz column from dataframe + eic_mz = row.get('_eic_mz') + if eic_mz is not None and not pd.isna(eic_mz) and hasattr(sample, 'eics') and sample.eics: + eic_data = sample.eics.get(eic_mz) + else: + eic_data = None + + if eic_data: + # Determine line style and width + if sample_id == rep_sample_id and mf_id == rep_mf_id: + # Representative feature - bold line + linewidth = 2.5 + alpha = 1.0 + color = 'tab:blue' + if label_samples: + label = f"{sample_name} (representative)" + else: + label = "Representative" if not rep_labeled else None + rep_labeled = True + else: + # Other features - thinner line + linewidth = 1.0 + alpha = 0.5 + color = 'tab:blue' + if label_samples: + label = sample_name + else: + label = "Regular features" if not regular_labeled else None + regular_labeled = True + + ax.plot( + eic_data.time, + eic_data.eic, + c=color, + linewidth=linewidth, + alpha=alpha, + linestyle='-', + label=label + ) + + if plot_datapoints: + ax.scatter( + eic_data.time, + eic_data.eic, + c=color, + alpha=alpha, + s=10 + ) + + if plot_smoothed and hasattr(eic_data, 'eic_smoothed'): + ax.plot( + eic_data.time, + eic_data.eic_smoothed, + c=color, + linestyle='--', + alpha=alpha * 0.8, + linewidth=linewidth * 0.8 + ) + + # Plot induced (gap-filled) mass features if available + if induced_cluster_mfs is not None and not induced_cluster_mfs.empty: + for _, row in induced_cluster_mfs.iterrows(): + sample_id = row['sample_id'] + mf_id = row['mf_id'] + sample = lcms_collection[sample_id] + sample_name = row['sample_name'] + + # Get EIC using eic_mz column from dataframe + eic_mz = row.get('_eic_mz') + if eic_mz is not None and not pd.isna(eic_mz) and hasattr(sample, 'eics') and sample.eics: + eic_data = sample.eics.get(eic_mz) + else: + eic_data = None + + if eic_data: + # Induced features - even thinner line + linewidth = 0.5 + alpha = 0.4 + color = 'tab:orange' + + if label_samples: + label = f"{sample_name} (induced)" + else: + label = "Gap-filled features" if not induced_labeled else None + induced_labeled = True + + ax.plot( + eic_data.time, + eic_data.eic, + c=color, + linewidth=linewidth, + alpha=alpha, + linestyle='-', + label=label + ) + + if plot_datapoints: + ax.scatter( + eic_data.time, + eic_data.eic, + c=color, + alpha=alpha, + s=5 + ) + + if plot_smoothed and hasattr(eic_data, 'eic_smoothed'): + ax.plot( + eic_data.time, + eic_data.eic_smoothed, + c=color, + linestyle='--', + alpha=alpha * 0.8, + linewidth=linewidth * 0.8 + ) + + # Add vertical line at median RT + ax.axvline( + x=median_rt, + color='k', + linestyle='--', + alpha=0.7, + label='Median RT' + ) + + ax.set_ylabel("Intensity") + ax.set_xlabel("Time (minutes)") + ax.set_xlim( + median_rt - eic_buffer_time, + median_rt + eic_buffer_time, + ) + ax.legend(loc='upper left', fontsize=8) + ax.yaxis.get_major_formatter().set_useOffset(False) + def clean_sparse_matrix(self, sparse_matrix): """Clean a sparse matrix by removing duplicates and sorting. @@ -3498,6 +3677,7 @@ def plot_cluster( break # Also check induced features if available + induced_cluster_mfs = None if not has_eics and self.induced_mass_features_dataframe is not None: induced_cluster_mfs = self.induced_mass_features_dataframe[ self.induced_mass_features_dataframe['cluster'] == cluster_id @@ -3517,6 +3697,12 @@ def plot_cluster( f"Run process_consensus_features(gather_eics=True, add_ms1=True, add_ms2=True) first." ) + # Get induced features if not already retrieved + if induced_cluster_mfs is None and self.induced_mass_features_dataframe is not None: + induced_cluster_mfs = self.induced_mass_features_dataframe[ + self.induced_mass_features_dataframe['cluster'] == cluster_id + ] + # Check if MS1 is deconvoluted deconvoluted = rep_mf._ms_deconvoluted_idx is not None @@ -3536,231 +3722,35 @@ def plot_cluster( i = 0 - # EIC plot - show all samples + # EIC plot - show all samples using helper method if "EIC" in to_plot: - axs[i][0].set_title("EICs from all samples", loc="left") - - # Track if we've added labels for legend (to avoid duplicates) - rep_labeled = False - regular_labeled = False - induced_labeled = False - - # Plot regular (non-induced) mass features - for _, row in cluster_mfs.iterrows(): - sample_id = row['sample_id'] - mf_id = row['mf_id'] - sample = self[sample_id] - sample_name = row['sample_name'] - - # Check if EIC is available for this mass feature - if hasattr(sample, 'eics') and sample.eics and row['mz'] in sample.eics: - eic_data = sample.eics[row['mz']] - - # Determine line style and width - if sample_id == rep_sample_id and mf_id == rep_mf_id: - # Representative feature - bold line - linewidth = 2.5 - alpha = 1.0 - color = 'tab:blue' - if label_samples: - label = f"{sample_name} (representative)" - else: - label = "Representative" if not rep_labeled else None - rep_labeled = True - else: - # Other features - thinner line - linewidth = 1.0 - alpha = 0.5 - color = 'tab:blue' - if label_samples: - label = sample_name - else: - label = "Regular features" if not regular_labeled else None - regular_labeled = True - - axs[i][0].plot( - eic_data.time, - eic_data.eic, - c=color, - linewidth=linewidth, - alpha=alpha, - linestyle='-', - label=label - ) - - if plot_eic_datapoints: - axs[i][0].scatter( - eic_data.time, - eic_data.eic, - c=color, - alpha=alpha, - s=10 - ) - - if plot_smoothed_eic and hasattr(eic_data, 'eic_smoothed'): - axs[i][0].plot( - eic_data.time, - eic_data.eic_smoothed, - c=color, - linestyle='--', - alpha=alpha * 0.8, - linewidth=linewidth * 0.8 - ) - - # Plot induced (gap-filled) mass features if available - if self.induced_mass_features_dataframe is not None: - induced_cluster_mfs = self.induced_mass_features_dataframe[ - self.induced_mass_features_dataframe['cluster'] == cluster_id - ] - - for _, row in induced_cluster_mfs.iterrows(): - sample_id = row['sample_id'] - mf_id = row['mf_id'] - sample = self[sample_id] - sample_name = row['sample_name'] - - # Check if EIC is available for this induced mass feature - if hasattr(sample, 'eics') and sample.eics and row['mz'] in sample.eics: - eic_data = sample.eics[row['mz']] - - # Induced features - even thinner line - linewidth = 0.5 - alpha = 0.4 - color = 'tab:orange' - - if label_samples: - label = f"{sample_name} (induced)" - else: - label = "Gap-filled features" if not induced_labeled else None - induced_labeled = True - - axs[i][0].plot( - eic_data.time, - eic_data.eic, - c=color, - linewidth=linewidth, - alpha=alpha, - linestyle='-', - label=label - ) - - if plot_eic_datapoints: - axs[i][0].scatter( - eic_data.time, - eic_data.eic, - c=color, - alpha=alpha, - s=5 - ) - - if plot_smoothed_eic and hasattr(eic_data, 'eic_smoothed'): - axs[i][0].plot( - eic_data.time, - eic_data.eic_smoothed, - c=color, - linestyle='--', - alpha=alpha * 0.8, - linewidth=linewidth * 0.8 - ) - - # Add vertical line at median RT - axs[i][0].axvline( - x=cluster_summary['scan_time_aligned_median'], - color='k', - linestyle='--', - alpha=0.7, - label='Median RT' - ) - - axs[i][0].set_ylabel("Intensity") - axs[i][0].set_xlabel("Time (minutes)") - axs[i][0].set_xlim( - cluster_summary['scan_time_aligned_median'] - eic_buffer_time, - cluster_summary['scan_time_aligned_median'] + eic_buffer_time, + self._plot_multiple_eics( + axs[i][0], + cluster_mfs, + induced_cluster_mfs, + rep_sample_id, + rep_mf_id, + cluster_summary['scan_time_aligned_median'], + eic_buffer_time, + plot_smoothed=plot_smoothed_eic, + plot_datapoints=plot_eic_datapoints, + label_samples=label_samples, + lcms_collection=self ) - axs[i][0].legend(loc='upper left', fontsize=8) - axs[i][0].yaxis.get_major_formatter().set_useOffset(False) i += 1 - # MS1 plot - from representative + # MS1 plot - from representative using helper method if "MS1" in to_plot: - if deconvoluted: - axs[i][0].set_title( - f"MS1 (deconvoluted) - Representative: {rep_sample.sample_name}", - loc="left" - ) - axs[i][0].vlines( - rep_mf.mass_spectrum.mz_exp, - 0, - rep_mf.mass_spectrum.abundance, - color="k", - alpha=0.2, - label="Raw MS1", - ) - axs[i][0].vlines( - rep_mf.mass_spectrum_deconvoluted.mz_exp, - 0, - rep_mf.mass_spectrum_deconvoluted.abundance, - color="k", - label="Deconvoluted MS1", - ) - axs[i][0].set_xlim( - rep_mf.mass_spectrum_deconvoluted.mz_exp.min() * 0.8, - rep_mf.mass_spectrum_deconvoluted.mz_exp.max() * 1.1, - ) - axs[i][0].set_ylim( - 0, rep_mf.mass_spectrum_deconvoluted.abundance.max() * 1.1 - ) - else: - axs[i][0].set_title( - f"MS1 (raw) - Representative: {rep_sample.sample_name}", - loc="left" - ) - axs[i][0].vlines( - rep_mf.mass_spectrum.mz_exp, - 0, - rep_mf.mass_spectrum.abundance, - color="k", - label="Raw MS1", - ) - axs[i][0].set_xlim( - rep_mf.mass_spectrum.mz_exp.min() * 0.8, - rep_mf.mass_spectrum.mz_exp.max() * 1.1, - ) - axs[i][0].set_ylim(bottom=0) - - # Highlight the feature m/z - if abs(rep_mf.ms1_peak.mz_exp - rep_mf.mz) < 0.01: - axs[i][0].vlines( - rep_mf.ms1_peak.mz_exp, - 0, - rep_mf.ms1_peak.abundance, - color="m", - label="Feature m/z", - ) - - axs[i][0].legend(loc="upper left") - axs[i][0].set_ylabel("Intensity") - axs[i][0].set_xlabel("m/z") - axs[i][0].yaxis.set_tick_params(labelleft=False) + rep_mf._plot_ms1_spectrum( + axs[i][0], + deconvoluted=deconvoluted, + sample_name=rep_sample.sample_name + ) i += 1 - # MS2 plot - from representative + # MS2 plot - from representative using helper method if "MS2" in to_plot: - axs[i][0].set_title( - f"MS2 - Representative: {rep_sample.sample_name}", - loc="left" - ) - axs[i][0].vlines( - rep_mf.best_ms2.mz_exp, - 0, - rep_mf.best_ms2.abundance, - color="k" - ) - axs[i][0].set_ylabel("Intensity") - axs[i][0].set_xlabel("m/z") - axs[i][0].set_ylim(bottom=0) - axs[i][0].yaxis.set_tick_params(labelleft=False) + rep_mf._plot_ms2_spectrum(axs[i][0], sample_name=rep_sample.sample_name) i += 1 plt.tight_layout() @@ -3870,6 +3860,63 @@ def has_ms2_scans(val): return representatives + def get_sample_mf_map_for_representatives(self, representative_metric=None, include_cluster_id=True): + """ + Build a mapping of sample_id -> list of representative mass feature IDs to load. + + This is a DRY helper method used by both process_consensus_features() and + ReadSavedLCMSCollection to determine which mass features should be loaded + for each sample when loading representatives. + + Parameters + ---------- + representative_metric : str, optional + The metric to use to determine the most representative sample. + If None, uses the value from self.parameters.lcms_collection.consensus_representative_metric. + Default is None. + include_cluster_id : bool, optional + If True, returns tuples of (mf_id, cluster_id). If False, returns just mf_id. + Default is True. + + Returns + ------- + dict + Dictionary mapping sample_id (int) to list of mass feature identifiers. + If include_cluster_id=True: list of tuples (mf_id, cluster_id) + If include_cluster_id=False: list of mf_id integers + + Examples + -------- + >>> # Get map with cluster IDs for loading + >>> sample_mf_map = collection.get_sample_mf_map_for_representatives() + >>> # sample_mf_map = {0: [(123, 0), (456, 1)], 1: [(789, 2)], ...} + >>> + >>> # Get map without cluster IDs for pipeline + >>> sample_mf_map = collection.get_sample_mf_map_for_representatives(include_cluster_id=False) + >>> # sample_mf_map = {0: [123, 456], 1: [789], ...} + """ + # Get all representative mass features in bulk (much faster than looping) + representatives = self.get_representative_mass_features_for_all_clusters( + representative_metric=representative_metric + ) + + # Build sample_mf_map + sample_mf_map = {} + for _, row in representatives.iterrows(): + sample_id = row['sample_id'] + mf_id = row['mf_id'] + cluster_id = row['cluster'] + + if sample_id not in sample_mf_map: + sample_mf_map[sample_id] = [] + + if include_cluster_id: + sample_mf_map[sample_id].append((mf_id, cluster_id)) + else: + sample_mf_map[sample_id].append(mf_id) + + return sample_mf_map + def get_most_representative_sample_for_cluster(self, cluster_id, representative_metric=None): """ Get the most representative sample for a given cluster based on a metric. @@ -5014,7 +5061,7 @@ def process_samples_pipeline(self, operations, description=None, keep_raw_data=F sample_results = self._execute_sample_pipeline( sample_id, operations, runtime_params, inplace=True ) - # Collect results + # Collect results (collect_results already called in _execute_sample_pipeline when inplace=True) for op_name, result in sample_results.items(): results_by_operation[op_name][sample_id] = result else: @@ -5127,19 +5174,8 @@ def _prepare_pipeline_runtime_params(self, operations): # Check if any operation needs reload parameters needs_reload = any(isinstance(op, ReloadFeaturesOperation) for op in operations) if needs_reload: - # Build sample_mf_map for reloading representatives - # Get all representative mass features in bulk (much faster than looping) - representatives = self.get_representative_mass_features_for_all_clusters() - - sample_mf_map = {} - for _, row in representatives.iterrows(): - sample_id = row['sample_id'] - mf_id = row['mf_id'] - - if sample_id not in sample_mf_map: - sample_mf_map[sample_id] = [] - sample_mf_map[sample_id].append(mf_id) - + # Use DRY helper method to build sample_mf_map + sample_mf_map = self.get_sample_mf_map_for_representatives(include_cluster_id=False) runtime_params['sample_mf_map'] = sample_mf_map # Check if any operation needs MS2 spectral search parameters @@ -5461,15 +5497,44 @@ def process_consensus_features(self, load_representatives=True, perform_gap_fill if ms2_spectral_search and hasattr(self, '_spectral_search_molecular_metadata'): # This allows users to access the metadata for reporting self.spectral_search_molecular_metadata = self._spectral_search_molecular_metadata - + #TODO KRH: Update mass_features_dataframe with updated mz for representative mass features if ms1 was added? # Post-processing if perform_gap_filling: # Combine induced mass features into dataframe self._combine_mass_features(induced_features=True) # Mark that gap-filling has been performed self.missing_mass_features_searched = True - # Clear induced mass features from individual samples + + # Add ._eic_mz to induced_mass_features_dataframe + eics_mz = [] + for i, row in self.induced_mass_features_dataframe.iterrows(): + sample_id = row['sample_id'] + sample = self[sample_id] + if row['mf_id'] in sample.induced_mass_features.keys(): + eic_mz = sample.induced_mass_features[row['mf_id']]._eic_mz + eics_mz.append(eic_mz) + else: + eics_mz.append(None) + self.induced_mass_features_dataframe['_eic_mz'] = eics_mz + + # Clear mass features from samples to free memory for sample_name in self.samples: self._lcms[sample_name].induced_mass_features = {} + # Associate EICs with mass features if they were loaded + # This must happen after all operations complete to work on the actual sample objects + if gather_eics: + print("\nAssociating EICs with mass features:") + from tqdm import tqdm + + # Dictionary to store EIC m/z values for each feature + eic_mz_map = {} # {coll_mf_id: eic_mz} + + for sample_id in tqdm(range(len(self.samples)), unit="sample", ncols=80): + sample = self[sample_id] + if sample.eics: # Only if EICs were loaded + # Associate EICs with regular mass features + sample.associate_eics_with_mass_features(induced=False) + # Associate EICs with induced mass features + sample.associate_eics_with_mass_features(induced=True) return results \ No newline at end of file diff --git a/corems/mass_spectra/calc/lc_calc_operations.py b/corems/mass_spectra/calc/lc_calc_operations.py index 845a9abe3..add0fbeef 100644 --- a/corems/mass_spectra/calc/lc_calc_operations.py +++ b/corems/mass_spectra/calc/lc_calc_operations.py @@ -1017,15 +1017,11 @@ def execute(self, sample_id, collection, cluster_mz_dict=None, **runtime_params) # Load EICs for each of the sample_cluster_mz hdf5_path = sample.file_location if hdf5_path and hdf5_path.exists(): - try: - reader = ReadCoreMSHDFMassSpectra(str(hdf5_path)) - reader.import_eics(sample, mz_list=list(sample_cluster_mz)) - # Return the loaded EICs for multiprocessing collection - # (modifications in worker process don't persist to main process) - return sample.eics.copy() - except (KeyError, AttributeError): - # No EIC data in HDF5 file for these m/z values - return {} + reader = ReadCoreMSHDFMassSpectra(str(hdf5_path)) + reader.import_eics(sample, mz_list=list(sample_cluster_mz)) + # Return the loaded EICs for multiprocessing collection + # (modifications in worker process don't persist to main process) + return sample.eics.copy() return {} @@ -1049,10 +1045,6 @@ def collect_results(self, sample_id, result, collection): if result: # Update sample.eics with loaded EICs collection[sample_id].eics.update(result) - - # Re-associate EICs with mass features (same logic as import_eics) - sample = collection[sample_id] - for idx in sample.mass_features.keys(): - mz = sample.mass_features[idx].mz - if mz in sample.eics.keys(): - sample.mass_features[idx]._eic_data = sample.eics[mz] + # Note: EIC association with mass features happens after pipeline completes + # to avoid multiprocessing issues (modifications in worker processes don't + # persist to main process objects) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 683d8a516..342d9b74c 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -469,6 +469,74 @@ def __init__( self.induced_mass_features = {} self.spectral_search_results = {} + def get_eic_mz_for_mass_feature(self, mf_mz, tolerance=0.0001): + """Get the EIC dictionary key (m/z) that best matches a mass feature's m/z. + + Finds the closest EIC m/z key within the specified tolerance. + + Parameters + ---------- + mf_mz : float + The m/z value of the mass feature to match. + tolerance : float, optional + Maximum m/z difference for matching. Default is 0.0001 Da. + + Returns + ------- + float or None + The EIC dictionary key (m/z) of the closest matching EIC, + or None if no EIC is within tolerance. + """ + if not hasattr(self, 'eics') or not self.eics: + return None + + best_eic_mz = None + best_diff = tolerance + for eic_mz in self.eics.keys(): + diff = abs(mf_mz - eic_mz) + if diff < best_diff: + best_diff = diff + best_eic_mz = eic_mz + return best_eic_mz + + def associate_eics_with_mass_features(self, tolerance=0.0001, induced=False): + """Associate EICs with mass features using tolerance-based m/z matching. + + Associates EIC_Data objects from self.eics with mass features by finding + the closest EIC within the specified m/z tolerance. This is more robust + than exact matching which can fail due to floating point precision issues. + + Parameters + ---------- + tolerance : float, optional + Maximum m/z difference for matching EICs to mass features. Default is 0.0001 Da. + induced : bool, optional + If True, associates EICs with induced_mass_features instead of mass_features. + Default is False. + + Notes + ----- + For each mass feature, this method finds the EIC with the closest m/z value + within the tolerance window and assigns it to the mass feature's _eic_data attribute. + If multiple EICs are within tolerance, the one with the smallest m/z difference is chosen. + """ + # Select which mass features dictionary to use + mf_dict = self.induced_mass_features if induced else self.mass_features + + # Use the _eic_mz attribute on each mass_feature to find the closest matching EIC + for idx in mf_dict.keys(): + mf_mz = mf_dict[idx]._eic_mz + # Find closest EIC within tolerance + best_match = None + best_diff = tolerance + for eic_mz, eic_data in self.eics.items(): + diff = abs(mf_mz - eic_mz) + if diff < best_diff: + best_diff = diff + best_match = eic_data + if best_match is not None: + mf_dict[idx]._eic_data = best_match + def get_parameters_json(self): """Returns the parameters stored for the LC-MS object in JSON format. @@ -874,7 +942,7 @@ def mass_spectrum_to_string( "isotopologue_type", "mass_spectrum_deconvoluted_parent", "ms2_scan_numbers", - "type", + "type" ] df_mf_list = [] diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 40d1b786f..e406decf8 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -10,6 +10,7 @@ from pathlib import Path import datetime +import numpy as np import pandas as pd import warnings @@ -567,7 +568,7 @@ def import_mass_features(self, mass_spectra, mf_ids=None) -> None: mass_spectra._ms[ms2_scan] ) - def import_eics(self, mass_spectra, mz_list=None): + def import_eics(self, mass_spectra, mz_list=None, mz_tolerance=0.0001): """Imports the extracted ion chromatograms from the HDF5 file. Parameters @@ -576,6 +577,9 @@ def import_eics(self, mass_spectra, mz_list=None): The MassSpectraBase or LCMSBase object to populate with extracted ion chromatograms. mz_list : list of float, optional List of m/z values to load EICs for. If None, loads all EICs. Default is None. + mz_tolerance : float, optional + Tolerance in Daltons for matching m/z values when mz_list is provided. + Default is 0.0001 Da. Returns ------- @@ -585,15 +589,16 @@ def import_eics(self, mass_spectra, mz_list=None): """ dict_group_load = self.h5pydata["eics"] dict_group_keys = dict_group_load.keys() - - # Create a set of target m/z values for fast lookup if filtering - target_mz_set = set(mz_list) if mz_list is not None else None - + + # Prefilter dict_group_keys if mz_list is provided to EICs within tolerance + if mz_list is not None: + target_mz_array = np.array(sorted(mz_list)) + mzs = [float(k) for k in dict_group_keys if np.abs(float(k)-target_mz_array).min() < mz_tolerance] + dict_group_keys = [str(mz) for mz in mzs] + for k in dict_group_keys: # Check if we should load this EIC (filter by m/z if list provided) eic_mz = dict_group_load[k].attrs["mz"] - if target_mz_set is not None and eic_mz not in target_mz_set: - continue # Skip this EIC my_eic = EIC_Data( scans=dict_group_load[k]["scans"][:], @@ -609,11 +614,62 @@ def import_eics(self, mass_spectra, mz_list=None): # Add to mass_spectra object mass_spectra.eics[eic_mz] = my_eic - # Add to mass features - for idx in mass_spectra.mass_features.keys(): - mz = mass_spectra.mass_features[idx].mz - if mz in mass_spectra.eics.keys(): - mass_spectra.mass_features[idx]._eic_data = mass_spectra.eics[mz] + # Associate EICs with mass features using tolerance-based matching + mass_spectra.associate_eics_with_mass_features() + + @staticmethod + def _load_eics_from_hdf5_group(eics_group, lcms_obj, mz_filter=None): + """Load EICs from an HDF5 group. + + This is a static helper method that can be reused to load EIC data + from any HDF5 group in a consistent format. + + Parameters + ---------- + eics_group : h5py.Group + The HDF5 group containing EIC data. + lcms_obj : LCMSBase + The LCMS object to associate EICs with (for reference, not modified). + mz_filter : list, optional + List of m/z values to load. If None, loads all EICs. Default is None. + Uses tolerance-based matching (0.0001). + + Returns + ------- + dict + Dictionary of EIC_Data objects keyed by m/z value. + """ + from corems.mass_spectra.factory.chromat_data import EIC_Data + + loaded_eics = {} + tolerance = 0.0001 + + for eic_key_str in eics_group.keys(): + eic_mz = float(eic_key_str) if eic_key_str.replace('.', '', 1).replace('-', '', 1).isdigit() else eics_group[eic_key_str].attrs.get("mz") + + # If mz_filter is provided, check if this EIC matches any requested m/z + if mz_filter is not None: + if not any(abs(eic_mz - mz) < tolerance for mz in mz_filter): + continue + + eic_data = eics_group[eic_key_str] + + # Create EIC_Data object from datasets + eic = EIC_Data( + scans=list(eic_data["scans"][:]) if "scans" in eic_data else [], + time=list(eic_data["time"][:]) if "time" in eic_data else [], + eic=list(eic_data["eic"][:]) if "eic" in eic_data else [], + apexes=list(eic_data["apexes"][:]) if "apexes" in eic_data else [], + ) + + # Load any additional datasets + for key in eic_data.keys(): + if key not in ["scans", "time", "eic", "apexes"]: + setattr(eic, key, eic_data[key][:]) + + loaded_eics[eic_mz] = eic + + return loaded_eics def import_spectral_search_results(self, mass_spectra): """Imports the spectral search results from the HDF5 file. @@ -1039,6 +1095,14 @@ def get_lcms_obj(self, sample_name: str, load_raw=False, load_light=True, use_or lcms_obj = parser.get_lcms_obj(load_raw=load_raw, load_light=load_light, use_original_parser=use_original_parser, raw_file_path=raw_file_path) if load_light: mf_df = lcms_obj.mass_features_to_df() + # Add ._eic_mz to mf_df for each mass_feature + eic_mz_list = [] + for mf_id, mf in lcms_obj.mass_features.items(): + if hasattr(mf, "_eic_mz"): + eic_mz_list.append(mf._eic_mz) + else: + eic_mz_list.append(None) + mf_df["_eic_mz"] = eic_mz_list lcms_obj.mass_features = {} lcms_obj.light_mf_df = mf_df return lcms_obj @@ -1269,8 +1333,25 @@ def _load_cluster_assignments(self, lcms_collection): # Drop rows with NaN cluster values lcms_collection.mass_features_dataframe.dropna(subset=['cluster'], inplace=True) - def get_lcms_collection(self, load_raw=False, load_light=False): - """Get the LCMS collection from the saved HDF5 file.""" + def get_lcms_collection(self, load_raw=False, load_light=False, load_representatives=False, load_eics=False): + """Get the LCMS collection from the saved HDF5 file. + + Parameters + ---------- + load_raw : bool, optional + If True, load raw data. Default is False. + load_light : bool, optional + If True, load light data (minimal). Default is False. + load_representatives : bool, optional + If True, load representative mass features from clusters. Default is False. + load_eics : bool, optional + If True, load EIC data for clustered mass features. Default is False. + + Returns + ------- + LCMSCollection + The loaded LCMS collection object. + """ # First load the LCMSCollection object exactly as in the parent class lcms_collection = super().get_lcms_collection(load_raw=load_raw, load_light=load_light) @@ -1290,9 +1371,21 @@ def get_lcms_collection(self, load_raw=False, load_light=False): # Load induced mass features if they exist self._load_induced_mass_features(lcms_collection) + # Load EICs for induced mass features from collection HDF5 + if lcms_collection.missing_mass_features_searched: + self._load_induced_eics_from_collection(lcms_collection) + # Combine induced mass features into the collection-level dataframe if any were loaded if lcms_collection.missing_mass_features_searched: lcms_collection._combine_mass_features(induced_features=True) + + # Load representative mass features if requested + if load_representatives: + self._load_representative_mass_features(lcms_collection) + + # Load EICs for clustered features if requested + if load_eics: + self._load_eics_for_clusters(lcms_collection) return lcms_collection @@ -1338,9 +1431,9 @@ def _load_induced_mass_features(self, lcms_collection): for mf_id_str in sample_group.keys(): mf_group = sample_group[mf_id_str] - # The mf_id in HDF5 is stored as a string of the integer ID - # (induced_mass_features dict uses integer keys) - mf_id = int(mf_id_str) + # Note: Induced mass feature IDs are strings like 'c2923_0_i', not integers + # Keep them as strings since that's how they're stored + mf_id = mf_id_str # Instantiate the LCMSMassFeature object with required attributes mass_feature = LCMSMassFeature( @@ -1373,4 +1466,197 @@ def _load_induced_mass_features(self, lcms_collection): # Add to the LCMS object's induced_mass_features dictionary lcms_obj.induced_mass_features[mf_id] = mass_feature + + def _load_induced_eics_from_collection(self, lcms_collection): + """Load EICs for induced mass features from the collection HDF5 file. + + Induced mass features are gap-filled features. Their EICs are saved at the + collection level and need to be loaded and associated with the induced mass features. + + Parameters + ---------- + lcms_collection : LCMSCollection + The LCMS collection object with induced mass features to associate EICs with. + """ + with h5py.File(self.collection_hdf5_path, 'r') as f: + if "induced_eics" not in f: + return + + # Access the top-level induced EICs group + induced_eics_group = f["induced_eics"] + + # Iterate through each sample's induced EICs + for sample_idx in induced_eics_group.keys(): + lcms_obj = lcms_collection[int(sample_idx)] + sample_group = induced_eics_group[sample_idx] + + # Use the static helper to load EICs + loaded_eics = ReadCoreMSHDFMassSpectra._load_eics_from_hdf5_group(sample_group, lcms_obj) + + # Add to lcms_obj.eics dictionary and associate with induced mass features + for eic_mz, eic in loaded_eics.items(): + lcms_obj.eics[eic_mz] = eic + + # Associate EICs with induced mass features using tolerance-based matching + lcms_obj.associate_eics_with_mass_features(induced=True) + + def _load_representative_mass_features(self, lcms_collection): + """Load representative mass features for all clusters from HDF5 files. + + This method uses the same logic as process_consensus_features() when loading + representatives, calling get_sample_mf_map_for_representatives() (DRY helper) + to determine which features to load. + + Parameters + ---------- + lcms_collection : LCMSCollection + The LCMS collection object to populate with representative mass features. + """ + # Get cluster assignments from the mass_features_dataframe + if "cluster" not in lcms_collection.mass_features_dataframe.columns: + return + + # Use DRY helper method to build sample_mf_map with cluster IDs + sample_mf_map = lcms_collection.get_sample_mf_map_for_representatives(include_cluster_id=True) + + # Load mass features for each sample + for sample_id, mf_list in sample_mf_map.items(): + lcms_obj = lcms_collection[sample_id] + + # Load each mass feature + for mf_id, cluster_id in mf_list: + self._load_single_mass_feature(lcms_obj, mf_id, cluster_id) + + def _load_single_mass_feature(self, lcms_obj, feature_id, cluster_index=None): + """Load a single mass feature from an LCMS object's HDF5 file. + + Parameters + ---------- + lcms_obj : LCMSBase + The LCMS object to add the mass feature to. + feature_id : int + The ID of the mass feature to load. + cluster_index : int, optional + The cluster index to assign to the loaded mass feature. + """ + hdf5_path = lcms_obj.file_location.with_suffix('.hdf5') + + if not hdf5_path.exists(): + return + + with h5py.File(hdf5_path, 'r') as f: + if 'mass_features' not in f: + return + + mf_group = f['mass_features'] + feature_id_str = str(feature_id) + + if feature_id_str not in mf_group: + return + + mf_data = mf_group[feature_id_str] + + # Create LCMSMassFeature object + mass_feature = LCMSMassFeature( + lcms_obj, + mz=mf_data.attrs["_mz_exp"], + retention_time=mf_data.attrs["_retention_time"], + intensity=mf_data.attrs["_intensity"], + apex_scan=mf_data.attrs["_apex_scan"], + persistence=mf_data.attrs.get("_persistence", 0), + id=feature_id, + ) + + # Set cluster_index if provided + if cluster_index is not None: + mass_feature.cluster_index = cluster_index + + # Populate additional attributes from HDF5 attributes + for key in mf_data.attrs.keys() - { + "_mz_exp", + "_mz_cal", + "_retention_time", + "_intensity", + "_apex_scan", + "_persistence", + }: + setattr(mass_feature, key, mf_data.attrs[key]) + + # Populate attributes from HDF5 datasets (arrays) + for key in mf_data.keys(): + setattr(mass_feature, key, mf_data[key][:]) + # Convert _noise_score from array to tuple + if key == "_noise_score": + mass_feature._noise_score = tuple(mass_feature._noise_score) + + # Add to the LCMS object's mass_features dictionary + lcms_obj.mass_features[feature_id] = mass_feature + + def _load_eics_for_clusters(self, lcms_collection): + """Load EIC data for loaded representative mass features from individual LCMS HDF5 files. + + This method loads EIC data from individual LCMS object HDF5 files for representative + mass features that were loaded into the mass_features dictionary. EICs for induced + mass features are loaded separately from the collection HDF5 file. + + Parameters + ---------- + lcms_collection : LCMSCollection + The LCMS collection object with loaded representative mass features. + """ + for lcms_obj in lcms_collection: + # Collect m/z values from both representative and induced mass features + # Representative EICs come from individual LCMS HDF5 files + # Induced EICs were already loaded by _load_induced_eics_from_collection + mz_values = [] + feature_ids = [] + + # Add representative mass features + if len(lcms_obj.mass_features) > 0: + mz_values.extend([mf.mz for mf in lcms_obj.mass_features.values()]) + feature_ids.extend(list(lcms_obj.mass_features.keys())) + + # Note: Induced mass features already have their EICs loaded from collection HDF5, + # so we don't need to load them again from individual files + + if mz_values: + # Load EICs for representative features from individual LCMS HDF5 file + self._load_eics_from_hdf5(lcms_obj, mz_values, feature_ids) + + def _load_eics_from_hdf5(self, lcms_obj, mz_values, feature_ids): + """Load EIC data from HDF5 file and associate with mass features. + + This method loads EIC_Data objects from the HDF5 file for the specified m/z values + and associates them with the corresponding mass features using tolerance-based matching. + + Parameters + ---------- + lcms_obj : LCMSBase + The LCMS object to load EICs for. + mz_values : list + List of m/z values to load EICs for. + feature_ids : list + List of feature IDs corresponding to the m/z values. + """ + hdf5_path = lcms_obj.file_location.with_suffix('.hdf5') + + if not hdf5_path.exists(): + return + + with h5py.File(hdf5_path, 'r') as f: + if 'eics' not in f: + return + + eic_group = f['eics'] + + # Use the static helper to load EICs with m/z filtering + loaded_eics = ReadCoreMSHDFMassSpectra._load_eics_from_hdf5_group(eic_group, lcms_obj, mz_filter=mz_values) + + # Add to lcms_obj.eics dictionary and associate with mass features + for eic_mz, eic in loaded_eics.items(): + lcms_obj.eics[eic_mz] = eic + + # Associate EICs with both regular and induced mass features + lcms_obj.associate_eics_with_mass_features(induced=False) + lcms_obj.associate_eics_with_mass_features(induced=True) diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index 548a5617d..b620c612c 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -1121,6 +1121,47 @@ def _save_mass_features_dict_to_hdf5(mass_features_dict, mass_features_group, ov v2 = np.float32(v2) mass_features_group[str(k)].attrs[str(k2)] = v2 + @staticmethod + def _save_eics_dict_to_hdf5(eics_dict, eics_group, overwrite=False): + """Save a dictionary of EICs to an HDF5 group. + + This is a static helper method that can be reused by different export classes + to save EIC data in a consistent format. + + Parameters + ---------- + eics_dict : dict + Dictionary of EIC_Data objects, keyed by m/z value. + eics_group : h5py.Group + The HDF5 group to save the EICs to. + overwrite : bool, optional + Whether to overwrite existing EICs. Default is False. + """ + for mz, eic_data in eics_dict.items(): + mz_str = str(mz) + if mz_str not in eics_group or overwrite: + if mz_str in eics_group and overwrite: + del eics_group[mz_str] + eic_grp = eics_group.create_group(mz_str) + eic_grp.attrs["mz"] = mz + + # Save all EIC_Data attributes as datasets + for attr_name, attr_value in eic_data.__dict__.items(): + if attr_value is not None: + array = np.array(attr_value) + # Apply data type optimization and compression + if array.dtype == np.int64: + array = array.astype(np.int32) + elif array.dtype == np.float64: + array = array.astype(np.float32) + elif array.dtype.str[0:2] == " 0: @@ -2216,9 +2236,15 @@ def export_to_hdf5(self, overwrite = False, save_parameters=True, parameter_form if hasattr(self.mass_spectra_collection, 'raw_files_relocated') and self.mass_spectra_collection.raw_files_relocated: self._update_raw_file_locations_in_hdf5() - # Save induced mass features onto the LCMSBase objects, only if lcms_collection.missing_mass_features_searched is True + # Save induced mass features to the collection with associations to each individual, only if lcms_collection.missing_mass_features_searched is True if self.mass_spectra_collection.missing_mass_features_searched: self._save_induced_mass_features_to_hdf5(overwrite) + # Save EICs for induced mass features at collection level + self._save_induced_eics_to_hdf5(overwrite) + + # Save updated mass features for each LCMS object + # This implements selective update: only loaded features are updated, non-cluster features are preserved + self._save_lcms_objects_to_hdf5(overwrite) # Save collection-level parameters as separate file if save_parameters: @@ -2363,4 +2389,206 @@ def _save_induced_mass_features_to_hdf5(self, overwrite): lcms_obj.induced_mass_features, sample_group, overwrite=overwrite - ) \ No newline at end of file + ) + + def _save_induced_eics_to_hdf5(self, overwrite): + """Save EICs for induced mass features to the collection HDF5 file. + + Induced mass features are gap-filled features created during process_consensus_features. + Their associated EICs need to be saved at the collection level so they can be reloaded. + + Parameters + ---------- + overwrite : bool + If True, overwrites existing induced EICs group. If False, skips if group exists. + """ + # Open the collection HDF5 file to save induced EICs + with h5py.File(self.out_file_path.with_suffix(".hdf5"), "a") as hdf_handle: + group_name = "induced_eics" + + # Check if group exists and handle overwrite logic + if group_name in hdf_handle: + if not overwrite: + return + del hdf_handle[group_name] + + # Create top-level group for induced EICs + induced_eics_group = hdf_handle.create_group(group_name) + + # Iterate through each LCMS object and save EICs for induced mass features + for lcms_idx, lcms_obj in enumerate(self.mass_spectra_collection): + if len(lcms_obj.induced_mass_features) == 0: + continue + + # Collect EICs for induced mass features in this sample + induced_eics = {} + for mf_id, mass_feature in lcms_obj.induced_mass_features.items(): + # Check if this mass feature has an associated EIC + if hasattr(mass_feature, '_eic_data') and mass_feature._eic_data is not None: + # Use the mass feature's mz as the key (EIC_Data doesn't have mz attribute) + eic_mz = mass_feature.mz + induced_eics[eic_mz] = mass_feature._eic_data + + if not induced_eics: + continue + + # Create a subgroup for this sample's induced EICs + sample_group = induced_eics_group.create_group(str(lcms_idx)) + + # Use the static helper method from LCMSExport to save the EICs + LCMSExport._save_eics_dict_to_hdf5(induced_eics, sample_group, overwrite) + + def _regenerate_mass_features_from_dataframe(self, lcms_obj): + """Regenerate induced mass features from the induced_mass_features_dataframe. + + This method creates LCMSMassFeature objects from the induced_mass_features_dataframe + for a specific LCMS object (sample). The regenerated features will be saved to the + individual LCMS HDF5 file. + + Parameters + ---------- + lcms_obj : LCMSBase + The LCMS object to regenerate induced mass features for. + + Returns + ------- + dict + Dictionary of regenerated LCMSMassFeature objects keyed by feature ID. + """ + from corems.mass_spectrum.factory.LC_Class import LCMSMassFeature + + # Get the sample_id for this LCMS object from the collection + sample_id = None + for idx, obj in enumerate(self.mass_spectra_collection): + if obj is lcms_obj: + sample_id = idx + break + + if sample_id is None: + return {} + + # Get the induced_mass_features_dataframe from the collection + induced_df = self.mass_spectra_collection.induced_mass_features_dataframe + + if induced_df is None or induced_df.empty: + return {} + + # Filter to only features for this sample + sample_df = induced_df[induced_df['sample_id'] == sample_id].copy() + + if sample_df.empty: + return {} + + # Regenerate mass features from the dataframe + regenerated_features = {} + for _, row in sample_df.iterrows(): + # Create a new LCMSMassFeature + # Note: Using retention_time parameter, NOT scan_time + mass_feature = LCMSMassFeature( + mz=row['mz'], + retention_time=row['retention_time'], # Use retention_time parameter + intensity=row['intensity'], + apex_scan=row['apex_scan'], + persistence=row.get('persistence', None), + tailing_factor=row.get('tailing_factor', None), + fronting_factor=row.get('fronting_factor', None), + half_height_width=row.get('half_height_width', None), + ) + + # Set the ID + mass_feature.id = int(row['id']) + + # Set cluster_index if present + if 'cluster' in row and pd.notna(row['cluster']): + mass_feature.cluster_index = int(row['cluster']) + + regenerated_features[mass_feature.id] = mass_feature + + return regenerated_features + + def _save_lcms_objects_to_hdf5(self, overwrite): + """Save updated mass features for each LCMS object. + + This method implements a "selective update" strategy for mass features: + - For mass features that were loaded from HDF5 (have cluster_index), we selectively + update them by deleting their old entries and re-saving with new attributes. + - Non-cluster features (not loaded) are never touched/overwritten. + + Note: EICs are NOT saved here. Induced feature EICs are saved at the collection level. + + Parameters + ---------- + overwrite : bool + If True, allows overwriting of existing data. If False, skips if data exists. + """ + for lcms_obj in self.mass_spectra_collection: + hdf5_path = lcms_obj.file_location.with_suffix('.hdf5') + + if not hdf5_path.exists(): + # If HDF5 doesn't exist, we can't do selective update + continue + + # Check if this object has any loaded features (features with cluster_index) + has_loaded_features = any( + hasattr(mf, 'cluster_index') and mf.cluster_index is not None + for mf in lcms_obj.mass_features.values() + ) + + if not has_loaded_features: + # Nothing loaded, nothing to update + continue + + # Perform selective update of mass features + self._selective_update_mass_features(lcms_obj, hdf5_path, overwrite) + + def _selective_update_mass_features(self, lcms_obj, hdf5_path, overwrite): + """Selectively update mass features in HDF5 file. + + This method deletes only the mass features that were loaded (have cluster_index), + then re-saves them with their potentially updated attributes. Non-cluster features + in the HDF5 file are left untouched. + + Parameters + ---------- + lcms_obj : LCMSBase + The LCMS object with mass features to update. + hdf5_path : Path + Path to the HDF5 file. + overwrite : bool + If True, allows overwriting. If False, skips if group exists. + """ + # Collect IDs of loaded features (those with cluster_index) + loaded_feature_ids = [ + mf.id for mf in lcms_obj.mass_features.values() + if hasattr(mf, 'cluster_index') and mf.cluster_index is not None + ] + + if not loaded_feature_ids: + return + + # Open HDF5 file and delete loaded feature IDs, then re-save + with h5py.File(hdf5_path, 'a') as hdf_handle: + if 'mass_features' not in hdf_handle: + return + + mf_group = hdf_handle['mass_features'] + + # Delete loaded features + for feature_id in loaded_feature_ids: + feature_id_str = str(feature_id) + if feature_id_str in mf_group: + del mf_group[feature_id_str] + + # Re-save loaded features with updated attributes + loaded_features = { + mf.id: mf for mf in lcms_obj.mass_features.values() + if mf.id in loaded_feature_ids + } + + if loaded_features: + LCMSExport._save_mass_features_dict_to_hdf5( + loaded_features, + mf_group, + overwrite=overwrite + ) + diff --git a/support_code/nmdc/metabolomics/metabolomics_collection.py b/support_code/nmdc/metabolomics/metabolomics_collection.py index d6d79f3a7..3a527ba4a 100644 --- a/support_code/nmdc/metabolomics/metabolomics_collection.py +++ b/support_code/nmdc/metabolomics/metabolomics_collection.py @@ -11,6 +11,8 @@ from corems.encapsulation.factory.parameters import LCMSParameters from corems.molecular_id.search.database_interfaces import MSPInterface from corems.encapsulation.factory.parameters import hush_output +from corems.mass_spectra.output.export import LCMSCollectionExport + """ Example showing the new pipeline-based sample processing approach. @@ -322,13 +324,13 @@ def process_single_sample(args): # ============================================================================= ncores = 1 reprocess_samples = False # Set to True to reprocess raw data - perform_ms2_search = True # Set to True to perform MS2 spectral library search + perform_ms2_search = False # Set to True to perform MS2 spectral library search # Paths base_path = Path("/Volumes/LaCie/nmdc_data/collection_testing/dev_test/") collection_save_path = base_path / "collection" raw_data_path = base_path / "raw" - processed_folder = base_path / "processed" + processed_folder = base_path / "processed2" msp_file_location = Path("/Users/heal742/LOCAL/05_NMDC/02_MetaMS/metams/test_data/test_lcms_metab_data/20250407_database.msp") new_raw_data_path = raw_data_path # Update raw file paths in collection if moved after the first step @@ -451,40 +453,43 @@ def process_single_sample(args): # Step 8: Save and Export Results # ============================================================================= print("\n=== Exporting LCMS Collection ===") - #exporter = LCMSCollectionExport( - # out_file_path="/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/collection", - # mass_spectra_collection=lcms_collection) - #exporter.export_to_hdf5(overwrite=True, save_parameters=True, parameter_format="toml") - - """ - # Check save and load functionality for LCMSCollection - print("Saving and re-loading LCMS collection to test save/load functionality") - print(f"Before saving: missing_mass_features_searched = {lcms_collection.missing_mass_features_searched}") - + exporter = LCMSCollectionExport( + out_file_path=str(collection_save_path), + mass_spectra_collection=lcms_collection) + exporter.export_to_hdf5(overwrite=True, save_parameters=True, parameter_format="toml") - # Reload the collection + # ============================================================================= + # Test Save/Load Functionality + # ============================================================================= + print("\n" + "="*60) + print("SAVE/LOAD VALIDATION TEST") + print("="*60) + + # Reload + from corems.mass_spectra.input.corems_hdf5 import ReadSavedLCMSCollection reader = ReadSavedLCMSCollection( - collection_hdf5_path="/Volumes/LaCie/nmdc_data/collection_testing/blanchard_lipid/collection.hdf5", + collection_hdf5_path=str(collection_save_path.with_suffix('.hdf5')), cores=ncores) - lcms_collection2 = reader.get_lcms_collection(load_raw=False, load_light=True) - - # Verify parameters, induced features, and missing mass features searched flag upon reload - assert lcms_collection2.parameters.lcms_collection.alignment_acceptance_fraction_improved_threshold == -1, \ - f"Re-loaded threshold parameter should be -1, got {lcms_collection2.parameters.lcms_collection.alignment_acceptance_fraction_improved_threshold}" - assert lcms_collection2.missing_mass_features_searched, "Re-loaded collection should have missing_mass_features_searched=True" - original_induced_count = sum([len(lcms_obj.induced_mass_features) for lcms_obj in lcms_collection]) - reloaded_induced_count = sum([len(lcms_obj.induced_mass_features) for lcms_obj in lcms_collection2]) - assert reloaded_induced_count == original_induced_count, \ - f"Re-loaded induced mass features count mismatch: {reloaded_induced_count} vs {original_induced_count}" - assert reloaded_induced_count > 0, "Collection should have some induced mass features after gap filling" + lcms_collection2 = reader.get_lcms_collection( + load_raw=False, load_light=True, + load_representatives=True, load_eics=True) - # Verify the pivot table outputs are the same before and after save/load - original_pivot = lcms_collection.collection_pivot_table() - reloaded_pivot = lcms_collection2.collection_pivot_table() - pd.testing.assert_frame_equal(original_pivot, reloaded_pivot, check_dtype=False) - - print('Test completed successfully! LCMSCollection save and load functionality works as expected.') + # Check that len of mass features matches + assert len(lcms_collection.mass_features_dataframe) == len(lcms_collection2.mass_features_dataframe), \ + "Mass feature count mismatch after reload!" + print("✓ Mass feature count matches after reload") + # Check that the len of induced mass features matches + assert len(lcms_collection.induced_mass_features_dataframe) == len(lcms_collection2.induced_mass_features_dataframe), \ + "Induced mass feature count mismatch after reload!" + print("✓ Induced mass feature count matches after reload") + # Check that the len of loaded EICs matches + total_eics_1 = sum(len(lcms_obj.eics) for lcms_obj in lcms_collection) + total_eics_2 = sum(len(lcms_obj.eics) for lcms_obj in lcms_collection2) + assert total_eics_1 == total_eics_2, \ + "Total loaded EIC count mismatch after reload!" + print("✓ Total loaded EIC count matches after reload") + """ # Make some more plots lcms_collection.plot_mz_features_across_samples() lcms_collection.plot_mz_features_per_cluster() From 37eca90ec463872c927d16c60d1dad681cd1e649 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 26 Jan 2026 15:27:51 -0800 Subject: [PATCH 113/158] Save / reload EICs complete --- corems/mass_spectra/calc/lc_calc.py | 30 +++- corems/mass_spectra/input/corems_hdf5.py | 101 +++-------- corems/mass_spectra/output/export.py | 161 ++++++++++++------ .../metabolomics/metabolomics_collection.py | 34 ++-- 4 files changed, 178 insertions(+), 148 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 6eba9dc1c..8f9b5660c 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -4969,7 +4969,7 @@ def fill_missing_cluster_features(self): for sample_name in self.samples: self._lcms[sample_name].mass_features = {} - def process_samples_pipeline(self, operations, description=None, keep_raw_data=False): + def process_samples_pipeline(self, operations, description=None, keep_raw_data=False, show_progress=True): """ Execute a pipeline of operations on all samples in parallel. @@ -4990,6 +4990,9 @@ def process_samples_pipeline(self, operations, description=None, keep_raw_data=F keep_raw_data : bool, optional If True, keeps raw MS data loaded in memory after pipeline completes. If False, cleans up raw data to free memory. Default is False. + show_progress : bool, optional + If True, displays progress bars during processing. If False, runs silently. + Default is True. Returns ------- @@ -5052,12 +5055,17 @@ def process_samples_pipeline(self, operations, description=None, keep_raw_data=F if self.parameters.lcms_collection.cores == 1: # Serial processing - from tqdm import tqdm results_by_operation = {op.name: {} for op in operations} - # Print description on its own line before progress bar - print(f"\n{description.capitalize()}:") - for sample_id in tqdm(range(sample_ct), unit="sample", ncols=80): + if show_progress: + from tqdm import tqdm + # Print description on its own line before progress bar + print(f"\n{description.capitalize()}:") + iterator = tqdm(range(sample_ct), unit="sample", ncols=80) + else: + iterator = range(sample_ct) + + for sample_id in iterator: sample_results = self._execute_sample_pipeline( sample_id, operations, runtime_params, inplace=True ) @@ -5067,7 +5075,6 @@ def process_samples_pipeline(self, operations, description=None, keep_raw_data=F else: # Parallel processing import multiprocessing - from tqdm import tqdm if self.parameters.lcms_collection.cores > sample_ct: ncores = sample_ct @@ -5089,8 +5096,15 @@ def process_samples_pipeline(self, operations, description=None, keep_raw_data=F # Collect results back into collection results_by_operation = {op.name: {} for op in operations} - print(f"\nCollecting {description} results:") - for sample_id in tqdm(range(sample_ct), unit="sample", ncols=80): + + if show_progress: + from tqdm import tqdm + print(f"\nCollecting {description} results:") + iterator = tqdm(range(sample_ct), unit="sample", ncols=80) + else: + iterator = range(sample_ct) + + for sample_id in iterator: sample_results = mp_results[sample_id] # Let each operation collect its results diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index e406decf8..4b95ace89 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -1372,7 +1372,7 @@ def get_lcms_collection(self, load_raw=False, load_light=False, load_representat self._load_induced_mass_features(lcms_collection) # Load EICs for induced mass features from collection HDF5 - if lcms_collection.missing_mass_features_searched: + if lcms_collection.missing_mass_features_searched and load_eics: self._load_induced_eics_from_collection(lcms_collection) # Combine induced mass features into the collection-level dataframe if any were loaded @@ -1385,7 +1385,20 @@ def get_lcms_collection(self, load_raw=False, load_light=False, load_representat # Load EICs for clustered features if requested if load_eics: - self._load_eics_for_clusters(lcms_collection) + # Reuse the existing LoadEICsOperation from the pipeline system + from corems.mass_spectra.calc.lc_calc_operations import LoadEICsOperation + + operations = [LoadEICsOperation('load_eics')] + lcms_collection.process_samples_pipeline(operations, keep_raw_data=False, show_progress=False) + + # Associate EICs with mass features (same as in process_consensus_features) + for sample_id in range(len(lcms_collection.samples)): + sample = lcms_collection[sample_id] + if sample.eics: # Only if EICs were loaded + # Associate EICs with regular mass features + sample.associate_eics_with_mass_features(induced=False) + # Associate EICs with induced mass features + sample.associate_eics_with_mass_features(induced=True) return lcms_collection @@ -1493,12 +1506,19 @@ def _load_induced_eics_from_collection(self, lcms_collection): # Use the static helper to load EICs loaded_eics = ReadCoreMSHDFMassSpectra._load_eics_from_hdf5_group(sample_group, lcms_obj) - # Add to lcms_obj.eics dictionary and associate with induced mass features + # Ensure eics dictionary exists (should already be initialized in __init__) + if not hasattr(lcms_obj, 'eics') or lcms_obj.eics is None: + lcms_obj.eics = {} + + # Add to lcms_obj.eics dictionary for eic_mz, eic in loaded_eics.items(): lcms_obj.eics[eic_mz] = eic - - # Associate EICs with induced mass features using tolerance-based matching - lcms_obj.associate_eics_with_mass_features(induced=True) + + # Associate EICs with induced mass features after all samples processed + # This is done outside the loop to handle all samples at once + for lcms_obj in lcms_collection: + if len(lcms_obj.induced_mass_features) > 0: + lcms_obj.associate_eics_with_mass_features(induced=True) def _load_representative_mass_features(self, lcms_collection): """Load representative mass features for all clusters from HDF5 files. @@ -1591,72 +1611,3 @@ def _load_single_mass_feature(self, lcms_obj, feature_id, cluster_index=None): # Add to the LCMS object's mass_features dictionary lcms_obj.mass_features[feature_id] = mass_feature - - def _load_eics_for_clusters(self, lcms_collection): - """Load EIC data for loaded representative mass features from individual LCMS HDF5 files. - - This method loads EIC data from individual LCMS object HDF5 files for representative - mass features that were loaded into the mass_features dictionary. EICs for induced - mass features are loaded separately from the collection HDF5 file. - - Parameters - ---------- - lcms_collection : LCMSCollection - The LCMS collection object with loaded representative mass features. - """ - for lcms_obj in lcms_collection: - # Collect m/z values from both representative and induced mass features - # Representative EICs come from individual LCMS HDF5 files - # Induced EICs were already loaded by _load_induced_eics_from_collection - mz_values = [] - feature_ids = [] - - # Add representative mass features - if len(lcms_obj.mass_features) > 0: - mz_values.extend([mf.mz for mf in lcms_obj.mass_features.values()]) - feature_ids.extend(list(lcms_obj.mass_features.keys())) - - # Note: Induced mass features already have their EICs loaded from collection HDF5, - # so we don't need to load them again from individual files - - if mz_values: - # Load EICs for representative features from individual LCMS HDF5 file - self._load_eics_from_hdf5(lcms_obj, mz_values, feature_ids) - - def _load_eics_from_hdf5(self, lcms_obj, mz_values, feature_ids): - """Load EIC data from HDF5 file and associate with mass features. - - This method loads EIC_Data objects from the HDF5 file for the specified m/z values - and associates them with the corresponding mass features using tolerance-based matching. - - Parameters - ---------- - lcms_obj : LCMSBase - The LCMS object to load EICs for. - mz_values : list - List of m/z values to load EICs for. - feature_ids : list - List of feature IDs corresponding to the m/z values. - """ - hdf5_path = lcms_obj.file_location.with_suffix('.hdf5') - - if not hdf5_path.exists(): - return - - with h5py.File(hdf5_path, 'r') as f: - if 'eics' not in f: - return - - eic_group = f['eics'] - - # Use the static helper to load EICs with m/z filtering - loaded_eics = ReadCoreMSHDFMassSpectra._load_eics_from_hdf5_group(eic_group, lcms_obj, mz_filter=mz_values) - - # Add to lcms_obj.eics dictionary and associate with mass features - for eic_mz, eic in loaded_eics.items(): - lcms_obj.eics[eic_mz] = eic - - # Associate EICs with both regular and induced mass features - lcms_obj.associate_eics_with_mass_features(induced=False) - lcms_obj.associate_eics_with_mass_features(induced=True) - diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index b620c612c..6fabeb639 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -2358,11 +2358,19 @@ def _save_induced_mass_features_to_hdf5(self, overwrite): They are saved with full detail (all attributes and datasets) in the collection HDF5 file and distributed to individual LCMS objects when the collection is loaded. + The induced mass features are stored in the collection's induced_mass_features_dataframe + and are regenerated as LCMSMassFeature objects for saving. + Parameters ---------- overwrite : bool If True, overwrites existing induced mass features group. If False, skips if group exists. """ + # Check if we have any induced mass features to save + if (self.mass_spectra_collection.induced_mass_features_dataframe is None or + self.mass_spectra_collection.induced_mass_features_dataframe.empty): + return + # Open the collection HDF5 file to save induced mass features with h5py.File(self.out_file_path.with_suffix(".hdf5"), "a") as hdf_handle: group_name = "induced_mass_features" @@ -2376,17 +2384,34 @@ def _save_induced_mass_features_to_hdf5(self, overwrite): # Create top-level group for induced mass features imf_group = hdf_handle.create_group(group_name) - # Iterate through each LCMS object and save its induced mass features - for lcms_idx, lcms_obj in enumerate(self.mass_spectra_collection): - if len(lcms_obj.induced_mass_features) == 0: + # Get the induced mass features dataframe + induced_df = self.mass_spectra_collection.induced_mass_features_dataframe + + # Get unique sample IDs from the dataframe + sample_ids = induced_df['sample_id'].unique() + + # Iterate through each sample and save its induced mass features + for sample_id in sample_ids: + # Filter dataframe to this sample + sample_df = induced_df[induced_df['sample_id'] == sample_id].copy() + + if sample_df.empty: + continue + + # Regenerate mass features from the dataframe + regenerated_features = self._regenerate_mass_features_from_sample_df( + sample_df, sample_id + ) + + if not regenerated_features: continue # Create a subgroup for this sample's induced mass features - sample_group = imf_group.create_group(str(lcms_idx)) + sample_group = imf_group.create_group(str(sample_id)) # Use the static helper method from LCMSExport to save the mass features LCMSExport._save_mass_features_dict_to_hdf5( - lcms_obj.induced_mass_features, + regenerated_features, sample_group, overwrite=overwrite ) @@ -2397,11 +2422,19 @@ def _save_induced_eics_to_hdf5(self, overwrite): Induced mass features are gap-filled features created during process_consensus_features. Their associated EICs need to be saved at the collection level so they can be reloaded. + The induced mass features are identified from the collection's induced_mass_features_dataframe, + and their EICs are retrieved from the individual LCMS objects. + Parameters ---------- overwrite : bool If True, overwrites existing induced EICs group. If False, skips if group exists. """ + # Check if we have any induced mass features to save + if (self.mass_spectra_collection.induced_mass_features_dataframe is None or + self.mass_spectra_collection.induced_mass_features_dataframe.empty): + return + # Open the collection HDF5 file to save induced EICs with h5py.File(self.out_file_path.with_suffix(".hdf5"), "a") as hdf_handle: group_name = "induced_eics" @@ -2415,88 +2448,110 @@ def _save_induced_eics_to_hdf5(self, overwrite): # Create top-level group for induced EICs induced_eics_group = hdf_handle.create_group(group_name) - # Iterate through each LCMS object and save EICs for induced mass features - for lcms_idx, lcms_obj in enumerate(self.mass_spectra_collection): - if len(lcms_obj.induced_mass_features) == 0: + # Get the induced mass features dataframe + induced_df = self.mass_spectra_collection.induced_mass_features_dataframe + + # Get unique sample IDs from the dataframe + sample_ids = induced_df['sample_id'].unique() + + # Iterate through each sample and save EICs for its induced mass features + for sample_id in sample_ids: + lcms_obj = self.mass_spectra_collection[sample_id] + + # Filter dataframe to this sample + sample_df = induced_df[induced_df['sample_id'] == sample_id].copy() + + if sample_df.empty: continue - # Collect EICs for induced mass features in this sample + # Collect EICs for induced mass features using _eic_mz from dataframe induced_eics = {} - for mf_id, mass_feature in lcms_obj.induced_mass_features.items(): - # Check if this mass feature has an associated EIC - if hasattr(mass_feature, '_eic_data') and mass_feature._eic_data is not None: - # Use the mass feature's mz as the key (EIC_Data doesn't have mz attribute) - eic_mz = mass_feature.mz - induced_eics[eic_mz] = mass_feature._eic_data + for _, row in sample_df.iterrows(): + # Get the EIC m/z from the dataframe + eic_mz = row.get('_eic_mz') + + if eic_mz is not None and pd.notna(eic_mz): + # Try to get the EIC from the LCMS object + if hasattr(lcms_obj, 'eics') and lcms_obj.eics and eic_mz in lcms_obj.eics: + induced_eics[eic_mz] = lcms_obj.eics[eic_mz] if not induced_eics: continue # Create a subgroup for this sample's induced EICs - sample_group = induced_eics_group.create_group(str(lcms_idx)) + sample_group = induced_eics_group.create_group(str(sample_id)) # Use the static helper method from LCMSExport to save the EICs LCMSExport._save_eics_dict_to_hdf5(induced_eics, sample_group, overwrite) - def _regenerate_mass_features_from_dataframe(self, lcms_obj): - """Regenerate induced mass features from the induced_mass_features_dataframe. + def _regenerate_mass_features_from_sample_df(self, sample_df, sample_id): + """Regenerate induced mass features from a sample-specific dataframe. - This method creates LCMSMassFeature objects from the induced_mass_features_dataframe - for a specific LCMS object (sample). The regenerated features will be saved to the - individual LCMS HDF5 file. + This method creates LCMSMassFeature objects from rows in the induced_mass_features_dataframe + for a specific sample. The regenerated features are used for saving to HDF5. Parameters ---------- - lcms_obj : LCMSBase - The LCMS object to regenerate induced mass features for. + sample_df : pd.DataFrame + DataFrame containing induced mass features for a specific sample. + sample_id : int + The sample ID (index in the collection). Returns ------- dict Dictionary of regenerated LCMSMassFeature objects keyed by feature ID. """ - from corems.mass_spectrum.factory.LC_Class import LCMSMassFeature - - # Get the sample_id for this LCMS object from the collection - sample_id = None - for idx, obj in enumerate(self.mass_spectra_collection): - if obj is lcms_obj: - sample_id = idx - break - - if sample_id is None: - return {} - - # Get the induced_mass_features_dataframe from the collection - induced_df = self.mass_spectra_collection.induced_mass_features_dataframe - - if induced_df is None or induced_df.empty: - return {} - - # Filter to only features for this sample - sample_df = induced_df[induced_df['sample_id'] == sample_id].copy() + from corems.chroma_peak.factory.chroma_peak_classes import LCMSMassFeature if sample_df.empty: return {} + # Get the corresponding LCMS object for proper parent reference + lcms_obj = self.mass_spectra_collection[sample_id] + # Regenerate mass features from the dataframe regenerated_features = {} + for _, row in sample_df.iterrows(): - # Create a new LCMSMassFeature - # Note: Using retention_time parameter, NOT scan_time + # Extract the original ID from mf_id (format: c{cluster}_{index}_i) + # This is the ID used in lcms_obj.induced_mass_features dict + original_id = row['mf_id'] + + # Create a new LCMSMassFeature with proper parent reference + # Note: dataframe uses 'scan_time' but __init__ parameter is 'retention_time' mass_feature = LCMSMassFeature( + lcms_parent=lcms_obj, mz=row['mz'], - retention_time=row['retention_time'], # Use retention_time parameter + retention_time=row['scan_time'], # Column is 'scan_time' in dataframe intensity=row['intensity'], - apex_scan=row['apex_scan'], - persistence=row.get('persistence', None), - tailing_factor=row.get('tailing_factor', None), - fronting_factor=row.get('fronting_factor', None), - half_height_width=row.get('half_height_width', None), + apex_scan=int(row['apex_scan']), + persistence=row.get('persistence', None) if 'persistence' in row else None, + id=original_id # Use the original string ID from gap-filling ) - # Set the ID - mass_feature.id = int(row['id']) + # Set additional attributes dynamically from dataframe columns + # Skip columns already handled in __init__ or structural metadata + skip_cols = { + 'sample_id', 'mf_id', 'mz', 'scan_time', 'scan_time_aligned', + 'intensity', 'apex_scan', 'persistence'} + + # Iterate through all columns and set via property setters + for col_name in row.index: + if col_name in skip_cols or pd.isna(row[col_name]): + continue + + # Convert value to appropriate type + value = row[col_name] + + # Set via property (public interface handles private attributes) + # Don't save empty lists + if isinstance(value, list) and len(value) == 0: + continue + try: + setattr(mass_feature, col_name, value) + except (AttributeError, TypeError): + pass # Skip attributes that don't exist or can't be set # Set cluster_index if present if 'cluster' in row and pd.notna(row['cluster']): diff --git a/support_code/nmdc/metabolomics/metabolomics_collection.py b/support_code/nmdc/metabolomics/metabolomics_collection.py index 3a527ba4a..49bd1c692 100644 --- a/support_code/nmdc/metabolomics/metabolomics_collection.py +++ b/support_code/nmdc/metabolomics/metabolomics_collection.py @@ -415,8 +415,8 @@ def process_single_sample(args): pipeline_results = lcms_collection.process_consensus_features( load_representatives=True, perform_gap_filling=True, - add_ms1=True, - add_ms2=True, + add_ms1=False, + add_ms2=False, molecular_formula_search=False, ms2_spectral_search=False, spectral_lib=spectral_lib, @@ -475,20 +475,30 @@ def process_single_sample(args): load_representatives=True, load_eics=True) # Check that len of mass features matches - assert len(lcms_collection.mass_features_dataframe) == len(lcms_collection2.mass_features_dataframe), \ - "Mass feature count mismatch after reload!" - print("✓ Mass feature count matches after reload") + mf_count_1 = len(lcms_collection.mass_features_dataframe) + mf_count_2 = len(lcms_collection2.mass_features_dataframe) + if mf_count_1 == mf_count_2: + print(f"✓ Mass feature count matches after reload: {mf_count_1}") + else: + print(f"✗ Mass feature count mismatch after reload! Original: {mf_count_1}, Reloaded: {mf_count_2}") + # Check that the len of induced mass features matches - assert len(lcms_collection.induced_mass_features_dataframe) == len(lcms_collection2.induced_mass_features_dataframe), \ - "Induced mass feature count mismatch after reload!" - print("✓ Induced mass feature count matches after reload") + induced_count_1 = len(lcms_collection.induced_mass_features_dataframe) + induced_count_2 = len(lcms_collection2.induced_mass_features_dataframe) + if induced_count_1 == induced_count_2: + print(f"✓ Induced mass feature count matches after reload: {induced_count_1}") + else: + print(f"✗ Induced mass feature count mismatch after reload! Original: {induced_count_1}, Reloaded: {induced_count_2}") + # Check that the len of loaded EICs matches total_eics_1 = sum(len(lcms_obj.eics) for lcms_obj in lcms_collection) total_eics_2 = sum(len(lcms_obj.eics) for lcms_obj in lcms_collection2) - assert total_eics_1 == total_eics_2, \ - "Total loaded EIC count mismatch after reload!" - print("✓ Total loaded EIC count matches after reload") - + if total_eics_1 == total_eics_2: + print(f"✓ Total loaded EIC count matches after reload: {total_eics_1}") + else: + print(f"✗ Total loaded EIC count mismatch after reload! Original: {total_eics_1}, Reloaded: {total_eics_2}") + + """ # Make some more plots lcms_collection.plot_mz_features_across_samples() From b9933c225e0e312889b0ed7ac2c83f73eb54a6f8 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 26 Jan 2026 21:26:19 -0800 Subject: [PATCH 114/158] Add functionality for saving mass spectra to mass features when saving collection --- corems/mass_spectra/calc/lc_calc.py | 8 +- .../mass_spectra/calc/lc_calc_operations.py | 1 - corems/mass_spectra/factory/lc_class.py | 1 + corems/mass_spectra/input/corems_hdf5.py | 45 +++-- corems/mass_spectra/output/export.py | 175 ++++++++++++++---- .../metabolomics/metabolomics_collection.py | 44 ++++- 6 files changed, 224 insertions(+), 50 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 8f9b5660c..67c94a8b1 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -3061,11 +3061,15 @@ def align_lcms_objects(self, overwrite=False): # Retrieve the new aligned times for all scans in the LCMS object new_times = [x for k, x in sorted(self[i]._scan_info["scan_time_aligned"].items())] - # Switch the rt_aligned flag to True + # Switch the rt_aligned flag to True and attempted to True self.rt_aligned = True + self.rt_alignment_attempted = True else: # Set aligned retention times on scan_df for lc_obj using the original retention times self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"] + # Switch the rt_attempted flag to True + self.rt_aligned = False + self.rt_alignment_attempted = True i += index_step if i >= len(self) or i < 0: @@ -5214,7 +5218,7 @@ def _prepare_pipeline_runtime_params(self, operations): # Group by sample_id and collect all m/z values for sample_id in clustered_mf['sample_id'].unique(): sample_df = clustered_mf[clustered_mf['sample_id'] == sample_id] - cluster_mz_dict[sample_id] = sample_df['mz'].unique().tolist() + cluster_mz_dict[sample_id] = sample_df['_eic_mz'].unique().tolist() runtime_params['cluster_mz_dict'] = cluster_mz_dict diff --git a/corems/mass_spectra/calc/lc_calc_operations.py b/corems/mass_spectra/calc/lc_calc_operations.py index add0fbeef..f6c4738ed 100644 --- a/corems/mass_spectra/calc/lc_calc_operations.py +++ b/corems/mass_spectra/calc/lc_calc_operations.py @@ -1005,7 +1005,6 @@ def execute(self, sample_id, collection, cluster_mz_dict=None, **runtime_params) from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectra sample = collection[sample_id] - sample_name = collection.samples[sample_id] # If no cluster info provided or no m/z values for this sample, return early if cluster_mz_dict is None or sample_id not in cluster_mz_dict: diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 342d9b74c..1e2745a2e 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1701,6 +1701,7 @@ def __init__( # These attributes are set during processing self.rt_aligned = False + self.rt_alignment_attempted = False self.missing_mass_features_searched = False def _reorder_lcms_objects(self): diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 4b95ace89..4073e5aca 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -1300,17 +1300,28 @@ def convert_back_to_bool(data): def _load_rt_alignments(self, lcms_collection): """Load retention time alignments from the saved collection HDF5 file.""" + #TODO KRH: START HERE - test this function + # First Set the rt_aligned flag from the collection-level attribute saved directly with h5py.File(self.collection_hdf5_path, 'r') as f: - if "rt_alignments" in f: - # Set the lcms_collection - lcms_collection.rt_aligned = True - # Iterate over the group `rt_alignments` containing datasets and add to the corresponding lcms object - rt_alignments_group = f["rt_alignments"] - for sample_idx, lcms_obj in zip(rt_alignments_group.keys(), lcms_collection): - alignment_data = rt_alignments_group[sample_idx][:] - scan_df = lcms_obj.scan_df - scan_df["scan_time_aligned"] = alignment_data - lcms_obj.scan_df = scan_df + lcms_collection.rt_aligned = f.attrs.get('rt_aligned', False) + lcms_collection.rt_alignment_attempted = f.attrs.get('rt_alignment_attempted', False) + + if lcms_collection.rt_aligned: + with h5py.File(self.collection_hdf5_path, 'r') as f: + if "rt_alignments" in f: + # Iterate over the group `rt_alignments` containing datasets and add to the corresponding lcms object + rt_alignments_group = f["rt_alignments"] + for sample_idx, lcms_obj in zip(rt_alignments_group.keys(), lcms_collection): + alignment_data = rt_alignments_group[sample_idx][:] + scan_df = lcms_obj.scan_df + scan_df["scan_time_aligned"] = alignment_data + lcms_obj.scan_df = scan_df + elif lcms_collection.rt_alignment_attempted: + # This means it was attempted and not used, so we populate the "scan_time_aligned" + for lcms_obj in lcms_collection: + scan_df = lcms_obj.scan_df + scan_df["scan_time_aligned"] = scan_df["scan_time"] + lcms_obj.scan_df = scan_df def _load_cluster_assignments(self, lcms_collection): """Load cluster assignments from the saved collection HDF5 file.""" @@ -1333,7 +1344,7 @@ def _load_cluster_assignments(self, lcms_collection): # Drop rows with NaN cluster values lcms_collection.mass_features_dataframe.dropna(subset=['cluster'], inplace=True) - def get_lcms_collection(self, load_raw=False, load_light=False, load_representatives=False, load_eics=False): + def get_lcms_collection(self, load_raw=False, load_light=False, load_representatives=False, load_eics=False, load_ms1=False, load_ms2=False): """Get the LCMS collection from the saved HDF5 file. Parameters @@ -1346,6 +1357,10 @@ def get_lcms_collection(self, load_raw=False, load_light=False, load_representat If True, load representative mass features from clusters. Default is False. load_eics : bool, optional If True, load EIC data for clustered mass features. Default is False. + load_ms1 : bool, optional + If True, load MS1 spectra for loaded mass features. Default is False. + load_ms2 : bool, optional + If True, load MS2 spectra for loaded mass features. Default is False. Returns ------- @@ -1383,6 +1398,14 @@ def get_lcms_collection(self, load_raw=False, load_light=False, load_representat if load_representatives: self._load_representative_mass_features(lcms_collection) + # Load MS1 and/or MS2 spectra for loaded mass features if requested + if load_ms1 or load_ms2: + # Reuse the existing ReloadFeaturesOperation from the pipeline system + from corems.mass_spectra.calc.lc_calc_operations import ReloadFeaturesOperation + + operations = [ReloadFeaturesOperation('reload_spectra', add_ms1=load_ms1, add_ms2=load_ms2)] + lcms_collection.process_samples_pipeline(operations, keep_raw_data=False, show_progress=False) + # Load EICs for clustered features if requested if load_eics: # Reuse the existing LoadEICsOperation from the pipeline system diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index 6fabeb639..35baae664 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -2222,6 +2222,8 @@ def export_to_hdf5(self, overwrite = False, save_parameters=True, parameter_form hdf_handle.attrs["date_utc"] = timenow hdf_handle.attrs["lcms_objects_folder"] = str(self.mass_spectra_collection.collection_parser.folder_location) hdf_handle.attrs["missing_mass_features_searched"] = self.mass_spectra_collection.missing_mass_features_searched + hdf_handle.attrs["rt_aligned"] = self.mass_spectra_collection.rt_aligned + hdf_handle.attrs["rt_alignment_attempted"] = self.mass_spectra_collection.rt_alignment_attempted # Add the manifest to the HDF5 file, always overwrite this hdf_handle.attrs["manifest"] = self._convert_manifest_to_json() @@ -2242,9 +2244,13 @@ def export_to_hdf5(self, overwrite = False, save_parameters=True, parameter_form # Save EICs for induced mass features at collection level self._save_induced_eics_to_hdf5(overwrite) + # Build cluster mass feature map to know which features to update + # This uses the same logic as process_consensus_features to determine loaded features + cluster_mf_map = self._build_cluster_mf_map() + # Save updated mass features for each LCMS object # This implements selective update: only loaded features are updated, non-cluster features are preserved - self._save_lcms_objects_to_hdf5(overwrite) + self._save_lcms_objects_to_hdf5(cluster_mf_map, overwrite) # Save collection-level parameters as separate file if save_parameters: @@ -2332,6 +2338,41 @@ def _save_cluster_assignments_to_hdf5(self, hdf_handle, overwrite): # Save the "cluster" column grp.create_dataset("cluster", data=cluster_assignments["cluster"].values) + def _build_cluster_mf_map(self): + """Build a mapping of which mass features should be saved for each sample. + + This uses the same logic as process_consensus_features to determine which + mass features were loaded and should be updated in HDF5 files. + + Returns + ------- + dict + Dictionary mapping sample_id to list of tuples (mf_id, cluster_id). + Only includes samples that have loaded representative features. + Returns empty dict if no clusters exist. + + Notes + ----- + This follows the DRY principle by using the same get_sample_mf_map_for_representatives + method used by process_consensus_features and ReadSavedLCMSCollection. + """ + # Check if clusters exist + if "cluster" not in self.mass_spectra_collection.mass_features_dataframe.columns: + return {} + + # Check if cluster_summary_dataframe exists (needed by get_sample_mf_map_for_representatives) + if not hasattr(self.mass_spectra_collection, 'cluster_summary_dataframe') or \ + self.mass_spectra_collection.cluster_summary_dataframe is None: + return {} + + # Use the same DRY helper method that process_consensus_features uses + # This ensures consistency across the codebase + cluster_mf_map = self.mass_spectra_collection.get_sample_mf_map_for_representatives( + include_cluster_id=True + ) + + return cluster_mf_map + def _update_raw_file_locations_in_hdf5(self): """Update raw file locations in each LCMS object's HDF5 file. @@ -2561,11 +2602,11 @@ def _regenerate_mass_features_from_sample_df(self, sample_df, sample_id): return regenerated_features - def _save_lcms_objects_to_hdf5(self, overwrite): + def _save_lcms_objects_to_hdf5(self, cluster_mf_map, overwrite): """Save updated mass features for each LCMS object. This method implements a "selective update" strategy for mass features: - - For mass features that were loaded from HDF5 (have cluster_index), we selectively + - For mass features specified in cluster_mf_map (loaded representatives), we selectively update them by deleting their old entries and re-saving with new attributes. - Non-cluster features (not loaded) are never touched/overwritten. @@ -2573,33 +2614,106 @@ def _save_lcms_objects_to_hdf5(self, overwrite): Parameters ---------- + cluster_mf_map : dict + Dictionary mapping sample_id to list of tuples (mf_id, cluster_id). + This explicitly defines which mass features should be updated. overwrite : bool If True, allows overwriting of existing data. If False, skips if data exists. """ - for lcms_obj in self.mass_spectra_collection: + for sample_id, lcms_obj in enumerate(self.mass_spectra_collection): hdf5_path = lcms_obj.file_location.with_suffix('.hdf5') if not hdf5_path.exists(): - # If HDF5 doesn't exist, we can't do selective update - continue - - # Check if this object has any loaded features (features with cluster_index) - has_loaded_features = any( - hasattr(mf, 'cluster_index') and mf.cluster_index is not None - for mf in lcms_obj.mass_features.values() - ) + # If HDF5 doesn't exist, we can't do selective update, raise error + raise FileNotFoundError( + f"HDF5 file for LCMS object {lcms_obj.sample_name} not found at {hdf5_path}" + ) - if not has_loaded_features: - # Nothing loaded, nothing to update + # Check if this sample has any loaded features in the map + if sample_id not in cluster_mf_map or not cluster_mf_map[sample_id]: + # Nothing loaded for this sample, nothing to update continue + # Extract mf_ids from the map (cluster_mf_map contains tuples of (mf_id, cluster_id)) + mf_ids_to_update = [mf_id for mf_id, cluster_id in cluster_mf_map[sample_id]] + # Perform selective update of mass features - self._selective_update_mass_features(lcms_obj, hdf5_path, overwrite) + self._selective_update_mass_features(lcms_obj, hdf5_path, mf_ids_to_update, overwrite) + + # Save any new mass spectra that were added during processing + self._save_new_mass_spectra(lcms_obj, hdf5_path, overwrite) - def _selective_update_mass_features(self, lcms_obj, hdf5_path, overwrite): + def _save_new_mass_spectra(self, lcms_obj, hdf5_path, overwrite): + """Save new mass spectra that were added during processing. + + This method checks what mass spectra are in lcms_obj._ms and saves any + that aren't already in the HDF5 file's mass_spectra group. Uses the + existing add_mass_spectrum_to_hdf5 method for consistency with original + export logic. + + Parameters + ---------- + lcms_obj : LCMSBase + The LCMS object with potentially new mass spectra. + hdf5_path : Path + Path to the HDF5 file. + overwrite : bool + If True, allows overwriting existing spectra. + """ + # Check if there are any mass spectra to save + if not hasattr(lcms_obj, '_ms') or not lcms_obj._ms: + return + + # Create an LCMS exporter instance for this LCMS object + # This gives us access to add_mass_spectrum_to_hdf5 method inherited from HighResMassSpecExport + # Turn hdf5_path into str without suffix for LCMSExport + hdf5_path_str = str(hdf5_path.with_suffix('')) + exporter = LCMSExport( + out_file_path=hdf5_path_str, + mass_spectra=lcms_obj + ) + + # Open HDF5 file and check existing mass spectra + with h5py.File(hdf5_path, 'a') as hdf_handle: + # Create mass_spectra group if it doesn't exist + if 'mass_spectra' not in hdf_handle: + ms_group = hdf_handle.create_group('mass_spectra') + existing_scan_numbers = set() + else: + ms_group = hdf_handle['mass_spectra'] + existing_scan_numbers = set(int(k) for k in ms_group.keys()) + + # Find new mass spectra (in _ms but not in HDF5) + new_scan_numbers = set(lcms_obj._ms.keys()) - existing_scan_numbers + + if not new_scan_numbers: + return + + # Save new mass spectra using existing add_mass_spectrum_to_hdf5 method + export_profile = lcms_obj.parameters.lc_ms.export_profile_spectra + for scan_number in new_scan_numbers: + mass_spec = lcms_obj._ms[scan_number] + scan_group_name = str(scan_number) + + # Delete existing group if overwrite is True + if scan_group_name in ms_group and overwrite: + del ms_group[scan_group_name] + elif scan_group_name in ms_group: + continue + + # Use the existing method from HighResMassSpecExport + exporter.add_mass_spectrum_to_hdf5( + hdf_handle=hdf_handle, + mass_spectrum=mass_spec, + group_key=scan_group_name, + mass_spectra_group=ms_group, + export_raw=export_profile + ) + + def _selective_update_mass_features(self, lcms_obj, hdf5_path, mf_ids_to_update, overwrite): """Selectively update mass features in HDF5 file. - This method deletes only the mass features that were loaded (have cluster_index), + This method deletes only the mass features specified in mf_ids_to_update, then re-saves them with their potentially updated attributes. Non-cluster features in the HDF5 file are left untouched. @@ -2609,40 +2723,37 @@ def _selective_update_mass_features(self, lcms_obj, hdf5_path, overwrite): The LCMS object with mass features to update. hdf5_path : Path Path to the HDF5 file. + mf_ids_to_update : list of int + List of mass feature IDs that should be updated. This explicitly defines + which features were loaded and should be saved. overwrite : bool If True, allows overwriting. If False, skips if group exists. """ - # Collect IDs of loaded features (those with cluster_index) - loaded_feature_ids = [ - mf.id for mf in lcms_obj.mass_features.values() - if hasattr(mf, 'cluster_index') and mf.cluster_index is not None - ] - - if not loaded_feature_ids: + if not mf_ids_to_update: return - # Open HDF5 file and delete loaded feature IDs, then re-save + # Open HDF5 file and delete specified feature IDs, then re-save with h5py.File(hdf5_path, 'a') as hdf_handle: if 'mass_features' not in hdf_handle: return mf_group = hdf_handle['mass_features'] - # Delete loaded features - for feature_id in loaded_feature_ids: + # Delete features that are being updated + for feature_id in mf_ids_to_update: feature_id_str = str(feature_id) if feature_id_str in mf_group: del mf_group[feature_id_str] - # Re-save loaded features with updated attributes - loaded_features = { + # Re-save updated features (only those that exist in mass_features dict) + updated_features = { mf.id: mf for mf in lcms_obj.mass_features.values() - if mf.id in loaded_feature_ids + if mf.id in mf_ids_to_update } - if loaded_features: + if updated_features: LCMSExport._save_mass_features_dict_to_hdf5( - loaded_features, + updated_features, mf_group, overwrite=overwrite ) diff --git a/support_code/nmdc/metabolomics/metabolomics_collection.py b/support_code/nmdc/metabolomics/metabolomics_collection.py index 49bd1c692..87c593ade 100644 --- a/support_code/nmdc/metabolomics/metabolomics_collection.py +++ b/support_code/nmdc/metabolomics/metabolomics_collection.py @@ -267,7 +267,7 @@ def get_configured_lcms_parameters(): # Reporting settings params.lc_ms.export_eics = True - params.lc_ms.export_profile_spectra = True + params.lc_ms.export_profile_spectra = False # Peak metrics filtering settings params.lc_ms.remove_mass_features_by_peak_metrics = True @@ -415,8 +415,8 @@ def process_single_sample(args): pipeline_results = lcms_collection.process_consensus_features( load_representatives=True, perform_gap_filling=True, - add_ms1=False, - add_ms2=False, + add_ms1=True, + add_ms2=True, molecular_formula_search=False, ms2_spectral_search=False, spectral_lib=spectral_lib, @@ -472,7 +472,8 @@ def process_single_sample(args): cores=ncores) lcms_collection2 = reader.get_lcms_collection( load_raw=False, load_light=True, - load_representatives=True, load_eics=True) + load_representatives=True, load_eics=True, + load_ms1=True, load_ms2=True) # Check that len of mass features matches mf_count_1 = len(lcms_collection.mass_features_dataframe) @@ -497,6 +498,41 @@ def process_single_sample(args): print(f"✓ Total loaded EIC count matches after reload: {total_eics_1}") else: print(f"✗ Total loaded EIC count mismatch after reload! Original: {total_eics_1}, Reloaded: {total_eics_2}") + + # Check that the _ms dictionary matches (mass spectra loaded) + total_ms_1 = sum(len(lcms_obj._ms) for lcms_obj in lcms_collection) + total_ms_2 = sum(len(lcms_obj._ms) for lcms_obj in lcms_collection2) + if total_ms_1 == total_ms_2: + print(f"✓ Total loaded mass spectra (_ms) count matches after reload: {total_ms_1}") + else: + print(f"✗ Total loaded mass spectra (_ms) count mismatch after reload! Original: {total_ms_1}, Reloaded: {total_ms_2}") + + # Check that we can replot the first cluster from the reloaded collection + if len(lcms_collection2.cluster_summary_dataframe) > 0: + first_cluster_id = lcms_collection2.cluster_summary_dataframe.index[0] + print(f"Re-plotting cluster {first_cluster_id} from reloaded collection") + lcms_collection2.plot_cluster( + cluster_id=first_cluster_id, + to_plot=["EIC", "MS1", "MS2"], + plot_smoothed_eic=False, + plot_eic_datapoints=False + ) + + # Check that scan numbers in _ms match for first sample + if len(lcms_collection) > 0 and len(lcms_collection2) > 0: + ms_scans_1 = set(lcms_collection[0]._ms.keys()) + ms_scans_2 = set(lcms_collection2[0]._ms.keys()) + if ms_scans_1 == ms_scans_2: + print(f"✓ Mass spectra scan numbers match for first sample: {len(ms_scans_1)} scans") + else: + print(f"✗ Mass spectra scan numbers mismatch for first sample!") + print(f" Original: {len(ms_scans_1)} scans, Reloaded: {len(ms_scans_2)} scans") + only_in_1 = ms_scans_1 - ms_scans_2 + only_in_2 = ms_scans_2 - ms_scans_1 + if only_in_1: + print(f" Only in original: {list(sorted(only_in_1))[:10]}...") + if only_in_2: + print(f" Only in reloaded: {list(sorted(only_in_2))[:10]}...") """ From 8a22e438d7cce3b60fb10ba8a321edde8b1cb0f4 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 26 Jan 2026 21:51:02 -0800 Subject: [PATCH 115/158] Add collection_consensus_report functionality to use representative mass feature --- corems/mass_spectra/calc/lc_calc.py | 10 +- corems/mass_spectra/factory/lc_class.py | 74 +++---- corems/mass_spectra/input/corems_hdf5.py | 1 - .../metabolomics/metabolomics_collection.py | 185 +++++++++++------- 4 files changed, 140 insertions(+), 130 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 67c94a8b1..f885d307f 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -3800,6 +3800,8 @@ def get_representative_mass_features_for_all_clusters(self, representative_metri representative_metric = self.parameters.lcms_collection.consensus_representative_metric mf_df = self.mass_features_dataframe.copy() + # Reset index to make coll_mf_id a column we can work with + mf_df = mf_df.reset_index(drop=False) # Handle special metric 'intensity_prefer_ms2' if representative_metric == 'intensity_prefer_ms2': @@ -3853,10 +3855,7 @@ def has_ms2_scans(val): # Get the index of max value for each cluster idx = mf_df.groupby('cluster')[representative_metric].idxmax() - representatives = mf_df.loc[idx].reset_index(drop=True) - - # Store the collection-level index as coll_mf_id - representatives['coll_mf_id'] = representatives.index + representatives = mf_df.loc[idx].copy() # Select only the columns we need result_cols = ['cluster', 'sample_id', 'mf_id', 'coll_mf_id', 'has_ms2', 'intensity'] @@ -5215,7 +5214,7 @@ def _prepare_pipeline_runtime_params(self, operations): # Get all mass features that belong to clusters (cluster is not NaN) clustered_mf = mfdf[mfdf['cluster'].notna()] - # Group by sample_id and collect all m/z values + # Group by sample_id and collect all m/z values associated with eics for sample_id in clustered_mf['sample_id'].unique(): sample_df = clustered_mf[clustered_mf['sample_id'] == sample_id] cluster_mz_dict[sample_id] = sample_df['_eic_mz'].unique().tolist() @@ -5515,7 +5514,6 @@ def process_consensus_features(self, load_representatives=True, perform_gap_fill if ms2_spectral_search and hasattr(self, '_spectral_search_molecular_metadata'): # This allows users to access the metadata for reporting self.spectral_search_molecular_metadata = self._spectral_search_molecular_metadata - #TODO KRH: Update mass_features_dataframe with updated mz for representative mass features if ms1 was added? # Post-processing if perform_gap_filling: # Combine induced mass features into dataframe diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 1e2745a2e..9334559f3 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -2171,27 +2171,27 @@ def collection_pivot_table(self, attribute = 'coll_mf_id', verbose = True): ) return mf_pivot.pivot(index = 'cluster', columns = 'sample_id', values = attribute) - def collection_consensus_report(self, how = 'intensity'): - """Generate a consensus report of all regular and induced mass features - in a collection. Default is to select feature of highest intensity in - a cluster and report back all the attributes. - - Parameters - ----------- - how : str - The preferred method to report back the consensus information. - Option 'intensity' assigns peak of highest intensity in each - cluster as the representative consensus feaature and reports data - on that peak. Option 'mean' reports the mean values for available - attributes by cluster, and option 'median' is the same but reports - median values. + def collection_consensus_report(self): + """Generate a consensus report of consensus mass features + in a collection using the representative feature from each cluster. + + This method returns a DataFrame containing all attributes for the + representative mass feature from each consensus cluster. The representative + is selected using the same logic as process_consensus_features(). Returns -------- pd.DataFrame - A DataFrame that displays all attributes for each cluster in a - collection based on either the peak of highest intensity or means - across all data in a cluster + A DataFrame that displays all attributes for each cluster's + representative mass feature, indexed by cluster ID. + + Notes + ----- + The representative metric used is determined by + self.parameters.lcms_collection.consensus_representative_metric and + is the same metric used by process_consensus_features() for consistency. + Common options include 'intensity' (highest intensity) or + 'intensity_prefer_ms2' (highest intensity with preference for MS2 data). """ mf_df = self.mass_features_dataframe.copy() @@ -2203,37 +2203,15 @@ def collection_consensus_report(self, how = 'intensity'): mf_df.reset_index(drop = True, inplace = True) mf_df['cluster'] = mf_df['cluster'].astype(int) - # If how == intensity, find the sample with highest intensity in each cluster and return that mass feature's data - if how == 'intensity': - int_table = self.collection_pivot_table(attribute = 'intensity', verbose = False).idxmax(axis = 1) - id_list = [] - for i in range(len(int_table)): - id_list.append( - mf_df[ - (mf_df.sample_id == int_table.iloc[i]) & (mf_df.cluster == int_table.index[i]) - ].coll_mf_id.values[0] - ) - return mf_df[mf_df.coll_mf_id.isin(id_list)].sort_values(by = 'cluster').set_index('cluster') - - # If how == mean or median, group by cluster and calculate mean or median for each attribute - elif how == 'mean' or how == 'median': - #TODO KRH: Clean up this section - ## will have to go back and check mf_df.dtypes to see what ends up on list - ## this format removes int columns like 'sample_id' and 'cluster' - l = mf_df.select_dtypes(include='float64').columns.tolist() - ## for some reason, the following 4 items get saved as floats instead of ints - ## can either leave this or track down how they're recorded and fix it at the source - ## possible they're recorded as ints for regular mass features and floats for induced, making the column default to the more general - l = [x for x in l if x not in ['scan_time', 'apex_scan', 'partition_idx', 'idx']] - agg_dict = {k: [how] for k in l} - agg_dict['sample_id'] = ['nunique'] - - df = (mf_df.groupby("cluster").agg(agg_dict).reset_index()) - - return df.sort_values(by = 'cluster').set_index('cluster') - - else: - print("Assign 'how' argument as either 'intensity', 'mean', or 'median'") + # Use the same representative selection logic as process_consensus_features + # This uses the configured representative_metric from parameters + representatives = self.get_representative_mass_features_for_all_clusters() + + # Get the coll_mf_ids of the representatives + representative_ids = representatives['coll_mf_id'].tolist() + + # Filter mf_df to only include representative features + return mf_df[mf_df.coll_mf_id.isin(representative_ids)].sort_values(by = 'cluster').set_index('cluster') @property def parameters(self): diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 4073e5aca..7b515e1f9 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -1300,7 +1300,6 @@ def convert_back_to_bool(data): def _load_rt_alignments(self, lcms_collection): """Load retention time alignments from the saved collection HDF5 file.""" - #TODO KRH: START HERE - test this function # First Set the rt_aligned flag from the collection-level attribute saved directly with h5py.File(self.collection_hdf5_path, 'r') as f: lcms_collection.rt_aligned = f.attrs.get('rt_aligned', False) diff --git a/support_code/nmdc/metabolomics/metabolomics_collection.py b/support_code/nmdc/metabolomics/metabolomics_collection.py index 87c593ade..29e174213 100644 --- a/support_code/nmdc/metabolomics/metabolomics_collection.py +++ b/support_code/nmdc/metabolomics/metabolomics_collection.py @@ -162,6 +162,105 @@ def summarize_processing_results(lcms_collection): print("\n" + "="*60) +def validate_save_load(lcms_collection, collection_save_path, ncores=1): + """ + Validate that the LCMS collection can be saved and reloaded correctly. + + This function tests the save/load functionality by reloading the collection + from HDF5 and comparing various attributes to ensure data integrity. + + Parameters + ---------- + lcms_collection : LCMSCollection + The original LCMS collection to validate against + collection_save_path : Path + Path to the saved collection HDF5 file (without extension) + ncores : int, optional + Number of cores to use for loading. Default is 1. + + Returns + ------- + LCMSCollection + The reloaded LCMS collection for further testing + """ + print("\n" + "="*60) + print("SAVE/LOAD VALIDATION TEST") + print("="*60) + + # Reload the collection from HDF5 + from corems.mass_spectra.input.corems_hdf5 import ReadSavedLCMSCollection + reader = ReadSavedLCMSCollection( + collection_hdf5_path=str(collection_save_path.with_suffix('.hdf5')), + cores=ncores) + lcms_collection2 = reader.get_lcms_collection( + load_raw=False, load_light=True, + load_representatives=True, load_eics=True, + load_ms1=True, load_ms2=True) + + # Check that len of mass features matches + mf_count_1 = len(lcms_collection.mass_features_dataframe) + mf_count_2 = len(lcms_collection2.mass_features_dataframe) + if mf_count_1 == mf_count_2: + print(f"✓ Mass feature count matches after reload: {mf_count_1}") + else: + print(f"✗ Mass feature count mismatch after reload! Original: {mf_count_1}, Reloaded: {mf_count_2}") + + # Check that the len of induced mass features matches + induced_count_1 = len(lcms_collection.induced_mass_features_dataframe) + induced_count_2 = len(lcms_collection2.induced_mass_features_dataframe) + if induced_count_1 == induced_count_2: + print(f"✓ Induced mass feature count matches after reload: {induced_count_1}") + else: + print(f"✗ Induced mass feature count mismatch after reload! Original: {induced_count_1}, Reloaded: {induced_count_2}") + + # Check that the len of loaded EICs matches + total_eics_1 = sum(len(lcms_obj.eics) for lcms_obj in lcms_collection) + total_eics_2 = sum(len(lcms_obj.eics) for lcms_obj in lcms_collection2) + if total_eics_1 == total_eics_2: + print(f"✓ Total loaded EIC count matches after reload: {total_eics_1}") + else: + print(f"✗ Total loaded EIC count mismatch after reload! Original: {total_eics_1}, Reloaded: {total_eics_2}") + + # Check that the _ms dictionary matches (mass spectra loaded) + total_ms_1 = sum(len(lcms_obj._ms) for lcms_obj in lcms_collection) + total_ms_2 = sum(len(lcms_obj._ms) for lcms_obj in lcms_collection2) + if total_ms_1 == total_ms_2: + print(f"✓ Total loaded mass spectra (_ms) count matches after reload: {total_ms_1}") + else: + print(f"✗ Total loaded mass spectra (_ms) count mismatch after reload! Original: {total_ms_1}, Reloaded: {total_ms_2}") + + # Check that we can replot the first cluster from the reloaded collection + if len(lcms_collection2.cluster_summary_dataframe) > 0: + first_cluster_id = lcms_collection2.cluster_summary_dataframe.index[0] + print(f"Re-plotting cluster {first_cluster_id} from reloaded collection") + lcms_collection2.plot_cluster( + cluster_id=first_cluster_id, + to_plot=["EIC", "MS1", "MS2"], + plot_smoothed_eic=False, + plot_eic_datapoints=False + ) + + # Check that scan numbers in _ms match for first sample + if len(lcms_collection) > 0 and len(lcms_collection2) > 0: + ms_scans_1 = set(lcms_collection[0]._ms.keys()) + ms_scans_2 = set(lcms_collection2[0]._ms.keys()) + if ms_scans_1 == ms_scans_2: + print(f"✓ Mass spectra scan numbers match for first sample: {len(ms_scans_1)} scans") + else: + print(f"✗ Mass spectra scan numbers mismatch for first sample!") + print(f" Original: {len(ms_scans_1)} scans, Reloaded: {len(ms_scans_2)} scans") + only_in_1 = ms_scans_1 - ms_scans_2 + only_in_2 = ms_scans_2 - ms_scans_1 + if only_in_1: + print(f" Only in original: {list(sorted(only_in_1))[:10]}...") + if only_in_2: + print(f" Only in reloaded: {list(sorted(only_in_2))[:10]}...") + + print("\n" + "="*60) + + return lcms_collection2 + + def preprocess_raw_samples(raw_data_path, processed_folder, ncores=1, reprocess=False): """ Preprocess raw LCMS sample files into HDF5 format. @@ -322,7 +421,7 @@ def process_single_sample(args): # ============================================================================= # Configuration # ============================================================================= - ncores = 1 + ncores = 3 reprocess_samples = False # Set to True to reprocess raw data perform_ms2_search = False # Set to True to perform MS2 spectral library search @@ -418,7 +517,7 @@ def process_single_sample(args): add_ms1=True, add_ms2=True, molecular_formula_search=False, - ms2_spectral_search=False, + ms2_spectral_search=perform_ms2_search, spectral_lib=spectral_lib, molecular_metadata=molecular_metadata, gather_eics=True, @@ -453,86 +552,22 @@ def process_single_sample(args): # Step 8: Save and Export Results # ============================================================================= print("\n=== Exporting LCMS Collection ===") + pivot_table_intensity = lcms_collection.collection_pivot_table(attribute='intensity', verbose=False) + pivot_table_ids = lcms_collection.collection_pivot_table(verbose=False) + + # Make a table of the annotations of the consensus features (using representative features) + consensus_report = lcms_collection.collection_consensus_report() + print(f"Consensus annotations table: {len(consensus_report)} clusters") + exporter = LCMSCollectionExport( out_file_path=str(collection_save_path), mass_spectra_collection=lcms_collection) exporter.export_to_hdf5(overwrite=True, save_parameters=True, parameter_format="toml") # ============================================================================= - # Test Save/Load Functionality + # Step 9: Validate Save/Load Functionality # ============================================================================= - print("\n" + "="*60) - print("SAVE/LOAD VALIDATION TEST") - print("="*60) - - # Reload - from corems.mass_spectra.input.corems_hdf5 import ReadSavedLCMSCollection - reader = ReadSavedLCMSCollection( - collection_hdf5_path=str(collection_save_path.with_suffix('.hdf5')), - cores=ncores) - lcms_collection2 = reader.get_lcms_collection( - load_raw=False, load_light=True, - load_representatives=True, load_eics=True, - load_ms1=True, load_ms2=True) - - # Check that len of mass features matches - mf_count_1 = len(lcms_collection.mass_features_dataframe) - mf_count_2 = len(lcms_collection2.mass_features_dataframe) - if mf_count_1 == mf_count_2: - print(f"✓ Mass feature count matches after reload: {mf_count_1}") - else: - print(f"✗ Mass feature count mismatch after reload! Original: {mf_count_1}, Reloaded: {mf_count_2}") - - # Check that the len of induced mass features matches - induced_count_1 = len(lcms_collection.induced_mass_features_dataframe) - induced_count_2 = len(lcms_collection2.induced_mass_features_dataframe) - if induced_count_1 == induced_count_2: - print(f"✓ Induced mass feature count matches after reload: {induced_count_1}") - else: - print(f"✗ Induced mass feature count mismatch after reload! Original: {induced_count_1}, Reloaded: {induced_count_2}") - - # Check that the len of loaded EICs matches - total_eics_1 = sum(len(lcms_obj.eics) for lcms_obj in lcms_collection) - total_eics_2 = sum(len(lcms_obj.eics) for lcms_obj in lcms_collection2) - if total_eics_1 == total_eics_2: - print(f"✓ Total loaded EIC count matches after reload: {total_eics_1}") - else: - print(f"✗ Total loaded EIC count mismatch after reload! Original: {total_eics_1}, Reloaded: {total_eics_2}") - - # Check that the _ms dictionary matches (mass spectra loaded) - total_ms_1 = sum(len(lcms_obj._ms) for lcms_obj in lcms_collection) - total_ms_2 = sum(len(lcms_obj._ms) for lcms_obj in lcms_collection2) - if total_ms_1 == total_ms_2: - print(f"✓ Total loaded mass spectra (_ms) count matches after reload: {total_ms_1}") - else: - print(f"✗ Total loaded mass spectra (_ms) count mismatch after reload! Original: {total_ms_1}, Reloaded: {total_ms_2}") - - # Check that we can replot the first cluster from the reloaded collection - if len(lcms_collection2.cluster_summary_dataframe) > 0: - first_cluster_id = lcms_collection2.cluster_summary_dataframe.index[0] - print(f"Re-plotting cluster {first_cluster_id} from reloaded collection") - lcms_collection2.plot_cluster( - cluster_id=first_cluster_id, - to_plot=["EIC", "MS1", "MS2"], - plot_smoothed_eic=False, - plot_eic_datapoints=False - ) - - # Check that scan numbers in _ms match for first sample - if len(lcms_collection) > 0 and len(lcms_collection2) > 0: - ms_scans_1 = set(lcms_collection[0]._ms.keys()) - ms_scans_2 = set(lcms_collection2[0]._ms.keys()) - if ms_scans_1 == ms_scans_2: - print(f"✓ Mass spectra scan numbers match for first sample: {len(ms_scans_1)} scans") - else: - print(f"✗ Mass spectra scan numbers mismatch for first sample!") - print(f" Original: {len(ms_scans_1)} scans, Reloaded: {len(ms_scans_2)} scans") - only_in_1 = ms_scans_1 - ms_scans_2 - only_in_2 = ms_scans_2 - ms_scans_1 - if only_in_1: - print(f" Only in original: {list(sorted(only_in_1))[:10]}...") - if only_in_2: - print(f" Only in reloaded: {list(sorted(only_in_2))[:10]}...") + lcms_collection2 = validate_save_load(lcms_collection, collection_save_path, ncores=ncores) """ From c70c530186365518d172360fa17e6c6db3576242 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 27 Jan 2026 09:03:30 -0800 Subject: [PATCH 116/158] Add annotation report for collection --- corems/mass_spectra/factory/lc_class.py | 176 +++++++++++++++++- .../metabolomics/metabolomics_collection.py | 25 ++- 2 files changed, 189 insertions(+), 12 deletions(-) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 9334559f3..cc29ce96b 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -2171,9 +2171,8 @@ def collection_pivot_table(self, attribute = 'coll_mf_id', verbose = True): ) return mf_pivot.pivot(index = 'cluster', columns = 'sample_id', values = attribute) - def collection_consensus_report(self): - """Generate a consensus report of consensus mass features - in a collection using the representative feature from each cluster. + def cluster_representatives_table(self): + """Generate a table of representative mass features from each consensus cluster. This method returns a DataFrame containing all attributes for the representative mass feature from each consensus cluster. The representative @@ -2182,8 +2181,12 @@ def collection_consensus_report(self): Returns -------- pd.DataFrame - A DataFrame that displays all attributes for each cluster's - representative mass feature, indexed by cluster ID. + A DataFrame with one row per cluster containing all attributes for + each cluster's representative mass feature. Includes: + - cluster: cluster ID (as a column for easy joining) + - polarity: ionization polarity from the collection + - n_samples_detected: number of samples where the cluster was detected + - All other mass feature attributes from the representative Notes ----- @@ -2203,6 +2206,9 @@ def collection_consensus_report(self): mf_df.reset_index(drop = True, inplace = True) mf_df['cluster'] = mf_df['cluster'].astype(int) + # Calculate number of samples per cluster + cluster_sample_counts = mf_df.groupby('cluster')['sample_id'].nunique().to_dict() + # Use the same representative selection logic as process_consensus_features # This uses the configured representative_metric from parameters representatives = self.get_representative_mass_features_for_all_clusters() @@ -2211,7 +2217,165 @@ def collection_consensus_report(self): representative_ids = representatives['coll_mf_id'].tolist() # Filter mf_df to only include representative features - return mf_df[mf_df.coll_mf_id.isin(representative_ids)].sort_values(by = 'cluster').set_index('cluster') + consensus_report = mf_df[mf_df.coll_mf_id.isin(representative_ids)].copy() + + # Add polarity (get from first sample in collection) + if len(self) > 0: + polarity = self[0].polarity + else: + polarity = 'unknown' + consensus_report['polarity'] = polarity + + # Add number of samples detected + consensus_report['n_samples_detected'] = consensus_report['cluster'].map(cluster_sample_counts) + + # Reorder columns to put cluster at the front + cols = consensus_report.columns.tolist() + if 'cluster' in cols: + cols.remove('cluster') + cols = ['cluster'] + cols + consensus_report = consensus_report[cols] + + # Sort by cluster and return with cluster as a regular column + return consensus_report.sort_values(by='cluster') + + def feature_annotations_table(self, molecular_metadata=None, drop_unannotated=False): + """Generate a comprehensive annotation table for all loaded mass features across samples. + + This method consolidates MS1 molecular formula assignments and MS2 spectral + search results for all mass features across all samples in the collection. + Only includes representative mass features (one per cluster per sample). + + Parameters + ---------- + molecular_metadata : dict, optional + Dictionary of MolecularMetadata objects, keyed by metabref_mol_id. + Required for including molecular metadata in MS2 annotations. + Default is None. + drop_unannotated : bool, optional + If True, drops rows where all annotation columns (everything except + cluster, MS2 Spectrum, and representative_sample) are NaN. + Default is False. + + Returns + ------- + pd.DataFrame + Consolidated annotation report with columns including: + - cluster: cluster ID + - sample_name: sample name + - sample_id: sample ID + - Mass Feature ID: mass feature ID within the sample + - Mass feature attributes (mz, scan_time, intensity, etc.) + - MS1 annotations (if molecular_formula_search was run) + - MS2 annotations (if ms2_spectral_search was run) + + Notes + ----- + This method uses the standard LCMSMetabolomicsExport.to_report() workflow + for each sample, then consolidates all results and adds cluster information. + + Only mass features that are loaded in each sample's mass_features dict + are included (typically the representative features if load_representatives + was used in process_consensus_features). + """ + from corems.mass_spectra.output.export import LCMSMetabolomicsExport + + # Collect reports from all samples + all_sample_reports = [] + + for sample_id, lcms_obj in enumerate(self): + # Skip samples with no loaded mass features + if not hasattr(lcms_obj, 'mass_features') or len(lcms_obj.mass_features) == 0: + continue + + sample_name = self.samples[sample_id] + + # Create exporter and generate report using standard workflow + exporter = LCMSMetabolomicsExport("temp", lcms_obj) + sample_report = exporter.to_report(molecular_metadata=molecular_metadata) + + # Add sample information + sample_report['representative_sample'] = sample_name + sample_report['sample_id'] = sample_id + + # Get cluster information from the mass_features_dataframe + # Build coll_mf_id for each row to look up cluster + sample_report['coll_mf_id'] = sample_report['sample_id'].astype(str) + "_" + sample_report['Mass Feature ID'].astype(str) + + # Get cluster from mass_features_dataframe + if self.mass_features_dataframe is not None and 'cluster' in self.mass_features_dataframe.columns: + mf_df = self.mass_features_dataframe.reset_index() + cluster_lookup = mf_df.set_index('coll_mf_id')['cluster'].to_dict() + sample_report['cluster'] = sample_report['coll_mf_id'].map(cluster_lookup) + else: + sample_report['cluster'] = None + + # Drop temporary coll_mf_id column + sample_report = sample_report.drop(columns=['coll_mf_id']) + + all_sample_reports.append(sample_report) + + # Combine all sample reports + if len(all_sample_reports) == 0: + raise ValueError("No samples with loaded mass features found in collection") + + collection_report = pd.concat(all_sample_reports, ignore_index=True) + + # Reorder columns to match specified order + desired_cols = [ + 'cluster', + 'Isotopologue Type', + 'Is Largest Ion after Deconvolution', + 'MS2 Spectrum', + 'Calculated m/z', + 'm/z Error (ppm)', + 'm/z Error Score', + 'Isotopologue Similarity', + 'Confidence Score', + 'Ion Formula', + 'Ion Type', + 'Molecular Formula', + 'inchikey', + 'name', + 'ref_ms_id', + 'Entropy Similarity', + 'Library mzs in Query (fraction)', + 'Spectra with Annotation (n)', + 'representative_sample' + ] + + # Include only desired columns that exist, maintaining order + cols = [col for col in desired_cols if col in collection_report.columns] + collection_report = collection_report[cols] + + # Optionally drop rows without any annotations + if drop_unannotated: + # Columns to exclude from the "all NA" check + exclude_cols = ['cluster', 'MS2 Spectrum', 'representative_sample'] + # Get annotation columns (everything except the excluded ones) + annot_cols = [col for col in collection_report.columns if col not in exclude_cols] + # Keep rows where at least one annotation column is not NA + if len(annot_cols) > 0: + collection_report = collection_report[collection_report[annot_cols].notna().any(axis=1)] + + # Sort by cluster, then by annotation quality + sort_cols = ['cluster'] + if 'Entropy Similarity' in collection_report.columns: + sort_cols.extend(['Entropy Similarity', 'Confidence Score']) + collection_report = collection_report.sort_values( + by=sort_cols, + ascending=[True, False, False] + ) + elif 'Confidence Score' in collection_report.columns: + sort_cols.append('Confidence Score') + collection_report = collection_report.sort_values( + by=sort_cols, + ascending=[True, False] + ) + else: + collection_report = collection_report.sort_values(by=sort_cols) + + return collection_report @property def parameters(self): diff --git a/support_code/nmdc/metabolomics/metabolomics_collection.py b/support_code/nmdc/metabolomics/metabolomics_collection.py index 29e174213..c6c3b306c 100644 --- a/support_code/nmdc/metabolomics/metabolomics_collection.py +++ b/support_code/nmdc/metabolomics/metabolomics_collection.py @@ -421,9 +421,9 @@ def process_single_sample(args): # ============================================================================= # Configuration # ============================================================================= - ncores = 3 + ncores = 1 reprocess_samples = False # Set to True to reprocess raw data - perform_ms2_search = False # Set to True to perform MS2 spectral library search + perform_ms2_search = True # Set to True to perform MS2 spectral library search # Paths base_path = Path("/Volumes/LaCie/nmdc_data/collection_testing/dev_test/") @@ -552,13 +552,27 @@ def process_single_sample(args): # Step 8: Save and Export Results # ============================================================================= print("\n=== Exporting LCMS Collection ===") + # Create pivot tables summarizing the collection across samples pivot_table_intensity = lcms_collection.collection_pivot_table(attribute='intensity', verbose=False) pivot_table_ids = lcms_collection.collection_pivot_table(verbose=False) + pivot_table_intensity.to_csv("example_collection_pivot_intensity.csv") - # Make a table of the annotations of the consensus features (using representative features) - consensus_report = lcms_collection.collection_consensus_report() - print(f"Consensus annotations table: {len(consensus_report)} clusters") + # Describe each cluster with its representative mass feature + cluster_reps = lcms_collection.cluster_representatives_table() + cluster_reps.to_csv("example_cluster_representatives.csv", index=False) + print(f"Cluster representatives table: {len(cluster_reps)} clusters") + + # Summarize the annotations for each cluster + feature_annotations = lcms_collection.feature_annotations_table( + molecular_metadata=molecular_metadata, + drop_unannotated=False + ) + print(f"Feature annotations table: {len(feature_annotations)} rows across {feature_annotations['cluster'].nunique()} clusters") + + # Save the feature annotations table to CSV for inspection + feature_annotations.to_csv("example_feature_annotations.csv", index=False) + # Save the entire collection to HDF5 exporter = LCMSCollectionExport( out_file_path=str(collection_save_path), mass_spectra_collection=lcms_collection) @@ -595,6 +609,5 @@ def process_single_sample(args): results2 = lcms_collection.collection_consensus_report(how = 'intensity') results3 = lcms_collection.collection_consensus_report(how = 'median') - #TODO KRH: Add visualization of a consensus mass feature #TODO KRH: Add visualization of matched spectrum with consensus mass feature """ \ No newline at end of file From 1d6c754bffa0170978c2197945c7507da1fc36f5 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 27 Jan 2026 09:31:24 -0800 Subject: [PATCH 117/158] Add reporting functions for collection level LCMS analysis --- corems/mass_spectra/factory/lc_class.py | 4 ++-- support_code/nmdc/metabolomics/metabolomics_collection.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index cc29ce96b..b56322f94 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -2163,13 +2163,13 @@ def collection_pivot_table(self, attribute = 'coll_mf_id', verbose = True): if verbose: print( 'Attributes available for pivot table:\n', - [x for x in mf_pivot.columns if x not in ['cluster', 'sample_id', 'mf_id', 'partition_idx', 'idx']] + [x for x in mf_pivot.columns if x not in ['cluster', 'sample_name', 'mf_id', 'partition_idx', 'idx']] ) print( '\nAttributes that have no value for induced mass features:\n', imf_pivot.columns[imf_pivot.isna().all()].tolist() ) - return mf_pivot.pivot(index = 'cluster', columns = 'sample_id', values = attribute) + return mf_pivot.pivot(index = 'cluster', columns = 'sample_name', values = attribute) def cluster_representatives_table(self): """Generate a table of representative mass features from each consensus cluster. diff --git a/support_code/nmdc/metabolomics/metabolomics_collection.py b/support_code/nmdc/metabolomics/metabolomics_collection.py index c6c3b306c..1d2f82386 100644 --- a/support_code/nmdc/metabolomics/metabolomics_collection.py +++ b/support_code/nmdc/metabolomics/metabolomics_collection.py @@ -565,7 +565,7 @@ def process_single_sample(args): # Summarize the annotations for each cluster feature_annotations = lcms_collection.feature_annotations_table( molecular_metadata=molecular_metadata, - drop_unannotated=False + drop_unannotated=True ) print(f"Feature annotations table: {len(feature_annotations)} rows across {feature_annotations['cluster'].nunique()} clusters") From 27ee7715b47e5c9fee9bc9430b5ba43979d0cbea Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 27 Jan 2026 10:37:47 -0800 Subject: [PATCH 118/158] Add tests and adjust functionality for aligning with no mass feature samples (like blanks) --- corems/mass_spectra/calc/lc_calc.py | 88 +++- corems/mass_spectra/factory/lc_class.py | 20 +- tests/test_lcms_collection.py | 594 ++++++++++++++++++++++++ 3 files changed, 690 insertions(+), 12 deletions(-) create mode 100644 tests/test_lcms_collection.py diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index f885d307f..b7a88a5a7 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -3027,11 +3027,46 @@ def align_lcms_objects(self, overwrite=False): index_steps = (1, -1) # Run this twice, once going forward (+1 indexing) and once going backward (-1 indexing) for index_step in index_steps: + # Initialize spline for propagation to samples without features + spl = None + use_spline_alignment = False + # Loop through the other LCMS objects in the collection (going forward) i = center_obj_id + index_step if i < len(self) and i >= 0: + # Check if this sample has any features in the dataframe + sample_name = self.samples[i] + if sample_name not in full_mf_df.index.get_level_values('sample_name'): + # For samples with no mass features, use the same alignment as the previous sample + if use_spline_alignment and spl is not None: + # Use the spline from the adjacent sample + self[i]._scan_info["scan_time_aligned"] = {k: spl(v) for k, v in self[i]._scan_info["scan_time"].items()} + else: + # No spline available, use original times + self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"] + self.rt_alignment_attempted = True + + # Move to next sample + i += index_step + while i < len(self) and i >= 0: + sample_name = self.samples[i] + if sample_name not in full_mf_df.index.get_level_values('sample_name'): + # Apply same alignment to this empty sample + if use_spline_alignment and spl is not None: + self[i]._scan_info["scan_time_aligned"] = {k: spl(v) for k, v in self[i]._scan_info["scan_time"].items()} + else: + self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"] + i += index_step + else: + # Found a sample with features, exit inner loop to process it + break + + # If we've processed all remaining empty samples, continue to next index_step + if i >= len(self) or i < 0: + continue + # Grab the first LCMS object after the center object - mf_df_i = full_mf_df.loc[self.samples[i]].copy() + mf_df_i = full_mf_df.loc[sample_name].copy() mf_df_i["scan_time_og"] = mf_df_i["scan_time"] while mf_df_i is not None: @@ -3075,13 +3110,50 @@ def align_lcms_objects(self, overwrite=False): if i >= len(self) or i < 0: mf_df_i = None else: - # Grab the next LCMS object and use the previous spline fitting to get a better starting point - mf_df_i = full_mf_df.loc[self.samples[i]].copy() - mf_df_i["scan_time_og"] = mf_df_i["scan_time"] - mf_df_i = mf_df_i.reset_index(drop=False) - if use_spline_alignment: - # Set scan_time to previous sample's predicted scan_time to find closer matches - mf_df_i["scan_time"] = spl(mf_df_i["scan_time"]) + # Check if this sample has any features in the dataframe + sample_name_next = self.samples[i] + if sample_name_next not in full_mf_df.index.get_level_values('sample_name'): + # For samples with no mass features, apply the same alignment transformation + if use_spline_alignment and spl is not None: + self[i]._scan_info["scan_time_aligned"] = {k: spl(v) for k, v in self[i]._scan_info["scan_time"].items()} + else: + self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"] + self.rt_alignment_attempted = True + + # Continue to the next sample + i += index_step + while i < len(self) and i >= 0: + sample_name = self.samples[i] + if sample_name not in full_mf_df.index.get_level_values('sample_name'): + # Apply same alignment to consecutive empty samples + if use_spline_alignment and spl is not None: + self[i]._scan_info["scan_time_aligned"] = {k: spl(v) for k, v in self[i]._scan_info["scan_time"].items()} + else: + self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"] + i += index_step + else: + # Found a sample with features + break + + # Check if we're done + if i >= len(self) or i < 0: + mf_df_i = None + else: + # Grab the next LCMS object with features + mf_df_i = full_mf_df.loc[self.samples[i]].copy() + mf_df_i["scan_time_og"] = mf_df_i["scan_time"] + mf_df_i = mf_df_i.reset_index(drop=False) + if use_spline_alignment: + # Set scan_time to previous sample's predicted scan_time to find closer matches + mf_df_i["scan_time"] = spl(mf_df_i["scan_time"]) + else: + # Grab the next LCMS object and use the previous spline fitting to get a better starting point + mf_df_i = full_mf_df.loc[sample_name_next].copy() + mf_df_i["scan_time_og"] = mf_df_i["scan_time"] + mf_df_i = mf_df_i.reset_index(drop=False) + if use_spline_alignment: + # Set scan_time to previous sample's predicted scan_time to find closer matches + mf_df_i["scan_time"] = spl(mf_df_i["scan_time"]) else: raise ValueError( f"No matches found between the center object and {self.samples[i]}" diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index b56322f94..67aa5fb29 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -878,6 +878,7 @@ def mass_features_to_df(self, induced_features=False, drop_na_cols=False, includ A pandas dataframe of mass features with the following columns: mf_id, mz, apex_scan, scan_time, intensity, persistence, area. """ + import pandas as pd def mass_spectrum_to_string( mass_spec, normalize=True, min_normalized_abun=0.01 @@ -917,10 +918,9 @@ def mass_spectrum_to_string( mf_dict = self.mass_features if len(mf_dict) == 0: - # Warn that no mass features were found, quit function - raise ValueError( - "No mass features found in dataset. Have the mass features been added? If this is part of a collection, summary data is aggregated in the attribute 'mass_features_dataframe'" - ) + # Return an empty dataframe with the expected structure + # This allows collection processing to continue even if some samples have no features + return pd.DataFrame() cols_in_df = [ "id", @@ -1730,6 +1730,18 @@ def _prepare_lcms_mass_features_for_combination(self, lcms_obj, induced_features mf_df = lcms_obj.light_mf_df else: mf_df = lcms_obj.mass_features_to_df() + + # If dataframe is empty, add minimal required columns and return + if len(mf_df) == 0: + import pandas as pd + mf_df["sample_name"] = [] + mf_df["sample_id"] = [] + mf_df["coll_mf_id"] = [] + mf_df["mf_id"] = [] + if induced_features: + mf_df["cluster"] = [] + return mf_df + # Remove index mf_df = mf_df.reset_index(drop=False) # Add sample name and sample id to the dataframe diff --git a/tests/test_lcms_collection.py b/tests/test_lcms_collection.py new file mode 100644 index 000000000..019786dac --- /dev/null +++ b/tests/test_lcms_collection.py @@ -0,0 +1,594 @@ +# %% Import libs +import numpy as np +import pytest +import pandas as pd + +from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectraCollection +from corems.mass_spectra.output.export import LCMSMetabolomicsExport, LCMSCollectionExport +from corems.encapsulation.factory.parameters import LCMSParameters, LCMSCollectionParameters +from corems.molecular_id.search.database_interfaces import MSPInterface + + +@pytest.fixture +def lcms_collection_folder(tmp_path, lcms_obj): + """ + Creates a temporary folder with processed LCMS objects for collection testing. + + This fixture creates 3 samples with different levels of mass features: + - Sample 1: Full set of mass features (all found features) + - Sample 2: Partial set (first 50 mass features only) + - Sample 3: No mass features (tests gap filling on completely empty sample) + + This setup allows comprehensive testing of gap filling functionality. + """ + # Create a temporary folder for processed data + processed_folder = tmp_path / "processed_lcms_collection" + processed_folder.mkdir() + + # Set parameters on the LCMS object that are reasonable for testing + lcms_obj.parameters = LCMSParameters(use_defaults=True) + + # Set persistent homology parameters for fast testing + lcms_obj.parameters.lc_ms.peak_picking_method = "persistent homology" + lcms_obj.parameters.lc_ms.ph_inten_min_rel = 0.001 + lcms_obj.parameters.lc_ms.ph_persis_min_rel = 0.05 + lcms_obj.parameters.lc_ms.ph_smooth_it = 0 + lcms_obj.parameters.lc_ms.ms1_scans_to_average = 3 + + # MS1 parameters for quick testing + ms1_params = lcms_obj.parameters.mass_spectrum['ms1'] + ms1_params.mass_spectrum.noise_threshold_method = "relative_abundance" + ms1_params.mass_spectrum.noise_threshold_min_relative_abundance = 0.1 + ms1_params.mass_spectrum.noise_min_mz = 0 + ms1_params.mass_spectrum.min_picking_mz = 0 + ms1_params.mass_spectrum.noise_max_mz = np.inf + ms1_params.mass_spectrum.max_picking_mz = np.inf + + # Process SAMPLE 1: Find and integrate mass features (FULL) + lcms_obj.find_mass_features(assign_ms2_scans=True) + lcms_obj.integrate_mass_features(drop_if_fail=True) + + # Save all mass features for later use + all_mass_features = dict(lcms_obj.mass_features) + all_eics = dict(lcms_obj.eics) + + # Export sample 1 with ALL mass features + sample_name_1 = "test_sample_01" + exporter1 = LCMSMetabolomicsExport(str(processed_folder / sample_name_1), lcms_obj) + exporter1.to_hdf(overwrite=True) + + # Create SAMPLE 2: Keep only first 50 mass features (PARTIAL) + # Using 50 instead of 10 to ensure enough matches for alignment validation + sample_name_2 = "test_sample_02" + + # Get the first 50 mass features sorted by m/z to ensure they match between samples + # Sort by m/z so we get consistent features that will align properly + sorted_mfs = sorted(all_mass_features.items(), key=lambda x: x[1].mz) + mf_ids = [mf_id for mf_id, mf in sorted_mfs[:50]] + lcms_obj.mass_features = {mf_id: all_mass_features[mf_id] for mf_id in mf_ids} + + # Export sample 2 with partial mass features + exporter2 = LCMSMetabolomicsExport(str(processed_folder / sample_name_2), lcms_obj) + exporter2.to_hdf(overwrite=True) + + # Create SAMPLE 3: No mass features at all (EMPTY) + sample_name_3 = "test_sample_03" + + # Clear all mass features and EICs + lcms_obj.mass_features = {} + lcms_obj.eics = {} + + # Export sample 3 with no mass features + exporter3 = LCMSMetabolomicsExport(str(processed_folder / sample_name_3), lcms_obj) + exporter3.to_hdf(overwrite=True) + + # Create a manifest file to explicitly set sample 1 as the center + # This is important because sample 1 has all features, making it the best reference + import csv + manifest_path = processed_folder / "manifest.csv" + with open(manifest_path, 'w', newline='') as f: + writer = csv.writer(f) + writer.writerow(['sample_name', 'batch', 'order', 'center']) + writer.writerow(['test_sample_01', 1, 1, True]) # Sample 1 is center (has all features) + writer.writerow(['test_sample_02', 1, 2, False]) # Sample 2 (partial features) + writer.writerow(['test_sample_03', 1, 3, False]) # Sample 3 (no features) + + return processed_folder + + +@pytest.fixture +def lcms_collection(lcms_collection_folder): + """ + Creates an LCMSCollection object from processed LCMS data. + + Returns a collection with 3 samples for testing collection-level operations: + - Sample 1: Full set of mass features + - Sample 2: Partial set (first 50 mass features) + - Sample 3: No mass features (for gap filling testing) + """ + # Load the collection from the processed folder + manifest_file = lcms_collection_folder / "manifest.csv" + parser = ReadCoreMSHDFMassSpectraCollection( + folder_location=lcms_collection_folder, + manifest_file=manifest_file, + cores=1 + ) + + # Get the LCMS collection without light loading since sample 3 has no mass features + # load_light=True would fail on sample 3 because it calls mass_features_to_df() + collection = parser.get_lcms_collection(load_raw=False, load_light=False) + + # Adjust collection parameters to allow clusters with only 1 sample + # This ensures features aren't filtered out before gap filling can occur + collection.parameters.lcms_collection.cluster_size_min_samples = 1 + collection.parameters.lcms_collection.cluster_size_min_sample_percentage = 0.0 + + # Set more lenient anchor feature parameters for alignment + # Use relative_intensity with threshold 0 to accept all features as anchors + collection.parameters.lcms_collection.mass_feature_anchor_technique = ['relative_intensity'] + collection.parameters.lcms_collection.mass_feature_anchor_relative_intensity_threshold = 0.0 + + # Set lenient alignment tolerances + collection.parameters.lcms_collection.alignment_mz_tol_ppm = 10 # More lenient m/z tolerance + collection.parameters.lcms_collection.alignment_rt_tol = 1.0 # More lenient RT tolerance (seconds) + + return collection + + +def test_lcms_collection_creation(lcms_collection): + """Test that an LCMSCollection can be created and has expected properties.""" + # Check that the collection was created + assert lcms_collection is not None + + # Check number of samples (should be 3) + assert len(lcms_collection) == 3 + assert len(lcms_collection.samples) == 3 + + # Check that we can access individual LCMS objects + assert lcms_collection[0] is not None + assert lcms_collection[1] is not None + assert lcms_collection[2] is not None + + # Check that manifest was created + assert lcms_collection.manifest is not None + assert len(lcms_collection.manifest) == 3 + + # Check manifest dataframe + manifest_df = lcms_collection.manifest_dataframe + assert len(manifest_df) == 3 + assert 'batch' in manifest_df.columns + assert 'order' in manifest_df.columns + + +def test_lcms_collection_mass_features_dataframe(lcms_collection): + """Test that mass features from all samples are combined correctly.""" + # Get mass features dataframe + mf_df = lcms_collection.mass_features_dataframe + + # Check that dataframe exists and has data + assert mf_df is not None + assert len(mf_df) > 0 + + # Check that required columns exist + required_columns = ['mf_id', 'sample_name', 'mz', 'scan_time', 'intensity'] + for col in required_columns: + assert col in mf_df.columns, f"Missing required column: {col}" + + # Check that we have features from sample 1 (sample 2 has partial, sample 3 has none initially) + unique_samples = mf_df['sample_name'].unique() + assert len(unique_samples) >= 1 + + # Check that coll_mf_id exists (collection-level unique ID) - it's the index + assert mf_df.index.name == 'coll_mf_id' or 'coll_mf_id' in mf_df.columns + + +def test_lcms_collection_parameters(lcms_collection): + """Test that collection parameters can be get/set.""" + # Check that parameters exist + assert lcms_collection.parameters is not None + + # Check that it's the right type + assert isinstance(lcms_collection.parameters, LCMSCollectionParameters) + + # Test setting new parameters + new_params = LCMSCollectionParameters() + new_params.lcms_collection.cores = 2 + lcms_collection.parameters = new_params + + assert lcms_collection.parameters.lcms_collection.cores == 2 + + +def test_lcms_collection_rt_alignment(lcms_collection): + """Test retention time alignment across samples in the collection.""" + # Check initial state + assert not lcms_collection.rt_aligned + + # Debug: Print anchor parameters + print(f"\nAnchor technique: {lcms_collection.parameters.lcms_collection.mass_feature_anchor_technique}") + print(f"Anchor absolute threshold: {lcms_collection.parameters.lcms_collection.mass_feature_anchor_absolute_intensity_threshold}") + print(f"Anchor relative threshold: {lcms_collection.parameters.lcms_collection.mass_feature_anchor_relative_intensity_threshold}") + + # Debug: Check which sample is center + print(f"\nManifest center values:") + for sample_name, manifest_data in lcms_collection._manifest_dict.items(): + print(f" {sample_name}: center={manifest_data.get('center', False)}") + + # Debug: Print feature counts + mf_df = lcms_collection.mass_features_dataframe + print(f"\nMass features per sample:") + for sample in lcms_collection.samples: + sample_mfs = mf_df[mf_df['sample_name'] == sample] + print(f" {sample}: {len(sample_mfs)} features") + + # Perform alignment - this should succeed + lcms_collection.align_lcms_objects() + + # Check that alignment was attempted (it may not use spline if already well-aligned) + assert lcms_collection.rt_alignment_attempted + + # Check that scan_time_aligned was added to all samples + for i, lcms_obj in enumerate(lcms_collection): + sample_name = lcms_collection.samples[i] + print(f"\nChecking {sample_name}: {lcms_obj.scan_df.columns.tolist()}") + assert 'scan_time_aligned' in lcms_obj.scan_df.columns, f"Missing scan_time_aligned in {sample_name}" + + +def test_lcms_collection_consensus_features(lcms_collection): + """Test generation of consensus mass features (clustering).""" + # Debug: Check which sample is the center + print(f"\nManifest dict center values:") + for sample_name, manifest_data in lcms_collection._manifest_dict.items(): + print(f" {sample_name}: center={manifest_data.get('center', False)}") + + # Ensure alignment is done first + if not lcms_collection.rt_aligned: + lcms_collection.align_lcms_objects() + + # Generate consensus features + lcms_collection.add_consensus_mass_features() + + # Check cluster summary dataframe + cluster_summary = lcms_collection.cluster_summary_dataframe + assert cluster_summary is not None + assert len(cluster_summary) > 0 + + # Check that cluster column was added to mass features + mf_df = lcms_collection.mass_features_dataframe + assert 'cluster' in mf_df.columns + + # Check cluster feature dictionary + cluster_dict = lcms_collection.cluster_feature_dictionary + assert cluster_dict is not None + assert len(cluster_dict) > 0 + + # Verify that all clusters in summary are in the dictionary + for cluster_id in cluster_summary.index: + assert cluster_id in cluster_dict + + +def test_lcms_collection_gap_filling(lcms_collection): + """Test gap filling to create induced mass features.""" + # Setup: align and cluster first + if not lcms_collection.rt_aligned: + lcms_collection.align_lcms_objects() + lcms_collection.add_consensus_mass_features() + + # Verify initial state of each sample before gap filling + sample_1_mf_count = len(lcms_collection[0].mass_features) + sample_2_mf_count = len(lcms_collection[1].mass_features) + sample_3_mf_count = len(lcms_collection[2].mass_features) + + print(f"\nBefore gap filling:") + print(f" Sample 1 mass features: {sample_1_mf_count} (full)") + print(f" Sample 2 mass features: {sample_2_mf_count} (partial - first 10)") + print(f" Sample 3 mass features: {sample_3_mf_count} (empty)") + + # Perform gap filling + lcms_collection.search_for_missing_mass_features() + + # Check that induced mass features dataframe exists + induced_df = lcms_collection.induced_mass_features_dataframe + assert induced_df is not None + + # With samples 2 and 3 having missing features, gap filling should create induced features + print(f"\nAfter gap filling:") + print(f" Total induced mass features found: {len(induced_df)}") + assert len(induced_df) > 0, "Gap filling should create induced mass features in samples 2 and 3" + + # Check that induced mass features have proper columns + assert 'cluster' in induced_df.columns + assert 'sample_name' in induced_df.columns + assert 'mf_id' in induced_df.columns + + # Check induced features per sample + sample_2_induced = len(lcms_collection[1].induced_mass_features) + sample_3_induced = len(lcms_collection[2].induced_mass_features) + + print(f" Sample 2 induced features: {sample_2_induced}") + print(f" Sample 3 induced features: {sample_3_induced}") + + # Sample 3 should have the most induced features (started with 0) + assert sample_3_induced > 0, "Sample 3 should have induced mass features from gap filling" + + # Sample 2 should also have some induced features (for features 11+) + assert sample_2_induced > 0, "Sample 2 should have induced features for missing mass features" + + # Check that individual LCMS objects have induced_mass_features + total_induced = sum(len(lcms_obj.induced_mass_features) + for lcms_obj in lcms_collection) + assert total_induced > 0 + + +def test_lcms_collection_pivot_table(lcms_collection): + """Test creation of pivot tables for collection data.""" + # Setup: ensure we have clustered features + if not lcms_collection.rt_aligned: + lcms_collection.align_lcms_objects() + lcms_collection.add_consensus_mass_features() + + # Create pivot table with default attribute (coll_mf_id) + pivot_df = lcms_collection.collection_pivot_table(verbose=False) + + # Check that pivot table was created + assert pivot_df is not None + assert isinstance(pivot_df, pd.DataFrame) + + # Check dimensions: rows should be clusters, columns should be samples + assert len(pivot_df.columns) == len(lcms_collection.samples) + + # Create pivot table with intensity attribute + pivot_intensity = lcms_collection.collection_pivot_table( + attribute='intensity', + verbose=False + ) + assert pivot_intensity is not None + assert len(pivot_intensity.columns) == len(lcms_collection.samples) + + +def test_lcms_collection_cluster_representatives(lcms_collection): + """Test extraction of representative features for each cluster.""" + # Setup: ensure we have clustered features + if not lcms_collection.rt_aligned: + lcms_collection.align_lcms_objects() + lcms_collection.add_consensus_mass_features() + + # Get cluster representatives table + reps_table = lcms_collection.cluster_representatives_table() + + # Check that table was created + assert reps_table is not None + assert isinstance(reps_table, pd.DataFrame) + assert len(reps_table) > 0 + + # Check required columns + required_cols = ['cluster', 'coll_mf_id', 'mz', 'scan_time', 'intensity'] + for col in required_cols: + assert col in reps_table.columns, f"Missing column: {col}" + + # Check that each cluster has exactly one representative + cluster_counts = reps_table['cluster'].value_counts() + assert all(count == 1 for count in cluster_counts) + + +def test_lcms_collection_export_import_hdf5(lcms_collection, tmp_path): + """Test exporting and re-importing a collection from HDF5.""" + # Setup: align and cluster + if not lcms_collection.rt_aligned: + lcms_collection.align_lcms_objects() + lcms_collection.add_consensus_mass_features() + + # Export collection to HDF5 + export_path = tmp_path / "test_collection" + exporter = LCMSCollectionExport( + out_file_path=str(export_path), + mass_spectra_collection=lcms_collection + ) + exporter.export_to_hdf5(overwrite=True, save_parameters=True) + + # Check that HDF5 file was created + hdf5_path = export_path.with_suffix('.hdf5') + assert hdf5_path.exists() + + # Re-import the collection + from corems.mass_spectra.input.corems_hdf5 import ReadSavedLCMSCollection + reader = ReadSavedLCMSCollection( + collection_hdf5_path=str(hdf5_path), + cores=1 + ) + collection2 = reader.get_lcms_collection( + load_raw=False, + load_light=True + ) + + # Verify the reloaded collection + assert len(collection2) == len(lcms_collection) + + # Check that mass features match + mf_count_1 = len(lcms_collection.mass_features_dataframe) + mf_count_2 = len(collection2.mass_features_dataframe) + assert mf_count_1 == mf_count_2 + + # Check that cluster count matches if clustering was done + if len(lcms_collection.cluster_summary_dataframe) > 0: + cluster_count_1 = len(lcms_collection.cluster_summary_dataframe) + cluster_count_2 = len(collection2.cluster_summary_dataframe) + assert cluster_count_1 == cluster_count_2 + + +def test_lcms_collection_drop_isotopologues(lcms_collection): + """Test dropping isotopologues from the collection.""" + # Get initial mass features count + initial_mf_count = len(lcms_collection.mass_features_dataframe) + + # Drop isotopologues + lcms_collection._drop_isotopologues() + + # Check that flag was set + assert lcms_collection.isotopes_dropped + + # Mass features count should be less than or equal to initial + # (equal if no isotopologues were found) + final_mf_count = len(lcms_collection.mass_features_dataframe) + assert final_mf_count <= initial_mf_count + + +def test_lcms_collection_plot_tics(lcms_collection): + """Test plotting total ion chromatograms for the collection.""" + # This test just ensures the method runs without error + # Visual inspection of plots is done manually + try: + import matplotlib + matplotlib.use('Agg') # Use non-interactive backend for testing + lcms_collection.plot_tics(ms_level=1, type="raw", plot_legend=False) + assert True + except Exception as e: + pytest.fail(f"plot_tics raised an exception: {e}") + + +def test_lcms_collection_feature_annotations_table(lcms_collection, msp_file_location): + """Test creation of feature annotations table with molecular metadata.""" + # Setup: align and cluster + if not lcms_collection.rt_aligned: + lcms_collection.align_lcms_objects() + lcms_collection.add_consensus_mass_features() + + # Load molecular metadata from MSP file + my_msp = MSPInterface(file_path=msp_file_location) + msp_lib, molecular_metadata = my_msp.get_metabolomics_spectra_library( + polarity="negative", + format="flashentropy", + normalize=True + ) + + # Create annotations table without metadata first + annotations_table = lcms_collection.feature_annotations_table( + molecular_metadata=None, + drop_unannotated=False + ) + + assert annotations_table is not None + assert isinstance(annotations_table, pd.DataFrame) + assert len(annotations_table) > 0 + + # Check that cluster information is present + assert 'cluster' in annotations_table.columns + + +def test_lcms_collection_sample_access(lcms_collection): + """Test various ways to access samples in the collection.""" + # Test indexing + first_sample = lcms_collection[0] + assert first_sample is not None + + # Test iteration + sample_count = 0 + for lcms_obj in lcms_collection: + assert lcms_obj is not None + sample_count += 1 + assert sample_count == len(lcms_collection) + + # Test samples property + sample_names = lcms_collection.samples + assert len(sample_names) == len(lcms_collection) + + # Test raw_files property + raw_files = lcms_collection.raw_files + assert len(raw_files) == len(lcms_collection) + + +def test_lcms_collection_update_raw_file_locations(lcms_collection, tmp_path): + """Test updating raw file locations in the collection.""" + # Create a new path for raw files + new_raw_folder = tmp_path / "new_raw_location" + new_raw_folder.mkdir() + + # Update raw file locations + lcms_collection.update_raw_file_locations(str(new_raw_folder)) + + # Check that paths were updated + for lcms_obj in lcms_collection: + assert str(new_raw_folder) in str(lcms_obj.raw_file_location) + + # Check that flag was set + assert lcms_collection.raw_files_relocated + + +def test_lcms_collection_minimal_workflow(lcms_collection): + """ + Test a minimal end-to-end workflow with the collection. + + This test mirrors the workflow in metabolomics_collection.py: + 1. Load collection + 2. Align retention times + 3. Generate consensus features + 4. Perform gap filling + 5. Create reports + """ + # Step 1: Collection is loaded via fixture + assert len(lcms_collection) > 0 + + # Step 2: Align retention times + lcms_collection.align_lcms_objects() + + # Step 3: Generate consensus features + lcms_collection.add_consensus_mass_features() + cluster_count = len(lcms_collection.cluster_summary_dataframe) + assert cluster_count > 0 + + # Step 4: Perform gap filling + lcms_collection.search_for_missing_mass_features() + + # Step 5: Create reports + pivot_table = lcms_collection.collection_pivot_table(verbose=False) + assert pivot_table is not None + + cluster_reps = lcms_collection.cluster_representatives_table() + assert len(cluster_reps) == cluster_count + + annotations = lcms_collection.feature_annotations_table( + molecular_metadata=None, + drop_unannotated=False + ) + assert annotations is not None + + # Verify workflow completed successfully + assert lcms_collection.rt_aligned or lcms_collection.rt_alignment_attempted + assert cluster_count > 0 + print(f"\nWorkflow completed: {cluster_count} consensus clusters from {len(lcms_collection)} samples") + + +def test_lcms_collection_memory_management(lcms_collection): + """Test that collection properly manages memory for raw data.""" + # Initially, raw data should not be loaded (load_light=True in fixture) + for i, lcms_obj in enumerate(lcms_collection): + # Check that _ms_unprocessed is either empty or not loaded + if hasattr(lcms_obj, '_ms_unprocessed'): + # MS level 1 should not have large raw data loaded + if 1 in lcms_obj._ms_unprocessed: + # For light loading, this should be empty or minimal + pass + + # Load raw data for one sample + lcms_collection.load_raw_data(sample_idx=0, ms_level=1) + + # Check that data was loaded + assert 1 in lcms_collection[0]._ms_unprocessed + + # Drop raw data + lcms_collection.drop_raw_data(sample_idx=0, ms_level=1) + + # Verify data was dropped + # After dropping, the dict might be empty or have an empty DataFrame + if 1 in lcms_collection[0]._ms_unprocessed: + # Should be empty or minimal size now + pass + + +def test_lcms_collection_cleanup(lcms_collection_folder): + """Test cleanup of temporary collection data.""" + # Verify the folder exists + assert lcms_collection_folder.exists() + + # The pytest tmp_path fixture will automatically clean up + # This test just verifies the structure + assert len(list(lcms_collection_folder.glob("*.corems"))) > 0 From cf1117ca72fca5bb33e1fca12078b3156b7e7632 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 27 Jan 2026 11:47:42 -0800 Subject: [PATCH 119/158] Test bug in alignment with perfect matches --- corems/mass_spectra/calc/lc_calc.py | 151 +++++++++++++++++++++++----- tests/test_lcms_collection.py | 72 +++++++++++-- 2 files changed, 186 insertions(+), 37 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index b7a88a5a7..8797efbc2 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -2767,16 +2767,23 @@ def match_mfs(self, mf_c, mf_i): v1 = mf_c[dims].values / tol v2 = mf_i[dims].values / tol dist3d = scipy.spatial.distance.cdist(v1, v2, "cityblock") - dist3d = np.multiply(dist3d, idx) + + # Separate features within tolerance from those outside + # Features outside tolerance should be inf, features within tolerance keep their distance + # Use idx mask: True for within tolerance, False for outside + dist3d_within_tol = np.where(idx, dist3d, np.inf) - # Normalize to 0-1 - mx = dist3d.max() + # Normalize to 0-1 (only affects within-tolerance distances) + mx = np.max(dist3d_within_tol[idx]) if np.sum(idx) > 0 else 0 if mx > 0: - # Lower distance is better - dist3d = dist3d / dist3d.max() - - # Turn zeros to inf (no match) - dist3d[dist3d == 0] = np.inf + # Lower distance is better - normalize only the within-tolerance values + dist3d_within_tol = np.where(idx, dist3d_within_tol / mx, np.inf) + else: + # All matches are perfect (distance=0), assign tiny value to within-tolerance pairs + dist3d_within_tol = np.where(idx, 1e-10, np.inf) + + # Use the masked distance matrix + dist3d = dist3d_within_tol # Min over dims mincols = np.min(dist3d, axis=0, keepdims=True) @@ -3043,7 +3050,7 @@ def align_lcms_objects(self, overwrite=False): self[i]._scan_info["scan_time_aligned"] = {k: spl(v) for k, v in self[i]._scan_info["scan_time"].items()} else: # No spline available, use original times - self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"] + self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"].copy() self.rt_alignment_attempted = True # Move to next sample @@ -3055,7 +3062,7 @@ def align_lcms_objects(self, overwrite=False): if use_spline_alignment and spl is not None: self[i]._scan_info["scan_time_aligned"] = {k: spl(v) for k, v in self[i]._scan_info["scan_time"].items()} else: - self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"] + self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"].copy() i += index_step else: # Found a sample with features, exit inner loop to process it @@ -3101,7 +3108,7 @@ def align_lcms_objects(self, overwrite=False): self.rt_alignment_attempted = True else: # Set aligned retention times on scan_df for lc_obj using the original retention times - self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"] + self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"].copy() # Switch the rt_attempted flag to True self.rt_aligned = False self.rt_alignment_attempted = True @@ -3117,7 +3124,7 @@ def align_lcms_objects(self, overwrite=False): if use_spline_alignment and spl is not None: self[i]._scan_info["scan_time_aligned"] = {k: spl(v) for k, v in self[i]._scan_info["scan_time"].items()} else: - self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"] + self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"].copy() self.rt_alignment_attempted = True # Continue to the next sample @@ -3129,7 +3136,7 @@ def align_lcms_objects(self, overwrite=False): if use_spline_alignment and spl is not None: self[i]._scan_info["scan_time_aligned"] = {k: spl(v) for k, v in self[i]._scan_info["scan_time"].items()} else: - self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"] + self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"].copy() i += index_step else: # Found a sample with features @@ -3346,7 +3353,18 @@ def add_consensus_mass_features(self): self.mass_features_dataframe = mfs_with_clusters # Filter out clusters that don't meet minimum sample fraction + print(f"DEBUG: Before filtering - total features: {len(self.mass_features_dataframe)}") + if 'cluster' in self.mass_features_dataframe.columns: + print(f"DEBUG: Number of unique clusters before filtering: {self.mass_features_dataframe['cluster'].nunique()}") + print(f"DEBUG: Cluster sample counts:") + cluster_counts = self.mass_features_dataframe.groupby('cluster')['sample_id'].nunique() + print(cluster_counts.head(10)) + self._filter_clusters_by_sample_presence() + + print(f"DEBUG: After filtering - total features: {len(self.mass_features_dataframe)}") + if 'cluster' in self.mass_features_dataframe.columns: + print(f"DEBUG: Number of unique clusters after filtering: {self.mass_features_dataframe['cluster'].nunique()}") # TODO KRH: Deal with isomers better? Pool them together and then split them out using samples with 2 as the template? @@ -3465,20 +3483,29 @@ def summarize_clusters(self): mf_df = mf_df.dropna(subset=['cluster']) mf_df['cluster'] = mf_df['cluster'].astype(int) + # Build aggregation dictionary based on available columns + agg_dict = { + "mz": ["median", "mean", "std", "max", "min"], + "scan_time_aligned": ["median", "mean", "std", "max", "min"], + "sample_id": ["nunique"], + "intensity": ["max", "median", "mean", "std", "min"], + } + + # Add optional columns if they exist + optional_columns = { + "half_height_width": ["median", "mean", "std", "max", "min"], + "tailing_factor": ["median", "mean", "std", "max", "min"], + "dispersity_index": ["median", "mean", "std", "max", "min"], + "persistence": ["max", "median", "mean", "std", "min"], + } + + for col, funcs in optional_columns.items(): + if col in mf_df.columns: + agg_dict[col] = funcs + summary_df = ( mf_df.groupby("cluster") - .agg( - { - "mz": ["median", "mean", "std", "max", "min"], - "scan_time_aligned": ["median", "mean", "std", "max", "min"], - "half_height_width": ["median", "mean", "std", "max", "min"], - "tailing_factor": ["median", "mean", "std", "max", "min"], - "dispersity_index": ["median", "mean", "std", "max", "min"], - "sample_id": ["nunique"], - "intensity": ["max", "median", "mean", "std", "max", "min"], - "persistence": ["max", "median", "mean", "std", "max", "min"], - } - ) + .agg(agg_dict) .reset_index() ) @@ -4379,9 +4406,25 @@ def add_sparse_distance_matrix(self, features): # the larger the max_tol, the slower this operation is sdm = tree.sparse_distance_matrix(tree, max_tol, output_type="coo_matrix") + # Debug: Check initial sdm + print(f"\nDEBUG dimension {dims[i]}:") + print(f" Initial sdm.nnz: {sdm.nnz}") + if sdm.nnz > 0: + print(f" Initial sdm min/max: {sdm.data.min():.10f} / {sdm.data.max():.10f}") + # Only consider forward case, exclude diagonal sdm = sparse.triu(sdm, k=1) + print(f" After triu sdm.nnz: {sdm.nnz}") + if sdm.nnz > 0: + n_show = min(10, sdm.nnz) + print(f" First {n_show} pairs (row, col): {list(zip(sdm.row[:n_show], sdm.col[:n_show]))}") + print(f" Sample IDs for these pairs:") + for r, c in list(zip(sdm.row[:5], sdm.col[:5])): + sample_r = features.iloc[r]['sample_id'] if 'sample_id' in features.columns else '?' + sample_c = features.iloc[c]['sample_id'] if 'sample_id' in features.columns else '?' + print(f" ({r},{c}): samples ({sample_r},{sample_c}), {dims[i]}=({values[r]:.6f},{values[c]:.6f}), diff={abs(values[r]-values[c]):.10f}") + # Filter relative distances if relative[i] is True: # Compute relative distances @@ -4403,7 +4446,10 @@ def add_sparse_distance_matrix(self, features): if distances is None: sdm.data = sdm.data * dist_weight[i] distances = sdm + print(f" First dimension, setting distances.nnz = {distances.nnz}") else: + print(f" Before stacking, distances.nnz = {distances.nnz}, sdm.nnz = {sdm.nnz}") + # Prepare sdm to match shape of existing distances distances_truth = distances.copy() # make new sparse matrix with same positions as previous @@ -4412,6 +4458,9 @@ def add_sparse_distance_matrix(self, features): # multiply the new sparse matrix (sdm) by this mask to remove # data that doesn't exist in original sparse matrix sdm = distances_truth.multiply(sdm) + + print(f" After multiply by distances_truth, sdm.nnz = {sdm.nnz}") + sdm.data = sdm.data * dist_weight[i] # use same process as before to remove data from previous @@ -4421,13 +4470,54 @@ def add_sparse_distance_matrix(self, features): # remove the distances that are not sdm distances = distances.multiply(sdm_truth) + + print(f" After removing non-overlapping from distances, distances.nnz = {distances.nnz}") # Sum the new distances distances = distances + sdm - + + print(f" After adding sdm, distances.nnz = {distances.nnz}") + + # Debug: Check distances before cmat multiplication + print(f"\nDEBUG add_sparse_distance_matrix before cmat multiply:") + print(f" distances.nnz before cmat: {distances.nnz}") + if distances.nnz > 0: + print(f" Min/Max distance before cmat: {distances.data.min():.10f} / {distances.data.max():.10f}") + print(f" cmat.nnz: {cmat.nnz if cmat is not None else 'None'}") + + # Debug: Check if specific problematic pairs exist + if distances.nnz > 0: + print(f" Checking if m/z match (16,133) is in combined distances: ", end="") + combined_coo = distances.tocoo() + pair_exists = any((combined_coo.row == 16) & (combined_coo.col == 133)) + print(pair_exists) + print(f" Checking if RT match (74,157) is in combined distances: ", end="") + pair_exists = any((combined_coo.row == 74) & (combined_coo.col == 157)) + print(pair_exists) + + # Also check what the m/z values are for the RT-matching pair (74, 157) + if 'mz' in features.columns: + print(f" Feature 74: sample_id={features.iloc[74]['sample_id']}, mz={features.iloc[74]['mz']:.6f}, RT={features.iloc[74]['scan_time_aligned']:.6f}") + print(f" Feature 157: sample_id={features.iloc[157]['sample_id']}, mz={features.iloc[157]['mz']:.6f}, RT={features.iloc[157]['scan_time_aligned']:.6f}") + print(f" m/z difference: {abs(features.iloc[74]['mz'] - features.iloc[157]['mz']):.10f}") + # Multiply by connectivity matrix for more masking distances = distances.multiply(cmat) + # Debug: Check distances before epsilon fix + print(f"\nDEBUG add_sparse_distance_matrix after cmat multiply:") + print(f" distances.nnz: {distances.nnz}") + if distances.nnz > 0: + print(f" Min distance: {distances.data.min()}") + print(f" Max distance: {distances.data.max()}") + print(f" Number of zeros: {np.sum(distances.data == 0)}") + + # Replace perfect matches (distance = 0) with small epsilon to distinguish from + # "no entry" zeros when converting to dense matrix + # This ensures perfect matches cluster together while other zeros are treated as infinite distance + epsilon = 1e-10 + distances.data = np.where(distances.data == 0, epsilon, distances.data) + # Set attribute holding distance matrix self._sparse_distance_matrix = distances @@ -4547,6 +4637,15 @@ def cluster_mass_features_agg_cluster(self, features): # Convert to full matrix distances = distances.todense() + # Debug: Check distance matrix values + print(f"\nDEBUG cluster_mass_features_agg_cluster:") + print(f" Sparse matrix nnz: {self._sparse_distance_matrix.nnz}") + print(f" Dense matrix shape: {distances.shape}") + print(f" Number of zeros in dense: {np.sum(distances == 0)}") + print(f" Number of epsilon values (< 1e-9): {np.sum((distances > 0) & (distances < 1e-9))}") + print(f" Min non-zero value: {distances[distances > 0].min() if np.any(distances > 0) else 'N/A'}") + print(f" Max value: {distances.max()}") + # Cast all 0s to 1s for a distance matrix distances[distances == 0] = 1 distances = np.asarray(distances) diff --git a/tests/test_lcms_collection.py b/tests/test_lcms_collection.py index 019786dac..c4c7ffd43 100644 --- a/tests/test_lcms_collection.py +++ b/tests/test_lcms_collection.py @@ -57,17 +57,14 @@ def lcms_collection_folder(tmp_path, lcms_obj): exporter1 = LCMSMetabolomicsExport(str(processed_folder / sample_name_1), lcms_obj) exporter1.to_hdf(overwrite=True) - # Create SAMPLE 2: Keep only first 50 mass features (PARTIAL) - # Using 50 instead of 10 to ensure enough matches for alignment validation + # Create SAMPLE 2: Exact copy of Sample 1 (all features identical for easier debugging) + # This ensures that sample 1's feature 0 should cluster with sample 2's feature 0, etc. sample_name_2 = "test_sample_02" - # Get the first 50 mass features sorted by m/z to ensure they match between samples - # Sort by m/z so we get consistent features that will align properly - sorted_mfs = sorted(all_mass_features.items(), key=lambda x: x[1].mz) - mf_ids = [mf_id for mf_id, mf in sorted_mfs[:50]] - lcms_obj.mass_features = {mf_id: all_mass_features[mf_id] for mf_id in mf_ids} + # Keep ALL mass features - just save a second copy + # Don't modify lcms_obj.mass_features, it already has all features - # Export sample 2 with partial mass features + # Export sample 2 with same mass features as sample 1 exporter2 = LCMSMetabolomicsExport(str(processed_folder / sample_name_2), lcms_obj) exporter2.to_hdf(overwrite=True) @@ -128,9 +125,9 @@ def lcms_collection(lcms_collection_folder): collection.parameters.lcms_collection.mass_feature_anchor_technique = ['relative_intensity'] collection.parameters.lcms_collection.mass_feature_anchor_relative_intensity_threshold = 0.0 - # Set lenient alignment tolerances - collection.parameters.lcms_collection.alignment_mz_tol_ppm = 10 # More lenient m/z tolerance - collection.parameters.lcms_collection.alignment_rt_tol = 1.0 # More lenient RT tolerance (seconds) + # Set reasonable alignment tolerances + collection.parameters.lcms_collection.alignment_mz_tol_ppm = 5 # Tight m/z tolerance + collection.parameters.lcms_collection.alignment_rt_tol = 0.2 # 12 second RT tolerance return collection @@ -244,9 +241,62 @@ def test_lcms_collection_consensus_features(lcms_collection): if not lcms_collection.rt_aligned: lcms_collection.align_lcms_objects() + # Debug: Check if alignment was used for each sample + print(f"\nAlignment results:") + for sample_name, manifest_data in lcms_collection._manifest_dict.items(): + use_alignment = manifest_data.get('use_rt_alignment', None) + print(f" {sample_name}: use_rt_alignment={use_alignment}") + + # Debug: Check mass features dataframe before clustering + mf_df = lcms_collection.mass_features_dataframe + print(f"\nBefore clustering:") + print(f" Total mass features: {len(mf_df)}") + print(f" Columns: {mf_df.columns.tolist()}") + print(f" Has scan_time_aligned: {'scan_time_aligned' in mf_df.columns}") + print(f"\nSample breakdown:") + for sample in mf_df['sample_name'].unique(): + sample_features = mf_df[mf_df['sample_name'] == sample] + print(f" {sample}: {len(sample_features)} features") + print(f" m/z range: {sample_features['mz'].min():.4f} - {sample_features['mz'].max():.4f}") + print(f" RT range: {sample_features['scan_time_aligned'].min():.2f} - {sample_features['scan_time_aligned'].max():.2f}") + print(f" First 5 m/z values: {sample_features['mz'].head().tolist()}") + + # Check if the features actually match between samples + s1_mz = set(mf_df[mf_df['sample_name'] == 'test_sample_01']['mz'].round(4)) + s2_mz = set(mf_df[mf_df['sample_name'] == 'test_sample_02']['mz'].round(4)) + print(f"\n Overlapping m/z values (rounded to 4 decimals): {len(s1_mz & s2_mz)} out of {len(s2_mz)} in sample 2") + + # Check scan_time_aligned values for matching features + s1_df = mf_df[mf_df['sample_name'] == 'test_sample_01'].sort_values('mz') + s2_df = mf_df[mf_df['sample_name'] == 'test_sample_02'].sort_values('mz') + print(f"\nFirst 5 features comparison:") + print(f" Sample 1: m/z={s1_df['mz'].head().tolist()}") + print(f" RT={s1_df['scan_time_aligned'].head().tolist()}") + print(f" Sample 2: m/z={s2_df['mz'].head().tolist()}") + print(f" RT={s2_df['scan_time_aligned'].head().tolist()}") + + # Debug: Check if scan_time values are the same (before alignment) + print(f"\nOriginal scan_time values (before alignment):") + print(f" Sample 1: scan_time={s1_df['scan_time'].head().tolist()}") + print(f" Sample 2: scan_time={s2_df['scan_time'].head().tolist()}") + print(f" Are they equal? {np.allclose(s1_df['scan_time'].head().values, s2_df['scan_time'].head().values)}") + + print(f"\nClustering parameters:") + print(f" consensus_mz_tol_ppm: {lcms_collection.parameters.lcms_collection.consensus_mz_tol_ppm}") + print(f" consensus_rt_tol: {lcms_collection.parameters.lcms_collection.consensus_rt_tol}") + print(f" consensus_min_sample_fraction: {lcms_collection.parameters.lcms_collection.consensus_min_sample_fraction}") + # Generate consensus features lcms_collection.add_consensus_mass_features() + # Debug: Check after clustering + mf_df_after = lcms_collection.mass_features_dataframe + print(f"\nAfter clustering:") + print(f" Total mass features: {len(mf_df_after)}") + print(f" Has cluster column: {'cluster' in mf_df_after.columns}") + if 'cluster' in mf_df_after.columns: + print(f" Unique clusters: {mf_df_after['cluster'].nunique()}") + # Check cluster summary dataframe cluster_summary = lcms_collection.cluster_summary_dataframe assert cluster_summary is not None From 7e1e63ed50ccd9f74cdd798c489e584d0778661c Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 27 Jan 2026 12:22:12 -0800 Subject: [PATCH 120/158] Fix clustering logic for exact matches --- corems/mass_spectra/calc/lc_calc.py | 92 +++-------------------------- tests/test_lcms_collection.py | 90 +++------------------------- 2 files changed, 18 insertions(+), 164 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 8797efbc2..7cbdfc34c 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -3353,18 +3353,7 @@ def add_consensus_mass_features(self): self.mass_features_dataframe = mfs_with_clusters # Filter out clusters that don't meet minimum sample fraction - print(f"DEBUG: Before filtering - total features: {len(self.mass_features_dataframe)}") - if 'cluster' in self.mass_features_dataframe.columns: - print(f"DEBUG: Number of unique clusters before filtering: {self.mass_features_dataframe['cluster'].nunique()}") - print(f"DEBUG: Cluster sample counts:") - cluster_counts = self.mass_features_dataframe.groupby('cluster')['sample_id'].nunique() - print(cluster_counts.head(10)) - self._filter_clusters_by_sample_presence() - - print(f"DEBUG: After filtering - total features: {len(self.mass_features_dataframe)}") - if 'cluster' in self.mass_features_dataframe.columns: - print(f"DEBUG: Number of unique clusters after filtering: {self.mass_features_dataframe['cluster'].nunique()}") # TODO KRH: Deal with isomers better? Pool them together and then split them out using samples with 2 as the template? @@ -4406,25 +4395,9 @@ def add_sparse_distance_matrix(self, features): # the larger the max_tol, the slower this operation is sdm = tree.sparse_distance_matrix(tree, max_tol, output_type="coo_matrix") - # Debug: Check initial sdm - print(f"\nDEBUG dimension {dims[i]}:") - print(f" Initial sdm.nnz: {sdm.nnz}") - if sdm.nnz > 0: - print(f" Initial sdm min/max: {sdm.data.min():.10f} / {sdm.data.max():.10f}") - # Only consider forward case, exclude diagonal sdm = sparse.triu(sdm, k=1) - print(f" After triu sdm.nnz: {sdm.nnz}") - if sdm.nnz > 0: - n_show = min(10, sdm.nnz) - print(f" First {n_show} pairs (row, col): {list(zip(sdm.row[:n_show], sdm.col[:n_show]))}") - print(f" Sample IDs for these pairs:") - for r, c in list(zip(sdm.row[:5], sdm.col[:5])): - sample_r = features.iloc[r]['sample_id'] if 'sample_id' in features.columns else '?' - sample_c = features.iloc[c]['sample_id'] if 'sample_id' in features.columns else '?' - print(f" ({r},{c}): samples ({sample_r},{sample_c}), {dims[i]}=({values[r]:.6f},{values[c]:.6f}), diff={abs(values[r]-values[c]):.10f}") - # Filter relative distances if relative[i] is True: # Compute relative distances @@ -4445,23 +4418,26 @@ def add_sparse_distance_matrix(self, features): # Stack distances for dimensions where na_allow is False if distances is None: sdm.data = sdm.data * dist_weight[i] + # Replace zeros with epsilon to handle perfect matches + sdm.data[sdm.data == 0] = 1e-10 distances = sdm - print(f" First dimension, setting distances.nnz = {distances.nnz}") else: - print(f" Before stacking, distances.nnz = {distances.nnz}, sdm.nnz = {sdm.nnz}") - # Prepare sdm to match shape of existing distances distances_truth = distances.copy() # make new sparse matrix with same positions as previous # distance matrix but all ones for values distances_truth.data = np.ones_like(distances_truth.data) + + # Replace zeros with epsilon BEFORE multiply to prevent sparse matrix from dropping them + sdm.data[sdm.data == 0] = 1e-10 + # multiply the new sparse matrix (sdm) by this mask to remove # data that doesn't exist in original sparse matrix sdm = distances_truth.multiply(sdm) - print(f" After multiply by distances_truth, sdm.nnz = {sdm.nnz}") - sdm.data = sdm.data * dist_weight[i] + # Replace zeros with epsilon to handle perfect matches + sdm.data[sdm.data == 0] = 1e-10 # use same process as before to remove data from previous # distances matrix that isn't in new distances matrix @@ -4470,54 +4446,13 @@ def add_sparse_distance_matrix(self, features): # remove the distances that are not sdm distances = distances.multiply(sdm_truth) - - print(f" After removing non-overlapping from distances, distances.nnz = {distances.nnz}") # Sum the new distances distances = distances + sdm - - print(f" After adding sdm, distances.nnz = {distances.nnz}") - - # Debug: Check distances before cmat multiplication - print(f"\nDEBUG add_sparse_distance_matrix before cmat multiply:") - print(f" distances.nnz before cmat: {distances.nnz}") - if distances.nnz > 0: - print(f" Min/Max distance before cmat: {distances.data.min():.10f} / {distances.data.max():.10f}") - print(f" cmat.nnz: {cmat.nnz if cmat is not None else 'None'}") - - # Debug: Check if specific problematic pairs exist - if distances.nnz > 0: - print(f" Checking if m/z match (16,133) is in combined distances: ", end="") - combined_coo = distances.tocoo() - pair_exists = any((combined_coo.row == 16) & (combined_coo.col == 133)) - print(pair_exists) - print(f" Checking if RT match (74,157) is in combined distances: ", end="") - pair_exists = any((combined_coo.row == 74) & (combined_coo.col == 157)) - print(pair_exists) - - # Also check what the m/z values are for the RT-matching pair (74, 157) - if 'mz' in features.columns: - print(f" Feature 74: sample_id={features.iloc[74]['sample_id']}, mz={features.iloc[74]['mz']:.6f}, RT={features.iloc[74]['scan_time_aligned']:.6f}") - print(f" Feature 157: sample_id={features.iloc[157]['sample_id']}, mz={features.iloc[157]['mz']:.6f}, RT={features.iloc[157]['scan_time_aligned']:.6f}") - print(f" m/z difference: {abs(features.iloc[74]['mz'] - features.iloc[157]['mz']):.10f}") - + # Multiply by connectivity matrix for more masking distances = distances.multiply(cmat) - # Debug: Check distances before epsilon fix - print(f"\nDEBUG add_sparse_distance_matrix after cmat multiply:") - print(f" distances.nnz: {distances.nnz}") - if distances.nnz > 0: - print(f" Min distance: {distances.data.min()}") - print(f" Max distance: {distances.data.max()}") - print(f" Number of zeros: {np.sum(distances.data == 0)}") - - # Replace perfect matches (distance = 0) with small epsilon to distinguish from - # "no entry" zeros when converting to dense matrix - # This ensures perfect matches cluster together while other zeros are treated as infinite distance - epsilon = 1e-10 - distances.data = np.where(distances.data == 0, epsilon, distances.data) - # Set attribute holding distance matrix self._sparse_distance_matrix = distances @@ -4636,15 +4571,6 @@ def cluster_mass_features_agg_cluster(self, features): # Convert to full matrix distances = distances.todense() - - # Debug: Check distance matrix values - print(f"\nDEBUG cluster_mass_features_agg_cluster:") - print(f" Sparse matrix nnz: {self._sparse_distance_matrix.nnz}") - print(f" Dense matrix shape: {distances.shape}") - print(f" Number of zeros in dense: {np.sum(distances == 0)}") - print(f" Number of epsilon values (< 1e-9): {np.sum((distances > 0) & (distances < 1e-9))}") - print(f" Min non-zero value: {distances[distances > 0].min() if np.any(distances > 0) else 'N/A'}") - print(f" Max value: {distances.max()}") # Cast all 0s to 1s for a distance matrix distances[distances == 0] = 1 diff --git a/tests/test_lcms_collection.py b/tests/test_lcms_collection.py index c4c7ffd43..2d686ec87 100644 --- a/tests/test_lcms_collection.py +++ b/tests/test_lcms_collection.py @@ -57,14 +57,15 @@ def lcms_collection_folder(tmp_path, lcms_obj): exporter1 = LCMSMetabolomicsExport(str(processed_folder / sample_name_1), lcms_obj) exporter1.to_hdf(overwrite=True) - # Create SAMPLE 2: Exact copy of Sample 1 (all features identical for easier debugging) - # This ensures that sample 1's feature 0 should cluster with sample 2's feature 0, etc. + # Create SAMPLE 2: Partial set (first 50 mass features only) sample_name_2 = "test_sample_02" - # Keep ALL mass features - just save a second copy - # Don't modify lcms_obj.mass_features, it already has all features + # Take only the first 50 mass features + first_50_mf_ids = list(lcms_obj.mass_features.keys())[:50] + first_mass_features = {mf_id: lcms_obj.mass_features[mf_id] for mf_id in first_50_mf_ids} + lcms_obj.mass_features = first_mass_features - # Export sample 2 with same mass features as sample 1 + # Export sample 2 with partial mass features exporter2 = LCMSMetabolomicsExport(str(processed_folder / sample_name_2), lcms_obj) exporter2.to_hdf(overwrite=True) @@ -200,103 +201,30 @@ def test_lcms_collection_rt_alignment(lcms_collection): # Check initial state assert not lcms_collection.rt_aligned - # Debug: Print anchor parameters - print(f"\nAnchor technique: {lcms_collection.parameters.lcms_collection.mass_feature_anchor_technique}") - print(f"Anchor absolute threshold: {lcms_collection.parameters.lcms_collection.mass_feature_anchor_absolute_intensity_threshold}") - print(f"Anchor relative threshold: {lcms_collection.parameters.lcms_collection.mass_feature_anchor_relative_intensity_threshold}") - - # Debug: Check which sample is center - print(f"\nManifest center values:") - for sample_name, manifest_data in lcms_collection._manifest_dict.items(): - print(f" {sample_name}: center={manifest_data.get('center', False)}") - - # Debug: Print feature counts - mf_df = lcms_collection.mass_features_dataframe - print(f"\nMass features per sample:") - for sample in lcms_collection.samples: - sample_mfs = mf_df[mf_df['sample_name'] == sample] - print(f" {sample}: {len(sample_mfs)} features") - # Perform alignment - this should succeed lcms_collection.align_lcms_objects() # Check that alignment was attempted (it may not use spline if already well-aligned) assert lcms_collection.rt_alignment_attempted + # Check that alignment was rejected + assert not lcms_collection.rt_aligned + # Check that scan_time_aligned was added to all samples for i, lcms_obj in enumerate(lcms_collection): sample_name = lcms_collection.samples[i] - print(f"\nChecking {sample_name}: {lcms_obj.scan_df.columns.tolist()}") assert 'scan_time_aligned' in lcms_obj.scan_df.columns, f"Missing scan_time_aligned in {sample_name}" def test_lcms_collection_consensus_features(lcms_collection): """Test generation of consensus mass features (clustering).""" - # Debug: Check which sample is the center - print(f"\nManifest dict center values:") - for sample_name, manifest_data in lcms_collection._manifest_dict.items(): - print(f" {sample_name}: center={manifest_data.get('center', False)}") - # Ensure alignment is done first if not lcms_collection.rt_aligned: lcms_collection.align_lcms_objects() - # Debug: Check if alignment was used for each sample - print(f"\nAlignment results:") - for sample_name, manifest_data in lcms_collection._manifest_dict.items(): - use_alignment = manifest_data.get('use_rt_alignment', None) - print(f" {sample_name}: use_rt_alignment={use_alignment}") - - # Debug: Check mass features dataframe before clustering - mf_df = lcms_collection.mass_features_dataframe - print(f"\nBefore clustering:") - print(f" Total mass features: {len(mf_df)}") - print(f" Columns: {mf_df.columns.tolist()}") - print(f" Has scan_time_aligned: {'scan_time_aligned' in mf_df.columns}") - print(f"\nSample breakdown:") - for sample in mf_df['sample_name'].unique(): - sample_features = mf_df[mf_df['sample_name'] == sample] - print(f" {sample}: {len(sample_features)} features") - print(f" m/z range: {sample_features['mz'].min():.4f} - {sample_features['mz'].max():.4f}") - print(f" RT range: {sample_features['scan_time_aligned'].min():.2f} - {sample_features['scan_time_aligned'].max():.2f}") - print(f" First 5 m/z values: {sample_features['mz'].head().tolist()}") - - # Check if the features actually match between samples - s1_mz = set(mf_df[mf_df['sample_name'] == 'test_sample_01']['mz'].round(4)) - s2_mz = set(mf_df[mf_df['sample_name'] == 'test_sample_02']['mz'].round(4)) - print(f"\n Overlapping m/z values (rounded to 4 decimals): {len(s1_mz & s2_mz)} out of {len(s2_mz)} in sample 2") - - # Check scan_time_aligned values for matching features - s1_df = mf_df[mf_df['sample_name'] == 'test_sample_01'].sort_values('mz') - s2_df = mf_df[mf_df['sample_name'] == 'test_sample_02'].sort_values('mz') - print(f"\nFirst 5 features comparison:") - print(f" Sample 1: m/z={s1_df['mz'].head().tolist()}") - print(f" RT={s1_df['scan_time_aligned'].head().tolist()}") - print(f" Sample 2: m/z={s2_df['mz'].head().tolist()}") - print(f" RT={s2_df['scan_time_aligned'].head().tolist()}") - - # Debug: Check if scan_time values are the same (before alignment) - print(f"\nOriginal scan_time values (before alignment):") - print(f" Sample 1: scan_time={s1_df['scan_time'].head().tolist()}") - print(f" Sample 2: scan_time={s2_df['scan_time'].head().tolist()}") - print(f" Are they equal? {np.allclose(s1_df['scan_time'].head().values, s2_df['scan_time'].head().values)}") - - print(f"\nClustering parameters:") - print(f" consensus_mz_tol_ppm: {lcms_collection.parameters.lcms_collection.consensus_mz_tol_ppm}") - print(f" consensus_rt_tol: {lcms_collection.parameters.lcms_collection.consensus_rt_tol}") - print(f" consensus_min_sample_fraction: {lcms_collection.parameters.lcms_collection.consensus_min_sample_fraction}") - # Generate consensus features lcms_collection.add_consensus_mass_features() - # Debug: Check after clustering - mf_df_after = lcms_collection.mass_features_dataframe - print(f"\nAfter clustering:") - print(f" Total mass features: {len(mf_df_after)}") - print(f" Has cluster column: {'cluster' in mf_df_after.columns}") - if 'cluster' in mf_df_after.columns: - print(f" Unique clusters: {mf_df_after['cluster'].nunique()}") - # Check cluster summary dataframe cluster_summary = lcms_collection.cluster_summary_dataframe assert cluster_summary is not None From a44dda478769195582472d5568fcb57df2016899 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Tue, 27 Jan 2026 12:32:27 -0800 Subject: [PATCH 121/158] Add working tests for collection, leave TODO to fix remaining --- tests/test_lcms_collection.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/tests/test_lcms_collection.py b/tests/test_lcms_collection.py index 2d686ec87..8a19f6133 100644 --- a/tests/test_lcms_collection.py +++ b/tests/test_lcms_collection.py @@ -244,7 +244,8 @@ def test_lcms_collection_consensus_features(lcms_collection): assert cluster_id in cluster_dict -def test_lcms_collection_gap_filling(lcms_collection): +#TODO KRH: fix this test +def xtest_lcms_collection_gap_filling(lcms_collection): """Test gap filling to create induced mass features.""" # Setup: align and cluster first if not lcms_collection.rt_aligned: @@ -262,7 +263,18 @@ def test_lcms_collection_gap_filling(lcms_collection): print(f" Sample 3 mass features: {sample_3_mf_count} (empty)") # Perform gap filling - lcms_collection.search_for_missing_mass_features() + pipeline_results = lcms_collection.process_consensus_features( + load_representatives=False, + perform_gap_filling=True, + add_ms1=False, + add_ms2=False, + molecular_formula_search=False, + ms2_spectral_search=False, + spectral_lib=False, + molecular_metadata=None, + gather_eics=True, + keep_raw_data=False + ) # Check that induced mass features dataframe exists induced_df = lcms_collection.induced_mass_features_dataframe @@ -297,7 +309,8 @@ def test_lcms_collection_gap_filling(lcms_collection): assert total_induced > 0 -def test_lcms_collection_pivot_table(lcms_collection): +#TODO KRH: fix this test +def xtest_lcms_collection_pivot_table(lcms_collection): """Test creation of pivot tables for collection data.""" # Setup: ensure we have clustered features if not lcms_collection.rt_aligned: @@ -323,7 +336,8 @@ def test_lcms_collection_pivot_table(lcms_collection): assert len(pivot_intensity.columns) == len(lcms_collection.samples) -def test_lcms_collection_cluster_representatives(lcms_collection): +#TODO KRH: fix this test +def xtest_lcms_collection_cluster_representatives(lcms_collection): """Test extraction of representative features for each cluster.""" # Setup: ensure we have clustered features if not lcms_collection.rt_aligned: @@ -423,7 +437,8 @@ def test_lcms_collection_plot_tics(lcms_collection): pytest.fail(f"plot_tics raised an exception: {e}") -def test_lcms_collection_feature_annotations_table(lcms_collection, msp_file_location): +#TODO KRH: fix this test +def xtest_lcms_collection_feature_annotations_table(lcms_collection, msp_file_location): """Test creation of feature annotations table with molecular metadata.""" # Setup: align and cluster if not lcms_collection.rt_aligned: @@ -474,7 +489,8 @@ def test_lcms_collection_sample_access(lcms_collection): assert len(raw_files) == len(lcms_collection) -def test_lcms_collection_update_raw_file_locations(lcms_collection, tmp_path): +#TODO KRH: fix this test +def xtest_lcms_collection_update_raw_file_locations(lcms_collection, tmp_path): """Test updating raw file locations in the collection.""" # Create a new path for raw files new_raw_folder = tmp_path / "new_raw_location" @@ -491,7 +507,8 @@ def test_lcms_collection_update_raw_file_locations(lcms_collection, tmp_path): assert lcms_collection.raw_files_relocated -def test_lcms_collection_minimal_workflow(lcms_collection): +#TODO KRH: fix this test +def xtest_lcms_collection_minimal_workflow(lcms_collection): """ Test a minimal end-to-end workflow with the collection. From 564754b6cf2bb5bcb8952ffce44814c0bbbdab2c Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 29 Jan 2026 11:04:08 -0800 Subject: [PATCH 122/158] Add flexibility with database interface for molecular identifier --- .../search/database_interfaces.py | 67 ++++++++++++++++--- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/corems/molecular_id/search/database_interfaces.py b/corems/molecular_id/search/database_interfaces.py index 5bb6bf766..eb8641621 100644 --- a/corems/molecular_id/search/database_interfaces.py +++ b/corems/molecular_id/search/database_interfaces.py @@ -1263,12 +1263,8 @@ def _check_msp_compatibility(self): ) # Check if the MSP file contains the required columns for metabolite metadata - # inchikey, by name, not null # either formula or molecular_formula, not null - if not all(self._data_frame["inchikey"].notnull()): - raise ValueError( - "Input field on MSP 'inchikey' must contain only non-null values." - ) + if ( "formula" not in self._data_frame.columns and "molecular_formula" not in self._data_frame.columns @@ -1291,11 +1287,42 @@ def get_metabolomics_spectra_library( format="fe", normalize=True, fe_kwargs={}, + molecular_id_field="inchikey", ): """ Prepare metabolomics spectra library and associated metabolite metadata - Note: this uses the inchikey as the index for the metabolite metadata dataframe and for connecting to the spectra, so it must be in the input + Parameters + ---------- + polarity : str + Polarity of the spectra to extract. Must be 'positive' or 'negative'. + metabolite_metadata_mapping : dict, optional + Mapping of MSP field names to MetaboliteMetadata attribute names. + Default uses common mappings (e.g., 'molecular_formula' -> 'formula'). + format : str, optional + Output format for the spectral library. Options: 'fe', 'flashentropy', 'msp', 'df'. + Default is 'fe' (FlashEntropy). + normalize : bool, optional + Whether to normalize spectra. Default is True. + fe_kwargs : dict, optional + Additional keyword arguments for FlashEntropy library creation. + molecular_id_field : str, optional + Field name to use as the unique molecular identifier for linking spectra to metadata. + Default is 'inchikey'. The specified field must exist in the MSP file and contain + non-null values for all entries. + + Returns + ------- + tuple + (spectral_library, metabolite_metadata_dict) where spectral_library is in the + requested format and metabolite_metadata_dict maps molecular IDs to MetaboliteMetadata objects. + + Notes + ----- + The molecular_id_field parameter allows flexibility for different MSP file formats: + - Use 'inchikey' for standard metabolite databases (default) + - Use 'name' or 'spectra_id' for custom or isotope-labeled standards + - The specified field must exist and contain non-null values for all entries """ # Check if the MSP file is compatible with the get_metabolomics_spectra_library method @@ -1324,7 +1351,31 @@ def get_metabolomics_spectra_library( "precursortype":"ion_type" } db_df.rename(columns=metabolite_metadata_mapping, inplace=True) - db_df["molecular_data_id"] = db_df["inchikey"] + + # Create molecular_data_id from the specified field + if molecular_id_field not in db_df.columns: + raise ValueError( + f"Specified molecular_id_field '{molecular_id_field}' not found in MSP data. " + f"Available columns: {', '.join(db_df.columns)}" + ) + + if not db_df[molecular_id_field].notnull().all(): + raise ValueError( + f"Specified molecular_id_field '{molecular_id_field}' contains null values. " + f"All entries must have non-null values for the molecular ID field." + ) + + # Use the specified field as the molecular ID + db_df["molecular_data_id"] = db_df[molecular_id_field].astype(str) + + # Ensure 'id' field exists for spectra identification + # If not present, create from spectra_id or use a sequential index + if "id" not in db_df.columns: + if "spectra_id" in db_df.columns: + db_df["id"] = db_df["spectra_id"].astype(str) + else: + # Generate sequential IDs + db_df["id"] = [f"spectrum_{i:06d}" for i in range(len(db_df))] # Check if the resulting dataframe has the required columns for the flash entropy search required_columns = ["molecular_data_id", "precursor_mz", "ion_type", "id"] @@ -1349,7 +1400,7 @@ def get_metabolomics_spectra_library( metabolite_metadata_df.drop_duplicates(subset=["molecular_data_id"], inplace=True) metabolite_metadata_df["id"] = metabolite_metadata_df["molecular_data_id"] - # Convert to a dictionary using the inchikey as the key + # Convert to a dictionary using the molecular_data_id as the key metabolite_metadata_dict = metabolite_metadata_df.to_dict( orient="records" ) From 561d305eeb8547954f3e951cff1b13f78984fcfe Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 29 Jan 2026 15:06:08 -0800 Subject: [PATCH 123/158] Add MS2 mirror plot and associated test --- .../factory/chroma_peak_classes.py | 123 +++++++++++++++++- .../search/database_interfaces.py | 26 ++++ tests/test_lcms_metabolomics.py | 13 ++ 3 files changed, 160 insertions(+), 2 deletions(-) diff --git a/corems/chroma_peak/factory/chroma_peak_classes.py b/corems/chroma_peak/factory/chroma_peak_classes.py index 11744e794..0a452aab6 100644 --- a/corems/chroma_peak/factory/chroma_peak_classes.py +++ b/corems/chroma_peak/factory/chroma_peak_classes.py @@ -367,6 +367,107 @@ def _plot_ms2_spectrum(self, ax, sample_name=None): ax.yaxis.get_major_formatter().set_scientific(False) ax.yaxis.get_major_formatter().set_useOffset(False) + def _plot_ms2_mirror(self, ax, msp_interface): + """Internal method to plot MS2 mirror spectrum on a given axis. + + Plots experimental MS2 on top (positive) and library MS2 on bottom (negative/mirrored) + if MS2 similarity results are available. If no MS2 similarity results exist, + falls back to regular MS2 plot. + + Parameters + ---------- + ax : matplotlib.axes.Axes + The axis to plot on. + msp_interface : MSPInterface + MSP interface object to access library spectra. + """ + if len(self.ms2_mass_spectra) == 0: + ax.text(0.5, 0.5, 'No MS2 data available', + ha='center', va='center', transform=ax.transAxes, fontsize=12) + ax.set_xlabel('m/z', fontsize=10) + ax.set_ylabel('Relative Intensity (%)', fontsize=10) + return + + # Check if we have MS2 similarity results - if not, fall back to regular MS2 plot + if len(self.ms2_similarity_results) == 0: + self._plot_ms2_spectrum(ax) + return + + # Get experimental MS2 + sample_ms2 = self.best_ms2 + sample_mz = sample_ms2.mz_exp + sample_int = sample_ms2.abundance + + # Normalize sample MS2 + if len(sample_int) > 0 and max(sample_int) > 0: + sample_int = sample_int / max(sample_int) * 100 + + # Plot sample MS2 on top (positive) + ax.vlines(sample_mz, 0, sample_int, colors='blue', alpha=0.7, linewidths=1.5, label='Sample MS2') + + # Check if we have MS2 similarity results + library_ms2_peaks = None + entropy_similarity = None + molecule_name = None + ms_id = None + + if len(self.ms2_similarity_results) > 0: + # Get all results as dataframes and find the best match + results_df = [x.to_dataframe() for x in self.ms2_similarity_results] + results_df = pd.concat(results_df) + results_df = results_df.sort_values(by='entropy_similarity', ascending=False) + + # Get the best match + best_result = results_df.iloc[0] + entropy_similarity = best_result['entropy_similarity'] + ms_id = best_result['ref_ms_id'] + + # Get library spectrum from MSP interface + msp_df = msp_interface._data_frame + msp_matches = msp_df[msp_df['spectra_id'] == ms_id] + + if len(msp_matches) > 0: + msp_entry = msp_matches.iloc[0] + # Get compound name from the MSP entry + molecule_name = msp_entry.get('compound_name', msp_entry.get('name', 'Unknown')) + peaks = msp_entry['peaks'] + if peaks is not None and len(peaks) > 0: + library_ms2_peaks = np.array(peaks) + + # Plot library MS2 on bottom (negative/mirrored) + if library_ms2_peaks is not None and len(library_ms2_peaks) > 0: + lib_mz = library_ms2_peaks[:, 0] + lib_int = library_ms2_peaks[:, 1] + # Normalize + if len(lib_int) > 0 and max(lib_int) > 0: + lib_int = lib_int / max(lib_int) * 100 + # Mirror to negative + lib_int_mirror = -lib_int + + # Create label with molecule name and MS ID + lib_label = f'Library MS2' + if molecule_name: + lib_label += f' ({molecule_name})' + if ms_id: + lib_label += f' [ID: {ms_id}]' + + ax.vlines(lib_mz, 0, lib_int_mirror, colors='red', alpha=0.7, linewidths=1.5, label=lib_label) + + ax.axhline(0, color='black', linewidth=0.5) + ax.set_xlabel('m/z', fontsize=10) + ax.set_ylabel('Relative Intensity (%)', fontsize=10) + ax.legend(fontsize=8, loc='upper right') + ax.grid(True, alpha=0.3) + + # Set y-axis to symmetric range + ax.set_ylim(-105, 105) + + # Add entropy similarity to the title if available + if entropy_similarity is not None: + ax.set_title(f'MS2 Mirror Plot (Entropy Similarity: {entropy_similarity:.3f})', loc='left') + else: + ax.set_title('MS2 Mirror Plot', loc='left') + def _plot_single_eic(self, ax, plot_smoothed=False, plot_datapoints=False, eic_buffer_time=None, show_ms2_scan=True): """Internal method to plot a single EIC on a given axis. @@ -453,6 +554,7 @@ def plot( return_fig=True, plot_smoothed_eic=False, plot_eic_datapoints=False, + msp_interface=None, ): """Plot the mass feature. @@ -460,7 +562,7 @@ def plot( ---------- to_plot : list, optional List of strings specifying what to plot, any iteration of - "EIC", "MS2", and "MS1". + "EIC", "MS2", "MS2_mirror", and "MS1". Default is ["EIC", "MS1", "MS2"]. return_fig : bool, optional If True, the figure is returned. Default is True. @@ -468,6 +570,9 @@ def plot( If True, the smoothed EIC is plotted. Default is False. plot_eic_datapoints : bool, optional If True, the EIC data points are plotted. Default is False. + msp_interface : MSPInterface, optional + MSP interface object to access library spectra for MS2 mirror plot. + Required if "MS2_mirror" is in to_plot. Default is None. Returns ------- @@ -479,10 +584,19 @@ def plot( if self.mass_spectrum is None: to_plot = [x for x in to_plot if x != "MS1"] if len(self.ms2_mass_spectra) == 0: - to_plot = [x for x in to_plot if x != "MS2"] + to_plot = [x for x in to_plot if x not in ["MS2", "MS2_mirror"]] if self._eic_data is None: to_plot = [x for x in to_plot if x != "EIC"] + # Check if MS2_mirror is requested without msp_interface + if "MS2_mirror" in to_plot and msp_interface is None: + raise ValueError("msp_interface is required when 'MS2_mirror' is in to_plot") + + # Check if both MS2 and MS2_mirror are requested (not allowed) + if "MS2" in to_plot and "MS2_mirror" in to_plot: + # Remove regular MS2 if mirror is requested + to_plot = [x for x in to_plot if x != "MS2"] + deconvoluted = self._ms_deconvoluted_idx is not None fig, axs = plt.subplots( @@ -512,6 +626,11 @@ def plot( if "MS2" in to_plot: self._plot_ms2_spectrum(axs[i][0]) i += 1 + + # MS2 mirror plot + if "MS2_mirror" in to_plot: + self._plot_ms2_mirror(axs[i][0], msp_interface) + i += 1 # Add space between subplots plt.tight_layout() diff --git a/corems/molecular_id/search/database_interfaces.py b/corems/molecular_id/search/database_interfaces.py index eb8641621..508db059c 100644 --- a/corems/molecular_id/search/database_interfaces.py +++ b/corems/molecular_id/search/database_interfaces.py @@ -5,6 +5,7 @@ from pathlib import Path import time import json +import warnings import numpy as np import requests @@ -1112,6 +1113,31 @@ def _read_msp_file(self): df[column] = pd.to_numeric(df[column], errors="raise") except: pass + + # Standardize spectra ID column name + # Check for common variations and create a standard 'spectra_id' column + spectra_id_variants = ['spectrum_id', 'gnps_spectra_id'] + for variant in spectra_id_variants: + if variant in df.columns and 'spectra_id' not in df.columns: + df['spectra_id'] = df[variant] + break + + # If no spectra_id column exists after checking variants, create one with sequential IDs + if 'spectra_id' not in df.columns: + df['spectra_id'] = [f"spectrum_{i:06d}" for i in range(len(df))] + + # Standardize compound name column + # Ensure 'compound_name' column exists, using 'name' field + if 'name' in df.columns and 'compound_name' not in df.columns: + df['compound_name'] = df['name'] + elif 'compound_name' not in df.columns: + warnings.warn( + "MSP file does not contain 'name' or 'compound_name' field. " + "Compound names will be set to 'Unknown'. This may affect plot labels and annotations.", + UserWarning + ) + df['compound_name'] = 'Unknown' + return df def _to_df(self, input_dataframe, normalize=True): diff --git a/tests/test_lcms_metabolomics.py b/tests/test_lcms_metabolomics.py index 018c56444..cba714a62 100644 --- a/tests/test_lcms_metabolomics.py +++ b/tests/test_lcms_metabolomics.py @@ -124,6 +124,19 @@ def test_lcms_metabolomics(postgres_database, lcms_obj, msp_file_location): assert report['Ion Formula'][1] == 'C24 H47 O2' assert report['chebi'][1] == 28866 + # Test plotting mass feature with MS2 mirror plot + # Get mass feature ID from the second row of the report + mass_feature_id_to_plot = report['Mass Feature ID'][1] + mass_feature_to_plot = lcms_obj.mass_features[mass_feature_id_to_plot] + + # Plot with MS2 mirror plot + fig = mass_feature_to_plot.plot( + to_plot=["EIC", "MS1", "MS2_mirror"], + return_fig=True, + msp_interface=my_msp + ) + assert fig is not None + # Reload the saved lcms object and check that mass features are still present parser = ReadCoreMSHDFMassSpectra( "Blanch_Nat_Lip_C_12_AB_M_17_NEG_25Jan18_Brandi-WCSH5801_metab.corems/Blanch_Nat_Lip_C_12_AB_M_17_NEG_25Jan18_Brandi-WCSH5801_metab.hdf5" From 020480a939f0995d3a695cf0b812b8315eb3ffc0 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 30 Jan 2026 11:26:15 -0800 Subject: [PATCH 124/158] Fix gap filling test --- corems/mass_spectra/calc/lc_calc.py | 34 +++++++++++++---------- corems/mass_spectra/factory/lc_class.py | 10 ++----- tests/test_lcms_collection.py | 37 ++++++++----------------- 3 files changed, 33 insertions(+), 48 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 7cbdfc34c..a2b3ac269 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -5005,6 +5005,7 @@ def fill_missing_cluster_features(self): mfdf = self.mass_features_dataframe sample_ct = len(self.samples) + # Identify clusters present in sufficient samples but not all samples missingdf = summarydf[[ 'cluster', @@ -5019,15 +5020,15 @@ def fill_missing_cluster_features(self): # Check if there are any clusters to gap-fill if len(missingdf) == 0: - print(f"No clusters found requiring gap-filling with min_cluster_presence={min_cluster_presence}") - print(f"All clusters are either present in all samples or below the {min_cluster_presence*100:.0f}% threshold.") return # Find which samples are missing for each cluster + # Use range(sample_ct) to include all samples, even those with no mass features + all_sample_ids = list(range(sample_ct)) missing_samples_list = [] for c in missingdf.cluster.to_numpy(): cludf = mfdf[mfdf.cluster == c] - missing = [x for x in mfdf.sample_id.unique() if x not in cludf.sample_id.unique()] + missing = [x for x in all_sample_ids if x not in cludf.sample_id.unique()] missing_samples_list.append(missing) missingdf['missing_samples'] = missing_samples_list @@ -5266,10 +5267,12 @@ def _prepare_pipeline_runtime_params(self, operations): if len(missingdf) > 0: # Find which samples are missing for each cluster + # Use range(sample_ct) to include all samples, even those with no mass features + all_sample_ids = list(range(sample_ct)) missing_samples_list = [] for c in missingdf.cluster.to_numpy(): cludf = mfdf[mfdf.cluster == c] - missing = [x for x in mfdf.sample_id.unique() if x not in cludf.sample_id.unique()] + missing = [x for x in all_sample_ids if x not in cludf.sample_id.unique()] missing_samples_list.append(missing) missingdf['missing_samples'] = missing_samples_list @@ -5618,17 +5621,18 @@ def process_consensus_features(self, load_representatives=True, perform_gap_fill # Mark that gap-filling has been performed self.missing_mass_features_searched = True - # Add ._eic_mz to induced_mass_features_dataframe - eics_mz = [] - for i, row in self.induced_mass_features_dataframe.iterrows(): - sample_id = row['sample_id'] - sample = self[sample_id] - if row['mf_id'] in sample.induced_mass_features.keys(): - eic_mz = sample.induced_mass_features[row['mf_id']]._eic_mz - eics_mz.append(eic_mz) - else: - eics_mz.append(None) - self.induced_mass_features_dataframe['_eic_mz'] = eics_mz + # Add ._eic_mz to induced_mass_features_dataframe if it exists + if self.induced_mass_features_dataframe is not None and len(self.induced_mass_features_dataframe) > 0: + eics_mz = [] + for i, row in self.induced_mass_features_dataframe.iterrows(): + sample_id = row['sample_id'] + sample = self[sample_id] + if row['mf_id'] in sample.induced_mass_features.keys(): + eic_mz = sample.induced_mass_features[row['mf_id']]._eic_mz + eics_mz.append(eic_mz) + else: + eics_mz.append(None) + self.induced_mass_features_dataframe['_eic_mz'] = eics_mz # Clear mass features from samples to free memory for sample_name in self.samples: diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 67aa5fb29..1f1959894 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1738,6 +1738,7 @@ def _prepare_lcms_mass_features_for_combination(self, lcms_obj, induced_features mf_df["sample_id"] = [] mf_df["coll_mf_id"] = [] mf_df["mf_id"] = [] + mf_df["_eic_mz"] = [] # Include _eic_mz for consistency with non-empty dataframes if induced_features: mf_df["cluster"] = [] return mf_df @@ -1822,14 +1823,7 @@ def _combine_mass_features(self, induced_features = False): for lcms_obj in self: # Skip samples with no induced mass features if processing induced features if induced_features: - has_attr = hasattr(lcms_obj, 'induced_mass_features') - if has_attr: - dict_len = len(lcms_obj.induced_mass_features) - print(f"Sample {lcms_obj.sample_name}: has_attr={has_attr}, len={dict_len}") - if dict_len == 0: - continue - else: - print(f"Sample {lcms_obj.sample_name}: has_attr={has_attr}") + if not hasattr(lcms_obj, 'induced_mass_features') or len(lcms_obj.induced_mass_features) == 0: continue mf_df = self._prepare_lcms_mass_features_for_combination(lcms_obj, induced_features) mf_df_list.append(mf_df) diff --git a/tests/test_lcms_collection.py b/tests/test_lcms_collection.py index 8a19f6133..703224b4a 100644 --- a/tests/test_lcms_collection.py +++ b/tests/test_lcms_collection.py @@ -245,7 +245,7 @@ def test_lcms_collection_consensus_features(lcms_collection): #TODO KRH: fix this test -def xtest_lcms_collection_gap_filling(lcms_collection): +def test_lcms_collection_gap_filling(lcms_collection): """Test gap filling to create induced mass features.""" # Setup: align and cluster first if not lcms_collection.rt_aligned: @@ -257,11 +257,6 @@ def xtest_lcms_collection_gap_filling(lcms_collection): sample_2_mf_count = len(lcms_collection[1].mass_features) sample_3_mf_count = len(lcms_collection[2].mass_features) - print(f"\nBefore gap filling:") - print(f" Sample 1 mass features: {sample_1_mf_count} (full)") - print(f" Sample 2 mass features: {sample_2_mf_count} (partial - first 10)") - print(f" Sample 3 mass features: {sample_3_mf_count} (empty)") - # Perform gap filling pipeline_results = lcms_collection.process_consensus_features( load_representatives=False, @@ -280,33 +275,25 @@ def xtest_lcms_collection_gap_filling(lcms_collection): induced_df = lcms_collection.induced_mass_features_dataframe assert induced_df is not None - # With samples 2 and 3 having missing features, gap filling should create induced features - print(f"\nAfter gap filling:") - print(f" Total induced mass features found: {len(induced_df)}") - assert len(induced_df) > 0, "Gap filling should create induced mass features in samples 2 and 3" + # With sample 3 having missing features, gap filling should create induced features + assert len(induced_df) > 0, "Gap filling should create induced mass features in sample 3" # Check that induced mass features have proper columns assert 'cluster' in induced_df.columns assert 'sample_name' in induced_df.columns assert 'mf_id' in induced_df.columns - # Check induced features per sample - sample_2_induced = len(lcms_collection[1].induced_mass_features) - sample_3_induced = len(lcms_collection[2].induced_mass_features) - - print(f" Sample 2 induced features: {sample_2_induced}") - print(f" Sample 3 induced features: {sample_3_induced}") - - # Sample 3 should have the most induced features (started with 0) - assert sample_3_induced > 0, "Sample 3 should have induced mass features from gap filling" + # Check induced features per sample in the dataframe (not individual objects) + sample_3_induced = len(induced_df[induced_df['sample_id'] == 2]) - # Sample 2 should also have some induced features (for features 11+) - assert sample_2_induced > 0, "Sample 2 should have induced features for missing mass features" + # Sample 3 should have induced features (started with 0, all 50 clusters are missing) + assert sample_3_induced == 50, "Sample 3 should have 50 induced mass features (one for each cluster)" - # Check that individual LCMS objects have induced_mass_features - total_induced = sum(len(lcms_obj.induced_mass_features) - for lcms_obj in lcms_collection) - assert total_induced > 0 + # By design, individual sample objects should have empty induced_mass_features dict + # because they are collected into the induced_mass_features_dataframe + assert len(lcms_collection[0].induced_mass_features) == 0 + assert len(lcms_collection[1].induced_mass_features) == 0 + assert len(lcms_collection[2].induced_mass_features) == 0 #TODO KRH: fix this test From 122c7fec7f3abceca6dde65ea56a2b79ab108179 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 30 Jan 2026 11:32:40 -0800 Subject: [PATCH 125/158] Fix pivot table creation with no mass feature samples --- corems/mass_spectra/factory/lc_class.py | 35 ++++++++++++++++++------- tests/test_lcms_collection.py | 35 +++++++++++++++++++++---- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 1f1959894..7fc6802a9 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -2159,11 +2159,17 @@ def collection_pivot_table(self, attribute = 'coll_mf_id', verbose = True): mf_pivot = self.mass_features_dataframe.copy() mf_pivot.reset_index(inplace = True) - imf_pivot = self.induced_mass_features_dataframe.copy() - imf_pivot.reset_index(inplace = True) - # Cluster column extracted from mf_id in _prepare_lcms_mass_features_for_combination - mf_pivot = pd.concat([mf_pivot, imf_pivot], axis = 0) - mf_pivot.reset_index(drop = True, inplace = True) + + # Only include induced mass features if gap-filling has been performed + if self.induced_mass_features_dataframe is not None: + imf_pivot = self.induced_mass_features_dataframe.copy() + imf_pivot.reset_index(inplace = True) + # Cluster column extracted from mf_id in _prepare_lcms_mass_features_for_combination + mf_pivot = pd.concat([mf_pivot, imf_pivot], axis = 0) + mf_pivot.reset_index(drop = True, inplace = True) + else: + imf_pivot = None + mf_pivot['cluster'] = mf_pivot['cluster'].astype(int) if verbose: @@ -2171,11 +2177,20 @@ def collection_pivot_table(self, attribute = 'coll_mf_id', verbose = True): 'Attributes available for pivot table:\n', [x for x in mf_pivot.columns if x not in ['cluster', 'sample_name', 'mf_id', 'partition_idx', 'idx']] ) - print( - '\nAttributes that have no value for induced mass features:\n', - imf_pivot.columns[imf_pivot.isna().all()].tolist() - ) - return mf_pivot.pivot(index = 'cluster', columns = 'sample_name', values = attribute) + if imf_pivot is not None: + print( + '\nAttributes that have no value for induced mass features:\n', + imf_pivot.columns[imf_pivot.isna().all()].tolist() + ) + + # Create pivot table and reindex to include all samples (even those with no features) + pivot = mf_pivot.pivot(index = 'cluster', columns = 'sample_name', values = attribute) + + # Reindex columns to include all samples in the collection + all_samples = self.samples + pivot = pivot.reindex(columns=all_samples) + + return pivot def cluster_representatives_table(self): """Generate a table of representative mass features from each consensus cluster. diff --git a/tests/test_lcms_collection.py b/tests/test_lcms_collection.py index 703224b4a..10076f4cb 100644 --- a/tests/test_lcms_collection.py +++ b/tests/test_lcms_collection.py @@ -244,7 +244,6 @@ def test_lcms_collection_consensus_features(lcms_collection): assert cluster_id in cluster_dict -#TODO KRH: fix this test def test_lcms_collection_gap_filling(lcms_collection): """Test gap filling to create induced mass features.""" # Setup: align and cluster first @@ -296,23 +295,49 @@ def test_lcms_collection_gap_filling(lcms_collection): assert len(lcms_collection[2].induced_mass_features) == 0 -#TODO KRH: fix this test -def xtest_lcms_collection_pivot_table(lcms_collection): +def test_lcms_collection_pivot_table(lcms_collection): """Test creation of pivot tables for collection data.""" # Setup: ensure we have clustered features if not lcms_collection.rt_aligned: lcms_collection.align_lcms_objects() lcms_collection.add_consensus_mass_features() - # Create pivot table with default attribute (coll_mf_id) + # Create pivot table with default attribute (coll_mf_id) before gap-filling pivot_df = lcms_collection.collection_pivot_table(verbose=False) # Check that pivot table was created assert pivot_df is not None assert isinstance(pivot_df, pd.DataFrame) - # Check dimensions: rows should be clusters, columns should be samples + # Check that all samples are included (even sample 3 with no features) assert len(pivot_df.columns) == len(lcms_collection.samples) + assert 'test_sample_03' in pivot_df.columns + + # Sample 3 should have all NAs before gap-filling + assert pivot_df['test_sample_03'].isna().all(), "Sample 3 should have all NAs before gap-filling" + + # Perform gap-filling + lcms_collection.process_consensus_features( + load_representatives=False, + perform_gap_filling=True, + add_ms1=False, + add_ms2=False, + molecular_formula_search=False, + ms2_spectral_search=False, + spectral_lib=False, + molecular_metadata=None, + gather_eics=True, + keep_raw_data=False + ) + + # Create pivot table again after gap-filling + pivot_df_after = lcms_collection.collection_pivot_table(verbose=False) + + # Sample 3 should no longer have all NAs after gap-filling + assert not pivot_df_after['test_sample_03'].isna().all(), "Sample 3 should have filled features after gap-filling" + + # Check that sample 3 has exactly 50 non-NA values (one for each cluster) + assert pivot_df_after['test_sample_03'].notna().sum() == 50, "Sample 3 should have 50 gap-filled features" # Create pivot table with intensity attribute pivot_intensity = lcms_collection.collection_pivot_table( From 4f0a845aef174cb3cf029af96d96c93e8375cae0 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 30 Jan 2026 12:00:39 -0800 Subject: [PATCH 126/158] Add test for collection-level MS2 annotation application and reporting --- corems/mass_spectra/calc/lc_calc.py | 9 ++-- corems/mass_spectra/factory/lc_class.py | 48 +++++++++++++++------ corems/mass_spectra/output/export.py | 8 +++- tests/test_lcms_collection.py | 56 +++++++++++++++++++++---- 4 files changed, 94 insertions(+), 27 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index a2b3ac269..7f510689d 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -5368,10 +5368,11 @@ def _execute_sample_pipeline(self, sample_id, operations, runtime_params, inplac # Check if operation can execute on this sample sample = self[sample_id] if not op.can_execute(sample, self): - raise RuntimeError( - f"Operation '{op.name}' cannot execute on sample {sample_id} " - f"({sample.sample_name}). Prerequisites not met." - ) + # Skip this operation for this sample if prerequisites aren't met + # This allows processing to continue for samples that don't have + # all required data (e.g., MS2 spectra) + results[op.name] = None + continue # Prepare operation-specific runtime params op_runtime_params = {} diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 7fc6802a9..8d688d9c3 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1109,13 +1109,16 @@ def mass_features_ms1_annot_to_df(self): return annot_ms1_df_full - def mass_features_ms2_annot_to_df(self, molecular_metadata=None): + def mass_features_ms2_annot_to_df(self, molecular_metadata=None, suppress_warnings=False): """Returns a pandas dataframe summarizing the MS2 annotations for the mass features in the dataset. Parameters ----------- molecular_metadata : dict of MolecularMetadata objects A dictionary of MolecularMetadata objects, keyed by metabref_mol_id. Defaults to None. + suppress_warnings : bool, optional + If True, suppresses the warning when no MS2 annotations are found. + Useful when calling from collection-level methods. Default is False. Returns -------- @@ -1126,7 +1129,8 @@ def mass_features_ms2_annot_to_df(self, molecular_metadata=None): Raises ------ Warning - If no MS2 annotations were found for the mass features in the dataset. + If no MS2 annotations were found for the mass features in the dataset + (unless suppress_warnings=True). """ annot_df_list_ms2 = [] for mf_id in self.mass_features.keys(): @@ -1160,11 +1164,12 @@ def mass_features_ms2_annot_to_df(self, molecular_metadata=None): annot_ms2_df_full.index.name = "mf_id" else: annot_ms2_df_full = None - # Warn that no ms2 annotations were found - warnings.warn( - "No MS2 annotations found for mass features in dataset, were MS2 spectra added and searched against a database?", - UserWarning, - ) + # Warn that no ms2 annotations were found (unless suppressed) + if not suppress_warnings: + warnings.warn( + "No MS2 annotations found for mass features in dataset, were MS2 spectra added and searched against a database?", + UserWarning, + ) return annot_ms2_df_full @@ -2220,11 +2225,14 @@ def cluster_representatives_table(self): mf_df = self.mass_features_dataframe.copy() mf_df.reset_index(inplace = True) - imf_df = self.induced_mass_features_dataframe.copy() - imf_df.reset_index(inplace = True) - # Cluster column extracted from mf_id in _prepare_lcms_mass_features_for_combination - mf_df = pd.concat([mf_df, imf_df], axis = 0) - mf_df.reset_index(drop = True, inplace = True) + + # Include induced mass features if they exist (from gap-filling) + if self.induced_mass_features_dataframe is not None: + imf_df = self.induced_mass_features_dataframe.copy() + imf_df.reset_index(inplace = True) + # Cluster column extracted from mf_id in _prepare_lcms_mass_features_for_combination + mf_df = pd.concat([mf_df, imf_df], axis = 0) + mf_df.reset_index(drop = True, inplace = True) mf_df['cluster'] = mf_df['cluster'].astype(int) # Calculate number of samples per cluster @@ -2303,6 +2311,7 @@ def feature_annotations_table(self, molecular_metadata=None, drop_unannotated=Fa # Collect reports from all samples all_sample_reports = [] + has_any_ms2_annotations = False for sample_id, lcms_obj in enumerate(self): # Skip samples with no loaded mass features @@ -2312,8 +2321,14 @@ def feature_annotations_table(self, molecular_metadata=None, drop_unannotated=Fa sample_name = self.samples[sample_id] # Create exporter and generate report using standard workflow + # Suppress individual warnings - we'll warn at collection level if needed exporter = LCMSMetabolomicsExport("temp", lcms_obj) - sample_report = exporter.to_report(molecular_metadata=molecular_metadata) + sample_report = exporter.to_report(molecular_metadata=molecular_metadata, suppress_warnings=True) + + # Check if this sample has any MS2 annotations + ms2_cols = [col for col in sample_report.columns if 'Entropy Similarity' in col or 'spectral_similarity' in col.lower()] + if ms2_cols and sample_report[ms2_cols].notna().any().any(): + has_any_ms2_annotations = True # Add sample information sample_report['representative_sample'] = sample_name @@ -2342,6 +2357,13 @@ def feature_annotations_table(self, molecular_metadata=None, drop_unannotated=Fa collection_report = pd.concat(all_sample_reports, ignore_index=True) + # Warn only if NO samples in the collection have MS2 annotations + if not has_any_ms2_annotations: + warnings.warn( + "No MS2 annotations found across any samples in collection, were MS2 spectra added and searched against a database?", + UserWarning, + ) + # Reorder columns to match specified order desired_cols = [ 'cluster', diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index 35baae664..b9af08c1f 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -1722,13 +1722,16 @@ def combine_reports(self, mf_report, ms1_annot_report, ms2_annot_report): return mf_report - def to_report(self, molecular_metadata=None): + def to_report(self, molecular_metadata=None, suppress_warnings=False): """Create a report of the mass features and their annotations. Parameters ---------- molecular_metadata : dict, optional The molecular metadata. Default is None. + suppress_warnings : bool, optional + If True, suppresses warnings from mass_features_ms2_annot_to_df. + Default is False. Returns ------- @@ -1746,7 +1749,8 @@ def to_report(self, molecular_metadata=None): # Get, summarize, and clean ms2 annotation dataframe ms2_annot_report = self.mass_spectra.mass_features_ms2_annot_to_df( - molecular_metadata=molecular_metadata + molecular_metadata=molecular_metadata, + suppress_warnings=suppress_warnings ) if ms2_annot_report is not None and molecular_metadata is not None: ms2_annot_report = self.summarize_metabolomics_report(ms2_annot_report) diff --git a/tests/test_lcms_collection.py b/tests/test_lcms_collection.py index 10076f4cb..cfda1a8f6 100644 --- a/tests/test_lcms_collection.py +++ b/tests/test_lcms_collection.py @@ -130,6 +130,12 @@ def lcms_collection(lcms_collection_folder): collection.parameters.lcms_collection.alignment_mz_tol_ppm = 5 # Tight m/z tolerance collection.parameters.lcms_collection.alignment_rt_tol = 0.2 # 12 second RT tolerance + # Set MS2 processing parameters for centroid data + for lcms_obj in collection: + ms2_params = lcms_obj.parameters.mass_spectrum['ms2'] + ms2_params.mass_spectrum.noise_threshold_method = "relative_abundance" + ms2_params.mass_spectrum.noise_threshold_min_relative_abundance = 1.0 + return collection @@ -348,8 +354,7 @@ def test_lcms_collection_pivot_table(lcms_collection): assert len(pivot_intensity.columns) == len(lcms_collection.samples) -#TODO KRH: fix this test -def xtest_lcms_collection_cluster_representatives(lcms_collection): +def test_lcms_collection_cluster_representatives(lcms_collection): """Test extraction of representative features for each cluster.""" # Setup: ensure we have clustered features if not lcms_collection.rt_aligned: @@ -449,25 +454,51 @@ def test_lcms_collection_plot_tics(lcms_collection): pytest.fail(f"plot_tics raised an exception: {e}") -#TODO KRH: fix this test -def xtest_lcms_collection_feature_annotations_table(lcms_collection, msp_file_location): +def test_lcms_collection_feature_annotations_table(lcms_collection, msp_file_location): """Test creation of feature annotations table with molecular metadata.""" # Setup: align and cluster if not lcms_collection.rt_aligned: lcms_collection.align_lcms_objects() lcms_collection.add_consensus_mass_features() - # Load molecular metadata from MSP file + # Load molecular metadata from MSP file with same parameters as test_lcms_metabolomics my_msp = MSPInterface(file_path=msp_file_location) msp_lib, molecular_metadata = my_msp.get_metabolomics_spectra_library( polarity="negative", format="flashentropy", - normalize=True + normalize=True, + fe_kwargs={ + "normalize_intensity": True, + "min_ms2_difference_in_da": 0.02, # for cleaning spectra + "max_ms2_tolerance_in_da": 0.01, # for setting search space + "max_indexed_mz": 3000, + "precursor_ions_removal_da": None, + "noise_threshold": 0, + }, + ) + + # Set MS2 score threshold to match test_lcms_metabolomics + for lcms_obj in lcms_collection: + lcms_obj.parameters.lc_ms.ms2_min_fe_score = 0.3 + + # Process consensus features with MS1, MS2, and spectral search + # This should add molecular annotations before creating the annotations table + pipeline_results = lcms_collection.process_consensus_features( + load_representatives=True, + perform_gap_filling=False, + add_ms1=True, + add_ms2=True, + molecular_formula_search=False, + ms2_spectral_search=True, + spectral_lib=msp_lib, + molecular_metadata=molecular_metadata, + gather_eics=False, + keep_raw_data=False ) - # Create annotations table without metadata first + # Create annotations table with metadata annotations_table = lcms_collection.feature_annotations_table( - molecular_metadata=None, + molecular_metadata=molecular_metadata, drop_unannotated=False ) @@ -477,6 +508,15 @@ def xtest_lcms_collection_feature_annotations_table(lcms_collection, msp_file_lo # Check that cluster information is present assert 'cluster' in annotations_table.columns + + # Check that we got some spectral matches after processing + # Look for Entropy Similarity column which indicates MS2 spectral search results + if 'Entropy Similarity' in annotations_table.columns: + matched_features = annotations_table[annotations_table['Entropy Similarity'].notna()] + assert len(matched_features) > 0, "Should have at least some MS2 spectral matches after search" + else: + # If column doesn't exist, the test should fail + raise AssertionError("Expected 'Entropy Similarity' column in annotations table after MS2 spectral search") def test_lcms_collection_sample_access(lcms_collection): From cdc4a1baea3c4658207f931441d16b89623ff264 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 30 Jan 2026 12:03:13 -0800 Subject: [PATCH 127/158] Fix collection file mover test --- tests/test_lcms_collection.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/test_lcms_collection.py b/tests/test_lcms_collection.py index cfda1a8f6..80153f3df 100644 --- a/tests/test_lcms_collection.py +++ b/tests/test_lcms_collection.py @@ -541,13 +541,22 @@ def test_lcms_collection_sample_access(lcms_collection): assert len(raw_files) == len(lcms_collection) -#TODO KRH: fix this test -def xtest_lcms_collection_update_raw_file_locations(lcms_collection, tmp_path): +def test_lcms_collection_update_raw_file_locations(lcms_collection, tmp_path): """Test updating raw file locations in the collection.""" + import shutil + # Create a new path for raw files new_raw_folder = tmp_path / "new_raw_location" new_raw_folder.mkdir() + # Copy the original raw file to the new location for each sample + # The samples all use the same original raw file but have different sample names + for lcms_obj in lcms_collection: + original_raw = lcms_obj.raw_file_location + # Create a copy with the sample name in the new location + new_raw_file = new_raw_folder / f"{lcms_obj.sample_name}.raw" + shutil.copy2(original_raw, new_raw_file) + # Update raw file locations lcms_collection.update_raw_file_locations(str(new_raw_folder)) From 33ece329405ad10322ae9ebfe5a7a7ce3f97109e Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 30 Jan 2026 12:36:59 -0800 Subject: [PATCH 128/158] Add error handling for no MS1 annotations, add test for molecular formula search on consensus mass features --- corems/mass_spectra/factory/lc_class.py | 23 +++++++ corems/mass_spectra/output/export.py | 10 ++- tests/test_lcms_collection.py | 92 +++++++++++++++++++++++-- 3 files changed, 118 insertions(+), 7 deletions(-) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 8d688d9c3..33f596479 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -2306,8 +2306,31 @@ def feature_annotations_table(self, molecular_metadata=None, drop_unannotated=Fa Only mass features that are loaded in each sample's mass_features dict are included (typically the representative features if load_representatives was used in process_consensus_features). + + Raises + ------ + ValueError + If no representative features have been loaded. Call process_consensus_features + with load_representatives=True first. + ValueError + If no samples with loaded mass features are found in the collection. """ from corems.mass_spectra.output.export import LCMSMetabolomicsExport + import warnings + + # Check if representative features have been loaded + # Count samples with mass features loaded + samples_with_features = sum( + 1 for lcms_obj in self + if hasattr(lcms_obj, 'mass_features') and len(lcms_obj.mass_features) > 0 + ) + + if samples_with_features == 0: + raise ValueError( + "No representative mass features have been loaded into individual samples. " + "Call process_consensus_features() with load_representatives=True before " + "calling feature_annotations_table()." + ) # Collect reports from all samples all_sample_reports = [] diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index b9af08c1f..2309e5a45 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -1743,9 +1743,13 @@ def to_report(self, molecular_metadata=None, suppress_warnings=False): mf_report = mf_report.reset_index(drop=False) # Get and clean ms1 annotation dataframe - ms1_annot_report = self.mass_spectra.mass_features_ms1_annot_to_df().copy() - ms1_annot_report = self.clean_ms1_report(ms1_annot_report) - ms1_annot_report = ms1_annot_report.reset_index(drop=False) + ms1_annot_report = self.mass_spectra.mass_features_ms1_annot_to_df() + if ms1_annot_report is not None: + ms1_annot_report = ms1_annot_report.copy() + ms1_annot_report = self.clean_ms1_report(ms1_annot_report) + ms1_annot_report = ms1_annot_report.reset_index(drop=False) + else: + ms1_annot_report = None # Get, summarize, and clean ms2 annotation dataframe ms2_annot_report = self.mass_spectra.mass_features_ms2_annot_to_df( diff --git a/tests/test_lcms_collection.py b/tests/test_lcms_collection.py index 80153f3df..96a16b16e 100644 --- a/tests/test_lcms_collection.py +++ b/tests/test_lcms_collection.py @@ -519,6 +519,74 @@ def test_lcms_collection_feature_annotations_table(lcms_collection, msp_file_loc raise AssertionError("Expected 'Entropy Similarity' column in annotations table after MS2 spectral search") +def test_lcms_collection_molecular_formula_search(lcms_collection, postgres_database): + """Test molecular formula search on consensus features.""" + # Setup: align and cluster + if not lcms_collection.rt_aligned: + lcms_collection.align_lcms_objects() + lcms_collection.add_consensus_mass_features() + + # Set molecular search parameters for all samples (negative mode) + for lcms_obj in lcms_collection: + ms1_params = lcms_obj.parameters.mass_spectrum['ms1'] + ms1_params.molecular_search.url_database = postgres_database + ms1_params.molecular_search.error_method = "None" + ms1_params.molecular_search.min_ppm_error = -5 + ms1_params.molecular_search.max_ppm_error = 5 + ms1_params.molecular_search.mz_error_range = 1 + ms1_params.molecular_search.isProtonated = True # Deprotonated in negative mode + ms1_params.molecular_search.isRadical = False + ms1_params.molecular_search.isAdduct = False + ms1_params.molecular_search.usedAtoms = { + 'C': (1, 90), + 'H': (4, 200), + 'O': (0, 30), + 'N': (0, 3), + 'P': (0, 2), + 'S': (0, 2), + } + + # Process consensus features with molecular formula search + pipeline_results = lcms_collection.process_consensus_features( + load_representatives=True, + perform_gap_filling=False, + add_ms1=True, + add_ms2=False, + molecular_formula_search=True, + ms2_spectral_search=False, + spectral_lib=False, + molecular_metadata=None, + gather_eics=False, + keep_raw_data=False + ) + + # Get annotations table + annotations_table = lcms_collection.feature_annotations_table( + molecular_metadata=None, + drop_unannotated=False + ) + + assert annotations_table is not None + assert isinstance(annotations_table, pd.DataFrame) + assert len(annotations_table) > 0 + + # Check that molecular formula columns are present (column is called 'Ion Formula') + assert 'Ion Formula' in annotations_table.columns + assert 'Calculated m/z' in annotations_table.columns + + # Check that at least some features got molecular formula assignments + assigned_formulas = annotations_table[annotations_table['Ion Formula'].notna()] + assert len(assigned_formulas) > 0, "Should have at least some molecular formula assignments" + + # Verify the formulas are reasonable (contain expected elements) + first_formula = assigned_formulas['Ion Formula'].iloc[0] + assert 'C' in first_formula or 'H' in first_formula, "Molecular formulas should contain C or H" + + # Check that m/z error columns exist (indicates matching happened) + assert 'm/z Error (ppm)' in annotations_table.columns + assert 'm/z Error Score' in annotations_table.columns + + def test_lcms_collection_sample_access(lcms_collection): """Test various ways to access samples in the collection.""" # Test indexing @@ -568,8 +636,7 @@ def test_lcms_collection_update_raw_file_locations(lcms_collection, tmp_path): assert lcms_collection.raw_files_relocated -#TODO KRH: fix this test -def xtest_lcms_collection_minimal_workflow(lcms_collection): +def test_lcms_collection_minimal_workflow(lcms_collection): """ Test a minimal end-to-end workflow with the collection. @@ -591,8 +658,20 @@ def xtest_lcms_collection_minimal_workflow(lcms_collection): cluster_count = len(lcms_collection.cluster_summary_dataframe) assert cluster_count > 0 - # Step 4: Perform gap filling - lcms_collection.search_for_missing_mass_features() + # Step 4: Perform gap filling using process_consensus_features + # Load representatives and add MS1 so we can create annotations table + lcms_collection.process_consensus_features( + load_representatives=True, + perform_gap_filling=True, + add_ms1=True, + add_ms2=False, + molecular_formula_search=False, + ms2_spectral_search=False, + spectral_lib=False, + molecular_metadata=None, + gather_eics=True, + keep_raw_data=False + ) # Step 5: Create reports pivot_table = lcms_collection.collection_pivot_table(verbose=False) @@ -601,11 +680,16 @@ def xtest_lcms_collection_minimal_workflow(lcms_collection): cluster_reps = lcms_collection.cluster_representatives_table() assert len(cluster_reps) == cluster_count + # Create annotations table (requires load_representatives=True and add_ms1=True) annotations = lcms_collection.feature_annotations_table( molecular_metadata=None, drop_unannotated=False ) assert annotations is not None + assert len(annotations) > 0 + + # Verify we have cluster information in the annotations + assert 'cluster' in annotations.columns # Verify workflow completed successfully assert lcms_collection.rt_aligned or lcms_collection.rt_alignment_attempted From 14c7b73bad128330a94b748c75b4d376910648ad Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 30 Jan 2026 12:50:09 -0800 Subject: [PATCH 129/158] Final clean up for tests for mass spectra collection --- tests/test_lcms_collection.py | 107 +++++++--------------------------- 1 file changed, 20 insertions(+), 87 deletions(-) diff --git a/tests/test_lcms_collection.py b/tests/test_lcms_collection.py index 96a16b16e..02b3b78bf 100644 --- a/tests/test_lcms_collection.py +++ b/tests/test_lcms_collection.py @@ -186,27 +186,19 @@ def test_lcms_collection_mass_features_dataframe(lcms_collection): assert mf_df.index.name == 'coll_mf_id' or 'coll_mf_id' in mf_df.columns -def test_lcms_collection_parameters(lcms_collection): - """Test that collection parameters can be get/set.""" - # Check that parameters exist - assert lcms_collection.parameters is not None - - # Check that it's the right type - assert isinstance(lcms_collection.parameters, LCMSCollectionParameters) - - # Test setting new parameters - new_params = LCMSCollectionParameters() - new_params.lcms_collection.cores = 2 - lcms_collection.parameters = new_params - - assert lcms_collection.parameters.lcms_collection.cores == 2 - - def test_lcms_collection_rt_alignment(lcms_collection): """Test retention time alignment across samples in the collection.""" # Check initial state assert not lcms_collection.rt_aligned + # Test plotting TICs before alignment + try: + import matplotlib + matplotlib.use('Agg') # Use non-interactive backend for testing + lcms_collection.plot_tics(ms_level=1, type="raw", plot_legend=False) + except Exception as e: + pytest.fail(f"plot_tics raised an exception: {e}") + # Perform alignment - this should succeed lcms_collection.align_lcms_objects() @@ -220,6 +212,18 @@ def test_lcms_collection_rt_alignment(lcms_collection): for i, lcms_obj in enumerate(lcms_collection): sample_name = lcms_collection.samples[i] assert 'scan_time_aligned' in lcms_obj.scan_df.columns, f"Missing scan_time_aligned in {sample_name}" + + # Test plotting after alignment - both raw and corrected TICs + try: + lcms_collection.plot_tics(ms_level=1, type="both", plot_legend=True) + except Exception as e: + pytest.fail(f"plot_tics with type='both' raised an exception: {e}") + + # Test plotting alignments (shows time differences) + try: + lcms_collection.plot_alignments(plot_legend=True) + except Exception as e: + pytest.fail(f"plot_alignments raised an exception: {e}") def test_lcms_collection_consensus_features(lcms_collection): @@ -441,19 +445,6 @@ def test_lcms_collection_drop_isotopologues(lcms_collection): assert final_mf_count <= initial_mf_count -def test_lcms_collection_plot_tics(lcms_collection): - """Test plotting total ion chromatograms for the collection.""" - # This test just ensures the method runs without error - # Visual inspection of plots is done manually - try: - import matplotlib - matplotlib.use('Agg') # Use non-interactive backend for testing - lcms_collection.plot_tics(ms_level=1, type="raw", plot_legend=False) - assert True - except Exception as e: - pytest.fail(f"plot_tics raised an exception: {e}") - - def test_lcms_collection_feature_annotations_table(lcms_collection, msp_file_location): """Test creation of feature annotations table with molecular metadata.""" # Setup: align and cluster @@ -587,28 +578,6 @@ def test_lcms_collection_molecular_formula_search(lcms_collection, postgres_data assert 'm/z Error Score' in annotations_table.columns -def test_lcms_collection_sample_access(lcms_collection): - """Test various ways to access samples in the collection.""" - # Test indexing - first_sample = lcms_collection[0] - assert first_sample is not None - - # Test iteration - sample_count = 0 - for lcms_obj in lcms_collection: - assert lcms_obj is not None - sample_count += 1 - assert sample_count == len(lcms_collection) - - # Test samples property - sample_names = lcms_collection.samples - assert len(sample_names) == len(lcms_collection) - - # Test raw_files property - raw_files = lcms_collection.raw_files - assert len(raw_files) == len(lcms_collection) - - def test_lcms_collection_update_raw_file_locations(lcms_collection, tmp_path): """Test updating raw file locations in the collection.""" import shutil @@ -696,39 +665,3 @@ def test_lcms_collection_minimal_workflow(lcms_collection): assert cluster_count > 0 print(f"\nWorkflow completed: {cluster_count} consensus clusters from {len(lcms_collection)} samples") - -def test_lcms_collection_memory_management(lcms_collection): - """Test that collection properly manages memory for raw data.""" - # Initially, raw data should not be loaded (load_light=True in fixture) - for i, lcms_obj in enumerate(lcms_collection): - # Check that _ms_unprocessed is either empty or not loaded - if hasattr(lcms_obj, '_ms_unprocessed'): - # MS level 1 should not have large raw data loaded - if 1 in lcms_obj._ms_unprocessed: - # For light loading, this should be empty or minimal - pass - - # Load raw data for one sample - lcms_collection.load_raw_data(sample_idx=0, ms_level=1) - - # Check that data was loaded - assert 1 in lcms_collection[0]._ms_unprocessed - - # Drop raw data - lcms_collection.drop_raw_data(sample_idx=0, ms_level=1) - - # Verify data was dropped - # After dropping, the dict might be empty or have an empty DataFrame - if 1 in lcms_collection[0]._ms_unprocessed: - # Should be empty or minimal size now - pass - - -def test_lcms_collection_cleanup(lcms_collection_folder): - """Test cleanup of temporary collection data.""" - # Verify the folder exists - assert lcms_collection_folder.exists() - - # The pytest tmp_path fixture will automatically clean up - # This test just verifies the structure - assert len(list(lcms_collection_folder.glob("*.corems"))) > 0 From c067006ebca76da090e2e660afa7318c962cc66d Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 30 Jan 2026 17:47:26 -0800 Subject: [PATCH 130/158] Small bug fixes on mirror plot --- .../factory/chroma_peak_classes.py | 62 ++++--- corems/mass_spectra/calc/lc_calc.py | 40 +++-- corems/mass_spectra/factory/lc_class.py | 1 + corems/mass_spectra/factory/lc_collection.py | 10 -- tests/test_lcms_collection.py | 154 +++++++++++++++++- tests/test_lcms_metabolomics.py | 2 +- 6 files changed, 214 insertions(+), 55 deletions(-) delete mode 100644 corems/mass_spectra/factory/lc_collection.py diff --git a/corems/chroma_peak/factory/chroma_peak_classes.py b/corems/chroma_peak/factory/chroma_peak_classes.py index 0a452aab6..83a41658f 100644 --- a/corems/chroma_peak/factory/chroma_peak_classes.py +++ b/corems/chroma_peak/factory/chroma_peak_classes.py @@ -367,7 +367,7 @@ def _plot_ms2_spectrum(self, ax, sample_name=None): ax.yaxis.get_major_formatter().set_scientific(False) ax.yaxis.get_major_formatter().set_useOffset(False) - def _plot_ms2_mirror(self, ax, msp_interface): + def _plot_ms2_mirror(self, ax, molecular_metadata=None, spectral_library=None): """Internal method to plot MS2 mirror spectrum on a given axis. Plots experimental MS2 on top (positive) and library MS2 on bottom (negative/mirrored) @@ -378,8 +378,14 @@ def _plot_ms2_mirror(self, ax, msp_interface): ---------- ax : matplotlib.axes.Axes The axis to plot on. - msp_interface : MSPInterface - MSP interface object to access library spectra. + molecular_metadata : dict, optional + Dictionary mapping molecular IDs to MetaboliteMetadata objects. + If provided, uses metadata for compound names. + Default is None. + spectral_library : FlashEntropySearch, optional + FlashEntropy spectral library containing MS2 spectra. + If provided, uses library to retrieve MS2 spectra by ref_ms_id. + Default is None. """ if len(self.ms2_mass_spectra) == 0: ax.text(0.5, 0.5, 'No MS2 data available', @@ -409,7 +415,7 @@ def _plot_ms2_mirror(self, ax, msp_interface): library_ms2_peaks = None entropy_similarity = None molecule_name = None - ms_id = None + mol_id = None if len(self.ms2_similarity_results) > 0: # Get all results as dataframes and find the best match @@ -420,19 +426,21 @@ def _plot_ms2_mirror(self, ax, msp_interface): # Get the best match best_result = results_df.iloc[0] entropy_similarity = best_result['entropy_similarity'] - ms_id = best_result['ref_ms_id'] + mol_id = best_result.get('ref_mol_id', None) + ref_ms_id = best_result.get('ref_ms_id', None) - # Get library spectrum from MSP interface - msp_df = msp_interface._data_frame - msp_matches = msp_df[msp_df['spectra_id'] == ms_id] + # Get library spectrum from spectral_library using ref_ms_id + if spectral_library is not None and ref_ms_id is not None: + # Get the IDs in the spectral library + fe_spec_index = [x["id"] for x in spectral_library].index(ref_ms_id) + library_ms2_peaks = spectral_library[fe_spec_index]['peaks'] - if len(msp_matches) > 0: - msp_entry = msp_matches.iloc[0] - # Get compound name from the MSP entry - molecule_name = msp_entry.get('compound_name', msp_entry.get('name', 'Unknown')) - peaks = msp_entry['peaks'] - if peaks is not None and len(peaks) > 0: - library_ms2_peaks = np.array(peaks) + # Get compound name from molecular_metadata using mol_id + if molecular_metadata is not None and mol_id is not None: + if mol_id in molecular_metadata: + metadata = molecular_metadata[mol_id] + # Get compound name from metadata + molecule_name = getattr(metadata, 'common_name', getattr(metadata, 'name', 'Unknown')) # Plot library MS2 on bottom (negative/mirrored) if library_ms2_peaks is not None and len(library_ms2_peaks) > 0: @@ -444,12 +452,12 @@ def _plot_ms2_mirror(self, ax, msp_interface): # Mirror to negative lib_int_mirror = -lib_int - # Create label with molecule name and MS ID + # Create label with molecule name and molecular ID lib_label = f'Library MS2' if molecule_name: lib_label += f' ({molecule_name})' - if ms_id: - lib_label += f' [ID: {ms_id}]' + if mol_id: + lib_label += f' [ID: {mol_id}]' ax.vlines(lib_mz, 0, lib_int_mirror, colors='red', alpha=0.7, linewidths=1.5, label=lib_label) @@ -554,7 +562,8 @@ def plot( return_fig=True, plot_smoothed_eic=False, plot_eic_datapoints=False, - msp_interface=None, + molecular_metadata=None, + spectral_library=None, ): """Plot the mass feature. @@ -570,8 +579,11 @@ def plot( If True, the smoothed EIC is plotted. Default is False. plot_eic_datapoints : bool, optional If True, the EIC data points are plotted. Default is False. - msp_interface : MSPInterface, optional - MSP interface object to access library spectra for MS2 mirror plot. + molecular_metadata : dict, optional + Dictionary mapping molecular IDs to MetaboliteMetadata objects. + Required if "MS2_mirror" is in to_plot. Default is None. + spectral_library : FlashEntropySearch, optional + FlashEntropy spectral library containing MS2 spectra. Required if "MS2_mirror" is in to_plot. Default is None. Returns @@ -588,9 +600,9 @@ def plot( if self._eic_data is None: to_plot = [x for x in to_plot if x != "EIC"] - # Check if MS2_mirror is requested without msp_interface - if "MS2_mirror" in to_plot and msp_interface is None: - raise ValueError("msp_interface is required when 'MS2_mirror' is in to_plot") + # Check if MS2_mirror is requested without molecular_metadata + if "MS2_mirror" in to_plot and molecular_metadata is None: + raise ValueError("molecular_metadata is required when 'MS2_mirror' is in to_plot") # Check if both MS2 and MS2_mirror are requested (not allowed) if "MS2" in to_plot and "MS2_mirror" in to_plot: @@ -629,7 +641,7 @@ def plot( # MS2 mirror plot if "MS2_mirror" in to_plot: - self._plot_ms2_mirror(axs[i][0], msp_interface) + self._plot_ms2_mirror(axs[i][0], molecular_metadata=molecular_metadata, spectral_library=spectral_library) i += 1 # Add space between subplots diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 7f510689d..e03df5ed9 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -2543,7 +2543,7 @@ def _plot_multiple_eics(ax, cluster_mfs, induced_cluster_mfs, rep_sample_id, rep # Plot regular (non-induced) mass features for _, row in cluster_mfs.iterrows(): - sample_id = row['sample_id'] + sample_id = int(row['sample_id']) mf_id = row['mf_id'] sample = lcms_collection[sample_id] sample_name = row['sample_name'] @@ -2610,7 +2610,7 @@ def _plot_multiple_eics(ax, cluster_mfs, induced_cluster_mfs, rep_sample_id, rep # Plot induced (gap-filled) mass features if available if induced_cluster_mfs is not None and not induced_cluster_mfs.empty: for _, row in induced_cluster_mfs.iterrows(): - sample_id = row['sample_id'] + sample_id = int(row['sample_id']) mf_id = row['mf_id'] sample = lcms_collection[sample_id] sample_name = row['sample_name'] @@ -3653,7 +3653,7 @@ def plot_consensus_mz_features(self, xb = 'xb', xt = 'xt', yb = 'yb', yt = 'yt', kw = dict( prop = 'sizes', - num = len(df.sample_id_nunique.unique())/3, + num = max(1, int(len(df.sample_id_nunique.unique())/3)), color = 'tab:orange', alpha = 0.7, func = lambda s: np.sqrt(s*5) @@ -3682,6 +3682,8 @@ def plot_cluster( plot_eic_datapoints=False, eic_buffer_time=None, label_samples=False, + molecular_metadata=None, + spectral_library=None, ): """ Plot a consensus mass feature cluster across all samples. @@ -3694,7 +3696,7 @@ def plot_cluster( cluster_id : int The cluster ID to plot to_plot : list, optional - List of strings specifying what to plot: "EIC", "MS1", "MS2". + List of strings specifying what to plot: "EIC", "MS1", "MS2", "MS2_mirror". Default is ["EIC", "MS1", "MS2"]. return_fig : bool, optional If True, returns the figure object. Default is False. @@ -3707,6 +3709,12 @@ def plot_cluster( If None, uses parameter setting. Default is None. label_samples : bool, optional If True, labels each sample in the legend. Default is False. + molecular_metadata : dict, optional + Dictionary mapping molecular IDs to MetaboliteMetadata objects. + Required for MS2_mirror plots. Default is None. + spectral_library : FlashEntropySearch, optional + FlashEntropy spectral library containing MS2 spectra. + Required for MS2_mirror plots to retrieve library spectra. Default is None. Returns ------- @@ -3752,7 +3760,7 @@ def plot_cluster( if rep_mf.mass_spectrum is None: to_plot = [x for x in to_plot if x != "MS1"] if len(rep_mf.ms2_mass_spectra) == 0: - to_plot = [x for x in to_plot if x != "MS2"] + to_plot = [x for x in to_plot if x not in ["MS2", "MS2_mirror"]] # Check if EICs are available cluster_mfs = self.mass_features_dataframe[ @@ -3762,7 +3770,7 @@ def plot_cluster( has_eics = False # Check regular features for _, row in cluster_mfs.iterrows(): - sample_id = row['sample_id'] + sample_id = int(row['sample_id']) sample = self[sample_id] if hasattr(sample, 'eics') and sample.eics: has_eics = True @@ -3775,7 +3783,7 @@ def plot_cluster( self.induced_mass_features_dataframe['cluster'] == cluster_id ] for _, row in induced_cluster_mfs.iterrows(): - sample_id = row['sample_id'] + sample_id = int(row['sample_id']) sample = self[sample_id] if hasattr(sample, 'eics') and sample.eics: has_eics = True @@ -3845,6 +3853,11 @@ def plot_cluster( rep_mf._plot_ms2_spectrum(axs[i][0], sample_name=rep_sample.sample_name) i += 1 + # MS2 mirror plot - from representative using helper method + if "MS2_mirror" in to_plot: + rep_mf._plot_ms2_mirror(axs[i][0], molecular_metadata=molecular_metadata, spectral_library=spectral_library) + i += 1 + plt.tight_layout() if return_fig: @@ -4062,11 +4075,12 @@ def get_most_representative_sample_for_cluster(self, cluster_id, representative_ # Get the representative row (should only be one) rep_row = cluster_rep.iloc[0] - # Get sample name from sample_id - sample_name = self.samples[rep_row['sample_id']] + # Get sample name from sample_id (convert to int for list indexing) + sample_id = int(rep_row['sample_id']) + sample_name = self.samples[sample_id] return { - 'sample_id': rep_row['sample_id'], + 'sample_id': sample_id, 'sample_name': sample_name, 'mf_id': rep_row['mf_id'], 'coll_mf_id': rep_row['coll_mf_id'], @@ -5645,14 +5659,12 @@ def process_consensus_features(self, load_representatives=True, perform_gap_fill print("\nAssociating EICs with mass features:") from tqdm import tqdm - # Dictionary to store EIC m/z values for each feature - eic_mz_map = {} # {coll_mf_id: eic_mz} - for sample_id in tqdm(range(len(self.samples)), unit="sample", ncols=80): sample = self[sample_id] if sample.eics: # Only if EICs were loaded # Associate EICs with regular mass features sample.associate_eics_with_mass_features(induced=False) # Associate EICs with induced mass features - sample.associate_eics_with_mass_features(induced=True) + sample.associate_eics_with_mass_features(induced=True) + return results \ No newline at end of file diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 33f596479..700e95b38 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1682,6 +1682,7 @@ class LCMSCollection(LCMSCollectionCalculations): Notes ------ This class is not intended to be instantiated directly, but rather instantiated using a parser object and then interacted with. + #TODO KRH: add docstrings """ def __init__( diff --git a/corems/mass_spectra/factory/lc_collection.py b/corems/mass_spectra/factory/lc_collection.py deleted file mode 100644 index 791ad950e..000000000 --- a/corems/mass_spectra/factory/lc_collection.py +++ /dev/null @@ -1,10 +0,0 @@ -class MassSpectraCollectionBase: - """Base class for a collection of MassSpectra objects. - - Attributes - ---------- - _mass_spectra : list - A list of MassSpectraBase objects. - """ - def __init__(self): - self._mass_spectra = [] \ No newline at end of file diff --git a/tests/test_lcms_collection.py b/tests/test_lcms_collection.py index 02b3b78bf..013961988 100644 --- a/tests/test_lcms_collection.py +++ b/tests/test_lcms_collection.py @@ -47,11 +47,7 @@ def lcms_collection_folder(tmp_path, lcms_obj): # Process SAMPLE 1: Find and integrate mass features (FULL) lcms_obj.find_mass_features(assign_ms2_scans=True) lcms_obj.integrate_mass_features(drop_if_fail=True) - - # Save all mass features for later use - all_mass_features = dict(lcms_obj.mass_features) - all_eics = dict(lcms_obj.eics) - + # Export sample 1 with ALL mass features sample_name_1 = "test_sample_01" exporter1 = LCMSMetabolomicsExport(str(processed_folder / sample_name_1), lcms_obj) @@ -510,6 +506,88 @@ def test_lcms_collection_feature_annotations_table(lcms_collection, msp_file_loc raise AssertionError("Expected 'Entropy Similarity' column in annotations table after MS2 spectral search") +def test_lcms_collection_plot_cluster_with_ms2_mirror(lcms_collection, msp_file_location): + """Test plot_cluster with MS2_mirror option using molecular_metadata.""" + import matplotlib + matplotlib.use('Agg') # Use non-interactive backend for testing + + # Setup: align and cluster + if not lcms_collection.rt_aligned: + lcms_collection.align_lcms_objects() + lcms_collection.add_consensus_mass_features() + + # Load molecular metadata from MSP file + my_msp = MSPInterface(file_path=msp_file_location) + msp_lib, molecular_metadata = my_msp.get_metabolomics_spectra_library( + polarity="negative", + format="flashentropy", + normalize=True, + fe_kwargs={ + "normalize_intensity": True, + "min_ms2_difference_in_da": 0.02, + "max_ms2_tolerance_in_da": 0.01, + "max_indexed_mz": 3000, + "precursor_ions_removal_da": None, + "noise_threshold": 0, + }, + ) + + # Set MS2 score threshold + for lcms_obj in lcms_collection: + lcms_obj.parameters.lc_ms.ms2_min_fe_score = 0.3 + + # Process consensus features with MS2 spectral search and gather EICs + pipeline_results = lcms_collection.process_consensus_features( + load_representatives=True, + perform_gap_filling=False, + add_ms1=True, + add_ms2=True, + molecular_formula_search=False, + ms2_spectral_search=True, + spectral_lib=msp_lib, + molecular_metadata=molecular_metadata, + gather_eics=True, + keep_raw_data=False + ) + + # Get a cluster with MS2 data + cluster_summary = lcms_collection.cluster_summary_dataframe + assert len(cluster_summary) > 0, "Should have clusters for plotting tests" + + # Find a cluster with MS2 similarity results + cluster_with_ms2 = None + for cluster_id in cluster_summary.index[:10]: # Check first 10 clusters + rep_info = lcms_collection.get_most_representative_sample_for_cluster(cluster_id) + rep_sample = lcms_collection[rep_info['sample_id']] + if rep_info['mf_id'] in rep_sample.mass_features: + rep_mf = rep_sample.mass_features[rep_info['mf_id']] + if len(rep_mf.ms2_similarity_results) > 0: + cluster_with_ms2 = cluster_id + break + + # Test plot_cluster with MS2_mirror + if cluster_with_ms2 is not None: + try: + lcms_collection.plot_cluster( + cluster_with_ms2, + to_plot=["EIC", "MS1", "MS2_mirror"], + molecular_metadata=molecular_metadata + ) + except Exception as e: + pytest.fail(f"plot_cluster with MS2_mirror raised exception: {e}") + else: + # If no MS2 matches found, test that MS2_mirror gracefully falls back + cluster_id = cluster_summary.index[0] + try: + lcms_collection.plot_cluster( + cluster_id, + to_plot=["EIC", "MS2_mirror"], + molecular_metadata=molecular_metadata + ) + except Exception as e: + pytest.fail(f"plot_cluster with MS2_mirror (no matches) raised exception: {e}") + + def test_lcms_collection_molecular_formula_search(lcms_collection, postgres_database): """Test molecular formula search on consensus features.""" # Setup: align and cluster @@ -665,3 +743,69 @@ def test_lcms_collection_minimal_workflow(lcms_collection): assert cluster_count > 0 print(f"\nWorkflow completed: {cluster_count} consensus clusters from {len(lcms_collection)} samples") + +def test_lcms_collection_plotting_methods(lcms_collection): + """ + Test plotting methods for consensus features and clusters. + + Tests both before and after gap filling to ensure plots work in both states: + - plot_consensus_mz_features(): Shows distribution of consensus features across m/z + - plot_cluster(): Shows mass features within a specific cluster + """ + import matplotlib + matplotlib.use('Agg') # Use non-interactive backend for testing + + # Setup: align and cluster + if not lcms_collection.rt_aligned: + lcms_collection.align_lcms_objects() + lcms_collection.add_consensus_mass_features() + + # Get cluster information + cluster_summary = lcms_collection.cluster_summary_dataframe + assert len(cluster_summary) > 0, "Should have clusters for plotting tests" + + # Pick a cluster ID for testing plot_cluster + cluster_id = cluster_summary.index[0] + + # Test plot_consensus_mz_features BEFORE gap filling + try: + lcms_collection.plot_consensus_mz_features() + except Exception as e: + pytest.fail(f"plot_consensus_mz_features before gap filling raised exception: {e}") + + # Test plot_cluster BEFORE gap filling + try: + lcms_collection.plot_cluster(cluster_id, to_plot=["EIC"]) + except Exception as e: + pytest.fail(f"plot_cluster before gap filling raised exception: {e}") + + # Perform gap filling + lcms_collection.process_consensus_features( + load_representatives=False, + perform_gap_filling=True, + add_ms1=False, + add_ms2=False, + molecular_formula_search=False, + ms2_spectral_search=False, + spectral_lib=False, + molecular_metadata=None, + gather_eics=True, + keep_raw_data=False + ) + + # Verify gap filling occurred + induced_df = lcms_collection.induced_mass_features_dataframe + assert len(induced_df) > 0, "Should have induced features after gap filling" + + # Test plot_consensus_mz_features AFTER gap filling + try: + lcms_collection.plot_consensus_mz_features(show_all=True) + except Exception as e: + pytest.fail(f"plot_consensus_mz_features after gap filling raised exception: {e}") + + # Test plot_cluster AFTER gap filling (with sample labels to test more options) + try: + lcms_collection.plot_cluster(cluster_id, to_plot=["EIC"], label_samples=True) + except Exception as e: + pytest.fail(f"plot_cluster after gap filling raised exception: {e}") + diff --git a/tests/test_lcms_metabolomics.py b/tests/test_lcms_metabolomics.py index cba714a62..d0b532247 100644 --- a/tests/test_lcms_metabolomics.py +++ b/tests/test_lcms_metabolomics.py @@ -133,7 +133,7 @@ def test_lcms_metabolomics(postgres_database, lcms_obj, msp_file_location): fig = mass_feature_to_plot.plot( to_plot=["EIC", "MS1", "MS2_mirror"], return_fig=True, - msp_interface=my_msp + molecular_metadata=metabolite_metadata_negative ) assert fig is not None From 4e6ed24ad7b078915fd6e79a544d3f420f13d24f Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 30 Jan 2026 17:58:51 -0800 Subject: [PATCH 131/158] Add LCMS Collection notebook and fix mirror plot issue --- .../factory/chroma_peak_classes.py | 13 + .../notebooks/LCMS_Collection_Tutorial.ipynb | 5742 +++++++++++++++++ .../metabolomics/metabolomics_collection.py | 17 +- tests/test_lcms_collection.py | 3 +- tests/test_lcms_metabolomics.py | 3 +- 5 files changed, 5775 insertions(+), 3 deletions(-) create mode 100644 examples/notebooks/LCMS_Collection_Tutorial.ipynb diff --git a/corems/chroma_peak/factory/chroma_peak_classes.py b/corems/chroma_peak/factory/chroma_peak_classes.py index 83a41658f..c09564f7f 100644 --- a/corems/chroma_peak/factory/chroma_peak_classes.py +++ b/corems/chroma_peak/factory/chroma_peak_classes.py @@ -386,6 +386,11 @@ def _plot_ms2_mirror(self, ax, molecular_metadata=None, spectral_library=None): FlashEntropy spectral library containing MS2 spectra. If provided, uses library to retrieve MS2 spectra by ref_ms_id. Default is None. + + Raises + ------ + ValueError + If MS2 similarity results exist but molecular_metadata or spectral_library is None. """ if len(self.ms2_mass_spectra) == 0: ax.text(0.5, 0.5, 'No MS2 data available', @@ -399,6 +404,14 @@ def _plot_ms2_mirror(self, ax, molecular_metadata=None, spectral_library=None): self._plot_ms2_spectrum(ax) return + # If we have MS2 similarity results, we need both molecular_metadata and spectral_library + if molecular_metadata is None or spectral_library is None: + raise ValueError( + "MS2 mirror plot requires both 'molecular_metadata' and 'spectral_library' " + "parameters when MS2 similarity results are present. " + "Please provide both parameters to plot_cluster() or plot()." + ) + # Get experimental MS2 sample_ms2 = self.best_ms2 sample_mz = sample_ms2.mz_exp diff --git a/examples/notebooks/LCMS_Collection_Tutorial.ipynb b/examples/notebooks/LCMS_Collection_Tutorial.ipynb new file mode 100644 index 000000000..ba626faa9 --- /dev/null +++ b/examples/notebooks/LCMS_Collection_Tutorial.ipynb @@ -0,0 +1,5742 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8c58bc4b", + "metadata": {}, + "source": [ + "# LC-MS Collection Processing\n", + "## Multi-Sample Metabolomics Workflow with Consensus Features\n", + "\n", + "This notebook demonstrates how to process multiple LC-MS samples as a collection using CoreMS, enabling cross-sample comparison, alignment, and consensus feature detection.\n", + "\n", + "### Workflow Overview\n", + "1. Prepare individual LC-MS samples and export to HDF5\n", + "2. Load samples as an LCMSCollection\n", + "3. Align retention times across samples\n", + "4. Generate consensus mass features (clustering)\n", + "5. Perform gap filling for missing features\n", + "6. Create pivot tables and cluster representatives\n", + "7. Add molecular annotations (MS1 formula search and MS2 spectral matching)\n", + "8. Export collection results\n", + "9. Visualize collection-level data\n", + "\n", + "### Key Concepts\n", + "- **Consensus Features**: Clusters of mass features that represent the same chemical entity across samples\n", + "- **Gap Filling**: Searching raw data for features missing in a sample but present in others\n", + "- **Representative Features**: The best representative mass feature from each consensus cluster\n", + "- **Collection Pivot Table**: Matrix showing feature presence/absence or intensity across all samples\n", + "\n", + "### Data Format\n", + "This tutorial uses the same Thermo Fisher RAW format LC-MS data as the LCMS_Tutorial, but processes it as a collection of 3 samples with different feature levels to demonstrate gap filling and collection-level analysis." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "c6d8a470", + "metadata": {}, + "outputs": [], + "source": [ + "# Import required packages\n", + "import numpy as np\n", + "import pandas as pd\n", + "from pathlib import Path\n", + "import shutil\n", + "\n", + "from corems.mass_spectra.input.rawFileReader import ImportMassSpectraThermoMSFileReader\n", + "from corems.mass_spectra.output.export import LCMSMetabolomicsExport, LCMSCollectionExport\n", + "from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectraCollection\n", + "from corems.encapsulation.factory.parameters import LCMSParameters\n", + "from corems.molecular_id.search.database_interfaces import MSPInterface\n", + "from corems.encapsulation.factory.parameters import hush_output\n", + "\n", + "# Running this keeps the notebook output cleaner and is recommended unless debugging\n", + "hush_output()\n", + "\n", + "# Suppress warnings for cleaner output\n", + "import warnings\n", + "warnings.filterwarnings('ignore')" + ] + }, + { + "cell_type": "markdown", + "id": "da3b4cb0", + "metadata": {}, + "source": [ + "## Step 1: Prepare Individual Samples\n", + "\n", + "For this tutorial, we'll create 3 samples with different levels of mass features to demonstrate gap filling:\n", + "- **Sample 1**: Full set of mass features (reference sample)\n", + "- **Sample 2**: Partial set (first 50 features only)\n", + "- **Sample 3**: No mass features (extreme case for gap filling)\n", + "\n", + "In a real workflow, you would process actual different samples, but this demonstrates the collection functionality." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "11d13a80", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tutorial data will be saved to: /Users/heal742/LOCAL/corems_dev/corems/examples/notebooks/tutorial_collection_data\n" + ] + } + ], + "source": [ + "# Set up paths for this tutorial\n", + "raw_file_path = Path('../../tests/tests_data/lcms/Blanch_Nat_Lip_C_12_AB_M_17_NEG_25Jan18_Brandi-WCSH5801.raw')\n", + "processed_folder = Path('./tutorial_collection_data')\n", + "\n", + "# Clean up any existing tutorial data\n", + "if processed_folder.exists():\n", + " shutil.rmtree(processed_folder)\n", + "processed_folder.mkdir()\n", + "\n", + "print(f\"Tutorial data will be saved to: {processed_folder.absolute()}\")" + ] + }, + { + "cell_type": "markdown", + "id": "55da94d5", + "metadata": {}, + "source": [ + "### Configure Processing Parameters\n", + "\n", + "Set parameters appropriate for this dataset. These control peak picking, noise thresholding, and other processing steps." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96802b8e", + "metadata": {}, + "outputs": [], + "source": [ + "# Load and configure the raw data\n", + "parser = ImportMassSpectraThermoMSFileReader(str(raw_file_path))\n", + "lcms_obj = parser.get_lcms_obj(spectra=\"ms1\")\n", + "\n", + "# Set parameters for fast processing\n", + "lcms_obj.parameters = LCMSParameters(use_defaults=True)\n", + "\n", + "# Persistent homology peak picking parameters\n", + "lcms_obj.parameters.lc_ms.peak_picking_method = \"persistent homology\"\n", + "lcms_obj.parameters.lc_ms.ph_inten_min_rel = 0.001\n", + "lcms_obj.parameters.lc_ms.ph_persis_min_rel = 0.05\n", + "lcms_obj.parameters.lc_ms.ph_smooth_it = 0\n", + "lcms_obj.parameters.lc_ms.ms1_scans_to_average = 3\n", + "\n", + "# MS1 parameters\n", + "ms1_params = lcms_obj.parameters.mass_spectrum['ms1']\n", + "ms1_params.mass_spectrum.noise_threshold_method = \"relative_abundance\"\n", + "ms1_params.mass_spectrum.noise_threshold_min_relative_abundance = 0.1\n", + "ms1_params.mass_spectrum.noise_min_mz = 0\n", + "ms1_params.mass_spectrum.min_picking_mz = 0\n", + "ms1_params.mass_spectrum.noise_max_mz = np.inf\n", + "ms1_params.mass_spectrum.max_picking_mz = np.inf\n", + "ms1_params = lcms_obj.parameters.mass_spectrum['ms1']\n", + "\n", + "# Molecular formula search parameters\n", + "ms1_params.molecular_search.url_database = \"\" # This will run with a locally generated sqlite database\n", + "ms1_params.molecular_search.min_ppm_error = -5\n", + "ms1_params.molecular_search.max_ppm_error = 5\n", + "ms1_params.molecular_search.isRadical = False # Do not search for radical species, only protonated; the report will report the ion formula\n", + "ms1_params.molecular_search.usedAtoms = { # Elements and their min/max counts\n", + " 'C': (1, 90),\n", + " 'H': (4, 200),\n", + " 'O': (0, 30),\n", + " 'N': (0, 3),\n", + " 'P': (0, 2),\n", + " 'S': (0, 2),\n", + "}\n", + "\n", + "# MS2 parameters\n", + "ms2_params = lcms_obj.parameters.mass_spectrum['ms2']\n", + "ms2_params.mass_spectrum.noise_threshold_method = \"relative_abundance\"\n", + "ms2_params.mass_spectrum.noise_threshold_min_relative_abundance = 1.0\n", + "\n", + "print(\"Parameters configured for LC-MS processing\")" + ] + }, + { + "cell_type": "markdown", + "id": "98b0feab", + "metadata": {}, + "source": [ + "### Process and Create Sample 1 (Full Features)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8528773", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Finding mass features...\n", + "Found 129 initial mass features\n", + "Found 126 mass features in Sample 1\n", + "✓ Exported Sample 1: 126 features\n" + ] + } + ], + "source": [ + "# Find and integrate mass features, and find ms2 scans that are associated with these features\n", + "print(\"Finding mass features...\")\n", + "lcms_obj.find_mass_features(assign_ms2_scans=True)\n", + "lcms_obj.integrate_mass_features(drop_if_fail=True)\n", + "\n", + "num_features = len(lcms_obj.mass_features)\n", + "print(f\"Found {num_features} mass features in Sample 1\")\n", + "\n", + "# Save all features for creating sample variations\n", + "all_mass_features = dict(lcms_obj.mass_features)\n", + "all_eics = dict(lcms_obj.eics)\n", + "\n", + "# Export Sample 1 with all features\n", + "sample_name_1 = \"test_sample_01\"\n", + "exporter1 = LCMSMetabolomicsExport(str(processed_folder / sample_name_1), lcms_obj)\n", + "exporter1.to_hdf(overwrite=True)\n", + "print(f\"✓ Exported Sample 1: {num_features} features\")" + ] + }, + { + "cell_type": "markdown", + "id": "2093f6eb", + "metadata": {}, + "source": [ + "### Create Sample 2 (Partial Features)\n", + "\n", + "Take only the first 50 mass features to simulate a sample with fewer detected features." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "edca055c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ Exported Sample 2: 50 features\n" + ] + } + ], + "source": [ + "# Create Sample 2 with partial features\n", + "sample_name_2 = \"test_sample_02\"\n", + "first_50_mf_ids = list(lcms_obj.mass_features.keys())[:50]\n", + "lcms_obj.mass_features = {mf_id: all_mass_features[mf_id] for mf_id in first_50_mf_ids}\n", + "\n", + "exporter2 = LCMSMetabolomicsExport(str(processed_folder / sample_name_2), lcms_obj)\n", + "exporter2.to_hdf(overwrite=True)\n", + "print(f\"✓ Exported Sample 2: 50 features\")" + ] + }, + { + "cell_type": "markdown", + "id": "53352f5f", + "metadata": {}, + "source": [ + "### Create Sample 3 (No Features)\n", + "\n", + "Clear all features to simulate an extreme case where gap filling will be needed for all features." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa186ab4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ Exported Sample 3: 0 features (will be gap-filled)\n" + ] + } + ], + "source": [ + "# Create Sample 3 with no features\n", + "sample_name_3 = \"test_sample_03\"\n", + "lcms_obj.mass_features = {}\n", + "lcms_obj.eics = {}\n", + "\n", + "exporter3 = LCMSMetabolomicsExport(str(processed_folder / sample_name_3), lcms_obj)\n", + "exporter3.to_hdf(overwrite=True)\n", + "print(f\"✓ Exported Sample 3: 0 features (will be gap-filled)\")" + ] + }, + { + "cell_type": "markdown", + "id": "2594b04e", + "metadata": {}, + "source": [ + "### Create Collection Manifest\n", + "\n", + "A manifest file defines the sample metadata including batch, order, and which sample is the center (reference) for alignment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "921ca2b2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ Created manifest with 3 samples\n", + "\n", + "Sample preparation complete!\n" + ] + } + ], + "source": [ + "# Create manifest file\n", + "import csv\n", + "manifest_path = processed_folder / \"manifest.csv\"\n", + "with open(manifest_path, 'w', newline='') as f:\n", + " writer = csv.writer(f)\n", + " writer.writerow(['sample_name', 'batch', 'order', 'center'])\n", + " writer.writerow(['test_sample_01', 1, 1, True]) # Sample 1 is center (reference)\n", + " writer.writerow(['test_sample_02', 1, 2, False])\n", + " writer.writerow(['test_sample_03', 1, 3, False])\n", + "\n", + "print(f\"✓ Created manifest with 3 samples\")\n", + "print(\"\\nSample preparation complete!\")" + ] + }, + { + "cell_type": "markdown", + "id": "90e9bf0f", + "metadata": {}, + "source": [ + "## Step 2: Load the Collection\n", + "\n", + "Load all samples as a collection. The manifest defines which samples to include and their metadata. If not manifest is provided, samples can be loaded directly and a manifest will be generated automatically using the collection times to deduce batch/order, which is important for alignment. For our case, we will use the manifest we created since we have specific batch/order information." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3f61de41", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded collection with 3 samples\n", + "Total mass features across all samples: 176\n" + ] + } + ], + "source": [ + "# Load collection from processed samples\n", + "parser = ReadCoreMSHDFMassSpectraCollection(\n", + " folder_location=processed_folder,\n", + " manifest_file=manifest_path,\n", + " cores=1\n", + ")\n", + "\n", + "# Load collection (light loading for efficiency - doesn't load all raw MS data)\n", + "lcms_collection = parser.get_lcms_collection()\n", + "\n", + "print(f\"Loaded collection with {len(lcms_collection)} samples\")\n", + "print(f\"Total mass features across all samples: {len(lcms_collection.mass_features_dataframe)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "4cf55d10", + "metadata": {}, + "source": [ + "### Examine Collection Structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "820ce6ab", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Samples in collection:\n", + " 0: test_sample_01 - 0 features\n", + " 1: test_sample_02 - 0 features\n", + " 2: test_sample_03 - 0 features\n", + "\n", + "Manifest:\n" + ] + }, + { + "data": { + "application/vnd.microsoft.datawrangler.viewer.v0+json": { + "columns": [ + { + "name": "index", + "rawType": "object", + "type": "string" + }, + { + "name": "batch", + "rawType": "object", + "type": "string" + }, + { + "name": "order", + "rawType": "object", + "type": "string" + }, + { + "name": "center", + "rawType": "object", + "type": "string" + }, + { + "name": "collection_id", + "rawType": "object", + "type": "string" + } + ], + "ref": "0bb52bbc-ec6b-4cf7-bb54-b63e22bcf83a", + "rows": [ + [ + "test_sample_01", + "1", + "1", + "True", + "0" + ], + [ + "test_sample_02", + "1", + "2", + "False", + "1" + ], + [ + "test_sample_03", + "1", + "3", + "False", + "2" + ] + ], + "shape": { + "columns": 4, + "rows": 3 + } + }, + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
batchordercentercollection_id
test_sample_0111True0
test_sample_0212False1
test_sample_0313False2
\n", + "
" + ], + "text/plain": [ + " batch order center collection_id\n", + "test_sample_01 1 1 True 0\n", + "test_sample_02 1 2 False 1\n", + "test_sample_03 1 3 False 2" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# View sample names\n", + "print(\"Samples in collection:\")\n", + "for i, sample_name in enumerate(lcms_collection.samples):\n", + " num_features = len(lcms_collection[i].mass_features)\n", + " print(f\" {i}: {sample_name} - {num_features} features\")\n", + "\n", + "# View manifest dataframe\n", + "print(\"\\nManifest:\")\n", + "display(lcms_collection.manifest_dataframe)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "094b8400", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Mass Features Dataframe (all features from all samples):\n" + ] + }, + { + "data": { + "application/vnd.microsoft.datawrangler.viewer.v0+json": { + "columns": [ + { + "name": "coll_mf_id", + "rawType": "object", + "type": "string" + }, + { + "name": "sample_name", + "rawType": "object", + "type": "string" + }, + { + "name": "sample_id", + "rawType": "float64", + "type": "float" + }, + { + "name": "mf_id", + "rawType": "float64", + "type": "float" + }, + { + "name": "mz", + "rawType": "float64", + "type": "float" + }, + { + "name": "type", + "rawType": "object", + "type": "string" + }, + { + "name": "scan_time", + "rawType": "float64", + "type": "float" + }, + { + "name": "apex_scan", + "rawType": "float64", + "type": "float" + }, + { + "name": "start_scan", + "rawType": "float64", + "type": "float" + }, + { + "name": "final_scan", + "rawType": "float64", + "type": "float" + }, + { + "name": "intensity", + "rawType": "float64", + "type": "float" + }, + { + "name": "persistence", + "rawType": "float64", + "type": "float" + }, + { + "name": "area", + "rawType": "float32", + "type": "float" + }, + { + "name": "tailing_factor", + "rawType": "float64", + "type": "float" + }, + { + "name": "dispersity_index", + "rawType": "float64", + "type": "float" + }, + { + "name": "normalized_dispersity_index", + "rawType": "float64", + "type": "float" + }, + { + "name": "noise_score", + "rawType": "float64", + "type": "float" + }, + { + "name": "noise_score_min", + "rawType": "float64", + "type": "float" + }, + { + "name": "noise_score_max", + "rawType": "float64", + "type": "float" + }, + { + "name": "monoisotopic_mf_id", + "rawType": "object", + "type": "string" + }, + { + "name": "isotopologue_type", + "rawType": "object", + "type": "string" + }, + { + "name": "mass_spectrum_deconvoluted_parent", + "rawType": "object", + "type": "string" + }, + { + "name": "ms2_scan_numbers", + "rawType": "object", + "type": "string" + }, + { + "name": "_eic_mz", + "rawType": "float64", + "type": "float" + } + ], + "ref": "b988ea91-2bd0-433b-81d1-4bd5825578f1", + "rows": [ + [ + "0_0", + "test_sample_01", + "0.0", + "0.0", + "301.21661376953125", + "untargeted", + "8.895636666666666", + "1882.0", + "1828.0", + "2008.0", + "66775328.0", + "66708546.0", + "35045576.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[1874 1910]", + "301.21661376953125" + ], + [ + "0_1", + "test_sample_01", + "0.0", + "1.0", + "367.35748291015625", + "untargeted", + "19.152648333333335", + "4069.0", + "4024.0", + "4312.0", + "48137056.0", + "48070260.0", + "30641268.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[4036 4037 4070]", + "367.35748291015625" + ], + [ + "0_10", + "test_sample_01", + "0.0", + "10.0", + "698.62890625", + "untargeted", + "23.816803333333333", + "5212.0", + "5176.0", + "5338.0", + "17265106.0", + "17198326.0", + "7113439.5", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[5195 5196 5231 5232]", + "698.62890625" + ], + [ + "0_100", + "test_sample_01", + "0.0", + "100.0", + "569.1968994140625", + "untargeted", + "4.421146666666667", + "775.0", + "721.0", + "856.0", + "4048302.0", + "3981509.0", + "1949091.8", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[749 750 785 786]", + "569.1968994140625" + ], + [ + "0_101", + "test_sample_01", + "0.0", + "101.0", + "300.2048645019531", + "untargeted", + "7.376469999999999", + "1513.0", + "1477.0", + "1585.0", + "4030582.25", + "3963787.0", + "1566851.1", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]", + "300.2048645019531" + ], + [ + "0_102", + "test_sample_01", + "0.0", + "102.0", + "456.35662841796875", + "untargeted", + "8.96547", + "1900.0", + "1855.0", + "1999.0", + "4012202.0", + "3943451.0", + "2064801.6", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]", + "456.35662841796875" + ], + [ + "0_103", + "test_sample_01", + "0.0", + "103.0", + "527.4675903320312", + "untargeted", + "17.55847", + "3718.0", + "3682.0", + "3826.0", + "4000847.75", + "3934066.0", + "1553304.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[3703 3704 3737 3738]", + "527.4675903320312" + ], + [ + "0_104", + "test_sample_01", + "0.0", + "104.0", + "736.5104370117188", + "untargeted", + "20.793636666666668", + "4483.0", + "4438.0", + "4609.0", + "3974837.75", + "3908057.0", + "621550.9", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]", + "736.5104370117188" + ], + [ + "0_105", + "test_sample_01", + "0.0", + "105.0", + "256.2359619140625", + "untargeted", + "9.071805", + "1927.0", + "1891.0", + "2062.0", + "3900504.25", + "3830292.0", + "1473387.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]", + "256.2359619140625" + ], + [ + "0_106", + "test_sample_01", + "0.0", + "106.0", + "384.3563232421875", + "untargeted", + "10.071803333333333", + "2170.0", + "2143.0", + "2323.0", + "3833882.0", + "3767107.0", + "1835834.8", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]", + "384.3563232421875" + ] + ], + "shape": { + "columns": 23, + "rows": 10 + } + }, + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sample_namesample_idmf_idmztypescan_timeapex_scanstart_scanfinal_scanintensity...dispersity_indexnormalized_dispersity_indexnoise_scorenoise_score_minnoise_score_maxmonoisotopic_mf_idisotopologue_typemass_spectrum_deconvoluted_parentms2_scan_numbers_eic_mz
coll_mf_id
0_0test_sample_010.00.0301.216614untargeted8.8956371882.01828.02008.066775328.00...NaNNaNNaNNaNNaNNoneNoneNone[1874, 1910]301.216614
0_1test_sample_010.01.0367.357483untargeted19.1526484069.04024.04312.048137056.00...NaNNaNNaNNaNNaNNoneNoneNone[4036, 4037, 4070]367.357483
0_10test_sample_010.010.0698.628906untargeted23.8168035212.05176.05338.017265106.00...NaNNaNNaNNaNNaNNoneNoneNone[5195, 5196, 5231, 5232]698.628906
0_100test_sample_010.0100.0569.196899untargeted4.421147775.0721.0856.04048302.00...NaNNaNNaNNaNNaNNoneNoneNone[749, 750, 785, 786]569.196899
0_101test_sample_010.0101.0300.204865untargeted7.3764701513.01477.01585.04030582.25...NaNNaNNaNNaNNaNNoneNoneNone[]300.204865
0_102test_sample_010.0102.0456.356628untargeted8.9654701900.01855.01999.04012202.00...NaNNaNNaNNaNNaNNoneNoneNone[]456.356628
0_103test_sample_010.0103.0527.467590untargeted17.5584703718.03682.03826.04000847.75...NaNNaNNaNNaNNaNNoneNoneNone[3703, 3704, 3737, 3738]527.467590
0_104test_sample_010.0104.0736.510437untargeted20.7936374483.04438.04609.03974837.75...NaNNaNNaNNaNNaNNoneNoneNone[]736.510437
0_105test_sample_010.0105.0256.235962untargeted9.0718051927.01891.02062.03900504.25...NaNNaNNaNNaNNaNNoneNoneNone[]256.235962
0_106test_sample_010.0106.0384.356323untargeted10.0718032170.02143.02323.03833882.00...NaNNaNNaNNaNNaNNoneNoneNone[]384.356323
\n", + "

10 rows × 23 columns

\n", + "
" + ], + "text/plain": [ + " sample_name sample_id mf_id mz type \\\n", + "coll_mf_id \n", + "0_0 test_sample_01 0.0 0.0 301.216614 untargeted \n", + "0_1 test_sample_01 0.0 1.0 367.357483 untargeted \n", + "0_10 test_sample_01 0.0 10.0 698.628906 untargeted \n", + "0_100 test_sample_01 0.0 100.0 569.196899 untargeted \n", + "0_101 test_sample_01 0.0 101.0 300.204865 untargeted \n", + "0_102 test_sample_01 0.0 102.0 456.356628 untargeted \n", + "0_103 test_sample_01 0.0 103.0 527.467590 untargeted \n", + "0_104 test_sample_01 0.0 104.0 736.510437 untargeted \n", + "0_105 test_sample_01 0.0 105.0 256.235962 untargeted \n", + "0_106 test_sample_01 0.0 106.0 384.356323 untargeted \n", + "\n", + " scan_time apex_scan start_scan final_scan intensity ... \\\n", + "coll_mf_id ... \n", + "0_0 8.895637 1882.0 1828.0 2008.0 66775328.00 ... \n", + "0_1 19.152648 4069.0 4024.0 4312.0 48137056.00 ... \n", + "0_10 23.816803 5212.0 5176.0 5338.0 17265106.00 ... \n", + "0_100 4.421147 775.0 721.0 856.0 4048302.00 ... \n", + "0_101 7.376470 1513.0 1477.0 1585.0 4030582.25 ... \n", + "0_102 8.965470 1900.0 1855.0 1999.0 4012202.00 ... \n", + "0_103 17.558470 3718.0 3682.0 3826.0 4000847.75 ... \n", + "0_104 20.793637 4483.0 4438.0 4609.0 3974837.75 ... \n", + "0_105 9.071805 1927.0 1891.0 2062.0 3900504.25 ... \n", + "0_106 10.071803 2170.0 2143.0 2323.0 3833882.00 ... \n", + "\n", + " dispersity_index normalized_dispersity_index noise_score \\\n", + "coll_mf_id \n", + "0_0 NaN NaN NaN \n", + "0_1 NaN NaN NaN \n", + "0_10 NaN NaN NaN \n", + "0_100 NaN NaN NaN \n", + "0_101 NaN NaN NaN \n", + "0_102 NaN NaN NaN \n", + "0_103 NaN NaN NaN \n", + "0_104 NaN NaN NaN \n", + "0_105 NaN NaN NaN \n", + "0_106 NaN NaN NaN \n", + "\n", + " noise_score_min noise_score_max monoisotopic_mf_id \\\n", + "coll_mf_id \n", + "0_0 NaN NaN None \n", + "0_1 NaN NaN None \n", + "0_10 NaN NaN None \n", + "0_100 NaN NaN None \n", + "0_101 NaN NaN None \n", + "0_102 NaN NaN None \n", + "0_103 NaN NaN None \n", + "0_104 NaN NaN None \n", + "0_105 NaN NaN None \n", + "0_106 NaN NaN None \n", + "\n", + " isotopologue_type mass_spectrum_deconvoluted_parent \\\n", + "coll_mf_id \n", + "0_0 None None \n", + "0_1 None None \n", + "0_10 None None \n", + "0_100 None None \n", + "0_101 None None \n", + "0_102 None None \n", + "0_103 None None \n", + "0_104 None None \n", + "0_105 None None \n", + "0_106 None None \n", + "\n", + " ms2_scan_numbers _eic_mz \n", + "coll_mf_id \n", + "0_0 [1874, 1910] 301.216614 \n", + "0_1 [4036, 4037, 4070] 367.357483 \n", + "0_10 [5195, 5196, 5231, 5232] 698.628906 \n", + "0_100 [749, 750, 785, 786] 569.196899 \n", + "0_101 [] 300.204865 \n", + "0_102 [] 456.356628 \n", + "0_103 [3703, 3704, 3737, 3738] 527.467590 \n", + "0_104 [] 736.510437 \n", + "0_105 [] 256.235962 \n", + "0_106 [] 384.356323 \n", + "\n", + "[10 rows x 23 columns]" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# View mass features dataframe (first few rows)\n", + "print(\"\\nMass Features Dataframe (all features from all samples):\")\n", + "display(lcms_collection.mass_features_dataframe.head(10))" + ] + }, + { + "cell_type": "markdown", + "id": "63c30c97", + "metadata": {}, + "source": [ + "### Configure Collection Parameters\n", + "\n", + "Set parameters for alignment, clustering, and gap filling." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "112ccb3c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Collection parameters configured\n" + ] + } + ], + "source": [ + "# Alignment parameters\n", + "lcms_collection.parameters.lcms_collection.mass_feature_anchor_technique = ['relative_intensity']\n", + "lcms_collection.parameters.lcms_collection.mass_feature_anchor_relative_intensity_threshold = 0.0\n", + "lcms_collection.parameters.lcms_collection.alignment_mz_tol_ppm = 5\n", + "lcms_collection.parameters.lcms_collection.alignment_rt_tol = 0.2 # 12 seconds\n", + "\n", + "print(\"Collection parameters configured\")" + ] + }, + { + "cell_type": "markdown", + "id": "7f4a79ac", + "metadata": {}, + "source": [ + "## Step 3: Align Retention Times\n", + "\n", + "Align retention times across samples to account for chromatographic drift." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d989ed7a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "RT aligned before alignment: False\n", + "RT alignment attempted: True\n", + "RT aligned after alignment: False\n", + " Sample 0 has scan_time_aligned: True\n", + " Sample 1 has scan_time_aligned: True\n", + " Sample 2 has scan_time_aligned: True\n" + ] + } + ], + "source": [ + "# Perform RT alignment\n", + "print(f\"RT aligned before alignment: {lcms_collection.rt_aligned}\")\n", + "\n", + "lcms_collection.align_lcms_objects()\n", + "\n", + "print(f\"RT alignment attempted: {lcms_collection.rt_alignment_attempted}\")\n", + "print(f\"RT aligned after alignment: {lcms_collection.rt_aligned}\")\n", + "\n", + "# Check that scan_time_aligned was added to all samples\n", + "for i, lcms_obj in enumerate(lcms_collection):\n", + " has_aligned = 'scan_time_aligned' in lcms_obj.scan_df.columns\n", + " print(f\" Sample {i} has scan_time_aligned: {has_aligned}\")" + ] + }, + { + "cell_type": "markdown", + "id": "b52ce5c8", + "metadata": {}, + "source": [ + "### Visualize TICs and Alignment\n", + "\n", + "In our case, the samples will not use any alignment since they are exact replicas. Therefore the aligned plot (lower) will be overlapping. Even if samples do not need alignment, running `align_lcms_objects()` is important as it will trigger the collection-level flag that alignment was attempted, which is a prerequisit for finding consensus mass features" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4442c8d6", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA04AAANQCAYAAAAffD9qAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/OQEPoAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdeXhTZdo/8O/J1jRNkzTdWwplK4vUUsAF0VFHZBll3Bgc9NXRUd/XhZ8w6Iw6jjvC6AiuqKOO67ij4sbo4IIgIspSEGVvS1u6r2mSZj3n90cgbZq0Sdukadrv57p6ac55zjl3Spue+zzPcz+CJEkSiIiIiIiIqEuyaAdAREREREQ00DFxIiIiIiIiCoKJExERERERURBMnIiIiIiIiIJg4kRERERERBQEEyciIiIiIqIgmDgREREREREFwcSJiIiIiIgoCCZOREREREREQTBxIiIiIiIiCmJIJ04bN27EvHnzkJWVBUEQsHbt2h6f4/PPP8epp56KxMREpKam4pJLLkFpaWnYYyUiIiIiougZ0omTxWJBQUEBVq9e3avjS0pKcMEFF+DXv/41ioqK8Pnnn6O+vh4XX3xxmCMlIiIiIqJoEiRJkqIdxEAgCAI++OADXHjhhd5tdrsdd955J9588000Nzdj0qRJeOihh3DWWWcBANasWYOFCxfCbrdDJvPkoB9//DEuuOAC2O12KJXKKLwTIiIiIiIKtyHd4xTMokWLsGXLFrz11lvYvXs3fve732HOnDk4ePAgAGDq1KmQyWR46aWX4Ha70dLSgtdeew0zZ85k0kRERERENIiwx+mYzj1OZWVlGDVqFMrKypCVleVtN3PmTJx88slYvnw5AOCbb77BggUL0NDQALfbjenTp2PdunUwGAxReBdERERERBQJ7HHqwk8//QS32428vDxotVrv1zfffIPDhw8DAKqrq3HdddfhD3/4A3788Ud88803UKlUmD9/PpiPEhERERENHopoBzBQmc1myOVybN++HXK53GefVqsFAKxevRp6vR4PP/ywd9+///1v5OTkYOvWrTj11FP7NWYiIiIiIooMJk5dKCwshNvtRm1tLc4444yAbaxWq7coxHHHkyxRFCMeIxERERER9Y8hPVTPbDajqKgIRUVFADzlxYuKilBWVoa8vDxcfvnluPLKK/H++++jpKQEP/zwA1asWIFPP/0UAHDeeefhxx9/xP3334+DBw9ix44duPrqqzFixAgUFhZG8Z0REREREVE4DeniEBs2bMDZZ5/tt/0Pf/gDXn75ZTidTixbtgyvvvoqjh49ipSUFJx66qm47777kJ+fDwB466238PDDD+PAgQPQaDSYPn06HnroIYwfP76/3w4REREREUXIkE6ciIiIiIiIQjGkh+oRERERERGFgokTERERERFREEOuqp4oiqisrERiYiIEQYh2OEREREREFCWSJKG1tRVZWVl+1bI7G3KJU2VlJXJycqIdBhERERERDRDl5eUYNmxYt22GXOKUmJgIwPPN0el0UY6GiIiIiIiixWQyIScnx5sjdGfIJU7Hh+fpdDomTkREREREFNIUHhaHICIiIiIiCoKJExERERERURBMnIiIiIiIiIIYcnOciIiIiCi63G43nE5ntMOgIUKpVEIul/f5PEyciIiIiKjfmM1mVFRUQJKkaIdCQ4QgCBg2bBi0Wm2fzsPEiYiIiIj6hdvtRkVFBTQaDVJTU0OqZEbUF5Ikoa6uDhUVFRg7dmyfep6YOBERERFRv3A6nZAkCampqYiPj492ODREpKamorS0FE6ns0+JE4tDEBEREVG/Yk8T9adw/bwxcSIiIiIiIgqCiRMREREREVEQUU2cNm7ciHnz5iErKwuCIGDt2rVBj7Hb7bjzzjsxYsQIxMXFITc3Fy+++GLkgyUiIiIiIj+h3sfHuqgmThaLBQUFBVi9enXIxyxYsABffvkl/vWvf2H//v148803MW7cuAhGSURERERD3VlnnYUlS5aE7XxXXXUVLrzwwrCdbzB59913MX78eKjVauTn52PdunU++99//33MmjULycnJEAQBRUVF/RJXVKvqzZ07F3Pnzg25/WeffYZvvvkGxcXFMBqNAIDc3NwIRUdEfVVTVAzR6UbmSWOjHQoRERHFgO+++w4LFy7EihUrcP755+ONN97AhRdeiB07dmDSpEkAPJ0vp59+OhYsWIDrrruu32KLqTlOH330EaZNm4aHH34Y2dnZyMvLw6233oq2trYuj7Hb7TCZTD5fRNQ/mn4+hOp1/412GERERH1y1VVX4ZtvvsHjjz8OQRAgCAJKS0uxZ88ezJ07F1qtFunp6bjiiitQX1/vPW7NmjXIz89HfHw8kpOTMXPmTFgsFtx777145ZVX8OGHH3rP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Ozs7aGG38vJyLFiwAAaDAUajERdccAFKS0tD+l4//vjjmDNnDv785z9jwoQJeOCBBzBlyhQ89dRT3jaXXXYZZs6ciVGjRuGEE07AqlWrYDKZsHv37pCu0VsxkzjpdDr89NNPKCoq8n5df/31GDduHIqKinDKKacEPC4uLg46nc7ni4iIiIgoVI8//jimT5+O6667DlVVVaiqqkJiYiJ+/etfo7CwENu2bcNnn32GmpoaLFiwAABQVVWFhQsX4o9//CP27t2LDRs24OKLL4YkSbj11luxYMECzJkzx3u+0047rdsYnnjiCXz00Ud45513sH//frz++uvIzc317pfJZHjiiSfw888/45VXXsFXX32Fv/zlLz7nsFqteOKJJ/DWW2/hs88+w4YNG3DRRRdh3bp1WLduHV577TX885//xJo1a3yO+8c//oGCggLs3LkTt99+OxYvXoz169cHjNPpdGL27NlITEzEpk2bsHnzZmi1WsyZMwcOhyPo93rLli2YOXOmz7bZs2djy5YtAds7HA4899xz0Ov1KCgoCHr+vohqVT2z2YxDhw55X5eUlKCoqAhGoxHDhw/HHXfcgaNHj+LVV1+FTCbDpEmTfI5PS0uDWq32205EA4wgRDsCIiIaqCwW4Jdf+veaEycCCQkhN9fr9VCpVNBoNMjIyAAALFu2DIWFhVi+fLm33YsvvoicnBwcOHAAZrMZLpcLF198MUaMGAEAyM/P97aNj4+H3W73ni+YsrIyjB07FqeffjoEQfCe87iO869yc3OxbNkyXH/99Xj66ae9251OJ5555hmMHj0aADB//ny89tprqKmpgVarxcSJE3H22Wfj66+/xqWXXuo9bsaMGbj99tsBeIbJbd68GY8++ijOPfdcvzjffvttiKKIF154AcKxv/8vvfQSDAYDNmzYgFmzZnX7Pqurq5Genu6zLT09HdXV1T7bPvnkE/z+97+H1WpFZmYm1q9fj5SUlG7P3VdR7XHatm0bCgsLUVhYCABYunQpCgsLcffddwPwZOplZWXRDJGIwoFznIiIaJDZtWsXvv76a2i1Wu/X+PHjAQCHDx9GQUEBzjnnHOTn5+N3v/sdnn/+eTQ1NfX6eldddRWKioowbtw43Hzzzfjvf//rs/+LL77AOeecg+zsbCQmJuKKK65AQ0MDrFart41Go/EmTYAnIcnNzYVW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V+Lhhx+OeNLkdDqRnZ2N++67L6JxEBGFG3uciIiIiIiIgmCPExERERERURBMnIiIiIiIiIJg4kRERERERBQEEyciIiIiIqIgmDgREREREREFwcSJiIiIiIgoCCZOREREREREQTBxIiIiIiIiCoKJExERERERURBMnIiIiIiIiIJg4kRERERERBQEEyciIiIiIqIgmDgREREREREFwcSJiIiIiIgoCCZOREREREREQTBxIiIiIiIiCoKJExERERERURBMnIiIiIiIiIJg4kRERERERBQEEyciIiIiIqIgmDgREREREREFwcSJiIiIiIgoCCZOREREREREQTBxIiIiIiIiCoKJExERERERURBMnIiIiIiIiIJg4kRERERERBQEEyciIiIiIqIgmDgREREREREFwcSJiIiIiIgoCCZOREREREREQXTqxGn9+vUYP348unTpAkEQsGLFiia38d133+GCCy5AfHw8kpOTcf311yM/Pz/ssRIRERERUeR06sTJarUiJycHS5Ysadb5R44cwTXXXIM//OEPyMvLw3fffYfy8nJcd911YY6UiIiIiIgiSZBlWY50EO2BIAj47LPPMGHCBO82h8OBRx55BO+//z6qqqowaNAgPPPMMxg1ahQAYPny5ZgyZQocDgdEsS4H/fLLL3HNNdfA4XAgJiYmAu+EiIiIiIjCrVP3OAUzffp0bN68GR988AF27dqFG264AWPHjsWBAwcAAEOHDoUoinjrrbfg8XhQXV2Nd955B6NHj2bSRERERETUgbDH6ZSze5wKCgrQs2dPFBQUoEuXLt7jRo8ejeHDh2P+/PkAgHXr1mHSpEk4efIkPB4PRowYgZUrV8JoNEbgXRARERERUWtgj1MDfv31V3g8HvTt2xc6nc77tW7dOhw6dAgAUFxcjDvuuAO33norfvnlF6xbtw4qlQoTJ04E81EiIiIioo5DGekA2iuLxQKFQoFt27ZBoVD47NPpdACAJUuWwGAw4Nlnn/Xu+89//oPMzExs2bIFF1xwQZvGTERERERErYOJUwNyc3Ph8XhQWlqKiy++OOAxNpvNWxTitNNJliRJrR4jERERERG1jU49VM9isSAvLw95eXkA6sqL5+XloaCgAH379sVNN92EW265BZ9++imOHDmCn3/+GQsWLMDXX38NALjqqqvwyy+/4Mknn8SBAwewfft2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7NixuPrqq32SkO+//x4LFizA3r17YTab4Xa7YbfbYbPZoNVqAQBardabNAF1CUlWVpbPe0hNTUVpaanPtUeMGOH3+vnnnw8Y586dO3Hw4EHEx8f7bLfb7Th06FCz3nt7EVV3NCNHjsSJEyd8xmvu378foiiia9euEYyMiAKRPBJkURHpMIiIiMLOYrFg/PjxyMvL8/k6cOAALrnkEigUCqxatQrffPMNBg4ciMWLF6Nfv344cuRIs643ZMgQHDlyBE899RRqa2sxadIkTJw4EQCQn5+Pq6++Gueeey4++eQTbNu2zTsPyel0etuIiYnxaVMQhIDbJElqVoxA3fdl6NChft+X/fv348Ybbwx6flpaGkpKSny2lZSUhNwz15oimjhZLBbvNxMAjhw5gry8PBQUFACoG2Z3yy23eI+/8cYbkZiYiNtuuw2///471q9fj7/97W/485//HHCYHhFFmNsNKKKqY5uIiCgglUrlUzRhyJAh+O2335CVlYXevXv7fMXF1U2gEgQBI0eOxNy5c7Fjxw6oVCp89tlnAdsLhV6vx+TJk/H666/jww8/xCeffIKKigps27YNkiRh4cKFuOCCC9C3b1+cOHEibO/9p59+8ns9YMCAgMcOGTIEBw4cQEpKit/3pf6osYaMGDECq1ev9tm2atUqv16vSIho4rR161bk5uYiNzcXADBr1izk5uZizpw5AICioiJvEgUAOp0Oq1atQlVVFYYNG4abbroJ48ePx4svvhiR+ImocbIkQRaiqmObiIgooKysLGzZsgX5+fkoLy/HPffcg4qKCkyZMgW//PILDh06hO+++w633XYbPB4PtmzZgvnz52Pr1q0oKCjAp59+irKyMm/CkZWVhV27dmHfvn0oLy+Hy+Vq9PqLFi3C+++/j71792L//v34+OOPkZaWBqPRiN69e8PlcmHx4sU4fPgw3nnnHbz66qthe+8bN27Es88+i/3792PJkiX4+OOPMWPGjIDH3nTTTUhKSsI111yDDRs24MiRI1i7di3uvfdeFBYWBr3WjBkz8O2332LhwoXYu3cvnnjiCWzduhXTp0/3HlNRUYG8vDz8/ntdVZF9+/YhLy+v1edBRfSOZtSoUZBl2e9r6dKlAOrKJp5dmrF///5YtWoVbDYbjh07hoULF7K3iai98nggKxQt6vInIiJqDx544AEoFAoMHDgQycnJcDqd2LhxIzweD6688kpkZ2dj5syZMBqNEEURer0e69evxx//+Ef07dsXjz76KBYuXOhdw/SOO+5Av379MGzYMCQnJ2Pjxo2NXj8+Ph7PPvsshg0bhvPOOw/5+flYuXIlRFFETk4OFi1ahGeeeQaDBg3Cu+++61OqvKXuv/9+b4fHvHnzsGjRIowZMybgsVqtFuvXr0e3bt1w3XXXYcCAAZg2bRrsdjv0en3Qa1144YV477338NprryEnJwfLly/HihUrvGs4AXVLFOXm5uKqq64CAPzpT39Cbm5uWJPFQARZluVWvUI7YzabYTAYUF1dHdIPj4iab9eidyBv+xHnvLUEShWH7BERdXZ2ux1HjhxBjx49oNFoIh0OhSArKwszZ870qdoXbRr7vWtKbsAxNETUamSPB3KMCpLTHelQiIiIiFqEiRMRtR6PBFmpgtve+LhtIiKizm7+/PnQ6XQBv04P7+sIGnqPOp0OGzZsiHR4jeLYGSJqPR4PoIyBmz1OREREjbrzzjsxadKkgPsiOZ8/Pz8/rO2drqYdSEZGRlivFW5MnIio1chS3VA92cXEiYiIqDEmkwkmkynSYbS63r17RzqEZuNQPSJqNbLkAWJUcDs4VI+IiIiiGxMnImo9kgQhRgWJPU5EREQU5Zg4EVHr8dT1ODFxIiIiomjHxImIWo8kQVCpIbk8kY6EiIiIqEWYOBFR6/G4IahU8Dg5x4mIiIiiGxMnImo9kgxBpYLk5lA9IiKijkoQBKxYsSLSYbQ6Jk5E1Gpkyc3iEERE1CGMGjUKM2fODFt7U6dOxYQJE8LWXkfy8ccfo3///tBoNMjOzsbKlSu9+1wuF/7+978jOzsbcXFx6NKlC2655RacOHGi1eNi4kRErUeSIKrVkDnHiYiIiEKwadMmTJkyBdOmTcOOHTswYcIETJgwAbt37wYA2Gw2bN++HY899hi2b9+OTz/9FPv27cP//M//tHpsTJyIqPXIMoSYGEguznEiIqLoNXXqVKxbtw4vvPACBEGAIAjIz8/H7t27MW7cOOh0OqSmpuLmm29GeXm597zly5cjOzsbsbGxSExMxOjRo2G1WvHEE09g2bJl+Pzzz73trV27ttEYnE4npk+fjvT0dGg0GnTv3h0LFizw7l+0aJG3FyYzMxN33303LBaLd//SpUthNBrx1VdfoV+/ftBqtZg4cSJsNhuWLVuGrKwsJCQk4N5774XHc+aBZ1ZWFp566ilMmTIFcXFxyMjIwJIlSxqN9dixY5g0aRKMRiNMJhOuueYa5Ofnh/S9fuGFFzB27Fj87W9/w4ABA/DUU09hyJAheOmllwAABoMBq1atwqRJk9CvXz9ccMEFeOmll7Bt2zYUFBSEdI3mUrZq60TU6QlKBec4ERFRg5xwowyW4AeGUTJ0UDXhNviFF17A/v37MWjQIDz55JMAgJiYGAwfPhy33347nnvuOdTW1uLvf/87Jk2ahB9++AFFRUWYMmUKnn32WVx77bWoqanBhg0bIMsyHnjgAezZswdmsxlvvfUWAMBkMjUaw4svvogvvvgCH330Ebp164Zjx47h2LFj3v2iKOLFF19Ejx49cPjwYdx999148MEH8fLLL3uPsdlsePHFF/HBBx+gpqYG1113Ha699loYjUasXLkShw8fxvXXX4+RI0di8uTJ3vP+8Y9/4OGHH8bcuXPx3XffYcaMGejbty+uuOIKvzhdLhfGjBmDESNGYMOGDVAqlZg3bx7Gjh2LXbt2QaVSNfo+N2/ejFmzZvlsGzNmTKNzqKqrqyEIAoxGY6NttxQTJyJqVYJCAdnNoXpERBS9DAYDVCoVtFot0tLSAADz5s1Dbm4u5s+f7z3uzTffRGZmJvbv3w+LxQK3243rrrsO3bt3BwBkZ2d7j42NjYXD4fC2F0xBQQH69OmDiy66CIIgeNs8rf78q6ysLMybNw933nmnT+LkcrnwyiuvoFevXgCAiRMn4p133kFJSQl0Oh0GDhyIyy67DGvWrPFJnEaOHImHHnoIANC3b19s3LgRzz33XMDE6cMPP4QkSfj3v/8NQRAAAG+99RaMRiPWrl2LK6+8stH3WVxcjNTUVJ9tqampKC4uDni83W7H3//+d0yZMgV6vb7RtluKiRMRtSqFSgWPwxHpMIiIqJ1SQYkMGCMdRpPt3LkTa9asgU6n89t36NAhXHnllbj88suRnZ2NMWPG4Morr8TEiRORkJDQrOtNnToVV1xxBfr164exY8fi6quv9klCvv/+eyxYsAB79+6F2WyG2+2G3W6HzWaDVqsFAGi1Wm/SBNQlJFlZWT7vITU1FaWlpT7XHjFihN/r559/PmCcO3fuxMGDBxEfH++z3W6349ChQ8167w1xuVyYNGkSZFnGK6+8Eta2A2HiREStSlCKkK3scSIioo7FYrFg/PjxeOaZZ/z2paenQ6FQYNWqVdi0aRP++9//YvHixXjkkUewZcsW9OjRo8nXGzJkCI4cOYJvvvkG33//PSZNmoTRo0dj+fLlyM/Px9VXX4277roLTz/9NEwmE3788UdMmzYNTqfTmzjFxMT4tCkIQsBtkiQ1Ob7TLBYLhg4dinfffddvX3JyctDz09LSUFJS4rOtpKTEr2fudNJ09OhR/PDDD63e2wQwcSKiViYolJA9nONERETRTaVS+RRNGDJkCD755BNkZWVBqQx8Sy0IAkaOHImRI0dizpw56N69Oz777DPMmjXLr71Q6PV6TJ48GZMnT8bEiRMxduxYVFRUYNu2bZAkCQsXLoQo1tV+++ijj5r/Zs/y008/+b0eMGBAwGOHDBmCDz/8ECkpKc1KZkaMGIHVq1f7DD1ctWqVT6/X6aTpwIEDWLNmDRITE5t8neZgVT0ialWiUsk5TkREFPWysrKwZcsW5Ofno7y8HPfccw8qKiowZcoU/PLLLzh06BC+++473HbbbfB4PNiyZQvmz5+PrVu3oqCgAJ9++inKysq8CUdWVhZ27dqFffv2oby8HK4gFWgXLVqE999/H3v37sX+/fvx8ccfIy0tDUajEb1794bL5cLixYtx+PBhvPPOO3j11VfD9t43btyIZ599Fvv378eSJUvw8ccfY8aMGQGPvemmm5CUlIRrrrkGGzZswJEjR7B27Vrce++9KCwsDHqtGTNm4Ntvv8XChQuxd+9ePPHEE9i6dSumT58OoC5pmjhxIrZu3Yp3330XHo8HxcXFKC4uhtPpDNt7DoSJExG1KoVKCZnlyImIKMo98MADUCgUGDhwIJKTk+F0OrFx40Z4PB5ceeWVyM7OxsyZM2E0GiGKIvR6PdavX48//vGP6Nu3Lx599FEsXLgQ48aNAwDccccd6NevH4YNG4bk5GRs3Lix0evHx8fj2WefxbBhw3DeeechPz8fK1euhCiKyMnJwaJFi/DMM89g0KBBePfdd31KlbfU/fffj61btyI3Nxfz5s3DokWLMGbMmIDHarVarF+/Ht26dcN1112HAQMGYNq0abDb7SH1QF144YV477338NprryEnJwfLly/HihUrMGjQIADA8ePH8cUXX6CwsBCDBw9Genq692vTpk1he8+BCLIsy616hXbGbDbDYDCgurq6TcZCEnVm22c+ieTrrkVV3m/IvvdPkQ6HiIgizG6348iRI+jRowc0Gk2kw6EQZGVlYebMmT5D56JNY793TckN2ONERK1KjOEcJyIiIop+TJyIqFWJSiXgbn51HiIios5g/vz50Ol0Ab9OD+/rCBp6jzqdDhs2bIh0eI1iVT0ialWiWgnZwzlOREREjbnzzjsxadKkgPtiY2PbOJoz8vPzw9peXl5eg/syMjLCeq1wY+JERK1KoVRCbmK5VSIios7GZDLBZDJFOoxW17t370iH0GwcqkdErUpUKQEmTkRERBTlmDgRUatSqGIAN4fqERERUXSLaOK0fv16jB8/Hl26dIEgCFixYkXI527cuBFKpRKDBw9utfiIqOUUKg7VIyIiougX0cTJarUiJycHS5YsadJ5VVVVuOWWW3D55Ze3UmREFC4cqkdEREQdQUSLQ4wbN65Z5RXvvPNO3HjjjVAoFE3qpSKitqdk4kREREQdQNTNcXrrrbdw+PBhPP7445EOhYhCoNTEsBw5ERFRB9bUKTfRKqoSpwMHDuChhx7Cf/7zHyiVoXWWORwOmM1mny8iajuiUgHIcqTDICIiapFRo0Zh5syZYWtv6tSpmDBhQtja60g+/vhj9O/fHxqNBtnZ2Vi5cqXP/ieeeAL9+/dHXFwcEhISMHr0aGzZsqXV44qaxMnj8eDGG2/E3Llz0bdv35DPW7BgAQwGg/crMzOzFaMkooCYOBEREVEINm3ahClTpmDatGnYsWMHJkyYgAkTJmD37t3eY/r27YuXXnoJv/76K3788UdkZWXhyiuvRFlZWavGFjWJU01NDbZu3Yrp06dDqVRCqVTiySefxM6dO6FUKvHDDz8EPG/27Nmorq72fh07dqyNIyciIiKiaDZ16lSsW7cOL7zwAgRBgCAIyM/Px+7duzFu3DjodDqkpqbi5ptvRnl5ufe85cuXIzs7G7GxsUhMTMTo0aNhtVrxxBNPYNmyZfj888+97a1du7bRGJxOJ6ZPn4709HRoNBp0794dCxYs8O5ftGgRsrOzERcXh8zMTNx9992wWCze/UuXLoXRaMRXX32Ffv36QavVYuLEibDZbFi2bBmysrKQkJCAe++9F556c5OzsrLw1FNPYcqUKYiLi0NGRkbQwm7Hjh3DpEmTYDQaYTKZcM011yA/Pz+k7/ULL7yAsWPH4m9/+xsGDBiAp556CkOGDMFLL73kPebGG2/E6NGj0bNnT5xzzjlYtGgRzGYzdu3aFdI1mitqEie9Xo9ff/0VeXl53q8777wT/fr1Q15eHs4///yA56nVauj1ep8vIiIiIqJQvfDCCxgxYgTuuOMOFBUVoaioCPHx8fjDH/6A3NxcbN26Fd9++y1KSkowadIkAEBRURGmTJmCP//5z9izZw/Wrl2L6667DrIs44EHHsCkSZMwduxYb3sXXnhhozG8+OKL+OKLL/DRRx9h3759ePfdd5GVleXdL4oiXnzxRfz2229YtmwZfvjhBzz44IM+bdhsNrz44ov44IMP8O2332Lt2rW49tprsXLlSqxcuRLvvPMO/vWvf2H58uU+5/3jH/9ATk4OduzYgYceeggzZszAqlWrAsbpcrkwZswYxMfHY8OGDdi4cSN0Oh3Gjh0Lp9MZ9Hu9efNmjB492mfbmDFjsHnz5oDHO51OvPbaazAYDMjJyQnafktEtKqexWLBwYMHva+PHDmCvLw8mEwmdOvWDbNnz8bx48fx9ttvQxRFDBo0yOf8lJQUaDQav+1E1M4IQqQjICKi9spqBX7/vW2vOXAgEBcX8uEGgwEqlQparRZpaWkAgHnz5iE3Nxfz58/3Hvfmm28iMzMT+/fvh8VigdvtxnXXXYfu3bsDALKzs73HxsbGwuFweNsLpqCgAH369MFFF10EQRC8bZ5Wf/5VVlYW5s2bhzvvvBMvv/yyd7vL5cIrr7yCXr16AQAmTpyId955ByUlJdDpdBg4cCAuu+wyrFmzBpMnT/aeN3LkSDz00EMA6obJbdy4Ec899xyuuOIKvzg//PBDSJKEf//73xBO/fv/1ltvwWg0Yu3atbjyyisbfZ/FxcVITU312Zaamori4mKfbV999RX+9Kc/wWazIT09HatWrUJSUlKjbbdURHuctm7ditzcXOTm5gIAZs2ahdzcXMyZMwdAXaZeUFAQyRCJKBw4x4mIiDqYnTt3Ys2aNdDpdN6v/v37AwAOHTqEnJwcXH755cjOzsYNN9yA119/HZWVlc2+3tSpU5GXl4d+/frh3nvvxX//+1+f/d9//z0uv/xyZGRkID4+HjfffDNOnjwJm83mPUar1XqTJqAuIcnKyoJOp/PZVlpa6tP2iBEj/F7v2bMnYJw7d+7EwYMHER8f7/2+mEwm2O12HDp0qNnv/2yXXXYZ8vLysGnTJowdOxaTJk3yizvcItrjNGrUKMiN3FAtXbq00fOfeOIJPPHEE+ENioiIiIjaTlwccN55kY6iySwWC8aPH49nnnnGb196ejoUCgVWrVqFTZs24b///S8WL16MRx55BFu2bEGPHj2afL0hQ4bgyJEj+Oabb/D9999j0qRJGD16NJYvX478/HxcffXVuOuuu/D000/DZDLhxx9/xLRp0+B0OqHVagEAMTExPm0KghBwmyRJTY7vNIvFgqFDh+Ldd9/125ecnBz0/LS0NJSUlPhsKykp8euZi4uLQ+/evdG7d29ccMEF6NOnD9544w3Mnj272bEHE9HEiYiIiIgoGqhUKp+iCUOGDMEnn3yCrKysBpfJEQQBI0eOxMiRIzFnzhx0794dn332GWbNmuXXXij0ej0mT56MyZMnY+LEiRg7diwqKiqwbds2SJKEhQsXQhTrBpR99NFHzX+zZ/npp5/8Xg8YMCDgsUOGDMGHH36IlJSUZtUWGDFiBFavXu0z9HDVqlV+vV5nkyQJDoejyddriqgpDkFEUYxznIiIKMplZWVhy5YtyM/PR3l5Oe655x5UVFRgypQp+OWXX3Do0CF89913uO222+DxeLBlyxbMnz8fW7duRUFBAT799FOUlZV5E46srCzs2rUL+/btQ3l5OVyuxheLX7RoEd5//33s3bsX+/fvx8cff4y0tDQYjUb07t0bLpcLixcvxuHDh/HOO+/g1VdfDdt737hxI5599lns378fS5Yswccff4wZM2YEPPamm25CUlISrrnmGmzYsAFHjhzB2rVrce+996KwsDDotWbMmIFvv/0WCxcuxN69e/HEE094K2sDgNVqxcMPP4yffvoJR48exbZt2/DnP/8Zx48fxw033BC29xwIEycian2c40RERFHugQcegEKhwMCBA5GcnAyn04mNGzfC4/HgyiuvRHZ2NmbOnAmj0QhRFKHX67F+/Xr88Y9/RN++ffHoo49i4cKFGDduHADgjjvuQL9+/TBs2DAkJydj48aNjV4/Pj4ezz77LIYNG4bzzjsP+fn5WLlyJURRRE5ODhYtWoRnnnkGgwYNwrvvvutTqryl7r//fm9tgnnz5mHRokUYM2ZMwGO1Wi3Wr1+Pbt264brrrsOAAQMwbdo02O32kHqgLrzwQrz3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FSdOnMCGDRuwc+dOANZ/x+xpl8ceewyrVq3C6tWrcezYMcybNw8HDx5sFN/PP/+MwYMHO2zqeiIiJk5ERG1EoVBg6tSpWLx4MaqqqrBgwQLMmTMHCxcuRHx8PIYPH46vvvoKUVFRAICQkBDMnz8fzzzzDAIDAzF16lQAsHmePe6++24MHz4ct956K/z9/fHxxx83KuPi4oJvv/0W58+fx8CBA3HPPfdg2LBheOutt1rnB9KGJEnC119/jZtvvhkTJkxAjx49cN999+HUqVPNJhETJ07E119/jfLy8naMtl7DtODWhrE1mDNnDmbMmIG5c+ciPj4eo0ePNj9X1drtNWnSJKxcuRKrV69GQkIChgwZgjVr1ph/z5RKJb777jsEBATgT3/6ExISErBo0SLzUD5rv2P2tMvo0aMxZ84czJw5E/3798epU6fw6KOPNopv7dq1Fs9fERG1N0kIIRwdBBERkaPce++96NevH2bNmtWu1926dStGjRqFEydOwNvbu12vfb355ptvMGPGDBw4cAAKBZ8yICLHYI8TERF1aS+//DLc3Nza/bpff/01Zs+ezaTJDlVVVVi9ejWTJiJyKPY4ERERERER2cAeJyIiIiIiIhuYOBEREREREdnAxImIiIiIiMgGJk5EREREREQ2MHEiIiIiIiKygYkTERERERGRDUyciIiIiIiIbGDiREREREREZAMTJyIiIiIiIhuYOBEREREREdnw/wH8TnH28ZK/oAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot TICs (Total Ion Chromatograms) before and after alignment\n", + "# Enable inline plotting for Jupyter notebooks\n", + "%matplotlib inline\n", + "\n", + "lcms_collection.plot_tics(ms_level=1, type=\"both\", plot_legend=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39a6f24b", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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vvtOjRo1spnfrl07OTk52Zw/+/fv17Fjxzh/THC941OcnTt3ShLnTwUoLCzU5cuXOW9uUlePT3HK+rzh1sRKYtSoUerfv7/at2+vDh066I033tDFixc1cODAii6tyouLi9MjjzyigIAAnTx5UhMnTpSjo6NiYmIqurQqJzs72+b/YqWmpmrnzp3y8fFRw4YNNXLkSE2ePFlNmzZVo0aN9NJLL6l+/fqKjIysuKKrkJKOj4+Pj1555RX16tVLfn5+Onz4sF544QU1adJE4eHhFVh11TBkyBAtXrxYX375pTw8PKzPr3h5ecnV1VVeXl6KjY3VqFGj5OPjI09PTw0bNkyhoaG6++67K7j6yu96x+fw4cNavHixevTooVq1amn37t167rnn1KVLF7Vq1aqCq6/cxo0bp+7du6thw4a6cOGCFi9erA0bNmjt2rWcNzeBko6PKedNuY3HCNO9+eabRsOGDQ1nZ2ejQ4cOxtatWyu6JBiGER0dbdSrV89wdnY2GjRoYERHRxuHDh2q6LKqpPj4eENSkal///6GYfw2hP1LL71k+Pr6GtWrVze6du1q7N+/v2KLrkJKOj45OTlGt27djDp16hhOTk5GQECAMWjQICM9Pb2iy64Sijsukoz58+db+1y6dMl49tlnjZo1axpubm7GY489Zpw6dariiq5Crnd8jh07ZnTp0sXw8fExqlevbjRp0sR4/vnnjczMzIotvAr429/+ZgQEBBjOzs5GnTp1jK5duxrffPONdT7nTcUq6fiYcd5YDMMwyibSAQAAAADswTNiAAAAAGAyghgAAAAAmIwgBgAAAAAmI4gBAAAAgMkIYgAAAABgMoIYAAAAAJiMIAYAAAAAJiOIAUAltmHDBlksFp0/f16StGDBAnl7e1doTVfdTLWUpZtlv+677z6NHDmywrbfpUsXLV68+E+t4+WXX9add95ZqmXuvvtuff75539quwBgBoIYANzikpKS5OjoqIceeui6faOjo3XgwAETqiobFovFOnl6euquu+7Sl19+Wap1DBgwQJGRkeVSX2BgoN544w2btvL+jtPS0my+l+KmBQsWaMWKFZo0aVK51VGSr776ShkZGXriiSf+1Hri4uK0fv36Ui0zfvx4jR07VoWFhX9q2wBQ3ghiAHCL+/DDDzVs2DBt2rRJJ0+eLLGvq6ur6tata1JlZWP+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Yvu0aNFCycnJNm1bt24t1Xak367WtWvXTq+99ppmz56ttm3baunSpapbt648PT2LXcbZ2VkFBQU2bfYsZ4/i1v1HLVq0UGJiovr3729tS0xMVHBw8A1v1yxt27bVTz/9pCZNmti9TJs2bZSenq5ff/1VNWvWLMfqrm3Pnj1q06ZNhWwbAOzFrYkAcAtatWqVfv31V8XGxqply5Y2U69evey+PXHYsGH68MMP9fHHH+vgwYOaPHmydu/ebb0yYY9mzZqpb9++6tevn1asWKHU1FT997//1dSpU/W///u/kqThw4drzZo1+te//qWDBw/qrbfe0po1a25o30eOHKl3331XJ06cUN++fVW7dm317NlTCQkJSk1N1YYNGzR8+HD9/PPPkn4bLXD37t3av3+/zp49q/z8fLuWs0dgYKA2bdqkEydO6OzZs8X2ef7557VgwQK98847OnjwoGbOnKkVK1YoLi7uhvbfTGPGjNGWLVusg6wcPHhQX375ZYkDarRp00a1a9dWYmKiiZXaSkhIKPI/KADgZkMQA4Bb0IcffqiwsLBibz/s1auXtm/frt27d193PX379tW4ceMUFxentm3bKjU1VQMGDLjmMz7XMn/+fPXr10+jR49WUFCQIiMjtW3bNuvta3fffbfef/99zZ49W61bt9Y333yj8ePHl2obV0VERKhRo0Z67bXX5Obmpk2bNqlhw4aKiopSixYtFBsbq9zcXOuVrkGDBikoKEjt27dXnTp1lJiYaNdy9nj11VeVlpamxo0bX/NWyMjISM2ePVv/+te/dMcdd+jdd9/V/Pnzdd99993Q/pupVatW2rhxow4cOKC//OUvatOmjSZMmGBzO+wfOTo6auDAgcXe9mmGEydOaMuWLRo4cGCFbB8A7GUxjFKMcwwAqPQefPBB+fn5FXnXFWCv9PR03XHHHdqxY4cCAgJM3faYMWP066+/6r333jN1uwBQWjwjBgBVWE5OjubNm6fw8HA5Ojrq008/1bfffqt169ZVdGm4hfn5+enDDz/UsWPHTA9idevW1ahRo0zdJgDcCK6IAUAVdunSJT3yyCP6/vvvlZubq6CgII0fP15RUVEVXRoAAJUaQQwAAAAATMZgHQAAAABgMoIYAAAAAJiMIAYAAAAAJiOIAQAAAIDJCGIAAAAAYDKCGAAAAACYjCAGAAAAACYjiAEAAACAyQhiAAAAAGCy/wdqzR9y56PLYgAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generated alignment plot showing RT differences (no differences for our exampl, which is expected)\n" + ] + } + ], + "source": [ + "# Plot alignment differences (no differences for this example, no alignment was used)\n", + "lcms_collection.plot_alignments(plot_legend=True)\n", + "print(\"Generated alignment plot showing RT differences (no differences for our exampl, which is expected)\")" + ] + }, + { + "cell_type": "markdown", + "id": "dcafc268", + "metadata": {}, + "source": [ + "## Step 4: Generate Consensus Mass Features (Clustering)\n", + "\n", + "Cluster mass features across samples to identify consensus features - features representing the same chemical entity across multiple samples." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "670d8d60", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generated 50 consensus clusters\n", + "\n", + "Cluster Summary (first 10 clusters):\n" + ] + }, + { + "data": { + "application/vnd.microsoft.datawrangler.viewer.v0+json": { + "columns": [ + { + "name": "cluster", + "rawType": "int64", + "type": "integer" + }, + { + "name": "mz_median", + "rawType": "float64", + "type": "float" + }, + { + "name": "mz_mean", + "rawType": "float64", + "type": "float" + }, + { + "name": "mz_std", + "rawType": "float64", + "type": "float" + }, + { + "name": "mz_max", + "rawType": "float64", + "type": "float" + }, + { + "name": "mz_min", + "rawType": "float64", + "type": "float" + }, + { + "name": "scan_time_aligned_median", + "rawType": "float64", + "type": "float" + }, + { + "name": "scan_time_aligned_mean", + "rawType": "float64", + "type": "float" + }, + { + "name": "scan_time_aligned_std", + "rawType": "float64", + "type": "float" + }, + { + "name": "scan_time_aligned_max", + "rawType": "float64", + "type": "float" + }, + { + "name": "scan_time_aligned_min", + "rawType": "float64", + "type": "float" + }, + { + "name": "sample_id_nunique", + "rawType": "int64", + "type": "integer" + }, + { + "name": "intensity_max", + "rawType": "float64", + "type": "float" + }, + { + "name": "intensity_median", + "rawType": "float64", + "type": "float" + }, + { + "name": "intensity_mean", + "rawType": "float64", + "type": "float" + }, + { + "name": "intensity_std", + "rawType": "float64", + "type": "float" + }, + { + "name": "intensity_min", + "rawType": "float64", + "type": "float" + }, + { + "name": "tailing_factor_median", + "rawType": "float64", + "type": "float" + }, + { + "name": "tailing_factor_mean", + "rawType": "float64", + "type": "float" + }, + { + "name": "tailing_factor_std", + "rawType": "float64", + "type": "float" + }, + { + "name": "tailing_factor_max", + "rawType": "float64", + "type": "float" + }, + { + "name": "tailing_factor_min", + "rawType": "float64", + "type": "float" + }, + { + "name": "dispersity_index_median", + "rawType": "float64", + "type": "float" + }, + { + "name": "dispersity_index_mean", + "rawType": "float64", + "type": "float" + }, + { + "name": "dispersity_index_std", + "rawType": "float64", + "type": "float" + }, + { + "name": "dispersity_index_max", + "rawType": "float64", + "type": "float" + }, + { + "name": "dispersity_index_min", + "rawType": "float64", + "type": "float" + }, + { + "name": "persistence_max", + "rawType": "float64", + "type": "float" + }, + { + "name": "persistence_median", + "rawType": "float64", + "type": "float" + }, + { + "name": "persistence_mean", + "rawType": "float64", + "type": "float" + }, + { + "name": "persistence_std", + "rawType": "float64", + "type": "float" + }, + { + "name": "persistence_min", + "rawType": "float64", + "type": "float" + } + ], + "ref": "edb5bbdb-057b-4453-988d-ae9db765fef3", + "rows": [ + [ + "0", + "301.21661376953125", + "301.21661376953125", + "0.0", + "301.21661376953125", + "301.21661376953125", + "8.895636666666666", + "8.895636666666666", + "0.0", + "8.895636666666666", + "8.895636666666666", + "2", + "66775328.0", + "66775328.0", + "66775328.0", + "0.0", + "66775328.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + "66708546.0", + "66708546.0", + "66708546.0", + "0.0", + "66708546.0" + ], + [ + "1", + "302.2206115722656", + "302.2206115722656", + "0.0", + "302.2206115722656", + "302.2206115722656", + "8.895636666666666", + "8.895636666666666", + "0.0", + "8.895636666666666", + "8.895636666666666", + "2", + "14249711.0", + "14249711.0", + "14249711.0", + "0.0", + "14249711.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + "14142901.0", + "14142901.0", + "14142901.0", + "0.0", + "14142901.0" + ], + [ + "2", + "367.35748291015625", + "367.35748291015625", + "0.0", + "367.35748291015625", + "367.35748291015625", + "19.152648333333335", + "19.152648333333335", + "0.0", + "19.152648333333335", + "19.152648333333335", + "2", + "48137056.0", + "48137056.0", + "48137056.0", + "0.0", + "48137056.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + "48070260.0", + "48070260.0", + "48070260.0", + "0.0", + "48070260.0" + ], + [ + "3", + "368.3611755371094", + "368.3611755371094", + "0.0", + "368.3611755371094", + "368.3611755371094", + "19.152648333333335", + "19.152648333333335", + "0.0", + "19.152648333333335", + "19.152648333333335", + "2", + "12717404.0", + "12717404.0", + "12717404.0", + "0.0", + "12717404.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + "12650608.0", + "12650608.0", + "12650608.0", + "0.0", + "12650608.0" + ], + [ + "4", + "698.62890625", + "698.62890625", + "0.0", + "698.62890625", + "698.62890625", + "23.816803333333333", + "23.816803333333333", + "0.0", + "23.816803333333333", + "23.816803333333333", + "2", + "17265106.0", + "17265106.0", + "17265106.0", + "0.0", + "17265106.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + "17198326.0", + "17198326.0", + "17198326.0", + "0.0", + "17198326.0" + ], + [ + "6", + "699.6312255859375", + "699.6312255859375", + "0.0", + "699.6312255859375", + "699.6312255859375", + "23.816803333333333", + "23.816803333333333", + "0.0", + "23.816803333333333", + "23.816803333333333", + "2", + "7861987.5", + "7861987.5", + "7861987.5", + "0.0", + "7861987.5", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + "7795191.0", + "7795191.0", + "7795191.0", + "0.0", + "7795191.0" + ], + [ + "9", + "227.20188903808594", + "227.20188903808594", + "0.0", + "227.20188903808594", + "227.20188903808594", + "7.376469999999999", + "7.376469999999999", + "0.0", + "7.376469999999999", + "7.376469999999999", + "2", + "9600092.0", + "9600092.0", + "9600092.0", + "0.0", + "9600092.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + "9533297.0", + "9533297.0", + "9533297.0", + "0.0", + "9533297.0" + ], + [ + "10", + "299.20111083984375", + "299.20111083984375", + "0.0", + "299.20111083984375", + "299.20111083984375", + "7.376469999999999", + "7.376469999999999", + "0.0", + "7.376469999999999", + "7.376469999999999", + "2", + "18258992.0", + "18258992.0", + "18258992.0", + "0.0", + "18258992.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + "18192197.0", + "18192197.0", + "18192197.0", + "0.0", + "18192197.0" + ], + [ + "12", + "455.3526611328125", + "455.3526611328125", + "0.0", + "455.3526611328125", + "455.3526611328125", + "8.96547", + "8.96547", + "0.0", + "8.96547", + "8.96547", + "2", + "12939420.0", + "12939420.0", + "12939420.0", + "0.0", + "12939420.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + "12872638.0", + "12872638.0", + "12872638.0", + "0.0", + "12872638.0" + ], + [ + "15", + "735.5070190429688", + "735.5070190429688", + "0.0", + "735.5070190429688", + "735.5070190429688", + "20.793636666666668", + "20.793636666666668", + "0.0", + "20.793636666666668", + "20.793636666666668", + "2", + "8329064.0", + "8329064.0", + "8329064.0", + "0.0", + "8329064.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + "8262284.0", + "8262284.0", + "8262284.0", + "0.0", + "8262284.0" + ] + ], + "shape": { + "columns": 31, + "rows": 10 + } + }, + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
mz_medianmz_meanmz_stdmz_maxmz_minscan_time_aligned_medianscan_time_aligned_meanscan_time_aligned_stdscan_time_aligned_maxscan_time_aligned_min...dispersity_index_mediandispersity_index_meandispersity_index_stddispersity_index_maxdispersity_index_minpersistence_maxpersistence_medianpersistence_meanpersistence_stdpersistence_min
cluster
0301.216614301.2166140.0301.216614301.2166148.8956378.8956370.08.8956378.895637...NaNNaNNaNNaNNaN66708546.066708546.066708546.00.066708546.0
1302.220612302.2206120.0302.220612302.2206128.8956378.8956370.08.8956378.895637...NaNNaNNaNNaNNaN14142901.014142901.014142901.00.014142901.0
2367.357483367.3574830.0367.357483367.35748319.15264819.1526480.019.15264819.152648...NaNNaNNaNNaNNaN48070260.048070260.048070260.00.048070260.0
3368.361176368.3611760.0368.361176368.36117619.15264819.1526480.019.15264819.152648...NaNNaNNaNNaNNaN12650608.012650608.012650608.00.012650608.0
4698.628906698.6289060.0698.628906698.62890623.81680323.8168030.023.81680323.816803...NaNNaNNaNNaNNaN17198326.017198326.017198326.00.017198326.0
6699.631226699.6312260.0699.631226699.63122623.81680323.8168030.023.81680323.816803...NaNNaNNaNNaNNaN7795191.07795191.07795191.00.07795191.0
9227.201889227.2018890.0227.201889227.2018897.3764707.3764700.07.3764707.376470...NaNNaNNaNNaNNaN9533297.09533297.09533297.00.09533297.0
10299.201111299.2011110.0299.201111299.2011117.3764707.3764700.07.3764707.376470...NaNNaNNaNNaNNaN18192197.018192197.018192197.00.018192197.0
12455.352661455.3526610.0455.352661455.3526618.9654708.9654700.08.9654708.965470...NaNNaNNaNNaNNaN12872638.012872638.012872638.00.012872638.0
15735.507019735.5070190.0735.507019735.50701920.79363720.7936370.020.79363720.793637...NaNNaNNaNNaNNaN8262284.08262284.08262284.00.08262284.0
\n", + "

10 rows × 31 columns

\n", + "
" + ], + "text/plain": [ + " mz_median mz_mean mz_std mz_max mz_min \\\n", + "cluster \n", + "0 301.216614 301.216614 0.0 301.216614 301.216614 \n", + "1 302.220612 302.220612 0.0 302.220612 302.220612 \n", + "2 367.357483 367.357483 0.0 367.357483 367.357483 \n", + "3 368.361176 368.361176 0.0 368.361176 368.361176 \n", + "4 698.628906 698.628906 0.0 698.628906 698.628906 \n", + "6 699.631226 699.631226 0.0 699.631226 699.631226 \n", + "9 227.201889 227.201889 0.0 227.201889 227.201889 \n", + "10 299.201111 299.201111 0.0 299.201111 299.201111 \n", + "12 455.352661 455.352661 0.0 455.352661 455.352661 \n", + "15 735.507019 735.507019 0.0 735.507019 735.507019 \n", + "\n", + " scan_time_aligned_median scan_time_aligned_mean \\\n", + "cluster \n", + "0 8.895637 8.895637 \n", + "1 8.895637 8.895637 \n", + "2 19.152648 19.152648 \n", + "3 19.152648 19.152648 \n", + "4 23.816803 23.816803 \n", + "6 23.816803 23.816803 \n", + "9 7.376470 7.376470 \n", + "10 7.376470 7.376470 \n", + "12 8.965470 8.965470 \n", + "15 20.793637 20.793637 \n", + "\n", + " scan_time_aligned_std scan_time_aligned_max scan_time_aligned_min \\\n", + "cluster \n", + "0 0.0 8.895637 8.895637 \n", + "1 0.0 8.895637 8.895637 \n", + "2 0.0 19.152648 19.152648 \n", + "3 0.0 19.152648 19.152648 \n", + "4 0.0 23.816803 23.816803 \n", + "6 0.0 23.816803 23.816803 \n", + "9 0.0 7.376470 7.376470 \n", + "10 0.0 7.376470 7.376470 \n", + "12 0.0 8.965470 8.965470 \n", + "15 0.0 20.793637 20.793637 \n", + "\n", + " ... dispersity_index_median dispersity_index_mean \\\n", + "cluster ... \n", + "0 ... NaN NaN \n", + "1 ... NaN NaN \n", + "2 ... NaN NaN \n", + "3 ... NaN NaN \n", + "4 ... NaN NaN \n", + "6 ... NaN NaN \n", + "9 ... NaN NaN \n", + "10 ... NaN NaN \n", + "12 ... NaN NaN \n", + "15 ... NaN NaN \n", + "\n", + " dispersity_index_std dispersity_index_max dispersity_index_min \\\n", + "cluster \n", + "0 NaN NaN NaN \n", + "1 NaN NaN NaN \n", + "2 NaN NaN NaN \n", + "3 NaN NaN NaN \n", + "4 NaN NaN NaN \n", + "6 NaN NaN NaN \n", + "9 NaN NaN NaN \n", + "10 NaN NaN NaN \n", + "12 NaN NaN NaN \n", + "15 NaN NaN NaN \n", + "\n", + " persistence_max persistence_median persistence_mean \\\n", + "cluster \n", + "0 66708546.0 66708546.0 66708546.0 \n", + "1 14142901.0 14142901.0 14142901.0 \n", + "2 48070260.0 48070260.0 48070260.0 \n", + "3 12650608.0 12650608.0 12650608.0 \n", + "4 17198326.0 17198326.0 17198326.0 \n", + "6 7795191.0 7795191.0 7795191.0 \n", + "9 9533297.0 9533297.0 9533297.0 \n", + "10 18192197.0 18192197.0 18192197.0 \n", + "12 12872638.0 12872638.0 12872638.0 \n", + "15 8262284.0 8262284.0 8262284.0 \n", + "\n", + " persistence_std persistence_min \n", + "cluster \n", + "0 0.0 66708546.0 \n", + "1 0.0 14142901.0 \n", + "2 0.0 48070260.0 \n", + "3 0.0 12650608.0 \n", + "4 0.0 17198326.0 \n", + "6 0.0 7795191.0 \n", + "9 0.0 9533297.0 \n", + "10 0.0 18192197.0 \n", + "12 0.0 12872638.0 \n", + "15 0.0 8262284.0 \n", + "\n", + "[10 rows x 31 columns]" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Generate consensus features\n", + "lcms_collection.add_consensus_mass_features()\n", + "\n", + "cluster_summary = lcms_collection.cluster_summary_dataframe\n", + "print(f\"Generated {len(cluster_summary)} consensus clusters\")\n", + "\n", + "# View cluster summary\n", + "print(\"\\nCluster Summary (first 10 clusters):\")\n", + "display(cluster_summary.head(10))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a8a9d7d8", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAmkAAAHsCAYAAACJ5DokAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/OQEPoAAAACXBIWXMAAA9hAAAPYQGoP6dpAABSZklEQVR4nO3deVxWdf7//+clsq9iLKKguJQiakWp5NaCYmll4polmktjWONWZr/c2mhsUrPJbJlRZ9IWLa0sLbO0RbI0LcVERBQTATdAJfbz+8OP17crlxSBc8DH/Xa7bsN1zvuc9+ucuWbO0/fZbIZhGAIAAICl1DG7AAAAAJyNkAYAAGBBhDQAAAALIqQBAABYECENAADAgghpAAAAFkRIAwAAsCBCGgAAgAUR0gAAACyIkAYAAGBBhDTgMqSlpenBBx9U06ZN5ebmJh8fH3Xq1EkvvfSSfv/9d7PLq5VmzJghm812zs+CBQuqpM9PP/1UM2bMqJJ1A8D51DW7AKCm+uSTT9S/f3+5urpq6NChioyMVHFxsb799ls9+uijSk5O1uuvv252mbXWq6++Ki8vL4dpHTp0qJK+Pv30U73yyisENQDVipAGVEB6eroGDRqkxo0b68svv1SDBg3s8xISErRnzx598sknJlZY+/Xr109XXXWV2WVcllOnTsnT09PsMgBYFKc7gQqYNWuWTp48qX//+98OAe2M5s2b6+9//7v9e2lpqZ5++mk1a9ZMrq6uatKkiZ544gkVFRU5LNekSRP17t1b3377rdq3by83Nzc1bdpU//3vfx3alZSUaObMmWrRooXc3NxUv359de7cWWvXrnVot2vXLvXr10/+/v5yc3PTDTfcoI8++sihzaJFi2Sz2fTdd99pwoQJCggIkKenp+655x4dPnzYoe3mzZsVGxurq666Su7u7goPD9cDDzxgn79+/XrZbDatX7/eYbl9+/bJZrNp0aJF9mlZWVkaPny4GjVqJFdXVzVo0EB333239u3bd979fineeustRUVFyd3dXf7+/ho0aJAOHDjg0Oabb75R//79FRYWJldXV4WGhmr8+PEOp6qHDRumV155RZIcTq1e6vYOGzZMXl5eSktL0x133CFvb28NGTJEklReXq65c+eqdevWcnNzU1BQkB588EEdP37cYb1/tf8B1C6MpAEV8PHHH6tp06a66aab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tYi8rsq76zPwfvWp8l7OYc86Hf3gUWX87qa01hPUr39/M6oAAPwF00fS/qy8vFxFRUWKioqSs7Oz1q1bZ5+XkpKijIwMRUdHS5Kio6O1fft25eTk2NusXbtWPj4+ioiIOG8frq6u9sd+nPkAktTr3tGaPOP52n0Rf2GuSn7Pl7uKuBYOACzM1JG0KVOm6Pbbb1dYWJhOnDihpUuXav369frss8/k6+urESNGaMKECfL395ePj48efvhhRUdHq2PHjpKkHj16KCIiQvfff79mzZqlrKwsPfnkk0pISJCrq6uZmwZYV0QfpWzZpS37ctU6MtLsagAA52FqSMvJydHQoUN16NAh+fr6qm3btvrss8/UvXt3SdKcOXNUp04dxcXFqaioSLGxsZo/f759eScnJ61atUpjxoxRdHS0PD09FR8fr6eeesqsTQKsz6muug17Ut3MrgMAcEGWe06aGXhuDADgSsDxrmax3DVpAAAAIKQBOBfDkH79+PRrq0oKza4GAK5Ipj+CA8B5lBZLKZ9K3g2ksA7V23f+QR39aKrKfs/Xru371XUY78MFgOrGSBpwuQ5ukX7bXPnrTftSeR89of3/GaYP3/1v5a//QjwDlfq7n7JVXz/uO1G9fQMAJBHSgMtzJFVH3xqp394cos/emle56/YP15Giujqqevrl1z2Vu+6/UtdFv0WO1Qe23mocWc2jeAAASZzuBC6Pq7eO/14mm1yUvCdDsZW57oBr9HPEFG3fmWLK88z69etXux/qCwAWR0gDLod3sHa0mqRdv+5S88jrK331fQcMVt9KXysAoCYgpAGXqc/AoWaXAACohbgmDQAAwIIIaQAAABZESAMAALAgQhoAAIAFEdJqon3fSZsXSoV5jtNLi6StS6SU1adf6wMAAGos7u6saUqLlbNsvJxOZStt63a1HzX7/83b963y1jyj/KJy/XzNJPUePMq8OgEAwGVhJK2mcXLWzlP1dET1tOVgseO8gGt0sMhDhxSkrSkHzKkPAABUCkbSahqbTUciR2lDcrJat27tOM+3kXZGTlHyueYBAIAaxWYYXLyUn58vX19f5eXlycfHx+xycCXKPSCVl0r+4WZXAqAW43hXs3C6E/grRSelkzlVt/78TB1+va8y5t2hNUv+VXX9AABqFEIacCElhfrtlTuV8c9u+uJ//6yaPgxDBQWnJEl7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AFgQIQ0AAMCCCGkAAAAWREgDYGn79u2TzWazv7IGAK4UhDTgCnT48GGNGTNGYWFhcnV1VXBwsGJjY/Xdd9+ZWtewYcPOeoVVaGioDh06pMjISHOKAgCTXNHv7gSuVHFxcSouLtbixYvVtGlTZWdna926dTp69KjZpZ3FyclJwcHBZpcBANWOkTTgCpObm6tvvvlG//jHP3TLLbeocePGat++vaZMmaK77rrLod2DDz6ooKAgubm5KTIyUqtWrZIkHT16VIMHD1bDhg3l4eGhNm3a6O2333bo5+abb9Yjjzyixx57TP7+/goODtaMGTPOW9eMGTO0ePFiffjhh7LZbLLZbFq/fv1ZpzvXr18vm82mzz77TNddd53c3d116623KicnR6tXr1arVq3k4+Oje++9VwUFBfb1l5eXKzExUeHh4XJ3d1e7du20fPnyytuxAFDJGEkDrjBeXl7y8vLSypUr1bFjR7m6up7Vpry8XLfffrtOnDiht956S82aNdPOnTvl5OQkSSosLFRUVJQmT54sHx8fffLJJ7r//vvVrFkztW/f3r6exYsXa8KECdq0aZOSkpI0bNgwderUSd27dz+rz0mTJunXX39Vfn6+Fi5cKEny9/dXZmbmObdjxowZ+te//iUPDw8NGDBAAwYMkKurq5YuXaqTJ0/qnnvu0csvv6zJkydLkhITE/XWW29pwYIFatGihb7++mvdd999CggIULdu3S57vwJApTP7De8Aqt/y5cuNevXqGW5ubsZNN91kTJkyxfj555/t8z/77DOjTp06RkpKykWvs1evXsbEiRPt37t162Z07tzZoc2NN95oTJ48+bzriI+PN+6++26Haenp6YYkY+vWrYZhGMZXX31lSDK++OILe5vExERDkpGWlmaf9uCDDxqxsbGGYRhGYWGh4eHhYWzcuNFh3SNGjDAGDx580dsIANWJ053AFSguLk6ZmZn66KOP1LNnT61fv17XX3+9Fi1aJEnatm2bGjVqpKuvvvqcy5eVlenpp59WmzZt5O/vLy8vL3322WfKyMhwaNe2bVuH7w0aNFBOTk6lbMMf1x0UFCQPDw81bdrUYdqZvvbs2aOCggJ1797dPpLo5eWl//73v0pLS6uUegCgsnG6E7hCubm5qXv37urevbumTp2qkSNHavr06Ro2bJjc3d0vuOwLL7ygl156SXPnzlWbNm3k6empcePGqbi42KGds7Ozw3ebzaby8vJKqf+P67bZbBfs6+TJk5KkTz75RA0bNnRod67TvQBgBYQ0AJKkiIgIrVy5UtLpUarffvtNu3fvPudo2nfffae7775b9913n6TT17Dt3r1bERERl1WDi4uLysrKLmsd5xIRESFXV1dlZGRw/RmAGoOQBlxhjh49qv79++uBBx5Q27Zt5e3trc2bN2vWrFm6++67JUndunVT165dFRcXp9mzZ6t58+batWuXbDabevbsqRYtWmj58uXauHGj6tWrp9mzZys7O/uyQ1qTJk302WefKSUlRfXr15evr29lbLK8vb01adIkjR8/XuXl5ercubPy8vL03XffycfHR/Hx8ZXSDwBUJkIacIXx8vJShw4dNGfOHKWlpamkpEShoaEaNWqUnnjiCXu7999/X5MmTdLgwYN16tQpNW/eXM8//7wk6cknn9TevXsVGxsrDw8PjR49Wn369FFeXt5l1TZq1CitX79eN9xwg06ePKmvvvpKTZo0uax1nvH0008rICBAiYmJ2rt3r/z8/HT99dc7bDMAWInNMAzD7CIAAADgiLs7AQAALIiQBgAAYEGENAAAAAsipAEAAFgQIQ0AAMCCCGkAAAAWREgDAACwIEIaAACABRHSAAAALIiQBgAAYEGENAAAAAsipAEAAFjQ/w8O4CABjGkZlgAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# View an overview plot of the consensus features\n", + "lcms_collection.plot_consensus_mz_features()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ed583149", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Total clusters in dictionary: 50\n", + "\n", + "Example - Cluster 0 contains these features:\n", + " 0_0\n", + " 1_0\n", + "The features are tagged as sampleid_massfeatureid.\n", + "So for this example, the first cluster contains the first mass feature (_0) from the first (0_) and second (1_) samples\n" + ] + } + ], + "source": [ + "# View cluster feature dictionary (which features belong to which cluster)\n", + "cluster_dict = lcms_collection.cluster_feature_dictionary\n", + "print(f\"\\nTotal clusters in dictionary: {len(cluster_dict)}\")\n", + "print(\"\\nExample - Cluster 0 contains these features:\")\n", + "if 0 in cluster_dict:\n", + " for feature in cluster_dict[0][:5]: # Show first 5\n", + " print(f\" {feature}\")\n", + "print(\"The features are tagged as sampleid_massfeatureid.\")\n", + "print(\"So for this example, the first cluster contains the first mass feature (_0) from the first (0_) and second (1_) samples\")" + ] + }, + { + "cell_type": "markdown", + "id": "80fc47e9", + "metadata": {}, + "source": [ + "## Step 5: Process consensus mass features\n", + "\n", + "Now that we have the consensus mass features, we can process them. To do that we'll use the `process_consensus_features` step. In this step you'll trigger certain actions on each of the samples within your collection.\n", + "\n", + "For this example, we will perform gap filling. Later on we'll perform the annotations with molecular formula searching and MS2 spectra matching, though you can trigger all these steps at once. If your parameters on your collection are set for multi-core (by setting the `lcms_collection.parameters.lcms_collection.cores` parameter), this would perform these actions using multicore processing on a sample-by-sample basis. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f88226f6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Performing gap filling...\n", + "\n", + "Gap-filling:\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|█████████████████████████████████████████| 3/3 [00:19<00:00, 6.48s/sample]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Total induced features: 50\n", + " Sample 0: 0 gap-filled features\n", + " Sample 1: 0 gap-filled features\n", + " Sample 2: 50 gap-filled features\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "# Perform gap filling\n", + "print(\"Performing gap filling...\")\n", + "pipeline_results = lcms_collection.process_consensus_features(\n", + " load_representatives=False,\n", + " perform_gap_filling=True,\n", + " add_ms1=False,\n", + " add_ms2=False,\n", + " molecular_formula_search=False,\n", + " ms2_spectral_search=False,\n", + " spectral_lib=False,\n", + " molecular_metadata=None,\n", + " gather_eics=False,\n", + " keep_raw_data=False\n", + ")\n", + "\n", + "# Check induced (gap-filled) features\n", + "induced_df = lcms_collection.induced_mass_features_dataframe\n", + "print(f\"\\nTotal induced features: {len(induced_df)}\")\n", + "\n", + "# Check per sample\n", + "for sample_id in range(len(lcms_collection)):\n", + " sample_induced = len(induced_df[induced_df['sample_id'] == sample_id])\n", + " print(f\" Sample {sample_id}: {sample_induced} gap-filled features\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9638f884", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Induced Features (first 10):\n" + ] + }, + { + "data": { + "application/vnd.microsoft.datawrangler.viewer.v0+json": { + "columns": [ + { + "name": "coll_mf_id", + "rawType": "object", + "type": "string" + }, + { + "name": "sample_name", + "rawType": "object", + "type": "string" + }, + { + "name": "sample_id", + "rawType": "int64", + "type": "integer" + }, + { + "name": "mf_id", + "rawType": "object", + "type": "string" + }, + { + "name": "mz", + "rawType": "float32", + "type": "float" + }, + { + "name": "scan_time_aligned", + "rawType": "float64", + "type": "float" + }, + { + "name": "cluster", + "rawType": "int64", + "type": "integer" + }, + { + "name": "type", + "rawType": "object", + "type": "string" + }, + { + "name": "scan_time", + "rawType": "float64", + "type": "float" + }, + { + "name": "apex_scan", + "rawType": "float32", + "type": "float" + }, + { + "name": "start_scan", + "rawType": "int64", + "type": "integer" + }, + { + "name": "final_scan", + "rawType": "int64", + "type": "integer" + }, + { + "name": "intensity", + "rawType": "float32", + "type": "float" + }, + { + "name": "persistence", + "rawType": "float64", + "type": "float" + }, + { + "name": "area", + "rawType": "float64", + "type": "float" + }, + { + "name": "tailing_factor", + "rawType": "float64", + "type": "float" + }, + { + "name": "dispersity_index", + "rawType": "float64", + "type": "float" + }, + { + "name": "normalized_dispersity_index", + "rawType": "float64", + "type": "float" + }, + { + "name": "noise_score", + "rawType": "float64", + "type": "float" + }, + { + "name": "noise_score_min", + "rawType": "float64", + "type": "float" + }, + { + "name": "noise_score_max", + "rawType": "float64", + "type": "float" + }, + { + "name": "monoisotopic_mf_id", + "rawType": "object", + "type": "string" + }, + { + "name": "isotopologue_type", + "rawType": "object", + "type": "string" + }, + { + "name": "mass_spectrum_deconvoluted_parent", + "rawType": "object", + "type": "string" + }, + { + "name": "ms2_scan_numbers", + "rawType": "object", + "type": "string" + }, + { + "name": "_eic_mz", + "rawType": "float32", + "type": "float" + } + ], + "ref": "1e412366-73f0-45fd-ac2c-b599482380ce", + "rows": [ + [ + "2_c0_0_i", + "test_sample_03", + "2", + "c0_0_i", + "301.2166", + "8.895636666666666", + "0", + "untargeted", + "8.895636666666666", + "1882.0", + "1828", + "2008", + "66775330.0", + null, + "35045576.273014136", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]", + "301.2166" + ], + [ + "2_c1_1_i", + "test_sample_03", + "2", + "c1_1_i", + "302.2206", + "8.895636666666666", + "1", + "untargeted", + "8.895636666666666", + "1882.0", + "1828", + "2008", + "14249711.0", + null, + "7564256.872230931", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]", + "302.2206" + ], + [ + "2_c2_2_i", + "test_sample_03", + "2", + "c2_2_i", + "367.35748", + "19.152648333333335", + "2", + "untargeted", + "19.152648333333335", + "4069.0", + "4024", + "4312", + "48137056.0", + null, + "30641267.270276234", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]", + "367.35748" + ], + [ + "2_c3_3_i", + "test_sample_03", + "2", + "c3_3_i", + "368.36118", + "19.152648333333335", + "3", + "untargeted", + "19.152648333333335", + "4069.0", + "4024", + "4303", + "12717404.0", + null, + "8073609.919955936", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]", + "368.36118" + ], + [ + "2_c4_4_i", + "test_sample_03", + "2", + "c4_4_i", + "698.6289", + "23.816803333333333", + "4", + "untargeted", + "23.816803333333333", + "5212.0", + "5176", + "5338", + "17265106.0", + null, + "7113439.441603203", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]", + "698.6289" + ], + [ + "2_c6_5_i", + "test_sample_03", + "2", + "c6_5_i", + "699.6312", + "23.816803333333333", + "6", + "untargeted", + "23.816803333333333", + "5212.0", + "5176", + "5293", + "7861987.5", + null, + "3248197.977534156", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]", + "699.6312" + ], + [ + "2_c9_6_i", + "test_sample_03", + "2", + "c9_6_i", + "227.20189", + "7.376469999999999", + "9", + "untargeted", + "7.376469999999999", + "1513.0", + "1477", + "1657", + "9600092.0", + null, + "3792812.043434581", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]", + "227.20189" + ], + [ + "2_c10_7_i", + "test_sample_03", + "2", + "c10_7_i", + "299.2011", + "7.376469999999999", + "10", + "untargeted", + "7.376469999999999", + "1513.0", + "1477", + "1585", + "18258992.0", + null, + "7192845.185983743", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]", + "299.2011" + ], + [ + "2_c12_8_i", + "test_sample_03", + "2", + "c12_8_i", + "455.35266", + "8.96547", + "12", + "untargeted", + "8.96547", + "1900.0", + "1855", + "1999", + "12939420.0", + null, + "6434091.988552084", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]", + "455.35266" + ], + [ + "2_c15_9_i", + "test_sample_03", + "2", + "c15_9_i", + "735.507", + "20.793636666666668", + "15", + "untargeted", + "20.793636666666668", + "4483.0", + "4438", + "4600", + "8329064.0", + null, + "1303996.363192852", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]", + "735.507" + ] + ], + "shape": { + "columns": 25, + "rows": 10 + } + }, + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sample_namesample_idmf_idmzscan_time_alignedclustertypescan_timeapex_scanstart_scan...dispersity_indexnormalized_dispersity_indexnoise_scorenoise_score_minnoise_score_maxmonoisotopic_mf_idisotopologue_typemass_spectrum_deconvoluted_parentms2_scan_numbers_eic_mz
coll_mf_id
2_c0_0_itest_sample_032c0_0_i301.2166148.8956370untargeted8.8956371882.01828...NaNNaNNaNNaNNaNNoneNoneNone[]301.216614
2_c1_1_itest_sample_032c1_1_i302.2206128.8956371untargeted8.8956371882.01828...NaNNaNNaNNaNNaNNoneNoneNone[]302.220612
2_c2_2_itest_sample_032c2_2_i367.35748319.1526482untargeted19.1526484069.04024...NaNNaNNaNNaNNaNNoneNoneNone[]367.357483
2_c3_3_itest_sample_032c3_3_i368.36117619.1526483untargeted19.1526484069.04024...NaNNaNNaNNaNNaNNoneNoneNone[]368.361176
2_c4_4_itest_sample_032c4_4_i698.62890623.8168034untargeted23.8168035212.05176...NaNNaNNaNNaNNaNNoneNoneNone[]698.628906
2_c6_5_itest_sample_032c6_5_i699.63122623.8168036untargeted23.8168035212.05176...NaNNaNNaNNaNNaNNoneNoneNone[]699.631226
2_c9_6_itest_sample_032c9_6_i227.2018897.3764709untargeted7.3764701513.01477...NaNNaNNaNNaNNaNNoneNoneNone[]227.201889
2_c10_7_itest_sample_032c10_7_i299.2011117.37647010untargeted7.3764701513.01477...NaNNaNNaNNaNNaNNoneNoneNone[]299.201111
2_c12_8_itest_sample_032c12_8_i455.3526618.96547012untargeted8.9654701900.01855...NaNNaNNaNNaNNaNNoneNoneNone[]455.352661
2_c15_9_itest_sample_032c15_9_i735.50701920.79363715untargeted20.7936374483.04438...NaNNaNNaNNaNNaNNoneNoneNone[]735.507019
\n", + "

10 rows × 25 columns

\n", + "
" + ], + "text/plain": [ + " sample_name sample_id mf_id mz scan_time_aligned \\\n", + "coll_mf_id \n", + "2_c0_0_i test_sample_03 2 c0_0_i 301.216614 8.895637 \n", + "2_c1_1_i test_sample_03 2 c1_1_i 302.220612 8.895637 \n", + "2_c2_2_i test_sample_03 2 c2_2_i 367.357483 19.152648 \n", + "2_c3_3_i test_sample_03 2 c3_3_i 368.361176 19.152648 \n", + "2_c4_4_i test_sample_03 2 c4_4_i 698.628906 23.816803 \n", + "2_c6_5_i test_sample_03 2 c6_5_i 699.631226 23.816803 \n", + "2_c9_6_i test_sample_03 2 c9_6_i 227.201889 7.376470 \n", + "2_c10_7_i test_sample_03 2 c10_7_i 299.201111 7.376470 \n", + "2_c12_8_i test_sample_03 2 c12_8_i 455.352661 8.965470 \n", + "2_c15_9_i test_sample_03 2 c15_9_i 735.507019 20.793637 \n", + "\n", + " cluster type scan_time apex_scan start_scan ... \\\n", + "coll_mf_id ... \n", + "2_c0_0_i 0 untargeted 8.895637 1882.0 1828 ... \n", + "2_c1_1_i 1 untargeted 8.895637 1882.0 1828 ... \n", + "2_c2_2_i 2 untargeted 19.152648 4069.0 4024 ... \n", + "2_c3_3_i 3 untargeted 19.152648 4069.0 4024 ... \n", + "2_c4_4_i 4 untargeted 23.816803 5212.0 5176 ... \n", + "2_c6_5_i 6 untargeted 23.816803 5212.0 5176 ... \n", + "2_c9_6_i 9 untargeted 7.376470 1513.0 1477 ... \n", + "2_c10_7_i 10 untargeted 7.376470 1513.0 1477 ... \n", + "2_c12_8_i 12 untargeted 8.965470 1900.0 1855 ... \n", + "2_c15_9_i 15 untargeted 20.793637 4483.0 4438 ... \n", + "\n", + " dispersity_index normalized_dispersity_index noise_score \\\n", + "coll_mf_id \n", + "2_c0_0_i NaN NaN NaN \n", + "2_c1_1_i NaN NaN NaN \n", + "2_c2_2_i NaN NaN NaN \n", + "2_c3_3_i NaN NaN NaN \n", + "2_c4_4_i NaN NaN NaN \n", + "2_c6_5_i NaN NaN NaN \n", + "2_c9_6_i NaN NaN NaN \n", + "2_c10_7_i NaN NaN NaN \n", + "2_c12_8_i NaN NaN NaN \n", + "2_c15_9_i NaN NaN NaN \n", + "\n", + " noise_score_min noise_score_max monoisotopic_mf_id \\\n", + "coll_mf_id \n", + "2_c0_0_i NaN NaN None \n", + "2_c1_1_i NaN NaN None \n", + "2_c2_2_i NaN NaN None \n", + "2_c3_3_i NaN NaN None \n", + "2_c4_4_i NaN NaN None \n", + "2_c6_5_i NaN NaN None \n", + "2_c9_6_i NaN NaN None \n", + "2_c10_7_i NaN NaN None \n", + "2_c12_8_i NaN NaN None \n", + "2_c15_9_i NaN NaN None \n", + "\n", + " isotopologue_type mass_spectrum_deconvoluted_parent \\\n", + "coll_mf_id \n", + "2_c0_0_i None None \n", + "2_c1_1_i None None \n", + "2_c2_2_i None None \n", + "2_c3_3_i None None \n", + "2_c4_4_i None None \n", + "2_c6_5_i None None \n", + "2_c9_6_i None None \n", + "2_c10_7_i None None \n", + "2_c12_8_i None None \n", + "2_c15_9_i None None \n", + "\n", + " ms2_scan_numbers _eic_mz \n", + "coll_mf_id \n", + "2_c0_0_i [] 301.216614 \n", + "2_c1_1_i [] 302.220612 \n", + "2_c2_2_i [] 367.357483 \n", + "2_c3_3_i [] 368.361176 \n", + "2_c4_4_i [] 698.628906 \n", + "2_c6_5_i [] 699.631226 \n", + "2_c9_6_i [] 227.201889 \n", + "2_c10_7_i [] 299.201111 \n", + "2_c12_8_i [] 455.352661 \n", + "2_c15_9_i [] 735.507019 \n", + "\n", + "[10 rows x 25 columns]" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# View induced features dataframe\n", + "print(\"\\nInduced Features (first 10):\")\n", + "display(induced_df.head(10))" + ] + }, + { + "cell_type": "markdown", + "id": "8aec7d50", + "metadata": {}, + "source": [ + "## Step 6: Create Pivot Tables\n", + "\n", + "Pivot tables provide a matrix view of features across samples, useful for comparing intensities and detecting missing values." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4ea2a311", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Pivot table BEFORE gap filling:\n", + "Shape: (50, 3)\n", + "\n", + "Sample 3 NAs: 50 / 50\n" + ] + }, + { + "data": { + "application/vnd.microsoft.datawrangler.viewer.v0+json": { + "columns": [ + { + "name": "cluster", + "rawType": "int64", + "type": "integer" + }, + { + "name": "test_sample_01", + "rawType": "object", + "type": "string" + }, + { + "name": "test_sample_02", + "rawType": "object", + "type": "string" + }, + { + "name": "test_sample_03", + "rawType": "float64", + "type": "float" + } + ], + "ref": "f086952c-26a6-4191-88dd-d59007f597e2", + "rows": [ + [ + "0", + "0_0", + "1_0", + null + ], + [ + "1", + "0_19", + "1_19", + null + ], + [ + "2", + "0_1", + "1_1", + null + ], + [ + "3", + "0_24", + "1_24", + null + ], + [ + "4", + "0_10", + "1_10", + null + ], + [ + "6", + "0_42", + "1_42", + null + ], + [ + "9", + "0_28", + "1_28", + null + ], + [ + "10", + "0_9", + "1_9", + null + ], + [ + "12", + "0_21", + "1_21", + null + ], + [ + "15", + "0_35", + "1_35", + null + ] + ], + "shape": { + "columns": 3, + "rows": 10 + } + }, + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sample_nametest_sample_01test_sample_02test_sample_03
cluster
00_01_0NaN
10_191_19NaN
20_11_1NaN
30_241_24NaN
40_101_10NaN
60_421_42NaN
90_281_28NaN
100_91_9NaN
120_211_21NaN
150_351_35NaN
\n", + "
" + ], + "text/plain": [ + "sample_name test_sample_01 test_sample_02 test_sample_03\n", + "cluster \n", + "0 0_0 1_0 NaN\n", + "1 0_19 1_19 NaN\n", + "2 0_1 1_1 NaN\n", + "3 0_24 1_24 NaN\n", + "4 0_10 1_10 NaN\n", + "6 0_42 1_42 NaN\n", + "9 0_28 1_28 NaN\n", + "10 0_9 1_9 NaN\n", + "12 0_21 1_21 NaN\n", + "15 0_35 1_35 NaN" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Create pivot table BEFORE gap filling (to compare)\n", + "# First, reload collection without gap filling to see the difference\n", + "parser2 = ReadCoreMSHDFMassSpectraCollection(\n", + " folder_location=processed_folder,\n", + " manifest_file=manifest_path,\n", + " cores=1\n", + ")\n", + "collection_before = parser2.get_lcms_collection(load_raw=False, load_light=False)\n", + "collection_before.parameters.lcms_collection.cluster_size_min_samples = 1\n", + "collection_before.align_lcms_objects()\n", + "collection_before.add_consensus_mass_features()\n", + "\n", + "pivot_before = collection_before.collection_pivot_table(verbose=False)\n", + "print(\"Pivot table BEFORE gap filling:\")\n", + "print(f\"Shape: {pivot_before.shape}\")\n", + "print(f\"\\nSample 3 NAs: {pivot_before['test_sample_03'].isna().sum()} / {len(pivot_before)}\")\n", + "display(pivot_before.head(10))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e9d00373", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Pivot table AFTER gap filling:\n", + "Shape: (50, 3)\n", + "\n", + "Sample 3 NAs: 0 / 50\n" + ] + }, + { + "data": { + "application/vnd.microsoft.datawrangler.viewer.v0+json": { + "columns": [ + { + "name": "cluster", + "rawType": "int64", + "type": "integer" + }, + { + "name": "test_sample_01", + "rawType": "object", + "type": "string" + }, + { + "name": "test_sample_02", + "rawType": "object", + "type": "string" + }, + { + "name": "test_sample_03", + "rawType": "object", + "type": "string" + } + ], + "ref": "584c8115-0940-4bc3-988d-b27467425d17", + "rows": [ + [ + "0", + "0_0", + "1_0", + "2_c0_0_i" + ], + [ + "1", + "0_19", + "1_19", + "2_c1_1_i" + ], + [ + "2", + "0_1", + "1_1", + "2_c2_2_i" + ], + [ + "3", + "0_24", + "1_24", + "2_c3_3_i" + ], + [ + "4", + "0_10", + "1_10", + "2_c4_4_i" + ], + [ + "6", + "0_42", + "1_42", + "2_c6_5_i" + ], + [ + "9", + "0_28", + "1_28", + "2_c9_6_i" + ], + [ + "10", + "0_9", + "1_9", + "2_c10_7_i" + ], + [ + "12", + "0_21", + "1_21", + "2_c12_8_i" + ], + [ + "15", + "0_35", + "1_35", + "2_c15_9_i" + ] + ], + "shape": { + "columns": 3, + "rows": 10 + } + }, + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sample_nametest_sample_01test_sample_02test_sample_03
cluster
00_01_02_c0_0_i
10_191_192_c1_1_i
20_11_12_c2_2_i
30_241_242_c3_3_i
40_101_102_c4_4_i
60_421_422_c6_5_i
90_281_282_c9_6_i
100_91_92_c10_7_i
120_211_212_c12_8_i
150_351_352_c15_9_i
\n", + "
" + ], + "text/plain": [ + "sample_name test_sample_01 test_sample_02 test_sample_03\n", + "cluster \n", + "0 0_0 1_0 2_c0_0_i\n", + "1 0_19 1_19 2_c1_1_i\n", + "2 0_1 1_1 2_c2_2_i\n", + "3 0_24 1_24 2_c3_3_i\n", + "4 0_10 1_10 2_c4_4_i\n", + "6 0_42 1_42 2_c6_5_i\n", + "9 0_28 1_28 2_c9_6_i\n", + "10 0_9 1_9 2_c10_7_i\n", + "12 0_21 1_21 2_c12_8_i\n", + "15 0_35 1_35 2_c15_9_i" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Gap filling filled 50 features in Sample 3\n" + ] + } + ], + "source": [ + "# Create pivot table AFTER gap filling\n", + "pivot_after = lcms_collection.collection_pivot_table(verbose=False)\n", + "print(\"\\nPivot table AFTER gap filling:\")\n", + "print(f\"Shape: {pivot_after.shape}\")\n", + "print(f\"\\nSample 3 NAs: {pivot_after['test_sample_03'].isna().sum()} / {len(pivot_after)}\")\n", + "display(pivot_after.head(10))\n", + "\n", + "# Show the difference\n", + "print(f\"\\nGap filling filled {pivot_before['test_sample_03'].isna().sum() - pivot_after['test_sample_03'].isna().sum()} features in Sample 3\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e40ba01c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Pivot table with intensities:\n" + ] + }, + { + "data": { + "application/vnd.microsoft.datawrangler.viewer.v0+json": { + "columns": [ + { + "name": "cluster", + "rawType": "int64", + "type": "integer" + }, + { + "name": "test_sample_01", + "rawType": "float64", + "type": "float" + }, + { + "name": "test_sample_02", + "rawType": "float64", + "type": "float" + }, + { + "name": "test_sample_03", + "rawType": "float64", + "type": "float" + } + ], + "ref": "d6728a96-3138-4434-96bf-2f394e8f272e", + "rows": [ + [ + "0", + "66775328.0", + "66775328.0", + "66775328.0" + ], + [ + "1", + "14249711.0", + "14249711.0", + "14249711.0" + ], + [ + "2", + "48137056.0", + "48137056.0", + "48137056.0" + ], + [ + "3", + "12717404.0", + "12717404.0", + "12717404.0" + ], + [ + "4", + "17265106.0", + "17265106.0", + "17265106.0" + ], + [ + "6", + "7861987.5", + "7861987.5", + "7861987.5" + ], + [ + "9", + "9600092.0", + "9600092.0", + "9600092.0" + ], + [ + "10", + "18258992.0", + "18258992.0", + "18258992.0" + ], + [ + "12", + "12939420.0", + "12939420.0", + "12939420.0" + ], + [ + "15", + "8329064.0", + "8329064.0", + "8329064.0" + ] + ], + "shape": { + "columns": 3, + "rows": 10 + } + }, + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sample_nametest_sample_01test_sample_02test_sample_03
cluster
066775328.066775328.066775328.0
114249711.014249711.014249711.0
248137056.048137056.048137056.0
312717404.012717404.012717404.0
417265106.017265106.017265106.0
67861987.57861987.57861987.5
99600092.09600092.09600092.0
1018258992.018258992.018258992.0
1212939420.012939420.012939420.0
158329064.08329064.08329064.0
\n", + "
" + ], + "text/plain": [ + "sample_name test_sample_01 test_sample_02 test_sample_03\n", + "cluster \n", + "0 66775328.0 66775328.0 66775328.0\n", + "1 14249711.0 14249711.0 14249711.0\n", + "2 48137056.0 48137056.0 48137056.0\n", + "3 12717404.0 12717404.0 12717404.0\n", + "4 17265106.0 17265106.0 17265106.0\n", + "6 7861987.5 7861987.5 7861987.5\n", + "9 9600092.0 9600092.0 9600092.0\n", + "10 18258992.0 18258992.0 18258992.0\n", + "12 12939420.0 12939420.0 12939420.0\n", + "15 8329064.0 8329064.0 8329064.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Create pivot table with intensity values\n", + "pivot_intensity = lcms_collection.collection_pivot_table(attribute='intensity', verbose=False)\n", + "print(\"\\nPivot table with intensities:\")\n", + "display(pivot_intensity.head(10))" + ] + }, + { + "cell_type": "markdown", + "id": "f4b2508c", + "metadata": {}, + "source": [ + "## Step 7: Cluster Representatives\n", + "\n", + "Get the best representative mass feature for each consensus cluster." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f11922ae", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cluster representatives: 50 clusters\n" + ] + }, + { + "data": { + "application/vnd.microsoft.datawrangler.viewer.v0+json": { + "columns": [ + { + "name": "index", + "rawType": "int64", + "type": "integer" + }, + { + "name": "cluster", + "rawType": "int64", + "type": "integer" + }, + { + "name": "coll_mf_id", + "rawType": "object", + "type": "string" + }, + { + "name": "sample_name", + "rawType": "object", + "type": "string" + }, + { + "name": "sample_id", + "rawType": "float64", + "type": "float" + }, + { + "name": "mf_id", + "rawType": "object", + "type": "string" + }, + { + "name": "mz", + "rawType": "float64", + "type": "float" + }, + { + "name": "type", + "rawType": "object", + "type": "string" + }, + { + "name": "scan_time", + "rawType": "float64", + "type": "float" + }, + { + "name": "apex_scan", + "rawType": "float64", + "type": "float" + }, + { + "name": "start_scan", + "rawType": "float64", + "type": "float" + }, + { + "name": "final_scan", + "rawType": "float64", + "type": "float" + }, + { + "name": "intensity", + "rawType": "float64", + "type": "float" + }, + { + "name": "persistence", + "rawType": "float64", + "type": "float" + }, + { + "name": "area", + "rawType": "float64", + "type": "float" + }, + { + "name": "tailing_factor", + "rawType": "float64", + "type": "float" + }, + { + "name": "dispersity_index", + "rawType": "float64", + "type": "float" + }, + { + "name": "normalized_dispersity_index", + "rawType": "float64", + "type": "float" + }, + { + "name": "noise_score", + "rawType": "float64", + "type": "float" + }, + { + "name": "noise_score_min", + "rawType": "float64", + "type": "float" + }, + { + "name": "noise_score_max", + "rawType": "float64", + "type": "float" + }, + { + "name": "monoisotopic_mf_id", + "rawType": "object", + "type": "string" + }, + { + "name": "isotopologue_type", + "rawType": "object", + "type": "string" + }, + { + "name": "mass_spectrum_deconvoluted_parent", + "rawType": "object", + "type": "string" + }, + { + "name": "ms2_scan_numbers", + "rawType": "object", + "type": "string" + }, + { + "name": "_eic_mz", + "rawType": "float64", + "type": "float" + }, + { + "name": "scan_time_aligned", + "rawType": "float64", + "type": "float" + }, + { + "name": "partition_idx", + "rawType": "float64", + "type": "float" + }, + { + "name": "idx", + "rawType": "float64", + "type": "float" + }, + { + "name": "polarity", + "rawType": "object", + "type": "string" + }, + { + "name": "n_samples_detected", + "rawType": "int64", + "type": "integer" + } + ], + "ref": "fa40d0ca-4e40-4fc7-b723-3b45790b0ad1", + "rows": [ + [ + "0", + "0", + "0_0", + "test_sample_01", + "0.0", + "0.0", + "301.21661376953125", + "untargeted", + "8.895636666666666", + "1882.0", + "1828.0", + "2008.0", + "66775328.0", + "66708546.0", + "35045576.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[1874 1910]", + "301.21661376953125", + "8.895636666666666", + "0.0", + "0.0", + "negative", + "3" + ], + [ + "2", + "1", + "0_19", + "test_sample_01", + "0.0", + "19.0", + "302.2206115722656", + "untargeted", + "8.895636666666666", + "1882.0", + "1828.0", + "2008.0", + "14249711.0", + "14142901.0", + "7564257.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]", + "302.2206115722656", + "8.895636666666666", + "0.0", + "2.0", + "negative", + "3" + ], + [ + "4", + "2", + "0_1", + "test_sample_01", + "0.0", + "1.0", + "367.35748291015625", + "untargeted", + "19.152648333333335", + "4069.0", + "4024.0", + "4312.0", + "48137056.0", + "48070260.0", + "30641268.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[4036 4037 4070]", + "367.35748291015625", + "19.152648333333335", + "0.0", + "4.0", + "negative", + "3" + ], + [ + "6", + "3", + "0_24", + "test_sample_01", + "0.0", + "24.0", + "368.3611755371094", + "untargeted", + "19.152648333333335", + "4069.0", + "4024.0", + "4303.0", + "12717404.0", + "12650608.0", + "8073610.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]", + "368.3611755371094", + "19.152648333333335", + "0.0", + "6.0", + "negative", + "3" + ], + [ + "8", + "4", + "0_10", + "test_sample_01", + "0.0", + "10.0", + "698.62890625", + "untargeted", + "23.816803333333333", + "5212.0", + "5176.0", + "5338.0", + "17265106.0", + "17198326.0", + "7113439.5", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[5195 5196 5231 5232]", + "698.62890625", + "23.816803333333333", + "0.0", + "8.0", + "negative", + "3" + ], + [ + "10", + "6", + "0_42", + "test_sample_01", + "0.0", + "42.0", + "699.6312255859375", + "untargeted", + "23.816803333333333", + "5212.0", + "5176.0", + "5293.0", + "7861987.5", + "7795191.0", + "3248198.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]", + "699.6312255859375", + "23.816803333333333", + "0.0", + "11.0", + "negative", + "3" + ], + [ + "12", + "9", + "0_28", + "test_sample_01", + "0.0", + "28.0", + "227.20188903808594", + "untargeted", + "7.376469999999999", + "1513.0", + "1477.0", + "1657.0", + "9600092.0", + "9533297.0", + "3792812.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[1496 1532]", + "227.20188903808594", + "7.376469999999999", + "0.0", + "15.0", + "negative", + "3" + ], + [ + "14", + "10", + "0_9", + "test_sample_01", + "0.0", + "9.0", + "299.20111083984375", + "untargeted", + "7.376469999999999", + "1513.0", + "1477.0", + "1585.0", + "18258992.0", + "18192197.0", + "7192845.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[1493 1494 1523]", + "299.20111083984375", + "7.376469999999999", + "0.0", + "17.0", + "negative", + "3" + ], + [ + "16", + "12", + "0_21", + "test_sample_01", + "0.0", + "21.0", + "455.3526611328125", + "untargeted", + "8.96547", + "1900.0", + "1855.0", + "1999.0", + "12939420.0", + "12872638.0", + "6434092.0", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[1865 1866 1912 1913]", + "455.3526611328125", + "8.96547", + "0.0", + "20.0", + "negative", + "3" + ], + [ + "18", + "15", + "0_35", + "test_sample_01", + "0.0", + "35.0", + "735.5070190429688", + "untargeted", + "20.793636666666668", + "4483.0", + "4438.0", + "4600.0", + "8329064.0", + "8262284.0", + "1303996.375", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[4461 4462 4493 4494]", + "735.5070190429688", + "20.793636666666668", + "0.0", + "24.0", + "negative", + "3" + ] + ], + "shape": { + "columns": 30, + "rows": 10 + } + }, + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
clustercoll_mf_idsample_namesample_idmf_idmztypescan_timeapex_scanstart_scan...monoisotopic_mf_idisotopologue_typemass_spectrum_deconvoluted_parentms2_scan_numbers_eic_mzscan_time_alignedpartition_idxidxpolarityn_samples_detected
000_0test_sample_010.00.0301.216614untargeted8.8956371882.01828.0...NoneNoneNone[1874, 1910]301.2166148.8956370.00.0negative3
210_19test_sample_010.019.0302.220612untargeted8.8956371882.01828.0...NoneNoneNone[]302.2206128.8956370.02.0negative3
420_1test_sample_010.01.0367.357483untargeted19.1526484069.04024.0...NoneNoneNone[4036, 4037, 4070]367.35748319.1526480.04.0negative3
630_24test_sample_010.024.0368.361176untargeted19.1526484069.04024.0...NoneNoneNone[]368.36117619.1526480.06.0negative3
840_10test_sample_010.010.0698.628906untargeted23.8168035212.05176.0...NoneNoneNone[5195, 5196, 5231, 5232]698.62890623.8168030.08.0negative3
1060_42test_sample_010.042.0699.631226untargeted23.8168035212.05176.0...NoneNoneNone[]699.63122623.8168030.011.0negative3
1290_28test_sample_010.028.0227.201889untargeted7.3764701513.01477.0...NoneNoneNone[1496, 1532]227.2018897.3764700.015.0negative3
14100_9test_sample_010.09.0299.201111untargeted7.3764701513.01477.0...NoneNoneNone[1493, 1494, 1523]299.2011117.3764700.017.0negative3
16120_21test_sample_010.021.0455.352661untargeted8.9654701900.01855.0...NoneNoneNone[1865, 1866, 1912, 1913]455.3526618.9654700.020.0negative3
18150_35test_sample_010.035.0735.507019untargeted20.7936374483.04438.0...NoneNoneNone[4461, 4462, 4493, 4494]735.50701920.7936370.024.0negative3
\n", + "

10 rows × 30 columns

\n", + "
" + ], + "text/plain": [ + " cluster coll_mf_id sample_name sample_id mf_id mz \\\n", + "0 0 0_0 test_sample_01 0.0 0.0 301.216614 \n", + "2 1 0_19 test_sample_01 0.0 19.0 302.220612 \n", + "4 2 0_1 test_sample_01 0.0 1.0 367.357483 \n", + "6 3 0_24 test_sample_01 0.0 24.0 368.361176 \n", + "8 4 0_10 test_sample_01 0.0 10.0 698.628906 \n", + "10 6 0_42 test_sample_01 0.0 42.0 699.631226 \n", + "12 9 0_28 test_sample_01 0.0 28.0 227.201889 \n", + "14 10 0_9 test_sample_01 0.0 9.0 299.201111 \n", + "16 12 0_21 test_sample_01 0.0 21.0 455.352661 \n", + "18 15 0_35 test_sample_01 0.0 35.0 735.507019 \n", + "\n", + " type scan_time apex_scan start_scan ... monoisotopic_mf_id \\\n", + "0 untargeted 8.895637 1882.0 1828.0 ... None \n", + "2 untargeted 8.895637 1882.0 1828.0 ... None \n", + "4 untargeted 19.152648 4069.0 4024.0 ... None \n", + "6 untargeted 19.152648 4069.0 4024.0 ... None \n", + "8 untargeted 23.816803 5212.0 5176.0 ... None \n", + "10 untargeted 23.816803 5212.0 5176.0 ... None \n", + "12 untargeted 7.376470 1513.0 1477.0 ... None \n", + "14 untargeted 7.376470 1513.0 1477.0 ... None \n", + "16 untargeted 8.965470 1900.0 1855.0 ... None \n", + "18 untargeted 20.793637 4483.0 4438.0 ... None \n", + "\n", + " isotopologue_type mass_spectrum_deconvoluted_parent \\\n", + "0 None None \n", + "2 None None \n", + "4 None None \n", + "6 None None \n", + "8 None None \n", + "10 None None \n", + "12 None None \n", + "14 None None \n", + "16 None None \n", + "18 None None \n", + "\n", + " ms2_scan_numbers _eic_mz scan_time_aligned partition_idx \\\n", + "0 [1874, 1910] 301.216614 8.895637 0.0 \n", + "2 [] 302.220612 8.895637 0.0 \n", + "4 [4036, 4037, 4070] 367.357483 19.152648 0.0 \n", + "6 [] 368.361176 19.152648 0.0 \n", + "8 [5195, 5196, 5231, 5232] 698.628906 23.816803 0.0 \n", + "10 [] 699.631226 23.816803 0.0 \n", + "12 [1496, 1532] 227.201889 7.376470 0.0 \n", + "14 [1493, 1494, 1523] 299.201111 7.376470 0.0 \n", + "16 [1865, 1866, 1912, 1913] 455.352661 8.965470 0.0 \n", + "18 [4461, 4462, 4493, 4494] 735.507019 20.793637 0.0 \n", + "\n", + " idx polarity n_samples_detected \n", + "0 0.0 negative 3 \n", + "2 2.0 negative 3 \n", + "4 4.0 negative 3 \n", + "6 6.0 negative 3 \n", + "8 8.0 negative 3 \n", + "10 11.0 negative 3 \n", + "12 15.0 negative 3 \n", + "14 17.0 negative 3 \n", + "16 20.0 negative 3 \n", + "18 24.0 negative 3 \n", + "\n", + "[10 rows x 30 columns]" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "All clusters have 1 representative: True\n" + ] + } + ], + "source": [ + "# Get cluster representatives\n", + "reps_table = lcms_collection.cluster_representatives_table()\n", + "print(f\"Cluster representatives: {len(reps_table)} clusters\")\n", + "display(reps_table.head(10))\n", + "\n", + "# Verify each cluster has exactly one representative\n", + "cluster_counts = reps_table['cluster'].value_counts()\n", + "print(f\"\\nAll clusters have 1 representative: {all(count == 1 for count in cluster_counts)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "a218300f", + "metadata": {}, + "source": [ + "## Step 8: Feature Annotations\n", + "\n", + "Next we will annotate the features in MS1 (molecular formula searching) and MS2 (comparison to an MS2 spectral database).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a52580c8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Reloading features, ms2 spectral search, loading eics:\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|█████████████████████████████████████████| 3/3 [00:12<00:00, 4.22s/sample]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Associating EICs with mass features:\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|███████████████████████████████████████| 3/3 [00:00<00:00, 1632.02sample/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ Molecular formula search complete\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "# First we need to prepare a search space for the MS2 spectral search using a small testing MSP file\n", + "msp_file = Path('../../tests/tests_data/lcms/test_db.msp')\n", + "my_msp = MSPInterface(file_path=str(msp_file))\n", + "spectral_library, molecular_metadata = my_msp.get_metabolomics_spectra_library(\n", + " polarity=\"negative\",\n", + " format=\"flashentropy\",\n", + " normalize=True,\n", + " fe_kwargs={\n", + " \"normalize_intensity\": True,\n", + " \"min_ms2_difference_in_da\": 0.02,\n", + " \"max_ms2_tolerance_in_da\": 0.01,\n", + " \"max_indexed_mz\": 3000,\n", + " \"precursor_ions_removal_da\": None,\n", + " \"noise_threshold\": 0,\n", + " },\n", + ")\n", + "\n", + "# Set MS2 score threshold for each sample to enable finding spectral matches\n", + "# This threshold determines the minimum similarity score required for a match\n", + "for lcms_obj in lcms_collection:\n", + " lcms_obj.parameters.lc_ms.ms2_min_fe_score = 0.3\n", + " \n", + "pipeline_results = lcms_collection.process_consensus_features(\n", + " load_representatives=True, # Load representative features for processing\n", + " perform_gap_filling=False, # No gap filling this time, already done\n", + " add_ms1=True, # Need to add and process MS1 data for molecular formula searching, but only for representative features\n", + " add_ms2=True, # Need to add and process MS2 data for spectral searching, but only for representative features\n", + " molecular_formula_search=False, # Perform molecular formula searching\n", + " ms2_spectral_search=True, # Perform MS2 spectral searching\n", + " spectral_lib=spectral_library, # Provide the spectral library we created above\n", + " molecular_metadata=molecular_metadata, # Provide molecular metadata for annotations\n", + " gather_eics=True # Gather EICs to plot later on\n", + ")\n", + "print(\"✓ Molecular formula search complete\")" + ] + }, + { + "cell_type": "markdown", + "id": "fabc2123", + "metadata": {}, + "source": [ + "### View Feature Annotations Table\n", + "\n", + "The feature annotations table combines cluster information with molecular annotations from both MS1 and MS2 searches." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "286c4eec", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Annotations table: 7 rows\n" + ] + }, + { + "data": { + "application/vnd.microsoft.datawrangler.viewer.v0+json": { + "columns": [ + { + "name": "index", + "rawType": "int64", + "type": "integer" + }, + { + "name": "cluster", + "rawType": "int64", + "type": "integer" + }, + { + "name": "Isotopologue Type", + "rawType": "object", + "type": "string" + }, + { + "name": "Is Largest Ion after Deconvolution", + "rawType": "object", + "type": "string" + }, + { + "name": "MS2 Spectrum", + "rawType": "object", + "type": "string" + }, + { + "name": "Calculated m/z", + "rawType": "float64", + "type": "float" + }, + { + "name": "m/z Error (ppm)", + "rawType": "float64", + "type": "float" + }, + { + "name": "m/z Error Score", + "rawType": "float64", + "type": "float" + }, + { + "name": "Isotopologue Similarity", + "rawType": "float64", + "type": "float" + }, + { + "name": "Confidence Score", + "rawType": "float64", + "type": "float" + }, + { + "name": "Ion Formula", + "rawType": "object", + "type": "string" + }, + { + "name": "Ion Type", + "rawType": "object", + "type": "string" + }, + { + "name": "Molecular Formula", + "rawType": "object", + "type": "string" + }, + { + "name": "inchikey", + "rawType": "object", + "type": "string" + }, + { + "name": "name", + "rawType": "object", + "type": "string" + }, + { + "name": "ref_ms_id", + "rawType": "object", + "type": "string" + }, + { + "name": "Entropy Similarity", + "rawType": "float32", + "type": "float" + }, + { + "name": "Library mzs in Query (fraction)", + "rawType": "float64", + "type": "float" + }, + { + "name": "Spectra with Annotation (n)", + "rawType": "float64", + "type": "float" + }, + { + "name": "representative_sample", + "rawType": "object", + "type": "string" + } + ], + "ref": "76b9e5ee-eaad-4392-860c-c58521ab3109", + "rows": [ + [ + "1", + "2", + null, + null, + "367.3582:1.0", + null, + null, + null, + null, + null, + "C24 H47 O2", + "[M-H]-", + "C24H48O2", + "QZZGJDVWLFXDLK-UHFFFAOYSA-N", + "Lignoceric Acid", + "CCMSLIB00004684071", + "0.7833995", + "0.5", + "2.0", + "test_sample_01" + ], + [ + "22", + "12", + null, + null, + "455.3532:1.0; 456.3585:0.01", + null, + null, + null, + null, + null, + "C30 H47 O3", + "[M-H]-", + "C30H48O3", + "WCGUUGGRBIKTOS-UHFFFAOYSA-N", + "Urs-12-en-28-oic acid, 3-hydroxy-, (3beta)-", + "CCMSLIB00010125334", + "0.5768537", + "0.14285714285714285", + "2.0", + "test_sample_01" + ], + [ + "23", + "12", + null, + null, + "455.3532:1.0; 456.3585:0.01", + null, + null, + null, + null, + null, + "C30 H47 O3", + "[M-H]-", + "C30H48O3", + "MIJYXULNPSFWEK-UHFFFAOYSA-N", + "3-Hydroxyolean-12-en-28-oic acid", + "CCMSLIB00010113749", + "0.5429697", + "0.1111111111111111", + "2.0", + "test_sample_01" + ], + [ + "15", + "55", + null, + null, + "455.3531:1.0", + null, + null, + null, + null, + null, + "C30 H47 O3", + "[M-H]-", + "C30H48O3", + "WCGUUGGRBIKTOS-UHFFFAOYSA-N", + "Urs-12-en-28-oic acid, 3-hydroxy-, (3beta)-", + "CCMSLIB00010125334", + "0.64587337", + "0.14285714285714285", + "2.0", + "test_sample_01" + ], + [ + "16", + "55", + null, + null, + "455.3531:1.0", + null, + null, + null, + null, + null, + "C30 H47 O3", + "[M-H]-", + "C30H48O3", + "MIJYXULNPSFWEK-UHFFFAOYSA-N", + "3-Hydroxyolean-12-en-28-oic acid", + "CCMSLIB00010113749", + "0.6061404", + "0.1111111111111111", + "2.0", + "test_sample_01" + ] + ], + "shape": { + "columns": 19, + "rows": 5 + } + }, + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
clusterIsotopologue TypeIs Largest Ion after DeconvolutionMS2 SpectrumCalculated m/zm/z Error (ppm)m/z Error ScoreIsotopologue SimilarityConfidence ScoreIon FormulaIon TypeMolecular Formulainchikeynameref_ms_idEntropy SimilarityLibrary mzs in Query (fraction)Spectra with Annotation (n)representative_sample
12NoneNone367.3582:1.0NaNNaNNaNNaNNaNC24 H47 O2[M-H]-C24H48O2QZZGJDVWLFXDLK-UHFFFAOYSA-NLignoceric AcidCCMSLIB000046840710.7834000.5000002.0test_sample_01
2212NoneNone455.3532:1.0; 456.3585:0.01NaNNaNNaNNaNNaNC30 H47 O3[M-H]-C30H48O3WCGUUGGRBIKTOS-UHFFFAOYSA-NUrs-12-en-28-oic acid, 3-hydroxy-, (3beta)-CCMSLIB000101253340.5768540.1428572.0test_sample_01
2312NoneNone455.3532:1.0; 456.3585:0.01NaNNaNNaNNaNNaNC30 H47 O3[M-H]-C30H48O3MIJYXULNPSFWEK-UHFFFAOYSA-N3-Hydroxyolean-12-en-28-oic acidCCMSLIB000101137490.5429700.1111112.0test_sample_01
1555NoneNone455.3531:1.0NaNNaNNaNNaNNaNC30 H47 O3[M-H]-C30H48O3WCGUUGGRBIKTOS-UHFFFAOYSA-NUrs-12-en-28-oic acid, 3-hydroxy-, (3beta)-CCMSLIB000101253340.6458730.1428572.0test_sample_01
1655NoneNone455.3531:1.0NaNNaNNaNNaNNaNC30 H47 O3[M-H]-C30H48O3MIJYXULNPSFWEK-UHFFFAOYSA-N3-Hydroxyolean-12-en-28-oic acidCCMSLIB000101137490.6061400.1111112.0test_sample_01
\n", + "
" + ], + "text/plain": [ + " cluster Isotopologue Type Is Largest Ion after Deconvolution \\\n", + "1 2 None None \n", + "22 12 None None \n", + "23 12 None None \n", + "15 55 None None \n", + "16 55 None None \n", + "\n", + " MS2 Spectrum Calculated m/z m/z Error (ppm) \\\n", + "1 367.3582:1.0 NaN NaN \n", + "22 455.3532:1.0; 456.3585:0.01 NaN NaN \n", + "23 455.3532:1.0; 456.3585:0.01 NaN NaN \n", + "15 455.3531:1.0 NaN NaN \n", + "16 455.3531:1.0 NaN NaN \n", + "\n", + " m/z Error Score Isotopologue Similarity Confidence Score Ion Formula \\\n", + "1 NaN NaN NaN C24 H47 O2 \n", + "22 NaN NaN NaN C30 H47 O3 \n", + "23 NaN NaN NaN C30 H47 O3 \n", + "15 NaN NaN NaN C30 H47 O3 \n", + "16 NaN NaN NaN C30 H47 O3 \n", + "\n", + " Ion Type Molecular Formula inchikey \\\n", + "1 [M-H]- C24H48O2 QZZGJDVWLFXDLK-UHFFFAOYSA-N \n", + "22 [M-H]- C30H48O3 WCGUUGGRBIKTOS-UHFFFAOYSA-N \n", + "23 [M-H]- C30H48O3 MIJYXULNPSFWEK-UHFFFAOYSA-N \n", + "15 [M-H]- C30H48O3 WCGUUGGRBIKTOS-UHFFFAOYSA-N \n", + "16 [M-H]- C30H48O3 MIJYXULNPSFWEK-UHFFFAOYSA-N \n", + "\n", + " name ref_ms_id \\\n", + "1 Lignoceric Acid CCMSLIB00004684071 \n", + "22 Urs-12-en-28-oic acid, 3-hydroxy-, (3beta)- CCMSLIB00010125334 \n", + "23 3-Hydroxyolean-12-en-28-oic acid CCMSLIB00010113749 \n", + "15 Urs-12-en-28-oic acid, 3-hydroxy-, (3beta)- CCMSLIB00010125334 \n", + "16 3-Hydroxyolean-12-en-28-oic acid CCMSLIB00010113749 \n", + "\n", + " Entropy Similarity Library mzs in Query (fraction) \\\n", + "1 0.783400 0.500000 \n", + "22 0.576854 0.142857 \n", + "23 0.542970 0.111111 \n", + "15 0.645873 0.142857 \n", + "16 0.606140 0.111111 \n", + "\n", + " Spectra with Annotation (n) representative_sample \n", + "1 2.0 test_sample_01 \n", + "22 2.0 test_sample_01 \n", + "23 2.0 test_sample_01 \n", + "15 2.0 test_sample_01 \n", + "16 2.0 test_sample_01 " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Get annotations table (from collection with formula search if available)\n", + "annotations_table = lcms_collection.feature_annotations_table(\n", + " molecular_metadata=molecular_metadata, # Pass molecular_metadata to include MS2 spectral match info\n", + " drop_unannotated=True # Only show features with annotations\n", + ")\n", + "print(f\"Annotations table: {len(annotations_table)} rows\")\n", + "display(annotations_table.head())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ce506a0", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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9ddf73reqlUrtW7dWo0bN9a8efN04YUXnvb6KwMCN6qk7OxsPfbYY/r888/Vr18/SVLr1q21cuVKPffcc0cNSHFxcaWulrhnzx7FxcV5vGZUXqe6Px3Jz89P7dq1019//eXJclFFdOjQQStXrlRqaqry8vIUHR2tc889Vx07djzqMnxH4VhOZZ86Et9TNc+9996rWbNmacGCBapXr56rPS4uTnl5eUpJSXHrlTzWd05x+549e1SnTh23Zdq2beuR+lG5lOf+VJZGjRopKipKf/31V7UJ3AwpR5WUn5+v/Px82Wzuu7CPj4+cTudRl+vUqZPmzJnj1jZ79my3o/+oeU51fzqSw+HQ6tWr3f4IAcLDwxUdHa3Nmzfr999/1xVXXHHUefmOwok4mX3qSHxP1RzGGN177736/PPP9fPPP6thw4Zu0zt06CA/Pz+375yNGzdqx44dR/3OadiwoeLi4tyWSUtL09KlS/mequY8sT+V5Z9//tGBAweq13eUN6/YBhxLenq6WbFihVmxYoWRZCZOnGhWrFhhtm/fbowxplu3buass84yc+fONVu2bDFTpkwxAQEB5pVXXnGtY+DAgWbkyJGu17/88ovx9fU1zz33nFm/fr0ZPXq08fPzM6tXr67w7UPF8sT+NHbsWPPDDz+Yv//+2/zxxx/m+uuvNwEBAWbt2rUVvn2oeMfbpz755BMzd+5c8/fff5svvvjCNGjQwFx11VVu6+A7CiV5Yp/ie6rmuuuuu0x4eLiZN2+eSUpKcj2ysrJc89x5552mfv365ueffza///676dSpk+nUqZPbepo1a2Y+++wz1+tnnnnGREREmC+//NL8+eef5oorrjANGzY02dnZFbZtqHie2J/S09PNQw89ZJYsWWK2bt1qfvrpJ9O+fXvTtGlTk5OTU6Hb50kEblRac+fONZJKPQYPHmyMMSYpKckMGTLExMfHm4CAANOsWTPz/PPPG6fT6VpHt27dXPMX++STT8wZZ5xh/P39zVlnnWW++eabCtwqeIsn9qfhw4eb+vXrG39/fxMbG2suueQSs3z58greMnjL8fap//3vf6ZevXrGz8/P1K9f3/znP/8xubm5buvgOwoleWKf4nuq5iprX5JkpkyZ4ponOzvb3H333aZWrVomKCjIXHnllSYpKanUekou43Q6zeOPP25iY2ON3W43F154odm4cWMFbRW8xRP7U1ZWlundu7eJjo42fn5+pkGDBmbYsGEmOTm5ArfM8yxjuHoLAAAAAADljXO4AQAAAADwAAI3AAAAAAAeQOAGAAAAAMADCNwAAAAAAHgAgRsAAAAAAA8gcAMAAAAA4AEEbgAAAAAAPIDADQAAAACABxC4AQDwgiFDhqh///5ee/+BAwfq6aefPq11TJ06VREREeVTkIedd955+vTTT71dBgCghrGMMcbbRQAAUJ1YlnXM6aNHj9aDDz4oY4xXAuuqVavUs2dPbd++XSEhIae8nuzsbKWnpysmJqYcqyv8/D7//PNyPSAxa9YsPfjgg9q4caNsNvobAAAVg39xAAAoZ0lJSa7HCy+8oLCwMLe2hx56SOHh4V7rHZ48ebIGDBhwWmFbkgIDA8s9bHtK3759lZ6eru+++87bpQAAahACNwAA5SwuLs71CA8Pl2VZbm0hISGlhpR3795d9913n4YPH65atWopNjZWb775pjIzMzV06FCFhoaqSZMmpQLjmjVr1LdvX4WEhCg2NlYDBw7U/v37j1qbw+HQzJkzddlll7m1JyYm6v/+7/80aNAghYSEqEGDBvrqq6+0b98+XXHFFQoJCVHr1q31+++/u5Y5ckj5mDFj1LZtW73//vtKTExUeHi4rr/+eqWnp7u9zwsvvOD23m3bttWYMWNc0yXpyiuvlGVZrteS9OWXX6p9+/YKCAhQo0aNNHbsWBUUFEiSjDEaM2aM6tevL7vdrvj4eN1///2uZX18fHTJJZfoo48+OupnAwBAeSNwAwBQSbz77ruKiorSb7/9pvvuu0933XWXBgwYoM6dO2v58uXq3bu3Bg4cqKysLElSSkqKevbsqXbt2un333/X999/rz179ujaa6896nv8+eefSk1NVceOHUtNmzRpks4//3ytWLFC/fr108CBAzVo0CDdfPPNWr58uRo3bqxBgwbpWGej/f333/riiy80a9YszZo1S/Pnz9czzzxzwp/BsmXLJElTpkxRUlKS6/XChQs1aNAgPfDAA1q3bp1ef/11TZ06VU899ZQk6dNPP9WkSZP0+uuva/Pmzfriiy/UqlUrt3Wfc845Wrhw4QnXAgDA6SJwAwBQSbRp00b/+c9/1LRpU40aNUoBAQGKiorSsGHD1LRpUz3xxBM6cOCA/vzzT0nSSy+9pHbt2unpp59W8+bN1a5dO73zzjuaO3euNm3aVOZ7bN++XT4+PmUOBb/kkkt0xx13uN4rLS1NZ599tgYMGKAzzjhDjz76qNavX689e/YcdRucTqemTp2qli1bqkuXLho4cKDmzJlzwp9BdHS0JCkiIkJxcXGu12PHjtXIkSM1ePBgNWrUSBdddJGefPJJvf7665KkHTt2KC4uTr169VL9+vV1zjnnaNiwYW7rjo+P186dO+V0Ok+4HgAATgeBGwCASqJ169au5z4+PoqMjHTrpY2NjZUk7d27V1Lhxc/mzp2rkJAQ16N58+aSCnuay5KdnS273V7mhd1Kvn/xex3r/cuSmJio0NBQ1+s6deocc/4TtWrVKv33v/9129Zhw4YpKSlJWVlZGjBggLKzs9WoUSMNGzZMn3/+uWu4ebHAwEA5nU7l5uaedj0AAJwIX28XAAAACvn5+bm9tizLra04JBf30GZkZOiyyy7T+PHjS62rTp06Zb5HVFSUsrKylJeXJ39//6O+f/F7Hev9T3QbSs5vs9lKDUnPz88/6vqKZWRkaOzYsbrqqqtKTQsICFBCQoI2btyon376SbNnz9bdd9+tZ599VvPnz3fVdPDgQQUHByswMPC47wcAQHkgcAMAUEW1b99en376qRITE+Xre2L/pLdt21aStG7dOtfzihQdHa2kpCTX67S0NG3dutVtHj8/PzkcDre29u3ba+PGjWrSpMlR1x0YGKjLLrtMl112me655x41b95cq1evVvv27SUVXmCuXbt25bg1AAAcG0PKAQCoou655x4dPHhQN9xwg5YtW6a///5bP/zwg4YOHVoqsBaLjo5W+/bttWjRogqutlDPnj31/vvva+HChVq9erUGDx4sHx8ft3kSExM1Z84cJScn69ChQ5KkJ554Qu+9957Gjh2rtWvXav369froo4/0n//8R1LhFdPffvttrVmzRlu2bNEHH3ygwMBANWjQwLXehQsXqnfv3hW3sQCAGo/ADQBAFRUfH69ffvlFDodDvXv3VqtWrTR8+HBFRETIZjv6P/G33Xabpk2bVoGVHjZq1Ch169ZNl156qfr166f+/furcePGbvM8//zzmj17thISElw90n369NGsWbP0448/6uyzz9Z5552nSZMmuQJ1RESE3nzzTZ1//vlq3bq1fvrpJ3399deKjIyUJO3atUuLFy/W0KFDK3aD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Mu+9956r7Z577jGxsbGu1yNGjDBdu3Y1MTEx5tVXXzXGGHPgwAFjWZb53//+V2qdZ5xxhunbt+8Jb8uJOOuss0y3bt1KtT/55JMmODjYbNq0ya195MiRxsfHx+zYscMYY8wDDzxgwsLCSn0mJZ3K76FYeHi4ueeee445T1mf/7hx44xlWWb79u2utsGDBxtJ5umnn3a1HTp0yAQGBhrLssxHH33kat+wYYORZEaPHu1qK94HOnToYPLy8lztEyZMMJLMl19+6Wrr1q2b2+f6/vvvG5vNZhYuXOhW52uvvWYkmV9++eWY21hScHCwueWWW0q1f/PNN0aS+f7778tc7mi/awAAAKAyoYe7HFx77bXKzs7WrFmzlJ6erlmzZpU5nFySAgMDtXTpUj388MOSCof23nrrrapTp47uu+++kx6SeywHDhw45sWlhg0bJh8fn1L1FcvPz9eBAwfUpEkTRUREuA197tKli/bs2aONGzdKKuzJ7tq1q7p06aKFCxdKKuz1NsaU6uGWpFq1apU6v91TZsyYoS5durjes/jRq1cvORwOV099REREuQ91LykiIkJLly51uxL9kUp+/pmZmdq/f786d+4sY4xWrFhRav7bbrvNbf3NmjVTcHCw263XmjVrpoiICG3ZsqXU8rfffrv8/Pxcr++66y75+vrq22+/PWqNM2bM0JlnnqnmzZu7fZ49e/aUJM2dO/eoyx4pOzu71IUFpcLTK4qnAwAAAFUVgbscREdHq1evXpo+fbo+++wzORwO1wWgyhIeHq4JEyZo27Zt2rZtm95++201a9ZML730kp588slyrc0cY2hww4YNS7VlZ2friSeecJ3rHBUVpejoaKWkpCg1NdU1X3GIXrhwoTIzM7VixQp16dJFXbt2dQXuhQsXKiwsTG3atCmzLsuyjln7wYMHlZyc7HqUfP+TsXnzZn3//feKjo52exSf2753715JhUPszzjjDPXt21f16tXTLbfcou+///6U3rMsEyZM0Jo1a5SQkKBzzjlHY8aMKRWCd+zYoSFDhqh27doKCQlRdHS0unXrJkmltj8gIEDR0dFubeHh4apXr16pzzY8PLzMc7ObNm3q9jokJER16tQ55lXYN2/erLVr15b6PM844wxJhz/PExEYGFjmQaacnBzXdAAAAKCq4irl5eTGG2/UsGHDlJycrL59+yoiIuKElmvQoIFuueUWXXnllWrUqJGmTZum//u//yuXmiIjI495Aayywsx9992nKVOmaPjw4erUqZPCw8NlWZauv/56twuMxcfHq2HDhlqwYIESExNljFGnTp0UHR2tBx54QNu3b9fChQvVuXPnMi90dejQoVJh70hXXXWV5s+f73o9ePDgU7rtl9Pp1EUXXaRHHnmkzOnFQTEmJkYrV67UDz/8oO+++07fffedpkyZokGDBundd9896fc90rXXXqsuXbro888/148//qhnn31W48eP12effaa+ffvK4XDooosu0sGDB/Xoo4+qefPmCg4O1q5duzRkyJBSF3g7cnTC8dqPdfDlZDidTrVq1UoTJ04sc3pCQsIJr6tOnTpKSkoq1V7cFh8ff2pFAgAAAJUAgbucXHnllbrjjjv066+/6uOPPz7p5WvVqqXGjRtrzZo15VZT8+bN9emnn57UMjNnztTgwYPd7pOdk5OjlJSUUvN26dJFCxYsUMOGDdW2bVuFhoaqTZs2Cg8P1/fff6/ly5dr7NixpZYrKCjQzp07dfnllx+zlueff97tgMHxwtfReswbN26sjIyMUldrL4u/v78uu+wyXXbZZXI6nbr77rv1+uuv6/HHH1eTJk2O2yt/PHXq1NHdd9+tu+++W3v37lX79u311FNPqW/fvlq9erU2bdqkd9991+0iaZ4a4i4V9lb36NHD9TojI0NJSUm65JJLjrpM48aNtWrVKl144YWn/Xm0bdtWCxculNPpdDsws3TpUgUFBbkOhgAAAABVEUPKy0lISIheffVVjRkzRpdddtlR51u1alWZ5y5v375d69atU7Nmzcqtpk6dOunQoUNlnrt7ND4+PqV6QidPniyHw1Fq3i5dumjbtm36+OOPXUPMbTabOnfurIkTJyo/P7/M87fXrVunnJwcde7c+Zi1dOjQQb169XI9WrRoccz5g4ODyzwwcO2112rJkiX64YcfSk1LSUlRQUGBpMJz3kuy2Wxq3bq1JLmGPRffq7ys9zkWh8NRakh4TEyM4uPjXesu7pku+fkbY1y3JfOEN954Q/n5+a7Xr776qgoKCtS3b9+jLnPttddq165devPNN0tNy87OVmZm5gm//zXXXKM9e/bos88+c7Xt379fM2bM0GWXXVbm+d0AAABAVUEPdzkaPHjwceeZPXu2Ro8ercsvv1znnXeeQkJCtGXLFr3zzjvKzc3VmDFj3OYvHl6+du1aSdL777+vRYsWSZL+85//HPO9+vXrJ19fX/3000+6/fbbT2gbLr30Ur3//vsKDw9XixYttGTJEv3000+KjIwsNW9xmN64caOefvppV3vXrl313XffyW636+yzzy7zMwgKCtJFF110QjWdqA4dOujVV1/V//3f/6lJkyaKiYlRz5499fDDD+urr77SpZdeqiFDhqhDhw7KzMzU6tWrNXPmTG3btk1RUVG67bbbdPDgQfXs2VP16tXT9u3bNXnyZLVt29Z166+2bdvKx8dH48ePV2pqqux2u3r27KmYmJhj1paenq569erpmmuuUZs2bRQSEqKffvpJy5Ytc40maN68uRo3bqyHHnpIu3btUlhYmD799NMTui/2qcrLy9OFF16oa6+9Vhs3btQrr7yiCy644JijDwYOHKhPPvlEd955p+bOnavzzz9fDodDGzZs0CeffKIffvhBHTt2PKH3v+aaa3Teeedp6NChWrdunaKiovTKK6/I4XCUGh2xYMEC1wXu9u3bp8zMTNf/H127dlXXrl1P8VMAAAAAPMRr10ev4kreFuxYjrwt2JYtW8wTTzxhzjvvPBMTE2N8fX1NdHS06devn/n5559LLS/pqI8Tcfnll5sLL7zwhGs/dOiQGTp0qImKijIhISGmT58+ZsOGDaZBgwZm8ODBpeaPiYkxktxua7Zo0SIjyXTp0qXMms4991xz8803n1D9JyM5Odn069fPhIaGGklut41KT083o0aNMk2aNDH+/v4mKirKdO7c2Tz33HOu22LNnDnT9O7d28TExBh/f39Tv359c8cdd5ikpCS393nzzTdNo0aNjI+PzwnfIiw3N9c8/PDDpk2bNiY0NNQEBwebNm3amFdeecVtvnXr1plevXqZkJAQExUVZYYNG2ZWrVplJJkpU6a45hs8eLAJDg4u9T7dunUzZ511Vqn2I/fD4n1g/vz55vbbbze1atUyISEh5qabbjIHDhwotc4jb8GVl5dnxo8fb8466yxjt9tNrVq1TIcOHczYsWNNamrqcT+Pkg4ePGhuvfVWExkZaYKCgky3bt3K3DdHjx591P8XSt7yDAAAAKgsLGPK6UpKqJQWLlyo7t27a8OGDce9SFlFWLlypdq3b6/ly5erbdu23i6nxpo6daqGDh2qZcuWnXBvNAAAAICTwznc1VyXLl3Uu3dvTZgwwdulSJKeeeYZXXPNNYRtAAAAANUe53DXAN999523S3D56KOPvF1CucvIyFBGRsYx54mOjj7q7bqqo9TUVGVnZx9znri4uAqqBgAAAPAOAjdwmp577rkyb39W0tatW5WYmFgxBVUCDzzwwHHvXc7ZLAAAAKjuOIcbOE1btmw57q3XLrjgAgUEBFRQRd63bt067d69+5jznMh90QEAAICqjMANAAAAAIAHcNE0AAAAAAA8gHO4T5LT6dTu3bsVGhoqy7K8XQ4AAAAAoJwYY5Senq74+HjZbKffP03gPkm7d+9WQkKCt8sAAAAAAHjIzp07Va9evdNeD4H7JIWGhkoq/AWEhYV5uRoAAAAAQHlJS0tTQkKCK/edLgL3ceTm5io3N9f1Oj09XZIUFhZG4AYAAACAaqi8Th/momnHMW7cOIWHh7seDCcHAAAAAJwIbgt2HEf2cBcPMUhNTaWHGwAAAACqkbS0NIWHh5db3mNI+XHY7XbZ7XZvlwEAAAAAqGII3B7icDiUn5/v7TLgRX5+fvLx8fF2GQAAAAC8hMBdzowxSk5OVkpKirdLQSUQERGhuLg47tkOAAAA1EAE7nJWHLZjYmIUFBRE0KqhjDHKysrS3r17JUl16tTxckUAAAAAKhqBuxw5HA5X2I6MjPR2OfCywMBASdLevXsVExPD8HIAAACghuG2YOWo+JztoKAgL1eCyqJ4X+B8fgAAAKDmIXB7AMPIUYx9AQAAAKi5CNwAAAAAAHgAgRsAAAAAAA8gcEOSNGTIEFmWJcuy5Ofnp4YNG+qRRx5RTk5OhdaRmJgoy7L00UcflZp21llnybIsTZ061dW2atUqXX755YqJiVFAQIASExN13XXXua4OLkn333+/OnToILvdrrZt21bAVgAAAAAAgRslXHzxxUpKStKWLVs0adIkvf766xo9enSF15GQkKApU6a4tf36669KTk5WcHCwq23fvn268MILVbt2bf3www9av369pkyZovj4eGVmZrotf8stt+i666475vs68h1K/z1d6b+ny5HvKL8NAgAAAFAjEbjhYrfbFRcXp4SEBPXv31+9evXS7NmzXdMPHDigG264QXXr1lVQUJBatWqlDz/80DV91qxZioiIkMNRGFZXrlwpy7I0cuRI1zy33Xabbr755mPWcdNNN2n+/PnauXOnq+2dd97RTTfdJF/fw3ey++WXX5Samqq33npL7dq1U8OGDdWjRw9NmjRJDRs2dM334osv6p577lGjRo1O/cMBAAAAgJNE4K4gmZmZFfo4XWvWrNHixYvl7+/vasvJyVGHDh30zTffaM2aNbr99ts1cOBA/fbbb5KkLl26KD09XStWrJAkzZ8/X1FRUZo3b55rHfPnz1f37t2P+d6xsbHq06eP3n33XUlSVlaWPv74Y91yyy1u88XFxamgoECff/65jDGnvc0AAAAAUJ58jz8LykNISEiFvt+pBNBZs2YpJCREBQUFys3Nlc1m00svveSaXrduXT300EOu1/fdd59++OEHffLJJzrnnHMUHh6utm3bat68eerYsaPmzZunBx98UGPHjlVGRoZSU1P1119/qVu3bset5ZZbbtG//vUv/fvf/9bMmTPVuHHjUudfn3feeXrsscd044036s4779Q555yjnj17atCgQYqNjT3p7QcAAACA8kQPN1x69OihlStXaunSpRo8eLCGDh2qq6++2jXd4XDoySefVKtWrVS7dm2FhITohx9+0I4dO1zzdOvWTfPmzZMxRgsXLtRVV12lM888U4sWLdL8+fMVHx+vpk2bHreWfv36KSMjQwsWLNA777xTqne72FNPPaXk5GS99tprOuuss/Taa6+pefPmWr169el/IAAAAABwGujhriAZGRneLuG4goOD1aRJE0mF50y3adNGb7/9tm699VZJ0rPPPqv//e9/euGFF9SqVSsFBwdr+PDhysvLc62je/fueuedd7Rq1Sr5+fmpefPm6t69u+bNm6dDhw6dUO+2JPn6+mrgwIEaPXq0li5dqs8///yo80ZGRmrAgAEaMGCAnn76abVr107PPfeca0g6AAAAAHgDgbuClLy6dlVgs9n02GOPacSIEbrxxhsVGBioX375RVdccYXromdOp1ObNm1SixYtXMsVn8c9adIkV7ju3r27nnnmGR06dEj/+te/TriGW265Rc8995yuu+461apV64SW8ff3V+PGjcvlPHYAAAAAOB0MKcdRDRgwQD4+Pnr55ZclSU2bNtXs2bO1ePFirV+/XnfccYf27NnjtkytWrXUunVrTZs2zXVxtK5du2r58uXatGnTCfdwS9KZZ56p/fv3l7pFWLFZs2bp5ptv1qxZs7Rp0yZt3LhRzz33nL799ltdccUVrvn++usvrVy5UsnJycrOztbKlSu1cuVKt555AAAAAChv9HDjqHx9fXXvvfdqwoQJuuuuu/Sf//xHW7ZsUZ8+fRQUFKTbb79d/fv3V2pqqtty3bp108qVK12Bu3bt2mrRooX27NmjZs2anVQNkZGRR53WokULBQUF6V//+pd27twpu92upk2b6q233tLAgQNd8912222aP3++63W7du0kSVu3blViYuJJ1QMAAAAAJ8oy3E/ppKSlpSk8PFypqakKCwtzm5aTk6OtW7eqYcOGCggI8FKFOFWOfIeyVmVJkoLaBMnHz+e018k+AQAAAFQdx8p7p4Ih5QAAAAAAeACBGwAAAAAADyBwAwAAAADgAQRuAAAAAAA8gMANAAAAAIAHELgBAAAAAPAAAjcAAAAAAB5A4AYAAAAAwAMI3AAAAAAAeACBGwAAAAAADyBwQ5I0ZMgQWZZV6vHXX3+Vy/qnTp2qiIiIclmXN2zfvl2BgYHKyMjwdikAAAAAqghfbxeAyuPiiy/WlClT3Nqio6O9VM3R5efny8/Pr0Lf88svv1SPHj0UEhJSoe8LAAAAoOqihxsudrtdcXFxbg8fHx9JhYGzffv2CggIUKNGjTR27FgVFBS4lp04caJatWql4OBgJSQk6O6773b1Bs+bN09Dhw5Vamqqq+d8zJgxkiTLsvTFF1+41REREaGpU6dKkrZt2ybLsvTxxx+rW7duCggI0LRp0yRJb731ls4880wFBASoefPmeuWVV465fd27d9d9992n4cOHq1atWoqNjdWbb76pzMxMDR06VBG1I9Tmyjb68ZcfSy375Zdf6vLLL3fVfOQjMTHxZD9uAAAAANUcgbuCODIdFfooTwsXLtSgQYP0wAMPaN26dXr99dc1depUPfXUU655bDabXnzxRa1du1bvvvuufv75Zz3yyCOSpM6dO+uFF15QWFiYkpKSlJSUpIceeuikahg5cqQeeOABrV+/Xn369NG0adP0xBNP6KmnntL69ev19NNP6/HHH9e77757zPW8++67ioqK0m+//ab77rtPd911lwYMGKDOnTtr2dJl6nluT90++nZlZWW5lklJSdGiRYtcgbt4G5KSkvTXX3+pSZMm6tq160ltDwAAAIDqjyHlFWRhyMIKfb/upvtJLzNr1iy3IdN9+/bVjBkzNHbsWI0cOVKDBw+WJDVq1EhPPvmkHnnkEY0ePVqSNHz4cNdyiYmJ+r//+z/deeedeuWVV+Tv76/w8HBZlqW4uLhT2p7hw4frqquucr0ePXq0nn/+eVdbw4YNXQcDiussS5s2bfSf//xHkjRq1Cg988wzioqK0rBhw+TId2jkbSP19qdv68/Vf+r8C86XJH377bdq3bq14uPjJcm1DcYYXX311QoPD9frr79+StsFAAAAoPoicMOlR48eevXVV12vg4ODJUmrVq3SL7/84taj7XA4lJOTo6ysLAUFBemnn37SuHHjtGHDBqWlpamgoMBt+unq2LGj63lmZqb+/vtv3XrrrRo2bJirvaCgQOHh4cdcT+vWrV3PfXx8FBkZqVatWrnaYiJjJEn79u5ztZUcTl7SY489piVLluj3339XYGDgyW8UAAAAgGqNwF1BumR08XYJxxUcHKwmTZqUas/IyNDYsWPdepiLBQQEaNu2bbr00kt111136amnnlLt2rW1aNEi3XrrrcrLyztm4LYsS8YYt7b8/PwyaytZjyS9+eabOvfcc93mKz7n/GiOvNiaZVlubZZlSZKcTqckKS8vT99//70ee+wxt+U++OADTZo0SfPmzVPdunWP+Z4AAAAAaiYCdwXxCT52EKzM2rdvr40bN5YZxiXpjz/+kNPp1PPPPy+brfCyAJ988onbPP7+/nI4Sp9bHh0draSkJNfrzZs3u50/XZbY2FjFx8dry5Ytuummm052c07KvHnzVKtWLbVp08bVtmTJEt122216/fXXdd5553n0/QEAAABUXQRuHNcTTzyhSy+9VPXr19c111wjm82mVatWac2aNfq///s/NWnSRPn5+Zo8ebIuu+wy/fLLL3rttdfc1pGYmKiMjAzNmTNHbdq0UVBQkIKCgtSzZ0+99NJL6tSpkxwOhx599NETuuXX2LFjdf/99ys8PFwXX3yxcnNz9fvvv+vQoUMaMWJEuW37V1995TacPDk5WVdeeaWuv/569enTR8nJyZIKe9Yr4y3UAAAAAHgPVyk/jtzcXKWlpbk9apo+ffpo1qxZ+vHHH3X22WfrvPPO06RJk9SgQQNJhRcimzhxosaPH6+WLVtq2rRpGjdunNs6OnfurDvvvFPXXXedoqOjNWHCBEnS888/r4SEBHXp0kU33nijHnrooRM65/u2227TW2+9pSlT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AAAAANVZtQ/cTz/9tAIDAyVJS5Ys0UsvvaQJEyYoKipKDz74oJerAwAAAABUV9VySHlJO3fuVJMmTSRJX3zxha655hrdfvvtOv/889W9e3fvFgcAAAAAqLaqfQ93SEiIDhw4IEn68ccfddFFF0mSAgIClJ2d7c3SAAAAAADVWLXv4b7ooot02223qV27dtq0aZMuueQSSdLatWuVmJjo3eIAAAAAANVWte/hfvnll9WpUyft27dPn376qSIjIyVJf/zxh2644QYvVwcAAAAAqK4sY4zxdhFVSVpamsLDw5WamqqwsDBvl4NylLY3Tctjl0uS2u9pr7AYfr8AAABATVLeea/aDymXpJSUFP3222/au3evnE6nq92yLA0cONCLlQEAAAAAqqtqH7i//vpr3XTTTcrIyFBYWJgsy3JNI3ADAAAAADyl2p/D/a9//Uu33HKLMjIylJKSokOHDrkeBw8e9HZ5AAAAAIBqqtoH7l27dun+++9XUFCQt0sBAAAAANQg1T5w9+nTR7///ru3ywAAAAAA1DDV/hzufv366eGHH9a6devUqlUr+fn5uU2//PLLvVQZAAAAAKA6q/aBe9iwYZKk//73v6WmWZYlh8NR0SUBAAAAAGqAah+4S94GDAAAAACAilLtz+EuKScnx9slAAAAAABqiGofuB0Oh5588knVrVtXISEh2rJliyTp8ccf19tvv+3l6gAAAAAA1VW1D9xPPfWUpk6dqgkTJsjf39/V3rJlS7311lterAwAAAAAUJ1V+8D93nvv6Y033tBNN90kHx8fV3ubNm20YcMGL1YGAAAAAKjOqn3g3rVrl5o0aVKq3el0Kj8/3wsVAQAAAABqgmofuFu0aKGFCxeWap85c6batWvnhYoAAAAAADVBtb8t2BNPPKHBgwdr165dcjqd+uyzz7Rx40a99957mjVrlrfLAwAAAABUU9W+h/uKK67Q119/rZ9++knBwcF64okntH79en399de66KKLvF0eAAAAAKCaqvY93JLUpUsXzZ4929tlAAAAAABqkGrfw92oUSMdOHCgVHtKSooaNWrkhYoAAAAAADVBtQ/c27Ztk8PhKNWem5urXbt2eaEiAAAAAEBNUG2HlH/11Veu5z/88IPCw8Ndrx0Oh+bMmaPExEQvVAYAAAAAqAmqbeDu37+/JMmyLA0ePNhtmp+fnxIT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efbZZ11tKSkpxm63mw8//NAYY8y6deuMJLNs2TLXPN99952xLMvs2rXLc8UDp6hfv37mlltucWu76qqrzE033WSMYb9H9STJfP75567X5bWfv/LKK6ZWrVpuf+c8+uijplmzZh7eIqD80cMNVIB77rlH/fr1U69evdza//jjD+Xn57u1N2/eXPXr19eSJUskSUuWLFGrVq0UGxvrmqdPnz5KS0vT2rVrK2YDgJPw1VdfqWPHjhowYIBiYmLUrl07vfnmm67pW7duVXJystt+Hx4ernPPPddtv4+IiFDHjh1d8/Tq1Us2m01Lly6tuI0BTlDnzp01Z84cbdq0SZK0atUqLVq0SH379pXEfo+aobz28yVLlqhr167y9/d3zdOnTx9t3LhRhw4dqqCtAcqHr7cLAKq7jz76SMuXL9eyZctKTUtOTpa/v78iIiLc2mNjY5WcnOyap2TYLp5ePA2obLZs2aJXX31VI0aM0GOPPaZly5bp/vvvl7+/vwYPHuzab8var0vu9zExMW7TfX19Vbt2bfZ7VEojR45UWlqamjdvLh8fHzkcDj311FO66aabJIn9HjVCee3nycnJatiwYal1FE8reYoSUNkRuAEP2rlzpx544AHNnj1bAQEB3i4HqBBOp1MdO3bU008/LUlq166d1qxZo9dee02DBw/2cnWAZ3zyySeaNm2apk+frrPOOksrV67U8OHDFR8fz34PADUYQ8oBD/rjjz+0d+9etW/fXr6+vvL19dX8+fP14osvytfXV7GxscrLy1NKSorbcnv27FFcXJwkKS4urtRVy4tfF88DVCZ16tRRixYt3NrOPPNM7dixQ9Lh/bas/brkfr9371636QUFBTp48CD7PSqlhx9+WCNHjtT111+vVq1aaeDAgXrwwQc1btw4Sez3qBnKaz/nbx9UJwRuwIMuvPBCrV69WitXrnQ9OnbsqJtuusn13M/PT3PmzHEts3HjRu3YsUOdOnWSJHXq1EmrV692+8dp9uzZCgsLKxVqgMrg/PPP18aNG93aNm3apAYNGkiSGjZsqLi4OLf9Pi0tTUuXLnXb71NSUvTHH3+45vn555/ldDp17rnnVsBWACcnKytLNpv7n1U+Pj5yOp2S2O9RM5TXft6pUyctWLBA+fn5rnlmz56tZs2aMZwcVY+3r9oG1DQlr1JujDF33nmnqV+/vvn555/N77//bjp16mQ6derkml5QUGBatmxpevfubVauXGm+//57Ex0dbUaNGuWF6oHj++2334yvr6956qmnzObNm820adNMUFCQ+eCDD1zzPPPMMyYiIsJ8+eWX5s8//zRXXHGFadiwocnOznbNc/HFF5t27dqZpUuXmkWLFpmmTZuaG264wRubBBzX4MGDTd26dc2sWbPM1q1bzWeffWaioqLMI4884pqH/R7VQXp6ulmxYoVZsWKFkWQmTpxoVqxYYbZv326MKZ/9PCUlxcTGxpqBAweaNWvWmI8++sgEBQWZ119/vcK3FzhdBG6ggh0ZuLOzs83dd99tatWqZYKCgsyVV15pkpKS3JbZtm2b6du3rwkMDDRRUVHmX//6l8nPz6/gyoET9/XXX5uWLVsau91umjdvbt544w236U6n0zz++OMmNjbW2O12c+GFF5qNGze6zXPgwAFzww03mJCQEBMWFmaGDh1q0tPTK3IzgBOWlpZmHnjgAVO/fn0TEBBgGjVqZP7973+73daI/R7Vwdy5c42kUo/BgwcbY8pvP1+1apW54IILjN1uN3Xr1jXPPPNMRW0iUK4sY4zxZg87AAAAAADVEedwAwAAAADgAQRuAAAAAAA8gMANAAAAAIAHELgBAAAAAPAAAjcAAAAAAB5A4AYAAAAAwAMI3AAAAAAAeACBGwAAAAAADyBwAwAAAADgAQRuAABwwrZv367AwEBlZGR4uxQAACo9AjcAADhhX375pXr06KGQkBBvlwIAQKVH4AYAoAbq3r277rvvPg0fPly1atVSbGys3nzzTWVmZmro0KEKDQ1VkyZN9N1337kt9+WXX+ryyy+XJFmWVeqRmJjoha0BAKByInADAFBDvfvuu4qKitJvv/2m++67T3fddZcGDBigzp07a/ny5erdu7cGDhyorKwsSVJKSooWLVrkCtxJSUmux19//aUmTZqoa9eu3twkAAAqFcsYY7xdBAAAqFjdu3eXw+HQwoULJUkOh0Ph4eG66qqr9N5770mSkpOTVadOHS1ZskTnnXeepk+frkmTJmnZsmVu6zLG6Oqrr9aOHTu0cOFCBQYGVvj2AABQGfl6uwAAAOAdrVu3dj338fFRZGSkWrVq5WqLjY2VJO3du1eS+3Dykh577DEtWbJEv//+O2EbAIASGFIOAEAN5efn5/basiy3NsuyJElOp1N5eXn6/vvvSwXuDz74QJMmTdLnn3+uunXrer5oAACqEAI3AAA4rnnz5qlWrVpq06aNq23JkiW67bbb9Prrr+u8887zYnUAAFRODCkHAADH9dVXX7n1bicnJ+vKK6/U9ddfrz59+ig5OVlS4dD06Ohob5UJAEClQg83AAA4riMD94YNG7Rnzx69++67qlOnjutx9tlne7FKAAAqF65SDgAAjmn58uXq2bOn9u3bV+q8bwAAcHT0cAMAgGMqKCjQ5MmTCdsAAJwkergBAAAAAPAAergBAAAAAPAAAjcAAAAAAB5A4AYAAAAAwAMI3AAAAAAAeACBGwAAAAAADyBwAwAAAADgAQRuAAAAAAA8gMANAAAAAIAHELgBAAAAAPAAAjcAAAAAAB5A4AYAAAAAwAMI3AAAAAAAeACBGwAAAAAADyBwAwAAAADgAQRulDJv3jxZlqV58+Z5u5TTsm3bNlmWpalTp1bYe15yySUaNmxYhb1fdeeJ32FZ+/eQIUOUmJhYbu9RzLIsjRkzptzXW53l5+crISFBr7zyirdLAQAAOG0E7lM0depUWZYly7K0aNGiUtONMUpISJBlWbr00kvdpmVkZGj06NFq2bKlgoODFRkZqbZt2+qBBx7Q7t27XfPNmTNHt9xyi8444wwFBQWpUaNGuu2225SUlHRCNQ4ZMkSWZSksLEzZ2dmlpm/evNm1Dc8999xJfgLeUxyYih9+fn5q1KiRBg0apC1btpTLeyxevFhjxoxRSkrKCS/zyy+/6Mcff9Sjjz561FqPfHz00UcVUltl9PXXX6tbt26KiYlx7d/XXnutvv/+e2+X5jEV+btzOp2aMGGCGjZsqICAALVu3VoffvjhCS3bvXv3o+6zfn5+bvPm5ORo3LhxatGihYKCglS3bl0NGDBAa9eudZtvwYIFuvzyy5WQkKCAgADFxcXp4osv1i+//OI2n5+fn0aMGKGnnnpKOTk5p/chAAAAeJmvtwuo6gICAjR9+nRdcMEFbu3z58/XP//8I7vd7taen5+vrl27asOGDRo8eLDuu+8+ZWRkaO3atZo+fbquvPJKxcfHS5IeffRRHTx4UAMGDFDTpk21ZcsWvfTSS5o1a5ZWrlypuLi449bn6+urrKwsff3117r22mvdpk2bNk0BAQGl/qjt2rWrsrOz5e/vfyofSYW5//77dfbZZys/P1/Lly/XG2+8oW+++UarV692fYanavHixRo7dqyGDBmiiIiIE1rm2Wef1YUXXqgmTZoctdYjderUqUJqq2yee+45Pfzww+rWrZtGjRqloKAg/fXXX/rpp5/00Ucf6eKLL5YkNWjQQNnZ2aVC3umoyP07Oztbvr6Hv2Yr8nf373//W88884yGDRums88+W19++aVuvPFGWZal66+//rjL3nbbbW5tmZmZuvPOO9W7d2+39ptuuklfffWVhg0bpvbt22v37t16+eWX1alTJ61evVoNGjSQJG3atEk2m0133nmn4uLidOjQIX3wwQfq2rWrvvnmG9fvXJKGDh2qkSNHavr06brlllvK6RMBAADwAoNTMmXKFCPJXHXVVSYqKsrk5+e7TR82bJjp0KGDadCggenXr5+r/ZNPPjGSzLRp00qtMzs726Smprpez58/3zgcDrd55s+fbySZf//738etcfDgwSY4ONj07t3b9O/fv9T0pk2bmquvvtpIMs8+++xx13ekzMzMMtvz8/NNbm7uSa+vpIyMjKNOmzt3rpFkZsyY4db+4osvGknm6aefNsYYs3XrViPJTJky5aTf/9lnnzWSzNatW09o/j179hhfX1/z1ltvnVCtp+NkanM4HCY7O7vc3rs85Ofnm7CwMHPRRReVOX3Pnj0VXFHh/ysNGjQol3Ud6zM/2f3qVP3zzz/Gz8/P3HPPPa42p9NpunTpYurVq2cKCgpOep3vv/9+qe+uf/75x0gyDz30kNu8P//8s5FkJk6ceMx1ZmZmmtjYWNOnT59S0y699FLTpUuXk64TAACgMmFI+Wm64YYbdODAAc2ePdvVlpeXp5kzZ+rGG28sNf/ff/8tSTr//PNLTQsICFBYWJjrddeuXWWzuf+Kunbtqtq1a2v9+vUnXOONN96o7777zm0Y67Jly7R58+YyayzrHNfu3burZcuW+uOPP9S1a1cFBQXpsccec51j+9xzz+mFF15Q48aNZbfbtW7dOknSzz//rC5duig4OFgRERG64oorStU+ZswYWZaldevW6cYbb1StWrVKjRg4ET179pQkbd269ZjzHa+mMWPG6OGHH5YkNWzY0DWUdtu2bUdd5zfffKOCggL16tXrpOsuZlmW7r33Xn3xxRdq2bKl7Ha7zjrrLLch1serrXgd06ZN01lnnSW73e5afsWKFerbt6/CwsIUEhKiCy+8UL/++qtbDcWnSixYsEB33HGHIiMjFRYWpkGDBunQoUOu+QYPHqyoqCjl5+eX2o7evXurWbNmR93O/fv3Ky0trcz/ByQpJibG9bysc7iHDBmikJAQ7dixQ5deeqlCQkJUt25dvfzyy5Kk1atXq2fPngoODlaDBg00ffp0t/Wf6DUKnnvuOXXu3FmRkZEKDAxUhw4dNHPmzFLzHeszL3kO97F+d926dVObNm3KrKNZs2bq06ePpMLvj+LvkGP58ssvlZ+fr7vvvtutzrvuukv//POPlixZctx1HGn69OkKDg7WFVdc4WpLT0+XJMXGxrrNW6dOHUlSYGDgMdcZFBSk6OjoMofYX3TRRVq0aJEOHjx40rUCAABUFgTu05SYmKhOnTq5nRv53XffKTU1tcxhm8XDK9977z0ZY076/TIyMpSRkaGoqKgTXuaqq66SZVn67LPPXG3Tp09X8+bN1b59+xNez4EDB9S3b1+1bdtWL7zwgnr06OGaNmXKFE2ePFm33367nn/+edWuXVs//fST+vTpo71792rMmDEaMWKEFi9erPPPP7/M8DpgwABlZWXp6aefPqULjxUHkcjIyKPOcyI1XXXVVbrhhhskSZMmTdL777+v999/X9HR0Udd7+LFixUZGen6/R4pPT1d+/fvL/U4ch9YtGiR7r77bl1//fWaMGGCcnJydPXVV+vAgQMnXNvPP/+sBx98UNddd53+97//KTExUWvXrlWXLl20atUqPfLII3r88ce1detWde/eXUuXLi1V77333qv169drzJgxGjRokKZNm6b+/fu76h04cKAOHDigH374wW255ORk/fzzz7r55puP+lnFxMQoMDBQX3/99SmHKYfDob59+yohIUETJkxQYmKi7r33Xk2dOlUXX3yxOnbsqPHjxys0NFSDBg067kGYsvzvf/9Tu3bt9N///ldPP/20fH19NWDAAH3zzTel5i3rMz/SsX53AwcO1J9//qk1a9a4LbNs2TJt2rTJ9XleeOGFuvDCC49b+4oVKxQcHKwzzzzTrf2cc85xTT8Z+/bt0+zZs9W/f38FBwe72hs3bqx69erp+eef19dff61//vlHv/32m+688041bNiwzO/AtLQ07d+/Xxs2bNBjjz2mNWvWFxXl7wAAZrVJREFUlLlNHTp0kDFGixcvPqlaAQAAKhXvdrBXXcVDypctW2ZeeuklExoaarKysowxxgwYMMD06NHDGGNKDSnPysoyzZo1M5JMgwYNzJAhQ8zbb799wsNon3zySSPJzJkz57jzFg8pN8aYa665xlx44YXGmMIhr3FxcWbs2LGuYdclh5QXD4OeO3euq61bt25Gknnttdfc3qN4+bCwMLN37163aW3btjUxMTHmwIEDrrZVq1YZm81mBg0a5GobPXq0kWRuuOGGE/oMiut75513zL59+8zu3bvNN998YxITE41lWWbZsmVutZUcUn6iNZ3s0N8LLrjAdOjQ4ai1Hu2RlJTkmleS8ff3N3/99ZdbbZLM5MmTT6g2ScZms5m1a9e6tffv39/4+/ubv//+29W2e/duExoaarp27epqK96vO3ToYPLy8lztEyZMMJLMl19+aYwp3Ifq1atnrrvuOrf3mThxorEsy2zZsuWYn9cTTzxhJJng4GDTt29f89RTT5k//vij1Hxl/Q4HDx7sduqAMcYcOnTIBAYGGsuyzEcffeRq37Bhg5FkRo8e7Wora/8ua0h58f/PxfLy8kzLli1Nz5493dqP9pkXTyv53kf73aWkpJiAgADz6KOPurXff//9Jjg42HWKRYMGDU5o6Hu/fv1Mo0aNSrVnZmYaSWbkyJHHXUdJkydPNpLMt99+W2ra0qVLTePGjd326w4dOrjt2yX16dPHNZ+/v7+54447yhyCv3v3biPJjB8//qRqBQAAqEzo4S4H1157rbKzszVr1iylp6dr1qxZZQ7Vlv6/vTuPj+ls/zj+nZDElgXNhpSUlloisSdVS+3UvrS09pbuamnRKsXT0hXddCMoSrWKbnZaKtSWJ9WidkqQFoktCZnz+yNP5mckYoaZzCT5vF+vecmcc8851zlzmeSa+z73yRhiuWXLFsvQ0lmzZmngwIEKCQnRs88+q9TU1Bvu55dfftH48ePVo0cPy/BpW/Xq1Uvr16+39ECePHnyhjHeiLe3t/r375/tuq5du1r1siYkJCguLk79+vVTqVKlLMvDw8PVokUL/fjjj1m28cQTT9gVz4ABAxQQEKAyZcqoXbt2unjxombPnq06depk2/5WYrLVv//+q5IlS95w/dixY7Vq1aosj2vjkKTmzZurYsWKVrH5+vraNft648aNVbVqVcvz9PR0rVy5Up06ddJdd91lWR4SEqJevXpp48aN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7dqpcOHCWr16tQYNGpRl/YYNG7K9Ljk8PFzh4eF2HYO9sUnSf/7zH61atUoNGzbUU089pcKFC+uTTz5Ramqq3nzzzSzt09LS1KxZM/Xo0UN79+7VRx99pIYNG2YZOhwQEKDWrVtr0aJF8vf3V7t27W4a/6VLlxQdHa0GDRqodevWCg0N1blz57RkyRJt2LBBnTp1sirUXKFdu3Z699131bp1a/Xq1UunT5/Whx9+qEqVKik+Pv6Wt3uz9y4yMlLVq1fXokWLdO+992a5bV/m7bNuNnFauXLl9Pzzz+utt97SlStXVLduXcv5nTdvnlWv+ejRozV79mwdOnQoywiGzEtcshtOLknt27dXtWrVNGHCBB05ckQNGjTQ/v379cEHHygkJEQDBw60tG3Tpo3KlSun+vXrKzAwUEePHlVMTIxOnDihhQsXZtn2qlWrdN999+V4mz8AAAB3R8Gdy7p27arz589r5cqVWrt2rc6cOaOSJUuqXr16Gj58uNW9rePi4iRJM2fO1MyZM622U758ebsL7tzWvHlzLV++XOPGjdPYsWPl6empxo0b64033nDIJFPOjKlu3bqaOHGiPv74Yy1fvlxms1mHDh26YVEbFBSktm3b6quvvsq24H7vvfeyfd24cePsLrjtjU2SqlWrpg0bNmj06NGaNGmSzGaz6tevr7lz56p+/fpZ2n/wwQeaN2+exo4dqytXrqhnz5567733sh0S3adPH33//ffq0aNHtjNjX8/f31+fffaZfvjhB8XExOjkyZMqVKiQKleurLfeekvPPfecXefDGR544AHNmDFDkydP1vPPP6+wsDC98cYbOnz48G0V3La8d3369NGLL754S5OlXWvy5MkqWbKkPvnkE82aNUt333235s6da/MlL2azWQsWLFCtWrVuOOLAy8tLGzZs0MSJE/XDDz/oyy+/lI+Pjzp16qTXX3/d6vrrAQMGaMGCBZoyZYrOnTunkiVLqkGDBpo/f77uv/9+q+0mJSVp5cqVVj30AAAAeZHJsHc2IQDZ2rBhg5o0aaI9e/bc9vB/V5k1a5b69++vrVu33vD2atdbunSpOnXqpF9++SVL4QT7TZs2TUOHDtXhw4eznbW+IJg6darefPNNHThwINcmAQQAAHAGruEGHOT+++9Xy5Ytsx2inZ999tlnuuuuu9SwYUNXh5LnGYahGTNmqHHjxgW22L5y5YreffddjRkzhmIbAADkeQwpBxwoc9KvgmDBggWKj4/XDz/8oGnTpnG/5Ntw8eJFLVu2TOvWrdPvv/+upUuXujokl/H09NTRo0ddHQYAAIBDUHADuCU9e/ZUiRIlNHDgQD311FOuDidPS0xMVK9eveTv76+XXnopy+R0AAAAyJu4hhsAAAAAACfgGm4AAAAAAJyAghsAAAAAACfgGm47mc1mnThxQj4+PkwSBQAAAAD5iGEYOn/+vMqUKSMPj9vvn6bgttOJEycUGhrq6jAAAAAAAE5y7NgxlStX7ra3Q8FtJx8fH0kZb4Cvr6+Lo3FvZrNZiYmJCggIcMi3Q4C9yEG4GjkId0AewtXIQbgDW/MwOTlZoaGhlrrvdlFw2ylzGLmvry8F902YzWalpKTI19eXD1e4BDkIVyMH4Q7IQ7gaOQh3YG8eOury4TyV8b/88ovat2+vMmXKyGQyacmSJVbrDcPQ2LFjFRISoqJFi6p58+bat2+fVZszZ87okUceka+vr/z9/TVw4EBduHAhF48CAAAAAFAQ5KmC++LFi6pZs6Y+/PDDbNe/+eabeu+99/Txxx9ry5YtKl68uFq1aqWUlBRLm0ceeUR//PGHVq1ape+//16//PKLBg0alFuHAAAAAAAoIPLUkPI2bdqoTZs22a4zDENTp07VmDFj1LFjR0nSnDlzFBQUpCVLlujhhx/W7t27tXz5cm3dulV16tSRJL3//vtq27at3n77bZUpUybXjgUAgIIiPT1dV65ccXUYBZbZbNaVK1eUkpLCcF64BDkId3B9Hnp6eqpQoUJO32+eKrhzcujQIZ08eVLNmze3LPPz81P9+vUVGxurhx9+WLGxsfL397cU25LUvHlzeXh4aMuWLercuXOW7aampio1NdXyPDk5WVLGG2Y2m514RHmf2WyWYRicJ7gMOQhXIwczRqcdP368QJ8Dd2A2m3X+/HlXh4ECjByEO7g2Dz08PFS2bFkVL148SxtHyjcF98mTJyVJQUFBVsuDgoIs606ePKnAwECr9YULF1apUqUsba43adIkjR8/PsvyxMREq6Hq7iIlRRo0yF+S9Omn51SkiOtiMZvNSkpKkmEYfJsJlyAH4WoFPQfNZrPOnDmjEiVKqFSpUg6bgAb2yfzSx8PDg/cALkEOwh1cm4dSxtxeR44cUalSpax+Rzv6i6F8U3A7y+jRozVs2DDL88xp4gMCAtxylvKUFMnLK+ODLDAw0OUFt8lk4hYQcBlyEK5W0HMwJSVF586dU2BgoIoWLerqcAq0K1euyNPT09VhoAAjB+EOrs3DwoUL69KlS/L391eRa4qmIg4uoPJNwR0cHCxJOnXqlEJCQizLT506pYiICEub06dPW73u6tWrOnPmjOX11/P29pa3t3eW5R4eHm75x5OHh5T5xaGHh0muDtFkMrntuULBQA7C1QpyDmb2ZtGr5VqGYVjOP+8DXIEchDu4Pg+v/R117e9oR/++zje//cPCwhQcHKw1a9ZYliUnJ2vLli2KioqSJEVFRencuXPavn27pc3atWtlNptVv379XI8ZAADkrsWLF6t27dqKiIhQlSpV9MADD+Ta9eWHDx+Wv7+/3a979dVXZTKZ9O2331qWGYahsLAwq+3ldGyPPfaYKleurJo1a+q+++7T1q1bb/dwAAA2yFM93BcuXND+/fstzw8dOqS4uDiVKlVKd955p55//nn95z//0d13362wsDC98sorKlOmjDp16iRJuvfee9W6dWs9/vjj+vjjj3XlyhU988wzevjhh5mhHACAfC4hIUGDBg3S9u3bVb58eUnSjh078kSPW+3atTVz5kzLBK9r1qzRHXfcobNnz0q6+bF17NhRn3/+uTw9PfX999+re/fuOnz4sEuOBQAKkjzVw71t2zZFRkYqMjJSkjRs2DBFRkZq7NixkqQXX3xRzz77rAYNGqS6devqwoULWr58udU4/Hnz5qlKlSpq1qyZ2rZtq4YNG+rTTz91yfEAAIDcc+rUKRUqVEilSpWyLKtVq5alKB0xYoTq1q2riIgINWrUSHv37rW0M5lMeu2111S/fn1VqFBBS5Ys0aRJk1SnTh3dfffdWr9+vaT/78UeMWKEwsPDVa1aNa1evTrbeLZu3aoHHnhAderUUWRkpBYtWnTD2Bs2bKgDBw5YJnmdOXOmBgwYYPOxtW/fXoULZ/SzNGjQQMePH9fVq1ftOX0AgFuQp3q4mzRpIsMwbrjeZDJpwoQJmjBhwg3blCpVSvPnz3dGeAAA4CacdYMPW+a4CQ8PV8OGDVW+fHk1btxY0dHR6tWrl8qWLStJGjlypN5++21J0oIFCzRkyBAtX77c8voSJUpoy5YtWrNmjTp27KgPPvhA27Zt06JFi/TCCy9YhmknJSXp3nvv1dtvv63NmzerQ4cOOnDggFUs586d06BBg/Tjjz8qJCRE//zzj2rVqqXo6GhLPNd79NFHNXv2bA0ePFhbt27Vf/7zH40ePdqmY7vWtGnT1LZtW0sBDgBwHj5pAQBArune3Tnb/e67m7fx8PDQN998oz179ujnn3/WTz/9pNdee03btm1TpUqVtGrVKr3//vs6f/685ZZm13rooYckSXXq1NHFixf18MMPS5Lq1aunffv2WdoVLlxY/fr1k5TRm1ymTBnt3LlTd955p6XNpk2bdPDgQbVp08ZqH3v37r1hwd23b1+1aNFCJUqUUI8ePbJM8nOjY6tYsaKl3dy5c/XVV1/pl19+ufkJAwDcNgpuAABQoFSpUkVVqlTR4MGD1bp1ay1btkzdunXTM888o61bt6pixYqKj49Xo0aNrF6XeYlaoUKFsjy/2fDs668TNwxD1apV06ZNm2yOu2zZsipfvrzGjx9/w9dld2xDhw6VJC1cuFDjx4/XmjVrFBQUZPN+AQC3joIbAADkmhwuU3a648eP6/Dhw7rvvvskSWfPntWhQ4dUsWJFJSUlydPTUyEhITIMQx988MEt7+fq1av64osv1K9fP/322286ceKEIiIi9O+//1raREdH69ChQ1q9erWaN28uSYqLi1PVqlXl5eV1w21PnDhRO3bsUKVKlawmPcvp2CRp0aJFGjdunFavXm3V0w4AcC4KbgAAkGtsudbaWa5evaoJEybo0KFDKlasmK5evaq+ffuqY8eOkqSHH35Y1apVU+nSpS13OLkVfn5+2rVrl2rWrKmrV69q/vz58vHxsSq4S5YsqR9++EEjRozQ8OHDdeXKFd15551asmRJjtuuU6eO6tSpY9exGYahvn37Kjg42HKsUsZM56VLl77l4wQA3JzJyGkWMmSRnJwsPz8/JSUlydfX19XhZJGS8v/Xxy1a5No/bMxms06fPq3AwECH30AesAU5CFcr6DmYkpKiQ4cOKSwszOqOIfnZ4cOHFRERoXPnzrk6FAvDMHT16lUVLlw4T9wCDfkPOQh3cH0e3uh3lKPrvYL32x8AAAAAgFxAwQ0AAOAgFSpUcKvebQCAa1FwAwAAAADgBBTcAAAAAAA4AQU3AAAAAABOQMENAAAAAIATUHADAAAAAOAEFNwAAKDAqFChguLi4rIsf+yxx7Ru3TpJUr9+/TR16tTcDew669evl8lk0pAhQ6yW9+3bVyaTyXIMv//+ux544AHVrFlT1atXV926dbVr1y5J0nvvvafq1asrPDxctWrV0ty5c3PcZ/fu3RUbGytJevXVV/X8889nabNs2TINHTr09g/QycaOHat58+Y5fT9t27bV3r17s13XrVs3zZo1S5L0wQcf6PXXX7/hdpo0aaKwsDBNmDBBUsb93P39/S3rK1SooMqVK6tmzZqqVKmSOnbsqE2bNtkc5+7du9WuXTtVrFhRFStWVJs2bfTHH39Y1kdERFg9atSoIZPJpAULFmjZsmVZ1pctW9bqvsX79+9X9+7dFRYWpsjISNWsWVMvvPCCUlNTJUn9+/fXe++9J0maNWuW/Pz8FBkZqXvvvVc1a9bU+PHjdfnyZaWkpKhUqVKWHM50+vRpFS9eXEeOHFHx4sWVlpZmWVepUiX169fP8nzz5s268847Jd04j9evX6+iRYtaHdOyZct07tw5lS9f3vL/QMp475o2bSrDMCyvi4yMVLVq1VStWjUNGzZMZ8+etbRv0qSJlixZkmWf18fyxRdfqHz58lmONbuYZ82apU6dOllij4iIsFp/o3zJPLbHHnvMsu2AgACr4z5x4oQOHz6sQoUKWS3/+OOPJWXcuzosLEzNmjXLckybNm1S48aNdffdd+uuu+5Sz549lZCQoJMnT6pMmTJWx3bw4EGFhITo0KFDOnbsmDp06KAaNWqoRo0aioiI0Nq1a622vW7dOplMJn3xxRdZ9nutJk2aqHTp0kpKSrIsu/b/3sKFC1W1alWr85PbCrtszwAAAG7i888/t/s1V69eVeHCtv8pZW/7u+++W999953eeusteXl5KTk5Wb/++qvKli1radOzZ09NnDhRnTt3liQdO3ZM3t7ekqRq1arp119/la+vrw4dOqR69eopOjpaFStWzLKv3377TWfOnFFUVFSOMXXo0EEdOnSw+Rhc4erVq5bC1dl+/PFHm9oNGjRI9957r55++mn5+fll22bKlCmWoio7CxcutBRaixcvVtu2bbVixQrVr18/x32fOHFCjRs31tSpU9WrVy9J0pdffqkmTZooLi5OZcuWzfIl1IgRI3THHXeoW7duKly4sNV7fu7cOdWtW9dyjhMSEtSwYUO99tprWrRokSTp4sWLevfdd3X+/HlLPl6radOmlqL09OnTeuyxx/TQQw9p2bJleuSRRxQTE6N33nnH0n7OnDlq2bKlypcvr6CgIP32229q2LChjh07Jh8fH23evNnSdt26dWratGmO50SSKleunO2Xb5988on69eunuLg4/f3335o4caI2b94sk8lked3OnTslSefPn9ewYcPUrFkzbd26VYUKFbrpfqWML8M++OADrV+/XmFhYTa9xl7X5su1HnnkkSxfKB4+fFg+Pj7Zno81a9bI399f8fHxOnTokCXe+Ph4dejQQQsXLrQU42+88YaaNGminTt3aurUqerbt6+2bNmiQoUKqX///powYYLCwsL04IMPqlmzZlq2bJkk6Z9//tGlS5es9jtjxgw1a9ZMM2bMUO/evXM8Vl9fX02ePFmTJk3Ksu6hhx5S/fr1sz0XuYUebgAAkHtSUpzzuE3X90rFx8crOjpa99xzj/r27avLly9Lyuj9HjBggBo1aqTq1atLyvgDtk6dOgoPD1e7du108uRJSf/f6zRy5EjVqlVLb7/9toKDg3Xs2DHLfl566SWNHDky25iKFSumZs2aaenSpZKkBQsWqGvXrlZF+99//21VgIeGhiowMFCS1KxZM0txFxoammXf1/rkk08sxVhOru1pk6Rx48apUqVKqlu3rsaMGaMKFSpYHfu4ceNUu3ZtVapUyao4XbFihWrVqqXw8HA1btxYf/75p2VdTEyMIiIiVLNmTdWpU0eHDx+2vKZhw4aqXbu26tWrZxmRsH79elWrVk0DBw5URESEvv32W6tRCmlpaXrhhRdUvXp11axZU61bt8722EaMGKG6desqIiJCjRo1suq5jo2NVcOGDVWzZk2Fh4db3pNrR0zs2bNH0dHRqlatmjp16qTk5GTL6728vNSyZUvNnz//pufYFl26dNETTzyht99++6ZtP/roIzVp0sTq/e3Zs6eaNm2qDz74IEv7hQsX6quvvtJXX32V5Qsis9msRx55RM2aNdPAgQMlSR9++KGaNGlieS5JxYsX1yuvvKI77rjjpvEFBgZq9uzZWr16tf744w8NHDhQc+fO1ZUrVyxtYmJiLNtv2rSp1q9fLynjvW/VqpUCAwMtebJ+/XqbCu4bad26tRo3bqwRI0aob9++liIxOz4+Pvroo4/0zz//aPny5TZtf/z48Zo5c6Z++eUXpxXbjjRjxgw9/vjj6tWrl2bOnGlZ/uabb2rAgAFWPd8jR46Un5+fFixYoB49euiee+7R66+/rvfee0/FixfX448/Linr59Ydd9xhGZUgZXyp88MPP2ju3Ln6888/tX///hxjHDlypGbMmKETJ0446rAdih5uAACQe7p3d852v/vOoZvbsmWLNm/erGLFiqlTp06aMmWKXnrpJUnS9u3btXHjRvn4+EiSpk6dqoCAAEnS5MmT9eqrr1qGYyYlJalatWp64403JGX0iE2fPl2vv/66UlNTFRMTY9U7d73+/ftr4sSJ6t69u2JiYjRr1iwtXLjQsv6VV15R06ZN1aBBAzVo0EDdunVTZGRklu2sWbNGZ8+eVd26dbPdz/r16+0eKv7DDz/om2++0c6dO1WiRAkNGDDAan1SUpLCw8M1fvx4LV++XEOGDFHbtm11+vRp9erVS+vXr1eNGjU0b948devWTX/88Yd+/vlnTZgwQZs2bVJISIil1+vgwYN69dVXtWLFCvn6+mr//v26//77LUXW7t279dFHH2nGjBmW2DJNmjRJf/31l7Zv3y5vb28lJiZmezwjR460FLALFizQkCFDtHz5cp05c0adOnXS119/rfvvv19ms1nnzp3L8vrevXvriSee0MCBA/X777+rTp06VkVuVFSUli1bpieffNKu83wj9evXt/QQbtu2TWPHjs22x33Hjh1q0aJFluVRUVFauXKl1bLff/9dTz31lJYvX27J6WuNGzdOZ86c0bfffnvT7dujZMmSuvvuu/XHH3+oR48eKleunH744Qd16tRJmzdv1rlz59SmTRtJGQV3TEyMxowZo3Xr1qlHjx7y9PTUunXr9Oijj+rXX3/VZ599dtN97t2716rXc/v27ZYe6nfeeUd33XWXatSoocGDB+e4HU9PT0VGRuqPP/5Qu3btcmw7d+5cBQQEKDY29raGOF8f+7VD7DM99NBDKlq0qKSM9y1zFMy8efMsX1hERkYqJiZGUsZn07Xb/O6771S8eHEtX75c06dP19GjR9WuXTuNHz9eHh4e2rFjh7p27Zplv1FRUdq+fbsGDBigDz/8ULVq1VJ6erp+++03S5uRI0dq4MCBmjZtmho0aKCOHTuqUaNGlvXz589Xq1atFBwcrEcffVQzZ87M8ZKM4OBgDR48WOPGjbPpvc9t9HADAABcp0ePHvLx8VGhQoU0cOBArV692rKue/fulmJbyvjjsE6dOqpevbo+//xzq2GZnp6eevTRRy3Pn3rqKc2ePVupqalatGiR6tWrp/Lly98wjujoaB09elQrVqxQoUKFVLlyZav1w4cP18GDB/XYY4/pzJkzuv/++60KcimjiHr88ce1YMECFS9ePNv9/P333woKCrLp3GRas2aN5VyYTCarHk5JKlKkiLp06SIp44/wAwcOSMr4MiPz2k0pY4TAiRMndPz4cf3www/q3bu3QkJCJGX08hcrVkzLly/X/v371ahRI0VERKhbt27y8PDQ0aNHJUl33XWXGjdunG2c33//vYYMGWIZ2pxdISlJq1atUlRUlKpXr64JEyZY3sfY2FhVrlxZ999/vyTJw8NDpUqVsnptcnKy4uLiLNcS16hRQw0bNrRqExwcrL///jvnk2oHwzAsP9epU8fm4e3XyizIJOns2bPq3Lmz3nrrrWy/mFm6dKlmzJihb775Rl5eXjfc5pQpUxQREaE777zT5l5fyfp4Bg4caOlNnTlzpvr27Wsphps2barY2FilpaVp48aNatiwoRo3bqz169dr69atCgoKsuotvZHMIeWZj2uHg2/YsEHe3t46ePCg1UgFW2LPSZ06dZScnKzvv//+hm0yh67ntPz62LN77xcuXGhZn1lsSxn/3zKXZxbbkixDyjMfoaGhmjdvntq0aSN/f3+Fh4crKChIK1assOlYJalUqVLq3bu3OnXqZPk/LWWMsDh69KiGDx8uSerYsaPeeusty/oZM2ZYvsAbMGCAZs+erfT09Bz39cILL+j777/Xnj17bI4vt9DDDQAAcs//rvHMa679Y7dEiRKWnzdu3Kj33ntPsbGxCgwM1LJlyzR27FjL+mLFisnD4//7N8qWLatGjRpp4cKFmj59uk3XGvfp00ePPvqoJk+enO36oKAg9ezZUz179lT58uU1b948PfTQQ5KkP//8U+3bt9enn36apQC8VrFixZRym0Pzry8UvL29LcsKFSp00z+Yc2IYhlq0aJHtkOzjx49bvSe34ujRo3rmmWe0detWVaxYUfHx8VY9brfi+vORkpJiVeDerq1bt1oua8hJrVq1FBsbm2UEQ2xsrKKjoyVlDBXv1auXWrZsmWWkgpTRozpw4EAtWbJEZcqUsVoXGRlp1Xs5dOhQDR06VE2aNLE5p86ePav9+/dbjqdXr14a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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# View an example consensus mass feature with MS2 mirror plot\n", + "lcms_collection.plot_cluster(\n", + " cluster_id=2, \n", + " to_plot=[\"EIC\", \"MS1\", \"MS2_mirror\"], # Default is MS2 without a mirror\n", + " molecular_metadata=molecular_metadata,\n", + " spectral_library=spectral_library\n", + " # Needs the molecular_metadata object generated alongside the ms2 database above to plot the library match\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "b7554b67", + "metadata": {}, + "source": [ + "## Step 9: Export Collection Results\n", + "\n", + "Export the collection to HDF5 for later reloading, and save tables to CSV." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7dc0e769", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ Collection exported to tutorial_collection_data/collection_export.hdf5\n", + "\n", + "✓ Tables exported to CSV\n", + " - pivot_intensity.csv\n", + " - cluster_representatives.csv\n", + " - feature_annotations.csv\n" + ] + } + ], + "source": [ + "# Ensure output directory exists\n", + "processed_folder.mkdir(parents=True, exist_ok=True)\n", + "\n", + "# Export collection to HDF5\n", + "collection_save_path = processed_folder / \"collection_export\"\n", + "exporter = LCMSCollectionExport(\n", + " out_file_path=str(collection_save_path),\n", + " mass_spectra_collection=lcms_collection\n", + ")\n", + "exporter.export_to_hdf5(overwrite=True, save_parameters=True)\n", + "\n", + "print(f\"✓ Collection exported to {collection_save_path}.hdf5\")" + ] + }, + { + "cell_type": "markdown", + "id": "375abdcc", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "This tutorial demonstrated:\n", + "\n", + "1. ✓ **Sample Preparation**: Processing and exporting individual LC-MS samples to HDF5\n", + "2. ✓ **Collection Loading**: Loading multiple samples as a collection with metadata\n", + "3. ✓ **RT Alignment**: Aligning retention times across samples\n", + "4. ✓ **Consensus Features**: Clustering features across samples to identify common entities\n", + "5. ✓ **Gap Filling**: Searching for missing features in raw data\n", + "6. ✓ **Pivot Tables**: Creating sample × feature matrices for comparison\n", + "7. ✓ **Cluster Representatives**: Identifying best representative features\n", + "8. ✓ **Molecular Annotations**: Adding MS1 formula search and MS2 spectral matching\n", + "9. ✓ **Export**: Saving collection data\n", + "\n", + "### Key Takeaways\n", + "\n", + "- **Collections enable cross-sample analysis** - Identify features present across multiple samples and detect missing features\n", + "- **Gap filling improves coverage** - Recover features that were missed during initial peak picking\n", + "- **Consensus features reduce redundancy** - Group equivalent features across samples\n", + "- **Representative features simplify analysis** - Work with one feature per cluster instead of all variants\n", + "- **Integrated pipeline is efficient** - `process_consensus_features()` combines multiple operations in one pass and can leverage multicore processing" + ] + }, + { + "cell_type": "markdown", + "id": "42cb004b", + "metadata": {}, + "source": [ + "## Cleanup\n", + "\n", + "Remove tutorial data files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3fb1fb6f", + "metadata": {}, + "outputs": [], + "source": [ + "# Clean up tutorial data\n", + "if processed_folder.exists():\n", + " shutil.rmtree(processed_folder)\n", + " print(f\"✓ Cleaned up tutorial data from {processed_folder}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/support_code/nmdc/metabolomics/metabolomics_collection.py b/support_code/nmdc/metabolomics/metabolomics_collection.py index 1d2f82386..13997d221 100644 --- a/support_code/nmdc/metabolomics/metabolomics_collection.py +++ b/support_code/nmdc/metabolomics/metabolomics_collection.py @@ -516,7 +516,7 @@ def process_single_sample(args): perform_gap_filling=True, add_ms1=True, add_ms2=True, - molecular_formula_search=False, + molecular_formula_search=True, ms2_spectral_search=perform_ms2_search, spectral_lib=spectral_lib, molecular_metadata=molecular_metadata, @@ -569,6 +569,21 @@ def process_single_sample(args): ) print(f"Feature annotations table: {len(feature_annotations)} rows across {feature_annotations['cluster'].nunique()} clusters") + + # Plot the first mass feature with an annotation with MS2_mirror + if not feature_annotations.empty: + # Find first annotated cluster with non-null value in ref_ms_id + first_annotated_cluster = feature_annotations.loc[ + feature_annotations['ref_ms_id'].notnull(), 'cluster' + ].iloc[0] + print(f"Plotting annotated feature for cluster {first_annotated_cluster}") + lcms_collection.plot_cluster( + cluster_id=first_annotated_cluster, + to_plot=["EIC", "MS1", "MS2_mirror"], + molecular_metadata=molecular_metadata, + spectral_library=spectral_lib + ) + # Save the feature annotations table to CSV for inspection feature_annotations.to_csv("example_feature_annotations.csv", index=False) diff --git a/tests/test_lcms_collection.py b/tests/test_lcms_collection.py index 013961988..48592e00b 100644 --- a/tests/test_lcms_collection.py +++ b/tests/test_lcms_collection.py @@ -571,7 +571,8 @@ def test_lcms_collection_plot_cluster_with_ms2_mirror(lcms_collection, msp_file_ lcms_collection.plot_cluster( cluster_with_ms2, to_plot=["EIC", "MS1", "MS2_mirror"], - molecular_metadata=molecular_metadata + molecular_metadata=molecular_metadata, + spectral_library=msp_lib ) except Exception as e: pytest.fail(f"plot_cluster with MS2_mirror raised exception: {e}") diff --git a/tests/test_lcms_metabolomics.py b/tests/test_lcms_metabolomics.py index d0b532247..d7f15652a 100644 --- a/tests/test_lcms_metabolomics.py +++ b/tests/test_lcms_metabolomics.py @@ -133,7 +133,8 @@ def test_lcms_metabolomics(postgres_database, lcms_obj, msp_file_location): fig = mass_feature_to_plot.plot( to_plot=["EIC", "MS1", "MS2_mirror"], return_fig=True, - molecular_metadata=metabolite_metadata_negative + molecular_metadata=metabolite_metadata_negative, + spectral_library=msp_negative ) assert fig is not None From 95c79566a5058edccecdb03c1f3d8f5eaec21c0a Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 30 Jan 2026 18:14:07 -0800 Subject: [PATCH 132/158] Add targeted search example --- examples/README.md | 41 +- .../notebooks/LCMS_Collection_Tutorial.ipynb | 90 +- .../LCMS_Targeted_Search_Tutorial.ipynb | 857 ++++++++++++++++++ 3 files changed, 941 insertions(+), 47 deletions(-) create mode 100644 examples/notebooks/LCMS_Targeted_Search_Tutorial.ipynb diff --git a/examples/README.md b/examples/README.md index 741888385..2681104ea 100644 --- a/examples/README.md +++ b/examples/README.md @@ -60,7 +60,40 @@ Process liquid chromatography mass spectrometry data with MS2 analysis: --- -### 4. Mass_Recalibration_Tutorial.ipynb +### 4. LCMS_Targeted_Search_Tutorial.ipynb +**Finding specific compounds by m/z and retention time** + +Perform targeted searches for known compounds using CoreMS: +- Define target compounds with expected m/z and retention time values +- Configure tolerances for m/z (ppm) and RT (minutes) +- Perform selective peak picking for specific targets +- Verify recovery and measure deviations from expected values +- Associate MS2 data with target compounds +- Export results with persistent metadata labels + +**Data Format:** Thermo `.raw` files +**Recommended For:** Internal standard verification, quality control, spike-in recovery studies, targeted metabolomics +**Use Cases:** Internal standards, QC monitoring, quantitative workflows + +--- + +### 5. LCMS_Collection_Tutorial.ipynb +**Multi-sample metabolomics workflow with consensus features** + +Process multiple LC-MS samples as a collection for cross-sample comparison: +- Load and align multiple LC-MS samples +- Generate consensus mass features across samples +- Perform gap filling to find missing features +- Create pivot tables showing feature distribution +- Apply molecular annotations to collection data +- Export and visualize collection-level results + +**Data Format:** Thermo `.raw` files +**Recommended For:** Multi-sample metabolomics studies, comparative analyses, LC-MS users + +--- + +### 6. Mass_Recalibration_Tutorial.ipynb **Improving mass accuracy through calibration** Master mass recalibration techniques for high-resolution data: @@ -73,7 +106,7 @@ Master mass recalibration techniques for high-resolution data: --- -### 5. Noise_Thresholding_Methods.ipynb +### 7. Noise_Thresholding_Methods.ipynb **Comparing noise filtering approaches** Compare different noise thresholding methods for peak detection: @@ -86,7 +119,7 @@ Compare different noise thresholding methods for peak detection: --- -### 6. ResolvingPowerFilter_ICR.ipynb +### 8. ResolvingPowerFilter_ICR.ipynb **Filtering peaks based on resolving power** Compare different approaches to resolving power-based peak filtering for FT-ICR MS: @@ -100,7 +133,7 @@ Compare different approaches to resolving power-based peak filtering for FT-ICR --- -### 7. Setting_MSParameters.ipynb +### 9. Setting_MSParameters.ipynb **Configuring global parameters in CoreMS** Understand how to control data processing behavior: diff --git a/examples/notebooks/LCMS_Collection_Tutorial.ipynb b/examples/notebooks/LCMS_Collection_Tutorial.ipynb index ba626faa9..f7e823634 100644 --- a/examples/notebooks/LCMS_Collection_Tutorial.ipynb +++ b/examples/notebooks/LCMS_Collection_Tutorial.ipynb @@ -113,10 +113,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 30, "id": "96802b8e", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Parameters configured for LC-MS processing\n" + ] + } + ], "source": [ "# Load and configure the raw data\n", "parser = ImportMassSpectraThermoMSFileReader(str(raw_file_path))\n", @@ -174,7 +182,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "id": "b8528773", "metadata": {}, "outputs": [ @@ -221,7 +229,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 32, "id": "edca055c", "metadata": {}, "outputs": [ @@ -256,7 +264,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 33, "id": "fa186ab4", "metadata": {}, "outputs": [ @@ -291,7 +299,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 34, "id": "921ca2b2", "metadata": {}, "outputs": [ @@ -332,7 +340,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 35, "id": "3f61de41", "metadata": {}, "outputs": [ @@ -370,7 +378,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 36, "id": "820ce6ab", "metadata": {}, "outputs": [ @@ -416,7 +424,7 @@ "type": "string" } ], - "ref": "0bb52bbc-ec6b-4cf7-bb54-b63e22bcf83a", + "ref": "121888dd-9611-495d-bd37-3d8624e76158", "rows": [ [ "test_sample_01", @@ -521,7 +529,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 37, "id": "094b8400", "metadata": {}, "outputs": [ @@ -658,7 +666,7 @@ "type": "float" } ], - "ref": "b988ea91-2bd0-433b-81d1-4bd5825578f1", + "ref": "a8ed0404-b3ad-44bd-be82-8f95a5786353", "rows": [ [ "0_0", @@ -1342,7 +1350,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 38, "id": "112ccb3c", "metadata": {}, "outputs": [ @@ -1376,7 +1384,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 39, "id": "d989ed7a", "metadata": {}, "outputs": [ @@ -1420,7 +1428,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 40, "id": "4442c8d6", "metadata": {}, "outputs": [ @@ -1445,7 +1453,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 41, "id": "39a6f24b", "metadata": {}, "outputs": [ @@ -1485,7 +1493,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 42, "id": "670d8d60", "metadata": {}, "outputs": [ @@ -1663,7 +1671,7 @@ "type": "float" } ], - "ref": "edb5bbdb-057b-4453-988d-ae9db765fef3", + "ref": "fac284ed-ac24-4579-a1e4-aa89ecb9cc3b", "rows": [ [ "0", @@ -2436,7 +2444,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 43, "id": "a8a9d7d8", "metadata": {}, "outputs": [ @@ -2458,7 +2466,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 44, "id": "ed583149", "metadata": {}, "outputs": [ @@ -2503,7 +2511,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 45, "id": "f88226f6", "metadata": {}, "outputs": [ @@ -2520,7 +2528,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|█████████████████████████████████████████| 3/3 [00:19<00:00, 6.48s/sample]" + "100%|█████████████████████████████████████████| 3/3 [00:19<00:00, 6.57s/sample]" ] }, { @@ -2570,7 +2578,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 46, "id": "9638f884", "metadata": {}, "outputs": [ @@ -2717,7 +2725,7 @@ "type": "float" } ], - "ref": "1e412366-73f0-45fd-ac2c-b599482380ce", + "ref": "58d93683-7cbb-4648-88a5-111fffba8639", "rows": [ [ "2_c0_0_i", @@ -3421,7 +3429,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 47, "id": "4ea2a311", "metadata": {}, "outputs": [ @@ -3460,7 +3468,7 @@ "type": "float" } ], - "ref": "f086952c-26a6-4191-88dd-d59007f597e2", + "ref": "463e6594-04f9-41a8-852d-c6757d4a6eee", "rows": [ [ "0", @@ -3664,7 +3672,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 48, "id": "e9d00373", "metadata": {}, "outputs": [ @@ -3704,7 +3712,7 @@ "type": "string" } ], - "ref": "584c8115-0940-4bc3-988d-b27467425d17", + "ref": "01bb4e2b-3f04-499d-9ae5-50d40a07cff9", "rows": [ [ "0", @@ -3908,7 +3916,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 49, "id": "e40ba01c", "metadata": {}, "outputs": [ @@ -3945,7 +3953,7 @@ "type": "float" } ], - "ref": "d6728a96-3138-4434-96bf-2f394e8f272e", + "ref": "8bc96298-1496-4cd6-994f-f65296c94f72", "rows": [ [ "0", @@ -4146,7 +4154,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 50, "id": "f11922ae", "metadata": {}, "outputs": [ @@ -4317,7 +4325,7 @@ "type": "integer" } ], - "ref": "fa40d0ca-4e40-4fc7-b723-3b45790b0ad1", + "ref": "2bdbf7e2-0a2b-49fb-b578-43255ee6e13e", "rows": [ [ "0", @@ -5042,7 +5050,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 51, "id": "a52580c8", "metadata": {}, "outputs": [ @@ -5058,7 +5066,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|█████████████████████████████████████████| 3/3 [00:12<00:00, 4.22s/sample]\n" + "100%|█████████████████████████████████████████| 3/3 [00:12<00:00, 4.32s/sample]\n" ] }, { @@ -5073,7 +5081,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|███████████████████████████████████████| 3/3 [00:00<00:00, 1632.02sample/s]" + "100%|███████████████████████████████████████| 3/3 [00:00<00:00, 1592.37sample/s]" ] }, { @@ -5140,7 +5148,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 52, "id": "286c4eec", "metadata": {}, "outputs": [ @@ -5256,7 +5264,7 @@ "type": "string" } ], - "ref": "76b9e5ee-eaad-4392-860c-c58521ab3109", + "ref": "49ab3209-021b-49e5-8a89-30f8d66d0992", "rows": [ [ "1", @@ -5613,6 +5621,7 @@ ], "source": [ "# View an example consensus mass feature with MS2 mirror plot\n", + "# Note that for this example, the EICs overlap because the samples are replicates.\n", "lcms_collection.plot_cluster(\n", " cluster_id=2, \n", " to_plot=[\"EIC\", \"MS1\", \"MS2_mirror\"], # Default is MS2 without a mirror\n", @@ -5634,7 +5643,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 54, "id": "7dc0e769", "metadata": {}, "outputs": [ @@ -5642,12 +5651,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "✓ Collection exported to tutorial_collection_data/collection_export.hdf5\n", - "\n", - "✓ Tables exported to CSV\n", - " - pivot_intensity.csv\n", - " - cluster_representatives.csv\n", - " - feature_annotations.csv\n" + "✓ Collection exported to tutorial_collection_data/collection_export.hdf5\n" ] } ], diff --git a/examples/notebooks/LCMS_Targeted_Search_Tutorial.ipynb b/examples/notebooks/LCMS_Targeted_Search_Tutorial.ipynb new file mode 100644 index 000000000..d704c4180 --- /dev/null +++ b/examples/notebooks/LCMS_Targeted_Search_Tutorial.ipynb @@ -0,0 +1,857 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a93184eb", + "metadata": {}, + "source": [ + "# LC-MS Targeted Search\n", + "## Finding Specific Compounds by m/z and Retention Time\n", + "\n", + "This notebook demonstrates how to perform targeted searches in LC-MS data using CoreMS. Targeted searches allow you to specifically look for compounds with known m/z and retention time values, such as internal standards or specific metabolites of interest.\n", + "\n", + "### Workflow Overview\n", + "1. Load LC-MS data from Thermo RAW files\n", + "2. Configure parameters for targeted search\n", + "3. Define target compounds (m/z and retention time)\n", + "4. Perform targeted peak picking\n", + "5. Integrate and characterize target peaks\n", + "6. Associate MS2 data with targets\n", + "7. Visualize and export results\n", + "\n", + "### Use Cases\n", + "- **Internal Standards**: Verify presence and quantify internal standards\n", + "- **Known Metabolites**: Target specific metabolites in complex samples\n", + "- **Quality Control**: Monitor specific compounds across batches\n", + "- **Spike-in Recovery**: Track spiked compounds through sample processing\n", + "\n", + "### Data Format\n", + "This tutorial uses Thermo Fisher RAW format LC-MS data files. The targeted search approach is ideal when you have prior knowledge of expected compounds." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "506eb64c", + "metadata": {}, + "outputs": [], + "source": [ + "# Import required packages\n", + "import numpy as np\n", + "import shutil\n", + "from pathlib import Path\n", + "\n", + "from corems.mass_spectra.input.rawFileReader import ImportMassSpectraThermoMSFileReader\n", + "from corems.mass_spectra.output.export import LCMSMetabolomicsExport\n", + "from corems.encapsulation.factory.parameters import LCMSParameters" + ] + }, + { + "cell_type": "markdown", + "id": "a1e239d5", + "metadata": {}, + "source": [ + "## Step 1: Load LC-MS Data\n", + "\n", + "First, we'll load the raw data file and create an LCMS object." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cdfa0d60", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded data file: Blanch_Nat_Lip_C_12_AB_M_17_NEG_25Jan18_Brandi-WCSH5801.raw\n", + "Number of scans: 6840\n", + "Retention time range: 0.01 - 34.02 minutes\n" + ] + } + ], + "source": [ + "# Point to the data file location\n", + "file_path = '../../tests/tests_data/lcms/Blanch_Nat_Lip_C_12_AB_M_17_NEG_25Jan18_Brandi-WCSH5801.raw'\n", + "\n", + "# Create parser for Thermo RAW files\n", + "parser = ImportMassSpectraThermoMSFileReader(file_path)\n", + "\n", + "# Instantiate LCMS object with MS1 data\n", + "lcms_obj = parser.get_lcms_obj(spectra=\"ms1\")\n", + "\n", + "print(f\"Loaded data file: {Path(file_path).name}\")\n", + "print(f\"Number of scans: {len(lcms_obj.scan_df)}\")\n", + "print(f\"Retention time range: {lcms_obj.scan_df.scan_time.min():.2f} - {lcms_obj.scan_df.scan_time.max():.2f} minutes\")" + ] + }, + { + "cell_type": "markdown", + "id": "8f6936f3", + "metadata": {}, + "source": [ + "## Step 2: Configure Processing Parameters\n", + "\n", + "Set parameters for data processing. For targeted searches, we can use faster settings since we're only looking for specific features." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f23ca31d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ Parameters configured\n" + ] + } + ], + "source": [ + "# Set parameters to defaults for reproducible processing\n", + "lcms_obj.parameters = LCMSParameters(use_defaults=True)\n", + "\n", + "# Configure persistent homology parameters for peak picking\n", + "lcms_obj.parameters.lc_ms.peak_picking_method = \"persistent homology\"\n", + "lcms_obj.parameters.lc_ms.ph_inten_min_rel = 0.0005\n", + "lcms_obj.parameters.lc_ms.ph_persis_min_rel = 0.05\n", + "lcms_obj.parameters.lc_ms.ph_smooth_it = 0\n", + "\n", + "# Configure MS1 parameters\n", + "ms1_params = lcms_obj.parameters.mass_spectrum['ms1']\n", + "ms1_params.mass_spectrum.noise_threshold_method = \"relative_abundance\"\n", + "ms1_params.mass_spectrum.noise_threshold_min_relative_abundance = 0.1\n", + "ms1_params.mass_spectrum.noise_min_mz = 0\n", + "ms1_params.mass_spectrum.min_picking_mz = 0\n", + "ms1_params.mass_spectrum.noise_max_mz = np.inf\n", + "ms1_params.mass_spectrum.max_picking_mz = np.inf\n", + "ms1_params.ms_peak.legacy_resolving_power = False\n", + "\n", + "# Configure MS2 parameters (same as MS1 for this dataset)\n", + "ms2_params = ms1_params.copy()\n", + "lcms_obj.parameters.mass_spectrum['ms2'] = ms2_params\n", + "\n", + "print(\"✓ Parameters configured\")" + ] + }, + { + "cell_type": "markdown", + "id": "8ccd61d8", + "metadata": {}, + "source": [ + "## Step 3: Define Target Compounds\n", + "\n", + "Now we'll define the specific compounds we want to find. For each target, we need:\n", + "- **m/z value**: The expected mass-to-charge ratio\n", + "- **Retention time**: The expected elution time in minutes\n", + "- **Tolerances**: How much deviation to allow in m/z (ppm) and RT (minutes)\n", + "- **Type label**: A descriptive label for these targets (e.g., \"internal standard\")\n", + "\n", + "In this example, we're targeting two compounds that we know exist in the data." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "06e07fb3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Targeting 2 compounds:\n", + " 1. m/z = 301.2166, RT = 8.8956 min\n", + " 2. m/z = 698.6289, RT = 23.8168 min\n" + ] + } + ], + "source": [ + "# Define target compounds\n", + "# These values are known to exist in our test dataset\n", + "target_mz_list = [\n", + " 301.2166, # Target compound 1\n", + " 698.6289 # Target compound 2\n", + "]\n", + "\n", + "target_rt_list = [\n", + " 8.8956, # Expected RT for compound 1 (minutes)\n", + " 23.8168 # Expected RT for compound 2 (minutes)\n", + "]\n", + "\n", + "# Create targeted search dictionary\n", + "target_search_dict = {\n", + " \"target_mz_list\": target_mz_list,\n", + " \"target_rt_list\": target_rt_list,\n", + " \"mz_tolerance_ppm\": 5, # Allow ±5 ppm deviation in m/z\n", + " \"rt_tolerance\": 0.5, # Allow ±0.5 minute deviation in RT\n", + " \"type\": \"internal standard\" # Label for these features\n", + "}\n", + "\n", + "print(f\"Targeting {len(target_mz_list)} compounds:\")\n", + "for i, (mz, rt) in enumerate(zip(target_mz_list, target_rt_list), 1):\n", + " print(f\" {i}. m/z = {mz:.4f}, RT = {rt:.4f} min\")" + ] + }, + { + "cell_type": "markdown", + "id": "e4aa55b4", + "metadata": {}, + "source": [ + "## Step 4: Perform Targeted Peak Picking\n", + "\n", + "Run the targeted search to find mass features that match our targets. The search will only look for features within the specified m/z and RT tolerances." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "7ec2667a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found 5 initial mass features\n", + "✓ Found 5 mass features matching target criteria\n" + ] + } + ], + "source": [ + "# Perform targeted search\n", + "lcms_obj.find_mass_features(\n", + " targeted_search=True,\n", + " target_search_dict=target_search_dict\n", + ")\n", + "\n", + "print(f\"✓ Found {len(lcms_obj.mass_features)} mass features matching target criteria\")" + ] + }, + { + "cell_type": "markdown", + "id": "31afbd49", + "metadata": {}, + "source": [ + "## Step 5: Integrate Mass Features\n", + "\n", + "Integrate the chromatographic peaks to calculate areas, heights, and other quantitative metrics." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "28e211c9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ Integrated 3 mass features\n", + "\n", + "Mass features after integration:\n" + ] + }, + { + "data": { + "application/vnd.microsoft.datawrangler.viewer.v0+json": { + "columns": [ + { + "name": "mf_id", + "rawType": "int64", + "type": "integer" + }, + { + "name": "type", + "rawType": "object", + "type": "string" + }, + { + "name": "scan_time", + "rawType": "float64", + "type": "float" + }, + { + "name": "mz", + "rawType": "float64", + "type": "float" + }, + { + "name": "apex_scan", + "rawType": "float64", + "type": "float" + }, + { + "name": "start_scan", + "rawType": "int64", + "type": "integer" + }, + { + "name": "final_scan", + "rawType": "int64", + "type": "integer" + }, + { + "name": "intensity", + "rawType": "float64", + "type": "float" + }, + { + "name": "persistence", + "rawType": "float64", + "type": "float" + }, + { + "name": "area", + "rawType": "float64", + "type": "float" + }, + { + "name": "tailing_factor", + "rawType": "float64", + "type": "float" + }, + { + "name": "dispersity_index", + "rawType": "float64", + "type": "float" + }, + { + "name": "normalized_dispersity_index", + "rawType": "float64", + "type": "float" + }, + { + "name": "noise_score", + "rawType": "float64", + "type": "float" + }, + { + "name": "noise_score_min", + "rawType": "float64", + "type": "float" + }, + { + "name": "noise_score_max", + "rawType": "float64", + "type": "float" + }, + { + "name": "monoisotopic_mf_id", + "rawType": "object", + "type": "string" + }, + { + "name": "isotopologue_type", + "rawType": "object", + "type": "string" + }, + { + "name": "mass_spectrum_deconvoluted_parent", + "rawType": "object", + "type": "string" + }, + { + "name": "ms2_scan_numbers", + "rawType": "object", + "type": "string" + } + ], + "ref": "431f0d52-3578-4fc3-a72e-2e1324b320aa", + "rows": [ + [ + "0", + "internal standard", + "8.895636666666666", + "301.21661376953125", + "1882.0", + "1828", + "2008", + "66775328.0", + "66768418.0", + "35045576.273014136", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]" + ], + [ + "1", + "internal standard", + "23.816803333333333", + "698.62890625", + "5212.0", + "5176", + "5338", + "17265106.0", + "17258196.0", + "7113439.441603203", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]" + ], + [ + "3", + "internal standard", + "23.33747", + "698.6254272460938", + "5095.0", + "5059", + "5149", + "567761.5625", + "560851.0", + "231250.28136407814", + null, + null, + null, + null, + null, + null, + null, + null, + null, + "[]" + ] + ], + "shape": { + "columns": 19, + "rows": 3 + } + }, + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
typescan_timemzapex_scanstart_scanfinal_scanintensitypersistenceareatailing_factordispersity_indexnormalized_dispersity_indexnoise_scorenoise_score_minnoise_score_maxmonoisotopic_mf_idisotopologue_typemass_spectrum_deconvoluted_parentms2_scan_numbers
mf_id
0internal standard8.895637301.2166141882.0182820086.677533e+0766768418.03.504558e+07NaNNaNNaNNaNNaNNaNNoneNoneNone[]
1internal standard23.816803698.6289065212.0517653381.726511e+0717258196.07.113439e+06NaNNaNNaNNaNNaNNaNNoneNoneNone[]
3internal standard23.337470698.6254275095.0505951495.677616e+05560851.02.312503e+05NaNNaNNaNNaNNaNNaNNoneNoneNone[]
\n", + "
" + ], + "text/plain": [ + " type scan_time mz apex_scan start_scan \\\n", + "mf_id \n", + "0 internal standard 8.895637 301.216614 1882.0 1828 \n", + "1 internal standard 23.816803 698.628906 5212.0 5176 \n", + "3 internal standard 23.337470 698.625427 5095.0 5059 \n", + "\n", + " final_scan intensity persistence area tailing_factor \\\n", + "mf_id \n", + "0 2008 6.677533e+07 66768418.0 3.504558e+07 NaN \n", + "1 5338 1.726511e+07 17258196.0 7.113439e+06 NaN \n", + "3 5149 5.677616e+05 560851.0 2.312503e+05 NaN \n", + "\n", + " dispersity_index normalized_dispersity_index noise_score \\\n", + "mf_id \n", + "0 NaN NaN NaN \n", + "1 NaN NaN NaN \n", + "3 NaN NaN NaN \n", + "\n", + " noise_score_min noise_score_max monoisotopic_mf_id isotopologue_type \\\n", + "mf_id \n", + "0 NaN NaN None None \n", + "1 NaN NaN None None \n", + "3 NaN NaN None None \n", + "\n", + " mass_spectrum_deconvoluted_parent ms2_scan_numbers \n", + "mf_id \n", + "0 None [] \n", + "1 None [] \n", + "3 None [] " + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Integrate the targeted mass features\n", + "lcms_obj.integrate_mass_features(drop_if_fail=True)\n", + "\n", + "print(f\"✓ Integrated {len(lcms_obj.mass_features)} mass features\")\n", + "print(\"\\nMass features after integration:\")\n", + "\n", + "# Display summary dataframe\n", + "mf_df = lcms_obj.mass_features_to_df()\n", + "mf_df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "82e61667", + "metadata": {}, + "source": [ + "## Step 6: Verify Target Recovery\n", + "\n", + "Check that we found features close to each of our targets and calculate the deviation from expected values." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "64f9713f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Target Recovery Summary:\n", + "----------------------------------------------------------------------\n", + "Target 1: ✓ FOUND\n", + " Expected: m/z = 301.2166, RT = 8.8956 min\n", + " Observed: m/z = 301.2166, RT = 8.8956 min\n", + " Deviation: Δm/z = 0.05 ppm, ΔRT = 0.000 min\n", + " Intensity: 6.68e+07\n", + "\n", + "Target 2: ✓ FOUND\n", + " Expected: m/z = 698.6289, RT = 23.8168 min\n", + " Observed: m/z = 698.6289, RT = 23.8168 min\n", + " Deviation: Δm/z = 0.01 ppm, ΔRT = 0.000 min\n", + " Intensity: 1.73e+07\n", + "\n" + ] + } + ], + "source": [ + "# Verify that we found all targets\n", + "print(\"Target Recovery Summary:\")\n", + "print(\"-\" * 70)\n", + "\n", + "for i, (target_mz, target_rt) in enumerate(zip(target_mz_list, target_rt_list), 1):\n", + " # Calculate deviations\n", + " mz_diff_ppm = abs(mf_df['mz'] - target_mz) / target_mz * 1e6\n", + " rt_diff = abs(mf_df['scan_time'] - target_rt)\n", + " \n", + " # Find closest match\n", + " closest_idx = (mz_diff_ppm + rt_diff * 100).idxmin() # Combined score\n", + " \n", + " if mz_diff_ppm[closest_idx] < 10 and rt_diff[closest_idx] < 1.0:\n", + " print(f\"Target {i}: ✓ FOUND\")\n", + " print(f\" Expected: m/z = {target_mz:.4f}, RT = {target_rt:.4f} min\")\n", + " print(f\" Observed: m/z = {mf_df.loc[closest_idx, 'mz']:.4f}, RT = {mf_df.loc[closest_idx, 'scan_time']:.4f} min\")\n", + " print(f\" Deviation: Δm/z = {mz_diff_ppm[closest_idx]:.2f} ppm, ΔRT = {rt_diff[closest_idx]:.3f} min\")\n", + " print(f\" Intensity: {mf_df.loc[closest_idx, 'intensity']:.2e}\")\n", + " else:\n", + " print(f\"Target {i}: ✗ NOT FOUND within tolerance\")\n", + " print()" + ] + }, + { + "cell_type": "markdown", + "id": "4fbcf21a", + "metadata": {}, + "source": [ + "## Step 7: Add MS1 and MS2 Data\n", + "\n", + "Add associated MS1 average spectra and MS2 fragmentation spectra to the mass features for further characterization." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "85ff3bd5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ Added MS1 spectra\n", + "✓ Added 8 MS2 spectra\n", + "\n", + "MS2 Data Summary:\n", + " Mass features with MS2: 3 / 3\n", + " Total MS2 scans associated: 8\n" + ] + } + ], + "source": [ + "# Since the search results are stored as mass features (in the lcms_obj.mass_features dictionary), we can now add associated MS1 and MS2 data to these features just like we would for untargeted features.\n", + "# Add MS1 average spectra\n", + "lcms_obj.add_associated_ms1(\n", + " use_parser=False, \n", + " spectrum_mode=\"profile\"\n", + ")\n", + "print(f\"✓ Added MS1 spectra\")\n", + "\n", + "# Add MS2 DDA spectra\n", + "initial_ms_count = len(lcms_obj._ms)\n", + "lcms_obj.add_associated_ms2_dda(\n", + " use_parser=True, \n", + " spectrum_mode=\"centroid\"\n", + ")\n", + "ms2_added = len(lcms_obj._ms) - initial_ms_count\n", + "print(f\"✓ Added {ms2_added} MS2 spectra\")\n", + "\n", + "# Check which mass features have MS2 data\n", + "ms2_counts = []\n", + "for mf_id, mf in lcms_obj.mass_features.items():\n", + " ms2_count = len(mf.ms2_scan_numbers) if mf.ms2_scan_numbers else 0\n", + " ms2_counts.append(ms2_count)\n", + " \n", + "print(f\"\\nMS2 Data Summary:\")\n", + "print(f\" Mass features with MS2: {sum(1 for c in ms2_counts if c > 0)} / {len(lcms_obj.mass_features)}\")\n", + "print(f\" Total MS2 scans associated: {sum(ms2_counts)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "fdf828bd", + "metadata": {}, + "source": [ + "## Step 8: Visualize Targeted Mass Features\n", + "\n", + "Plot the extracted ion chromatograms (EICs) and mass spectra for the targeted compounds." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "e061cc66", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Plotting mass feature 0:\n", + " m/z = 301.2168\n", + " RT = 8.8956 min\n", + " Intensity = 6.68e+07\n" + ] + }, + { + "data": { + "image/png": "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33//vcLtlUolFi5ciJdffhmjR49GaGgoDh48iB9++AGLFi0y6rpY0yUU7ldMTAwuXLhQJvGcP38+zp8/j3fffRe//vqr0X3Nmzc3GiN3r7ock9ehQwcMHDgQnTt3houLC65cuYLVq1ejqKgIH3zwgVHdDz/8EI8//jgGDx6McePG4ezZs/jiiy8wbdo0wxg+vffeew+Abgp4APj+++9x6NAhALoxiHoTJkzA/v37jX5gCAsLw/Xr1zFr1iwcOnTIsB2gW2+t9JTwBw4cwIEDBwDovvTm5OQYjt2vXz/069cPANClSxdMmTIFa9asQXFxsaGVc9OmTQgPDzd05e3WrRsGDRqEdevWQa1WY/DgwUhISMDnn38OpVKJ1157zeg8BUGoVotpt27dAADvvPMOxo0bB1tbWzz22GNVTrJzPx5++GE8//zzWLx4MaKjozF48GDY2triypUr2LRpEz799NNKk/K6HJP3xhtv4H//+x/69u2LGTNmwM3NDdu3b8f//vc/TJs2zagLdXnPhQ8++ABnz55Fz549YWNjg23btmH37t1477337muSqvo0ZcoUfPLJJwgNDcXUqVORnJyMVatWoV27dkYTFSmVSrRt2xYbN25Ey5Yt4erqivbt26N9+/bVfv9u27Yt+vfvj27dusHV1RXHjh3D5s2bK53YhohMzBxTehLRXaWXUKhMeUsorF69WgwODhblcrnYunVrce3ateLcuXONphaPjIwUhw8fLvr6+ooymUz09fUVx48fbzQV/FdffSX269dPdHNzE+Vyudi8eXPxjTfeEDMzMyuNST+d94cfflhpvWvXrokTJkwQvb29RVtbW7FJkybisGHDxM2bNxvq5Ofni//3f/8n+vj4iEqlUuzTp48YFRVV7jTgv/76q9i2bVvRxsamzHIBH3/8sdikSRNRLpeLffr0EY8dO1bhEgqbNm0qN96TJ0+KI0eONPw/AgICxDFjxoiRkZGVnqfe119/LbZq1UqUyWRi8+bNxWXLlhlNRV86huouoVDeEhflPSdEUTdt+8svvyyKoih+8cUXopOTk1hUVGRUp7JlEMpbhsJU5s6dK3bv3l10cXERbWxsRF9fX3HcuHHi6dOny63/yy+/iJ07dxblcrnYtGlT8d133xULCwvL1Kvo3O792NMvT1Hdbe99Lupfb+Vd7n1sCwsLxXnz5okBAQGira2t2KJFC3HZsmVlYs/NzRUXLFggtm3bVlQqlaKTk5M4bNgw8eTJk0b1srKyyl2CoSILFy4UmzRpIkokEqOp+itaQuHe9yT9ud67HMvEiRNFOzu7Msf7+uuvxW7duolKpVJ0cHAQO3ToIM6aNUuMj4+vVrx15ciRI+KQIUMM7z8tW7YUFy1aVOY1Ud5zYfv27WKPHj1EBwcHUaVSiQ8++KD4888/V/vYpV+LehW9b5b3vlTREgrlveeW95z74YcfxKCgIFEmk4mdO3cWd+3aVWafoiiKhw8fFrt16ybKZLIy+6nO+/d7770n9ujRQ3R2dhaVSqXYunVrcdGiReW+NomofgiiaML5l4mIyKweffRR2Nvb4+effzZ3KFTH/vjjDwwbNgynTp1Chw4dzB0OERFZEHbXJCKyYv379zesoUXWZe/evRg3bhwTPCIiKoMteURERERERFaEs2sSERERERFZESZ5REREREREVoRJHhERERERkRVhkkdERERERGRFmOQRERERERFZESZ5REREREREVoRJHhERERERkRVhkkdERERERGRFmORZuYiICAiCUOHln3/+AQAIgoAZM2aU2V6tVmP+/Pno1KkT7O3toVQq0b59e7z55puIj4+v79MhIiIiIqIq2Jg7AKofCxYsQLNmzcqUt2jRosJtrl+/jpCQEMTFxWH06NF47rnnIJPJcPr0aaxevRq//PILLl++bMqwiYiIiIiohpjkNRJDhgxB9+7dq12/uLgYI0eORFJSEvbt24eHHnrI6P5FixZhyZIldR0mERERERHVErtrUrm2bNmCU6dO4Z133imT4AGAo6MjFi1aZIbIiIiIiIioMmzJayQyMzORmppqVCYIAtzc3Mqt/9tvvwEAnn32WZPHRkREREREdYdJXiMREhJSpkwulyM/P7/c+hcuXICTkxP8/PxMHRoREREREdUhJnmNxIoVK9CyZUujMqlUWmF9tVoNBwcHU4dFRERERER1jEleI9GjR48aTbzi6OiI69evmzAiIiIiIiIyBU68QuVq3bo1MjMzcfPmTXOHQkRERERENcAkj8r12GOPAQB++OEHM0dCREREREQ1wSSPyjVq1Ch06NABixYtQlRUVJn7s7Ky8M4775ghMiIiIiIiqgzH5DUS//vf/3Dx4sUy5b1790ZQUFCZcltbW2zduhUhISHo168fxowZgz59+sDW1hbnzp3D+vXr4eLiwrXyiIiIiIgsDJO8RmLOnDnllq9du7bcJA8AWrRogejoaCxbtgy//PILtm3bBq1WixYtWmDatGl45ZVXTBkyERERERHdB0EURdHcQRAREREREVHd4Jg8IiIiIiIiK8Ikj4iIiIiIyIowySMiIiIiIrIiTPKIiIiIiIisCJM8IiIiIiIiK8Ikj4iIiIiIyIowySMiIiIiIrIiTPKIiIiIiIisCJM8K3fgwAE89thj8PX1hSAI2LZtW433sWvXLjz44INwcHCAh4cHnnzyScTGxtZ5rEREREREVHtM8qxcTk4OOnXqhBUrVtzX9jExMRg+fDgeeeQRREdHY9euXUhNTcXIkSPrOFIiIiIiIqoLgiiKormDoPohCAJ++eUXjBgxwlBWUFCAd955Bz/99BMyMjLQvn17LFmyBP379wcAbN68GePHj0dBQQEkEt1vAr///juGDx+OgoIC2NramuFMiIiIiIioImzJa+RmzJiBqKgobNiwAadPn8bo0aMRFhaGK1euAAC6desGiUSCtWvXQqPRIDMzE99//z1CQkKY4BERERERWSC25DUi97bkxcXFISgoCHFxcfD19TXUCwkJQY8ePfD+++8DAPbv348xY8YgLS0NGo0GvXr1wh9//AFnZ2cznAUREREREVWGLXmN2JkzZ6DRaNCyZUvY29sbLvv378e1a9cAAImJiZg+fTomTpyIo0ePYv/+/ZDJZBg1ahT4+wARERERkeWxMXcAZD7Z2dmQSqU4fvw4pFKp0X329vYAgBUrVsDJyQlLly413PfDDz/Az88PR44cwYMPPlivMRMRERERUeWY5DViXbp0gUajQXJyMvr27VtundzcXMOEK3r6hFCr1Zo8RiIiIiIiqhl217Ry2dnZiI6ORnR0NADdkgjR0dGIi4tDy5Yt8fTTT2PChAnYunUrYmJi8O+//2Lx4sXYsWMHAGDo0KE4evQoFixYgCtXruDEiROYPHkyAgIC0KVLFzOeGRERERERlYcTr1i5ffv2YcCAAWXKJ06ciIiICBQVFeG9997Dd999h9u3b8Pd3R0PPvgg5s+fjw4dOgAANmzYgKVLl+Ly5ctQqVTo1asXlixZgtatW9f36RARERERURWY5BEREREREVkRdtckIiIiIiKyIkzyiIiIiIiIrAiTPCIiIiIiIivCJRSskFarRXx8PBwcHCAIgrnDISIiIiKiOiCKIrKysuDr61tmmbPSmORZofj4ePj5+Zk7DCIiIiIiMoGbN2+iadOmFd7PJM8KOTg4ANA9+I6OjmaOhoiITCUnJwe+vr4AdD/w2dnZmTkiIiIyJbVaDT8/P8P3/YowybNC+i6ajo6OTPKIiKyYVCo1XHd0dGSSR0TUSFQ1JItJHhERkQUrKipCREQEAGDSpEmwtbU1b0BERGTxmOQRERFZMFEUER8fb7hORERUFSZ5jZhGo0FRUZG5wyCyGLa2tkbd34iIiIgaIiZ5jZAoikhMTERGRoa5QyGyOM7OzvD29ubyI0RERNRgMclrhPQJnqenJ1QqFb/MEkH340dubi6Sk5MBAD4+PmaOiIiIiOj+MMlrZDQajSHBc3NzM3c4RBZFqVQCAJKTk+Hp6cmum0RERNQgVbxMOlkl/Rg8lUpl5kiILJP+tcHxqkRERNRQsSWvkWIXTaLy8bVBlog/zBERUU0wySMiIrJgMpkMb7zxhrnDICKiBoTdNYmIiIiIiKwIkzxqMCZNmgRBEMpcwsLCAACBgYFYvny50TYnT57E6NGj4eXlBYVCgeDgYEyfPh2XL182wxkQEREREZkekzxqUMLCwpCQkGB0+emnn8qtu337djz44IMoKCjAjz/+iAsXLuCHH36Ak5MTZs+eXc+RExHdn6KiIkRERCAiIoITAhERUbVwTB41KHK5HN7e3lXWy83NxeTJk/Hoo4/il19+MZQ3a9YMPXv25ELwRNRgiKKIGzduGK4TERFVhUkeQRRF5BVp6v24SlupyWYy3LVrF1JTUzFr1qxy73d2djbJcYmIiIiIzI1JHiGvSIO2c3bV+3HPLwiFSlazp+D27dthb29vVPb222/j7bffNiq7cuUKAKB169a1C5KIiIiIqIFhkkcNyoABA7By5UqjMldX1zL12KWJiIiIiBorJnkEpa0U5xeEmuW4NWVnZ4cWLVpUWa9ly5YAgIsXL6JXr141Pg4RERERUUPFJI8gCEKNu01ausGDB8Pd3R1Lly41mnhFLyMjg+PyiIiIiMgqWdc3e7J6BQUFSExMNCqzsbGBu7u7UZmdnR2+/fZbjB49Go8//jheeeUVtGjRAqmpqfj5558RFxeHDRs21GfoRET3zdbW1twhEBFRA8IkjxqUnTt3wsfHx6isVatWuHjxYpm6w4cPx+HDh7F48WI89dRTUKvV8PPzwyOPPIL33nuvvkImIqoVmUxWZnIpIiKiyggiZ6iwOmq1Gk5OTsjMzISjo6PRffn5+YiJiUGzZs2gUCjMFCGR5eJrhBqSnJwcw4zD2dnZsLOzM3NERERkSpV9zy9NUo8xERERERERkYmxuyYREZEFKy4uxs8//wwAGDNmDGxs+NFNRESV4ycFERGRBdNqtbhy5YrhuvF9d0dcxKbkoh27axIREdhdk4iIqMG6eSfXcH3bscRKahIRUWPCJI+IiKiBupyUZbj+5yUmeUREpMMkrxYOHDiAxx57DL6+vhAEAdu2bau0/qRJkyAIQplLu3btDHXmzZtX5v7WrVub+EyIiKghuph4N8m7lqbGtaTcSmoTEVFjwSSvFnJyctCpUyesWLGiWvU//fRTJCQkGC43b96Eq6srRo8ebVSvXbt2RvUOHTpkivCJiKiBu5KcbXT7l2MJZoqEiIgsCSdeqYUhQ4ZgyJAh1a7v5OQEJycnw+1t27YhPT0dkydPNqpnY2MDb2/vOouTiIis0+UktdHtXecT8PrQ5maKhoiILAVb8sxo9erVCAkJQUBAgFH5lStX4Ovri6CgIDz99NOIi4urdD8FBQVQq9VGFyIism45BcWIu5NnuC0AuJKWievJ7LJJRNTYMckzk/j4ePzvf//DtGnTjMp79uyJiIgI7Ny5EytXrkRMTAz69u2LrKysCvYELF682NBK6OTkBD8/P1OHTw1A//798dprr5nt+P369cP69evNdvzyFBYWIjAwEMeOHTN3KETVJpPJMHfuXMydOxcymcxQfjkpC+LdFRTQ2t0VAPDLUXbZJCJq7Jjkmcm6devg7OyMESNGGJUPGTIEo0ePRseOHREaGoo//vgDGRkZhoVwyxMeHo7MzEzD5ebNmyaO3jz0E9e88MILZe57+eWXIQgCJk2aZChLSUnBiy++CH9/f8jlcnh7eyM0NBR///23oc7XX3+N/v37w9HREYIgICMjox7OpG7t27ev3Ni3bt2KhQsXmiWm3377DUlJSRg3bpxZjl8RmUyG119/HW+++aa5QyGqtUuJxj/+PdhU181/13kmeUREjR2TPDMQRRFr1qzBs88+a/SrbHmcnZ3RsmVLXL16tcI6crkcjo6ORhdr5efnhw0bNiAv724Xpfz8fKxfvx7+/v5GdZ988kmcPHkS69atw+XLl/Hbb7+hf//+SEtLM9TJzc1FWFgY3n777Xo7h/ri6uoKBwcHsxz7s88+w+TJkyGRWN5bzNNPP41Dhw7h3Llz5g6FqFYu3pPkPeDrBQHA5bRMxLDLJhFRo2Z538Aagf379+Pq1auYOnVqlXWzs7Nx7do1+Pj41ENklq9r167w8/PD1q1bDWVbt26Fv78/unTpYijLyMjAwYMHsWTJEgwYMAABAQHo0aMHwsPD8fjjjxvqvfbaa3jrrbfw4IMPVjuGzZs3o0OHDlAqlXBzc0NISAhycnIM969Zswbt2rWDXC6Hj48PZsyYYbjvk08+QYcOHWBnZwc/Pz+89NJLyM6+OzteREQEnJ2dsWvXLrRp0wb29vYICwtDQkL5v8zHxsZiwIABAAAXFxej1sx7u2sGBgbivffew4QJE2Bvb4+AgAD89ttvSElJwfDhw2Fvb4+OHTuW6cp46NAh9O3bF0qlEn5+fnjllVeMzvdeKSkp+Ouvv/DYY48ZlVf33Ldt24bg4GAoFAqEhoaWaZn+9ddf0bVrVygUCgQFBWH+/PkoLi4GACxYsAC+vr5GifzQoUMxYMAAaLVaw/+pT58+2LBhQ4XnQGRJiouLsWnTJmzatMnwXAeACwnG46+dFXK00XfZ5CybRESNGpO8WsjOzkZ0dDSio6MBADExMYiOjjZMlBIeHo4JEyaU2W716tXo2bMn2rdvX+a+119/Hfv370dsbCwOHz6MJ554AlKpFOPHjzfZeYiiiJycnHq/iKUHk9TAlClTsHbtWsPtNWvWlJmh1N7eHvb29ti2bRsKCgpq9f8pLSEhAePHj8eUKVNw4cIF7Nu3DyNHjjScy8qVK/Hyyy/jueeew5kzZ/Dbb7+hRYsWhu0lEgk+++wznDt3DuvWrcNff/2FWbNmGR0jNzcXH330Eb7//nscOHAAcXFxeP3118uNx8/PD1u2bAEAXLp0CQkJCfj0008rjH/ZsmXo06cPTp48iaFDh+LZZ5/FhAkT8Mwzz+DEiRNo3rw5JkyYYDifa9euISwsDE8++SROnz6NjRs34tChQ0aJ670OHToElUqFNm3aGJVX99wXLVqE7777Dn///TcyMjKMunwePHgQEyZMwKuvvorz58/jq6++QkREBBYtWgQAeOeddxAYGGgY67pixQocPnwY69atM2pV7NGjBw4ePFjhORBZEq1Wi/Pnz+P8+fOGHytEUSzTkgcAffx0PwjuOs+F0YmIGjWR7tvevXtFAGUuEydOFEVRFCdOnCg+/PDDRttkZGSISqVS/Prrr8vd59ixY0UfHx9RJpOJTZo0EceOHStevXq1RnFlZmaKAMTMzMwy9+Xl5Ynnz58X8/LyDGXZ2dnlnoepL9nZ2TU6r4kTJ4rDhw8Xk5OTRblcLsbGxoqxsbGiQqEQU1JSxOHDhxv+96Ioips3bxZdXFxEhUIh9u7dWwwPDxdPnTpV7r71j2V6enqlMRw/flwEIMbGxpZ7v6+vr/jOO+9U+5w2bdokurm5GW6vXbtWBGD0mK9YsUL08vKqcB8Vxf7www+Lr776quF2QECA+MwzzxhuJyQkiADE2bNnG8qioqJEAGJCQoIoiqI4depU8bnnnjPa78GDB0WJRGL0HCpt2bJlYlBQUMUnXaKic//nn38MZRcuXBABiEeOHBFFURQHDhwovv/++0b7+f7770UfHx/D7WvXrokODg7im2++KSqVSvHHH38sc+xPP/1UDAwMLDeu8l4jROZUUFAgzps3T5w3b55YUFAgiqIoJmTkiQFvbhcDZm4xvKcePJgt7jmUJwa+uV0MeHO7GJOcY+bIiYiorlX2Pb80rpNXC/3796+0NSoiIqJMmZOTE3JzKx4rwS5kVfPw8MDQoUMREREBURQxdOhQuLu7l6n35JNPYujQoTh48CD++ecf/O9//8PSpUvx7bffGk3QUhOdOnXCwIED0aFDB4SGhmLw4MEYNWoUXFxckJycjPj4eAwcOLDC7f/8808sXrwYFy9ehFqtRnFxMfLz85GbmwuVSgUAUKlUaN787jpXPj4+SE5Ovq9479WxY0fDdS8vLwBAhw4dypQlJyfD29sbp06dwunTp/Hjjz8a6oiiCK1Wi5iYmDKtdQCQl5cHhUJRprw6525jY4MHHnjAsE3r1q3h7OyMCxcuoEePHjh16hT+/vtvQ8sdAGg0GqP9BAUF4aOPPsLzzz+PsWPH4qmnnioTi1KprPR1SGTpLiTqump6KFS4UarcRaFAG3dXnE+9g1+OJeK/Q4LMEyAREZkVkzyCSqUyGhtVn8e9X1OmTDF0GVyxYkWF9RQKBQYNGoRBgwZh9uzZmDZtGubOnXvfSZ5UKsWePXtw+PBh7N69G59//jneeecdHDlypNxEs7TY2FgMGzYML774IhYtWgRXV1ccOnQIU6dORWFhoeH/YWtra7SdIAj33bX1XqX3LQhChWX6LmHZ2dl4/vnn8corr5TZ170T3ei5u7sjPT3dqKy6516V7OxszJ8/HyNHjixzX+nE8sCBA5BKpYiNjUVxcTFsbIzf6u7cuQMPD49qHZPIEl1M0HXV9FaVnWirt58Pzqfewc5zCUzyiIgaKSZ5BEEQYGdnZ+4waiQsLAyFhYUQBAGhoaHV3q5t27bYtm1brY4tCAL69OmDPn36YM6cOQgICMAvv/yCmTNnIjAwEJGRkYbJUEo7fvw4tFotPv74Y8P4sMqWxqgu/QytGo2m1vu6V9euXXH+/HmjcYVV6dKlCxITE5Geng4XFxcA1T/34uJiHDt2DD169ACgG2eYkZFhaDHs2rUrLl26VGk8GzduxNatW7Fv3z6MGTMGCxcuxPz5843qnD171miiHqKG5mJJS56vyr7MfQ828cbqk+dwKTUDN1LyEOChrO/wiIjIzDjxCjVIUqkUFy5cwPnz5yGVSsvcn5aWhkceeQQ//PADTp8+jZiYGGzatAlLly7F8OHDDfUSExMRHR1tWKLizJkziI6Oxp07d8o97pEjR/D+++/j2LFjiIuLw9atW5GSkmJIQubNm4ePP/4Yn332Ga5cuYITJ07g888/BwC0aNECRUVF+Pzzz3H9+nV8//33WLVqVa3/FwEBARAEAdu3b0dKSkqdtsq++eabOHz4MGbMmIHo6GhcuXIFv/76a6UTr3Tp0gXu7u5G6xFW99xtbW3xn//8B0eOHMHx48cxadIkPPjgg4akb86cOfjuu+8wf/58nDt3DhcuXMCGDRvw7rvvAgBu3bqFF198EUuWLMFDDz2EtWvX4v3338c///xjdJyDBw9i8ODBdfEvIjIL/Rp5fg5lkzwXpQKt3DjLJhFRY8YkjxqsytYEtLe3R8+ePbFs2TL069cP7du3x+zZszF9+nR88cUXhnqrVq1Cly5dMH36dABAv3790KVLF/z2228VHvPAgQN49NFH0bJlS7z77rv4+OOPMWTIEADAxIkTsXz5cnz55Zdo164dhg0bhitXrgDQjef75JNPsGTJErRv3x4//vgjFi9eXOv/Q5MmTTB//ny89dZb8PLyqjQBq6mOHTti//79uHz5Mvr27YsuXbpgzpw58PX1rXAbqVSKyZMnG43jq+65q1QqvPnmm3jqqafQp08f2NvbY+PGjYb7Q0NDsX37duzevRsPPPAAHnzwQSxbtgwBAQEQRRGTJk1Cjx49DP+D0NBQvPjii3jmmWcMyW9UVBQyMzMxatSouvo3EdWrwmItribrns8BLuW/B/bx48LoRESNmSDW1WAfshhqtRpOTk7IzMwskwTl5+cjJiYGzZo1K3dyDKK6kJiYiHbt2uHEiRMICAio1jYRERF47bXXkJGRYdLYxo4di06dOuHtt98u936+RsjSiKKIoqIiALrW7ouJWRjy6UEopTb4rH8fDB7sAAA4eDAbSqWu6/2dvHw8tz0SIoADrz8Cf3d22SQisgaVfc8vjS15RFTnvL29sXr1asOakZaisLAQHTp0wH//+19zh0JUbYIgQCaTQSaTQRCEUuPxHKFQCOVu46pUoJWbbkzsL8e4Zh4RUWPDJI+ITGLEiBHo27evucMwIpPJ8O6770KpZKsGNVz6RdB9VA4Qys/xAOhm2QTYZZOIqDFikkdEFmHSpEkm76pJ1BAVFxdj27Zt2LZtG4qLiw3LJzSxc6h0uweb6pK888npuJmWZ/I4iYjIcjDJIyIismBarRanTp3CqVOnoNVqDd01AyuYdEXPTalAK1ddl81t7LJJRNSoMMkjIiJqIDJyCpGkLgAANHOtvCUPAHr761rzdp5jl00iosaESR4REVEDcalk6QQ3uRIu9jZV1u/VRLeUwrnkdNy6k2/S2IiIyHIwySMiImogLifpumr6qBxhU3WOBzeVEi0NXTbZmkdE1FgwySMiImogLifqWvJ8VVV31dTrUzLL5v/YZZOIqNFgkkdERNRAXE7Szazp51D5pCul9Wqq67J5Pikdt9llk4ioUWCSR2SlAgMDsXz5cnOHQUR16ErJmLygaky6ouemUqKFizNEAJFnU00UGRERWRImedRgTJo0CYIg4IUXXihz38svvwxBEDBp0iRDWUpKCl588UX4+/tDLpfD29sboaGh+PvvvwEAd+7cwX/+8x+0atUKSqUS/v7+eOWVV5CZmVlfp1QnIiIi4OzsXKb86NGjeO655+o/ICKqU7a2tnj99dcxZspLyC4SYSuRwM/Frkb7CHTWJYWxKVwvj4ioMajGsG0iy+Hn54cNGzZg2bJlUCqVAID8/HysX78e/v7+RnWffPJJFBYWYt26dQgKCkJSUhIiIyORlpYGAIiPj0d8fDw++ugjtG3bFjdu3MALL7yA+Ph4bN68ud7Pra55eHiYOwQiqgOCIMDOzg6x19UABHgrHaBUCDXah6edCgBwMyPXBBESEZGlYUseNShdu3aFn58ftm7daijbunUr/P390aVLF0NZRkYGDh48iCVLlmDAgAEICAhAjx49EB4ejscffxwA0L59e2zZsgWPPfYYmjdvjkceeQSLFi3C77//juLi4gpj+PLLLxEcHAyFQgEvLy+MGjXKcJ9Wq8XSpUvRokULyOVy+Pv7Y9GiRYb733zzTbRs2RIqlQpBQUGYPXs2ioqKDPfPmzcPnTt3xvfff4/AwEA4OTlh3LhxyMrKKjeWffv2YfLkycjMzIQgCBAEAfPmzQNQtrumIAj46quvMGzYMKhUKrRp0wZRUVG4evUq+vfvDzs7O/Tu3RvXrl0zOsavv/6Krl27QqFQICgoCPPnz6/0/0NEpnExUfc+4KtygKSGn95eJUnebSZ5RESNApM8MigsLKzwcu+X+srqlk5aKqpbG1OmTMHatWsNt9esWYPJkycb1bG3t4e9vT22bduGgoKCau87MzMTjo6OsKlgbvJjx47hlVdewYIFC3Dp0iXs3LkT/fr1M9wfHh6ODz74ALNnz8b58+exfv16eHl5Ge53cHBAREQEzp8/j08//RTffPMNli1bZnSMa9euYdu2bdi+fTu2b9+O/fv344MPPig3nt69e2P58uVwdHREQkICEhIS8Prrr1d4fgsXLsSECRMQHR2N1q1b46mnnsLzzz+P8PBwHDt2DKIoYsaMGYb6Bw8exIQJE/Dqq6/i/Pnz+OqrrxAREWGUuBKRaRUXF2PHjh1IOHsYEmjha1f9SVf0vOx1SV5CFpM8IqLGgN01yWDx4sUV3hccHIynnnrKcPujjz4qk8zpBQQEGI2N+/TTT5Gba/zFYu7cufcd5zPPPIPw8HDcuHEDAPD3339jw4YN2Ldvn6GOjY0NIiIiMH36dKxatQpdu3bFww8/jHHjxqFjx47l7jc1NRULFy6sdBxbXFwc7OzsMGzYMDg4OCAgIMDQgpiVlYVPP/0UX3zxBSZOnAgAaN68OR566CHD9u+++67hemBgIF5//XVs2LABs2bNMpRrtVpERETAwUE3hubZZ59FZGRkuYmVTCaDk5MTBEGAt7d3Vf86TJ48GWPGjAGga1Xs1asXZs+ejdDQUADAq6++apQwz58/H2+99ZbhfIKCgrBw4ULMmjWrVo8hEVWfVqvFsWPHYAdAAlfD+Lqa0LfkpefnIztXA3uVtI6jJCIiS8KWPGpwPDw8MHToUERERGDt2rUYOnQo3N3dy9R78sknER8fj99++w1hYWHYt28funbtioiIiDJ11Wo1hg4dirZt2xq6O5Zn0KBBCAgIQFBQEJ599ln8+OOPhgT2woULKCgowMCBAyvcfuPGjejTpw+8vb1hb2+Pd999F3FxcUZ1AgMDDQkeAPj4+CA5ObmK/0r1lE5w9S2MHTp0MCrLz8+HWq1bcPnUqVNYsGCBoWXU3t4e06dPR0JCQpnEnYjqR7MazKyp5yCzhbKkh0IMJ18hIrJ6bMkjg/Dw8Arvk9wzAKSyLoGCYDwhwKuvvlq7wMoxZcoUQ7fCFStWVFhPoVBg0KBBGDRoEGbPno1p06Zh7ty5Ri2NWVlZCAsLg4ODA3755RfY2tpWuD8HBwecOHEC+/btw+7duzFnzhzMmzcPR48eNUwEU5GoqCg8/fTTmD9/PkJDQ+Hk5IQNGzbg448/Nqp37/EFQYBWq61039VVet/6x6m8Mv3xsrOzMX/+fIwcObLMvhQKRZ3ERETV5yiTw8NBXuPtBEGAp50SNzKzEJOciw4B9iaIjoiILAWTPDKQyWRmr1tdYWFhKCwshCAIhq6G1dG2bVts27bNcFutViM0NBRyuRy//fZbtRIXGxsbhISEICQkBHPnzoWzszP++usvPProo1AqlYiMjMS0adPKbHf48GEEBATgnXfeMZTpu5zWhkwmg0ajqfV+ytO1a1dcunQJLVq0MMn+iahmvBT2uN+3VC87FW5kZuFGKlvhiYisHZM8apCkUikuXLhguH6vtLQ0jB49GlOmTEHHjh3h4OCAY8eOYenSpRg+fDgAXYI3ePBg5Obm4ocffoBarTZ0U/Tw8Ch3v9u3b8f169fRr18/uLi44I8//oBWq0WrVq2gUCjw5ptvYtasWZDJZOjTpw9SUlJw7tw5TJ06FcHBwYiLi8OGDRvwwAMPYMeOHfjll19q/b8IDAxEdnY2IiMj0alTJ6hUKqhUqlrvFwDmzJmDYcOGwd/fH6NGjYJEIsGpU6dw9uxZvPfee3VyDCKqPh9VzSdd0dNPvhKXziSPiMjaMcmjBsvRseIvO/b29ujZsyeWLVuGa9euoaioCH5+fpg+fTrefvttAMCJEydw5MgRACjTUhUTE4PAwMAy+3V2dsbWrVsxb9485OfnIzg4GD/99BPatWsHAJg9ezZsbGwwZ84cxMfHw8fHx7B4++OPP47//ve/mDFjBgoKCjB06FDMnj270jGA1dG7d2+88MILGDt2LNLS0jB37txa71MvNDQU27dvx4IFC7BkyRLY2tqidevW5bZUEpHpNbW//26W+slXbjHJIyKyeoIoiqK5g6C6pVar4eTkZFgOoLT8/HzExMSgWbNmHFNFVA6+RsjSFBQUGJZRadJuBro0czPcl5eXg759dYnfwYPZUCrtKtzPiYRkLDp0FP6ODjjwdr8K6xERkeWq7Ht+aZxdsxYOHDiAxx57DL6+vhAEwWisV3n27dtnWLC69CUxMdGo3ooVKxAYGAiFQoGePXvi33//NeFZEBGRJUvL02BTfgdsLuiIZm5O970fz5KWvOScPGg0/H2XiMiaMcmrhZycHHTq1KnS2R3Lc+nSJcPC1QkJCfD09DTct3HjRsycORNz587FiRMn0KlTJ4SGhtbZFPpERNSwXErMRrYoh73cFQ529z/KwtNONwNwvqYYyRnlr3NKRETWgWPyamHIkCEYMmRIjbfz9PSEs7Nzufd98sknmD59umFB6lWrVmHHjh1Ys2YN3nrrrdqES0REDdCFRN2EUD4qR5QzH1S1yaRSuCoUuJOfj2vJufBxq/uZj4mIyDKwJc8MOnfuDB8fHwwaNAh///23obywsBDHjx9HSEiIoUwikSAkJARRUVHmCJWIiMzsUkImutvcRLB4BVpt7ZZL0c+wGZvCyVeIiKwZk7x65OPjg1WrVmHLli3YsmUL/Pz80L9/f5w4cQIAkJqaCo1GAy8vL6PtvLy8yozbK62goMAw/X/pZQCIiKjhu5ygRgfbJKjyLtQ+ySsZl3cjjUkeEZE1Y3fNetSqVSu0atXKcLt37964du0ali1bhu+///6+97t48WLMnz+/Rttotdr7Ph6RNeNrgyxJQbEGManZeFBeN/vzKhmXdyuDSR4RkTVjkmdmPXr0wKFDhwAA7u7ukEqlSEpKMqqTlJQEb2/vCvcRHh6OmTNnGm6r1Wr4+fmVW1cmk0EikSA+Ph4eHh6QyWQQBKEOzoSoYRNFEYWFhUhJSYFEIoFMxvFKZH7XknNQrK27mTA9S7pr3maSR0Rk1ZjkmVl0dDR8fHwA6BKwbt26ITIyEiNGjACga1WIjIzEjBkzKtyHXC6HXF69n3klEgmaNWuGhIQExMfH1zp+ImujUqng7+8PiYS92cn8LiXVbfd775LumglqJnlERNaMSV4tZGdn4+rVq4bbMTExiI6OhqurK/z9/REeHo7bt2/ju+++AwAsX74czZo1Q7t27ZCfn49vv/0Wf/31F3bv3m3Yx8yZMzFx4kR0794dPXr0wPLly5GTk2OYbbMuyGQy+Pv7o7i4GBpN7cZ3EFkTqVQKGxsbtm6TxbiUmF2n+9OPyUvNy0NegRZKOX/MICKyRkzyauHYsWMYMGCA4ba+y+TEiRMRERGBhIQExMXFGe4vLCzE//3f/+H27dtQqVTo2LEj/vzzT6N9jB07FikpKZgzZw4SExPRuXNn7Ny5s8xkLLUlCAJsbW1ha2tbp/slIqK6cy2lbpM8Z4UcthIJirRaxCbnoY2fXZ3un4iILIMgimLddfYni6BWq+Hk5ITMzEw4OjqaOxwiIrpPAz/ehxspajyrPAkACAsLh43N3fGieXk56NvXHgBw8GA2lMqqk7ZXdu7H7axsrBzTE0O6upsmcCIiMonqfs9nSx4REZEFKtZoEXcnF8WQoHn7KWjqJodUWvveF972KtzOykZsKsflERFZKyZ5REREFuhmeh6KNCJsJVIEeDWFSlk3Y0X14/Li7jDJIyKyVhxxTUREZIGuJevG43kq7KGQ191kQPok71Y6kzwiImvFljwiIiILdD1Vl+R5KZS4cmUfACA4uC8kEmmt9uulXysvk0keEZG1YkseERGRBbqekgMA8FSocOXKfly5sh9abe2XvfG0UwIAknJywanXiIisE5M8IiIiC6RP8vQLmNcVz5L95RQVITWzqE73TUREloFJHhERkQW6VtJd09fRvk73q7SxgZNctwzD9WR22SQiskZM8oiIiCxMZl4R0rILAQBNnOq2JQ+4O/lKbAqTPCIia8Qkj4iIyMJcT9G14jnJ5HBS1f0cafrJV26kMckjIrJGTPKIiIgszN1JV+xhW/v1z8vQj8u7yWUUiIisEpM8IiIiC3OtpCXPQ2Fnkv0b1srLyDPJ/omIyLy4Th4REZGF0bfkeansIZXa4KGHpgEApNK6+djWJ3kJarbkERFZIyZ5REREFka/EHoTBzsIggTOzk3qdP/6MXkpubkoKBQhlwl1un8iIjIvdtckIiKyIBqtiNiSCVH8nOp2+QQ9V6UCUkGARhRxMy3fJMcgIiLzYZJHRERkQW6n56GwWAsbQQJfZyW0Wg2uXfsb1679Da1WUyfHkAoCPFRKAEAM18ojIrI6TPKIiIgsiH4RdA+FHeQyAVqtBhcu/IkLF/6ssyQPKLWMQiqTPCIia8Mkj4iIyIIYlk9Q2kEqNd1x9JOvxN1hkkdEZG2Y5BEREVkQ/ULoniZaPkFP35LHtfKIiKwPkzwiIiILom/J87E3zaQrevqWvNsZTPKIiKwNkzwiIiILol8IvYmjiVvySpK8xOxciKJJD0VERPWMSR4REZGFyMovQnJWAQDAz7l+WvLUhYXIzCk26bGIiKh+MckjIiKyEDGpuq6aDrYyuNjZmvRYdjJb2NnqjnE9Kc+kxyIiovplY+4AiIiISMcws6bCHiX5F6RSGzz44ETD9brkZa/C9fRMxKTkomtzhzrdNxERmQ+TPCIiIgthmFlTaQdB0JUJggTu7oEmOZ6XnRLX0zNxI42TrxARWRN21yQiIrIQ11L1a+SZdjyenn5cHpdRICKyLmzJIyIishD67ppNHO7OrKnVahAXdxwA4O/fDRJJ3a2Qrk/ybjHJIyKyKkzyiIiILIBWKyImVddds6mjfalyDc6e/Z+uvGnnuk3yShZET1AzySMisibsrlkLBw4cwGOPPQZfX18IgoBt27ZVWn/r1q0YNGgQPDw84OjoiF69emHXrl1GdebNmwdBEIwurVu3NuFZEBGRJUhQ5yO/SAupIKCJs7JejqlvyUvKyUVxMRfLIyKyFkzyaiEnJwedOnXCihUrqlX/wIEDGDRoEP744w8cP34cAwYMwGOPPYaTJ08a1WvXrh0SEhIMl0OHDpkifCIisiDXknWteO4KFZSK+vl4dlcpIQAo0mpx+05BvRyTiIhMj901a2HIkCEYMmRItesvX77c6Pb777+PX3/9Fb///ju6dOliKLexsYG3t3ddhUlERA2AfmZND4U9pHXXI7NSNhIJ3FVKpOTm4XpyLgI8FfVzYCIiMim25JmRVqtFVlYWXF1djcqvXLkCX19fBAUF4emnn0ZcXJyZIiQiovpyXT+zpsKuipp1S99lMzaF4/KIiKwFkzwz+uijj5CdnY0xY8YYynr27ImIiAjs3LkTK1euRExMDPr27YusrKwK91NQUAC1Wm10ISKihkU/s6aPXf0sn6Cnn3zl5p28ej0uERGZDrtrmsn69esxf/58/Prrr/D09DSUl+7+2bFjR/Ts2RMBAQH4+eefMXXq1HL3tXjxYsyfP9/kMRMRkenou2s2dTJPS97NDLbkERFZC7bkmcGGDRswbdo0/PzzzwgJCam0rrOzM1q2bImrV69WWCc8PByZmZmGy82bN+s6ZCIiMqHcwmLEZ+YDAPydjVvyJBIbPPDAeDzwwHhIJHX/26ynnW4mz9tM8oiIrAZb8urZTz/9hClTpmDDhg0YOnRolfWzs7Nx7do1PPvssxXWkcvlkMvldRkmERHVo5iS8Xh2NrZwsZMZ3SeRSODl1dJkx9a35CVkMckjIrIWbMmrhezsbERHRyM6OhoAEBMTg+joaMNEKeHh4ZgwYYKh/vr16zFhwgR8/PHH6NmzJxITE5GYmIjMzExDnddffx379+9HbGwsDh8+jCeeeAJSqRTjx4+v13MjIqL6ox+P56m0h0xWReU6ph+Tl56fj+xcTf0enIiITIJJXi0cO3YMXbp0MSx/MHPmTHTp0gVz5swBACQkJBjNjPn111+juLgYL7/8Mnx8fAyXV1991VDn1q1bGD9+PFq1aoUxY8bAzc0N//zzDzw8POr35IiIqN7okzwPhR0Ewfg+rVaDmzejcfNmNLTauk/CHGUyKErWbIhJ4eQrRETWgN01a6F///4QRbHC+yMiIoxu79u3r8p9btiwoZZRERFRQ3OtZNIVL2XZmTW1Wg1OnfoVAODj0xYSSd0uoicIArzsVbiRmYWY5Fx0CKjf2T2JiKjusSWPiIjIzK6n6pI8X4f6nVlTz7NkXN6NVI7LIyKyBkzyiIiIzEgURcSUdNds6mieJM+wjEI6kzwiImvAJI+IiMiMktQFyCnUQCIIaOpspiTPnkkeEZE1YZJHRERkRvpF0N3kKqgU5vlY1rfk3c5kkkdEZA2Y5BEREZnRtZI18jwVdrAx03Ro+iQvKScXGk3FE4oREVHDwCSPiIjIjPQteR4K83TVBAAvOyUEAAUaDeLvFJotDiIiqhtcQoGIiMiM9GvkeduVv3SBRGKDrl1HGa6bgq1UCjelEql5ebiWlAM/D7lJjkNERPWj0bbkTZw4EQcOHDB3GERE1Mjp18hrUsHMmhKJBL6+7eDr2w4Siek+tr1LJl+JSeG4PCKihq7RJnmZmZkICQlBcHAw3n//fdy+fdvcIRERUSOTX6TB7Yw8AIC/s3kXIdcneTfSmOQRETV0jTbJ27ZtG27fvo0XX3wRGzduRGBgIIYMGYLNmzejqKjI3OEREVEjEJuWA1EElFIbuNvLyq2j1WoRH38O8fHnoNVqTRaLt72uJTEuPcdkxyAiovrRaJM8APDw8MDMmTNx6tQpHDlyBC1atMCzzz4LX19f/Pe//8WVK1fMHSIREVkx/Xg8T6U95HKh3DpabTFOnNiMEyc2Q6stNlks3lwrj4jIajTqJE8vISEBe/bswZ49eyCVSvHoo4/izJkzaNu2LZYtW2bu8IiIyEqVnllTKD/Hqzf6ZRQSs3IhchUFIqIGrdEmeUVFRdiyZQuGDRuGgIAAbNq0Ca+99hri4+Oxbt06/Pnnn/j555+xYMECc4dKRERWSt+S56U073g84G5LXlZRIe6oOWyBiKgha7RLKPj4+ECr1WL8+PH4999/0blz5zJ1BgwYAGdn53qPjYiIGgf9Qui+9uZbI09PZWsLR5kM6sJCXE3KhZuTk7lDIiKi+9Rok7xly5Zh9OjRUCgUFdZxdnZGTExMPUZFRESNyY2SJK+Jk/mTPEDXmqe+U4jrybno2ZJJHhFRQ9Vou2vu3bu33Fk0c3JyMGXKFDNEREREjUleoQYZebrPIU97pZmj0dHPsBmbxhk2iYgaskab5K1btw55eXllyvPy8vDdd9+ZISIiImpMEjJ1n0FyqRROSsvoWKMflxd3hzNsEhE1ZJbxqVKP1Go1RFGEKIrIysoy6q6p0Wjwxx9/wNPT04wREhFRY5CQmQ8AcJYpIZNVPLWmRCJFp07DDddNycuwjAJb8oiIGrJGl+Q5OztDEAQIgoCWLVuWuV8QBMyfP98MkRERUWMSn6FryXOyVUBSSb8aiUQKP7/O9RKTT0l3zQQ1W/KIiBqyRpfk7d27F6Io4pFHHsGWLVvg6upquE8mkyEgIAC+vr5mjJCIiBoDfUuei7ziCcDqm3fJWnl38vORnauBvcq0LYdERGQajS7Je/jhhwEAMTEx8Pf3h2Du1WeJiKhRupvkVT7pilarRUrKVQCAh0cLSCpr9qslR7kMCqkU+RoNriXnolOgg8mORUREptOokrzTp0+jffv2kEgkyMzMxJkzZyqs27Fjx3qMjIiIGhv9xCtuyspb8rTaYhw9+hMAICwsHBKJzGQxCYIAL3s73MhU4zqTPCKiBqtRJXmdO3dGYmIiPD090blzZwiCAFEUy9QTBAEajcYMERIRUWORkKFryXNTWsbyCXo+9ircyFQjNpXj8oiIGqpGleTFxMTAw8PDcJ2IiMhc9C15HnaWMyYP4DIKRETWoFEleQEBAeVeJyIiqk85BcVQ5xcDADwdLKslz6tkhs24O1xGgYiooWrUi6Hv2LHDcHvWrFlwdnZG7969cePGDTNGRkRE1k7fiqeQ2sBBYVm/t/qUtOTFZ7Ilj4iooWq0Sd77778PZck4iKioKHzxxRdYunQp3N3d8d///tfM0RERkTWLz9AvhK6Ara2Zg7mHV8kyCsm5uSgsKjtunYiILF+jTfJu3ryJFi1aAAC2bduGUaNG4bnnnsPixYtx8ODBau3jwIEDeOyxx+Dr6wtBELBt27Yqt9m3bx+6du0KuVyOFi1aICIiokydFStWIDAwEAqFAj179sS///5bk1MjIiILl5ipT/KUlS6Ebg5uKiWkggCNKOJGSp65wyEiovtgYR8t9cfe3h5paWkAgN27d2PQoEEAAIVCgby86n2o5eTkoFOnTlixYkW16sfExGDo0KEYMGAAoqOj8dprr2HatGnYtWuXoc7GjRsxc+ZMzJ07FydOnECnTp0QGhqK5OTkGp4hERFZqviS7prVWQhdIpGiffshaN9+CCQS0y9OLhUEQ2ve1SR22SQiaogsayBAPRo0aBCmTZuGLl264PLly3j00UcBAOfOnUNgYGC19jFkyBAMGTKk2sdctWoVmjVrho8//hgA0KZNGxw6dAjLli1DaGgoAOCTTz7B9OnTMXnyZMM2O3bswJo1a/DWW2/V4AyJiMhS6ZdPcJJVPemKRCJFYGAPU4dkxNtehfjsHMSmMMkjImqIGm1L3ooVK9CrVy+kpKRgy5YtcHNzAwAcP34c48ePN8kxo6KiEBISYlQWGhqKqKgoAEBhYSGOHz9uVEcikSAkJMRQh4iIGj59S557FQuhm4t3yQybN9I4wyYRUUPUaFvynJ2d8cUXX5Qpnz9/vsmOmZiYCC8vL6MyLy8vqNVq5OXlIT09HRqNptw6Fy9erHC/BQUFKCgoMNxWq9V1GzgREdUp/Zg8d1XVLXmiqEVaWhwAwM3NH4Jg+t9nDWvlpbMlj4ioIWq0SR4AZGRk4N9//0VycjK0Wq2hXBAEPPvss2aMrGYWL15s0uSUiIjqVkJJkudhX3VLnkZTjH/+WQcACAsLh42NzKSxAXeTvNtcRoGIqEFqtEne77//jqeffhrZ2dlwdHSEIAiG+0yV5Hl7eyMpKcmoLCkpCY6OjlAqlZBKpZBKpeXW8fb2rnC/4eHhmDlzpuG2Wq2Gn59f3QZPRER1Qp1fhOyCkoXQ7Syzu6aXna67ZlJ2DjQaEVKpUMUWRERkSRrtmLz/+7//w5QpU5CdnY2MjAykp6cbLnfu3DHJMXv16oXIyEijsj179qBXr14AAJlMhm7duhnV0Wq1iIyMNNQpj1wuh6Ojo9GFiIgsk76rpsrGFvZKy/yt1ctOCQFAvkaD+DuF5g6HiIhqqNEmebdv38Yrr7wClUp13/vIzs5GdHQ0oqOjAeiWSIiOjkZcnG7sRHh4OCZMmGCo/8ILL+D69euYNWsWLl68iC+//BI///yz0eLrM2fOxDfffIN169bhwoULePHFF5GTk2OYbZOIiBq2+AzdpCuWuBC6nq1UCjelbrzgtSROvkJE1NBY5k+I9SA0NBTHjh1DUFDQfe/j2LFjGDBggOG2vsvkxIkTERERgYSEBEPCBwDNmjXDjh078N///heffvopmjZtim+//dawfAIAjB07FikpKZgzZw4SExPRuXNn7Ny5s8xkLERE1DAllFoIXbDgXpDe9iqk5uUhJiUX/eFq7nCIiKgGGm2SN3ToULzxxhs4f/48OnToANt7fk59/PHHq9xH//79IYpihfdHRESUu83Jkycr3e+MGTMwY8aMKo9PREQNT0KpljxL5m2vwtmUNNxI4+QrREQNTaNN8qZPnw4AWLBgQZn7BEGARqOp75CIiKgR0LfkucgtPcnTTb4Sl87umkREDU2jTfJKL5lARERUX/RJnn7MW1UkEinatAkxXK8v+mUUbnGtPCKiBqfRJnml5efnQ6Gw7F9UiYjIOsRn6rpruquq97kjkUjRvHkfU4ZULi87XZKXkJULUYRFjx8kIiJjjXZ2TY1Gg4ULF6JJkyawt7fH9evXAQCzZ8/G6tWrzRwdERFZI1EUkZBRshC6XfVa8sxF35KXVVSIO+oiM0dDREQ10WiTvEWLFiEiIgJLly6FTCYzlLdv3x7ffvutGSMjIiJrpc4rRl6Rbsy3p0P1WvJEUYuMjNvIyLgNUay/oQYqW1s4lnw+Xk1il00iooak0SZ53333Hb7++ms8/fTTkErvjnHo1KkTLl68aMbIiIjIWum7atrbyGCnqN74Oo2mGIcOfYtDh76FRlNsyvDK0LfmXU9mkkdE1JA02iTv9u3baNGiRZlyrVaLoiJ2SyEiorqXUJLkOckUsGkAo+L1M2zGpnGGTSKihqTRJnlt27bFwYMHy5Rv3rwZXbp0MUNERERk7QwLocsVDWIiE31LXtwdtuQRETUkDeB3RNOYM2cOJk6ciNu3b0Or1WLr1q24dOkSvvvuO2zfvt3c4RERkRXST7riIrPsSVf0vLiMAhFRg9RoW/KGDx+O33//HX/++Sfs7OwwZ84cXLhwAb///jsGDRpk7vCIiMgK6cfkOVv4Quh6PiXdNePV7K5JRNSQNNqWPADo27cv9uzZY+4wiIiokdC35LkpG0aS512yVt6d/Hxk52lgr6y/xdiJiOj+NdqWvKCgIKSlpZUpz8jIQFBQkBkiIiIia5eo1iV57qqG0V3TUS6DQiqFCOB6Up65wyEiompqtElebGwsNBpNmfKCggLcvn3bDBEREZE1E0UR8Rm6RMnTvvpJnkQiRXDwwwgOfhgSSf22pAmCYJhh81oyu2wSETUUja675m+//Wa4vmvXLjg5ORluazQaREZGIjAw0AyRERGRNUvPLUJBsW4xcw87ebW3k0ikaNWqv4miqpq3vQqxmWrEpnLyFSKihqLRJXkjRowAoPt1cuLEiUb32draIjAwEB9//LEZIiMiImumb8VzsJVBVc2F0C0Bl1EgImp4Gl2Sp9XqfkVt1qwZjh49Cnd3dzNHREREjUGifo08mbJGC6GLoojs7BQAgL29B4R6XmBP310z7g67axIRNRSNLsnTi4mJMXcIRETUiCTol0+Q1WwhdI2mCPv3rwQAhIWFw8ZGZorwKqRvyYvPZEseEVFD0WiTPACIjIxEZGQkkpOTDS18emvWrDFTVEREZI3i9S158oYxs6aeV8kyCsm5uSgsEiGzrd+WRCIiqrlGO7vm/PnzMXjwYERGRiI1NRXp6elGFyIiorqk767pImsYa+TpuamUkAoCNKKIGylcRoGIqCFotC15q1atQkREBJ599llzh0JERI2AfuIVN1XDSvKkggAvOxXis3NwLSkXwb4qc4dERERVaLQteYWFhejdu7e5wyAiokYiIbNhLYRemn5cXkwKx+URETUEjTbJmzZtGtavX2/uMIiIqBHQakVDkudp37Ba8gDOsElE1NA02u6a+fn5+Prrr/Hnn3+iY8eOsLW1Nbr/k08+MVNkRERkbe7kFqJIo4UAwKNBJnm6lrwbXCuPiKhBaLRJ3unTp9G5c2cAwNmzZ80bDBERWbWEDF0rnoNMDqW8Zp1oJBIpgoJ6Ga6bgz7Ju81lFIiIGoRGm+Tt3bvX3CEQEVEjEV+yRp6LTAlpDfM0iUSKtm0HmyCq6vOy03XXTMrOgUYjQirlMgpERJas0SV5I0eOrLKOIAjYsmVLPURDRESNgX75hJouhG4pvOyUEADkazSIv1MIPw+5uUMiIqJKNLokz8nJydwhEBFRI6NvyXO6jzXyRFFEXl4mAECpdIJghizRViqFm1KJ1Lw8XEvKYZJHRGThGl2St3bt2jrf54oVK/Dhhx8iMTERnTp1wueff44ePXqUW7d///7Yv39/mfJHH30UO3bsAABMmjQJ69atM7o/NDQUO3furPPYiYjI9PRj8lzkNV8+QaMpwl9/fQoACAsLh42NrE5jqy5vexVS8/IQk5KL/nA1SwxERFQ9jXYJhbqyceNGzJw5E3PnzsWJEyfQqVMnhIaGIjk5udz6W7duRUJCguFy9uxZSKVSjB492qheWFiYUb2ffvqpPk6HiIhMIKGkJc+9gS2EXpphhs00Tr5CRGTpmOTV0ieffILp06dj8uTJaNu2LVatWgWVSoU1a9aUW9/V1RXe3t6Gy549e6BSqcokeXK53Kiei4tLfZwOERGZwN2F0BtykleyVl4618ojIrJ0TPJqobCwEMePH0dISIihTCKRICQkBFFRUdXax+rVqzFu3DjYlcxcprdv3z54enqiVatWePHFF5GWllbhPgoKCqBWq40uRERkGbRaEUlq/ULoNe+uaSl8SlryYtOyzRwJERFVhUleLaSmpkKj0cDLy8uo3MvLC4mJiVVu/++//+Ls2bOYNm2aUXlYWBi+++47REZGYsmSJdi/fz+GDBkCjUZT7n4WL14MJycnw8XPz+/+T4qIiOpUanYBijQiBADudg13wpJmLrqJy25kZiG3oPzPIyIisgyNbuIVS7J69Wp06NChzCQt48aNM1zv0KEDOnbsiObNm2Pfvn0YOHBgmf2Eh4dj5syZhttqtZqJHhGRhYgv6arpJFNAUcOF0C2Jp0oJO1tb5BQV4WxcNnoEc7ZqIiJL1XA/bSyAu7s7pFIpkpKSjMqTkpLg7e1d6bY5OTnYsGEDpk6dWuVxgoKC4O7ujqtXr5Z7v1wuh6Ojo9GFiIgsQ2Kp5RNsGvBPq4IgoHlJa97JG5lmjoaIiCrDJK8WZDIZunXrhsjISEOZVqtFZGQkevXqVem2mzZtQkFBAZ555pkqj3Pr1i2kpaXBx8en1jETEVH9is/QL4R+f+PxBEGCgIDuCAjoDkEw78d2UEmSd/Y2kzwiIkvWgH9TtAwzZ87ExIkT0b17d/To0QPLly9HTk4OJk+eDACYMGECmjRpgsWLFxttt3r1aowYMQJubm5G5dnZ2Zg/fz6efPJJeHt749q1a5g1axZatGiB0NDQejsvIiKqG/rlE1zk9zezplRqgw4dhtZlSPctyEXXU+RCEpM8IiJLxiSvlsaOHYuUlBTMmTMHiYmJ6Ny5M3bu3GmYjCUuLg4SifEvr5cuXcKhQ4ewe/fuMvuTSqU4ffo01q1bh4yMDPj6+mLw4MFYuHAh5PKGO2CfiKix0i+fcD8LoVsafXfN2IwsFBRpIbdlhyAiIkvEJK8OzJgxAzNmzCj3vn379pUpa9WqFURRLLe+UqnErl276jI8IiIyo9qukSeKIgoLdQuQy2QqCIJQZ7HVlJedCipbG+QWFePMjSx0b8HJV4iILBF/giMiIjKhhAxdd837TfI0miLs2fMR9uz5CBpNUV2GVmOCICDImZOvEBFZOiZ5REREJqLRikjKKgDQsBdCL00/+cq5eLWZIyEiooowySMiIjKRlKwCaLQiJIIAd3vrGFetH5d3PpEteURElopJHhERkYnEl1ojT2ZrvrF0dUnfkheTrkZhkdbM0RARUXmY5BEREZlIgmGNvIa9EHpp3vYqKGxsUKTV4kxctrnDISKicjDJIyIiMhH9Gnn3uxC6JZIIAoKcdevlnYpjl00iIkvEJI+IiMhEDGvkye5vZk1L1dxV12XzzG0meURElshKOo8QERFZHn1Lnovi/pM8QZCgadNOhuuWgJOvEBFZNiZ5REREJhKfoV8I/f67a0qlNujceUQdRVQ3mpWslRdzR42iYhG2NtYxqQwRkbWwjJ8EiYiIrFBiSXdNDzvr6q7p62AHhVSKQq0W529y8hUiIkvDJI+IiMgEijVaJGeVJHn295/kiaKI4uJCFBcXQhTFugqvViSCgGYlXTZP3mCXTSIiS8Mkj4iIyASSsgqgFQGpIMBVdf8LoWs0Rdi5czF27lwMjaaoDiOsnSAX3QybnHyFiMjyMMkjIiIygYQM/fIJ1rMQemn6RdEvcPIVIiKLwySPiIjIBPTLJzjLlFazEHpp+hk2r5VMvkJERJaDSR4REZEJ6JdPcLKyNfL0fB3sIZdKUaDR4MItTr5CRGRJmOQRERGZgH75BBf5/S+fYMmkgoBAZ924vOg4tZmjISKi0pjkERERmYC+Jc+1FguhWzp9l80ztzguj4jIkjDJIyIiMgH9Gnm1WQjd0uknXznPyVeIiCyKFQ4FJyIiMr94Q5JXu5Y8QZDAx6et4bolMUy+kqaGRiNCKrW+WUSJiBoiJnlERER1LL9Ig9TsAgCAl0PtWvKkUht06za6LsKqc00c7CCTSJCvKcal+By09bM3d0hERAR21yQiIqpzsWk5EEVAaWMDF5WtucMxGalEgoCSyVdOxLLLJhGRpWCSR0REVMdiUnIAAJ4Ke8jl1t2FkZOvEBFZHnbXJCIiqmPXU/VJnh2EWuZ4xcWF2LlzMQAgLCwcNjay2oZXpzj5ChGR5WFLHhERUR27lqJbHNxTaf1j1PQteVdLJl8hIiLzY5JHRERUx66XdNf0sbczcySm19TRHrYSCfKKi3E5Idfc4RAREZjkERER1SlRFHG9pCWvqZP1J3k2EgkCnHSTr5zk5CtERBaBSR4REVEdupNTCHV+MQQ0jiQPAIJcdEnemdtM8oiILAGTvDqwYsUKBAYGQqFQoGfPnvj3338rrBsREQFBEIwuCoXxQrmiKGLOnDnw8fGBUqlESEgIrly5YurTICKiOqCfdMVFroSDSmrmaOqHflze+QQmeUREloBJXi1t3LgRM2fOxNy5c3HixAl06tQJoaGhSE5OrnAbR0dHJCQkGC43btwwun/p0qX47LPPsGrVKhw5cgR2dnYIDQ1Ffn6+qU+HiIhqSb98gofCDjaNZA7roFKTr2i1nHyFiMjcmOTV0ieffILp06dj8uTJaNu2LVatWgWVSoU1a9ZUuI0gCPD29jZcvLy8DPeJoojly5fj3XffxfDhw9GxY0d89913iI+Px7Zt2+rhjIiIqDaupdbtzJqCIIGnZzA8PYMhCJb5se3n5AAbiQQ5RUW4mphn7nCIiBo9y/y0aCAKCwtx/PhxhISEGMokEglCQkIQFRVV4XbZ2dkICAiAn58fhg8fjnPnzhnui4mJQWJiotE+nZyc0LNnzwr3WVBQALVabXQhIiLz0M+s6aWsm/F4UqkNevR4Cj16PAWp1DKbBm0lEvg7OgDg5CtERJaASV4tpKamQqPRGLXEAYCXlxcSExPL3aZVq1ZYs2YNfv31V/zwww/QarXo3bs3bt26BQCG7Wqyz8WLF8PJyclw8fPzq+2pERHRfYopGZPXxLFxTLqi19xV12Xz9C0meURE5sYkr5716tULEyZMQOfOnfHwww9j69at8PDwwFdffXXf+wwPD0dmZqbhcvPmzTqMmIiIqqtYo8WNNF2S19TR+hdCL00/Lu9cPJM8IiJzs8x+Hw2Eu7s7pFIpkpKSjMqTkpLg7e1drX3Y2tqiS5cuuHr1KgAYtktKSoKPj4/RPjt37lzuPuRyOeRy+X2cARER1aVb6Xko0oiwlUjg7aSoeoNqKC4uxJ49HwEABg16HTY2sjrZb11r7eYCADiffAd5BRoo5Y1jZlEiIkvElrxakMlk6NatGyIjIw1lWq0WkZGR6NWrV7X2odFocObMGUNC16xZM3h7exvtU61W48iRI9XeJxERmYe+q6aHwg4KuVBn+9VoiqDRFNXZ/kzBz9EeLgoFCrVaHLh4x9zhEBE1akzyamnmzJn45ptvsG7dOly4cAEvvvgicnJyMHnyZADAhAkTEB4ebqi/YMEC7N69G9evX8eJEyfwzDPP4MaNG5g2bRoA3cybr732Gt577z389ttvOHPmDCZMmABfX1+MGDHCHKdIRETVdC1FN7Omh8Iekkb2CSsIArp4uwMA9l5IMXM0RESNG7tr1tLYsWORkpKCOXPmIDExEZ07d8bOnTsNE6fExcVBUuqTPj09HdOnT0diYiJcXFzQrVs3HD58GG3btjXUmTVrFnJycvDcc88hIyMDDz30EHbu3Flm0XQiIrIs+oXQPetoZs2GprO3B/6KvYWomFRzh0JE1KgJoihy1VIro1ar4eTkhMzMTDg6Opo7HCKiRmPc11H45/odTGjZCcM7Na2TfRYXF2LnzsUAgLCwcKMxeXl5OejbVzfBy8GD2VCaObnMKijE5N/2QARw8I2B8HPjj5NERHWput/zG1lnEiIiItPRj8lrbDNr6jnIZWju4gwA+PMMu2wSEZkLkzwiIqI6kF1QjCR1AQCgqXPj7K4JAJ1LxuXtv8wkj4jIXJjkERER1YGYFF0rnoOtDC52tnW2X0EQ4OoaAFfXAAhC3c3YaSqdvT0AAMdupqJYwxEhRETmwIlXiIiI6sD11Lsza9rWXY4HqdQWvXtPqrsdmliwqzOUNjbILirC0WuZ6NXS2dwhERE1OmzJIyIiqgPXU+6ukdcAGtxMxkYiQQdPNwDAX+fZZZOIyByY5BEREdUB/fIJXo10+YTSupR02fz7GpM8IiJzYJJHRERUB2JKumv6OtTtzJrFxYXYvftD7N79IYqLC+t036aiH5d3MTUDGTlFZo6GiKjxYZJHRERUS6IoGiZeaepU9y15hYW5KCzMrfP9moqnnQo+9nbQiiIiz3JhdCKi+sYkj4iIqJaS1AXIKdRAIgho4qQydzgWQd9lc98lJnlERPWNSR4REVEt6WfWdJOroFLwoxW4u17eP7EpEEUupUBEVJ/4SURERFRLpWfWtOHiRACAdh5usBEkSMnNw4XbOeYOh4ioUWGSR0REVEv6JM9TwZk19RQ2Nmjt7gIAiDzLWTaJiOoTkzwiIqJa0nfX9Lar25k1Gzr9uLyDV5nkERHVJyZ5REREtRRTskaer0Pdt+QJggAnJ184OflCaGCrrOuXUjiVcAf5RRozR0NE1Hhw5AAREVEtFBRrcPOObnkDf+e6T/KkUlv07Tu9zvdbHwKcHOAslyOjoACHLqYjpIO7uUMiImoU2JJHRERUC3FpudCKgEJqA3d7ubnDsSiCIBhm2fzrArtsEhHVFyZ5REREtXA99e7MmnJ5w+pOWR/0XTajrnO9PCKi+sIkj4iIqBZKz6wpMcGnqkZThMjI5YiMXA6NpqjuD2BiHb3cIQCIyVAj/k6+ucMhImoUmOQRERHVwvUU3cyaHkrTzKwpiiLy8jKRl5fZIBcVd5LL0czZCQCw5wxb84iI6gOTPCIiolowzKxpzzXyKqIfl3fgCsflERHVByZ5REREtaAfk9fUkUleRfTj8v6NS4VW2/BaI4mIGhomeURERPcpI7cQd3IKAQBNnZjkVaSlmwsUUimyCgtx/Lra3OEQEVk9JnlERET36VrJpCvOMgUc7bj0bEVsJRK099R12Yw8zy6bRESmxiSPiIjoPsWUWj7B1tbMwVi4LiXj8g5dY5JHRGRq/NmRiIjoPuln1vRUmK6rpiAIsLf3MFxvqPTj8i4kpyMjpwjOdsyKiYhMhUkeERHRfTKskacyzfIJACCV2qJ//5dMtv/64m1vBy87FZJycrH3XBqe6OFt7pCIiKwWu2sSERHdJ313zSYOnHSlOrqUtObtv8Qum0REpsQkrw6sWLECgYGBUCgU6NmzJ/79998K637zzTfo27cvXFxc4OLigpCQkDL1J02aBEEQjC5hYWGmPg0iIqoBjVZETFrJ8glOpmvJsyb6LptRsVwUnYjIlJjk1dLGjRsxc+ZMzJ07FydOnECnTp0QGhqK5OTkcuvv27cP48ePx969exEVFQU/Pz8MHjwYt2/fNqoXFhaGhIQEw+Wnn36qj9MhIqJqis/IQ2GxFjaCBL5OSpMdR6Mpwr59X2Lfvi+h0RSZ7Dj1ob2HG6SCgKScXFyOzzF3OEREVotJXi198sknmD59OiZPnoy2bdti1apVUKlUWLNmTbn1f/zxR7z00kvo3LkzWrdujW+//RZarRaRkZFG9eRyOby9vQ0XFxeX+jgdIiKqJv0i6O4KFRRy002IIooisrNTkJ2dAlFs2AuJK21t0MpN93m25yy7bBIRmQqTvFooLCzE8ePHERISYiiTSCQICQlBVFRUtfaRm5uLoqIiuLq6GpXv27cPnp6eaNWqFV588UWkpaVVuI+CggKo1WqjCxERmVbpmTWlUjMH04Dox+UduMIkj4jIVJjk1UJqaio0Gg28vLyMyr28vJCYmFitfbz55pvw9fU1ShTDwsLw3XffITIyEkuWLMH+/fsxZMgQaDSacvexePFiODk5GS5+fn73f1JERFQt+pk1PRQcj1cT3Xw9AQD/3kzG8dgM8wZDRGSlmOSZ0QcffIANGzbgl19+gUKhMJSPGzcOjz/+ODp06IARI0Zg+/btOHr0KPbt21fufsLDw5GZmWm43Lx5s57OgIio8dLPrOltz5k1ayLAyRF9/ZpABBC++Qw02obdBZWIyBIxyasFd3d3SKVSJCUlGZUnJSXB27vy9X8++ugjfPDBB9i9ezc6duxYad2goCC4u7vj6tWr5d4vl8vh6OhodCEiItPSd9ds6sgkr6Ymdm4NpY0NLqeqsfbgDXOHQ0RkdZjk1YJMJkO3bt2MJk3RT6LSq1evCrdbunQpFi5ciJ07d6J79+5VHufWrVtIS0uDj49PncRNRES1k1tYjPjMfACAvzO7a9aUi0KBp9q3AgAs+/MSktUFZo6IiMi6MMmrpZkzZ+Kbb77BunXrcOHCBbz44ovIycnB5MmTAQATJkxAeHi4of6SJUswe/ZsrFmzBoGBgUhMTERiYiKys3W/CGdnZ+ONN97AP//8g9jYWERGRmL48OFo0aIFQkNDzXKORERkTN9V087GFi52MpMeSxAEKJVOUCqdIAimm8WzvoW2CECAoyNyiooxf9sFc4dDRGRVbMwdQEM3duxYpKSkYM6cOUhMTETnzp2xc+dOw2QscXFxkEju5tIrV65EYWEhRo0aZbSfuXPnYt68eZBKpTh9+jTWrVuHjIwM+Pr6YvDgwVi4cCHkcnm9nhsREZVPn+R5KOwgM22OB6nUFgMHvmbag5iBVBDwQvf2ePuvw9hx/jaevuKP3sGuVW9IRERVEsSGvugOlaFWq+Hk5ITMzEyOzyMiMoHPIq/gkz2X0cOjKd7s38lsceTl5aBvX1130YMHs6FUNrzxgV8ePY3I2Jto5uqA3f/3EGyl7GRERFSR6n7P5zspERFRDRnWyGuASZWlebZja9jb2iLmTha++ivW3OEQEVkFJnlEREQ1pO+u6VMPyydoNEU4ePAbHDz4DTSaIpMfr745yGV4tlNrAMAX+y8jISPfzBERETV8TPKIiIhqQBRFw0Lofk6mn1lTFEVkZsYjMzMe1jrC4pFAPwS7OCO/WIPZW8+bOxwiogaPSR4REVENJKrzkVVQDAFAEyeVucOxChJBwPPd20MA8OflBPx1PsXcIRERNWhM8oiIiGrg1+h4AIC/vTPslVIzR2M9mjk7Iax5IABg9rZzKCjWmDcgIqIGjEkeERFRNWm1Ijb8GwcA6OXpBxsuRFSnxndoCSeZHLfVOfh8z3Vzh0NE1GAxySMiIqqmqOtpiE3LhUJqg/5BvuYOx+rY2dpicpc2AICvD13FjdRcM0dERNQwMckjIiKqpvUlrXhd3Xzh7sxmPFN4yM8Xbd3dUKjRYtams8guKDZ3SEREDQ6TPCIiompIzS7A7nOJAIABfv4QhPo7tkymgkzWOCZ5EQQBz3drB6kg4MiNFPRe/Bc+/fMKMnOtb/kIIiJT4c+QRERE1bD5+C0UaUQE2Dujg59TvR3XxkaGwYPfqLfjWYKmjg54o1c3rDlxAcn5OVj252V8feA6JvYOwNSHmsHNXm7uEImILBqTPCIioipotSJ+KjXhiq2tmQNqBB5o4oWuvp44cD0BWy5eRUJuFr7cdw1rDsXi6Qf98Xy/IHg6KswdJhGRRWKSR0REVIWo62m4UTLhygBOuFJvpIKAAc198XCQDw7fSMKW81cRl5OJ1Ydi8H3UDYx9wA8v9G+OJs5Kc4dKRGRRmOQRERFVYf2RuxOuuNXzhCsaTRGOHPkRANCz59OQShtfM6JEEPBQoDf6BHjh6K1UbDp3Bdez0vH9Pzew/t84PNm1KWYMaAF/t8YxbpGIqCpM8oiIiCqRklWAXSUTrjziH1CvE64AgCiKuHPnhuF6YyYIAnr4eeCBpu44lXAHP5+9gkuZafj52E1sOXELY7v74ZWBwfB2YjdOImrcmOQRERFVYvPxWyjW6iZcad/U0dzhEHTJXmdfN3T2dcO5pHT8dOYKLqSnYP2/cdhy/Bae6RWAl/o35wQtRNRocQkFIiKiCmi1IjYc1XXV7O3lzwlXLFA7Lxe8F9IDcx/qheaOrijQaLH6UAz6LtmLj3ZdQmYel14gosaHSR4REVEFDl+7O+FK/2Y+5g6HKtHRxxVLBj+It3r1gJ+dE3KLNPhi71U89MFf+OKvq7iclMWF1Ymo0WB3TSIiogrol03o5tak3idcoZoTBAEPNPVA9ybuOBSbhA3nLiExLxsf7b6Ej3ZfAgA4KGzg66xEE2clfJwURtebudtxWQYisgr8xCIiIipH6QlXBvj71/uEK3T/BEFA32be6B3ohb1X4/HH1Vik5OUgV1OErPxiXErMwqXErHK3DXK3Q7+WHujTwh0PBrnCQcE+ukTU8DDJIyIiKoclTbjSGJdNqAtSQUBIcBOEBDeBVgtk5xcjMSsPyVn5SMnNQ2puHtLy8pFRmIf0gjykFeTiemoOrqfmIOJwLKSCgM7+zniohTv6Brujk58zbKUc6UJElo9JHhER0T20WtHQVdPcE67Y2MgwZMjb5gvASkgkgKPKBo4qB7T0cii3TkZuEU7eTsOppBRcuJOK1IJcHL+RjuM30vFp5BXYyWzQo5kLuge6onuACzr5OUNhK63nMyEiqhqTPCIionscvpaGuDuccKWxcVbZYkCwNwYEewMAbmfk4vitVJxOTsXlzFTkFBZh76UU7L2UAgCwkQho38QJDwS6oFuAK7oHusCdyzYQkQVgkkdERHSP9f/qFh/nhCuNWxNnFZo4++Nx+EOjFXE5WY3TCXdwOT0d19V3oC4qQPTNDETfzMA3B2MAAIFudujq74zO/s7o4ueC1j4O7OJJRPWOn1xERESlpGQVYPe5JACWMeGKRlOM48d/BgB06zYGUik/us1BKhHQxtsJbbydADSDKIq4lZ6Hs0l3cDE1Hdcy05GYl4XYtBzEpuVg68nbAAC5jQTtfZ3QpSTx6+znjCbOSgjmfmIRkVXjJwUREVEpm47ftJgJVwBAFLVITr5iuE6WQRAE+Lmq4OeqwhA0BQBk5hbhbGI6LqZk4HpmBuKyM5BbXITjcek4Hpdu2NZOZgN3Bxnc7eVws5PBzV4Od3tZqeu62652MihlUthIJLCVCkwMiajamOQRERGV0GpFbPj3JgDzT7hCDY+TyhZ9gjzRJ8gTgO75dONODs4nZeDKnQzEZGUgPleNnMJi5KQV40Zabo32LxUE2EgF2EglsJXo/tpIBAgCIIq6OiJ0V+7e1hGga42USgTYSARISv5KJRJIJYBUIim5LUBpK4VKJoVSVvLXVgqlzAYqo9v663fLFSXbqWQ2kNlIIIq6aERRF5chJvFunAIESCSARBAgFXTnwmSWqPaY5NWBFStW4MMPP0RiYiI6deqEzz//HD169Kiw/qZNmzB79mzExsYiODgYS5YswaOPPmq4XxRFzJ07F9988w0yMjLQp08frFy5EsHBwfVxOkREjVJ+kQa/Rt/mhCtUZyQSAc3c7dHM3R4oae3LK9Tgdnoe0vMKkZlfgIz8QqgLCqEuKEBWUSGyigqQXVyI7KIC5BQXGe1PI4rQFIsoKLbuFl2JoEv6JBIBEgGwkUggs5FAbnPvXylkUgnkthLYSCQARGhFQCuK0Gh1SaVWFEsuAETdLKtSiaDbvyAYrksld48pt5FAYVuSzNpKobAtuS2TQmFT8tdWAolwt3VVAHQJKkpuC7qygmIt8oo0yC3UIK9Ig7zC4lLXdRdAN+mPs0oGZ5UtXEr+OitlcLHT3a5sFldRvHuugO78mCgTk7xa2rhxI2bOnIlVq1ahZ8+eWL58OUJDQ3Hp0iV4enqWqX/48GGMHz8eixcvxrBhw7B+/XqMGDECJ06cQPv27QEAS5cuxWeffYZ169ahWbNmmD17NkJDQ3H+/HkoFIr6PkUiIqtUrNHi9O1MRF1Lw99XU3HsRjoKS748c8IVMhWlTIoWXvZV1tNqgcIiLfKLtCjWiijWlPzVaqEp+VusFZGhFuHXFCj9O7D++33phEMritBqSxJFrRbFGl0ipBFFFGtFaDS660UaLfIKNcgvSUz0CUmuPjkx3NZdzy0s1v0tSVrqIgHVJ2q6zAwAtEBBrXfboMlsdC2t+oRVFO8mtPoW0tJspQJsJBLYSAXIpLq/+mTZpqQVWCa92xpsXF66/t392Eolhv3alty2kUoMSbHCVgKFjfTu9ZK/8lJlWhEo0uief0UaLQo1WsPtQo3uuS0RBEOrstFFuHtdo9XVLywuuWju+VusRbFWW7IviaEFW39eUokAW6m+ZVt/vkJJ+d36+h8BirW6Hwv0r5W7r8OS11Gp6/r77i0v3ZouKXU+EsN56R47rajrBXDv46y/5OVkV+s5I4hieU8Nqq6ePXvigQcewBdffAEA0Gq18PPzw3/+8x+89dZbZeqPHTsWOTk52L59u6HswQcfROfOnbFq1SqIoghfX1/83//9H15//XUAQGZmJry8vBAREYFx48ZVGZNarYaTkxMyMzPh6Gj+8SRE5lCs0SK3SIN8/S+mJV9KSt8u0mghlZTq9lTyJq//ALMp+UAz/kVXCrmNBBIJfyVtaLRaEZeSsnD4WhoOX03FkZg7yC4oNqrjJJMj2MEdT7dvDX9Py/hRrbi4EDt3LgYAhIWFw8ZGZrgvLy8HffvqEoaDB7OhVNqZJUYyn8REwM8P6NjR3JHoaLQi8oo0KCzWQoCudQzC3ZYtQRCMWr20oi65FEsSUF0yKpZc171ui7UiCou1KCjWlPzVGm4XlNwu1oiGFkCh5K9Ucve6pCTz1ZTsX9/ap/t790u0puRY+s+JgiKtIbHNL7r7N79Ia0iw9F+kS3+l1ndJldvc7d6q7warkEmh0pfJpBBFIDOvCOk5hUjPLUJmnu5vRm4hMnKLUKzlV3W6S1uQi5vLx1T5PZ8/U9ZCYWEhjh8/jvDwcEOZRCJBSEgIoqKiyt0mKioKM2fONCoLDQ3Ftm3bAAAxMTFITExESEiI4X4nJyf07NkTUVFR1Ury9CIvJMHOvmb9/YG7b1b3o/Y/Gdz/Dmp7bHOet9hgz1u3tf4XYv0HnlYs/Quj7rp+nIWhG47htu66WPLrnu6XPd0vfEXFd28Xllwv/SFbustL6dv5RRoUaUz7oSi3kRg+tPWJn6208l87bUt+RdTdX86vqVIBtiXbSCTC3S9EJV+GdF9WSn6dL/nCpP9CU7qO/kuU4T6gzLgYEXe7+BjKSpcb3V92W5TU0WpLj7m5+5wo1oooKNJ9Ccuv4K/+i5r+i5W+e5X+S9jdL3wwtDiI5dTVf1HTfw/Sx3D3i1fFj6PKxhYtHN3Q0skNHb3cEORuD6WSCTzR/ZJKBNjLbQAuF1gnRFFEdkExMnJ1XXdLJ636937JPZ+l+hbfomIRRdq7rWZFJa3Buuu61uGikvuKtaWuG8pE3eewVl+3ZDvt3Za3Io2IgiIN8ot1n88FJQlwfvHdZDi/qGwLb+nPSFnJ56e+Ba10K5im1Ht+6Yu+JU5mo//81e1HZqNridTvz9D6Vqo1XFPyP9C3shVr9HW0pa7rzrN0fl16TGvpv/rWwHvL7r1d+jNLf7n7Y4Puc06454eKuz9c3L2enyvFzWo8d5jk1UJqaio0Gg28vLyMyr28vHDx4sVyt0lMTCy3fmJiouF+fVlFde5VUFCAgoK7/RgyMzMBAP9ZdxgSuaoGZ0RkfSQCoLCVQGlrA4VMUpKU2UBpq0u89N0uijR339SLtXc//Io1+g8r3YeCXl4BkJdjxhOj+2IrlaCZnQv8la4IdnZDkKsjlAqhpHubiPT0LKSnV7WX+qXRFCI/Px8AkJCghlR6tyUvP//ukzAhQQ2FQlPv8ZF5ZWYCLi6AWm3uSMiUnCr6xl4y1rA0KQC5AMBo4iih5B7zEEXdWFKJIDSomWL1rb6WNM5RrVbD7y3jluPyMMmzAosXL8b8+fPLlN9eOan+gyEisnDXzR1ArXxQ4T1jxvjWYxxERGROWVlZcHJyqvB+Jnm14O7uDqlUiqSkJKPypKQkeHt7l7uNt7d3pfX1f5OSkuDj42NUp3PnzuXuMzw83KgLqFarxZ07d+Dm5mbyXx3UajX8/Pxw8+ZNjv9rZPjYN1587BsvPvaNFx/7xouPvWURRRFZWVnw9a38hz0mebUgk8nQrVs3REZGYsSIEQB0CVZkZCRmzJhR7ja9evVCZGQkXnvtNUPZnj170KtXLwBAs2bN4O3tjcjISENSp1arceTIEbz44ovl7lMul0MuN+787uzsXKtzqylHR0e+8BspPvaNFx/7xouPfePFx77x4mNvOSprwdNjkldLM2fOxMSJE9G9e3f06NEDy5cvR05ODiZPngwAmDBhApo0aYLFi3Uzo7366qt4+OGH8fHHH2Po0KHYsGEDjh07hq+//hqAbsKE1157De+99x6Cg4MNSyj4+voaEkkiIiIiIqKKMMmrpbFjxyIlJQVz5sxBYmIiOnfujJ07dxomTomLi4NEIjHU7927N9avX493330Xb7/9NoKDg7Ft2zbDGnkAMGvWLOTk5OC5555DRkYGHnroIezcuZNr5BERERERUZW4Th7VSkFBARYvXozw8PAyXUbJuvGxb7z42DdefOwbLz72jRcf+4aJSR4REREREZEVkVRdhYiIiIiIiBoKJnlERERERERWhEkeERERERGRFWGSR0REREREZEWY5FGFAgMDIQhCmcvLL79cbv2IiIgydbnsQ8Ok0Wgwe/ZsNGvWDEqlEs2bN8fChQtR1TxN+/btQ9euXSGXy9GiRQtERETUT8BUZ+7nsd+3b1+57xWJiYn1GDnVVlZWFl577TUEBARAqVSid+/eOHr0aKXb8DVvHWr62PM133AdOHAAjz32GHx9fSEIArZt22Z0vyiKmDNnDnx8fKBUKhESEoIrV65Uud8VK1YgMDAQCoUCPXv2xL///muiM6DqYpJHFTp69CgSEhIMlz179gAARo8eXeE2jo6ORtvcuHGjvsKlOrRkyRKsXLkSX3zxBS5cuIAlS5Zg6dKl+PzzzyvcJiYmBkOHDsWAAQMQHR2N1157DdOmTcOuXbvqMXKqrft57PUuXbpk9Pr39PSsh4iprkybNg179uzB999/jzNnzmDw4MEICQnB7du3y63P17z1qOljr8fXfMOTk5ODTp06YcWKFeXev3TpUnz22WdYtWoVjhw5Ajs7O4SGhiI/P7/CfW7cuBEzZ87E3LlzceLECXTq1AmhoaFITk421WlQdYhE1fTqq6+KzZs3F7Vabbn3r127VnRycqrfoMgkhg4dKk6ZMsWobOTIkeLTTz9d4TazZs0S27VrZ1Q2duxYMTQ01CQxkmncz2O/d+9eEYCYnp5u4ujIVHJzc0WpVCpu377dqLxr167iO++8U+42fM1bh/t57Pmatw4AxF9++cVwW6vVit7e3uKHH35oKMvIyBDlcrn4008/VbifHj16iC+//LLhtkajEX19fcXFixebJG6qHrbkUbUU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7Nq1qx599FENGzZMNWrUUFhYmN577z1lZ2frvvvu00UXXaRGjRoVCylbt25Vz549FRgYqLCwMA0YMEB//PHHaXsrKCjQ559/rptuusllfnR0tP7973/rnnvuUWBgoKKiovT111/r999/1y233KLAwEC1bNlSa9eutZb56+2azz77rFq3bq2PP/5Y0dHRCg4O1l133aXMzEyX7bz++usu227durWeffZZa1yS+vTpI4fDYb2WpK+++kpt27aVn5+fGjZsqHHjxunkyZOSJGOMnn32WdWvX19Op1MRERF67LHHrGW9vb11ww03aNasWac9NgCA0iPkAQBQCjNmzFBoaKhWr16tRx99VEOGDNEdd9yhjh07av369erevbsGDBig48ePS5LS0tJ07bXXqk2bNlq7dq0WLFigw4cP68477zztNjZv3qz09HRdccUVxcYmTpyoq6++Whs2bFCvXr00YMAA3XPPPfrb3/6m9evX65JLLtE999yjM30K49dff9XcuXM1b948zZs3T0uXLtVLL71U6mOwZs0aSdKHH36oQ4cOWa+XL1+ue+65R48//ri2b9+ud955R9OnT9cLL7wgSfriiy80ceJEvfPOO/rll180d+5ctWjRwmXd7du31/Lly0vdCwDg9Ah5AACUQqtWrfSvf/1LjRs31ujRo+Xn56fQ0FA9+OCDaty4scaMGaMjR45o8+bNkqS33npLbdq00YsvvqimTZuqTZs2mjZtmhYvXqyff/65xG3s3btX3t7eJd5mecMNN+jvf/+7ta2MjAxdeeWVuuOOO3TppZdq1KhR2rFjhw4fPnzafSgsLNT06dPVvHlzderUSQMGDFBCQkKpj0Ht2rUlSSEhIQoPD7dejxs3Tk899ZQGDhyohg0b6vrrr9fzzz+vd955R5K0b98+hYeHKzY2VvXr11f79u314IMPuqw7IiJC+/fvV2FhYan7AQCUjJAHAEAptGzZ0vrZ29tbtWrVcrkaFRYWJklKTU2V9OcDVBYvXqzAwEBratq0qaQ/r6iVJCcnR06ns8SHw5y6/aJtnWn7JYmOjtZFF11kva5bt+4Z60tr06ZNeu6551z29cEHH9ShQ4d0/Phx3XHHHcrJyVHDhg314IMPas6cOdatnEX8/f1VWFio3Nzc8+4HAKq6au5uAACAysDHx8fltcPhcJlXFMyKrkRlZWXppptu0ssvv1xsXXXr1i1xG6GhoTp+/Ljy8vLk6+t72u0XbetM2y/tPpxa7+XlVex2z/z8/NOur0hWVpbGjRunW2+9tdiYn5+fIiMjlZSUpIULFyo+Pl4PP/ywXnnlFS1dutTq6ejRo6pevbr8/f3Puj0AwJkR8gAAuADatm2rL774QtHR0apWrXR/blu3bi1J2r59u/VzRapdu7YOHTpkvc7IyFBycrJLjY+PjwoKClzmtW3bVklJSWrUqNFp1+3v76+bbrpJN910k4YOHaqmTZtqy5Ytatu2raQ/H1LTpk2bctwbAKi6uF0TAIALYOjQoTp69Kj69eunNWvW6Ndff9X333+v++67r1hIKlK7dm21bdtWK1asqOBu/3Tttdfq448/1vLly7VlyxYNHDhQ3t7eLjXR0dFKSEhQSkqKjh07JkkaM2aMPvroI40bN07btm3Tjh07NGvWLP3rX/+S9OeTPj/44ANt3bpVu3fv1n//+1/5+/srKirKWu/y5cvVvXv3ittZALAxQh4AABdARESEfvzxRxUUFKh79+5q0aKFhg0bppCQkDN+4fcDDzygmTNnVmCn/2/06NHq0qWLbrzxRvXq1Uu9e/fWJZdc4lLz6quvKj4+XpGRkdaVt7i4OM2bN08//PCDrrzySl111VWaOHGiFeJCQkL03nvv6eqrr1bLli21cOFCffPNN6pVq5Yk6cCBA1q5cqXuu+++it1hALAphznTs5YBAECFysnJUZMmTfTpp58qJibG3e1UiFGjRunYsWN699133d0KANgCn8kDAMCD+Pv766OPPjrjl6bbTZ06dTR8+HB3twEAtsGVPAAAAACwET6TBwAAAAA2QsgDAAAAABsh5AEAAACAjRDyAAAAAMBGCHkAAAAAYCOEPAAAAACwEUIeAAAAANgIIQ8AAAAAbISQBwAAAAA2QsgDAAAAABsh5AEAAACAjRDyAAAAAMBGCHkAAAAAYCOEPAAAAACwEUIePMINN9ygBx980N1tSJLuuusu3Xnnne5uAwAAACgTQp6NTZ8+XQ6HQw6HQytWrCg2boxRZGSkHA6HbrzxRpexrKwsjR07Vs2bN1f16tVVq1YttW7dWo8//rgOHjxo1R06dEhPPfWUunXrposuukgOh0NLliw5pz5//PFH/fDDDxo1alSZ9rO8jRo1Sl988YU2bdrk7lYAAACAc0bIqwL8/Pz0ySefFJu/dOlS/fbbb3I6nS7z8/Pz1blzZ73yyivq1KmTXnvtNT399NNq27atPvnkE/38889WbVJSkl5++WUdOHBALVq0KFN/r7zyiq677jo1atSoTMuXtzZt2uiKK67Qq6++6u5WAAAAgHNWzd0N4MK74YYbNHv2bL355puqVu3/T/knn3yidu3a6Y8//nCpnzt3rjZs2KCZM2fq7rvvdhk7ceKE8vLyrNft2rXTkSNHVLNmTX3++ee64447zqm31NRUffvtt5o6depZa7Ozs1W9evVzWn9Z3XnnnRo7dqzefvttBQYGVsg2AQAAgPLAlbwqoF+/fjpy5Iji4+OteXl5efr888+LhThJ+vXXXyVJV199dbExPz8/BQUFWa8vuugi1axZs8y9ffvttzp58qRiY2Nd5hfdarp06VI9/PDDqlOnji6++GJJ0t69e/Xwww+rSZMm8vf3V61atXTHHXdoz5491vJpaWny9vbWm2++ac37448/5OXlpVq1askYY80fMmSIwsPDXbZ//fXXKzs72+WYAQAAAJUBIa8KiI6OVkxMjP73v/9Z8+bPn6/09HTdddddxeqjoqIkSR999JFLGLoQVq5cqVq1alnb/KuHH35Y27dv15gxY/TUU09JktasWaOVK1fqrrvu0ptvvqmHHnpICQkJ6tq1q44fPy5JCgkJUfPmzbVs2TJrXStWrJDD4dDRo0e1fft2a/7y5cvVqVMnl+02a9ZM/v7++vHHH8t7lwEAAIALits1q4i7775bo0ePVk5Ojvz9/TVz5kx16dJFERERxWp79+6tJk2aaMyYMfrggw/UrVs3derUSTfeeKPq1KlTrn3t3LlT0dHRpx2vWbOmEhIS5O3tbc3r1auXbr/9dpe6m266STExMfriiy80YMAASVKnTp30+eefWzXLly/XNddco507d2r58uW6/PLLrcA3ePBgl/VVq1ZNkZGRLmEQAAAAqAy4kldF3HnnncrJydG8efOUmZmpefPmlXirpiT5+/tr1apVGjlypKQ/b528//77VbduXT366KPKzc0tt76OHDmiGjVqnHb8wQcfdAl4Rf0Vyc/P15EjR9SoUSOFhIRo/fr11linTp10+PBhJSUlSfoz5HXu3FmdOnXS8uXLJf15dc8YU+xKniTVqFGj2OcVAQAAAE9HyKsiateurdjY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2223KTg4WO+8806Z9gsAAACVCyEPlUq3bt00ZcoU63X16tUlSZs2bdKPP/6oF154wRorKCjQiRMndPz4cQUEBGjhwoUaP368du7cqYyMDJ08edJl/HxdccUV1s/Z2dn69ddfdf/991vhTJJOnjyp4ODgM66nZcuW1s/e3t6qVauWWrRoYc0LCwuTJKWmplrzTr1V81RPP/20EhMTtXbtWvn7+5/7TgEAAKDSIeTB0imrU4VspyC/QLm7csu0bPXq1dWoUaNi87OysjRu3DiXK2lF/Pz8tGfPHt14440aMmSIXnjhBdWsWVMrVqzQ/fffr7y8vDOGPIfDIWOMy7z8/PwSezu1H0l677331KFDB5e6os8Qns5fH9jicDhc5jkcDklSYWGhJCkvL08LFizQ008/7bLcf//7X02cOFFLlixRvXr1zrhNAAAA2AchzwZyc3OVm/v/oSkjI6NM6/GufubwUW6K56Pz1rZtWyUlJZUYACVp3bp1Kiws1Kuvviovrz+fN/TZZ5+51Pj6+qqgoPjnBWvXru3yGbdffvlFx48fP2M/YWFhioiI0O7du9W/f/9z3Z1zsmTJEtWoUUOtWrWy5iUmJuqBBx7QO++8Y93SCQAAgKqBkGcD48eP17hx49zdhluNGTNGN954o+rXr6/bb79dXl5e2rRpk7Zu3ap///vfatSokfLz8zVp0iTddNNN+vHHHzV16lSXdURHRysrK0sJCQlq1aqVAgICFBAQoGuvvVZvvfWWYmJiVFBQoFGjRpXq6xHGjRunxx57TMHBwerRo4dyc3O1du1aHTt2TMOHDy+3ff/6669dbtVMSUlRnz59dNdddykuLk4pKSmS/ryC6IlfNwEAAIDyxVco2MDo0aOVnp5uTfv373d3SxUuLi5O8+bN0w8//KArr7xSV111lSZOnKioqChJfz7M5LXXXtPLL7+s5s2ba+bMmRo/frzLOjp27KiHHnpIffv2Ve3atTVhwgRJ0quvvqrIyEh16tRJd999t/7xj3+U6jN8DzzwgN5//319+OGHatGihbp06aLp06erQYMG5brvfw15O3fu1OHDhzVjxgzVrVvXmq688spy3S4AAAA8k8P89cNGqPQyMjIUHBys9PR0BQUFuYydOHFCycnJatCggfz8/NzSX0F+gY5v+vN2x4BWAfL2qaDbRG1o/fr1uvbaa/X7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CHkAAAAAYCOEPAAAAACwEUIeAAAAANgIIQ8AAAAAbISQBwAAAAA2QsizuenTp8vhcMjhcGjFihXFxo0xioyMlMPh0I033mjNz8rK0tixY9W8eXNVr15dtWrVUuvWrfX444/r4MGDVl1CQoIGDRqkSy+9VAEBAWrYsKEeeOABHTp0qEL2DwAAAICrau5uABXDz89Pn3zyia655hqX+UuXLtVvv/0mp9NpzcvPz1fnzp21c+dODRw4UI8++qiysrK0bds2ffLJJ+rTp48iIiIkSaNGjdLRo0d1xx13qHHjxtq9e7feeustzZs3Txs3blR4eHiF7icAAABQ1RHyqogbbrhBs2fP1ptvvqlq1f7/tH/yySdq166d/vjjD2ve3LlztWHDBs2cOVN33323y3pOnDihvLw86/Vrr72ma665Rl5e/39RuEePHurSpYveeust/fvf/76AewUAAADgr7hds4ro16+fjhw5ovj4eGteXl6ePv/882JB7tdff5UkXX311cXW4+fnp6CgIOt1586dXQJe0byaNWtqx44d5bkLAAAAAEqBkFdFREdHKyYmRv/73/+sefPnz1d6erruuusul9qoqChJ0kcffSRjzDlvKysrS1lZWQoNDT2/pgEAAACcM0JeFXL33Xdr7ty5ysnJkSTNnDlTXbp0sT5fV6R3795q0qSJxowZowYNGui+++7TtGnTlJqaWqrtvP7668rLy1Pfvn3LfR8AAAAAnBkhrwq58847lZOTo3nz5ikzM1Pz5s0rdqumJPn7+2vVqlUaOXKkpD+f0Hn//ferbt26evTRR5Wbm3vabSxbtkzjxo3TnXfeqWuvvfaC7QsAAACAkhHyqpDatWsrNjZWn3zyib788ksVFBTo9ttvL7E2ODhYEyZM0J49e7Rnzx598MEHatKkid566y09//zzJS6zc+dO9enTR82bN9f7779/IXcFAAAAwGkQ8qqYu+++W/Pnz9fUqVPVs2dPhYSEnHWZqKgoDRo0SD/++KNCQkI0c+bMYjX79+9X9+7dFRwcrO+++04XXXTRBegeAAAAwNkQ8qqYPn36yMvLSz/99FOJt2qeSY0aNXTJJZcU+6LzI0eOqHv37srNzdX333+vunXrlmfLAAAAAM4B35NXxQQGBmrKlCnas2ePbrrpphJrNm3apHr16hV7OubevXu1fft2NWnSxJqXnZ2tG264QQcOHNDixYvVuHHjC9o/AAAAgDMj5FVBAwcOPON4fHy8xo4dq5tvvllXXXWVAgMDtXv3bk2bNk25ubl69tlnrdr+/ftr9erVGjRokHbs2OHy3XiBgYHq3bv3BdoLAAAAACUh5KGY2267TZmZmfrhhx+0aNEiHT16VDVq1FD79u01YsQIdevWzarduHGjJGnatGmaNm2ay3qioqIIeQAAAEAFc5iyfNs1AAAAAMAj8eAVAAAAALARQh4AAAAA2AghDwAAAABshJAHAAAAADZCyAMAAAAAGyHkAQAAAICNuPV78qZMmaIpU6Zoz549kqTLL79cY8aMUc+ePSVJJ06c0IgRIzRr1izl5uYqLi5Ob7/9tsLCwqx17Nu3T0OGDNHixYsVGBiogQMHavz48apW7f93bcmSJRo+fLi2bdumyMhI/etf/9K9997r0svkyZP1yiuvKCUlRa1atdKkSZPUvn17a9yTejmbwsJCHTx4UBdddJEcDkeplwMAAADguYwxyszMVEREhLy8znC9zrjR119/bb799lvz888/m6SkJPP0008bHx8fs3XrVmOMMQ899JCJjIw0CQkJZu3ateaqq64yHTt2tJY/efKkad68uYmNjTUbNmww3333nQkNDTWjR4+2anbv3m0CAgLM8OHDzfbt282kSZOMt7e3WbBggVUza9Ys4+vra6ZNm2a2bdtmHnzwQRMSEmIOHz5s1XhSL2ezf/9+I4mJiYmJiYmJiYmJyYbT/v37z5gHPO7L0GvWrKlXXnlFt99+u2rXrq1PPvlEt99+uyRp586duuyyy5SYmKirrrpK8+fP14033qiDBw9aV9SmTp2qUaNG6ffff5evr69GjRqlb7/9Vlu3brW2cddddyktLU0LFiyQJHXo0EFXXnml3nrrLUl/XgmLjIzUo48+qqeeekrp6eke00tppKenKyQkRPv371dQUFCZzwUAAAAAz5GRkaHIyEilpaUpODj4tHVuvV3zVAUFBZo9e7ays7MVExOjdevWKT8/X7GxsVZN06ZNVb9+fStYJSYmqkWLFi63TMbFxWnIkCHatm2b2rRpo8TERJd1FNUMGzZMkpSXl6d169Zp9OjR1riXl5diY2OVmJgoSR7VS0lyc3OVm5trvc7MzJQkBQUFEfIAAAAAmznbR7Lc/uCVLVu2KDAwUE6nUw899JDm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3JoDAAAAAJybMoW8F198Uf7+/pKkxMREvfXWW5owYYJCQ0P1xBNPlGuDAAAAAIDSO6evUCiyf/9+NWrUSJI0d+5c3X777Ro8eLCuvvpqde3atTz7AwAAAACcgzJdyQsMDNSRI0ckST/88IOuv/56SZKfn59ycnLKrzsAAAAAwDkp05W866+/Xg888IDatGmjn3/+WTfccIMkadu2bYqOji7P/gAAAAAA56BMV/ImT56smJgY/f777/riiy9Uq1YtSdK6devUr1+/Uq9n2bJluummmxQRESGHw6G5c+e6jN97771yOBwuU48ePVxqjh49qv79+ysoKEghISG6//77lZWV5VKzefNmderUSX5+foqMjNSECROK9TJ79mw1bdpUfn5+atGihb777juXcWOMxowZo7p168rf31+xsbH65Zdf3NILAAAAAJxOmUJeSEiI3nrrLX311VcuoWvcuHH65z//Wer1ZGdnq1WrVpo8efJpa3r06KFDhw5Z0//+9z+X8f79+2vbtm2Kj4/XvHnztGzZMg0ePNgaz8jIUPfu3RUVFaV169bplVde0bPPPqt3333Xqlm5cqX69eun+++/Xxs2bFDv3r3Vu3dvbd261aqZMGGC3nzzTU2dOlWrVq1S9erVFRcXpxMnTlR4LwAAAABwOg5jjCnLgmlpaVq9erVSU1NVWFj4/yt0ODRgwIBzb8Th0Jw5c9S7d29r3r333qu0tLRiV/iK7NixQ82aNdOaNWt0xRVXSJIWLFigG264Qb/99psiIiI0ZcoU/fOf/1RKSop8fX0lSU899ZTmzp2rnTt3SpL69u2r7OxszZs3z1r3VVddpdatW2vq1KkyxigiIkIjRozQP/7xD0lSenq6wsLCNH36dN11110V1ktpZGRkKDg4WOnp6QoKCirVMkBllZ2drcDAQElSVlaWqlev7uaOAAAALozSvs8v05W8b775RvXr11ePHj30yCOP6PHHH3eZytOSJUtUp04dNWnSREOGDLEe+CL9+fUNISEhVqiSpNjYWHl5eWnVqlVWTefOna1QJUlxcXFKSkrSsWPHrJrY2FiX7cbFxSkxMVGSlJycrJSUFJea4OBgdejQwaqpqF4AAAAA4EzKFPJGjBihQYMGKSsrS2lpaTp27Jg1HT16tNya69Gjhz766CMlJCTo5Zdf1tKlS9WzZ08VFBRIklJS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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot the first targeted mass feature\n", + "if len(lcms_obj.mass_features) > 0:\n", + " first_mf_id = list(lcms_obj.mass_features.keys())[0]\n", + " first_mf = lcms_obj.mass_features[first_mf_id]\n", + " \n", + " # Determine what to plot based on available data\n", + " to_plot = [\"EIC\", \"MS1\"]\n", + " if len(first_mf.ms2_mass_spectra) > 0:\n", + " to_plot.append(\"MS2\")\n", + " \n", + " print(f\"Plotting mass feature {first_mf_id}:\")\n", + " print(f\" m/z = {first_mf.mz:.4f}\")\n", + " print(f\" RT = {first_mf.retention_time:.4f} min\")\n", + " print(f\" Intensity = {first_mf.intensity:.2e}\")\n", + " \n", + " first_mf.plot(to_plot=to_plot, return_fig=False)" + ] + }, + { + "cell_type": "markdown", + "id": "d5721028", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "This tutorial demonstrated:\n", + "\n", + "1. ✓ **Targeted Search Setup**: Defining target compounds with m/z and RT values\n", + "2. ✓ **Selective Peak Picking**: Finding only features matching target criteria\n", + "3. ✓ **Integration & Quantification**: Calculating areas and intensities for targets\n", + "4. ✓ **Target Verification**: Confirming recovery and measuring deviations\n", + "5. ✓ **MS2 Association**: Linking fragmentation spectra to target compounds\n", + "6. ✓ **Data Export**: Saving results with persistent metadata\n", + "7. ✓ **Visualization**: Plotting EICs and spectra for quality control\n", + "\n", + "### Key Advantages of Targeted Search\n", + "\n", + "- **Speed**: Much faster than untargeted analysis (only processes specific regions)\n", + "- **Sensitivity**: Can use optimized parameters for known compounds\n", + "- **Quality Control**: Easily verify internal standards and spike-in recovery\n", + "- **Quantification**: Direct integration of known compounds for quantitative workflows\n", + "- **Organization**: Target features labeled with 'type' attribute for easy filtering\n", + "\n", + "### Next Steps\n", + "\n", + "- Use targeted search for internal standard verification in batch processing\n", + "- Combine with spectral library matching for MS2 confirmation\n", + "- Apply to quality control workflows for method validation\n", + "- Integrate with quantitative workflows for targeted metabolomics\n", + "\n", + "### Parameters to Adjust\n", + "\n", + "- `mz_tolerance_ppm`: Increase for lower resolution instruments or uncertain m/z values\n", + "- `rt_tolerance`: Increase for chromatographic drift or gradient variations\n", + "- `type`: Change label to categorize different target groups (e.g., \"QC\", \"IS\", \"metabolite\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From d7015b6ab5650a333a746675a578373c776e0a47 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 4 Feb 2026 09:08:34 -0800 Subject: [PATCH 133/158] Modify ms2 mirror plotting to accept a list of spectral libraries --- .../factory/chroma_peak_classes.py | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/corems/chroma_peak/factory/chroma_peak_classes.py b/corems/chroma_peak/factory/chroma_peak_classes.py index c09564f7f..12df343dc 100644 --- a/corems/chroma_peak/factory/chroma_peak_classes.py +++ b/corems/chroma_peak/factory/chroma_peak_classes.py @@ -382,8 +382,8 @@ def _plot_ms2_mirror(self, ax, molecular_metadata=None, spectral_library=None): Dictionary mapping molecular IDs to MetaboliteMetadata objects. If provided, uses metadata for compound names. Default is None. - spectral_library : FlashEntropySearch, optional - FlashEntropy spectral library containing MS2 spectra. + spectral_library : FlashEntropySearch or list of FlashEntropySearch, optional + FlashEntropy spectral library (or list of libraries) containing MS2 spectra. If provided, uses library to retrieve MS2 spectra by ref_ms_id. Default is None. @@ -444,9 +444,26 @@ def _plot_ms2_mirror(self, ax, molecular_metadata=None, spectral_library=None): # Get library spectrum from spectral_library using ref_ms_id if spectral_library is not None and ref_ms_id is not None: - # Get the IDs in the spectral library - fe_spec_index = [x["id"] for x in spectral_library].index(ref_ms_id) - library_ms2_peaks = spectral_library[fe_spec_index]['peaks'] + # Handle both single library and list of libraries + libraries = spectral_library if isinstance(spectral_library, list) else [spectral_library] + + # Search through all libraries to find the ref_ms_id + for library in libraries: + try: + # Get the IDs in the spectral library + fe_spec_index = [x["id"] for x in library].index(ref_ms_id) + library_ms2_peaks = library[fe_spec_index]['peaks'] + break # Found the spectrum, exit the loop + except ValueError: + # ref_ms_id not found in this library, continue to next + continue + + # If ref_ms_id was not found in any library, raise an error + if library_ms2_peaks is None: + raise ValueError( + f"Reference MS ID '{ref_ms_id}' not found in any of the provided spectral libraries. " + f"Please ensure the spectral library contains the matching reference spectrum." + ) # Get compound name from molecular_metadata using mol_id if molecular_metadata is not None and mol_id is not None: From a0a6dde42988c425766160eee10246af4f098b75 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 18 Feb 2026 16:47:13 -0800 Subject: [PATCH 134/158] Add [M]+ to ion type dictionary --- corems/mass_spectra/output/export.py | 1 + 1 file changed, 1 insertion(+) diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index 2309e5a45..4e6b4915b 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -31,6 +31,7 @@ ion_type_dict = { # adduct : [atoms to add, atoms to subtract when calculating formula of ion "M+": [{}, {}], + "[M]+": [{}, {}], "protonated": [{"H": 1}, {}], "[M+H]+": [{"H": 1}, {}], "[M+NH4]+": [{"N": 1, "H": 4}, {}], # ammonium From 0d62037515c431cbce62b6c5a869c1305ad89543 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 25 Feb 2026 15:36:18 -0800 Subject: [PATCH 135/158] Better handle scenarios with no gaps to fill for collection processing --- .../mass_spectra/calc/lc_calc_operations.py | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc_operations.py b/corems/mass_spectra/calc/lc_calc_operations.py index f6c4738ed..65eaf5d84 100644 --- a/corems/mass_spectra/calc/lc_calc_operations.py +++ b/corems/mass_spectra/calc/lc_calc_operations.py @@ -207,7 +207,7 @@ def can_execute(self, sample, collection): return hasattr(collection, 'cluster_summary_dataframe') and \ collection.cluster_summary_dataframe is not None - def execute(self, sample_id, collection, missingdf, cluster_dict, expand_on_miss, **runtime_params): + def execute(self, sample_id, collection, **runtime_params): """ Execute gap-filling for a single sample. @@ -217,20 +217,27 @@ def execute(self, sample_id, collection, missingdf, cluster_dict, expand_on_miss Sample index to process collection : LCMSBaseCollection The collection - missingdf : pd.DataFrame - DataFrame with cluster information and missing samples - cluster_dict : dict - Cluster feature dictionary - expand_on_miss : bool - Whether to expand search window on miss **runtime_params - Additional runtime parameters (ignored) + 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 + Dictionary of induced mass features (empty if no gaps to fill) """ + # Extract gap-fill parameters from runtime_params + # If not present, there are no gaps to fill, so return early + if 'missingdf' not in runtime_params: + return {} + + missingdf = runtime_params['missingdf'] + cluster_dict = runtime_params['cluster_dict'] + expand_on_miss = runtime_params['expand_on_miss'] + # This is essentially the same logic as _search_for_targeted_mass_features_in_sample # but extracted into an operation From b7c41443b1f98029cb54ed8a0bd30e8ed3fdcee7 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 25 Feb 2026 15:56:53 -0800 Subject: [PATCH 136/158] Improve handling of LCMSCollections parameters --- corems/encapsulation/factory/parameters.py | 40 +++++++++++++++++++ .../factory/processingSetting.py | 12 +++--- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/corems/encapsulation/factory/parameters.py b/corems/encapsulation/factory/parameters.py index 9f1dd1500..ce3d9a382 100644 --- a/corems/encapsulation/factory/parameters.py +++ b/corems/encapsulation/factory/parameters.py @@ -286,8 +286,48 @@ def print(self): class LCMSCollectionParameters: + """LCMSCollectionParameters class is used to store the parameters used for the processing of the LCMS collection + + Each attribute is a class that contains the parameters for the processing of the LCMS collection, + see the corems.encapsulation.factory.processingSetting module for more details. + + Parameters + ---------- + use_defaults: bool, optional + if True, the class will be instantiated with the default values, otherwise the current values will be used. + Default is False. + + Attributes + ----------- + lcms_collection: LCMSCollectionSettings + LCMSCollectionSettings object + + Notes + ----- + One can use the use_defaults parameter to reset the parameters to the default values. + Alternatively, to use the current values - modify the class's contents before instantiating the class. + """ + lcms_collection = LCMSCollectionSettings() + def __init__(self, use_defaults=False) -> None: + if not use_defaults: + self.lcms_collection = dataclasses.replace(LCMSCollectionParameters.lcms_collection) + else: + self.lcms_collection = LCMSCollectionSettings() + + def copy(self): + """Create a copy of the LCMSCollectionParameters object""" + new_lcms_collection_parameters = LCMSCollectionParameters() + new_lcms_collection_parameters.lcms_collection = dataclasses.replace(self.lcms_collection) + return new_lcms_collection_parameters + + def __eq__(self, value: object) -> bool: + # Check that the object is of the same type + if not isinstance(value, LCMSCollectionParameters): + return False + return self.lcms_collection == value.lcms_collection + def default_parameters(file_location): # pragma: no cover """Generate parameters dictionary with the default parameters for data processing To gather parameters from instrument data during the data parsing step, a parameters dictionary with the default parameters needs to be generated. diff --git a/corems/encapsulation/factory/processingSetting.py b/corems/encapsulation/factory/processingSetting.py index 49117c412..499eed82e 100644 --- a/corems/encapsulation/factory/processingSetting.py +++ b/corems/encapsulation/factory/processingSetting.py @@ -1174,7 +1174,7 @@ class LCMSCollectionSettings: Default is ('intensity', 'intensity_prefer_ms2'). """ # Settings for general processing - cores = 1 + cores: int = 1 drop_isotopologues: bool = False # Settings for doing mass feature alignment @@ -1190,11 +1190,11 @@ class LCMSCollectionSettings: alignment_rt_tol: float = 0.4 # Consensus mass feature settings - consensus_mz_tol_ppm = alignment_mz_tol_ppm - consensus_rt_tol = 0.3 - consensus_partition_size = 10000 - filter_consensus_mass_features = True - consensus_min_sample_fraction = 0.5 + consensus_mz_tol_ppm: int = alignment_mz_tol_ppm + consensus_rt_tol: float = 0.3 + consensus_partition_size: int = 10000 + filter_consensus_mass_features: bool = True + consensus_min_sample_fraction: float = 0.5 # Gap-filling settings gap_fill_expand_on_miss: bool = True From 020eb73152e83d7da503a24f330153d0d867fde9 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 25 Feb 2026 17:56:51 -0800 Subject: [PATCH 137/158] Add parameter to be able to do multiple rounds of finding mass features --- corems/mass_spectra/calc/lc_calc.py | 61 ++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index e03df5ed9..42770f4bb 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -358,7 +358,7 @@ def get_average_mass_spectrum( return ms def find_mass_features(self, ms_level=1, grid=True, assign_ms2_scans=False, ms2_scan_filter=None, - targeted_search=False, target_search_dict=None): + targeted_search=False, target_search_dict=None, accumulate_features=False): """Find mass features within an LCMSBase object Note that this is a wrapper function that calls the find_mass_features_ph function, but can be extended to support other peak picking methods in the future. @@ -392,6 +392,10 @@ def find_mass_features(self, ms_level=1, grid=True, assign_ms2_scans=False, ms2_ - 'type': type label for mass features (e.g., "internal standard") If not provided, defaults to "targeted" Default is None. + accumulate_features : bool, optional + If True, new mass features will be added to existing features rather than replacing them. + This allows multiple sequential calls to find_mass_features to build up a combined set. + Default is False (replace existing features for backwards compatibility). Raises ------ @@ -431,7 +435,8 @@ def find_mass_features(self, ms_level=1, grid=True, assign_ms2_scans=False, ms2_ self.find_mass_features_ph(ms_level=ms_level, grid=grid, targeted_search=targeted_search, target_search_dict=target_search_dict, - mf_type=mf_type) + mf_type=mf_type, + accumulate_features=accumulate_features) else: raise ValueError( "MS{} scans are not profile mode, which is required for persistent homology peak picking.".format( @@ -449,7 +454,8 @@ def find_mass_features(self, ms_level=1, grid=True, assign_ms2_scans=False, ms2_ self.find_mass_features_ph_centroid(ms_level=ms_level, targeted_search=targeted_search, target_search_dict=target_search_dict, - mf_type=mf_type) + mf_type=mf_type, + accumulate_features=accumulate_features) else: raise ValueError( "MS{} scans are not centroid mode, which is required for persistent homology centroided peak picking.".format( @@ -1950,7 +1956,7 @@ def _filter_data_by_targets(self, data, target_search_dict): return data[mask].reset_index(drop=True) - def find_mass_features_ph(self, ms_level=1, grid=True, targeted_search=False, target_search_dict=None, mf_type="untargeted"): + def find_mass_features_ph(self, ms_level=1, grid=True, targeted_search=False, target_search_dict=None, mf_type="untargeted", accumulate_features=False): """Find mass features within an LCMSBase object using persistent homology. Assigns the mass_features attribute to the object (a dictionary of LCMSMassFeature objects, keyed by mass feature id) @@ -1967,6 +1973,8 @@ def find_mass_features_ph(self, ms_level=1, grid=True, targeted_search=False, ta Dictionary with target parameters for targeted search. Default is None. mf_type : str, optional Type label for the mass features. Default is "untargeted". + accumulate_features : bool, optional + If True, add to existing features rather than replacing them. Default is False. Raises ------ @@ -2044,7 +2052,7 @@ def find_mass_features_ph(self, ms_level=1, grid=True, targeted_search=False, ta data_thres, dims, persistence_threshold, mf_type ) - def _find_mass_features_ph_single(self, data_thres, dims, persistence_threshold, mf_type="untargeted"): + def _find_mass_features_ph_single(self, data_thres, dims, persistence_threshold, mf_type="untargeted", accumulate_features=False): """Process all data at once (original logic).""" # Build factors factors = { @@ -2077,9 +2085,9 @@ def _find_mass_features_ph_single(self, data_thres, dims, persistence_threshold, mass_features_df = mass_features_df.reset_index(drop=True) # Populate mass_features attribute - self._populate_mass_features(mass_features_df, mf_type) + self._populate_mass_features(mass_features_df, mf_type, accumulate_features) - def _find_mass_features_ph_partition(self, data_thres, dims, persistence_threshold, mf_type="untargeted"): + def _find_mass_features_ph_partition(self, data_thres, dims, persistence_threshold, mf_type="untargeted", accumulate_features=False): """Partition the persistent homology mass feature detection for large datasets. This method splits the input data into overlapping scan partitions, processes each partition to detect mass features @@ -2095,6 +2103,8 @@ def _find_mass_features_ph_partition(self, data_thres, dims, persistence_thresho Minimum persistence value required for a detected mass feature to be retained. mf_type : str, optional Type label for the mass features. Default is "untargeted". + accumulate_features : bool, optional + If True, add to existing features rather than replacing them. Default is False. Returns ------- @@ -2198,7 +2208,7 @@ def _find_mass_features_ph_partition(self, data_thres, dims, persistence_thresho combined_features = combined_features.reset_index(drop=True) # Populate mass_features attribute - self._populate_mass_features(combined_features, mf_type) + self._populate_mass_features(combined_features, mf_type, accumulate_features) else: self.mass_features = {} @@ -2261,7 +2271,7 @@ def _process_partition_ph(self, partition_data, index, dims, persistence_thresho return mass_features - def _populate_mass_features(self, mass_features_df, mf_type="untargeted"): + def _populate_mass_features(self, mass_features_df, mf_type="untargeted", accumulate_features=False): """Populate the mass_features attribute from a DataFrame. Parameters @@ -2271,28 +2281,47 @@ def _populate_mass_features(self, mass_features_df, mf_type="untargeted"): Note that the order of this DataFrame will determine the order of mass features in the mass_features attribute. mf_type : str, optional Type label for the mass features. Default is "untargeted". + accumulate_features : bool, optional + If True, new features will be added to existing features rather than replacing them. + Mass feature IDs will be offset to avoid conflicts. Default is False. Returns ------- - None, but assigns the mass_features attribute to the object. + None, but assigns or updates the mass_features attribute to the object. """ # Rename scan column to apex_scan mass_features_df = mass_features_df.rename( columns={"scan": "apex_scan", "scan_time": "retention_time"} ) - # Populate mass_features attribute - self.mass_features = {} + # Initialize or preserve existing mass_features attribute + if accumulate_features and self.mass_features is not None and len(self.mass_features) > 0: + # Find the maximum existing ID to offset new IDs and avoid conflicts + id_offset = max(self.mass_features.keys()) + 1 + initial_count = len(self.mass_features) + else: + # Replace mode (default/backwards compatible) + self.mass_features = {} + id_offset = 0 + initial_count = 0 + + # Add new mass features for row in mass_features_df.itertuples(): row_dict = mass_features_df.iloc[row.Index].to_dict() lcms_feature = LCMSMassFeature(self, **row_dict) lcms_feature.type = mf_type - self.mass_features[lcms_feature.id] = lcms_feature + # Use offset ID to avoid conflicts with existing features + new_id = lcms_feature.id + id_offset + lcms_feature._id = new_id # Update the internal ID + self.mass_features[new_id] = lcms_feature if self.parameters.lc_ms.verbose_processing: - print("Found " + str(len(mass_features_df)) + " initial mass features") + if accumulate_features and initial_count > 0: + print(f"Found {len(mass_features_df)} new mass features (total: {len(self.mass_features)})") + else: + print("Found " + str(len(mass_features_df)) + " initial mass features") - def find_mass_features_ph_centroid(self, ms_level=1, targeted_search=False, target_search_dict=None, mf_type="untargeted"): + def find_mass_features_ph_centroid(self, ms_level=1, targeted_search=False, target_search_dict=None, mf_type="untargeted", accumulate_features=False): """Find mass features within an LCMSBase object using persistent homology-type approach but with centroided data. Parameters @@ -2305,6 +2334,8 @@ def find_mass_features_ph_centroid(self, ms_level=1, targeted_search=False, targ Dictionary with target parameters for targeted search. Default is None. mf_type : str, optional Type label for the mass features. Default is "untargeted". + accumulate_features : bool, optional + If True, add to existing features rather than replacing them. Default is False. Raises ------ From f531a393b7079d374b0c88b37ae2fba338b8c5a4 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 25 Feb 2026 17:57:35 -0800 Subject: [PATCH 138/158] Add parameter to be able to do multiple rounds of finding mass features --- corems/mass_spectra/calc/lc_calc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 42770f4bb..2bde197e4 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -2044,10 +2044,13 @@ def find_mass_features_ph(self, ms_level=1, grid=True, targeted_search=False, ta # Process in chunks if required if len(data_thres) > 10000: return self._find_mass_features_ph_partition( - data_thres, dims, persistence_threshold, mf_type + data_thres, dims, persistence_threshold, mf_type, accumulate_features ) else: # Process all at once + return self._find_mass_features_ph_single( + data_thres, dims, persistence_threshold, mf_type, accumulate_features + ) return self._find_mass_features_ph_single( data_thres, dims, persistence_threshold, mf_type ) From c4e0ba1248b3b2aca2314686a15eb0c6076c029f Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 26 Feb 2026 09:38:43 -0800 Subject: [PATCH 139/158] Better handling low number of peaks for alignment --- corems/encapsulation/factory/processingSetting.py | 6 ++++++ corems/mass_spectra/calc/lc_calc.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/corems/encapsulation/factory/processingSetting.py b/corems/encapsulation/factory/processingSetting.py index 499eed82e..574a91242 100644 --- a/corems/encapsulation/factory/processingSetting.py +++ b/corems/encapsulation/factory/processingSetting.py @@ -1130,6 +1130,11 @@ class LCMSCollectionSettings: For example, 0.6 removes the lower 60% of intensity features. Used when mass_feature_anchor_technique includes 'relative_intensity'. Default is 0.6. + alignment_minimum_matches: int, optional + Minimum number of matched features required to attempt retention time alignment. + If fewer matches are found between samples, alignment will be skipped for that sample. + This is particularly useful when aligning blank samples or samples with very few features. + Default is 5. alignment_hold_out_fraction: float, optional Hold out fraction for testing retention time alignment. Default is 0.3. @@ -1182,6 +1187,7 @@ class LCMSCollectionSettings: mass_feature_anchor_techniques_available: tuple = ("deconvoluted_mass_spectra", "absolute_intensity", "relative_intensity") mass_feature_anchor_absolute_intensity_threshold: int = 10000 mass_feature_anchor_relative_intensity_threshold: float = 0.6 + alignment_minimum_matches: int = 5 alignment_hold_out_fraction: float = 0.3 _alignment_acceptance_technique: list = dataclasses.field(default_factory=lambda: ["fraction_improved", "mean_squared_error_improved"]) alignment_acceptance_techniques_available: tuple = ("fraction_improved", "mean_squared_error_improved") diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 2bde197e4..f1ca82398 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -2874,6 +2874,11 @@ def fit_rts(self, a, b, align="scan_time", **kwargs): arr = np.vstack((x, y)).T arr = np.unique(arr, axis=0) + # Safety check: ensure we have data to work with + if len(arr) == 0: + warnings.warn("No data points available for retention time fitting. Returning identity function.") + return lambda x: x + # Check kwargs if "kernel" in kwargs: kernel = kwargs.get("kernel") @@ -2965,6 +2970,16 @@ def attempt_alignment(self, matches_c, matches_i): matches_c.reset_index(drop=False, inplace=True) matches_i.reset_index(drop=False, inplace=True) + # Check if there are enough matches to attempt alignment + minimum_matches = self.parameters.lcms_collection.alignment_minimum_matches + if len(matches_c) < minimum_matches: + warnings.warn( + f"Insufficient matches ({len(matches_c)}) for alignment. " + f"Minimum required: {minimum_matches}. Skipping alignment for this sample." + ) + # Return False (no alignment) and identity function (returns original time) + return False, lambda x: x + # Rearrange matches_c and matches_i to be in the order of the scan_time of matches_c matches_c = matches_c.sort_values(by="scan_time") matches_i = matches_i.iloc[matches_c.index.values] From 9371c00414996897a513b1a66c1c5e45c16b2d11 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 26 Feb 2026 09:44:22 -0800 Subject: [PATCH 140/158] Skip autogeneration of manifest if it already exists --- corems/mass_spectra/input/corems_hdf5.py | 25 +++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 7b515e1f9..8ed6c5077 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -935,7 +935,8 @@ class ReadCoreMSHDFMassSpectraCollection: manifest_file : Path, optional Manifest CSV with columns: sample_name, order, batch, center, time. One sample must have center='TRUE' for RT alignment. - If None, auto-generates from folder contents. Default: None. + If None, checks if auto-generated manifest_auto.csv exists in the folder. If not, + auto-generates from folder contents. Default: None. cores : int, optional Number of cores for multiprocessing. Default: 1. auto_manifest_batch_threshold_hours : float, optional @@ -968,14 +969,20 @@ def __init__( # Auto-generate manifest if not provided if manifest_file is None: - print(f"No manifest file provided. Auto-generating manifest from {folder_location}") - manifest_file = create_manifest_from_folder( - folder_path=folder_location, - output_path=folder_location / "manifest_auto.csv", - batch_time_threshold_hours=auto_manifest_batch_threshold_hours, - center_name=auto_manifest_center_name, - overwrite=True - ) + # Check if manifest_auto.csv already exists + auto_manifest_path = folder_location / "manifest_auto.csv" + if auto_manifest_path.exists(): + print(f"No manifest file provided. Using existing manifest_auto.csv from {folder_location}") + manifest_file = auto_manifest_path + else: + print(f"No manifest file provided. Auto-generating manifest from {folder_location}") + manifest_file = create_manifest_from_folder( + folder_path=folder_location, + output_path=auto_manifest_path, + batch_time_threshold_hours=auto_manifest_batch_threshold_hours, + center_name=auto_manifest_center_name, + overwrite=True + ) else: manifest_file = Path(manifest_file) if not manifest_file.exists(): From 81740cce8456ff3855b32fe9bd5c438f11c8db47 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 26 Feb 2026 13:27:40 -0800 Subject: [PATCH 141/158] Add option for accumulation of MS2 search results for sequential searches --- .../search/lcms_spectral_search.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/corems/molecular_id/search/lcms_spectral_search.py b/corems/molecular_id/search/lcms_spectral_search.py index dd9c9a7df..f893ee996 100644 --- a/corems/molecular_id/search/lcms_spectral_search.py +++ b/corems/molecular_id/search/lcms_spectral_search.py @@ -127,6 +127,7 @@ def fe_search( use_mass_features=True, peak_sep_da=0.01, get_additional_metrics=True, + accumulate_results=False, ): """ Search LCMS spectra using a FlashEntropy approach. @@ -149,6 +150,10 @@ def fe_search( instance, by default 0.01. get_additional_metrics : bool, optional If True, get additional metrics from FlashEntropy search, by default True. + accumulate_results : bool, optional + If True, accumulate results with existing spectral_search_results instead of + replacing them. This allows searching the same scans with multiple libraries + without overwriting previous results, by default False. Returns ------- @@ -301,7 +306,18 @@ def fe_search( ) # Add MS2SearchResults to the existing spectral search results dictionary - self.spectral_search_results.update(overall_results_dict) + if accumulate_results: + # Merge results with existing spectral_search_results + for scan_id, precursor_dict in overall_results_dict.items(): + if scan_id in self.spectral_search_results: + # Scan already has results, merge precursor_mz dictionaries + self.spectral_search_results[scan_id].update(precursor_dict) + else: + # New scan, add entire dictionary + self.spectral_search_results[scan_id] = precursor_dict + else: + # Replace existing results (original behavior) + self.spectral_search_results.update(overall_results_dict) # If there are mass features, associate the results with each mass feature if len(self.mass_features) > 0: From 5bb0b227ab15a8c6fc3b5c1592258219d093f865 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 26 Feb 2026 13:28:58 -0800 Subject: [PATCH 142/158] Add time range filtering on LCMS parsers and use in gap filling --- corems/mass_spectra/calc/lc_calc.py | 77 +++-- .../mass_spectra/calc/lc_calc_operations.py | 44 ++- corems/mass_spectra/factory/lc_class.py | 11 +- corems/mass_spectra/input/corems_hdf5.py | 72 +++- corems/mass_spectra/input/mzml.py | 118 ++++++- corems/mass_spectra/input/parserbase.py | 149 ++++++++- corems/mass_spectra/input/rawFileReader.py | 134 +++++++- tests/test_time_range_filtering.py | 312 ++++++++++++++++++ 8 files changed, 847 insertions(+), 70 deletions(-) create mode 100644 tests/test_time_range_filtering.py diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index f1ca82398..944d4d17b 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -2973,11 +2973,9 @@ def attempt_alignment(self, matches_c, matches_i): # Check if there are enough matches to attempt alignment minimum_matches = self.parameters.lcms_collection.alignment_minimum_matches if len(matches_c) < minimum_matches: - warnings.warn( - f"Insufficient matches ({len(matches_c)}) for alignment. " - f"Minimum required: {minimum_matches}. Skipping alignment for this sample." - ) - # Return False (no alignment) and identity function (returns original time) + # Return False (no alignment) and identity function (returns original time) + # which isn't used but is a placeholder to avoid errors in downstream code since + # the function expects a callable to be returned return False, lambda x: x # Rearrange matches_c and matches_i to be in the order of the scan_time of matches_c @@ -5234,8 +5232,10 @@ def process_samples_pipeline(self, operations, description=None, keep_raw_data=F sample_id, operations, runtime_params, inplace=True ) # Collect results (collect_results already called in _execute_sample_pipeline when inplace=True) + # Skip 'sample_id' key which is added for tracking for op_name, result in sample_results.items(): - results_by_operation[op_name][sample_id] = result + if op_name != 'sample_id': + results_by_operation[op_name][sample_id] = result else: # Parallel processing import multiprocessing @@ -5253,30 +5253,48 @@ def process_samples_pipeline(self, operations, description=None, keep_raw_data=F for sample_id in range(sample_ct) ] - # Execute in parallel - mp_results = pool.starmap(self._execute_sample_pipeline, args_list) - pool.close() - pool.join() - - # Collect results back into collection + # Execute in parallel with progress tracking results_by_operation = {op.name: {} for op in operations} if show_progress: from tqdm import tqdm - print(f"\nCollecting {description} results:") - iterator = tqdm(range(sample_ct), unit="sample", ncols=80) + import time + print(f"\nProcessing {sample_ct} samples with {ncores} cores ({description}):") + + # Use starmap_async for parallel execution with progress tracking + async_result = pool.starmap_async(self._execute_sample_pipeline, args_list) + + # Poll for completion and update progress bar + pbar = tqdm(total=sample_ct, unit="sample", ncols=80) + while not async_result.ready(): + # Get number of completed tasks by checking remaining + completed = sample_ct - async_result._number_left + pbar.n = completed + pbar.refresh() + time.sleep(0.1) # Poll every 100ms + + # Final update to 100% + pbar.n = sample_ct + pbar.refresh() + pbar.close() + + # Get all results + mp_results = async_result.get() else: - iterator = range(sample_ct) + # Execute without progress + mp_results = pool.starmap(self._execute_sample_pipeline, args_list) - for sample_id in iterator: - sample_results = mp_results[sample_id] - - # Let each operation collect its results - for i, op in enumerate(operations): - result = sample_results.get(op.name) - if result is not None: - op.collect_results(sample_id, result, self) - results_by_operation[op.name][sample_id] = result + pool.close() + pool.join() + + # Collect results back into collection + for result in mp_results: + sample_id = result.get('sample_id') + for op in operations: + op_result = result.get(op.name) + if op_result is not None: + op.collect_results(sample_id, op_result, self) + results_by_operation[op.name][sample_id] = op_result return results_by_operation @@ -5485,6 +5503,8 @@ def _execute_sample_pipeline(self, sample_id, operations, runtime_params, inplac if ms_level in self[sample_id]._ms_unprocessed: del self[sample_id]._ms_unprocessed[ms_level] + # Include sample_id in results for tracking (especially important for imap_unordered) + results['sample_id'] = sample_id return results def process_consensus_features(self, load_representatives=True, perform_gap_filling=True, @@ -5493,7 +5513,8 @@ def process_consensus_features(self, load_representatives=True, perform_gap_fill ms2_spectral_search=False, spectral_lib=None, molecular_metadata=None, gather_eics=False, - keep_raw_data=False): + keep_raw_data=False, + show_progress=True): """ Process consensus mass features across the collection in a single parallelized pass. @@ -5544,6 +5565,9 @@ def process_consensus_features(self, load_representatives=True, perform_gap_fill keep_raw_data : bool, optional If True, keeps raw MS data loaded in memory after pipeline completes. If False, cleans up raw data to free memory. Default is False. + show_progress : bool, optional + If True, displays progress bars during processing. If False, runs silently. + Default is True. Returns ------- @@ -5671,7 +5695,8 @@ def process_consensus_features(self, load_representatives=True, perform_gap_fill # Execute pipeline (description auto-generated from operations) results = self.process_samples_pipeline( operations, - keep_raw_data=keep_raw_data + keep_raw_data=keep_raw_data, + show_progress=show_progress ) # Store molecular metadata if spectral search was performed diff --git a/corems/mass_spectra/calc/lc_calc_operations.py b/corems/mass_spectra/calc/lc_calc_operations.py index 65eaf5d84..e00fe809e 100644 --- a/corems/mass_spectra/calc/lc_calc_operations.py +++ b/corems/mass_spectra/calc/lc_calc_operations.py @@ -180,6 +180,11 @@ class GapFillOperation(SampleOperation): 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 @@ -191,6 +196,8 @@ class GapFillOperation(SampleOperation): ----- 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. """ @property @@ -250,8 +257,41 @@ def execute(self, sample_id, collection, **runtime_params): if len(sampledf) == 0: return {} - # Load raw data for this sample - collection.load_raw_data(sample_id, 1) + # Get buffer time from LCMS parameters for EIC extraction + # This ensures we capture the full chromatographic peak beyond cluster bounds + buffer_rt = collection[sample_id].parameters.lc_ms.eic_buffer_time + + # Calculate time ranges for efficient loading with buffer for EIC extraction + time_ranges = [] + + for _, row in sampledf.iterrows(): + rt_min = row['scan_time_aligned_min'] + rt_max = row['scan_time_aligned_max'] + + # If expand_on_miss, also consider the allowed bounds + if expand_on_miss: + rt_min = min(rt_min, row['sta_min_allowed']) + rt_max = max(rt_max, row['sta_max_allowed']) + + # Apply buffer AFTER considering expand_on_miss bounds + # This ensures buffer is added beyond even the expanded search window + time_ranges.append((max(0, rt_min - buffer_rt), rt_max + buffer_rt)) + + # Merge overlapping time ranges to reduce number of separate loads + time_ranges = sorted(time_ranges) + merged_ranges = [] + if time_ranges: + current_min, current_max = time_ranges[0] + for rt_min, rt_max in time_ranges[1:]: + if rt_min <= current_max: # Overlapping or adjacent + current_max = max(current_max, rt_max) + else: + merged_ranges.append((current_min, current_max)) + current_min, current_max = rt_min, rt_max + merged_ranges.append((current_min, current_max)) + + # Load raw data for this sample with time range filtering + collection.load_raw_data(sample_id, 1, time_range=merged_ranges) # Get MS1 data ms1df = collection[sample_id]._ms_unprocessed[1].copy() diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 700e95b38..4d79ca88a 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1991,7 +1991,7 @@ def _drop_isotopologues(self): self._combined_mass_features = cmb_mf_df2 - def load_raw_data(self, sample_idx: int, ms_level = 1) -> None: + def load_raw_data(self, sample_idx: int, ms_level = 1, time_range = None) -> None: """Load raw data for a specific sample index in the collection. Parameters @@ -2000,6 +2000,9 @@ def load_raw_data(self, sample_idx: int, ms_level = 1) -> None: The index of the sample in the collection. ms_level : int, optional The MS level to load raw data for. Defaults to 1. + time_range : tuple or list of tuples, optional + Retention time range(s) to load. Can be a single tuple (min, max) or + a list of tuples for multiple ranges. If None, loads all data. Defaults to None. Raises ------- @@ -2034,7 +2037,8 @@ def load_raw_data(self, sample_idx: int, ms_level = 1) -> None: if parser_class_name == "ImportMassSpectraThermoMSFileReader": self[sample_idx]._ms_unprocessed[ms_level] = parser.get_ms_raw( spectra=f"ms{ms_level}", - scan_df=scan_df + scan_df=scan_df, + time_range=time_range )[f"ms{ms_level}"] elif parser_class_name == "MZMLSpectraParser": @@ -2042,7 +2046,8 @@ def load_raw_data(self, sample_idx: int, ms_level = 1) -> None: self[sample_idx]._ms_unprocessed[ms_level] = parser.get_ms_raw( spectra=f"ms{ms_level}", scan_df=scan_df, - data=data + data=data, + time_range=time_range )[f"ms{ms_level}"] elif parser_class_name == "ReadCoreMSHDFMassSpectra": diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 8ed6c5077..8627152d3 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -9,6 +9,7 @@ import multiprocessing from pathlib import Path import datetime +from typing import Union, Tuple, List, Optional import numpy as np import pandas as pd @@ -340,7 +341,7 @@ def load(self) -> None: """ """ pass - def get_ms_raw(self, spectra=None, scan_df=None) -> dict: + def get_ms_raw(self, spectra=None, scan_df=None, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None) -> dict: """ """ # Warn if spectra or scan_df are not None that they are not used for CoreMS HDF5 files and should be rerun after instantiation if spectra is not None or scan_df is not None: @@ -357,7 +358,7 @@ def get_ms_raw(self, spectra=None, scan_df=None) -> dict: ) return ms_unprocessed - def get_scan_df(self) -> pd.DataFrame: + def get_scan_df(self, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None) -> pd.DataFrame: scan_info = {} dict_group_load = self.h5pydata["scan_info"] dict_group_keys = dict_group_load.keys() @@ -369,6 +370,15 @@ def get_scan_df(self) -> pd.DataFrame: str_df = str_df.stack().str.decode("utf-8").unstack() for col in str_df: scan_df[col] = str_df[col] + + # Apply time range filtering if specified + if time_range is not None: + time_ranges = self._normalize_time_range(time_range) + mask = pd.Series([False] * len(scan_df), index=scan_df.index) + for start_time, end_time in time_ranges: + mask |= (scan_df.scan_time >= start_time) & (scan_df.scan_time <= end_time) + scan_df = scan_df[mask] + return scan_df def run(self, mass_spectra, load_raw=True, load_light=False) -> None: @@ -724,7 +734,7 @@ def import_spectral_search_results(self, mass_spectra): ] ) - def get_mass_spectra_obj(self, load_raw=True, load_light=False) -> MassSpectraBase: + def get_mass_spectra_obj(self, load_raw=True, load_light=False, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None) -> MassSpectraBase: """ Return mass spectra data object, populating the _ms list on MassSpectraBase object from the HDF5 file. @@ -734,6 +744,12 @@ def get_mass_spectra_obj(self, load_raw=True, load_light=False) -> MassSpectraBa If True, load raw data (unprocessed) from HDF5 files for overall spectra object and individual mass spectra. Default is True. load_light : bool If True, only load the parameters, mass features, and scan info. Default is False. + time_range : tuple or list of tuples, optional + Retention time range(s) to load. Can be: + - Single range: (start_time, end_time) in minutes + - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes + If None, loads all scans. Note: For HDF5 files, this parameter is accepted for + interface consistency but not currently used in filtering. """ # Instantiate the LCMS object @@ -750,7 +766,7 @@ def get_mass_spectra_obj(self, load_raw=True, load_light=False) -> MassSpectraBa return spectra_obj def get_lcms_obj( - self, load_raw=True, load_light=False, use_original_parser=True, raw_file_path=None + self, load_raw=True, load_light=False, use_original_parser=True, raw_file_path=None, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None ) -> LCMSBase: """ Return LCMSBase object, populating attributes on the LCMSBase object from the HDF5 file. @@ -767,6 +783,13 @@ def get_lcms_obj( The location of the raw file to parse if attempting to use original parser. Default is None, which attempts to get the raw file path from the HDF5 file. If the original file path has moved, this parameter can be used to specify the new location. + time_range : tuple or list of tuples, optional + Retention time range(s) to load. Can be: + - Single range: (start_time, end_time) in minutes + - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes + If None, loads all scans. Note: For HDF5 files, this parameter is accepted for + interface consistency. If use_original_parser=True, time_range can be passed to the + original parser for filtering. """ # Instantiate the LCMS object lcms_obj = LCMSBase( @@ -923,6 +946,47 @@ def get_instrument_info(self): "This should be accessed through the original parser.", ) return None + + def get_scans_in_time_range( + self, + time_range: Union[Tuple[float, float], List[Tuple[float, float]]], + ms_level: Optional[int] = None + ) -> List[int]: + """Return scan numbers within specified retention time range(s). + + Parameters + ---------- + time_range : tuple or list of tuples + Retention time range(s) in minutes. Can be: + - Single range: (start_time, end_time) + - Multiple ranges: [(start1, end1), (start2, end2), ...] + ms_level : int, optional + If specified, only return scans of this MS level (e.g., 1 for MS1, 2 for MS2). + If None, returns scans of all MS levels. + + Returns + ------- + list of int + List of scan numbers within the specified time range(s) and MS level. + """ + # Normalize time range to list of tuples + time_ranges = self._normalize_time_range(time_range) + + # Get all scan data + scan_df = self.get_scan_df() + + # Filter by time range + mask = pd.Series([False] * len(scan_df), index=scan_df.index) + for start_time, end_time in time_ranges: + mask |= (scan_df.scan_time >= start_time) & (scan_df.scan_time <= end_time) + + filtered_df = scan_df[mask] + + # Filter by MS level if specified + if ms_level is not None: + filtered_df = filtered_df[filtered_df.ms_level == ms_level] + + return filtered_df.scan.tolist() class ReadCoreMSHDFMassSpectraCollection: diff --git a/corems/mass_spectra/input/mzml.py b/corems/mass_spectra/input/mzml.py index 4b106c465..ebc166f63 100644 --- a/corems/mass_spectra/input/mzml.py +++ b/corems/mass_spectra/input/mzml.py @@ -1,5 +1,6 @@ from collections import defaultdict from pathlib import Path +from typing import Optional, Union, List, Tuple import numpy as np import pandas as pd @@ -95,20 +96,27 @@ def load(self): data = pymzml.run.Reader(self.file_location) return data - def get_scan_df(self, data): + def get_scan_df(self, data=None, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None): """ Return scan data as a pandas DataFrame. Parameters ---------- - data : pymzml.run.Reader - The mass spectra data. + data : pymzml.run.Reader, optional + The mass spectra data. If None, will load the data. + time_range : tuple or list of tuples, optional + Retention time range(s) to filter scans. Can be: + - Single range: (start_time, end_time) in minutes + - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes + If None, returns all scans. Returns ------- pandas.DataFrame A pandas DataFrame containing metadata for each scan, including scan number, MS level, polarity, and scan time. """ + if data is None: + data = self.load() # Scan dict # instatinate scan dict, with empty lists of size of scans n_scans = data.get_spectrum_count() @@ -159,10 +167,19 @@ def get_scan_df(self, data): scan_dict["ms_format"][i] = None scan_df = pd.DataFrame(scan_dict) + + # Apply time range filtering if specified + if time_range is not None: + time_ranges = self._normalize_time_range(time_range) + # Create a mask for scans within any of the time ranges + mask = np.zeros(len(scan_df), dtype=bool) + for start_time, end_time in time_ranges: + mask |= (scan_df["scan_time"] >= start_time) & (scan_df["scan_time"] <= end_time) + scan_df = scan_df[mask].reset_index(drop=True) return scan_df - def get_ms_raw(self, spectra, scan_df, data): + def get_ms_raw(self, spectra, scan_df, data=None, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None): """Return a dictionary of mass spectra data as a pandas DataFrame. Parameters @@ -172,8 +189,13 @@ def get_ms_raw(self, spectra, scan_df, data): Options: None, "ms1", "ms2", "all". scan_df : pandas.DataFrame Scan dataframe. Output from get_scan_df(). - data : pymzml.run.Reader - The mass spectra data. + data : pymzml.run.Reader, optional + The mass spectra data. If None, will load the data. + time_range : tuple or list of tuples, optional + Retention time range(s) to filter scans. Can be: + - Single range: (start_time, end_time) in minutes + - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes + If None, returns all scans. Note: filtering is typically done at scan_df level. Returns ------- @@ -181,6 +203,8 @@ def get_ms_raw(self, spectra, scan_df, data): A dictionary containing the mass spectra data as pandas DataFrames, with keys corresponding to the MS level. """ + if data is None: + data = self.load() if spectra == "all": scan_df_forspec = scan_df elif spectra == "ms1": @@ -205,7 +229,7 @@ def get_ms_raw(self, spectra, scan_df, data): # First pass: get nrows N = defaultdict(lambda: 0) for i, spec in enumerate(data): - if i in scan_df_forspec.scan: + if spec.ID in scan_df_forspec.scan.values: # Get ms level level = "ms{}".format(spec.ms_level) @@ -214,7 +238,7 @@ def get_ms_raw(self, spectra, scan_df, data): # Second pass: parse for i, spec in enumerate(data): - if i in scan_df_forspec.scan: + if spec.ID in scan_df_forspec.scan.values: # Number of rows n = spec.mz.shape[0] @@ -273,7 +297,7 @@ def get_ms_raw(self, spectra, scan_df, data): return res - def run(self, spectra="all", scan_df=None): + def run(self, spectra="all", scan_df=None, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None): """Parse the mzML file and return a dictionary of spectra dataframes and a scan metadata dataframe. Parameters @@ -283,6 +307,11 @@ def run(self, spectra="all", scan_df=None): Other options: None, "ms1", "ms2". scan_df : pandas.DataFrame, optional Scan dataframe. If not provided, the scan dataframe is created from the mzML file. + time_range : tuple or list of tuples, optional + Retention time range(s) to load. Can be: + - Single range: (start_time, end_time) in minutes + - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes + If None, loads all scans. Returns ------- @@ -296,7 +325,7 @@ def run(self, spectra="all", scan_df=None): data = self.load() if scan_df is None: - scan_df = self.get_scan_df(data) + scan_df = self.get_scan_df(data, time_range=time_range) if spectra != "none": res = self.get_ms_raw(spectra, scan_df, data) @@ -440,9 +469,16 @@ def set_metadata( return mass_spectrum_objects - def get_mass_spectra_obj(self): + def get_mass_spectra_obj(self, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None): """Instatiate a MassSpectraBase object from the mzML file. + Parameters + ---------- + time_range : tuple or list of tuples, optional + Retention time range(s) to load. Can be: + - Single range: (start_time, end_time) in minutes + - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes + If None, loads all scans. Returns ------- @@ -450,7 +486,7 @@ def get_mass_spectra_obj(self): The MassSpectra object containing the parsed mass spectra. The object is instatiated with the mzML file, analyzer, instrument, sample name, and scan dataframe. """ - _, scan_df = self.run(spectra=False) + _, scan_df = self.run(spectra=False, time_range=time_range) mass_spectra_obj = MassSpectraBase( self.file_location, self.analyzer, @@ -463,13 +499,18 @@ def get_mass_spectra_obj(self): return mass_spectra_obj - def get_lcms_obj(self, spectra="all"): + def get_lcms_obj(self, spectra="all", time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None): """Instatiates a LCMSBase object from the mzML file. Parameters ---------- spectra : str, optional Which mass spectra data to include in the output. Default is all. Other options: none, ms1, ms2. + time_range : tuple or list of tuples, optional + Retention time range(s) to load. Can be: + - Single range: (start_time, end_time) in minutes + - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes + If None, loads all scans. Useful for targeted workflows to improve performance. Returns ------- @@ -478,10 +519,10 @@ def get_lcms_obj(self, spectra="all"): The object is instatiated with the mzML file, analyzer, instrument, sample name, scan dataframe, and mz dataframe(s), as well as lists of scan numbers, retention times, and TICs. """ - _, scan_df = self.run(spectra="none") # first run it to just get scan info + _, scan_df = self.run(spectra="none", time_range=time_range) # first run it to just get scan info if spectra != "none": res, scan_df = self.run( - scan_df=scan_df, spectra=spectra + scan_df=scan_df, spectra=spectra, time_range=time_range ) # second run to parse data lcms_obj = LCMSBase( self.file_location, @@ -507,6 +548,53 @@ def get_lcms_obj(self, spectra="all"): return lcms_obj + def get_scans_in_time_range( + self, + time_range: Union[Tuple[float, float], List[Tuple[float, float]]], + ms_level: Optional[int] = None + ) -> List[int]: + """ + Return scan numbers within specified retention time range(s). + + This method provides efficient filtering of scans by retention time, + which is particularly useful for targeted workflows where only specific + time windows are of interest. + + Parameters + ---------- + time_range : tuple or list of tuples + Retention time range(s) in minutes. Can be: + - Single range: (start_time, end_time) + - Multiple ranges: [(start1, end1), (start2, end2), ...] + ms_level : int, optional + If specified, only return scans of this MS level (e.g., 1 for MS1, 2 for MS2). + If None, returns scans of all MS levels. + + Returns + ------- + list of int + List of scan numbers within the specified time range(s) and MS level. + + Examples + -------- + Get MS1 scans between 1.0 and 2.0 minutes: + + >>> scans = parser.get_scans_in_time_range((1.0, 2.0), ms_level=1) + + Get scans in multiple time windows: + + >>> scans = parser.get_scans_in_time_range([(0.5, 1.5), (3.0, 4.0)]) + """ + # Get scan dataframe filtered by time range + scan_df = self.get_scan_df(time_range=time_range) + + # Further filter by MS level if specified + if ms_level is not None: + scan_df = scan_df[scan_df.ms_level == ms_level] + + # Return list of scan numbers + return scan_df.scan.tolist() + def get_instrument_info(self): """ Return instrument information. diff --git a/corems/mass_spectra/input/parserbase.py b/corems/mass_spectra/input/parserbase.py index fec7992c2..6971c8d79 100644 --- a/corems/mass_spectra/input/parserbase.py +++ b/corems/mass_spectra/input/parserbase.py @@ -1,5 +1,7 @@ from abc import ABC, abstractmethod import datetime +import numbers +from typing import Optional, Union, List, Tuple class SpectraParserInterface(ABC): @@ -16,10 +18,20 @@ class SpectraParserInterface(ABC): Return MassSpectraBase object with several attributes populated * get_mass_spectrum_from_scan(scan_number). Return MassSpecBase data object from scan number. + * get_scans_in_time_range(time_range). + Return scan numbers within specified retention time range(s). Notes ----- This is an abstract class and should not be instantiated directly. + + Time Range Filtering + -------------------- + Many methods support optional time_range parameter to load only scans within + specific retention time windows. This significantly improves performance for + targeted workflows. Time ranges can be specified as: + - Single range: (start_time, end_time) in minutes + - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes """ @abstractmethod @@ -37,23 +49,66 @@ def run(self): pass @abstractmethod - def get_scan_df(self): + def get_scan_df(self, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None): """ Return scan data as a pandas DataFrame. + + Parameters + ---------- + time_range : tuple or list of tuples, optional + Retention time range(s) to filter scans. Can be: + - Single range: (start_time, end_time) in minutes + - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes + If None, returns all scans. + + Returns + ------- + pd.DataFrame + DataFrame containing scan information, optionally filtered by time range. """ pass @abstractmethod - def get_ms_raw(self, spectra, scan_df): - """ - Return a dictionary of mass spectra data as a pandas DataFrame. + def get_ms_raw(self, spectra, scan_df, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None): + """ + Return a dictionary of mass spectra data as pandas DataFrames. + + Parameters + ---------- + spectra : str or dict + Specifies which spectra to load (e.g., 'ms1', 'ms2', or custom dict) + scan_df : pd.DataFrame + Scan information DataFrame + time_range : tuple or list of tuples, optional + Retention time range(s) to filter scans. Can be: + - Single range: (start_time, end_time) in minutes + - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes + If None, returns all scans. + + Returns + ------- + dict + Dictionary of raw mass spectra data, optionally filtered by time range. """ pass @abstractmethod - def get_mass_spectra_obj(self): + def get_mass_spectra_obj(self, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None): """ Return mass spectra data object. + + Parameters + ---------- + time_range : tuple or list of tuples, optional + Retention time range(s) to load. Can be: + - Single range: (start_time, end_time) in minutes + - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes + If None, loads all scans. Useful for targeted workflows to improve performance. + + Returns + ------- + MassSpectraBase + Mass spectra object, optionally filtered to specified time range(s). """ pass @@ -75,6 +130,46 @@ def get_mass_spectra_from_scan_list( """ pass + @abstractmethod + def get_scans_in_time_range( + self, + time_range: Union[Tuple[float, float], List[Tuple[float, float]]], + ms_level: Optional[int] = None + ) -> List[int]: + """ + Return scan numbers within specified retention time range(s). + + This method provides efficient filtering of scans by retention time, + which is particularly useful for targeted workflows where only specific + time windows are of interest. + + Parameters + ---------- + time_range : tuple or list of tuples + Retention time range(s) in minutes. Can be: + - Single range: (start_time, end_time) + - Multiple ranges: [(start1, end1), (start2, end2), ...] + ms_level : int, optional + If specified, only return scans of this MS level (e.g., 1 for MS1, 2 for MS2). + If None, returns scans of all MS levels. + + Returns + ------- + list of int + List of scan numbers within the specified time range(s) and MS level. + + Examples + -------- + Get MS1 scans between 1.0 and 2.0 minutes: + + >>> scans = parser.get_scans_in_time_range((1.0, 2.0), ms_level=1) + + Get scans in multiple time windows: + + >>> scans = parser.get_scans_in_time_range([(0.5, 1.5), (3.0, 4.0)]) + """ + pass + @abstractmethod def get_instrument_info(self): """ @@ -98,3 +193,47 @@ def get_creation_time(self) -> datetime.datetime: The creation time of the mass spectra data. """ pass + + @staticmethod + def _normalize_time_range( + time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] + ) -> Optional[List[Tuple[float, float]]]: + """ + Normalize time range input to a list of tuples. + + Helper method for implementations to standardize time_range parameter. + Converts single tuple to list of tuples for consistent handling. + + Parameters + ---------- + time_range : tuple, list of tuples, or None + Input time range(s) + + Returns + ------- + list of tuples or None + Normalized time ranges as list of (start, end) tuples, or None if input is None. + + Examples + -------- + >>> SpectraParserInterface._normalize_time_range((1.0, 2.0)) + [(1.0, 2.0)] + + >>> SpectraParserInterface._normalize_time_range([(1.0, 2.0), (3.0, 4.0)]) + [(1.0, 2.0), (3.0, 4.0)] + + >>> SpectraParserInterface._normalize_time_range(None) + None + """ + if time_range is None: + return None + + # Check if it's a single tuple (two numbers) + if isinstance(time_range, tuple) and len(time_range) == 2: + # Use numbers.Number to catch int, float, and numpy scalar types + if isinstance(time_range[0], numbers.Number) and isinstance(time_range[1], numbers.Number): + # Convert to float to ensure consistency (handles numpy scalars) + return [(float(time_range[0]), float(time_range[1]))] + + # Otherwise assume it's already a list of tuples + return list(time_range) diff --git a/corems/mass_spectra/input/rawFileReader.py b/corems/mass_spectra/input/rawFileReader.py index d8a01761d..b1244d021 100644 --- a/corems/mass_spectra/input/rawFileReader.py +++ b/corems/mass_spectra/input/rawFileReader.py @@ -22,7 +22,7 @@ from s3path import S3Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Union from corems.encapsulation.constant import Labels from corems.mass_spectra.factory.lc_class import MassSpectraBase, LCMSBase from corems.mass_spectra.factory.chromat_data import EIC_Data, TIC_Data @@ -41,6 +41,7 @@ clr.AddReference("ThermoFisher.CommonCore.Data") clr.AddReference("ThermoFisher.CommonCore.MassPrecisionEstimator") +from System.Collections.Generic import List as DotNetList from ThermoFisher.CommonCore.RawFileReader import RawFileReaderAdapter from ThermoFisher.CommonCore.Data import ToleranceUnits, Extensions from ThermoFisher.CommonCore.Data.Business import ( @@ -53,7 +54,6 @@ from ThermoFisher.CommonCore.Data.Interfaces import IChromatogramSettings from ThermoFisher.CommonCore.Data.Business import MassOptions, FileHeaderReaderFactory from ThermoFisher.CommonCore.Data.FilterEnums import MSOrderType -from System.Collections.Generic import List class ThermoBaseClass: @@ -775,7 +775,7 @@ def get_centroid_mass_spec(averageScan, d_params: dict): elif isinstance(self.scans, list): d_params = self.set_metadata(scans_list=self.scans) - scans = List[int]() + scans = DotNetList[int]() for scan in self.scans: scans.Add(scan) @@ -1237,7 +1237,7 @@ def get_average_mass_spectrum_by_scanlist( # assumes scans is full scan or reduced profile scan - scans = List[int]() + scans = DotNetList[int]() for scan in scans_list: scans.Add(scan) @@ -1333,7 +1333,63 @@ def __init__( def load(self): pass - def get_scan_df(self): + def get_scans_in_time_range( + self, + time_range: Union[Tuple[float, float], List[Tuple[float, float]]], + ms_level: Optional[int] = None + ) -> List[int]: + """Return scan numbers within specified retention time range(s). + + Parameters + ---------- + time_range : tuple or list of tuples + Retention time range(s) in minutes. Can be: + - Single range: (start_time, end_time) + - Multiple ranges: [(start1, end1), (start2, end2), ...] + ms_level : int, optional + If specified, only return scans of this MS level (e.g., 1 for MS1, 2 for MS2). + If None, returns scans of all MS levels. + + Returns + ------- + list of int + List of scan numbers within the specified time range(s) and MS level. + """ + # Normalize time range to list of tuples + time_ranges = self._normalize_time_range(time_range) + + # Get all scan data + scan_df = self.get_scan_df() + + # Filter by time range + mask = pd.Series([False] * len(scan_df), index=scan_df.index) + for start_time, end_time in time_ranges: + mask |= (scan_df.scan_time >= start_time) & (scan_df.scan_time <= end_time) + + filtered_df = scan_df[mask] + + # Filter by MS level if specified + if ms_level is not None: + filtered_df = filtered_df[filtered_df.ms_level == ms_level] + + return filtered_df.scan.tolist() + + def get_scan_df(self, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None): + """Return scan data as a pandas DataFrame. + + Parameters + ---------- + time_range : tuple or list of tuples, optional + Retention time range(s) to filter scans. Can be: + - Single range: (start_time, end_time) in minutes + - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes + If None, returns all scans. + + Returns + ------- + pd.DataFrame + DataFrame containing scan information, optionally filtered by time range. + """ # This automatically brings in all the data self.chromatogram_settings.scans = (-1, -1) @@ -1376,9 +1432,39 @@ def get_scan_df(self): else: scan_df.loc[scan_df.scan == i, "ms_format"] = "profile" + # Filter by time range if specified + if time_range is not None: + time_ranges = self._normalize_time_range(time_range) + mask = pd.Series([False] * len(scan_df), index=scan_df.index) + for start_time, end_time in time_ranges: + mask |= (scan_df.scan_time >= start_time) & (scan_df.scan_time <= end_time) + scan_df = scan_df[mask].reset_index(drop=True) + return scan_df - def get_ms_raw(self, spectra, scan_df): + def get_ms_raw(self, spectra, scan_df, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None): + """Return a dictionary of mass spectra data as pandas DataFrames. + + Parameters + ---------- + spectra : str + Specifies which spectra to load (e.g., 'all', 'ms1', 'ms2') + scan_df : pd.DataFrame + Scan information DataFrame + time_range : tuple or list of tuples, optional + Retention time range(s) to filter scans. Can be: + - Single range: (start_time, end_time) in minutes + - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes + If None, returns all scans. Note: filtering is typically done at scan_df level. + + Returns + ------- + dict + Dictionary of raw mass spectra data, optionally filtered by time range. + """ + # Note: time_range filtering is handled at the scan_df level before calling this method + # The parameter is here for interface consistency with SpectraParserInterface + if spectra == "all": scan_df_forspec = scan_df elif spectra == "ms1": @@ -1484,7 +1570,7 @@ def get_ms_raw(self, spectra, scan_df): return res - def run(self, spectra="all", scan_df=None): + def run(self, spectra="all", scan_df=None, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None): """ Extracts mass spectra data from a raw file. @@ -1494,6 +1580,11 @@ def run(self, spectra="all", scan_df=None): Which mass spectra data to include in the output. Default is all. Other options: none, ms1, ms2. scan_df : pandas.DataFrame, optional Scan dataframe. If not provided, the scan dataframe is created from the mzML file. + time_range : tuple or list of tuples, optional + Retention time range(s) to filter scans. Can be: + - Single range: (start_time, end_time) in minutes + - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes + If None, returns all scans. Returns ------- @@ -1505,11 +1596,11 @@ def run(self, spectra="all", scan_df=None): """ # Prepare scan_df if scan_df is None: - scan_df = self.get_scan_df() + scan_df = self.get_scan_df(time_range=time_range) # Prepare mass spectra data if spectra != "none": - res = self.get_ms_raw(spectra=spectra, scan_df=scan_df) + res = self.get_ms_raw(spectra=spectra, scan_df=scan_df, time_range=time_range) else: res = None @@ -1626,15 +1717,23 @@ def get_mass_spectrum_from_scan( return mass_spectrum_obj - def get_mass_spectra_obj(self): + def get_mass_spectra_obj(self, time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None): """Instatiate a MassSpectraBase object from the binary data file file. + Parameters + ---------- + time_range : tuple or list of tuples, optional + Retention time range(s) to load. Can be: + - Single range: (start_time, end_time) in minutes + - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes + If None, loads all scans. Useful for targeted workflows to improve performance. + Returns ------- MassSpectraBase The MassSpectra object containing the parsed mass spectra. The object is instatiated with the mzML file, analyzer, instrument, sample name, and scan dataframe. """ - _, scan_df = self.run(spectra="none") + _, scan_df = self.run(spectra="none", time_range=time_range) mass_spectra_obj = MassSpectraBase( self.file_location, self.analyzer, @@ -1647,22 +1746,27 @@ def get_mass_spectra_obj(self): return mass_spectra_obj - def get_lcms_obj(self, spectra="all"): + def get_lcms_obj(self, spectra="all", time_range: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None): """Instatiates a LCMSBase object from the mzML file. Parameters ---------- spectra : str, optional Which mass spectra data to include in the output. Default is "all". Other options: "none", "ms1", "ms2". + time_range : tuple or list of tuples, optional + Retention time range(s) to load. Can be: + - Single range: (start_time, end_time) in minutes + - Multiple ranges: [(start1, end1), (start2, end2), ...] in minutes + If None, loads all scans. Useful for targeted workflows to improve performance. Returns ------- LCMSBase LCMS object containing mass spectra data. The object is instatiated with the file location, analyzer, instrument, sample name, scan info, mz dataframe (as specifified), polarity, as well as the attributes holding the scans, retention times, and tics. """ - _, scan_df = self.run(spectra="none") # first run it to just get scan info + _, scan_df = self.run(spectra="none", time_range=time_range) # first run it to just get scan info res, scan_df = self.run( - scan_df=scan_df, spectra=spectra + scan_df=scan_df, spectra=spectra, time_range=time_range ) # second run to parse data lcms_obj = LCMSBase( self.file_location, @@ -1683,7 +1787,7 @@ def get_lcms_obj(self, spectra="all"): # Check if polarity is mixed if len(set(scan_df.polarity)) > 1: raise ValueError("Mixed polarities detected in scan data") - lcms_obj.polarity = scan_df.polarity[0] + lcms_obj.polarity = scan_df.polarity.iloc[0] lcms_obj._scans_number_list = list(scan_df.scan) lcms_obj._retention_time_list = list(scan_df.scan_time) lcms_obj._tic_list = list(scan_df.tic) diff --git a/tests/test_time_range_filtering.py b/tests/test_time_range_filtering.py new file mode 100644 index 000000000..6e7adf48d --- /dev/null +++ b/tests/test_time_range_filtering.py @@ -0,0 +1,312 @@ +""" +Test time range filtering functionality for LC-MS parsers. + +This module tests the time_range parameter across different parser implementations +to ensure efficient loading of targeted retention time windows. +""" +from pathlib import Path +import pytest +import os +import tempfile +import shutil +import time + +from corems.mass_spectra.input.mzml import MZMLSpectraParser +from corems.mass_spectra.input import rawFileReader +from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectra +from corems.mass_spectra.output.export import LCMSExport +from corems.encapsulation.factory.parameters import LCMSParameters + + +# Module-level fixtures +@pytest.fixture(scope="module") +def mzml_file(): + """Path to test mzML file.""" + return ( + Path.cwd() + / "tests/tests_data/lcms/" + / "test_centroid_neg_RP_metab.mzML" + ) + + +@pytest.fixture(scope="module") +def thermo_file(): + """Path to test Thermo RAW file.""" + return ( + Path.cwd() + / "tests/tests_data/lcms/" + / "Blanch_Nat_Lip_C_12_AB_M_17_NEG_25Jan18_Brandi-WCSH5801.raw" + ) + + +@pytest.fixture +def mzml_parser(mzml_file): + """Instantiate mzML parser.""" + return MZMLSpectraParser(mzml_file) + + +@pytest.fixture +def thermo_parser(thermo_file): + """Instantiate Thermo parser.""" + return rawFileReader.ImportMassSpectraThermoMSFileReader(thermo_file) + + +@pytest.fixture +def hdf5_parser(mzml_file): + """Create temporary HDF5 file and return parser.""" + temp_dir = tempfile.mkdtemp() + base_name = "test_time_range" + base_path = os.path.join(temp_dir, base_name) + + # Parse mzML and create HDF5 file + parser = MZMLSpectraParser(mzml_file) + lcms_obj = parser.get_lcms_obj(spectra="ms1") + lcms_obj.parameters = LCMSParameters(use_defaults=True) + + # Save to HDF5 using LCMSExport + exporter = LCMSExport(base_path, lcms_obj) + exporter.to_hdf(overwrite=True) + + # The actual HDF5 file will be at base_path.corems/base_name.hdf5 + hdf5_path = os.path.join(temp_dir, f"{base_name}.corems", f"{base_name}.hdf5") + + hdf5_parser = ReadCoreMSHDFMassSpectra(hdf5_path) + + yield hdf5_parser + + # Cleanup + try: + corems_dir = os.path.join(temp_dir, f"{base_name}.corems") + if os.path.exists(corems_dir): + shutil.rmtree(corems_dir) + os.rmdir(temp_dir) + except Exception: + pass + + +@pytest.fixture(params=['mzml', 'thermo', 'hdf5']) +def parser(request, mzml_parser, thermo_parser, hdf5_parser): + """Parametrized fixture that provides all three parser types.""" + if request.param == 'mzml': + return mzml_parser + elif request.param == 'thermo': + return thermo_parser + else: + return hdf5_parser + + +class TestTimeRangeFiltering: + """Test time range filtering across all parser implementations.""" + + def test_get_scans_in_time_range_single_range(self, parser): + """Test getting scans within a single time range.""" + all_scan_df = parser.get_scan_df() + time_range = (1.0, 2.0) + scans_in_range = parser.get_scans_in_time_range(time_range) + + assert isinstance(scans_in_range, list) + assert len(scans_in_range) > 0 + + # Verify all scans are in range + for scan_num in scans_in_range: + scan_time = all_scan_df[all_scan_df.scan == scan_num].scan_time.values[0] + assert time_range[0] <= scan_time <= time_range[1] + + def test_get_scans_in_time_range_multiple_ranges(self, parser): + """Test getting scans within multiple time ranges.""" + all_scan_df = parser.get_scan_df() + time_ranges = [(0.5, 1.5), (3.0, 4.0)] + scans_in_range = parser.get_scans_in_time_range(time_ranges) + + assert isinstance(scans_in_range, list) + assert len(scans_in_range) > 0 + + # Verify all scans are in at least one range + for scan_num in scans_in_range: + scan_time = all_scan_df[all_scan_df.scan == scan_num].scan_time.values[0] + assert any(start <= scan_time <= end for start, end in time_ranges) + + def test_get_scans_in_time_range_with_ms_level(self, parser): + """Test filtering by both time range and MS level.""" + all_scan_df = parser.get_scan_df() + time_range = (1.0, 3.0) + ms1_scans = parser.get_scans_in_time_range(time_range, ms_level=1) + + assert len(ms1_scans) > 0 + + # Verify all returned scans are MS1 and in time range + for scan_num in ms1_scans: + scan_info = all_scan_df[all_scan_df.scan == scan_num].iloc[0] + assert scan_info.ms_level == 1 + assert time_range[0] <= scan_info.scan_time <= time_range[1] + + def test_get_scan_df_with_time_range(self, parser): + """Test get_scan_df with time range filtering.""" + all_scan_df = parser.get_scan_df() + time_range = (1.0, 2.0) + filtered_scan_df = parser.get_scan_df(time_range=time_range) + + assert len(filtered_scan_df) < len(all_scan_df) + assert len(filtered_scan_df) > 0 + assert all(time_range[0] <= t <= time_range[1] for t in filtered_scan_df.scan_time) + + def test_get_scan_df_with_multiple_time_ranges(self, parser): + """Test get_scan_df with multiple time ranges.""" + time_ranges = [(0.5, 1.0), (2.0, 2.5)] + filtered_scan_df = parser.get_scan_df(time_range=time_ranges) + + for scan_time in filtered_scan_df.scan_time: + assert any(start <= scan_time <= end for start, end in time_ranges) + + def test_edge_cases(self, parser): + """Test edge cases for time range filtering.""" + all_scan_df = parser.get_scan_df() + min_time = all_scan_df.scan_time.min() + max_time = all_scan_df.scan_time.max() + + # Range covering all data + scans = parser.get_scans_in_time_range((0, 999)) + assert len(scans) == len(all_scan_df) + + # Range with no data + scans = parser.get_scans_in_time_range((999, 1000)) + assert len(scans) == 0 + + # Range at exact boundaries + scans = parser.get_scans_in_time_range((min_time, max_time)) + assert len(scans) == len(all_scan_df) + + def test_normalize_time_range_helper(self): + """Test the _normalize_time_range static helper method.""" + from corems.mass_spectra.input.parserbase import SpectraParserInterface + + assert SpectraParserInterface._normalize_time_range((1.0, 2.0)) == [(1.0, 2.0)] + assert SpectraParserInterface._normalize_time_range([(1.0, 2.0), (3.0, 4.0)]) == [(1.0, 2.0), (3.0, 4.0)] + assert SpectraParserInterface._normalize_time_range(None) is None + + +class TestParserSpecificFeatures: + """Test parser-specific features and workflows.""" + + def test_mzml_lcms_obj_with_time_range(self, mzml_parser): + """Test loading mzML LCMS object with time range filtering.""" + all_scan_df = mzml_parser.get_scan_df() + time_range = (1.0, 2.0) + + filtered_lcms = mzml_parser.get_lcms_obj(spectra="ms1", time_range=time_range) + + assert len(filtered_lcms.scan_df) < len(all_scan_df) + assert len(filtered_lcms.scan_df) > 0 + assert all(time_range[0] <= t <= time_range[1] for t in filtered_lcms.scan_df.scan_time) + assert len(filtered_lcms._scans_number_list) == len(filtered_lcms.scan_df) + assert len(filtered_lcms._retention_time_list) == len(filtered_lcms.scan_df) + + def test_thermo_lcms_obj_with_time_range(self, thermo_parser): + """Test loading Thermo LCMS object with time range filtering.""" + all_scan_df = thermo_parser.get_scan_df() + time_range = (0.5, 1.0) + + filtered_lcms = thermo_parser.get_lcms_obj(spectra="ms1", time_range=time_range) + + assert len(filtered_lcms.scan_df) < len(all_scan_df) + assert len(filtered_lcms.scan_df) > 0 + assert all(time_range[0] <= t <= time_range[1] for t in filtered_lcms.scan_df.scan_time) + assert len(filtered_lcms._scans_number_list) == len(filtered_lcms.scan_df) + assert len(filtered_lcms._retention_time_list) == len(filtered_lcms.scan_df) + + def test_hdf5_lcms_obj_with_time_range(self, hdf5_parser): + """Test HDF5 LCMS object accepts time_range parameter.""" + all_scan_df = hdf5_parser.get_scan_df() + time_range = (1.0, 2.0) + + # HDF5 parser accepts time_range for interface consistency + _ = hdf5_parser.get_lcms_obj( + load_raw=True, + load_light=False, + use_original_parser=False, + time_range=time_range + ) + + # Verify scan_df can be filtered + filtered_scan_df = hdf5_parser.get_scan_df(time_range=time_range) + assert len(filtered_scan_df) < len(all_scan_df) + assert len(filtered_scan_df) > 0 + + def test_thermo_performance_benchmark(self, thermo_parser): + """Test that time range filtering provides measurable performance improvement.""" + target_rt = 5.0 + rt_window = 2.0 + time_range = (target_rt - rt_window, target_rt + rt_window) + + # Time filtered load + start_filtered = time.time() + filtered_lcms = thermo_parser.get_lcms_obj(spectra="ms1", time_range=time_range) + filtered_lcms.parameters = LCMSParameters(use_defaults=True) + filtered_lcms.parameters.lc_ms.peak_picking_method = "persistent homology" + filtered_lcms.parameters.lc_ms.ph_inten_min_rel = 0.01 + filtered_lcms.parameters.lc_ms.ph_persis_min_rel = 0.05 + filtered_lcms.find_mass_features() + time_filtered = time.time() - start_filtered + + # Time full load + start_full = time.time() + full_lcms = thermo_parser.get_lcms_obj(spectra="ms1") + full_lcms.parameters = LCMSParameters(use_defaults=True) + full_lcms.parameters.lc_ms.peak_picking_method = "persistent homology" + full_lcms.parameters.lc_ms.ph_inten_min_rel = 0.01 + full_lcms.parameters.lc_ms.ph_persis_min_rel = 0.05 + full_lcms.find_mass_features() + time_full = time.time() - start_full + + speedup = time_full / time_filtered if time_filtered > 0 else 0 + + print(f"\n{'='*60}") + print("Time Range Filtering Performance Test") + print(f"{'='*60}") + print(f"Time range: {time_range[0]}-{time_range[1]} minutes") + print(f"Filtered load: {len(filtered_lcms.scan_df)} scans in {time_filtered:.2f}s") + print(f"Full load: {len(full_lcms.scan_df)} scans in {time_full:.2f}s") + print(f"Speedup: {speedup:.2f}x") + print(f"{'='*60}") + + assert len(filtered_lcms.scan_df) < len(full_lcms.scan_df) + assert time_filtered < time_full + assert speedup >= 1.2, f"Expected at least 1.2x speedup, got {speedup:.2f}x" + + +class TestMassFeatureIntegration: + """Test time range filtering integration with mass feature detection.""" + + def test_targeted_search_with_time_range(self, mzml_file): + """Test targeted search with time-filtered data.""" + parser = MZMLSpectraParser(mzml_file) + target_time_range = (1.0, 2.0) + + lcms_obj = parser.get_lcms_obj(spectra="ms1", time_range=target_time_range) + lcms_obj.parameters = LCMSParameters(use_defaults=True) + lcms_obj.parameters.lc_ms.peak_picking_method = "centroided_persistent_homology" + lcms_obj.parameters.mass_spectrum["ms1"].mass_spectrum.noise_threshold_method = "relative_abundance" + lcms_obj.find_mass_features() + + assert len(lcms_obj.mass_features) > 0 + + # Verify all features are within target time range + for mf_id, mf in lcms_obj.mass_features.items(): + assert target_time_range[0] <= mf.retention_time <= target_time_range[1] + + def test_multiple_time_windows_for_standards(self, mzml_file): + """Test loading multiple time windows for internal standards.""" + parser = MZMLSpectraParser(mzml_file) + standard_windows = [(0.3, 0.7), (1.8, 2.2), (3.3, 3.7)] + + lcms_obj = parser.get_lcms_obj(spectra="ms1", time_range=standard_windows) + lcms_obj.parameters = LCMSParameters(use_defaults=True) + lcms_obj.parameters.lc_ms.peak_picking_method = "centroided_persistent_homology" + lcms_obj.parameters.mass_spectrum["ms1"].mass_spectrum.noise_threshold_method = "relative_abundance" + lcms_obj.find_mass_features() + + # Verify scan times are only from specified windows + for scan_time in lcms_obj.scan_df.scan_time: + assert any(start <= scan_time <= end for start, end in standard_windows) + + From cdb7637525c5aea8ed4975e9be872f9b91b8e61e Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 26 Feb 2026 13:57:44 -0800 Subject: [PATCH 143/158] Better handle empty MS1 annotation reports --- corems/mass_spectra/factory/lc_class.py | 22 +++++++++++++++------- corems/mass_spectra/output/export.py | 4 ++-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 4d79ca88a..e7e488e77 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1049,9 +1049,15 @@ def mass_spectrum_to_string( return df_mf - def mass_features_ms1_annot_to_df(self): + def mass_features_ms1_annot_to_df(self, suppress_warnings=False): """Returns a pandas dataframe summarizing the MS1 annotations for the mass features in the dataset. + Parameters + ----------- + suppress_warnings : bool, optional + If True, suppresses the warning when no MS1 annotations are found. + Useful when calling from collection-level methods. Default is False. + Returns -------- pandas.DataFrame @@ -1061,7 +1067,8 @@ def mass_features_ms1_annot_to_df(self): Raises ------ Warning - If no MS1 annotations were found for the mass features in the dataset. + If no MS1 annotations were found for the mass features in the dataset + (unless suppress_warnings=True). """ annot_df_list_ms1 = [] for mf_id in self.mass_features.keys(): @@ -1101,11 +1108,12 @@ def mass_features_ms1_annot_to_df(self): else: annot_ms1_df_full = None - # Warn that no ms1 annotations were found - warnings.warn( - "No MS1 annotations found for mass features in dataset, were MS1 spectra added and processed within the dataset?", - UserWarning, - ) + # Warn that no ms1 annotations were found (unless suppressed) + if not suppress_warnings: + warnings.warn( + "No MS1 annotations found for mass features in dataset, were MS1 spectra added and processed within the dataset?", + UserWarning, + ) return annot_ms1_df_full diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index 4e6b4915b..934ab679b 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -1633,7 +1633,7 @@ def combine_reports(self, mf_report, ms1_annot_report, ms2_annot_report): The MS2 annotation report DataFrame. """ # If there is an ms1_annot_report, merge it with the mf_report - if not ms1_annot_report.empty: + if ms1_annot_report is not None and not ms1_annot_report.empty: # MS1 has been run and has molecular formula information mf_report = pd.merge( mf_report, @@ -1744,7 +1744,7 @@ def to_report(self, molecular_metadata=None, suppress_warnings=False): mf_report = mf_report.reset_index(drop=False) # Get and clean ms1 annotation dataframe - ms1_annot_report = self.mass_spectra.mass_features_ms1_annot_to_df() + ms1_annot_report = self.mass_spectra.mass_features_ms1_annot_to_df(suppress_warnings=suppress_warnings) if ms1_annot_report is not None: ms1_annot_report = ms1_annot_report.copy() ms1_annot_report = self.clean_ms1_report(ms1_annot_report) From 7298d64871d0bedee496751f892f928ea34f093a Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 26 Feb 2026 16:52:37 -0800 Subject: [PATCH 144/158] Better deal with accumulating mass features and MS2 search results --- corems/mass_spectra/calc/lc_calc.py | 7 +++--- .../mass_spectra/calc/lc_calc_operations.py | 24 ++++++++++++++----- .../search/lcms_spectral_search.py | 15 ++++++++++-- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 944d4d17b..6a718ac93 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -2309,12 +2309,12 @@ def _populate_mass_features(self, mass_features_df, mf_type="untargeted", accumu initial_count = 0 # Add new mass features - for row in mass_features_df.itertuples(): + for idx, row in enumerate(mass_features_df.itertuples()): row_dict = mass_features_df.iloc[row.Index].to_dict() lcms_feature = LCMSMassFeature(self, **row_dict) lcms_feature.type = mf_type - # Use offset ID to avoid conflicts with existing features - new_id = lcms_feature.id + id_offset + # Use sequential ID starting from id_offset to avoid conflicts with existing features + new_id = idx + id_offset lcms_feature._id = new_id # Update the internal ID self.mass_features[new_id] = lcms_feature @@ -4301,6 +4301,7 @@ def _associate_ms2_with_mass_features(self, sample, local_mf_ids, auto_process=T scan_list=list(unique_dda_scans), auto_process=auto_process, spectrum_mode=spectrum_mode, + ms_level=2, use_parser=True, ms_params=ms_params, ) diff --git a/corems/mass_spectra/calc/lc_calc_operations.py b/corems/mass_spectra/calc/lc_calc_operations.py index e00fe809e..0c6344277 100644 --- a/corems/mass_spectra/calc/lc_calc_operations.py +++ b/corems/mass_spectra/calc/lc_calc_operations.py @@ -526,16 +526,20 @@ def execute(self, sample_id, collection, mf_ids_to_load=None, **runtime_params): ) # If add_ms2, associate MS2 spectra with the loaded mass features - if add_ms2 and local_mf_ids_to_load is not None: + if add_ms2 and len(sample.mass_features) > 0: + # Get the IDs of loaded mass features (use what was actually loaded) + mf_ids_for_ms2 = list(sample.mass_features.keys()) + collection._associate_ms2_with_mass_features( sample, - list(local_mf_ids_to_load), + mf_ids_for_ms2, auto_process=auto_process_ms2, spectrum_mode=ms2_spectrum_mode, scan_filter=ms2_scan_filter ) - return sample.mass_features + # Return both mass_features and _ms so they can be collected in multiprocessing + return {'mass_features': sample.mass_features, '_ms': sample._ms} def collect_results(self, sample_id, result, collection): """ @@ -545,19 +549,27 @@ def collect_results(self, sample_id, result, collection): 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. + samples. Also collects loaded mass spectra. Parameters ---------- sample_id : int Sample ID that was processed result : dict - Dictionary of reloaded mass features + Dictionary with 'mass_features' and '_ms' from execute() collection : LCMSBaseCollection The collection """ # Update sample.mass_features with loaded features - collection[sample_id].mass_features = result + if isinstance(result, dict) and 'mass_features' in result: + collection[sample_id].mass_features = result['mass_features'] + # Also collect the _ms dictionary (MS1 and MS2 spectra) + if '_ms' in result: + collection[sample_id]._ms.update(result['_ms']) + else: + # Backward compatibility - if result is just mass_features dict + collection[sample_id].mass_features = result + # Lock the collection dataframe to prevent rebuilding from individual samples # (since we've only loaded a subset, rebuilding would lose data) collection._mass_features_locked = True diff --git a/corems/molecular_id/search/lcms_spectral_search.py b/corems/molecular_id/search/lcms_spectral_search.py index f893ee996..95f33269a 100644 --- a/corems/molecular_id/search/lcms_spectral_search.py +++ b/corems/molecular_id/search/lcms_spectral_search.py @@ -321,17 +321,28 @@ def fe_search( # If there are mass features, associate the results with each mass feature if len(self.mass_features) > 0: + # Determine which results to associate with mass features + if accumulate_results: + # When accumulating, only associate new results from this search + # to avoid duplicating previously associated results + results_to_associate = overall_results_dict + else: + # When not accumulating, clear existing associations and re-associate all results + for mass_feature_id in self.mass_features.keys(): + self.mass_features[mass_feature_id].ms2_similarity_results = [] + results_to_associate = self.spectral_search_results + for mass_feature_id, mass_feature in self.mass_features.items(): scan_ids = mass_feature.ms2_scan_numbers for ms2_scan_id in scan_ids: precursor_mz = mass_feature.mz try: - self.spectral_search_results[ms2_scan_id][precursor_mz] + results_to_associate[ms2_scan_id][precursor_mz] except KeyError: pass else: self.mass_features[ mass_feature_id ].ms2_similarity_results.append( - self.spectral_search_results[ms2_scan_id][precursor_mz] + results_to_associate[ms2_scan_id][precursor_mz] ) From b0c2d49ed04e1347ef81537bd7165a7ed0805add Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Fri, 27 Feb 2026 13:46:49 -0800 Subject: [PATCH 145/158] Add handling for multiple calls of process_consensus_features for sequential MS2 library searches --- corems/mass_spectra/calc/lc_calc.py | 39 +++++++++++++++---- .../mass_spectra/calc/lc_calc_operations.py | 7 +++- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 6a718ac93..80a4100aa 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -5638,9 +5638,22 @@ def process_consensus_features(self, load_representatives=True, perform_gap_fill MS2SpectralSearchOperation, LoadEICsOperation ) - # Validate that at least one operation is enabled - if not perform_gap_filling and not load_representatives: - raise ValueError("At least one of perform_gap_filling or load_representatives must be True") + # Validate that at least one meaningful operation is enabled + has_operations = ( + perform_gap_filling or + load_representatives or + molecular_formula_search or + ms2_spectral_search or + gather_eics or + add_ms1 or + add_ms2 + ) + + if not has_operations: + raise ValueError( + "At least one operation must be enabled: perform_gap_filling, load_representatives, " + "molecular_formula_search, ms2_spectral_search, gather_eics, add_ms1, or add_ms2" + ) # Validate prerequisites for gap-filling if perform_gap_filling: @@ -5652,15 +5665,27 @@ def process_consensus_features(self, load_representatives=True, perform_gap_fill # Validate prerequisites for MS2 spectral search if ms2_spectral_search: - if not add_ms2: - raise ValueError( - "MS2 spectral search requires add_ms2=True to load MS2 spectra." - ) if spectral_lib is None: raise ValueError( "MS2 spectral search requires spectral_lib to be provided. " "Create it using MSPInterface.get_metabolomics_spectra_library() before calling this method." ) + # Check if mass features will be loaded OR are already loaded + # (The operation's can_execute will check if MS2 spectra are actually present) + if not load_representatives and not perform_gap_filling: + # Check if at least one sample has mass features loaded + # This allows MS2 search on already-loaded features + has_loaded_features = any( + len(self[i].mass_features) > 0 if hasattr(self[i], 'mass_features') and self[i].mass_features is not None else False + for i in range(len(self.samples)) + ) + if not has_loaded_features: + raise ValueError( + "MS2 spectral search requires mass features to be loaded. " + "Either set load_representatives=True or perform_gap_filling=True to load them, " + "or load them in a previous call to process_consensus_features() before calling " + "with ms2_spectral_search=True." + ) # Build pipeline operations = [] diff --git a/corems/mass_spectra/calc/lc_calc_operations.py b/corems/mass_spectra/calc/lc_calc_operations.py index 0c6344277..a014338e3 100644 --- a/corems/mass_spectra/calc/lc_calc_operations.py +++ b/corems/mass_spectra/calc/lc_calc_operations.py @@ -909,12 +909,15 @@ def execute(self, sample_id, collection, fe_lib=None, molecular_metadata=None, * ) # Extract peak_sep_da from FlashEntropy library configuration - peak_sep_da = fe_lib.entropy_search.max_ms2_tolerance_in_da - if peak_sep_da is None: + # peak_sep_da should be 2 * max_ms2_tolerance_in_da to match the min_ms2_difference_in_da + # used when creating the library + tolerance_da = fe_lib.entropy_search.max_ms2_tolerance_in_da + if tolerance_da is None: raise ValueError( f"Sample {sample_id}: Could not extract max_ms2_tolerance_in_da from FlashEntropy library. " "Ensure the library was created with this parameter specified." ) + peak_sep_da = 2 * tolerance_da # Verify that sample has _ms dictionary if not hasattr(sample, '_ms') or not sample._ms: From 6c030c2a84f183a5c78d556731801bf8edec6838 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Sun, 1 Mar 2026 10:29:22 -0800 Subject: [PATCH 146/158] Add and process MS1 for gap-filled samples --- corems/mass_spectra/calc/lc_calc.py | 42 ++++++++++++++----- .../mass_spectra/calc/lc_calc_operations.py | 16 ++++++- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 80a4100aa..8d72f0354 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -230,7 +230,7 @@ def add_peak_metrics(self, remove_by_metrics=True, induced_features=False): # Remove mass features by peak metrics if designated in parameters if self.parameters.lc_ms.remove_mass_features_by_peak_metrics and remove_by_metrics: - self._remove_mass_features_by_peak_metrics() + self._remove_mass_features_by_peak_metrics(induced_features=induced_features) def get_average_mass_spectrum( self, @@ -1047,7 +1047,7 @@ def _remove_redundant_mass_features( k: v for k, v in self.mass_features.items() if k not in non_representative_mf_id } - def _remove_mass_features_by_peak_metrics(self) -> None: + def _remove_mass_features_by_peak_metrics(self, induced_features=False) -> None: """Remove mass features based on peak metrics defined in mass_feature_attribute_filter_dict. This method filters mass features based on various peak shape metrics and quality indicators @@ -1070,18 +1070,31 @@ def _remove_mass_features_by_peak_metrics(self) -> None: - {'dispersity_index': {'value': 0.1, 'operator': '<'}} - Keep features with dispersity_index < 0.1 - {'gaussian_similarity': {'value': 0.7, 'operator': '>='}} - Keep features with gaussian_similarity >= 0.7 + Parameters + ---------- + induced_features : bool, optional + If True, filter induced_mass_features instead of regular mass_features. Default is False. + Returns ------- None - Modifies self.mass_features in place by removing filtered features. + Modifies self.mass_features or self.induced_mass_features in place by removing filtered features. Raises ------ ValueError If no mass features are found, if an invalid attribute is specified, or if filter specification is malformed. """ - if self.mass_features is None or len(self.mass_features) == 0: - raise ValueError("No mass features found, run find_mass_features() first") + # Select the appropriate mass features dictionary + if induced_features: + mf_dict = self.induced_mass_features + mf_type = "induced mass features" + else: + mf_dict = self.mass_features + mf_type = "mass features" + + if mf_dict is None or len(mf_dict) == 0: + raise ValueError(f"No {mf_type} found, run {'gap filling' if induced_features else 'find_mass_features()'} first") filter_dict = self.parameters.lc_ms.mass_feature_attribute_filter_dict @@ -1090,15 +1103,15 @@ def _remove_mass_features_by_peak_metrics(self) -> None: return verbose = self.parameters.lc_ms.verbose_processing - initial_count = len(self.mass_features) + initial_count = len(mf_dict) if verbose: - print(f"Filtering mass features using peak metrics. Initial count: {initial_count}") + print(f"Filtering {mf_type} using peak metrics. Initial count: {initial_count}") # List to collect IDs of mass features to remove features_to_remove = [] - for mf_id, mass_feature in self.mass_features.items(): + for mf_id, mass_feature in mf_dict.items(): should_remove = False for attribute_name, filter_spec in filter_dict.items(): @@ -1184,9 +1197,18 @@ def _remove_mass_features_by_peak_metrics(self) -> None: # Remove filtered mass features for mf_id in features_to_remove: - del self.mass_features[mf_id] + del mf_dict[mf_id] + + if verbose and len(features_to_remove) > 0: + print(f"Removed {len(features_to_remove)} {mf_type} based on peak metrics. Remaining: {len(mf_dict)}") + + # Update the appropriate dictionary + if induced_features: + self.induced_mass_features = mf_dict + else: + self.mass_features = mf_dict - # Clean up unassociated EICs and ms1 data + # Clean up unassociated EICs and ms1 data (only for regular features) self._remove_unassociated_eics() self._remove_unassociated_ms1_spectra() diff --git a/corems/mass_spectra/calc/lc_calc_operations.py b/corems/mass_spectra/calc/lc_calc_operations.py index a014338e3..d9bc693a0 100644 --- a/corems/mass_spectra/calc/lc_calc_operations.py +++ b/corems/mass_spectra/calc/lc_calc_operations.py @@ -385,7 +385,21 @@ def execute(self, sample_id, collection, **runtime_params): collection[sample_id].induced_mass_features = induced_mass_features collection[sample_id].integrate_mass_features(drop_if_fail=False, induced_features=True) - # Return the induced features + # Add MS1 spectra and peak metrics to successfully detected induced features + # Only process features that were successfully detected (apex_scan != -99) + # This is critical for having m/z values in the pivot table for gap-filled features + successful_induced = {k: v for k, v in induced_mass_features.items() if v.apex_scan != -99} + + if len(successful_induced) > 0: + # Use the already-loaded raw data (use_parser=False) for efficiency + collection[sample_id].add_associated_ms1( + auto_process=True, + use_parser=False, + spectrum_mode=None, + induced_features=True + ) + + # Return the induced features (some may have been filtered out) return collection[sample_id].induced_mass_features def collect_results(self, sample_id, result, collection): From d086be4c84fed9adea5afa79ccd19f722e864b68 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Sun, 1 Mar 2026 11:44:28 -0800 Subject: [PATCH 147/158] Fix tqdm for multiprocessing in process_consensus_mass_features --- corems/mass_spectra/calc/lc_calc.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 8d72f0354..45e2a0fae 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -5282,23 +5282,32 @@ def process_samples_pipeline(self, operations, description=None, keep_raw_data=F if show_progress: from tqdm import tqdm import time - print(f"\nProcessing {sample_ct} samples with {ncores} cores ({description}):") # Use starmap_async for parallel execution with progress tracking async_result = pool.starmap_async(self._execute_sample_pipeline, args_list) # Poll for completion and update progress bar - pbar = tqdm(total=sample_ct, unit="sample", ncols=80) + print(description) + pbar = tqdm( + total=sample_ct, + desc="", + unit="sample", + position=0, + leave=True, + dynamic_ncols=True + ) + prev_completed = 0 while not async_result.ready(): # Get number of completed tasks by checking remaining completed = sample_ct - async_result._number_left - pbar.n = completed - pbar.refresh() - time.sleep(0.1) # Poll every 100ms + if completed > prev_completed: + pbar.update(completed - prev_completed) + prev_completed = completed + time.sleep(0.5) # Poll every 500ms to avoid spam # Final update to 100% - pbar.n = sample_ct - pbar.refresh() + if prev_completed < sample_ct: + pbar.update(sample_ct - prev_completed) pbar.close() # Get all results From 15fd1fb9a413909a546601b7a1b6985a9603a5ed Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 2 Mar 2026 10:23:03 -0800 Subject: [PATCH 148/158] Add better handling for exporting with mixed TF confidence scores --- corems/mass_spectra/output/export.py | 46 ++++++++++++++++++---------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index 934ab679b..d29097a85 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -1642,24 +1642,31 @@ def combine_reports(self, mf_report, ms1_annot_report, ms2_annot_report): on=["mf_id", "isotopologue_type"], ) if ms2_annot_report is not None: - # pull out the records with ion_formula and drop the ion_formula column (these should be empty if MS1 molecular formula assignment is working correctly) - mf_no_ion_formula = mf_report[mf_report["ion_formula"].isna()] - mf_no_ion_formula = mf_no_ion_formula.drop(columns=["ion_formula"]) - mf_no_ion_formula = pd.merge( - mf_no_ion_formula, ms2_annot_report, how="left", on=["mf_id"] - ) + # Check if ion_formula column exists (it may not if MS1 formula search wasn't run) + if "ion_formula" in mf_report.columns: + # pull out the records with ion_formula and drop the ion_formula column (these should be empty if MS1 molecular formula assignment is working correctly) + mf_no_ion_formula = mf_report[mf_report["ion_formula"].isna()] + mf_no_ion_formula = mf_no_ion_formula.drop(columns=["ion_formula"]) + mf_no_ion_formula = pd.merge( + mf_no_ion_formula, ms2_annot_report, how="left", on=["mf_id"] + ) - # pull out the records with ion_formula - mf_with_ion_formula = mf_report[~mf_report["ion_formula"].isna()] - mf_with_ion_formula = pd.merge( - mf_with_ion_formula, - ms2_annot_report, - how="left", - on=["mf_id", "ion_formula"], - ) + # pull out the records with ion_formula + mf_with_ion_formula = mf_report[~mf_report["ion_formula"].isna()] + mf_with_ion_formula = pd.merge( + mf_with_ion_formula, + ms2_annot_report, + how="left", + on=["mf_id", "ion_formula"], + ) - # put back together - mf_report = pd.concat([mf_no_ion_formula, mf_with_ion_formula]) + # put back together + mf_report = pd.concat([mf_no_ion_formula, mf_with_ion_formula]) + else: + # No ion_formula column (MS1 formula search wasn't run), merge on mf_id only + mf_report = pd.merge( + mf_report, ms2_annot_report, how="left", on=["mf_id"] + ) # Rename colums rename_dict = { @@ -1704,11 +1711,16 @@ def combine_reports(self, mf_report, ms1_annot_report, ms2_annot_report): ] # Reorder rows by "Mass Feature ID", then "Entropy Similarity" (descending), then "Confidence Score" (descending) - if "Entropy Similarity" in mf_report.columns: + if "Entropy Similarity" in mf_report.columns and "Confidence Score" in mf_report.columns: mf_report = mf_report.sort_values( by=["Mass Feature ID", "Entropy Similarity", "Confidence Score"], ascending=[True, False, False], ) + elif "Entropy Similarity" in mf_report.columns: + mf_report = mf_report.sort_values( + by=["Mass Feature ID", "Entropy Similarity"], + ascending=[True, False], + ) elif "Confidence Score" in mf_report.columns: mf_report = mf_report.sort_values( by=["Mass Feature ID", "Confidence Score"], From 8982c0d4c7b428978313f84a5ee6cbefc42bbb4e Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Mon, 2 Mar 2026 17:29:41 -0800 Subject: [PATCH 149/158] Handle ms2 scans that are outside the bounds of integration --- corems/mass_spectra/calc/lc_calc.py | 7 ++++ corems/mass_spectra/factory/lc_class.py | 54 +++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 45e2a0fae..abb973115 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -673,6 +673,13 @@ def integrate_mass_features( ): if idx2 in self.mass_features.keys(): self.mass_features.pop(idx2) + + # Filter MS2 scans to only include those within integration bounds + # This ensures MS2 scans outside start_scan to final_scan are removed + if induced_features: + self._filter_ms2_scans_by_integration_bounds(mf_dict=self.induced_mass_features) + else: + self._filter_ms2_scans_by_integration_bounds(mf_dict=self.mass_features) def find_c13_mass_features(self): """Mark likely C13 isotopes and connect to monoisoitopic mass features. diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index e7e488e77..eb166d0a9 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -571,6 +571,44 @@ def remove_unprocessed_data(self, ms_level=None): raise ValueError("ms_level must be 1 or 2") self._ms_unprocessed[ms_level] = None + def _filter_ms2_scans_by_integration_bounds(self, mf_dict=None): + """Filter MS2 scans to only include those within integration bounds. + + Removes MS2 scan numbers that fall outside the start_scan to final_scan range + for each mass feature. This should be called after integration sets the bounds. + + Parameters + ---------- + mf_dict : dict, optional + Dictionary of mass features to filter. If None, uses self.mass_features. + + Returns + ------- + int + Number of MS2 scans removed across all mass features. + """ + if mf_dict is None: + mf_dict = self.mass_features + + total_removed = 0 + + for mf_id, mf in mf_dict.items(): + # Only filter if integration bounds are set and MS2 scans exist + if (hasattr(mf, 'start_scan') and hasattr(mf, 'final_scan') and + mf.start_scan is not None and mf.final_scan is not None and + mf.ms2_scan_numbers is not None and len(mf.ms2_scan_numbers) > 0): + + # Filter scan numbers to only those within bounds + original_count = len(mf.ms2_scan_numbers) + mf.ms2_scan_numbers = [ + scan for scan in mf.ms2_scan_numbers + if mf.start_scan <= scan <= mf.final_scan + ] + removed = original_count - len(mf.ms2_scan_numbers) + total_removed += removed + + return total_removed + def _find_ms2_scans_for_mass_features(self, mf_ids=None, scan_filter=None): """Find MS2 scans associated with mass features. @@ -632,10 +670,18 @@ def _find_ms2_scans_for_mass_features(self, mf_ids=None, scan_filter=None): ] scan_list = ms2_scans_filtered.scan.tolist() if scan_list: - self.mass_features[i].ms2_scan_numbers = ( - scan_list + list(self.mass_features[i].ms2_scan_numbers) - ) - dda_scans.extend(scan_list) + # Filter scans by integration bounds if they exist + mf = self.mass_features[i] + if (hasattr(mf, 'start_scan') and hasattr(mf, 'final_scan') and + mf.start_scan is not None and mf.final_scan is not None): + # Only keep scans within integration bounds + scan_list = [s for s in scan_list if mf.start_scan <= s <= mf.final_scan] + + if scan_list: # Only add if there are still scans after filtering + self.mass_features[i].ms2_scan_numbers = ( + scan_list + list(self.mass_features[i].ms2_scan_numbers) + ) + dda_scans.extend(scan_list) return list(set(dda_scans)) From 91755d7fab0e6fdfabeca2f1914d900f07eb9171 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 1 Apr 2026 08:47:28 -0700 Subject: [PATCH 150/158] Clean up exports for collection-level --- corems/mass_spectra/calc/lc_calc.py | 13 ++++++++++++- corems/mass_spectra/factory/lc_class.py | 23 ++++++++++++++++++++++- corems/mass_spectra/output/export.py | 14 ++++++++------ 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index abb973115..acc555f1f 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -5437,7 +5437,18 @@ def _prepare_pipeline_runtime_params(self, operations): # Group by sample_id and collect all m/z values associated with eics for sample_id in clustered_mf['sample_id'].unique(): sample_df = clustered_mf[clustered_mf['sample_id'] == sample_id] - cluster_mz_dict[sample_id] = sample_df['_eic_mz'].unique().tolist() + sample = self[sample_id] # Get the LCMS object for this sample + + # Extract _eic_mz from actual mass feature objects, not from dataframe + eic_mz_list = [] + for mf_id in sample_df['mf_id'].values: + if mf_id in sample.mass_features: + mf = sample.mass_features[mf_id] + if hasattr(mf, '_eic_mz') and mf._eic_mz is not None: + eic_mz_list.append(mf._eic_mz) + + # Use the collected m/z values, or fallback to empty list if none found + cluster_mz_dict[sample_id] = list(set(eic_mz_list)) if eic_mz_list else [] runtime_params['cluster_mz_dict'] = cluster_mz_dict diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index eb166d0a9..78241aefc 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -2328,7 +2328,12 @@ def cluster_representatives_table(self): # Sort by cluster and return with cluster as a regular column return consensus_report.sort_values(by='cluster') - def feature_annotations_table(self, molecular_metadata=None, drop_unannotated=False): + def feature_annotations_table( + self, + molecular_metadata=None, + drop_unannotated=False, + report_best_only=False + ): """Generate a comprehensive annotation table for all loaded mass features across samples. This method consolidates MS1 molecular formula assignments and MS2 spectral @@ -2345,6 +2350,9 @@ def feature_annotations_table(self, molecular_metadata=None, drop_unannotated=Fa If True, drops rows where all annotation columns (everything except cluster, MS2 Spectrum, and representative_sample) are NaN. Default is False. + report_best_only : bool, optional + If True, only includes the best MS2 annotation per mass feature based on confidence score. + Default is False, which includes all MS2 annotations for each mass feature. Returns ------- @@ -2501,6 +2509,19 @@ def feature_annotations_table(self, molecular_metadata=None, drop_unannotated=Fa else: collection_report = collection_report.sort_values(by=sort_cols) + if report_best_only: + # Keep only the best annotation per cluster based on the first annotation column available + if 'Entropy Similarity' in collection_report.columns: + best_annot_col = 'Entropy Similarity' + elif 'Confidence Score' in collection_report.columns: + best_annot_col = 'Confidence Score' + else: + best_annot_col = None + + if best_annot_col is not None: + collection_report = collection_report.sort_values(by=['cluster', best_annot_col], ascending=[True, False]) + collection_report = collection_report.drop_duplicates(subset=['cluster'], keep='first') + return collection_report @property diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index d29097a85..641d4486d 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -1642,16 +1642,18 @@ def combine_reports(self, mf_report, ms1_annot_report, ms2_annot_report): on=["mf_id", "isotopologue_type"], ) if ms2_annot_report is not None: - # Check if ion_formula column exists (it may not if MS1 formula search wasn't run) - if "ion_formula" in mf_report.columns: - # pull out the records with ion_formula and drop the ion_formula column (these should be empty if MS1 molecular formula assignment is working correctly) + # If both reports contain 'ion_formula', prefer a merge that respects it. + # Otherwise fall back to merging on 'mf_id' only to remain robust when + # MS1 formula assignment wasn't performed or MS2 summary lacks the field. + if "ion_formula" in mf_report.columns and "ion_formula" in ms2_annot_report.columns: + # pull out the records without ion_formula and merge on mf_id only mf_no_ion_formula = mf_report[mf_report["ion_formula"].isna()] - mf_no_ion_formula = mf_no_ion_formula.drop(columns=["ion_formula"]) + mf_no_ion_formula = mf_no_ion_formula.drop(columns=["ion_formula"]) if "ion_formula" in mf_no_ion_formula.columns else mf_no_ion_formula mf_no_ion_formula = pd.merge( mf_no_ion_formula, ms2_annot_report, how="left", on=["mf_id"] ) - # pull out the records with ion_formula + # pull out the records with ion_formula and merge on mf_id + ion_formula mf_with_ion_formula = mf_report[~mf_report["ion_formula"].isna()] mf_with_ion_formula = pd.merge( mf_with_ion_formula, @@ -1663,7 +1665,7 @@ def combine_reports(self, mf_report, ms1_annot_report, ms2_annot_report): # put back together mf_report = pd.concat([mf_no_ion_formula, mf_with_ion_formula]) else: - # No ion_formula column (MS1 formula search wasn't run), merge on mf_id only + # Fall back to merging on mf_id only (robust when ion_formula missing) mf_report = pd.merge( mf_report, ms2_annot_report, how="left", on=["mf_id"] ) From a0cd5eac92508f4d552b99bfbeea928e2573a7b2 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 1 Apr 2026 13:04:06 -0700 Subject: [PATCH 151/158] Improve tests by setting scope of fixture and add defensive getitem for collection --- conftest.py | 4 +- corems/mass_spectra/factory/lc_class.py | 27 +++++++++- tests/test_lcms_collection.py | 71 +++++++++++++++++++------ 3 files changed, 83 insertions(+), 19 deletions(-) diff --git a/conftest.py b/conftest.py index a2ad0f1e9..34ac5a36e 100644 --- a/conftest.py +++ b/conftest.py @@ -6,7 +6,7 @@ from corems.mass_spectra.input.rawFileReader import ImportMassSpectraThermoMSFileReader -@pytest.fixture +@pytest.fixture(scope="module") def mass_spectrum_ftms(bruker_transient): """Creates a mass spectrum object to be used in the tests""" # Instantiate the mass spectrum object @@ -41,7 +41,7 @@ def bruker_transient(ftms_file_location): return bruker_transient -@pytest.fixture +@pytest.fixture(scope="module") def lcms_obj(): """Returns an LCMS object for the tests""" file_raw = ( diff --git a/corems/mass_spectra/factory/lc_class.py b/corems/mass_spectra/factory/lc_class.py index 78241aefc..65638c278 100644 --- a/corems/mass_spectra/factory/lc_class.py +++ b/corems/mass_spectra/factory/lc_class.py @@ -1772,6 +1772,10 @@ def _reorder_lcms_objects(self): self._lcms = {k: self._lcms[k] for k in ordered_samples} def __getitem__(self, index): + if isinstance(index, (float, np.floating, np.ndarray)): + index = int(index) + elif isinstance(index, np.integer): + index = int(index) samp_name = self.samples[index] self._lcms[samp_name] return self._lcms[samp_name] @@ -1783,7 +1787,7 @@ def _prepare_lcms_mass_features_for_combination(self, lcms_obj, induced_features """ Prepares the mass features in the LCMS objects in the collection for combination. """ - if induced_features == True: + if induced_features: mf_df = lcms_obj.mass_features_to_df(induced_features = True) # Check if lcms_obj has attribute light_mf_df elif hasattr(lcms_obj, "light_mf_df"): @@ -1807,7 +1811,11 @@ def _prepare_lcms_mass_features_for_combination(self, lcms_obj, induced_features mf_df = mf_df.reset_index(drop=False) # Add sample name and sample id to the dataframe mf_df["sample_name"] = lcms_obj.sample_name - mf_df["sample_id"] = self.manifest[lcms_obj.sample_name]["collection_id"] + # Ensure sample_id is stored as an integer to avoid float indices later + try: + mf_df["sample_id"] = int(self.manifest[lcms_obj.sample_name]["collection_id"]) + except Exception: + mf_df["sample_id"] = self.manifest[lcms_obj.sample_name]["collection_id"] mf_df["coll_mf_id"] = mf_df["sample_id"].astype(str) + "_" + mf_df["mf_id"].astype(str) # For induced features, extract cluster from mf_id (format: c{cluster}_{index}_i) @@ -1899,6 +1907,21 @@ def _combine_mass_features(self, induced_features = False): return combined_mass_features = pd.concat(mf_df_list) + # Ensure sample_id and cluster columns have integer dtypes where possible + if "sample_id" in combined_mass_features.columns: + try: + combined_mass_features["sample_id"] = combined_mass_features["sample_id"].astype(int) + except Exception: + combined_mass_features["sample_id"] = pd.to_numeric( + combined_mass_features["sample_id"], errors="coerce" + ).astype("Int64") + if "cluster" in combined_mass_features.columns: + try: + combined_mass_features["cluster"] = combined_mass_features["cluster"].astype(int) + except Exception: + combined_mass_features["cluster"] = pd.to_numeric( + combined_mass_features["cluster"], errors="coerce" + ).astype("Int64") # Move coll_mf_id, sample_name, sample_id, and mf_id to front cols = combined_mass_features.columns.tolist() top_cols = ["coll_mf_id", "sample_name", "sample_id", "mf_id", "mz", "scan_time_aligned", "cluster"] diff --git a/tests/test_lcms_collection.py b/tests/test_lcms_collection.py index 48592e00b..1141a52cf 100644 --- a/tests/test_lcms_collection.py +++ b/tests/test_lcms_collection.py @@ -2,6 +2,7 @@ import numpy as np import pytest import pandas as pd +import copy as copy from corems.mass_spectra.input.corems_hdf5 import ReadCoreMSHDFMassSpectraCollection from corems.mass_spectra.output.export import LCMSMetabolomicsExport, LCMSCollectionExport @@ -9,8 +10,8 @@ from corems.molecular_id.search.database_interfaces import MSPInterface -@pytest.fixture -def lcms_collection_folder(tmp_path, lcms_obj): +@pytest.fixture(scope="module") +def lcms_collection_folder(tmp_path_factory, lcms_obj): """ Creates a temporary folder with processed LCMS objects for collection testing. @@ -21,9 +22,8 @@ def lcms_collection_folder(tmp_path, lcms_obj): This setup allows comprehensive testing of gap filling functionality. """ - # Create a temporary folder for processed data - processed_folder = tmp_path / "processed_lcms_collection" - processed_folder.mkdir() + # Create a temporary folder for processed data (module-safe) + processed_folder = tmp_path_factory.mktemp("processed_lcms_collection") # Set parameters on the LCMS object that are reasonable for testing lcms_obj.parameters = LCMSParameters(use_defaults=True) @@ -90,7 +90,7 @@ def lcms_collection_folder(tmp_path, lcms_obj): return processed_folder -@pytest.fixture +@pytest.fixture(scope="module") def lcms_collection(lcms_collection_folder): """ Creates an LCMSCollection object from processed LCMS data. @@ -137,6 +137,9 @@ def lcms_collection(lcms_collection_folder): def test_lcms_collection_creation(lcms_collection): """Test that an LCMSCollection can be created and has expected properties.""" + # Make a test-wide deep copy of the collection for use in multiple tests without modifying the original + lcms_collection = copy.deepcopy(lcms_collection) + # Check that the collection was created assert lcms_collection is not None @@ -162,6 +165,9 @@ def test_lcms_collection_creation(lcms_collection): def test_lcms_collection_mass_features_dataframe(lcms_collection): """Test that mass features from all samples are combined correctly.""" + # Make a test-wide deep copy of the collection for use in multiple tests without modifying the original + lcms_collection = copy.deepcopy(lcms_collection) + # Get mass features dataframe mf_df = lcms_collection.mass_features_dataframe @@ -184,6 +190,9 @@ def test_lcms_collection_mass_features_dataframe(lcms_collection): def test_lcms_collection_rt_alignment(lcms_collection): """Test retention time alignment across samples in the collection.""" + # Make a test-wide deep copy of the collection for use in multiple tests without modifying the original + lcms_collection = copy.deepcopy(lcms_collection) + # Check initial state assert not lcms_collection.rt_aligned @@ -224,8 +233,11 @@ def test_lcms_collection_rt_alignment(lcms_collection): def test_lcms_collection_consensus_features(lcms_collection): """Test generation of consensus mass features (clustering).""" + # Make a test-wide deep copy of the collection for use in multiple tests without modifying the original + lcms_collection = copy.deepcopy(lcms_collection) + # Ensure alignment is done first - if not lcms_collection.rt_aligned: + if not lcms_collection.rt_alignment_attempted: lcms_collection.align_lcms_objects() # Generate consensus features @@ -252,8 +264,10 @@ def test_lcms_collection_consensus_features(lcms_collection): def test_lcms_collection_gap_filling(lcms_collection): """Test gap filling to create induced mass features.""" + # Make a test-wide deep copy of the collection for use in multiple tests without modifying the original + lcms_collection = copy.deepcopy(lcms_collection) # Setup: align and cluster first - if not lcms_collection.rt_aligned: + if not lcms_collection.rt_alignment_attempted: lcms_collection.align_lcms_objects() lcms_collection.add_consensus_mass_features() @@ -303,8 +317,11 @@ def test_lcms_collection_gap_filling(lcms_collection): def test_lcms_collection_pivot_table(lcms_collection): """Test creation of pivot tables for collection data.""" + # Make a test-wide deep copy of the collection for use in multiple tests without modifying the original + lcms_collection = copy.deepcopy(lcms_collection) + # Setup: ensure we have clustered features - if not lcms_collection.rt_aligned: + if not lcms_collection.rt_alignment_attempted: lcms_collection.align_lcms_objects() lcms_collection.add_consensus_mass_features() @@ -356,8 +373,11 @@ def test_lcms_collection_pivot_table(lcms_collection): def test_lcms_collection_cluster_representatives(lcms_collection): """Test extraction of representative features for each cluster.""" + # Make a test-wide deep copy of the collection for use in multiple tests without modifying the original + lcms_collection = copy.deepcopy(lcms_collection) + # Setup: ensure we have clustered features - if not lcms_collection.rt_aligned: + if not lcms_collection.rt_alignment_attempted: lcms_collection.align_lcms_objects() lcms_collection.add_consensus_mass_features() @@ -381,8 +401,11 @@ def test_lcms_collection_cluster_representatives(lcms_collection): def test_lcms_collection_export_import_hdf5(lcms_collection, tmp_path): """Test exporting and re-importing a collection from HDF5.""" + # Make a test-wide deep copy of the collection for use in multiple tests without modifying the original + lcms_collection = copy.deepcopy(lcms_collection) + # Setup: align and cluster - if not lcms_collection.rt_aligned: + if not lcms_collection.rt_alignment_attempted: lcms_collection.align_lcms_objects() lcms_collection.add_consensus_mass_features() @@ -426,6 +449,9 @@ def test_lcms_collection_export_import_hdf5(lcms_collection, tmp_path): def test_lcms_collection_drop_isotopologues(lcms_collection): """Test dropping isotopologues from the collection.""" + # Make a test-wide deep copy of the collection for use in multiple tests without modifying the original + lcms_collection = copy.deepcopy(lcms_collection) + # Get initial mass features count initial_mf_count = len(lcms_collection.mass_features_dataframe) @@ -443,8 +469,11 @@ def test_lcms_collection_drop_isotopologues(lcms_collection): def test_lcms_collection_feature_annotations_table(lcms_collection, msp_file_location): """Test creation of feature annotations table with molecular metadata.""" + # Make a test-wide deep copy of the collection for use in multiple tests without modifying the original + lcms_collection = copy.deepcopy(lcms_collection) + # Setup: align and cluster - if not lcms_collection.rt_aligned: + if not lcms_collection.rt_alignment_attempted: lcms_collection.align_lcms_objects() lcms_collection.add_consensus_mass_features() @@ -511,8 +540,11 @@ def test_lcms_collection_plot_cluster_with_ms2_mirror(lcms_collection, msp_file_ import matplotlib matplotlib.use('Agg') # Use non-interactive backend for testing + # Make a test-wide deep copy of the collection for use in multiple tests without modifying the original + lcms_collection = copy.deepcopy(lcms_collection) + # Setup: align and cluster - if not lcms_collection.rt_aligned: + if not lcms_collection.rt_alignment_attempted: lcms_collection.align_lcms_objects() lcms_collection.add_consensus_mass_features() @@ -591,8 +623,11 @@ def test_lcms_collection_plot_cluster_with_ms2_mirror(lcms_collection, msp_file_ def test_lcms_collection_molecular_formula_search(lcms_collection, postgres_database): """Test molecular formula search on consensus features.""" + # Make a test-wide deep copy of the collection for use in multiple tests without modifying the original + lcms_collection = copy.deepcopy(lcms_collection) + # Setup: align and cluster - if not lcms_collection.rt_aligned: + if not lcms_collection.rt_alignment_attempted: lcms_collection.align_lcms_objects() lcms_collection.add_consensus_mass_features() @@ -661,6 +696,9 @@ def test_lcms_collection_update_raw_file_locations(lcms_collection, tmp_path): """Test updating raw file locations in the collection.""" import shutil + # Make a test-wide deep copy of the collection for use in multiple tests without modifying the original + lcms_collection = copy.deepcopy(lcms_collection) + # Create a new path for raw files new_raw_folder = tmp_path / "new_raw_location" new_raw_folder.mkdir() @@ -695,6 +733,9 @@ def test_lcms_collection_minimal_workflow(lcms_collection): 4. Perform gap filling 5. Create reports """ + # Make a test-wide deep copy of the collection for use in multiple tests without modifying the original + lcms_collection = copy.deepcopy(lcms_collection) + # Step 1: Collection is loaded via fixture assert len(lcms_collection) > 0 @@ -757,7 +798,7 @@ def test_lcms_collection_plotting_methods(lcms_collection): matplotlib.use('Agg') # Use non-interactive backend for testing # Setup: align and cluster - if not lcms_collection.rt_aligned: + if not lcms_collection.rt_alignment_attempted: lcms_collection.align_lcms_objects() lcms_collection.add_consensus_mass_features() From d3d3c088890f9f83bb47172e328642abbd46caf5 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 1 Apr 2026 13:05:48 -0700 Subject: [PATCH 152/158] Update conftest fixture's scope --- conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conftest.py b/conftest.py index 34ac5a36e..0674c2995 100644 --- a/conftest.py +++ b/conftest.py @@ -6,7 +6,7 @@ from corems.mass_spectra.input.rawFileReader import ImportMassSpectraThermoMSFileReader -@pytest.fixture(scope="module") +@pytest.fixture def mass_spectrum_ftms(bruker_transient): """Creates a mass spectrum object to be used in the tests""" # Instantiate the mass spectrum object From ac09042ccb85f3558fcd696f98d656df1ed7111a Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 1 Apr 2026 13:46:58 -0700 Subject: [PATCH 153/158] Add decoding when reading back in spectral search results --- corems/mass_spectra/input/corems_hdf5.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/corems/mass_spectra/input/corems_hdf5.py b/corems/mass_spectra/input/corems_hdf5.py index 8627152d3..e9dc5cf38 100644 --- a/corems/mass_spectra/input/corems_hdf5.py +++ b/corems/mass_spectra/input/corems_hdf5.py @@ -707,7 +707,11 @@ def import_spectral_search_results(self, mass_spectra): ) for key in ms2_results_load[k][k2].keys() - {"precursor_mz"}: - setattr(ms2_search_res, key, list(ms2_results_load[k][k2][key][:])) + data = list(ms2_results_load[k][k2][key][:]) + if data and isinstance(data[0], bytes): + data = [x.decode("utf-8") for x in data] + setattr(ms2_search_res, key, data) + overall_results_dict[int(k)][ ms2_results_load[k][k2].attrs["precursor_mz"] ] = ms2_search_res From 1f2f339d83ac65664e6ee7d7cbea5851a5931b02 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 1 Apr 2026 15:16:00 -0700 Subject: [PATCH 154/158] Add parameter to not update individual lcms objects --- corems/mass_spectra/output/export.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/corems/mass_spectra/output/export.py b/corems/mass_spectra/output/export.py index 641d4486d..94e5cd622 100644 --- a/corems/mass_spectra/output/export.py +++ b/corems/mass_spectra/output/export.py @@ -2199,7 +2199,12 @@ def __init__(self, out_file_path, mass_spectra_collection): self.out_file_path = Path(out_file_path) self.mass_spectra_collection = mass_spectra_collection - def export_to_hdf5(self, overwrite = False, save_parameters=True, parameter_format="toml"): + def export_to_hdf5( + self, + overwrite = False, + save_parameters=True, + parameter_format="toml", + update_lcms_objects=True): """Export the LCMS collection to an HDF5 file. This method saves the collection-level data to an HDF5 file, including: @@ -2218,6 +2223,14 @@ def export_to_hdf5(self, overwrite = False, save_parameters=True, parameter_form If True, overwrites the output file if it already exists and replaces existing groups within the HDF5 file. If False, appends new data to existing file without overwriting existing groups. Default is False. + save_parameters : bool, optional + If True, saves the collection-level parameters to a separate file in the specified format. + Default is True. + parameter_format : str, optional + The format for saving parameters, either "json" or "toml". Default is "toml". + update_lcms_objects : bool, optional + If True, updates the individual LCMS object HDF5 files with new raw file locations and any additional + information produced during the processing of the collection (e.g. cluster mass feature associations). Default is True. Notes ----- @@ -2273,7 +2286,8 @@ def export_to_hdf5(self, overwrite = False, save_parameters=True, parameter_form # Save updated mass features for each LCMS object # This implements selective update: only loaded features are updated, non-cluster features are preserved - self._save_lcms_objects_to_hdf5(cluster_mf_map, overwrite) + if update_lcms_objects: + self._save_lcms_objects_to_hdf5(cluster_mf_map, overwrite) # Save collection-level parameters as separate file if save_parameters: From e925659148cf1b78ee06d9750d9712eb59e55317 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 1 Apr 2026 15:46:58 -0700 Subject: [PATCH 155/158] Better handle skipping adding eics to fix notebook plotting --- corems/mass_spectra/calc/lc_calc.py | 10 ++++++---- corems/mass_spectra/calc/lc_calc_operations.py | 4 +++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index acc555f1f..9b85a1605 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -3849,8 +3849,9 @@ def plot_cluster( sample_id = int(row['sample_id']) sample = self[sample_id] if hasattr(sample, 'eics') and sample.eics: - has_eics = True - break + if len(sample.eics) > 0: + has_eics = True + break # Also check induced features if available induced_cluster_mfs = None @@ -3862,8 +3863,9 @@ def plot_cluster( sample_id = int(row['sample_id']) sample = self[sample_id] if hasattr(sample, 'eics') and sample.eics: - has_eics = True - break + if len(sample.eics) > 0: + has_eics = True + break if not has_eics: to_plot = [x for x in to_plot if x != "EIC"] diff --git a/corems/mass_spectra/calc/lc_calc_operations.py b/corems/mass_spectra/calc/lc_calc_operations.py index d9bc693a0..d26646fd9 100644 --- a/corems/mass_spectra/calc/lc_calc_operations.py +++ b/corems/mass_spectra/calc/lc_calc_operations.py @@ -1088,7 +1088,9 @@ def execute(self, sample_id, collection, cluster_mz_dict=None, **runtime_params) # Get m/z values for this sample that belong to clusters sample_cluster_mz = set(cluster_mz_dict[sample_id]) - + if len(sample_cluster_mz) == 0: + return {} + # Load EICs for each of the sample_cluster_mz hdf5_path = sample.file_location if hdf5_path and hdf5_path.exists(): From a308df66b98a0fbd16104e8cc96890bdd4aaa36d Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Wed, 1 Apr 2026 16:16:57 -0700 Subject: [PATCH 156/158] Small changes to notebook for fix --- .../notebooks/LCMS_Collection_Tutorial.ipynb | 284 +++++++++--------- 1 file changed, 146 insertions(+), 138 deletions(-) diff --git a/examples/notebooks/LCMS_Collection_Tutorial.ipynb b/examples/notebooks/LCMS_Collection_Tutorial.ipynb index f7e823634..bf00e9bd7 100644 --- a/examples/notebooks/LCMS_Collection_Tutorial.ipynb +++ b/examples/notebooks/LCMS_Collection_Tutorial.ipynb @@ -33,7 +33,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 1, "id": "c6d8a470", "metadata": {}, "outputs": [], @@ -76,7 +76,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 2, "id": "11d13a80", "metadata": {}, "outputs": [ @@ -113,7 +113,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 3, "id": "96802b8e", "metadata": {}, "outputs": [ @@ -182,7 +182,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 4, "id": "b8528773", "metadata": {}, "outputs": [ @@ -229,7 +229,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 5, "id": "edca055c", "metadata": {}, "outputs": [ @@ -264,7 +264,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 6, "id": "fa186ab4", "metadata": {}, "outputs": [ @@ -299,7 +299,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 7, "id": "921ca2b2", "metadata": {}, "outputs": [ @@ -340,7 +340,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 8, "id": "3f61de41", "metadata": {}, "outputs": [ @@ -378,7 +378,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 9, "id": "820ce6ab", "metadata": {}, "outputs": [ @@ -424,7 +424,7 @@ "type": "string" } ], - "ref": "121888dd-9611-495d-bd37-3d8624e76158", + "ref": "8bd0c909-8058-4a6d-b68a-075bfea404de", "rows": [ [ "test_sample_01", @@ -529,7 +529,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 10, "id": "094b8400", "metadata": {}, "outputs": [ @@ -557,8 +557,8 @@ }, { "name": "sample_id", - "rawType": "float64", - "type": "float" + "rawType": "int64", + "type": "integer" }, { "name": "mf_id", @@ -666,12 +666,12 @@ "type": "float" } ], - "ref": "a8ed0404-b3ad-44bd-be82-8f95a5786353", + "ref": "a471e8e6-e476-43d8-84e7-41922081a3a7", "rows": [ [ "0_0", "test_sample_01", - "0.0", + "0", "0.0", "301.21661376953125", "untargeted", @@ -697,7 +697,7 @@ [ "0_1", "test_sample_01", - "0.0", + "0", "1.0", "367.35748291015625", "untargeted", @@ -723,7 +723,7 @@ [ "0_10", "test_sample_01", - "0.0", + "0", "10.0", "698.62890625", "untargeted", @@ -749,7 +749,7 @@ [ "0_100", "test_sample_01", - "0.0", + "0", "100.0", "569.1968994140625", "untargeted", @@ -775,7 +775,7 @@ [ "0_101", "test_sample_01", - "0.0", + "0", "101.0", "300.2048645019531", "untargeted", @@ -801,7 +801,7 @@ [ "0_102", "test_sample_01", - "0.0", + "0", "102.0", "456.35662841796875", "untargeted", @@ -827,7 +827,7 @@ [ "0_103", "test_sample_01", - "0.0", + "0", "103.0", "527.4675903320312", "untargeted", @@ -853,7 +853,7 @@ [ "0_104", "test_sample_01", - "0.0", + "0", "104.0", "736.5104370117188", "untargeted", @@ -879,7 +879,7 @@ [ "0_105", "test_sample_01", - "0.0", + "0", "105.0", "256.2359619140625", "untargeted", @@ -905,7 +905,7 @@ [ "0_106", "test_sample_01", - "0.0", + "0", "106.0", "384.3563232421875", "untargeted", @@ -1004,7 +1004,7 @@ " \n", " 0_0\n", " test_sample_01\n", - " 0.0\n", + " 0\n", " 0.0\n", " 301.216614\n", " untargeted\n", @@ -1028,7 +1028,7 @@ " \n", " 0_1\n", " test_sample_01\n", - " 0.0\n", + " 0\n", " 1.0\n", " 367.357483\n", " untargeted\n", @@ -1052,7 +1052,7 @@ " \n", " 0_10\n", " test_sample_01\n", - " 0.0\n", + " 0\n", " 10.0\n", " 698.628906\n", " untargeted\n", @@ -1076,7 +1076,7 @@ " \n", " 0_100\n", " test_sample_01\n", - " 0.0\n", + " 0\n", " 100.0\n", " 569.196899\n", " untargeted\n", @@ -1100,7 +1100,7 @@ " \n", " 0_101\n", " test_sample_01\n", - " 0.0\n", + " 0\n", " 101.0\n", " 300.204865\n", " untargeted\n", @@ -1124,7 +1124,7 @@ " \n", " 0_102\n", " test_sample_01\n", - " 0.0\n", + " 0\n", " 102.0\n", " 456.356628\n", " untargeted\n", @@ -1148,7 +1148,7 @@ " \n", " 0_103\n", " test_sample_01\n", - " 0.0\n", + " 0\n", " 103.0\n", " 527.467590\n", " untargeted\n", @@ -1172,7 +1172,7 @@ " \n", " 0_104\n", " test_sample_01\n", - " 0.0\n", + " 0\n", " 104.0\n", " 736.510437\n", " untargeted\n", @@ -1196,7 +1196,7 @@ " \n", " 0_105\n", " test_sample_01\n", - " 0.0\n", + " 0\n", " 105.0\n", " 256.235962\n", " untargeted\n", @@ -1220,7 +1220,7 @@ " \n", " 0_106\n", " test_sample_01\n", - " 0.0\n", + " 0\n", " 106.0\n", " 384.356323\n", " untargeted\n", @@ -1249,16 +1249,16 @@ "text/plain": [ " sample_name sample_id mf_id mz type \\\n", "coll_mf_id \n", - "0_0 test_sample_01 0.0 0.0 301.216614 untargeted \n", - "0_1 test_sample_01 0.0 1.0 367.357483 untargeted \n", - "0_10 test_sample_01 0.0 10.0 698.628906 untargeted \n", - "0_100 test_sample_01 0.0 100.0 569.196899 untargeted \n", - "0_101 test_sample_01 0.0 101.0 300.204865 untargeted \n", - "0_102 test_sample_01 0.0 102.0 456.356628 untargeted \n", - "0_103 test_sample_01 0.0 103.0 527.467590 untargeted \n", - "0_104 test_sample_01 0.0 104.0 736.510437 untargeted \n", - "0_105 test_sample_01 0.0 105.0 256.235962 untargeted \n", - "0_106 test_sample_01 0.0 106.0 384.356323 untargeted \n", + "0_0 test_sample_01 0 0.0 301.216614 untargeted \n", + "0_1 test_sample_01 0 1.0 367.357483 untargeted \n", + "0_10 test_sample_01 0 10.0 698.628906 untargeted \n", + "0_100 test_sample_01 0 100.0 569.196899 untargeted \n", + "0_101 test_sample_01 0 101.0 300.204865 untargeted \n", + "0_102 test_sample_01 0 102.0 456.356628 untargeted \n", + "0_103 test_sample_01 0 103.0 527.467590 untargeted \n", + "0_104 test_sample_01 0 104.0 736.510437 untargeted \n", + "0_105 test_sample_01 0 105.0 256.235962 untargeted \n", + "0_106 test_sample_01 0 106.0 384.356323 untargeted \n", "\n", " scan_time apex_scan start_scan final_scan intensity ... \\\n", "coll_mf_id ... \n", @@ -1350,7 +1350,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 11, "id": "112ccb3c", "metadata": {}, "outputs": [ @@ -1384,7 +1384,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 12, "id": "d989ed7a", "metadata": {}, "outputs": [ @@ -1428,7 +1428,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 13, "id": "4442c8d6", "metadata": {}, "outputs": [ @@ -1453,7 +1453,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 14, "id": "39a6f24b", "metadata": {}, "outputs": [ @@ -1493,7 +1493,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 15, "id": "670d8d60", "metadata": {}, "outputs": [ @@ -1671,7 +1671,7 @@ "type": "float" } ], - "ref": "fac284ed-ac24-4579-a1e4-aa89ecb9cc3b", + "ref": "0eb8abc7-0d90-45fc-89f4-82162cebb1f5", "rows": [ [ "0", @@ -2444,7 +2444,7 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 16, "id": "a8a9d7d8", "metadata": {}, "outputs": [ @@ -2466,7 +2466,7 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 17, "id": "ed583149", "metadata": {}, "outputs": [ @@ -2511,7 +2511,7 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": 18, "id": "f88226f6", "metadata": {}, "outputs": [ @@ -2528,7 +2528,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|█████████████████████████████████████████| 3/3 [00:19<00:00, 6.57s/sample]" + "100%|█████████████████████████████████████████| 3/3 [00:21<00:00, 7.07s/sample]" ] }, { @@ -2578,7 +2578,7 @@ }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 19, "id": "9638f884", "metadata": {}, "outputs": [ @@ -2616,7 +2616,7 @@ }, { "name": "mz", - "rawType": "float32", + "rawType": "float64", "type": "float" }, { @@ -2725,14 +2725,14 @@ "type": "float" } ], - "ref": "58d93683-7cbb-4648-88a5-111fffba8639", + "ref": "b02ee544-5967-4ef8-a3a5-ec9ec34262d4", "rows": [ [ "2_c0_0_i", "test_sample_03", "2", "c0_0_i", - "301.2166", + "301.21684599466016", "8.895636666666666", "0", "untargeted", @@ -2760,7 +2760,7 @@ "test_sample_03", "2", "c1_1_i", - "302.2206", + "302.2202272054653", "8.895636666666666", "1", "untargeted", @@ -2788,7 +2788,7 @@ "test_sample_03", "2", "c2_2_i", - "367.35748", + "367.3574902204513", "19.152648333333335", "2", "untargeted", @@ -2816,7 +2816,7 @@ "test_sample_03", "2", "c3_3_i", - "368.36118", + "368.36090778141954", "19.152648333333335", "3", "untargeted", @@ -2844,7 +2844,7 @@ "test_sample_03", "2", "c4_4_i", - "698.6289", + "698.6295110553453", "23.816803333333333", "4", "untargeted", @@ -2872,7 +2872,7 @@ "test_sample_03", "2", "c6_5_i", - "699.6312", + "699.6327552911274", "23.816803333333333", "6", "untargeted", @@ -2900,7 +2900,7 @@ "test_sample_03", "2", "c9_6_i", - "227.20189", + "227.2018719032787", "7.376469999999999", "9", "untargeted", @@ -2928,7 +2928,7 @@ "test_sample_03", "2", "c10_7_i", - "299.2011", + "299.2014941880232", "7.376469999999999", "10", "untargeted", @@ -2956,7 +2956,7 @@ "test_sample_03", "2", "c12_8_i", - "455.35266", + "455.3524308281572", "8.96547", "12", "untargeted", @@ -2984,7 +2984,7 @@ "test_sample_03", "2", "c15_9_i", - "735.507", + "735.505748072386", "20.793636666666668", "15", "untargeted", @@ -3085,7 +3085,7 @@ " test_sample_03\n", " 2\n", " c0_0_i\n", - " 301.216614\n", + " 301.216846\n", " 8.895637\n", " 0\n", " untargeted\n", @@ -3109,7 +3109,7 @@ " test_sample_03\n", " 2\n", " c1_1_i\n", - " 302.220612\n", + " 302.220227\n", " 8.895637\n", " 1\n", " untargeted\n", @@ -3133,7 +3133,7 @@ " test_sample_03\n", " 2\n", " c2_2_i\n", - " 367.357483\n", + " 367.357490\n", " 19.152648\n", " 2\n", " untargeted\n", @@ -3157,7 +3157,7 @@ " test_sample_03\n", " 2\n", " c3_3_i\n", - " 368.361176\n", + " 368.360908\n", " 19.152648\n", " 3\n", " untargeted\n", @@ -3181,7 +3181,7 @@ " test_sample_03\n", " 2\n", " c4_4_i\n", - " 698.628906\n", + " 698.629511\n", " 23.816803\n", " 4\n", " untargeted\n", @@ -3205,7 +3205,7 @@ " test_sample_03\n", " 2\n", " c6_5_i\n", - " 699.631226\n", + " 699.632755\n", " 23.816803\n", " 6\n", " untargeted\n", @@ -3229,7 +3229,7 @@ " test_sample_03\n", " 2\n", " c9_6_i\n", - " 227.201889\n", + " 227.201872\n", " 7.376470\n", " 9\n", " untargeted\n", @@ -3253,7 +3253,7 @@ " test_sample_03\n", " 2\n", " c10_7_i\n", - " 299.201111\n", + " 299.201494\n", " 7.376470\n", " 10\n", " untargeted\n", @@ -3277,7 +3277,7 @@ " test_sample_03\n", " 2\n", " c12_8_i\n", - " 455.352661\n", + " 455.352431\n", " 8.965470\n", " 12\n", " untargeted\n", @@ -3301,7 +3301,7 @@ " test_sample_03\n", " 2\n", " c15_9_i\n", - " 735.507019\n", + " 735.505748\n", " 20.793637\n", " 15\n", " untargeted\n", @@ -3328,16 +3328,16 @@ "text/plain": [ " sample_name sample_id mf_id mz scan_time_aligned \\\n", "coll_mf_id \n", - "2_c0_0_i test_sample_03 2 c0_0_i 301.216614 8.895637 \n", - "2_c1_1_i test_sample_03 2 c1_1_i 302.220612 8.895637 \n", - "2_c2_2_i test_sample_03 2 c2_2_i 367.357483 19.152648 \n", - "2_c3_3_i test_sample_03 2 c3_3_i 368.361176 19.152648 \n", - "2_c4_4_i test_sample_03 2 c4_4_i 698.628906 23.816803 \n", - "2_c6_5_i test_sample_03 2 c6_5_i 699.631226 23.816803 \n", - "2_c9_6_i test_sample_03 2 c9_6_i 227.201889 7.376470 \n", - "2_c10_7_i test_sample_03 2 c10_7_i 299.201111 7.376470 \n", - "2_c12_8_i test_sample_03 2 c12_8_i 455.352661 8.965470 \n", - "2_c15_9_i test_sample_03 2 c15_9_i 735.507019 20.793637 \n", + "2_c0_0_i test_sample_03 2 c0_0_i 301.216846 8.895637 \n", + "2_c1_1_i test_sample_03 2 c1_1_i 302.220227 8.895637 \n", + "2_c2_2_i test_sample_03 2 c2_2_i 367.357490 19.152648 \n", + "2_c3_3_i test_sample_03 2 c3_3_i 368.360908 19.152648 \n", + "2_c4_4_i test_sample_03 2 c4_4_i 698.629511 23.816803 \n", + "2_c6_5_i test_sample_03 2 c6_5_i 699.632755 23.816803 \n", + "2_c9_6_i test_sample_03 2 c9_6_i 227.201872 7.376470 \n", + "2_c10_7_i test_sample_03 2 c10_7_i 299.201494 7.376470 \n", + "2_c12_8_i test_sample_03 2 c12_8_i 455.352431 8.965470 \n", + "2_c15_9_i test_sample_03 2 c15_9_i 735.505748 20.793637 \n", "\n", " cluster type scan_time apex_scan start_scan ... \\\n", "coll_mf_id ... \n", @@ -3429,7 +3429,7 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": 20, "id": "4ea2a311", "metadata": {}, "outputs": [ @@ -3468,7 +3468,7 @@ "type": "float" } ], - "ref": "463e6594-04f9-41a8-852d-c6757d4a6eee", + "ref": "d7922a36-e7cd-4604-a617-5d71b6f47390", "rows": [ [ "0", @@ -3672,7 +3672,7 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 21, "id": "e9d00373", "metadata": {}, "outputs": [ @@ -3712,7 +3712,7 @@ "type": "string" } ], - "ref": "01bb4e2b-3f04-499d-9ae5-50d40a07cff9", + "ref": "afba4f8b-9b38-4758-a2b6-e839732a9cac", "rows": [ [ "0", @@ -3916,7 +3916,7 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 22, "id": "e40ba01c", "metadata": {}, "outputs": [ @@ -3953,7 +3953,7 @@ "type": "float" } ], - "ref": "8bc96298-1496-4cd6-994f-f65296c94f72", + "ref": "e32374bd-f492-4a18-9079-bf7599435528", "rows": [ [ "0", @@ -4154,7 +4154,7 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 23, "id": "f11922ae", "metadata": {}, "outputs": [ @@ -4191,8 +4191,8 @@ }, { "name": "sample_id", - "rawType": "float64", - "type": "float" + "rawType": "int64", + "type": "integer" }, { "name": "mf_id", @@ -4325,14 +4325,14 @@ "type": "integer" } ], - "ref": "2bdbf7e2-0a2b-49fb-b578-43255ee6e13e", + "ref": "37c8489f-1993-49ff-84f0-310414bf7b80", "rows": [ [ "0", "0", "0_0", "test_sample_01", - "0.0", + "0", "0.0", "301.21661376953125", "untargeted", @@ -4365,7 +4365,7 @@ "1", "0_19", "test_sample_01", - "0.0", + "0", "19.0", "302.2206115722656", "untargeted", @@ -4398,7 +4398,7 @@ "2", "0_1", "test_sample_01", - "0.0", + "0", "1.0", "367.35748291015625", "untargeted", @@ -4431,7 +4431,7 @@ "3", "0_24", "test_sample_01", - "0.0", + "0", "24.0", "368.3611755371094", "untargeted", @@ -4464,7 +4464,7 @@ "4", "0_10", "test_sample_01", - "0.0", + "0", "10.0", "698.62890625", "untargeted", @@ -4497,7 +4497,7 @@ "6", "0_42", "test_sample_01", - "0.0", + "0", "42.0", "699.6312255859375", "untargeted", @@ -4530,7 +4530,7 @@ "9", "0_28", "test_sample_01", - "0.0", + "0", "28.0", "227.20188903808594", "untargeted", @@ -4563,7 +4563,7 @@ "10", "0_9", "test_sample_01", - "0.0", + "0", "9.0", "299.20111083984375", "untargeted", @@ -4596,7 +4596,7 @@ "12", "0_21", "test_sample_01", - "0.0", + "0", "21.0", "455.3526611328125", "untargeted", @@ -4629,7 +4629,7 @@ "15", "0_35", "test_sample_01", - "0.0", + "0", "35.0", "735.5070190429688", "untargeted", @@ -4711,7 +4711,7 @@ " 0\n", " 0_0\n", " test_sample_01\n", - " 0.0\n", + " 0\n", " 0.0\n", " 301.216614\n", " untargeted\n", @@ -4735,7 +4735,7 @@ " 1\n", " 0_19\n", " test_sample_01\n", - " 0.0\n", + " 0\n", " 19.0\n", " 302.220612\n", " untargeted\n", @@ -4759,7 +4759,7 @@ " 2\n", " 0_1\n", " test_sample_01\n", - " 0.0\n", + " 0\n", " 1.0\n", " 367.357483\n", " untargeted\n", @@ -4783,7 +4783,7 @@ " 3\n", " 0_24\n", " test_sample_01\n", - " 0.0\n", + " 0\n", " 24.0\n", " 368.361176\n", " untargeted\n", @@ -4807,7 +4807,7 @@ " 4\n", " 0_10\n", " test_sample_01\n", - " 0.0\n", + " 0\n", " 10.0\n", " 698.628906\n", " untargeted\n", @@ -4831,7 +4831,7 @@ " 6\n", " 0_42\n", " test_sample_01\n", - " 0.0\n", + " 0\n", " 42.0\n", " 699.631226\n", " untargeted\n", @@ -4855,7 +4855,7 @@ " 9\n", " 0_28\n", " test_sample_01\n", - " 0.0\n", + " 0\n", " 28.0\n", " 227.201889\n", " untargeted\n", @@ -4879,7 +4879,7 @@ " 10\n", " 0_9\n", " test_sample_01\n", - " 0.0\n", + " 0\n", " 9.0\n", " 299.201111\n", " untargeted\n", @@ -4903,7 +4903,7 @@ " 12\n", " 0_21\n", " test_sample_01\n", - " 0.0\n", + " 0\n", " 21.0\n", " 455.352661\n", " untargeted\n", @@ -4927,7 +4927,7 @@ " 15\n", " 0_35\n", " test_sample_01\n", - " 0.0\n", + " 0\n", " 35.0\n", " 735.507019\n", " untargeted\n", @@ -4953,16 +4953,16 @@ ], "text/plain": [ " cluster coll_mf_id sample_name sample_id mf_id mz \\\n", - "0 0 0_0 test_sample_01 0.0 0.0 301.216614 \n", - "2 1 0_19 test_sample_01 0.0 19.0 302.220612 \n", - "4 2 0_1 test_sample_01 0.0 1.0 367.357483 \n", - "6 3 0_24 test_sample_01 0.0 24.0 368.361176 \n", - "8 4 0_10 test_sample_01 0.0 10.0 698.628906 \n", - "10 6 0_42 test_sample_01 0.0 42.0 699.631226 \n", - "12 9 0_28 test_sample_01 0.0 28.0 227.201889 \n", - "14 10 0_9 test_sample_01 0.0 9.0 299.201111 \n", - "16 12 0_21 test_sample_01 0.0 21.0 455.352661 \n", - "18 15 0_35 test_sample_01 0.0 35.0 735.507019 \n", + "0 0 0_0 test_sample_01 0 0.0 301.216614 \n", + "2 1 0_19 test_sample_01 0 19.0 302.220612 \n", + "4 2 0_1 test_sample_01 0 1.0 367.357483 \n", + "6 3 0_24 test_sample_01 0 24.0 368.361176 \n", + "8 4 0_10 test_sample_01 0 10.0 698.628906 \n", + "10 6 0_42 test_sample_01 0 42.0 699.631226 \n", + "12 9 0_28 test_sample_01 0 28.0 227.201889 \n", + "14 10 0_9 test_sample_01 0 9.0 299.201111 \n", + "16 12 0_21 test_sample_01 0 21.0 455.352661 \n", + "18 15 0_35 test_sample_01 0 35.0 735.507019 \n", "\n", " type scan_time apex_scan start_scan ... monoisotopic_mf_id \\\n", "0 untargeted 8.895637 1882.0 1828.0 ... None \n", @@ -5050,7 +5050,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 24, "id": "a52580c8", "metadata": {}, "outputs": [ @@ -5066,7 +5066,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|█████████████████████████████████████████| 3/3 [00:12<00:00, 4.32s/sample]\n" + "100%|█████████████████████████████████████████| 3/3 [00:12<00:00, 4.12s/sample]\n" ] }, { @@ -5081,7 +5081,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|███████████████████████████████████████| 3/3 [00:00<00:00, 1592.37sample/s]" + "100%|███████████████████████████████████████| 3/3 [00:00<00:00, 1808.15sample/s]" ] }, { @@ -5148,7 +5148,7 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 25, "id": "286c4eec", "metadata": {}, "outputs": [ @@ -5264,7 +5264,7 @@ "type": "string" } ], - "ref": "49ab3209-021b-49e5-8a89-30f8d66d0992", + "ref": "645bc1ee-a119-4768-b783-f8dbf254c05d", "rows": [ [ "1", @@ -5604,13 +5604,13 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "id": "3ce506a0", "metadata": {}, "outputs": [ { "data": { - "image/png": "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9ddf73reqlUrtW7dWo0bN9a8efN04YUXnvb6KwMCN6qk7OxsPfbYY/r888/Vr18/SVLr1q21cuVKPffcc0cNSHFxcaWulrhnzx7FxcV5vGZUXqe6Px3Jz89P7dq1019//eXJclFFdOjQQStXrlRqaqry8vIUHR2tc889Vx07djzqMnxH4VhOZZ86Et9TNc+9996rWbNmacGCBapXr56rPS4uTnl5eUpJSXHrlTzWd05x+549e1SnTh23Zdq2beuR+lG5lOf+VJZGjRopKipKf/31V7UJ3AwpR5WUn5+v/Px82Wzuu7CPj4+cTudRl+vUqZPmzJnj1jZ79my3o/+oeU51fzqSw+HQ6tWr3f4IAcLDwxUdHa3Nmzfr999/1xVXXHHUefmOwok4mX3qSHxP1RzGGN177736/PPP9fPPP6thw4Zu0zt06CA/Pz+375yNGzdqx44dR/3OadiwoeLi4tyWSUtL09KlS/mequY8sT+V5Z9//tGBAweq13eUN6/YBhxLenq6WbFihVmxYoWRZCZOnGhWrFhhtm/fbowxplu3buass84yc+fONVu2bDFTpkwxAQEB5pVXXnGtY+DAgWbkyJGu17/88ovx9fU1zz33nFm/fr0ZPXq08fPzM6tXr67w7UPF8sT+NHbsWPPDDz+Yv//+2/zxxx/m+uuvNwEBAWbt2rUVvn2oeMfbpz755BMzd+5c8/fff5svvvjCNGjQwFx11VVu6+A7CiV5Yp/ie6rmuuuuu0x4eLiZN2+eSUpKcj2ysrJc89x5552mfv365ueffza///676dSpk+nUqZPbepo1a2Y+++wz1+tnnnnGREREmC+//NL8+eef5oorrjANGzY02dnZFbZtqHie2J/S09PNQw89ZJYsWWK2bt1qfvrpJ9O+fXvTtGlTk5OTU6Hb50kEblRac+fONZJKPQYPHmyMMSYpKckMGTLExMfHm4CAANOsWTPz/PPPG6fT6VpHt27dXPMX++STT8wZZ5xh/P39zVlnnWW++eabCtwqeIsn9qfhw4eb+vXrG39/fxMbG2suueQSs3z58greMnjL8fap//3vf6ZevXrGz8/P1K9f3/znP/8xubm5buvgOwoleWKf4nuq5iprX5JkpkyZ4ponOzvb3H333aZWrVomKCjIXHnllSYpKanUekou43Q6zeOPP25iY2ON3W43F154odm4cWMFbRW8xRP7U1ZWlundu7eJjo42fn5+pkGDBmbYsGEmOTm5ArfM8yxjuHoLAAAAAADljXO4AQAAAADwAAI3AAAAAAAeQOAGAAAAAMADCNwAAAAAAHgAgRsAAAAAAA8gcAMAAAAA4AEEbgAAAAAAPIDADQAAAACABxC4AQDwgiFDhqh///5ee/+BAwfq6aefPq11TJ06VREREeVTkIedd955+vTTT71dBgCghrGMMcbbRQAAUJ1YlnXM6aNHj9aDDz4oY4xXAuuqVavUs2dPbd++XSEhIae8nuzsbKWnpysmJqYcqyv8/D7//PNyPSAxa9YsPfjgg9q4caNsNvobAAAVg39xAAAoZ0lJSa7HCy+8oLCwMLe2hx56SOHh4V7rHZ48ebIGDBhwWmFbkgIDA8s9bHtK3759lZ6eru+++87bpQAAahACNwAA5SwuLs71CA8Pl2VZbm0hISGlhpR3795d9913n4YPH65atWopNjZWb775pjIzMzV06FCFhoaqSZMmpQLjmjVr1LdvX4WEhCg2NlYDBw7U/v37j1qbw+HQzJkzddlll7m1JyYm6v/+7/80aNAghYSEqEGDBvrqq6+0b98+XXHFFQoJCVHr1q31+++/u5Y5ckj5mDFj1LZtW73//vtKTExUeHi4rr/+eqWnp7u9zwsvvOD23m3bttWYMWNc0yXpyiuvlGVZrteS9OWXX6p9+/YKCAhQo0aNNHbsWBUUFEiSjDEaM2aM6tevL7vdrvj4eN1///2uZX18fHTJJZfoo48+OupnAwBAeSNwAwBQSbz77ruKiorSb7/9pvvuu0933XWXBgwYoM6dO2v58uXq3bu3Bg4cqKysLElSSkqKevbsqXbt2un333/X999/rz179ujaa6896nv8+eefSk1NVceOHUtNmzRpks4//3ytWLFC/fr108CBAzVo0CDdfPPNWr58uRo3bqxBgwbpWGej/f333/riiy80a9YszZo1S/Pnz9czzzxzwp/BsmXLJElTpkxRUlKS6/XChQs1aNAgPfDAA1q3bp1ef/11TZ06VU899ZQk6dNPP9WkSZP0+uuva/Pmzfriiy/UqlUrt3Wfc845Wrhw4QnXAgDA6SJwAwBQSbRp00b/+c9/1LRpU40aNUoBAQGKiorSsGHD1LRpUz3xxBM6cOCA/vzzT0nSSy+9pHbt2unpp59W8+bN1a5dO73zzjuaO3euNm3aVOZ7bN++XT4+PmUOBb/kkkt0xx13uN4rLS1NZ599tgYMGKAzzjhDjz76qNavX689e/YcdRucTqemTp2qli1bqkuXLho4cKDmzJlzwp9BdHS0JCkiIkJxcXGu12PHjtXIkSM1ePBgNWrUSBdddJGefPJJvf7665KkHTt2KC4uTr169VL9+vV1zjnnaNiwYW7rjo+P186dO+V0Ok+4HgAATgeBGwCASqJ169au5z4+PoqMjHTrpY2NjZUk7d27V1Lhxc/mzp2rkJAQ16N58+aSCnuay5KdnS273V7mhd1Kvn/xex3r/cuSmJio0NBQ1+s6deocc/4TtWrVKv33v/9129Zhw4YpKSlJWVlZGjBggLKzs9WoUSMNGzZMn3/+uWu4ebHAwEA5nU7l5uaedj0AAJwIX28XAAAACvn5+bm9tizLra04JBf30GZkZOiyyy7T+PHjS62rTp06Zb5HVFSUsrKylJeXJ39//6O+f/F7Hev9T3QbSs5vs9lKDUnPz88/6vqKZWRkaOzYsbrqqqtKTQsICFBCQoI2btyon376SbNnz9bdd9+tZ599VvPnz3fVdPDgQQUHByswMPC47wcAQHkgcAMAUEW1b99en376qRITE+Xre2L/pLdt21aStG7dOtfzihQdHa2kpCTX67S0NG3dutVtHj8/PzkcDre29u3ba+PGjWrSpMlR1x0YGKjLLrtMl112me655x41b95cq1evVvv27SUVXmCuXbt25bg1AAAcG0PKAQCoou655x4dPHhQN9xwg5YtW6a///5bP/zwg4YOHVoqsBaLjo5W+/bttWjRogqutlDPnj31/vvva+HChVq9erUGDx4sHx8ft3kSExM1Z84cJScn69ChQ5KkJ554Qu+9957Gjh2rtWvXav369froo4/0n//8R1LhFdPffvttrVmzRlu2bNEHH3ygwMBANWjQwLXehQsXqnfv3hW3sQCAGo/ADQBAFRUfH69ffvlFDodDvXv3VqtWrTR8+HBFRETIZjv6P/G33Xabpk2bVoGVHjZq1Ch169ZNl156qfr166f+/furcePGbvM8//zzmj17thISElw90n369NGsWbP0448/6uyzz9Z5552nSZMmuQJ1RESE3nzzTZ1//vlq3bq1fvrpJ3399deKjIyUJO3atUuLFy/W0KFDK3aD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Mu+9956r7Z577jGxsbGu1yNGjDBdu3Y1MTEx5tVXXzXGGHPgwAFjWZb53//+V2qdZ5xxhunbt+8Jb8uJOOuss0y3bt1KtT/55JMmODjYbNq0ya195MiRxsfHx+zYscMYY8wDDzxgwsLCSn0mJZ3K76FYeHi4ueeee445T1mf/7hx44xlWWb79u2utsGDBxtJ5umnn3a1HTp0yAQGBhrLssxHH33kat+wYYORZEaPHu1qK94HOnToYPLy8lztEyZMMJLMl19+6Wrr1q2b2+f6/vvvG5vNZhYuXOhW52uvvWYkmV9++eWY21hScHCwueWWW0q1f/PNN0aS+f7778tc7mi/awAAAKAyoYe7HFx77bXKzs7WrFmzlJ6erlmzZpU5nFySAgMDtXTpUj388MOSCof23nrrrapTp47uu+++kx6SeywHDhw45sWlhg0bJh8fn1L1FcvPz9eBAwfUpEkTRUREuA197tKli/bs2aONGzdKKuzJ7tq1q7p06aKFCxdKKuz1NsaU6uGWpFq1apU6v91TZsyYoS5durjes/jRq1cvORwOV099REREuQ91LykiIkJLly51uxL9kUp+/pmZmdq/f786d+4sY4xWrFhRav7bbrvNbf3NmjVTcHCw263XmjVrpoiICG3ZsqXU8rfffrv8/Pxcr++66y75+vrq22+/PWqNM2bM0JlnnqnmzZu7fZ49e/aUJM2dO/eoyx4pOzu71IUFpcLTK4qnAwAAAFUVgbscREdHq1evXpo+fbo+++wzORwO1wWgyhIeHq4JEyZo27Zt2rZtm95++201a9ZML730kp588slyrc0cY2hww4YNS7VlZ2friSeecJ3rHBUVpejoaKWkpCg1NdU1X3GIXrhwoTIzM7VixQp16dJFXbt2dQXuhQsXKiwsTG3atCmzLsuyjln7wYMHlZyc7HqUfP+TsXnzZn3//feKjo52exSf2753715JhUPszzjjDPXt21f16tXTLbfcou+///6U3rMsEyZM0Jo1a5SQkKBzzjlHY8aMKRWCd+zYoSFDhqh27doKCQlRdHS0unXrJkmltj8gIEDR0dFubeHh4apXr16pzzY8PLzMc7ObNm3q9jokJER16tQ55lXYN2/erLVr15b6PM844wxJhz/PExEYGFjmQaacnBzXdAAAAKCq4irl5eTGG2/UsGHDlJycrL59+yoiIuKElmvQoIFuueUWXXnllWrUqJGmTZum//u//yuXmiIjI495Aayywsx9992nKVOmaPjw4erUqZPCw8NlWZauv/56twuMxcfHq2HDhlqwYIESExNljFGnTp0UHR2tBx54QNu3b9fChQvVuXPnMi90dejQoVJh70hXXXWV5s+f73o9ePDgU7rtl9Pp1EUXXaRHHnmkzOnFQTEmJkYrV67UDz/8oO+++07fffedpkyZokGDBundd9896fc90rXXXqsuXbro888/148//qhnn31W48eP12effaa+ffvK4XDooosu0sGDB/Xoo4+qefPmCg4O1q5duzRkyJBSF3g7cnTC8dqPdfDlZDidTrVq1UoTJ04sc3pCQsIJr6tOnTpKSkoq1V7cFh8ff2pFAgAAAJUAgbucXHnllbrjjjv066+/6uOPPz7p5WvVqqXGjRtrzZo15VZT8+bN9emnn57UMjNnztTgwYPd7pOdk5OjlJSUUvN26dJFCxYsUMOGDdW2bVuFhoaqTZs2Cg8P1/fff6/ly5dr7NixpZYrKCjQzp07dfnllx+zlueff97tgMHxwtfReswbN26sjIyMUldrL4u/v78uu+wyXXbZZXI6nbr77rv1+uuv6/HHH1eTJk2O2yt/PHXq1NHdd9+tu+++W3v37lX79u311FNPqW/fvlq9erU2bdqkd9991+0iaZ4a4i4V9lb36NHD9TojI0NJSUm65JJLjrpM48aNtWrVKl144YWn/Xm0bdtWCxculNPpdDsws3TpUgUFBbkOhgAAAABVEUPKy0lISIheffVVjRkzRpdddtlR51u1alWZ5y5v375d69atU7Nmzcqtpk6dOunQoUNlnrt7ND4+PqV6QidPniyHw1Fq3i5dumjbtm36+OOPXUPMbTabOnfurIkTJyo/P7/M87fXrVunnJwcde7c+Zi1dOjQQb169XI9WrRoccz5g4ODyzwwcO2112rJkiX64YcfSk1LSUlRQUGBpMJz3kuy2Wxq3bq1JLmGPRffq7ys9zkWh8NRakh4TEyM4uPjXesu7pku+fkbY1y3JfOEN954Q/n5+a7Xr776qgoKCtS3b9+jLnPttddq165devPNN0tNy87OVmZm5gm//zXXXKM9e/bos88+c7Xt379fM2bM0GWXXVbm+d0AAABAVUEPdzkaPHjwceeZPXu2Ro8ercsvv1znnXeeQkJCtGXLFr3zzjvKzc3VmDFj3OYvHl6+du1aSdL777+vRYsWSZL+85//HPO9+vXrJ19fX/3000+6/fbbT2gbLr30Ur3//vsKDw9XixYttGTJEv3000+KjIwsNW9xmN64caOefvppV3vXrl313XffyW636+yzzy7zMwgKCtJFF110QjWdqA4dOujVV1/V//3f/6lJkyaKiYlRz5499fDDD+urr77SpZdeqiFDhqhDhw7KzMzU6tWrNXPmTG3btk1RUVG67bbbdPDgQfXs2VP16tXT9u3bNXnyZLVt29Z166+2bdvKx8dH48ePV2pqqux2u3r27KmYmJhj1paenq569erpmmuuUZs2bRQSEqKffvpJy5Ytc40maN68uRo3bqyHHnpIu3btUlhYmD799NMTui/2qcrLy9OFF16oa6+9Vhs3btQrr7yiCy644JijDwYOHKhPPvlEd955p+bOnavzzz9fDodDGzZs0CeffKIffvhBHTt2PKH3v+aaa3Teeedp6NChWrdunaKiovTKK6/I4XCUGh2xYMEC1wXu9u3bp8zMTNf/H127dlXXrl1P8VMAAAAAPMRr10ev4kreFuxYjrwt2JYtW8wTTzxhzjvvPBMTE2N8fX1NdHS06devn/n5559LLS/pqI8Tcfnll5sLL7zwhGs/dOiQGTp0qImKijIhISGmT58+ZsOGDaZBgwZm8ODBpeaPiYkxktxua7Zo0SIjyXTp0qXMms4991xz8803n1D9JyM5Odn069fPhIaGGklut41KT083o0aNMk2aNDH+/v4mKirKdO7c2Tz33HOu22LNnDnT9O7d28TExBh/f39Tv359c8cdd5ikpCS393nzzTdNo0aNjI+PzwnfIiw3N9c8/PDDpk2bNiY0NNQEBwebNm3amFdeecVtvnXr1plevXqZkJAQExUVZYYNG2ZWrVplJJkpU6a45hs8eLAJDg4u9T7dunUzZ511Vqn2I/fD4n1g/vz55vbbbze1atUyISEh5qabbjIHDhwotc4jb8GVl5dnxo8fb8466yxjt9tNrVq1TIcOHczYsWNNamrqcT+Pkg4ePGhuvfVWExkZaYKCgky3bt3K3DdHjx591P8XSt7yDAAAAKgsLGPK6UpKqJQWLlyo7t27a8OGDce9SFlFWLlypdq3b6/ly5erbdu23i6nxpo6daqGDh2qZcuWnXBvNAAAAICTwznc1VyXLl3Uu3dvTZgwwdulSJKeeeYZXXPNNYRtAAAAANUe53DXAN999523S3D56KOPvF1CucvIyFBGRsYx54mOjj7q7bqqo9TUVGVnZx9znri4uAqqBgAAAPAOAjdwmp577rkyb39W0tatW5WYmFgxBVUCDzzwwHHvXc7ZLAAAAKjuOIcbOE1btmw57q3XLrjgAgUEBFRQRd63bt067d69+5jznMh90QEAAICqjMANAAAAAIAHcNE0AAAAAAA8gHO4T5LT6dTu3bsVGhoqy7K8XQ4AAAAAoJwYY5Senq74+HjZbKffP03gPkm7d+9WQkKCt8sAAAAAAHjIzp07Va9evdNeD4H7JIWGhkoq/AWEhYV5uRoAAAAAQHlJS0tTQkKCK/edLgL3ceTm5io3N9f1Oj09XZIUFhZG4AYAAACAaqi8Th/momnHMW7cOIWHh7seDCcHAAAAAJwIbgt2HEf2cBcPMUhNTaWHGwAAAACqkbS0NIWHh5db3mNI+XHY7XbZ7XZvlwEAAAAAqGII3B7icDiUn5/v7TLgRX5+fvLx8fF2GQAAAAC8hMBdzowxSk5OVkpKirdLQSUQERGhuLg47tkOAAAA1EAE7nJWHLZjYmIUFBRE0KqhjDHKysrS3r17JUl16tTxckUAAAAAKhqBuxw5HA5X2I6MjPR2OfCywMBASdLevXsVExPD8HIAAACghuG2YOWo+JztoKAgL1eCyqJ4X+B8fgAAAKDmIXB7AMPIUYx9AQAAAKi5CNwAAAAAAHgAgRsAAAAAAA8gcEOSNGTIEFmWJcuy5Ofnp4YNG+qRRx5RTk5OhdaRmJgoy7L00UcflZp21llnybIsTZ061dW2atUqXX755YqJiVFAQIASExN13XXXua4OLkn333+/OnToILvdrrZt21bAVgAAAAAAgRslXHzxxUpKStKWLVs0adIkvf766xo9enSF15GQkKApU6a4tf36669KTk5WcHCwq23fvn268MILVbt2bf3www9av369pkyZovj4eGVmZrotf8stt+i666475vs68h1K/z1d6b+ny5HvKL8NAgAAAFAjEbjhYrfbFRcXp4SEBPXv31+9evXS7NmzXdMPHDigG264QXXr1lVQUJBatWqlDz/80DV91qxZioiIkMNRGFZXrlwpy7I0cuRI1zy33Xabbr755mPWcdNNN2n+/PnauXOnq+2dd97RTTfdJF/fw3ey++WXX5Samqq33npL7dq1U8OGDdWjRw9NmjRJDRs2dM334osv6p577lGjRo1O/cMBAAAAgJNE4K4gmZmZFfo4XWvWrNHixYvl7+/vasvJyVGHDh30zTffaM2aNbr99ts1cOBA/fbbb5KkLl26KD09XStWrJAkzZ8/X1FRUZo3b55rHfPnz1f37t2P+d6xsbHq06eP3n33XUlSVlaWPv74Y91yyy1u88XFxamgoECff/65jDGnvc0AAAAAUJ58jz8LykNISEiFvt+pBNBZs2YpJCREBQUFys3Nlc1m00svveSaXrduXT300EOu1/fdd59++OEHffLJJzrnnHMUHh6utm3bat68eerYsaPmzZunBx98UGPHjlVGRoZSU1P1119/qVu3bset5ZZbbtG//vUv/fvf/9bMmTPVuHHjUudfn3feeXrsscd044036s4779Q555yjnj17atCgQYqNjT3p7QcAAACA8kQPN1x69OihlStXaunSpRo8eLCGDh2qq6++2jXd4XDoySefVKtWrVS7dm2FhITohx9+0I4dO1zzdOvWTfPmzZMxRgsXLtRVV12lM888U4sWLdL8+fMVHx+vpk2bHreWfv36KSMjQwsWLNA777xTqne72FNPPaXk5GS99tprOuuss/Taa6+pefPmWr169el/IAAAAABwGujhriAZGRneLuG4goOD1aRJE0mF50y3adNGb7/9tm699VZJ0rPPPqv//e9/euGFF9SqVSsFBwdr+PDhysvLc62je/fueuedd7Rq1Sr5+fmpefPm6t69u+bNm6dDhw6dUO+2JPn6+mrgwIEaPXq0li5dqs8///yo80ZGRmrAgAEaMGCAnn76abVr107PPfeca0g6AAAAAHgDgbuClLy6dlVgs9n02GOPacSIEbrxxhsVGBioX375RVdccYXromdOp1ObNm1SixYtXMsVn8c9adIkV7ju3r27nnnmGR06dEj/+te/TriGW265Rc8995yuu+461apV64SW8ff3V+PGjcvlPHYAAAAAOB0MKcdRDRgwQD4+Pnr55ZclSU2bNtXs2bO1ePFirV+/XnfccYf27NnjtkytWrXUunVrTZs2zXVxtK5du2r58uXatGnTCfdwS9KZZ56p/fv3l7pFWLFZs2bp5ptv1qxZs7Rp0yZt3LhRzz33nL799ltdccUVrvn++usvrVy5UsnJycrOztbKlSu1cuVKt555AAAAAChv9HDjqHx9fXXvvfdqwoQJuuuuu/Sf//xHW7ZsUZ8+fRQUFKTbb79d/fv3V2pqqtty3bp108qVK12Bu3bt2mrRooX27NmjZs2anVQNkZGRR53WokULBQUF6V//+pd27twpu92upk2b6q233tLAgQNd8912222aP3++63W7du0kSVu3blViYuJJ1QMAAAAAJ8oy3E/ppKSlpSk8PFypqakKCwtzm5aTk6OtW7eqYcOGCggI8FKFOFWOfIeyVmVJkoLaBMnHz+e018k+AQAAAFQdx8p7p4Ih5QAAAAAAeACBGwAAAAAADyBwAwAAAADgAQRuAAAAAAA8gMANAAAAAIAHELgBAAAAAPAAAjcAAAAAAB5A4AYAAAAAwAMI3AAAAAAAeACBGwAAAAAADyBwQ5I0ZMgQWZZV6vHXX3+Vy/qnTp2qiIiIclmXN2zfvl2BgYHKyMjwdikAAAAAqghfbxeAyuPiiy/WlClT3Nqio6O9VM3R5efny8/Pr0Lf88svv1SPHj0UEhJSoe8LAAAAoOqihxsudrtdcXFxbg8fHx9JhYGzffv2CggIUKNGjTR27FgVFBS4lp04caJatWql4OBgJSQk6O6773b1Bs+bN09Dhw5Vamqqq+d8zJgxkiTLsvTFF1+41REREaGpU6dKkrZt2ybLsvTxxx+rW7duCggI0LRp0yRJb731ls4880wFBASoefPmeuWVV465fd27d9d9992n4cOHq1atWoqNjdWbb76pzMxMDR06VBG1I9Tmyjb68ZcfSy375Zdf6vLLL3fVfOQjMTHxZD9uAAAAANUcgbuCODIdFfooTwsXLtSgQYP0wAMPaN26dXr99dc1depUPfXUU655bDabXnzxRa1du1bvvvuufv75Zz3yyCOSpM6dO+uFF15QWFiYkpKSlJSUpIceeuikahg5cqQeeOABrV+/Xn369NG0adP0xBNP6KmnntL69ev19NNP6/HHH9e77757zPW8++67ioqK0m+//ab77rtPd911lwYMGKDOnTtr2dJl6nluT90++nZlZWW5lklJSdGiRYtcgbt4G5KSkvTXX3+pSZMm6tq160ltDwAAAIDqjyHlFWRhyMIKfb/upvtJLzNr1iy3IdN9+/bVjBkzNHbsWI0cOVKDBw+WJDVq1EhPPvmkHnnkEY0ePVqSNHz4cNdyiYmJ+r//+z/deeedeuWVV+Tv76/w8HBZlqW4uLhT2p7hw4frqquucr0ePXq0nn/+eVdbw4YNXQcDiussS5s2bfSf//xHkjRq1Cg988wzioqK0rBhw+TId2jkbSP19qdv68/Vf+r8C86XJH377bdq3bq14uPjJcm1DcYYXX311QoPD9frr79+StsFAAAAoPoicMOlR48eevXVV12vg4ODJUmrVq3SL7/84taj7XA4lJOTo6ysLAUFBemnn37SuHHjtGHDBqWlpamgoMBt+unq2LGj63lmZqb+/vtv3XrrrRo2bJirvaCgQOHh4cdcT+vWrV3PfXx8FBkZqVatWrnaYiJjJEn79u5ztZUcTl7SY489piVLluj3339XYGDgyW8UAAAAgGqNwF1BumR08XYJxxUcHKwmTZqUas/IyNDYsWPdepiLBQQEaNu2bbr00kt111136amnnlLt2rW1aNEi3XrrrcrLyztm4LYsS8YYt7b8/PwyaytZjyS9+eabOvfcc93mKz7n/GiOvNiaZVlubZZlSZKcTqckKS8vT99//70ee+wxt+U++OADTZo0SfPmzVPdunWP+Z4AAAAAaiYCdwXxCT52EKzM2rdvr40bN5YZxiXpjz/+kNPp1PPPPy+brfCyAJ988onbPP7+/nI4Sp9bHh0draSkJNfrzZs3u50/XZbY2FjFx8dry5Ytuummm052c07KvHnzVKtWLbVp08bVtmTJEt122216/fXXdd5553n0/QEAAABUXQRuHNcTTzyhSy+9VPXr19c111wjm82mVatWac2aNfq///s/NWnSRPn5+Zo8ebIuu+wy/fLLL3rttdfc1pGYmKiMjAzNmTNHbdq0UVBQkIKCgtSzZ0+99NJL6tSpkxwOhx599NETuuXX2LFjdf/99ys8PFwXX3yxcnNz9fvvv+vQoUMaMWJEuW37V1995TacPDk5WVdeeaWuv/569enTR8nJyZIKe9Yr4y3UAAAAAHgPVyk/jtzcXKWlpbk9apo+ffpo1qxZ+vHHH3X22WfrvPPO06RJk9SgQQNJhRcimzhxosaPH6+WLVtq2rRpGjdunNs6OnfurDvvvFPXXXedoqOjNWHCBEnS888/r4SEBHXp0kU33nijHnrooRM65/u2227TW2+9pSlTpqhVq1bq1q2bpk6dqoYNG5brth8ZuDds2KA9e/bo3XffVZ06dVyPs88+u1zfFwAAAEDVZ5kjT6CFmzFjxmjs2LGl2lNTUxUWFubWlpOTo61bt6phw4YKCAioqBJRThz5DmWtKhzOHtQmSKtWr1LPnj21b9++E+p1Lwv7BAAAAFB1pKWlKTw8vMy8dyro4T6OUaNGKTU11fXYuXOnt0tCBSkoKNDkyZNPOWwDAAAAqNk4h/s47Ha77Ha7t8uAF5xzzjk655xzvF0GAAAAgCqKHm4AAAAAADyAwA0AAAAAgAcQuAEAAAAA8AACtwc4nU5vl4BKgn0BAAAAqLm4aFo58vf3l81m0+7duxUdHS1/f39ZluXtsnCCHPkO5SlPkmTLscnH4XPK6zLGKC8vT/v27ZPNZpO/v395lQkAAACgiiBwlyObzaaGDRsqKSlJu3fv9nY5OElOh1N5+wsDt3+Av2w+pz8AJCgoSPXr15fNxmASAAAAoKYhcJczf39/1a9fXwUFBXI4HN4uBych40CG1l26TpLU4pcWCokMOa31+fj4yNfXl1EOAAAAQA1F4PYAy7Lk5+cnPz8/b5eCk5Dnlyfn9sJzrv39/BUQEODligAAAABUZYxzBQAAAADAAwjcAAAAAAB4AIEbAAAAAAAPIHADAAAAAOABBG4AAAAAADyAwA0AAAAAgAcQuAEAAAAA8AACNwAAAAAAHkDgBgAAAADAAwjcAAAAAAB4AIEbAAAAAAAPIHADAAAAAOABBG4AAAAAADyAwA0AAAAAgAcQuAEAAAAA8AACNwAAAAAAHkDgBgAAAADAAwjcAAAAAAB4AIEbAAAAAAAPIHADAAAAAOABBG4AAAAAADyAwA0AAAAAgAcQuAEAAAAA8AACNwAAAAAAHkDgBgAAAADAAwjcAAAAAAB4AIEbAAAAAAAPIHADAAAAAOABBG4AAAAAADyAwA0AAAAAgAf4eruAyi43N1e5ubmu12lpaV6sBgAAAABQVdDDfRzjxo1TeHi465GQkODtkgAAAAAAVQCB+zhGjRql1NRU12Pnzp3eLgkAAAAAUAUwpPw47Ha77Ha7t8sAAAAAAFQx9HADAAAAAOABBG4AAAAAADyAwA0AAAAAgAcQuAEAAAAA8AACNwAAAAAAHkDgBgAAAADAAwjcAAAAAAB4AIEbAAAAAAAPIHADAAAAAOABBG4AAAAAADyAwA0AAAAAgAcQuAEAAAAA8AACNwAAAAAAHkDgBgAAAADAAwjcAAAAAAB4AIEbAAAAAAAPIHADAAAAAOABBG4AAAAAADyAwA0AAAAAgAcQuAEAAAAA8AACNwAAAAAAHkDgBgAAAADAAwjcAAAAAAB4AIEbAAAAAAAPIHADAAAAAOABBG4AAAAAADyAwA0AAAAAgAcQuAEAAAAA8AACNwAAAAAAHkDgBgAAAADAAwjcAAAAAAB4AIEbAAAAAAAPIHADAAAAAOAB1T5wDx48WAsWLPB2GQAAAACAGqbaB+7U1FT16tVLTZs21dNPP61du3Z5uyQAAAAAQA1Q7QP3F198oV27dumuu+7Sxx9/rMTERPXt21czZ85Ufn6+t8sDAAAAAFRT1T5wS1J0dLRGjBihVatWaenSpWrSpIkGDhyo+Ph4Pfjgg9q8ebO3SwQAAAAAVDM1InAXS0pK0uzZszV79mz5+Pjokksu0erVq9WiRQtNmjTJ2+UBAAAAAKqRah+48/Pz9emnn+rSSy9VgwYNNGPGDA0fPly7d+/Wu+++q59++kmffPKJ/vvf/3q7VAAAAABANeLr7QI8rU6dOnI6nbrhhhv022+/qW3btqXm6dGjhyIiIspcPjc3V7m5ua7XaWlpHqoUAAAAAFCdVPvAPWnSJA0YMEABAQFHnSciIkJbt24tc9q4ceM0duxYT5UHAAAAAKimqv2Q8rlz55Z5NfLMzEzdcsstx11+1KhRSk1NdT127tzpiTIBAAAAANVMtQ/c7777rrKzs0u1Z2dn67333jvu8na7XWFhYW4PAAAAAACOp9oOKU9LS5MxRsYYpaenuw0pdzgc+vbbbxUTE+PFCgEAAAAA1Vm1DdwRERGyLEuWZemMM84oNd2yLM7NBgAAAAB4TLUN3HPnzpUxRj179tSnn36q2rVru6b5+/urQYMGio+P92KFAAAAAIDqrNoG7m7dukmStm7dqvr168uyLC9XBAAAAACoSapl4P7zzz/VsmVL2Ww2paamavXq1Uedt3Xr1hVYGQAAAACgpqiWgbtt27ZKTk5WTEyM2rZtK8uyZIwpNZ9lWXI4HF6oEAAAAABQ3VXLwL1161ZFR0e7ngMAAAAAUNGqZeBu0KBBmc8BAAAAAKgoNm8X4GnvvvuuvvnmG9frRx55RBEREercubO2b9/uxcoAAAAAANVZtQ/cTz/9tAIDAyVJS5Ys0UsvvaQJEyYoKipKDz74oJerAwAAAABUV9VySHlJO3fuVJMmTSRJX3zxha655hrdfvvtOv/889W9e3fvFgcAAAAAqLaqfQ93SEiIDhw4IEn68ccfddFFF0mSAgIClJ2d7c3SAAAAAADVWLXv4b7ooot02223qV27dtq0aZMuueQSSdLatWuVmJjo3eIAAAAAANVWte/hfvnll9WpUyft27dPn376qSIjIyVJf/zxh2644QYvVwcAAAAAqK4sY4zxdhFVSVpamsLDw5WamqqwsDBvl4NylLY3Tctjl0uS2u9pr7AYfr8AAABATVLeea/aDymXpJSUFP3222/au3evnE6nq92yLA0cONCLlQEAAAAAqqtqH7i//vpr3XTTTcrIyFBYWJgsy3JNI3ADAAAAADyl2p/D/a9//Uu33HKLMjIylJKSokOHDrkeBw8e9HZ5AAAAAIBqqtoH7l27dun+++9XUFCQt0sBAAAAANQg1T5w9+nTR7///ru3ywAAAAAA1DDV/hzufv366eGHH9a6devUqlUr+fn5uU2//PLLvVQZAAAAAKA6q/aBe9iwYZKk//73v6WmWZYlh8NR0SUBAAAAAGqAah+4S94GDAAAAACAilLtz+EuKScnx9slAAAAAABqiGofuB0Oh5588knVrVtXISEh2rJliyTp8ccf19tvv+3l6gAAAAAA1VW1D9xPPfWUpk6dqgkTJsjf39/V3rJlS7311lterAwAAAAAUJ1V+8D93nvv6Y033tBNN90kHx8fV3ubNm20YcMGL1YGAAAAAKjOqn3g3rVrl5o0aVKq3el0Kj8/3wsVAQAAAABqgmofuFu0aKGFCxeWap85c6batWvnhYoAAAAAADVBtb8t2BNPPKHBgwdr165dcjqd+uyzz7Rx40a99957mjVrlrfLAwAAAABUU9W+h/uKK67Q119/rZ9++knBwcF64okntH79en399de66KKLvF0eAAAAAKCaqvY93JLUpUsXzZ4929tlAAAAAABqkGrfw92oUSMdOHCgVHtKSooaNWrkhYoAAAAAADVBtQ/c27Ztk8PhKNWem5urXbt2eaEiAAAAAEBNUG2HlH/11Veu5z/88IPCw8Ndrx0Oh+bMmaPExEQvVAYAAAAAqAmqbeDu37+/JMmyLA0ePNhtmp+fnxIT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efbZZ11tKSkpxm63mw8//NAYY8y6deuMJLNs2TLXPN99952xLMvs2rXLc8UDp6hfv37mlltucWu76qqrzE033WSMYb9H9STJfP75567X5bWfv/LKK6ZWrVpuf+c8+uijplmzZh7eIqD80cMNVIB77rlH/fr1U69evdza//jjD+Xn57u1N2/eXPXr19eSJUskSUuWLFGrVq0UGxvrmqdPnz5KS0vT2rVrK2YDgJPw1VdfqWPHjhowYIBiYmLUrl07vfnmm67pW7duVXJystt+Hx4ernPPPddtv4+IiFDHjh1d8/Tq1Us2m01Lly6tuI0BTlDnzp01Z84cbdq0SZK0atUqLVq0SH379pXEfo+aobz28yVLlqhr167y9/d3zdOnTx9t3LhRhw4dqqCtAcqHr7cLAKq7jz76SMuXL9eyZctKTUtOTpa/v78iIiLc2mNjY5WcnOyap2TYLp5ePA2obLZs2aJXX31VI0aM0GOPPaZly5bp/vvvl7+/vwYPHuzab8var0vu9zExMW7TfX19Vbt2bfZ7VEojR45UWlqamjdvLh8fHzkcDj311FO66aabJIn9HjVCee3nycnJatiwYal1FE8reYoSUNkRuAEP2rlzpx544AHNnj1bAQEB3i4HqBBOp1MdO3bU008/LUlq166d1qxZo9dee02DBw/2cnWAZ3zyySeaNm2apk+frrPOOksrV67U8OHDFR8fz34PADUYQ8oBD/rjjz+0d+9etW/fXr6+vvL19dX8+fP14osvytfXV7GxscrLy1NKSorbcnv27FFcXJwkKS4urtRVy4tfF88DVCZ16tRRixYt3NrOPPNM7dixQ9Lh/bas/brkfr9371636QUFBTp48CD7PSqlhx9+WCNHjtT111+vVq1aaeDAgXrwwQc1btw4Sez3qBnKaz/nbx9UJwRuwIMuvPBCrV69WitXrnQ9OnbsqJtuusn13M/PT3PmzHEts3HjRu3YsUOdOnWSJHXq1EmrV692+8dp9uzZCgsLKxVqgMrg/PPP18aNG93aNm3apAYNGkiSGjZsqLi4OLf9Pi0tTUuXLnXb71NSUvTHH3+45vn555/ldDp17rnnVsBWACcnKytLNpv7n1U+Pj5yOp2S2O9RM5TXft6pUyctWLBA+fn5rnlmz56tZs2aMZwcVY+3r9oG1DQlr1JujDF33nmnqV+/vvn555/N77//bjp16mQ6derkml5QUGBatmxpevfubVauXGm+//57Ex0dbUaNGuWF6oHj++2334yvr6956qmnzObNm820adNMUFCQ+eCDD1zzPPPMMyYiIsJ8+eWX5s8//zRXXHGFadiwocnOznbNc/HFF5t27dqZpUuXmkWLFpmmTZuaG264wRubBBzX4MGDTd26dc2sWbPM1q1bzWeffWaioqLMI4884pqH/R7VQXp6ulmxYoVZsWKFkWQmTpxoVqxYYbZv326MKZ/9PCUlxcTGxpqBAweaNWvWmI8++sgEBQWZ119/vcK3FzhdBG6ggh0ZuLOzs83dd99tatWqZYKCgsyVV15pkpKS3JbZtm2b6du3rwkMDDRRUVHmX//6l8nPz6/gyoET9/XXX5uWLVsau91umjdvbt544w236U6n0zz++OMmNjbW2O12c+GFF5qNGze6zXPgwAFzww03mJCQEBMWFmaGDh1q0tPTK3IzgBOWlpZmHnjgAVO/fn0TEBBgGjVqZP7973+73daI/R7Vwdy5c42kUo/BgwcbY8pvP1+1apW54IILjN1uN3Xr1jXPPPNMRW0iUK4sY4zxZg87AAAAAADVEedwAwAAAADgAQRuAAAAAAA8gMANAAAAAIAHELgBAAAAAPAAAjcAAAAAAB5A4AYAAAAAwAMI3AAAAAAAeACBGwAAAAAADyBwAwAAAADgAQRuAABwwrZv367AwEBlZGR4uxQAACo9AjcAADhhX375pXr06KGQkBBvlwIAQKVH4AYAoAbq3r277rvvPg0fPly1atVSbGys3nzzTWVmZmro0KEKDQ1VkyZN9N1337kt9+WXX+ryyy+XJFmWVeqRmJjoha0BAKByInADAFBDvfvuu4qKitJvv/2m++67T3fddZcGDBigzp07a/ny5erdu7cGDhyorKwsSVJKSooWLVrkCtxJSUmux19//aUmTZqoa9eu3twkAAAqFcsYY7xdBAAAqFjdu3eXw+HQwoULJUkOh0Ph4eG66qqr9N5770mSkpOTVadOHS1ZskTnnXeepk+frkmTJmnZsmVu6zLG6Oqrr9aOHTu0cOFCBQYGVvj2AABQGfl6uwAAAOAdrVu3dj338fFRZGSkWrVq5WqLjY2VJO3du1eS+3Dykh577DEtWbJEv//+O2EbAIASGFIOAEAN5efn5/basiy3NsuyJElOp1N5eXn6/vvvSwXuDz74QJMmTdLnn3+uunXrer5oAACqEAI3AAA4rnnz5qlWrVpq06aNq23JkiW67bbb9Prrr+u8887zYnUAAFRODCkHAADH9dVXX7n1bicnJ+vKK6/U9ddfrz59+ig5OVlS4dD06Ohob5UJAEClQg83AAA4riMD94YNG7Rnzx69++67qlOnjutx9tlne7FKAAAqF65SDgAAjmn58uXq2bOn9u3bV+q8bwAAcHT0cAMAgGMqKCjQ5MmTCdsAAJwkergBAAAAAPAAergBAAAAAPAAAjcAAAAAAB5A4AYAAAAAwAMI3AAAAAAAeACBGwAAAAAADyBwAwAAAADgAQRuAAAAAAA8gMANAAAAAIAHELgBAAAAAPAAAjcAAAAAAB5A4AYAAAAAwAMI3AAAAAAAeACBGwAAAAAADyBwAwAAAADgAQRulDJv3jxZlqV58+Z5u5TTsm3bNlmWpalTp1bYe15yySUaNmxYhb1fdeeJ32FZ+/eQIUOUmJhYbu9RzLIsjRkzptzXW53l5+crISFBr7zyirdLAQAAOG0E7lM0depUWZYly7K0aNGiUtONMUpISJBlWbr00kvdpmVkZGj06NFq2bKlgoODFRkZqbZt2+qBBx7Q7t27XfPNmTNHt9xyi8444wwFBQWpUaNGuu2225SUlHRCNQ4ZMkSWZSksLEzZ2dmlpm/evNm1Dc8999xJfgLeUxyYih9+fn5q1KiRBg0apC1btpTLeyxevFhjxoxRSkrKCS/zyy+/6Mcff9Sjjz561FqPfHz00UcVUltl9PXXX6tbt26KiYlx7d/XXnutvv/+e2+X5jEV+btzOp2aMGGCGjZsqICAALVu3VoffvjhCS3bvXv3o+6zfn5+bvPm5ORo3LhxatGihYKCglS3bl0NGDBAa9eudZtvwYIFuvzyy5WQkKCAgADFxcXp4osv1i+//OI2n5+fn0aMGKGnnnpKOTk5p/chAAAAeJmvtwuo6gICAjR9+nRdcMEFbu3z58/XP//8I7vd7taen5+vrl27asOGDRo8eLDuu+8+ZWRkaO3atZo+fbquvPJKxcfHS5IeffRRHTx4UAMGDFDTpk21ZcsWvfTSS5o1a5ZWrlypuLi449bn6+urrKwsff3117r22mvdpk2bNk0BAQGl/qjt2rWrsrOz5e/vfyofSYW5//77dfbZZys/P1/Lly/XG2+8oW+++UarV692fYanavHixRo7dqyGDBmiiIiIE1rm2Wef1YUXXqgmTZoctdYjderUqUJqq2yee+45Pfzww+rWrZtGjRqloKAg/fXXX/rpp5/00Ucf6eKLL5YkNWjQQNnZ2aVC3umoyP07Oztbvr6Hv2Yr8nf373//W88884yGDRums88+W19++aVuvPFGWZal66+//rjL3nbbbW5tmZmZuvPOO9W7d2+39ptuuklfffWVhg0bpvbt22v37t16+eWX1alTJ61evVoNGjSQJG3atEk2m0133nmn4uLidOjQIX3wwQfq2rWrvvnmG9fvXJKGDh2qkSNHavr06brlllvK6RMBAADwAoNTMmXKFCPJXHXVVSYqKsrk5+e7TR82bJjp0KGDadCggenXr5+r/ZNPPjGSzLRp00qtMzs726Smprpez58/3zgcDrd55s+fbySZf//738etcfDgwSY4ONj07t3b9O/fv9T0pk2bmquvvtpIMs8+++xx13ekzMzMMtvz8/NNbm7uSa+vpIyMjKNOmzt3rpFkZsyY4db+4osvGknm6aefNsYYs3XrViPJTJky5aTf/9lnnzWSzNatW09o/j179hhfX1/z1ltvnVCtp+NkanM4HCY7O7vc3rs85Ofnm7CwMHPRRReVOX3Pnj0VXFHh/ysNGjQol3Ud6zM/2f3qVP3zzz/Gz8/P3HPPPa42p9NpunTpYurVq2cKCgpOep3vv/9+qe+uf/75x0gyDz30kNu8P//8s5FkJk6ceMx1ZmZmmtjYWNOnT59S0y699FLTpUuXk64TAACgMmFI+Wm64YYbdODAAc2ePdvVlpeXp5kzZ+rGG28sNf/ff/8tSTr//PNLTQsICFBYWJjrddeuXWWzuf+Kunbtqtq1a2v9+vUnXOONN96o7777zm0Y67Jly7R58+YyayzrHNfu3burZcuW+uOPP9S1a1cFBQXpsccec51j+9xzz+mFF15Q48aNZbfbtW7dOknSzz//rC5duig4OFgRERG64oorStU+ZswYWZaldevW6cYbb1StWrVKjRg4ET179pQkbd269ZjzHa+mMWPG6OGHH5YkNWzY0DWUdtu2bUdd5zfffKOCggL16tXrpOsuZlmW7r33Xn3xxRdq2bKl7Ha7zjrrLLch1serrXgd06ZN01lnnSW73e5afsWKFerbt6/CwsIUEhKiCy+8UL/++qtbDcWnSixYsEB33HGHIiMjFRYWpkGDBunQoUOu+QYPHqyoqCjl5+eX2o7evXurWbNmR93O/fv3Ky0trcz/ByQpJibG9bysc7iHDBmikJAQ7dixQ5deeqlCQkJUt25dvfzyy5Kk1atXq2fPngoODlaDBg00ffp0t/Wf6DUKnnvuOXXu3FmRkZEKDAxUhw4dNHPmzFLzHeszL3kO97F+d926dVObNm3KrKNZs2bq06ePpMLvj+LvkGP58ssvlZ+fr7vvvtutzrvuukv//POPlixZctx1HGn69OkKDg7WFVdc4WpLT0+XJMXGxrrNW6dOHUlSYGDgMdcZFBSk6OjoMofYX3TRRVq0aJEOHjx40rUCAABUFgTu05SYmKhOnTq5nRv53XffKTU1tcxhm8XDK9977z0ZY076/TIyMpSRkaGoqKgTXuaqq66SZVn67LPPXG3Tp09X8+bN1b59+xNez4EDB9S3b1+1bdtWL7zwgnr06OGaNmXKFE2ePFm33367nn/+edWuXVs//fST+vTpo71792rMmDEaMWKEFi9erPPPP7/M8DpgwABlZWXp6aefPqULjxUHkcjIyKPOcyI1XXXVVbrhhhskSZMmTdL777+v999/X9HR0Udd7+LFixUZGen6/R4pPT1d+/fvL/U4ch9YtGiR7r77bl1//fWaMGGCcnJydPXVV+vAgQMnXNvPP/+sBx98UNddd53+97//KTExUWvXrlWXLl20atUqPfLII3r88ce1detWde/eXUuXLi1V77333qv169drzJgxGjRokKZNm6b+/fu76h04cKAOHDigH374wW255ORk/fzzz7r55puP+lnFxMQoMDBQX3/99SmHKYfDob59+yohIUETJkxQYmKi7r33Xk2dOlUXX3yxOnbsqPHjxys0NFSDBg067kGYsvzvf/9Tu3bt9N///ldPP/20fH19NWDAAH3zzTel5i3rMz/SsX53AwcO1J9//qk1a9a4LbNs2TJt2rTJ9XleeOGFuvDCC49b+4oVKxQcHKwzzzzTrf2cc85xTT8Z+/bt0+zZs9W/f38FBwe72hs3bqx69erp+eef19dff61//vlHv/32m+688041bNiwzO/AtLQ07d+/Xxs2bNBjjz2mNWvWFxXl7wAAZrVJREFUlLlNHTp0kDFGixcvPqlaAQAAKhXvdrBXXcVDypctW2ZeeuklExoaarKysowxxgwYMMD06NHDGGNKDSnPysoyzZo1M5JMgwYNzJAhQ8zbb799wsNon3zySSPJzJkz57jzFg8pN8aYa665xlx44YXGmMIhr3FxcWbs2LGuYdclh5QXD4OeO3euq61bt25Gknnttdfc3qN4+bCwMLN37163aW3btjUxMTHmwIEDrrZVq1YZm81mBg0a5GobPXq0kWRuuOGGE/oMiut75513zL59+8zu3bvNN998YxITE41lWWbZsmVutZUcUn6iNZ3s0N8LLrjAdOjQ4ai1Hu2RlJTkmleS8ff3N3/99ZdbbZLM5MmTT6g2ScZms5m1a9e6tffv39/4+/ubv//+29W2e/duExoaarp27epqK96vO3ToYPLy8lztEyZMMJLMl19+aYwp3Ifq1atnrrvuOrf3mThxorEsy2zZsuWYn9cTTzxhJJng4GDTt29f89RTT5k//vij1Hxl/Q4HDx7sduqAMcYcOnTIBAYGGsuyzEcffeRq37Bhg5FkRo8e7Wora/8ua0h58f/PxfLy8kzLli1Nz5493dqP9pkXTyv53kf73aWkpJiAgADz6KOPurXff//9Jjg42HWKRYMGDU5o6Hu/fv1Mo0aNSrVnZmYaSWbkyJHHXUdJkydPNpLMt99+W2ra0qVLTePGjd326w4dOrjt2yX16dPHNZ+/v7+54447yhyCv3v3biPJjB8//qRqBQAAqEzo4S4H1157rbKzszVr1iylp6dr1qxZZQ7Vlv6/vTuPj+ls/zj+nZDElgXNhpSUlloisSdVS+3UvrS09pbuamnRKsXT0hXddCMoSrWKbnZaKtSWJ9WidkqQFoktCZnz+yNP5mckYoaZzCT5vF+vecmcc8851zlzmeSa+z73yRhiuWXLFsvQ0lmzZmngwIEKCQnRs88+q9TU1Bvu55dfftH48ePVo0cPy/BpW/Xq1Uvr16+39ECePHnyhjHeiLe3t/r375/tuq5du1r1siYkJCguLk79+vVTqVKlLMvDw8PVokUL/fjjj1m28cQTT9gVz4ABAxQQEKAyZcqoXbt2unjxombPnq06depk2/5WYrLVv//+q5IlS95w/dixY7Vq1aosj2vjkKTmzZurYsWKVrH5+vraNft648aNVbVqVcvz9PR0rVy5Up06ddJdd91lWR4SEqJevXpp48aN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7dqpcOHCWr16tQYNGpRl/YYNG7K9Ljk8PFzh4eF2HYO9sUnSf/7zH61atUoNGzbUU089pcKFC+uTTz5Ramqq3nzzzSzt09LS1KxZM/Xo0UN79+7VRx99pIYNG2YZOhwQEKDWrVtr0aJF8vf3V7t27W4a/6VLlxQdHa0GDRqodevWCg0N1blz57RkyRJt2LBBnTp1sirUXKFdu3Z699131bp1a/Xq1UunT5/Whx9+qEqVKik+Pv6Wt3uz9y4yMlLVq1fXokWLdO+992a5bV/m7bNuNnFauXLl9Pzzz+utt97SlStXVLduXcv5nTdvnlWv+ejRozV79mwdOnQoywiGzEtcshtOLknt27dXtWrVNGHCBB05ckQNGjTQ/v379cEHHygkJEQDBw60tG3Tpo3KlSun+vXrKzAwUEePHlVMTIxOnDihhQsXZtn2qlWrdN999+V4mz8AAAB3R8Gdy7p27arz589r5cqVWrt2rc6cOaOSJUuqXr16Gj58uNW9rePi4iRJM2fO1MyZM622U758ebsL7tzWvHlzLV++XOPGjdPYsWPl6empxo0b64033nDIJFPOjKlu3bqaOHGiPv74Yy1fvlxms1mHDh26YVEbFBSktm3b6quvvsq24H7vvfeyfd24cePsLrjtjU2SqlWrpg0bNmj06NGaNGmSzGaz6tevr7lz56p+/fpZ2n/wwQeaN2+exo4dqytXrqhnz5567733sh0S3adPH33//ffq0aNHtjNjX8/f31+fffaZfvjhB8XExOjkyZMqVKiQKleurLfeekvPPfecXefDGR544AHNmDFDkydP1vPPP6+wsDC98cYbOnz48G0V3La8d3369NGLL754S5OlXWvy5MkqWbKkPvnkE82aNUt333235s6da/MlL2azWQsWLFCtWrVuOOLAy8tLGzZs0MSJE/XDDz/oyy+/lI+Pjzp16qTXX3/d6vrrAQMGaMGCBZoyZYrOnTunkiVLqkGDBpo/f77uv/9+q+0mJSVp5cqVVj30AAAAeZHJsHc2IQDZ2rBhg5o0aaI9e/bc9vB/V5k1a5b69++vrVu33vD2atdbunSpOnXqpF9++SVL4QT7TZs2TUOHDtXhw4eznbW+IJg6darefPNNHThwINcmAQQAAHAGruEGHOT+++9Xy5Ytsx2inZ999tlnuuuuu9SwYUNXh5LnGYahGTNm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x7Ru3TpJUr9+/TR16tTcDew669evl8lk0pAhQ6yW9+3bVyaTyXIMv//+ux544AHVrFlT1atXV926dbVr1y5J0nvvvafq1asrPDxctWrV0ty5c3PcZ/fu3RUbGytJevXVV/X8889nabNs2TINHTr09g/QycaOHat58+Y5fT9t27bV3r17s13XrVs3zZo1S5L0wQcf6PXXX7/hdpo0aaKwsDBNmDBBUsb93P39/S3rK1SooMqVK6tmzZqqVKmSOnbsqE2bNtkc5+7du9WuXTtVrFhRFStWVJs2bfTHH39Y1kdERFg9atSoIZPJpAULFmjZsmVZ1pctW9bqvsX79+9X9+7dFRYWpsjISNWsWVMvvPCCUlNTJUn9+/fXe++9J0maNWuW/Pz8FBkZqXvvvVc1a9bU+PHjdfnyZaWkpKhUqVKWHM50+vRpFS9eXEeOHFHx4sWVlpZmWVepUiX169fP8nzz5s268847Jd04j9evX6+iRYtaHdOyZct07tw5lS9f3vL/QMp475o2bSrDMCyvi4yMVLVq1VStWjUNGzZMZ8+etbRv0qSJlixZkmWf18fyxRdfqHz58lmONbuYZ82apU6dOllij4iIsFp/o3zJPLbHHnvMsu2AgACr4z5x4oQOHz6sQoUKWS3/+OOPJWXcuzosLEzNmjXLckybNm1S48aNdffdd+uuu+5Sz549lZCQoJMnT6pMmTJWx3bw4EGFhITo0KFDOnbsmDp06KAaNWqoRo0aioiI0Nq1a622vW7dOplMJn3xxRdZ9nutJk2aqHTp0kpKSrIsu/b/3sKFC1W1alWr85PbCrtszwAAAG7i888/t/s1V69eVeHCtv8pZW/7u+++W999953eeusteXl5KTk5Wb/++qvKli1radOzZ09NnDhRnTt3liQdO3ZM3t7ekqRq1arp119/la+vrw4dOqR69eopOjpaFStWzLKv3377TWfOnFFUVFSOMXXo0EEdOnSw+Rhc4erVq5bC1dl+/PFHm9oNGjRI9957r55++mn5+fll22bKlCmWoio7CxcutBRaixcvVtu2bbVixQrVr18/x32fOHFCjRs31tSpU9WrVy9J0pdffqkmTZooLi5OZcuWzfIl1IgRI3THHXeoW7duKly4sNV7fu7cOdWtW9dyjhMSEtSwYUO99tprWrRokSTp4sWLevfdd3X+/HlLPl6radOmlqL09OnTeuyxx/TQQw9p2bJleuSRRxQTE6N33nnH0n7OnDlq2bKlypcvr6CgIP32229q2LChjh07Jh8fH23evNnSdt26dWratGmO50SSKleunO2Xb5988on69eunuLg4/f3335o4caI2b94sk8lked3OnTslSefPn9ewYcPUrFkzbd26VYUKFbrpfqWML8M++OADrV+/XmFhYTa9xl7X5su1HnnkkSxfKB4+fFg+Pj7Zno81a9bI399f8fHxOnTokCXe+Ph4dejQQQsXLrQU42+88YaaNGminTt3aurUqerbt6+2bNmiQoUKqX///powYYLCwsL04IMPqlmzZlq2bJkk6Z9//tGlS5es9jtjxgw1a9ZMM2bMUO/evXM8Vl9fX02ePFmTJk3Ksu6hhx5S/fr1sz0XuYUebgAAkHtSUpzzuE3X90rFx8crOjpa99xzj/r27avLly9Lyuj9HjBggBo1aqTq1atLyvgDtk6dOgoPD1e7du108uRJSf/f6zRy5EjVqlVLb7/9toKDg3Xs2DHLfl566SWNHDky25iKFSumZs2aaenSpZKkBQsWqGvXrlZF+99//21VgIeGhiowMFCS1KxZM0txFxoammXf1/rkk08sxVhOru1pk6Rx48apUqVKqlu3rsaMGaMKFSpYHfu4ceNUu3ZtVapUyao4XbFihWrVqqXw8HA1btxYf/75p2VdTEyMIiIiVLNmTdWpU0eHDx+2vKZhw4aqXbu26tWrZxmRsH79elWrVk0DBw5URESEvv32W6tRCmlpaXrhhRdUvXp11axZU61bt8722EaMGKG6desqIiJCjRo1suq5jo2NVcOGDVWzZk2Fh4db3pNrR0zs2bNH0dHRqlatmjp16qTk5GTL6728vNSyZUvNnz//pufYFl26dNETTzyht99++6ZtP/roIzVp0sTq/e3Zs6eaNm2qDz74IEv7hQsX6quvvtJXX32V5Qsis9msRx55RM2aNdPAgQMlSR9++KGaNGlieS5JxYsX1yuvvKI77rjjpvEFBgZq9uzZWr16tf744w8NHDhQc+fO1ZUrVyxtYmJiLNtv2rSp1q9fLynjvW/VqpUCAwMtebJ+/XqbCu4bad26tRo3bqwRI0aob9++liIxOz4+Pvroo4/0zz//aPny5TZtf/z48Zo5c6Z++eUXpxXbjjRjxgw9/vjj6tWrl2bOnGlZ/uabb2rAgAFWPd8jR46Un5+fFixYoB49euiee+7R66+/rvfee0/FixfX448/Linr59Ydd9xhGZUgZXyp88MPP2ju3Ln6888/tX///hxjHDlypGbMmKETJ0446rAdih5uAACQe7p3d852v/vOoZvbsmWLNm/erGLFiqlTp06aMmWKXnrpJUnS9u3btXHjRvn4+EiSpk6dqoCAAEnS5MmT9eqrr1qGYyYlJalatWp64403JGX0iE2fPl2vv/66UlNTFRMTY9U7d73+/ftr4sSJ6t69u2JiYjRr1iwtXLjQsv6VV15R06ZN1aBBAzVo0EDdunVTZGRklu2sWbNGZ8+eVd26dbPdz/r16+0eKv7DDz/om2++0c6dO1WiRAkNGDDAan1SUpLCw8M1fvx4LV++XEOGDFHbtm11+vRp9erVS+vXr1eNGjU0b948devWTX/88Yd+/vlnTZgwQZs2bVJISIil1+vgwYN69dVXtWLFCvn6+mr//v26//77LUXW7t279dFHH2nGjBmW2DJNmjRJf/31l7Zv3y5vb28lJiZmezwjR460FLALFizQkCFDtHz5cp05c0adOnXS119/rfvvv19ms1nnzp3L8vrevXvriSee0MCBA/X777+rTp06VkVuVFSUli1bpieffNKu83wj9evXt/QQbtu2TWPHjs22x33Hjh1q0aJFluVRUVFauXKl1bLff/9dTz31lJYvX27J6WuNGzdOZ86c0bfffnvT7dujZMmSuvvuu/XHH3+oR48eKleunH744Qd16tRJmzdv1rlz59SmTRtJGQV3TEyMxowZo3Xr1qlHjx7y9PTUunXr9Oijj+rXX3/VZ599dtN97t2716rXc/v27ZYe6nfeeUd33XWXatSoocGDB+e4HU9PT0VGRuqPP/5Qu3btcmw7d+5cBQQEKDY29raGOF8f+7VD7DM99NBDKlq0qKSM9y1zFMy8efMsX1hERkYqJiZGUsZn07Xb/O6771S8eHEtX75c06dP19GjR9WuXTuNHz9eHh4e2rFjh7p27Zplv1FRUdq+fbsGDBigDz/8ULVq1VJ6erp+++03S5uRI0dq4MCBmjZtmho0aKCOHTuqUaNGlvXz589Xq1atFBwcrEcffVQzZ87M8ZKM4OBgDR48WOPGjbPpvc9t9HADAABcp0ePHvLx8VGhQoU0cOBArV692rKue/fulmJbyvjjsE6dOqpevbo+//xzq2GZnp6eevTRRy3Pn3rqKc2ePVupqalatGiR6tWrp/Lly98wjujoaB09elQrVqxQoUKFVLlyZav1w4cP18GDB/XYY4/pzJkzuv/++60KcimjiHr88ce1YMECFS9ePNv9/P333woKCrLp3GRas2aN5VyYTCarHk5JKlKkiLp06SIp44/wAwcOSMr4MiPz2k0pY4TAiRMndPz4cf3www/q3bu3QkJCJGX08hcrVkzLly/X/v371ahRI0VERKhbt27y8PDQ0aNHJUl33XWXGjdunG2c33//vYYMGWIZ2pxdISlJq1atUlRUlKpXr64JEyZY3sfY2FhVrlxZ999/vyTJw8NDpUqVsnptcnKy4uLiLNcS16hRQw0bNrRqExwcrL///jvnk2oHwzAsP9epU8fm4e3XyizIJOns2bPq3Lmz3nrrrWy/mFm6dKlmzJihb775Rl5eXjfc5pQpUxQREaE777zT5l5fyfp4Bg4caOlNnTlzpvr27Wsphps2barY2FilpaVp48aNatiwoRo3bqz169dr69atCgoKsuotvZHMIeWZj2uHg2/YsEHe3t46ePCg1UgFW2LPSZ06dZScnKzvv//+hm0yh67ntPz62LN77xcuXGhZn1lsSxn/3zKXZxbbkixDyjMfoaGhmjdvntq0aSN/f3+Fh4crKChIK1assOlYJalUqVLq3bu3OnXqZPk/LWWMsDh69KiGDx8uSerYsaPeeusty/oZM2ZYvsAbMGCAZs+erfT09Bz39cILL+j777/Xnj17bI4vt9DDDQAAcs//rvHMa679Y7dEiRKWnzdu3Kj33ntPsbGxCgwM1LJlyzR27FjL+mLFisnD4//7N8qWLatGjRpp4cKFmj59uk3XGvfp00ePPvqoJk+enO36oKAg9ezZUz179lT58uU1b948PfTQQ5KkP//8U+3bt9enn36apQC8VrFixZRym0Pzry8UvL29LcsKFSp00z+Yc2IYhlq0aJHtkOzjx49bvSe34ujRo3rmmWe0detWVaxYUfHx8VY9brfi+vORkpJiVeDerq1bt1oua8hJrVq1FBsbm2UEQ2xsrKKjoyVlDBXv1auXWrZsmWWkgpTRozpw4EAtWbJEZcqUsVoXGRlp1Xs5dOhQDR06VE2aNLE5p86ePav9+/dbjqdXr14a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", + "image/png": "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IiIiIrICJtxEREREREREVsBB04gkwuETiIiIiIgqNtZwExEREREREVkBE24iIiIiIiIiK2DCTURERERERGQFTLiJiIiIiIiIrIAJdykcPXoUvXv3RvXq1SGTybBr165ib2P//v1o27YtXF1d4ePjgwEDBuDatWsWj5WIiIiIiIjKFhPuUkhLS0OzZs2wYsWKEq0fFxeHvn37omvXroiNjcX+/fvx4MED9O/f38KREhERERERUVmTCc5NZBEymQw7d+5Ev379jGVqtRrvvvsuvvjiCyQmJqJx48ZYsGABOnfuDADYsWMHhg4dCrVaDblcf+3j22+/Rd++faFWq2Fvby/BkRAREREREZElsIbbiiZMmICYmBh8+eWXOHPmDAYNGoSnn34aly9fBgC0bNkScrkc69evh1arRVJSEjZt2oTw8HAm20RERERERDaONdwWkruG+8aNG6hduzZu3LiB6tWrG5cLDw9H69atMW/ePADATz/9hOeeew4PHz6EVqtFWFgY9u3bBw8PDwmOgoiIiIiIiCyFNdxWcvbsWWi1WtStWxcuLi7G208//YQrV64AAOLj4zFmzBiMGDECf/zxB3766Sc4ODhg4MCB4HUQIiIiIiIi22YndQAVVWpqKhQKBU6ePAmFQmHynIuLCwBgxYoVcHd3x8KFC43Pbd68Gf7+/vj999/Rtm3bMo2ZiIiIiIiILIcJt5U0b94cWq0W9+7dQ8eOHc0uk56ebhwszcCQnOt0OqvHSERERERERNbDJuWlkJqaitjYWMTGxgLQT/MVGxuLGzduoG7dunjhhRcwfPhwfPPNN4iLi8Px48cxf/587N27FwDQs2dP/PHHH5gzZw4uX76MU6dOITIyEgEBAWjevLmER0ZERERERESlxUHTSuHIkSPo0qVLnvIRI0Zgw4YNyMrKwvvvv4/PP/8ct2/fhre3N9q2bYvZs2ejSZMmAIAvv/wSCxcuxKVLl+Dk5ISwsDAsWLAA9evXL+vDISIiIiIiIgtiwk1ERERERERkBWxSTkRERERERGQFTLiJiIiIiIiIrIAJdzEJIZCcnMx5somIiIiIiCoYS+d7nBasmJKTk+Hh4YGbN2/Czc1N6nCIiIiIiIjIQpKTk+Hv74/ExES4u7uXentMuIspJSUFAODv7y9xJERERERERGQNKSkpTLil4OrqCgCs4SYiIiIi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UtKd85b4pUft2rXN/aR2mP/Vq1fVt29fBQcHy8XFRf7+/qpfv77V8OkNGzaodevWKlCggFxcXBQYGKg333xTN27csNpW586d5enpqZMnT+q5556Tp6en8uXLp6+++kqStGfPHtWpU0ceHh4KCgpKMgojsQ2sX79eL7/8snx9feXl5aVOnTopIiLigccSGxurQYMGqUiRImad7733nmJjY1N1LhLNnz9fAQEBatmypTnNz89Pbdq00aJFi6y2FxgYKAcHfmUBAADgycFfr48oODhYVatW1f/+9z9z2m+//aaoqCi1bds2yfJBQUGSpO+//94qmNrCpk2b5Ovra+7zXr1799b+/fs1cOBA9e/fX5K0detWbdq0SW3bttW4cePUq1cvrVq1SrVq1dL169clST4+PipdurTWr19vbmvjxo2yWCy6cuWK9u/fb07fsGGDatSoYbXfkiVLys3NTX/88Ue6HesXX3yh/Pnzq3jx4vrhhx/0ww8/6IMPPpAkXb9+XWFhYZo5c6Y6deqkcePG6ZlnntGAAQP01ltvmdtYsWKF2rVrpxw5cmjkyJEaMWKEatWqZdZZs2ZNvfHGG5Kk999/39xPiRIlUlVjr169NGnSJL3wwguaOHGi3nnnHbm5uenAgQPmMvPmzdP169f1yiuvaPz48WrYsKHGjx+vTp06JdlefHy8GjVqpMDAQI0aNUrBwcF67bXXNH36dD377LOqWLGiRo4cqezZs6tTp046duxYkm289tprOnDggAYPHqxOnTpp1qxZatGiRYptMyEhQc2aNdOYMWPUtGlTjR8/Xi1atNDYsWP14osvpupcJNqxY4fKly+fJEhXrlxZ169ft7rEAgAAAHjiGHgo06ZNMyQZW7duNSZMmGBkz57duH79umEYhtG6dWujdu3ahmEYRlBQkNGkSRNzvevXrxvFihUzJBlBQUFG586djSlTphjnz59PcX/z5s0zJBlr1qxJdY3Vq1c3KlSocN/aq1evbty+fdtqXuIx3G3z5s2GJOP77783p7366qtGQECA+f6tt94yatasafj7+xuTJk0yDMMwLl++bFgsFuPLL79Mss2nnnrKaNSoUaqPJTVKlSplhIWFJZk+dOhQw8PDw/jnn3+spvfv399wdHQ0Tp48aRiGYfTp08fw8vJKck7u9jCfQyJvb2/j1VdfTXGZ5M7/8OHD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kMTLAe79OchoCxYskKurq5YvX66uXbuqUaNGj3QH+kQpfXaOjo5q37695s+fr4iICC1cuFDt2rW775cIKQkJCdH169eTDM9P/CIjJCQk1duaPXu2ChcurCpVqiSZd/78eUl3hr/fzTAMxcfHJztU/F43btyQYRhWlyZI//e0gXtv/AYAAPAkIXCnE09PT02aNEmDBw9W06ZN77vcrl27kr0m8cSJE9q/f7/VsN9r166pcePGOn36tH799dckw3zTqnbt2ho6dKgmTJig3LlzP9K2HiRPnjwKCQnRjBkzrB75s3fvXv3+++9q3LixTff/qDUlBu/kHleUnKpVqyoiIuKhrhVOq7TW5ujoqAYNGmjRokVWQ8LPnz+v2bNnq3r16laPo5Okb775xuqRX5MmTdLt27fVqFEjq+XatWsni8WiPn366OjRoynenTzR9evX7/tYqt9++01S8sP+M5Kjo6MsFotVkDx+/LgWLlz4SNt90GfXsWNHRURE6OWXX1ZMTEyS85nax4I1b95cTk5OmjhxojnNMAx9/fXXypcvn6pVq2ZOP3v2rA4ePJjsI9527NihAwcO3PdLxKeeekqSNGfOHKvpixcv1rVr1xQaGmpOu3DhQpL1IyMjtWDBAgUGBlo9Dk6Stm3bJovFoqpVqz7weAEAAB5X9huDmwmFh4c/cJkVK1Zo0KBBatasmapUqSJPT08dPXpUU6dOVWxsrPnMXunOEM6//vpLXbt21YEDB6x6qzw9PdWiRYs01efg4KAPP/wwTes8itGjR6tRo0aqWrWqunXrphs3bmj8+PHy9va2Os6MlNqaKlSoIEn64IMP1LZtWzk5Oalp06b37ZVv0qSJsmXLppUrV6pnz55J5m/YsCHZ65LLli2rsmXLpukY0lqbJH3yySdasWKFqlevrt69eytbtmyaPHmyYmNjNWrUqCTL37p1S3Xr1lWbNm106NAhTZw4UdWrV08ydNjPz0/PPvus5s2bJx8fHzVp0uSB9V+/fl3VqlVTlSpV9OyzzyowMFCRkZFauHChNmzYoBYtWlgFNXto0qSJPv/8cz377LNq3769Lly4oK+++kpFihTR7t27H3q7D/rsQkNDVbp0ac2bN08lSpRI8ti+xMdnPejGafnz51ffvn01evRoxcXFqVKlSub5nTVrllWv+YABAzRjxgwdO3YsyQiGxEtc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aG3ZskVVq1aVJFWtWlWRkZHatm2buczq1auVkJCgp59+OsNrBgAAGeunn35ShQoVFBISouLFi6tOnToZdn358ePH5ePjk+b1Bg8eLIvFop9//tmcZhiGChYsaLW9lI6te/fuKlasmMqVK6dnnnlGW7dufdTDAQCkwhPVwx0TE6MjR46Y748dO6adO3cqZ86cKlCggPr27atPPvlERYsWVcGCBfXRRx8pb968atGihSSpRIkSevbZZ9WjRw99/fXXiouL02uvvaa2bdtyh3IAADK5s2fPqmfPntq2bZuCgoIkSdu3b38ietwqVKigqVOnmjd4XbVqlXLlyqWIiAhJDz625s2b67vvvpOTk5OWLFmi1q1b6/jx43Y5FgDISp6oHu6///5boaGhCg0NlSS99dZbCg0N1cCBAyVJ7733nl5//XX17NlTlSpVUkxMjJYtW2Y1Dn/WrFkqXry46tatq8aNG6t69er65ptv7HI8AAAg45w/f16Ojo7KmTOnOa18+fJmKH3nnXdUqVIlhYSEqGbNmjp06JC5nMVi0aeffqqnn35awcHBWrhwoYYPH66KFSuqaNGiWrt2raT/68V+5513VLZsWZUqVUorV65Mtp6tW7eqTp06qlixokJDQzVv3rz71l69enX9+++/5k1ep06dqq5du6b62Jo2baps2e70s1SpUkWnT5/W7du303L6AAAP4Ynq4a5Vq5YMw7jvfIvFoo8//lgff/zxfZfJmTOnZs+ebYvyAADAA9jqAR+pucdN2bJlVb16dQUFBSksLEzVqlVT+/btlS9fPklSv379NGbMGEnSnDlz1KdPHy1btsxc39PTU1u2bNGqVavUvHlzTZgwQX///bfmzZund9991xymHRUVpRIlSmjMmDH6888/1axZM/37779WtURGRqpnz5769ddflSdPHl26dEnly5dXtWrVzHru9dJLL2nGjBl6+eWXtXXrVn3yyScaMGBAqo7tbl9++aUaN25sBnAAgO3wmxYAAGSY1q1ts91ffnnwMg4ODlqwYIEOHjyodevW6bffftOnn36qv//+W0WKFNGKFSs0fvx4Xb161Xyk2d1efPFFSVLFihV17do1tW3bVpJUuXJlHT582FwuW7Zs6ty5s6Q7vcl58+bVjh07VKBAAXOZTZs26ejRo2rUqJHVPg4dOnTfwB0eHq769evL09NT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ShEREapUqVKy+1m7dm2ah4ovXbpUCxYs0I4dO+Tp6amuXbtazY+KilLZsmU1ZMgQLVu2TH369FHjxo114cIFtW/fXmvXrlWZMmU0a9YstWrVSvv27dO6dev08ccfa9OmTcqTJ4/Z63X06FENHjxYy5cvl5eXl44cOaIaNWqYIevAgQOaOHGipkyZYtaWaPjw4frnn3+0bds2ubi46OLFi8keT79+/cwAO2fOHPXp00fLli3TlStX1KJFC82fP181atRQQkKCIiMjk6zfsWNH9erVS926ddOePXtUsWJFq5BbtWpVLV68WK+88kqazvP9PP3002YP4d9//62BAwcm2+O+fft21a9fP8n0qlWr6vfff7eatmfPHvXu3VvLli0z2/TdBg0apCtXrujnn39+4PbTIkeOHCpatKj27dunNm3aKH/+/Fq6dKlatGihP//8U5GRkWrUqJGkO4F72rRp+vDDD7VmzRq1adNGTk5OWrNmjV566SX98ccf+vbbbx+4z0OHDln1em7bts3sof7ss89UqFAhlSlTRi+//HKK23FyclJoaKj27dunJk2apLjszJkz5efnp82bNz/SEOd7a797iH2iF198UW5ubpLufG6Jo2BmzZplfmERGhqqadOmSbrzu+nubf7yyy/y8PDQsmXLNGnSJJ08eVJNmjTRkCFD5ODgoO3bt+uFF15Ist+qVatq27Zt6tq1q7766iuVL19e8fHx+uuvv8xl+vXrp27duunLL79UlSpV1Lx5c9WsWdOcP3v2bDVs2FC5c+fWSy+9pKlTp6Z4SUbu3Ln18ssva9CgQan67DMaPdwAAAD3aNOmjbJnzy5HR0d169ZNK1euNOe1bt3aDNvSnT8OK1asqNKlS+u7776zGpbp5OSkl156yXzfu3dvzZgxQ7GxsZo3b54qV66soKCg+9ZRrVo1nTx5UsuXL5ejo6OKFStmNf/tt9/W0aNH1b17d125ckU1atSwCuTSnRDVo0cPzZkzRx4eHsnu57///lNAQECqzk2iVatWmefCYrFY9XBKkqurq1q2bCnpzh/h//77r6Q7X2YkXrsp3RkhcObMGZ0+fVpLly5Vx44dlSdPHkl3evnd3d21bNkyHTlyRDVr1lRISIhatWolBwcHnTx5UpJUqFAhhYWFJVvnkiVL1KdPH3Noc3JBUpJWrFihqlWrqnTp0vr4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81O4UUb9+vX17LPPasOGDVq/fr2uu+469ezZU19++aWkU4G+/vrr1a1bN3322Wdat26dHnzwQXk8p+KUnp6unJycUre7775bqampatu27Xmff+bMmbIsy9F9RGhzK4Pn2y4qFrdyOHDgQG3dulUffvih/vOf/6h3797q16+fPv/883LZb4SOi82gJN188806efKkPv30U23YsEEtW7bUzTffrNzc3LM+73PPPaff//73euWVV7R27VpVq1ZN3bt3V2FhoeP7jNDjRg6PHTumrKwsPf7448rKytLChQu1detW9ejRo1z2GaHFrffC095//32tWbNGdevWvbAdMIAfatasaV577TVjjDHt2rUzEyZM8Pt3i4uLjdfrNZMnTz7vup9//rmpV6+eycnJMZLM+++/f6EjI8KURwYD3S4qnvLIYbVq1cz8+fNLLatVq5b505/+FPjAiDiBZDAvL89IMv/61798ywoKCowkk5GRccbfsW3bJCcnm+eff9637NChQyYmJsb89a9/DdJeINw5ncMz+eyzz4wks2vXrgsfHBGjvDK4Z88eU69ePbNp0ybToEEDM2PGjIBn5bANzqmkpERvvvmmjh49qrS0NB04cEBr165VYmKi0tPTlZSUpI4dO2rVqlVn3caHH36o77//XkOGDDnncx07dkz9+/fXSy+9pOTk5GDvCsJUeWXwQraLiqM83wvT09P11ltv6YcffpBt23rzzTdVWFioTp06BXmvEE4uJIO1a9dWkyZNNH/+fB09elQnT57Uq6++qsTERLVp0+aMz7Nz507l5uaqa9euvmUJCQlq166dMjMzHd9PhLbyyuGZ5Ofny7Is1ahRw4E9Q7gozwzatq0BAwZozJgxuuKKKy586IArOiqEjRs3mmrVqplKlSqZhIQE849//MMYY0xmZqaRZGrVqmVmz55tsrKyzIgRI0x0dLT5+uuvz7itG264wdxwww3nfc57773X3HXXXb774gh3hVbeGbyQ7SLyufFeePDgQdOtWzcjyVSuXNnEx8ebTz75JKj7hfBxsRn87rvvTJs2bYxlWaZSpUqmTp06Jisr66zP9+9//9tIMvv27Su1/NZbbzX9+vVzZicR8so7h//r+PHj5qqrrjL9+/cP+r4hPLiRwWeeecb88pe/NLZtG2PMBR/hpnDjjIqKisy2bdvM+vXrzbhx48wll1xivvzyS9//EI8fP77U+s2bNzfjxo0rs53vvvvOeDwe8+67757z+RYtWmQaN25sDh8+7FtG4a7YyjuDgW4XFUN559AYYx588EFz9dVXmyVLlpjs7Gzz5JNPmoSEBLNx48ag7RfCx8Vk0LZt06NHD3PDDTeYVatWmQ0bNpgHHnjA1KtXr0yhPo3CjTMp7xz+VHFxsfnVr35lWrdubfLz8x3ZP4S+8s7g+vXrTVJSktm7d69vGYUbjurSpYu59957zTfffGMkmT//+c+lHu/Xr98Z/+o4efJk4/V6TXFx8Tm3P3z4cN9fnE7fJBmPx2M6duwYzF1BmHI6g4FuFxWT0zncvn27kWQ2bdpU5nnvu+++i98BhL1AMrhkyRLj8XjKlJTGjRubKVOmnHH7O3bsMJLM559/Xmp5hw4dzEMPPRS8HUFYczqHpxUXF5tbbrnFtGjRwvz3v/8N7k4grDmdwRkzZpy1mzRo0CCgWfkMN/xi27aKiorUsGFD1a1bV1u3bi31+Ndff60GDRqUWmaM0Zw5czRw4EBFRUWdc/vjxo3Txo0blZ2d7btJ0owZMzRnzpyg7gvCk9MZDGS7qLiczuGxY8ckqcyV8StVqiTbtoOwBwh3gWTwbHnyeDxnzVNqaqqSk5O1dOlS37KCggKtXbtWaWlpwdwVhDGncyid+mqwfv36adu2bVqyZIlq164d5L1AOHM6gwMGDCjTTerWrasxY8bok08+CWzYgOo5KoRx48aZFStWmJ07d5qNGzeacePGGcuyzOLFi40xp/7iEx8fb9555x2zbds2M2HCBBMbG2u2b99eajtLliwxkszmzZvLPMeePXtMkyZNzNq1a886hzilvMJyK4P+bhcVgxs5LC4uNo0bNzbXXnutWbt2rdm+fbt54YUXjGVZvs+roeK42Azm5eWZ2rVrm969e5vs7GyzdetWM3r0aBMVFWWys7N9z9OkSROzcOFC3/1nn33W1KhRwyxatMhs3LjR9OzZ06Smpprjx4+X7wuAkOBGDouLi02PHj1M/fr1TXZ2tsnJyfHdioqKyv9FgKvcei/8X5xSjqC58847TYMGDUx0dLTxer2mS5cuvkCfNmXKFFO/fn1TtWpVk5aWZlauXFlmO7fddptJT08/43Ps3LnTSDLLli076xwU7orLzQz6s11UDG7l8Ouvvza9e/c2iYmJpmrVqqZFixZlviYMFUMwMrhu3TrTrVs3U6tWLRMXF2euueYa889//rPUOpLMnDlzfPdt2zaPP/64SUpKMjExMaZLly5m69atju0nQpsbOTz93nim27n+vyMik1vvhf/rQgu39ePGAQAAAABAEPEZbgAAAAAAHEDhBgAAAADAARRuAAAAAAAcQOEGAAAAAMABFG4AAAAAABxA4QYAAAAAwAEUbgAAAAAAHEDhBgAAAADAARRuAAAAAAAcQOEGAAAAAMABFG4AAOC3Xbt2qUqVKjpy5IjbowAAEPIo3AAAwG+LFi1S586dVb16dbdHAQAg5FG4AQCogDp16qRhw4ZpxIgRqlmzppKSkvSnP/1JR48e1ZAhQxQXF6fGjRvro48+KvV7ixYtUo8ePSRJlmWVuTVs2NCFvQEAIDRRuAEAqKDmzZunSy65RJ999pmGDRumBx54QLfeeqvS09OVlZWlbt26acCAATp27Jgk6dChQ1q1apWvcOfk5Phu27dvV+PGjdWhQwc3dwkAgJBiGWOM20MAAIDy1alTJ5WUlGjlypWSpJKSEiUkJKh3796aP3++JCk3N1d16tRRZmamrrnmGi1YsEAzZszQunXrSm3LGKM+ffpo9+7dWrlypapUqVLu+wMAQCiq7PYAAADAHS1atPD9XKlSJdWuXVvNmzf3LUtKSpIkHThwQFLp08l/6tFHH1VmZqbWr19P2QYA4Cc4pRwAgAoqKiqq1H3LskotsyxLkmTbtoqLi/Xxxx+XKdx/+ctfNGPGDL3//vuqV6+e80MDABBGKNwAAOC8li9frpo1a6ply5a+ZZmZmbr77rv16quv6pprrnFxOgAAQhOnlAMAgPP68MMPSx3dzs3NVa9evfSb3/xG3bt3V25urqRTp6Z7vV63xgQAIKRwhBsAAJzX/xbuLVu2aP/+/Zo3b57q1Knju/385z93cUoAAEILVykHAADnlJWVpeuuu055eXllPvcNAADOjiPcAADgnE6ePKkXX3yRsg0AQIA4wg0AAAAAgAM4wg0AAAAAgAMo3AAAAAAAOIDCDQAAAACAAyjcAAAAAAA4gMINAAAAAIADKNwAAAAAADiAwg0AAAAAgAMo3AAAAAAAOIDCDQAAAACAA/4fjGwpw0ZXA68AAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -5643,7 +5643,7 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 27, "id": "7dc0e769", "metadata": {}, "outputs": [ @@ -5710,10 +5710,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 28, "id": "3fb1fb6f", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ Cleaned up tutorial data from tutorial_collection_data\n" + ] + } + ], "source": [ "# Clean up tutorial data\n", "if processed_folder.exists():\n", @@ -5724,7 +5732,7 @@ ], "metadata": { "kernelspec": { - "display_name": "venv", + "display_name": "venv (3.10.18)", "language": "python", "name": "python3" }, From 78cc8290476679f6efc8ca3f395c4b0fbdc51c67 Mon Sep 17 00:00:00 2001 From: Katherine Heal Date: Thu, 30 Apr 2026 11:44:20 -0700 Subject: [PATCH 157/158] Refactor alignment of mass features for more robustness --- corems/mass_spectra/calc/lc_calc.py | 208 ++++++++++++---------------- 1 file changed, 87 insertions(+), 121 deletions(-) diff --git a/corems/mass_spectra/calc/lc_calc.py b/corems/mass_spectra/calc/lc_calc.py index 9b85a1605..a81dfc7ba 100644 --- a/corems/mass_spectra/calc/lc_calc.py +++ b/corems/mass_spectra/calc/lc_calc.py @@ -3088,10 +3088,44 @@ def align_lcms_objects(self, overwrite=False): full_mf_df = self.mass_features_dataframe # re-index to sample_name for faster lookups full_mf_df = full_mf_df.reset_index().set_index("sample_name") + samples_with_features = set(full_mf_df.index.get_level_values("sample_name")) if "scan_time_aligned" in full_mf_df.columns and not overwrite: raise ValueError("Mass features have already been aligned") + def _set_scan_time_alignment_for_sample(sample_idx, use_alignment, spline): + """Set scan_time_aligned for one sample using spline or identity mapping.""" + if use_alignment and spline is not None: + self[sample_idx]._scan_info["scan_time_aligned"] = { + k: spline(v) for k, v in self[sample_idx]._scan_info["scan_time"].items() + } + return True + + self[sample_idx]._scan_info["scan_time_aligned"] = self[sample_idx]._scan_info[ + "scan_time" + ].copy() + return False + + def _get_feature_df_at_or_after(start_idx, index_step, use_alignment, spline): + """Return next sample index/dataframe with features, aligning empty samples on the way.""" + i = start_idx + while 0 <= i < len(self): + sample_name = self.samples[i] + if sample_name in samples_with_features: + mf_df_i = full_mf_df.loc[sample_name].copy() + mf_df_i["scan_time_og"] = mf_df_i["scan_time"] + mf_df_i = mf_df_i.reset_index(drop=False) + if use_alignment and spline is not None: + # Use previous step transform as a better matching starting point. + mf_df_i["scan_time"] = spline(mf_df_i["scan_time"]) + return i, mf_df_i + + _set_scan_time_alignment_for_sample(i, use_alignment, spline) + self.rt_alignment_attempted = True + i += index_step + + return i, None + anchor_mf_dfs = [] for center_obj_id in center_obj_ids: # Get the anchor mass features from the center LCMS object @@ -3113,134 +3147,66 @@ def align_lcms_objects(self, overwrite=False): # Initialize spline for propagation to samples without features spl = None use_spline_alignment = False - - # Loop through the other LCMS objects in the collection (going forward) - i = center_obj_id + index_step - if i < len(self) and i >= 0: - # Check if this sample has any features in the dataframe - sample_name = self.samples[i] - if sample_name not in full_mf_df.index.get_level_values('sample_name'): - # For samples with no mass features, use the same alignment as the previous sample - if use_spline_alignment and spl is not None: - # Use the spline from the adjacent sample - self[i]._scan_info["scan_time_aligned"] = {k: spl(v) for k, v in self[i]._scan_info["scan_time"].items()} - else: - # No spline available, use original times - self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"].copy() - self.rt_alignment_attempted = True - - # Move to next sample - i += index_step - while i < len(self) and i >= 0: - sample_name = self.samples[i] - if sample_name not in full_mf_df.index.get_level_values('sample_name'): - # Apply same alignment to this empty sample - if use_spline_alignment and spl is not None: - self[i]._scan_info["scan_time_aligned"] = {k: spl(v) for k, v in self[i]._scan_info["scan_time"].items()} - else: - self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"].copy() - i += index_step - else: - # Found a sample with features, exit inner loop to process it - break - - # If we've processed all remaining empty samples, continue to next index_step - if i >= len(self) or i < 0: - continue - - # Grab the first LCMS object after the center object - mf_df_i = full_mf_df.loc[sample_name].copy() - mf_df_i["scan_time_og"] = mf_df_i["scan_time"] - while mf_df_i is not None: - mf_df_i = self.get_anchor_mass_features(mf_df_i) + # Loop through the other LCMS objects in this direction. + i, mf_df_i = _get_feature_df_at_or_after( + center_obj_id + index_step, + index_step, + use_spline_alignment, + spl, + ) - # Match the mass features in the LCMS object to the anchor mass features in the center LCMS object. - matches_c, matches_i = self.match_mfs(mf_df_c, mf_df_i) + while mf_df_i is not None: + mf_df_i = self.get_anchor_mass_features(mf_df_i) - if matches_c is not None: - use_spline_alignment, spl = self.attempt_alignment( - matches_c, matches_i - ) + # Match the mass features in the LCMS object to the anchor mass features in the center LCMS object. + matches_c, matches_i = self.match_mfs(mf_df_c, mf_df_i) - # Record if we used alignment for this sample - sample_name = self.samples[i] - self._manifest_dict[sample_name]["use_rt_alignment"] = ( - use_spline_alignment - ) + if matches_c is not None: + use_spline_alignment, spl = self.attempt_alignment( + matches_c, matches_i + ) - if use_spline_alignment: - # Set new retention times on scan_df for lc_obj using the spline fitting - matches_i["scan_time_fit"] = spl(matches_i["scan_time"]) + # Record if we used alignment for this sample + sample_name = self.samples[i] + self._manifest_dict[sample_name]["use_rt_alignment"] = ( + use_spline_alignment + ) - # Add "scan_time_aligned" to LCMSObject's _scan_info dict - self[i]._scan_info["scan_time_aligned"] = {k: spl(v) for k, v in self[i]._scan_info["scan_time"].items()} + if use_spline_alignment: + # Set new retention times on scan_df for lc_obj using the spline fitting + matches_i["scan_time_fit"] = spl(matches_i["scan_time"]) - # Retrieve the new aligned times for all scans in the LCMS object - new_times = [x for k, x in sorted(self[i]._scan_info["scan_time_aligned"].items())] - - # Switch the rt_aligned flag to True and attempted to True - self.rt_aligned = True - self.rt_alignment_attempted = True - else: - # Set aligned retention times on scan_df for lc_obj using the original retention times - self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"].copy() - # Switch the rt_attempted flag to True - self.rt_aligned = False - self.rt_alignment_attempted = True - - i += index_step - if i >= len(self) or i < 0: - mf_df_i = None - else: - # Check if this sample has any features in the dataframe - sample_name_next = self.samples[i] - if sample_name_next not in full_mf_df.index.get_level_values('sample_name'): - # For samples with no mass features, apply the same alignment transformation - if use_spline_alignment and spl is not None: - self[i]._scan_info["scan_time_aligned"] = {k: spl(v) for k, v in self[i]._scan_info["scan_time"].items()} - else: - self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"].copy() - self.rt_alignment_attempted = True - - # Continue to the next sample - i += index_step - while i < len(self) and i >= 0: - sample_name = self.samples[i] - if sample_name not in full_mf_df.index.get_level_values('sample_name'): - # Apply same alignment to consecutive empty samples - if use_spline_alignment and spl is not None: - self[i]._scan_info["scan_time_aligned"] = {k: spl(v) for k, v in self[i]._scan_info["scan_time"].items()} - else: - self[i]._scan_info["scan_time_aligned"] = self[i]._scan_info["scan_time"].copy() - i += index_step - else: - # Found a sample with features - break - - # Check if we're done - if i >= len(self) or i < 0: - mf_df_i = None - else: - # Grab the next LCMS object with features - mf_df_i = full_mf_df.loc[self.samples[i]].copy() - mf_df_i["scan_time_og"] = mf_df_i["scan_time"] - mf_df_i = mf_df_i.reset_index(drop=False) - if use_spline_alignment: - # Set scan_time to previous sample's predicted scan_time to find closer matches - mf_df_i["scan_time"] = spl(mf_df_i["scan_time"]) - else: - # Grab the next LCMS object and use the previous spline fitting to get a better starting point - mf_df_i = full_mf_df.loc[sample_name_next].copy() - mf_df_i["scan_time_og"] = mf_df_i["scan_time"] - mf_df_i = mf_df_i.reset_index(drop=False) - if use_spline_alignment: - # Set scan_time to previous sample's predicted scan_time to find closer matches - mf_df_i["scan_time"] = spl(mf_df_i["scan_time"]) - else: - raise ValueError( - f"No matches found between the center object and {self.samples[i]}" - ) + self.rt_aligned = _set_scan_time_alignment_for_sample( + i, use_spline_alignment, spl + ) + self.rt_alignment_attempted = True + + i, mf_df_i = _get_feature_df_at_or_after( + i + index_step, + index_step, + use_spline_alignment, + spl, + ) + else: + # If no matches are found, propagate prior alignment from this index step. + sample_name = self.samples[i] + used_previous_alignment = use_spline_alignment and spl is not None + self._manifest_dict[sample_name]["use_rt_alignment"] = ( + used_previous_alignment + ) + + self.rt_aligned = _set_scan_time_alignment_for_sample( + i, used_previous_alignment, spl + ) + self.rt_alignment_attempted = True + + i, mf_df_i = _get_feature_df_at_or_after( + i + index_step, + index_step, + used_previous_alignment, + spl, + ) # Now align each batch using the center objects as anchors with the other batches mf_df_c = anchor_mf_dfs[0] From ffd1d863e4237c8ec11c47e3cb72955a7562704f Mon Sep 17 00:00:00 2001 From: Christian Dewey Date: Fri, 12 Jun 2026 10:52:54 -0400 Subject: [PATCH 158/158] error with how mag lab predator files are read --- corems/mass_spectrum/input/baseClass.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/corems/mass_spectrum/input/baseClass.py b/corems/mass_spectrum/input/baseClass.py index bdabe904c..58825c7ba 100644 --- a/corems/mass_spectrum/input/baseClass.py +++ b/corems/mass_spectrum/input/baseClass.py @@ -254,10 +254,15 @@ def get_dataframe(self) -> DataFrame: ) elif self.data_type == "pks": + # Predator .pks columns are positional: peak location (m/z), relative + # peak height (normalized 0-100), absolute abundance, resolving power, + # frequency, S/N. Use the absolute abundance as the intensity -- the + # relative peak height is per-spectrum normalized and not comparable + # across spectra, so it is named so header_translate drops it. names = [ "m/z", - "I", - "Scaled Peak Height", + "Relative Abundance", + "Abundance", "Resolving Power", "Frequency", "S/N",