From 9b4ad728409627ad137fa55a26da6958a4ce989b Mon Sep 17 00:00:00 2001 From: mjeppesen3 <mjeppesen3@huskers.unl.edu> Date: Fri, 1 Mar 2024 13:20:15 -0600 Subject: [PATCH] inital upload --- .../vimms/Chemicals.py | 628 +++++++ .../vimms/ChineseRestaurantProcess.py | 29 + .../vimms/Chromatograms.py | 132 ++ .../vimms/Common.py | 196 +++ .../vimms/Controller.py | 726 ++++++++ Synthetic data creation scripts/vimms/DIA.py | 349 ++++ .../vimms/DataGenerator.py | 656 +++++++ Synthetic data creation scripts/vimms/DsDA.py | 112 ++ .../vimms/Evaluation.py | 58 + .../vimms/MassSpec.py | 769 +++++++++ .../vimms/MatrixFactorisation.py | 242 +++ .../vimms/MzmlWriter.py | 160 ++ .../vimms/PlotsForPaper.py | 718 ++++++++ Synthetic data creation scripts/vimms/Roi.py | 336 ++++ .../vimms/SpectralUtils.py | 136 ++ .../vimms/TopNExperiment.py | 147 ++ .../vimms/__init__.py | 1 + .../01. Download Data.ipynb | 1174 +++++++++++++ .../02. MS1 Simulations.ipynb | 321 ++++ .../03. Multiple Samples Example.ipynb | 1503 +++++++++++++++++ .../04. Top-N Simulations.ipynb | 791 +++++++++ .../05. Varying N in Top-N Simulations.ipynb | 897 ++++++++++ .../vimms_data_generation/ee.py | 4 + .../vimms_data_generation/intermediate | 106 ++ .../vimms_data_generation/make.py | 762 +++++++++ .../vimms_data_generation/mk.py | 542 ++++++ .../multiple_samples_example.ipynb | 865 ++++++++++ .../vimms_data_generation/prepare-eics | 122 ++ .../vimms_data_generation/validate | 21 + 29 files changed, 12503 insertions(+) create mode 100644 Synthetic data creation scripts/vimms/Chemicals.py create mode 100644 Synthetic data creation scripts/vimms/ChineseRestaurantProcess.py create mode 100644 Synthetic data creation scripts/vimms/Chromatograms.py create mode 100644 Synthetic data creation scripts/vimms/Common.py create mode 100644 Synthetic data creation scripts/vimms/Controller.py create mode 100644 Synthetic data creation scripts/vimms/DIA.py create mode 100644 Synthetic data creation scripts/vimms/DataGenerator.py create mode 100644 Synthetic data creation scripts/vimms/DsDA.py create mode 100644 Synthetic data creation scripts/vimms/Evaluation.py create mode 100644 Synthetic data creation scripts/vimms/MassSpec.py create mode 100644 Synthetic data creation scripts/vimms/MatrixFactorisation.py create mode 100644 Synthetic data creation scripts/vimms/MzmlWriter.py create mode 100644 Synthetic data creation scripts/vimms/PlotsForPaper.py create mode 100644 Synthetic data creation scripts/vimms/Roi.py create mode 100644 Synthetic data creation scripts/vimms/SpectralUtils.py create mode 100644 Synthetic data creation scripts/vimms/TopNExperiment.py create mode 100644 Synthetic data creation scripts/vimms/__init__.py create mode 100644 Synthetic data creation scripts/vimms_data_generation/01. Download Data.ipynb create mode 100644 Synthetic data creation scripts/vimms_data_generation/02. MS1 Simulations.ipynb create mode 100644 Synthetic data creation scripts/vimms_data_generation/03. Multiple Samples Example.ipynb create mode 100644 Synthetic data creation scripts/vimms_data_generation/04. Top-N Simulations.ipynb create mode 100644 Synthetic data creation scripts/vimms_data_generation/05. Varying N in Top-N Simulations.ipynb create mode 100644 Synthetic data creation scripts/vimms_data_generation/ee.py create mode 100644 Synthetic data creation scripts/vimms_data_generation/intermediate create mode 100644 Synthetic data creation scripts/vimms_data_generation/make.py create mode 100644 Synthetic data creation scripts/vimms_data_generation/mk.py create mode 100644 Synthetic data creation scripts/vimms_data_generation/multiple_samples_example.ipynb create mode 100644 Synthetic data creation scripts/vimms_data_generation/prepare-eics create mode 100644 Synthetic data creation scripts/vimms_data_generation/validate diff --git a/Synthetic data creation scripts/vimms/Chemicals.py b/Synthetic data creation scripts/vimms/Chemicals.py new file mode 100644 index 00000000..e7b712c1 --- /dev/null +++ b/Synthetic data creation scripts/vimms/Chemicals.py @@ -0,0 +1,628 @@ +import copy +import glob +import math +import random +import re +from pathlib import Path + +import numpy as np +import scipy +import scipy.stats +import copy + +from vimms.ChineseRestaurantProcess import Restricted_Crp +from vimms.Common import LoggerMixin, CHEM_DATA, POS_TRANSFORMATIONS, load_obj, takeClosest, save_obj +from vimms.Chromatograms import EmpiricalChromatogram + +GET_MS2_BY_PEAKS = "sample" +GET_MS2_BY_SPECTRA = "spectra" + +class DatabaseCompound(object): + def __init__(self, name, chemical_formula, monisotopic_molecular_weight, smiles, inchi, inchikey): + self.name = name + self.chemical_formula = chemical_formula + self.monisotopic_molecular_weight = monisotopic_molecular_weight + self.smiles = smiles + self.inchi = inchi + self.inchikey = inchikey + + +class Formula(object): + def __init__(self, formula_string): + self.formula_string = formula_string + self.atom_names = ['C', 'H', 'N', 'O', 'P', 'S', 'Cl', 'I', 'Br', 'Si', 'F', 'D'] + self.atoms = {} + for atom in self.atom_names: + self.atoms[atom] = self._get_n_element(atom) + self.mass = self._get_mz() + + def _get_mz(self): + return self.compute_exact_mass() + + def _get_n_element(self, atom_name): + # Do some regex matching to find the numbers of the important atoms + ex = atom_name + '(?![a-z])' + '\d*' + m = re.search(ex, self.formula_string) + if m == None: + return 0 + else: + ex = atom_name + '(?![a-z])' + '(\d*)' + m2 = re.findall(ex, self.formula_string) + total = 0 + for a in m2: + if len(a) == 0: + total += 1 + else: + total += int(a) + return total + + def compute_exact_mass(self): + masses = {'C': 12.00000000000, 'H': 1.00782503214, 'O': 15.99491462210, 'N': 14.00307400524, + 'P': 30.97376151200, 'S': 31.97207069000, 'Cl': 34.96885271000, 'I': 126.904468, 'Br': 78.9183376, + 'Si': 27.9769265327, 'F': 18.99840320500, 'D': 2.01410177800} + exact_mass = 0.0 + for a in self.atoms: + exact_mass += masses[a] * self.atoms[a] + return exact_mass + + def __repr__(self): + return self.formula_string + + def __str__(self): + return self.formula_string + + +class Isotopes(object): + def __init__(self, formula): + self.formula = formula + self.C12_proportion = 0.989 + self.mz_diff = 1.0033548378 + # TODO: Add functionality for elements other than Carbon + + def get_isotopes(self, total_proportion): + peaks = [() for i in range(len(self._get_isotope_proportions(total_proportion)))] + for i in range(len(peaks)): + peaks[i] += (self._get_isotope_mz(self._get_isotope_names(i)),) + peaks[i] += (self._get_isotope_proportions(total_proportion)[i],) + peaks[i] += (self._get_isotope_names(i),) + return peaks + + # outputs [(mz_1, intensity_proportion_1, isotope_name_1),...,(mz_n, intensity_proportion_n, isotope_name_n)] + + def _get_isotope_proportions(self, total_proportion): + proportions = [] + while sum(proportions) < total_proportion: + proportions.extend( + [scipy.stats.binom.pmf(len(proportions), self.formula._get_n_element("C"), 1 - self.C12_proportion)]) + normalised_proportions = [proportions[i] / sum(proportions) for i in range(len(proportions))] + return normalised_proportions + + def _get_isotope_names(self, isotope_number): + if isotope_number == 0: + return "Mono" + else: + return str(isotope_number) + "C13" + + def _get_isotope_mz(self, isotope): + if isotope == "Mono": + return self.formula._get_mz() + elif isotope[-3:] == "C13": + return self.formula._get_mz() + float(isotope.split("C13")[0]) * self.mz_diff + else: + return None + + +class Adducts(object): + def __init__(self, formula, adduct_proportion_cutoff=0.05): + self.adduct_names = list(POS_TRANSFORMATIONS.keys()) + self.formula = formula + self.adduct_proportion_cutoff = adduct_proportion_cutoff + + def get_adducts(self): + adducts = [] + proportions = self._get_adduct_proportions() + for j in range(len(self.adduct_names)): + if proportions[j] != 0: + adducts.extend([(self._get_adduct_names()[j], proportions[j])]) + return adducts + + def _get_adduct_proportions(self): + # TODO: replace this with something proper + prior = np.ones(len(self.adduct_names)) * 0.1 + prior[0] = 1.0 # give more weight to the first one, i.e. M+H + proportions = np.random.dirichlet(prior) + while max(proportions) < 0.2: + proportions = np.random.dirichlet(prior) + proportions[np.where(proportions < self.adduct_proportion_cutoff)] = 0 + proportions = proportions / max(proportions) + proportions.tolist() + return proportions + + def _get_adduct_names(self): + return self.adduct_names + + +class Chemical(object): + + def __repr__(self): + raise NotImplementedError() + + +class UnknownChemical(Chemical): + """ + Chemical from an unknown chemical formula + """ + + def __init__(self, mz, rt, max_intensity, chromatogram, children=None): + self.max_intensity = max_intensity + self.isotopes = [(mz, 1, "Mono")] # [(mz, intensity_proportion, isotope,name)] + self.adducts = [("M+H", 1)] + self.rt = rt + self.chromatogram = chromatogram + self.children = children + self.ms_level = 1 + self.mz_diff = 0 + + def __repr__(self): + return 'UnknownChemical mz=%.4f rt=%.2f max_intensity=%.2f' % ( + self.isotopes[0][0], self.rt, self.max_intensity) + + def __eq__(self, other): + if not isinstance(other, UnknownChemical): + return False + return get_key(self) == get_key(other) + + def __hash__(self): + return hash(get_key(self)) + + +class KnownChemical(Chemical): + """ + Chemical from an known chemical formula + """ + + def __init__(self, formula, isotopes, adducts, rt, max_intensity, chromatogram, children=None, + include_adducts_isotopes=True, total_proportion=0.99): + self.formula = formula + self.mz_diff = isotopes.mz_diff + if include_adducts_isotopes == True: + self.isotopes = isotopes.get_isotopes(total_proportion) + self.adducts = adducts.get_adducts() + else: + mz = isotopes.get_isotopes(total_proportion)[0][0] + self.isotopes = [(mz, 1, "Mono")] + self.adducts = [("M+H", 1)] + self.rt = rt + self.max_intensity = max_intensity + self.chromatogram = chromatogram + self.children = children + self.ms_level = 1 + + def __repr__(self): + return 'KnownChemical - %r rt=%.2f max_intensity=%.2f' % ( + self.formula.formula_string, self.rt, self.max_intensity) + + def __eq__(self, other): + if not isinstance(other, KnownChemical): + return False + return self.formula.formula_string == other.formula.formula_string + + def __hash__(self): + return hash(self.formula.formula_string) + + + +class MSN(Chemical): + """ + ms2+ fragments + """ + + def __init__(self, mz, ms_level, prop_ms2_mass, parent_mass_prop, children=None, parent=None): + self.isotopes = [(mz, None, "MSN")] + self.ms_level = ms_level + self.prop_ms2_mass = prop_ms2_mass + self.parent_mass_prop = parent_mass_prop + self.children = children + self.parent = parent + + def __repr__(self): + return 'MSN Fragment mz=%.4f ms_level=%d' % (self.isotopes[0][0], self.ms_level) + + +class ChemicalCreator(LoggerMixin): + def __init__(self, peak_sampler, ROI_sources=None, database=None): + self.peak_sampler = peak_sampler + self.ROI_sources = ROI_sources + self.database = database + + # sort database compounds by their mass + if self.database is not None: + self.logger.debug('Sorting database compounds by masses') + compound_mass_list = [Formula(compound.chemical_formula).mass for compound in self.database] + sort_index = np.argsort(compound_mass_list) + self.compound_mass_list = np.array(compound_mass_list)[sort_index].tolist() + self.compound_list = np.array(self.database)[sort_index].tolist() + + def sample(self, mz_range, rt_range, min_ms1_intensity, n_ms1_peaks, ms_levels, alpha=math.inf, + fixed_mz=False, adduct_proportion_cutoff=0.05, roi_rt_range=None, include_adducts_isotopes=True, + get_children_method=GET_MS2_BY_PEAKS): + self.mz_range = mz_range + self.rt_range = rt_range + self.min_ms1_intensity = min_ms1_intensity + self.n_ms1_peaks = n_ms1_peaks + self.ms_levels = ms_levels + self.alpha = alpha + self.fixed_mz = fixed_mz + self.adduct_proportion_cutoff = adduct_proportion_cutoff + self.include_adducts_isotopes = include_adducts_isotopes + self.get_children_method = get_children_method + + # set up some counters + self.crp_samples = [[] for i in range(self.ms_levels)] + self.crp_index = [[] for i in range(self.ms_levels)] + self.counts = [[] for i in range(self.ms_levels)] + + # Report error if tries to use spectra to generate MS2+ spectra + if get_children_method == GET_MS2_BY_SPECTRA and self.ms_levels > 2: + NotImplementedError("Using spectra to generate MS2+ spectra is not yet implemented") + + # sample from kernel densities + if self.ms_levels > 2: + print("Warning ms_level > 3 not implemented properly yet. Uses scaled ms_level = 2 information for now") + n_ms1 = self._get_n(1) + self.logger.debug("{} chemicals to be created.".format(n_ms1)) + sampled_peaks = self.peak_sampler.get_peak(1, n_ms1, self.mz_range[0][0], self.mz_range[0][1], + self.rt_range[0][0], + self.rt_range[0][1], self.min_ms1_intensity) + # Get formulae from database and check there are enough of them + self.formula_list = self._sample_formulae(sampled_peaks) + + # Get file split information + split = self._get_n_ROI_files() + + # create chemicals + chemicals = [] + # load first ROI file + current_ROI = 0 + ROIs = self._load_ROI_file(current_ROI, roi_rt_range) + ROI_intensities = np.array([r.max_intensity for r in ROIs]) + for i in range(n_ms1): + if i == sum(split[0:(current_ROI + 1)]): + current_ROI += 1 + ROIs = self._load_ROI_file(current_ROI, roi_rt_range) + ROI_intensities = np.array([r.max_intensity for r in ROIs]) + formula = self.formula_list[i] + ROI = ROIs[self._get_ROI_idx(ROI_intensities, sampled_peaks[i].intensity)] + chem = self._get_known_ms1(formula, ROI, sampled_peaks[i], self.include_adducts_isotopes) + if self.fixed_mz: + chem.chromatogram.mzs = [0 for i in range( + len(chem.chromatogram.raw_mzs))] + chem.mzs = [0 for i in range( + len(chem.chromatogram.raw_mzs))] + if ms_levels > 1: + chem.children = self._get_children(self.get_children_method, chem) + chem.type = CHEM_DATA + chemicals.append(chem) + # if i % 100 == 0: + # self.logger.debug("i = {}".format(i)) + return chemicals + + def _get_n_ROI_files(self): + count = 0 + for i in range(len(self.ROI_sources)): + count += len(list(Path(self.ROI_sources[i]).glob('*.p'))) + split = np.array([int(np.floor(self.n_ms1_peaks / count)) for i in range(count)]) + split[0:int(self.n_ms1_peaks - sum(split))] += 1 + return split + + def _load_ROI_file(self, file_index, roi_rt_range=None): + num_ROI = 0 + for i in range(len(self.ROI_sources)): + ROI_files = list(Path(self.ROI_sources[i]).glob('*.p')) + len_ROI = len(ROI_files) + if len_ROI > file_index: + ROI_file = ROI_files[file_index - num_ROI] + ROI = load_obj(ROI_file) + # self.logger.debug("Loaded {}".format(ROI_file)) + if roi_rt_range is not None: + ROI = self._filter_ROI(ROI, roi_rt_range) + return ROI + num_ROI += len_ROI + + def _filter_ROI(self, ROI, roi_rt_range): + lower = roi_rt_range[0] + upper = roi_rt_range[1] + results = [chem for chem in ROI if lower < np.abs(chem.chromatogram.max_rt - chem.chromatogram.min_rt) < upper] + return results + + def _get_ROI_idx(self, ROI_intensities, intensity): + return (np.abs(ROI_intensities - intensity)).argmin() + + def _sample_formulae(self, sampled_peaks): + assert len(sampled_peaks) < len(self.database), 'The number of sampled peaks must be less than ' \ + 'the number of database compounds' + formula_set = set() + for formula_index in range(len(sampled_peaks)): + if formula_index % 500 == 0: + self.logger.debug('Sampling formula %d/%d' % (formula_index, len(sampled_peaks))) + + mz_peak_sample = sampled_peaks[formula_index].mz + idx = np.argsort(abs(self.compound_mass_list - mz_peak_sample)) + + list_index = 0 + compound_found = False + while compound_found is False: + pos = idx[list_index] + new_compound = self.compound_list[pos].chemical_formula + if str(new_compound) not in formula_set: + formula_set.add(str(new_compound)) + compound_found = True + list_index += 1 + return list(formula_set) + + def _get_children(self, get_children_method, parent, n_peaks=None): + if get_children_method == GET_MS2_BY_SPECTRA: + kids = self._get_children_spectra(parent) + return kids + elif get_children_method == GET_MS2_BY_PEAKS: + kids = self._get_children_sample(parent, n_peaks) + return kids + # TODO: add ability to get children through prediction from parent formula + # will need to add a default if MS2+ is requested + else: + raise ValueError("'get_children_method' must be either 'spectra' or 'sample'") + + def _get_children_spectra(self, parent): + # spectra is a list containing one MassSpec.Scan object + + spectra = self.peak_sampler.get_ms2_spectra()[0] + kids = [] + return kids + intensity_props = self._get_msn_proportions(None, None, spectra.intensities) + parent_mass_prop = self.peak_sampler.get_parent_intensity_proportion() + for i in range(len(spectra.mzs)): + kid = MSN(spectra.mzs[i], spectra.ms_level, intensity_props[i], parent_mass_prop, None, parent) + kids.append(kid) + return kids + + def _get_children_sample(self, parent, n_peaks=None): + children_ms_level = parent.ms_level + 1 + if n_peaks is None: + n_peaks = self._get_n(children_ms_level) + kids = [] + parent_mass_prop = self.peak_sampler.get_parent_intensity_proportion() + kids_intensity_proportions = self._get_msn_proportions(children_ms_level, n_peaks) + if self.alpha < math.inf: + # draws from here if using Chinese Restaurant Process (SLOW!!!) + for index_children in range(n_peaks): + next_crp, self.counts[children_ms_level - 1] = Restricted_Crp(self.alpha, + self.counts[children_ms_level - 1], + self.crp_index[children_ms_level - 1], + index_children) + self.crp_index[children_ms_level - 1].append(next_crp) + if next_crp == max(self.crp_index[children_ms_level - 1]): + kid = self._get_unknown_msn(children_ms_level, parent) + kid.prop_ms2_mass = kids_intensity_proportions[index_children] + if children_ms_level < self.ms_levels: + kid.children = self._get_children(self.get_children_method, kid) + self.crp_samples[children_ms_level - 1].append(kid) + else: + kid = copy.deepcopy(self.crp_samples[children_ms_level - 1][next_crp]) + kid.parent_mass_prop = parent_mass_prop + kid.parent = parent + kids.append(kid) + self.crp_samples[children_ms_level - 1].extend(kids) + else: + # Draws from here if children all independent + for index_children in range(n_peaks): + kid = self._get_unknown_msn(children_ms_level, parent) + kid.prop_ms2_mass = kids_intensity_proportions[index_children] + kid.parent_mass_prop = parent_mass_prop + if children_ms_level < self.ms_levels: + kid.children = self._get_children(self.get_children_method, kid) + kids.append(kid) + return kids + + def _get_msn_proportions(self, children_ms_level=None, n_peaks=None, children_intensities=None): + if children_intensities is None: + if children_ms_level == 2: + kids_intensities = self.peak_sampler.get_peak(children_ms_level, n_peaks) + else: + kids_intensities = self.peak_sampler.get_peak(2, n_peaks) + kids_intensities_total = sum([x.intensity for x in kids_intensities]) + kids_intensities_proportion = [x.intensity / kids_intensities_total for x in kids_intensities] + else: + kids_intensities = children_intensities + kids_intensities_total = sum(kids_intensities) + kids_intensities_proportion = kids_intensities / kids_intensities_total + return kids_intensities_proportion + + def _get_n(self, ms_level): + if ms_level == 1: + return int(self.n_ms1_peaks) + elif ms_level == 2: + return int(self.peak_sampler.n_peaks(2, 1)) + else: + return int(math.floor(self.peak_sampler.n_peaks(2, 1) / (5 ** (ms_level - 2)))) + + def _get_known_ms1(self, formula, ROI, sampled_peak, include_adducts_isotopes): # fix this + ## from sampled_peak.rt (XCMS output), we get the point where maximum intensity occurs + ## so when convering ROI to chemicals, we want to adjust the RT to align it with the point where max intensity occurs + rt = sampled_peak.rt + min2mid_rt_ROI = list(ROI.chromatogram.rts[np.where(ROI.chromatogram.intensities == 1)])[0] + adjusted_rt = rt - min2mid_rt_ROI + intensity = sampled_peak.intensity + formula = Formula(formula) + isotopes = Isotopes(formula) + adducts = Adducts(formula, self.adduct_proportion_cutoff) + return KnownChemical(formula, isotopes, adducts, adjusted_rt, intensity, ROI.chromatogram, None, include_adducts_isotopes) + + def _get_unknown_msn(self, ms_level, parent=None): # fix this + if ms_level == 2: + mz = self.peak_sampler.get_peak(ms_level, 1)[0].mz + else: + mz = self.peak_sampler.get_peak(2, 1)[0].mz + return MSN(mz, ms_level, None, None, None, parent) + + def _valid_ms1_chem(self, chem): + if chem.max_intensity < self.min_ms1_intensity: + return False + elif chem.rt < self.rt_range[0][0]: + return False + elif chem.rt > self.rt_range[0][1]: + return False + return True + + +class MultiSampleCreator(LoggerMixin): + + def __init__(self, original_dataset, n_samples, classes, intensity_noise_sd, + change_probabilities, change_differences_means, change_differences_sds, dropout_probabilities=None, + dropout_numbers=None, experimental_classes=None, experimental_probabilitities=None, + experimental_sds=None, save_location=None): + self.original_dataset = original_dataset + self.n_samples = n_samples + self.classes = classes + self.intensity_noise_sd = intensity_noise_sd + self.change_probabilities = change_probabilities + self.change_differences_means = change_differences_means + self.change_differences_sds = change_differences_sds + self.dropout_probabilities = dropout_probabilities + self.dropout_numbers = dropout_numbers + self.experimental_classes = experimental_classes + self.experimental_probabilitities = experimental_probabilitities + self.experimental_sds = experimental_sds + self.save_location = save_location + + self.sample_classes = [] + for index_classes in range(len(self.classes)): + self.sample_classes.extend([self.classes[index_classes] for i in range(n_samples[index_classes])]) + self.chemical_statuses = self._get_chemical_statuses() + self.chemical_differences_from_class1 = self._get_chemical_differences_from_class1() + if self.experimental_classes is not None: + self.sample_experimental_statuses = self._get_experimental_statuses() + self.experimental_effects = self._get_experimental_effects() + self.logger.debug("Classes, Statuses and Differences defined.") + + self.samples = [] + for index_sample in range(sum(self.n_samples)): + self.logger.debug("Dataset {} of {} created.".format(index_sample + 1, sum(self.n_samples))) + new_sample = copy.deepcopy(self.original_dataset) + which_class = np.where(np.array(self.classes) == self.sample_classes[index_sample]) + for index_chemical in range(len(new_sample)): + if not np.array(self.chemical_statuses)[which_class][0][index_chemical] == "missing": + original_intensity = new_sample[index_chemical].max_intensity + intensity = self._get_intensity(original_intensity, which_class, index_chemical) + adjusted_intensity = self._get_experimental_factor_effect(intensity, index_sample, index_chemical) + noisy_adjusted_intensity = self._get_noisy_intensity(adjusted_intensity) + new_sample[index_chemical].max_intensity = noisy_adjusted_intensity.tolist()[0] + chemicals_to_keep = np.where((np.array(self.chemical_statuses)[which_class][0]) != "missing") + new_sample = np.array(new_sample)[chemicals_to_keep].tolist() + if self.save_location is not None: + save_obj(new_sample, Path(self.save_location, 'sample_%d.p' % index_sample)) + self.samples.append(new_sample) + + def _get_chemical_statuses(self): + chemical_statuses = [np.array(["unchanged" for i in range(len(self.original_dataset))])] + chemical_statuses.extend([np.random.choice(["changed", "unchanged"], len(self.original_dataset), + p=[self.change_probabilities[i], 1 - self.change_probabilities[i]]) + for i in range(len(self.classes) - 1)]) + self.missing = self._get_missing_chemicals(chemical_statuses) + self.missing_chemicals = [np.array(self.original_dataset)[miss].tolist() for miss in self.missing] + for index_chemical in range(len(chemical_statuses)): + chemical_statuses[index_chemical][self.missing[index_chemical]] = "missing" + return chemical_statuses + + def _get_missing_chemicals(self, chemical_statuses): + missing = [] + while len(missing) != len(chemical_statuses): + if self.dropout_probabilities is not None: + if self.dropout_numbers is not None: + print("using dropout_probabilties rather than dropout_number.") + new_missing = list(np.where(np.random.binomial(1, self.dropout_probabilities[len(missing)], + len(self.original_dataset)))[0]) + if self.dropout_probabilities is None and self.dropout_numbers is not None: + new_missing = random.sample(range(0, len(self.original_dataset)), self.dropout_numbers) + missing.append(new_missing) + missing = [list(x) for x in set(tuple(sorted(x)) for x in missing)] + return missing + + def _get_experimental_statuses(self): + experimental_statuses = [] + for i in range(len(self.experimental_classes)): + class_allocation = np.random.choice(self.experimental_classes[i], sum(self.n_samples), + p=self.experimental_probabilitities[i]) + experimental_statuses.append(class_allocation) + return experimental_statuses + + def _get_experimental_effects(self): + experimental_effects = [] + for i in range(len(self.experimental_classes)): + coef = [np.random.normal(0, self.experimental_sds[i], len(self.experimental_classes[i])) for j in + range(len(self.original_dataset))] + experimental_effects.append(coef) + return experimental_effects + + def _get_chemical_differences_from_class1(self): + chemical_differences_from_class1 = [np.array([0 for i in range(len(self.original_dataset))]) for j in + range(len(self.classes))] + for index_classes in range(1, len(self.classes)): + coef_mean = self.change_differences_means[index_classes - 1] + coef_sd = self.change_differences_sds[index_classes - 1] + coef_len = sum(self.chemical_statuses[index_classes] == "changed") + coef = np.random.normal(coef_mean, coef_sd, coef_len) + chemical_differences_from_class1[index_classes][ + np.where(self.chemical_statuses[index_classes] == "changed")] = coef + return chemical_differences_from_class1 + + def _get_intensity(self, original_intensity, which_class, index_chemical): + intensity = original_intensity + self.chemical_differences_from_class1[which_class[0][0]][index_chemical] + return intensity + + def _get_experimental_factor_effect(self, intensity, index_sample, index_chemical): + experimental_factor_effect = 0.0 + if self.experimental_classes == None: + return intensity + else: + for index_factor in range(len(self.experimental_classes)): + which_experimental_status = self.sample_experimental_statuses[index_factor][index_sample] + which_experimental_class = np.where( + np.array(self.experimental_classes[index_factor]) == which_experimental_status) + experimental_factor_effect += self.experimental_effects[index_factor][index_chemical][ + which_experimental_class] + return intensity + experimental_factor_effect + + def _get_noisy_intensity(self, adjusted_intensity): + noisy_intensity = adjusted_intensity + np.random.normal(0, self.intensity_noise_sd[0], 1) + if noisy_intensity < 0: + print("Warning: Negative Intensities have been created") + return noisy_intensity + + +def get_absolute_intensity(chem, query_rt): + return chem.max_intensity * chem.chromatogram.get_relative_intensity(query_rt - chem.rt) + + +def get_key(chem): + ''' + Turns a chemical object into (mz, rt, intensity) tuples for equal comparison + :param chem: A chemical object + :return: a tuple of the three values + ''' + return (tuple(chem.isotopes), chem.rt, chem.max_intensity) + +def RestrictedChemicalCreator(N, ps, prop_ms2_mass=0.7, mz_range = [(0,1000)]): + dataset = [] + chrom = EmpiricalChromatogram(np.array([0,20]),np.array([0,0]),np.array([1,1])) + for i in range(N): + mz = ps.get_peak(1, 1, mz_range[0][0], mz_range[0][1])[0].mz + chem = UnknownChemical(mz, 0, 1E5, chrom, children=None) + n_children = int(ps.n_peaks(2, 1)) + parent_mass_prop = [1/n_children for k in range(n_children)] + children = [] + for j in range(n_children): + mz = ps.get_peak(2, 1)[0].mz + children.append(MSN(mz, 2, prop_ms2_mass, parent_mass_prop[j], None, chem)) + chem.children = children + dataset.append(chem) + return dataset diff --git a/Synthetic data creation scripts/vimms/ChineseRestaurantProcess.py b/Synthetic data creation scripts/vimms/ChineseRestaurantProcess.py new file mode 100644 index 00000000..6e735232 --- /dev/null +++ b/Synthetic data creation scripts/vimms/ChineseRestaurantProcess.py @@ -0,0 +1,29 @@ +import numpy as np + + +def discrete_draw(p): + # samples a discrete number based on a vector of probabilities + probs = [float(z) / sum(p) for z in p] + rv = np.random.multinomial(1, probs) + return int(np.where(np.random.multinomial(1, probs) == 1)[0]) + + +def Restricted_Crp(alpha, previous_counts, previous_ms2, len_current_ms2): + # Draws a value from a Chinese Restaurant process, but excludes values already part of the current sample + n = len(previous_ms2) + if previous_ms2 == []: + return 0, [1] + assign_probs = [None] * (len(previous_counts) + 1) + index_to_zero = previous_ms2[-(len_current_ms2):] + for i in range(len(previous_counts)): + if i in index_to_zero: + assign_probs[i] = 0 + else: + assign_probs[i] = previous_counts[i] / (n - 1 + alpha) + assign_probs[-1] = alpha / (n - 1 + alpha) + next_crp = discrete_draw(assign_probs) + if next_crp == (len(previous_counts)): + previous_counts.append(1) + else: + previous_counts[next_crp] += 1 + return next_crp, previous_counts diff --git a/Synthetic data creation scripts/vimms/Chromatograms.py b/Synthetic data creation scripts/vimms/Chromatograms.py new file mode 100644 index 00000000..733d82b2 --- /dev/null +++ b/Synthetic data creation scripts/vimms/Chromatograms.py @@ -0,0 +1,132 @@ +import numpy as np +import scipy.stats + + +class Chromatogram(object): + + def get_relative_intensity(self, query_rt): + raise NotImplementedError() + + def get_relative_mz(self, query_rt): + raise NotImplementedError() + + def _rt_match(self, rt): + raise NotImplementedError() + + +class EmpiricalChromatogram(Chromatogram): + """ + Empirical Chromatograms to be used within Chemicals + """ + + def __init__(self, rts, mzs, intensities, single_point_length=0.9): + self.raw_rts = rts + self.raw_mzs = mzs + self.raw_intensities = intensities + # ensures that all arrays are in sorted order + if len(rts) > 1: + p = rts.argsort() + rts = rts[p] + mzs = mzs[p] + intensities = intensities[p] + else: + rts = np.array([rts[0] - 0.5 * single_point_length, rts[0] + 0.5 * single_point_length]) + mzs = np.array([mzs[0], mzs[0]]) + intensities = np.array([intensities[0], intensities[0]]) + # normalise arrays + self.rts = rts - min(rts) + self.mzs = mzs - np.mean(mzs) # may want to just set this to 0 and remove from input + self.intensities = intensities / max(intensities) + # chromatogramDensityNormalisation(rts, intensities) + + self.min_rt = min(self.rts) + self.max_rt = max(self.rts) + + def get_relative_intensity(self, query_rt): + if not self._rt_match(query_rt): + return None + else: + neighbours_which = self._get_rt_neighbours_which(query_rt) + intensity_below = self.intensities[neighbours_which[0]] + intensity_above = self.intensities[neighbours_which[1]] + return intensity_below + (intensity_above - intensity_below) * self._get_distance(query_rt) + + def get_relative_mz(self, query_rt): + if not self._rt_match(query_rt): + return None + else: + neighbours_which = self._get_rt_neighbours_which(query_rt) + mz_below = self.mzs[neighbours_which[0]] + mz_above = self.mzs[neighbours_which[1]] + return mz_below + (mz_above - mz_below) * self._get_distance(query_rt) + + def _get_rt_neighbours(self, query_rt): + which_rt_below, which_rt_above = self._get_rt_neighbours_which(query_rt) + rt_below = self.rts[which_rt_below] + rt_above = self.rts[which_rt_above] + return [rt_below, rt_above] + + def _get_rt_neighbours_which(self, query_rt): + # find the max index of self.rts smaller than query_rt + pos = np.where(self.rts <= query_rt)[0] + which_rt_below = pos[-1] + + # take the min index of self.rts larger than query_rt + pos = np.where(self.rts > query_rt)[0] + which_rt_above = pos[0] + return [which_rt_below, which_rt_above] + + def _get_distance(self, query_rt): + rt_below, rt_above = self._get_rt_neighbours(query_rt) + return (query_rt - rt_below) / (rt_above - rt_below) + + def _rt_match(self, query_rt): + return self.min_rt < query_rt < self.max_rt + + def __eq__(self, other): + if not isinstance(other, EmpiricalChromatogram): + # don't attempt to compare against unrelated types + return NotImplemented + + return np.array_equal(sorted(self.raw_mzs), sorted(other.raw_mzs)) and \ + np.array_equal(sorted(self.raw_rts), sorted(other.raw_rts)) and \ + np.array_equal(sorted(self.raw_intensities), sorted(other.raw_intensities)) + + +# Make this more generalisable. Make scipy.stats... as input, However this makes it difficult to do the cutoff +class FunctionalChromatogram(Chromatogram): + """ + Functional Chromatograms to be used within Chemicals + """ + + def __init__(self, distribution, parameters, cutoff=0.01): + self.cutoff = cutoff + self.mz = 0 + if distribution == "normal": + self.distrib = scipy.stats.norm(parameters[0], parameters[1]) + elif distribution == "gamma": + self.distrib = scipy.stats.gamma(parameters[0], parameters[1], parameters[2]) + elif distribution == "uniform": + self.distrib = scipy.stats.uniform(parameters[0], parameters[1]) + else: + raise NotImplementedError("distribution not implemented") + self.min_rt = 0 + self.max_rt = self.distrib.ppf(1 - (self.cutoff / 2)) - self.distrib.ppf(self.cutoff / 2) + + def get_relative_intensity(self, query_rt): + if self._rt_match(query_rt) == False: + return None + else: + return (self.distrib.pdf(query_rt + self.distrib.ppf(self.cutoff / 2)) * (1 / (1 - self.cutoff))) + + def get_relative_mz(self, query_rt): + if self._rt_match(query_rt) == False: + return None + else: + return self.mz + + def _rt_match(self, query_rt): + if query_rt < 0 or query_rt > self.distrib.ppf(1 - (self.cutoff / 2)) - self.distrib.ppf(self.cutoff / 2): + return False + else: + return True diff --git a/Synthetic data creation scripts/vimms/Common.py b/Synthetic data creation scripts/vimms/Common.py new file mode 100644 index 00000000..2fec49e4 --- /dev/null +++ b/Synthetic data creation scripts/vimms/Common.py @@ -0,0 +1,196 @@ +import collections +import gzip +import logging +import math +import os +import pathlib +import pickle +import zipfile +from bisect import bisect_left + +import numpy as np + +# some useful constants +import requests +from tqdm import tqdm + +MZ = 'mz' +INTENSITY = 'intensity' +RT = 'rt' +MZ_INTENSITY_RT = MZ + '_' + INTENSITY + '_' + RT +N_PEAKS = 'n_peaks' +SCAN_DURATION = 'scan_duration' +POSITIVE = 'positive' +NEGATIVE = 'negative' +DEFAULT_MS1_SCAN_WINDOW = (0, 1e3) +CHEM_DATA = 'data' +CHEM_NOISE = 'noise' + +PROTON_MASS = 1.00727645199076 + + +def create_if_not_exist(out_dir): + if not os.path.exists(out_dir) and len(out_dir) > 0: + print('Created %s' % out_dir) + pathlib.Path(out_dir).mkdir(parents=True, exist_ok=True) + + +def save_obj(obj, filename): + """ + Save object to file + :param obj: the object to save + :param filename: the output file + :return: None + """ + out_dir = os.path.dirname(filename) + create_if_not_exist(out_dir) + print('Saving %s to %s' % (type(obj), filename)) + with gzip.GzipFile(filename, 'w') as f: + pickle.dump(obj, f, protocol=pickle.HIGHEST_PROTOCOL) + + +def load_obj(filename): + """ + Load saved object from file + :param filename: The file to load + :return: the loaded object + """ + try: + with gzip.GzipFile(filename, 'rb') as f: + return pickle.load(f) + except OSError: + logging.getLogger().warning('Old, invalid or missing pickle in %s. Please regenerate this file.' % filename) + return None + + +def chromatogramDensityNormalisation(rts, intensities): + """ + Definition to standardise the area under a chromatogram to 1. Returns updated intensities + """ + area = 0.0 + for rt_index in range(len(rts) - 1): + area += ((intensities[rt_index] + intensities[rt_index + 1]) / 2) / (rts[rt_index + 1] - rts[rt_index]) + new_intensities = [x * (1 / area) for x in intensities] + return new_intensities + + +# Note: M+H should come first in this dict because of the prior specification +POS_TRANSFORMATIONS = collections.OrderedDict() +POS_TRANSFORMATIONS['M+H'] = lambda mz: (mz + PROTON_MASS) +POS_TRANSFORMATIONS['[M+ACN]+H'] = lambda mz: (mz + 42.033823) +POS_TRANSFORMATIONS['[M+CH3OH]+H'] = lambda mz: (mz + 33.033489) +POS_TRANSFORMATIONS['[M+NH3]+H'] = lambda mz: (mz + 18.033823) +POS_TRANSFORMATIONS['M+Na'] = lambda mz: (mz + 22.989218) +POS_TRANSFORMATIONS['M+K'] = lambda mz: (mz + 38.963158) +POS_TRANSFORMATIONS['M+2Na-H'] = lambda mz: (mz + 44.971160) +POS_TRANSFORMATIONS['M+ACN+Na'] = lambda mz: (mz + 64.015765) +POS_TRANSFORMATIONS['M+2Na-H'] = lambda mz: (mz + 44.971160) +POS_TRANSFORMATIONS['M+2K+H'] = lambda mz: (mz + 76.919040) +POS_TRANSFORMATIONS['[M+DMSO]+H'] = lambda mz: (mz + 79.02122) +POS_TRANSFORMATIONS['[M+2ACN]+H'] = lambda mz: (mz + 83.060370) +POS_TRANSFORMATIONS['2M+H'] = lambda mz: (mz * 2) + 1.007276 +POS_TRANSFORMATIONS['M+ACN+Na'] = lambda mz: (mz + 64.015765) +POS_TRANSFORMATIONS['2M+NH4'] = lambda mz: (mz * 2) + 18.033823 + + +def adduct_transformation(mz, adduct): + f = POS_TRANSFORMATIONS[adduct] + return f(mz) + + +def takeClosest(myList, myNumber): + """ + Assumes myList is sorted. Returns closest value to myNumber. + + If two numbers are equally close, return the smallest number. + """ + pos = bisect_left(myList, myNumber) + if pos == 0: + return 0 + if pos == len(myList): + return -1 + before = myList[pos - 1] + after = myList[pos] + if after - myNumber < myNumber - before: + return pos + else: + return pos - 1 + + +def set_log_level_warning(): + logging.getLogger().setLevel(logging.WARNING) + + +def set_log_level_info(): + logging.getLogger().setLevel(logging.INFO) + + +def set_log_level_debug(): + logging.getLogger().setLevel(logging.DEBUG) + + +# see https://stackoverflow.com/questions/3375443/how-to-pickle-loggers +class LoggerMixin(): + @property + def logger(self): + # turn off annoying matplotlib messages + mpl_logger = logging.getLogger('matplotlib') + mpl_logger.setLevel(logging.WARNING) + # initalise basic config for all loggers + name = "{}".format(type(self).__name__) + format = '%(levelname)-7s: %(name)-30s : %(message)s' + logging.basicConfig(level=logging.getLogger().level, format=format) + logger = logging.getLogger(name) + return logger + + +def get_rt(spectrum): + ''' + Extracts RT value from a pymzml spectrum object + :param spectrum: a pymzml spectrum object + :return: the retention time (in seconds) + ''' + rt, units = spectrum.scan_time + if units == 'minute': + rt *= 60.0 + return rt + + +def find_nearest_index_in_array(array, value): + ''' + Finds index in array where the value is the nearest + :param array: + :param value: + :return: + ''' + idx = (np.abs(array - value)).argmin() + return idx + + +def download_file(url, out_file=None): + r = requests.get(url, stream=True) + total_size = int(r.headers.get('content-length', 0)); + block_size = 1024 + current_size = 0 + + if out_file is None: + out_file = url.rsplit('/', 1)[-1] # get the last part in url + print('Downloading %s' % out_file) + + with open(out_file, 'wb') as f: + for data in tqdm(r.iter_content(block_size), total=math.ceil(total_size//block_size) , unit='KB', unit_scale=True): + current_size += len(data) + f.write(data) + assert current_size == total_size + return out_file + + +def extract_zip_file(in_file, delete=True): + print('Extracting %s' % in_file) + with zipfile.ZipFile(file=in_file) as zip_file: + for file in tqdm(iterable=zip_file.namelist(), total=len(zip_file.namelist())): + zip_file.extract(member=file) + + if delete: + print('Deleting %s' % in_file) + os.remove(in_file) \ No newline at end of file diff --git a/Synthetic data creation scripts/vimms/Controller.py b/Synthetic data creation scripts/vimms/Controller.py new file mode 100644 index 00000000..7f627359 --- /dev/null +++ b/Synthetic data creation scripts/vimms/Controller.py @@ -0,0 +1,726 @@ +import sys +from collections import defaultdict, namedtuple + +EIC = namedtuple('EIC', 'name mzs rts its') + +import numpy as np +import pandas as pd +import pylab as plt +from tqdm import tqdm + +from vimms.Common import POSITIVE, DEFAULT_MS1_SCAN_WINDOW, LoggerMixin +from vimms.MassSpec import ScanParameters, IndependentMassSpectrometer +from vimms.MzmlWriter import MzmlWriter + +def roughly_equal( a, b, tol=0.00075 ): + return abs( a - b ) <= tol + +class FloatSet: + + def __init__( self, initial=None ): + self.values = [] + self.length = 0 + self.idx = 0 + if initial is not None: + for vv in initial: + self.add( vv ) + + def add( self, nval ): + self.values.append( nval ) + + def __iter__( self ): + self.values.sort( ) + temp = [ self.values[0] ] + for vv in self.values[1:]: + if not roughly_equal( vv, temp[-1], 0.05 ): + temp.append( vv ) + self.values = temp + self.length = len( temp ) + return self + + def __next__( self ): + if self.idx < self.length: + result = self.values[ self.idx ] + self.idx += 1 + return result + else: + raise StopIteration + + +class Controller(LoggerMixin): + def __init__(self, mass_spec): + self.scans = defaultdict(list) # key: ms level, value: list of scans for that level + self.mass_spec = mass_spec + self.make_plot = False + + def handle_scan(self, scan): + self.scans[scan.ms_level].append(scan) + self._process_scan(scan) + self._update_parameters(scan) + + def handle_acquisition_open(self): + raise NotImplementedError() + + def handle_acquisition_closing(self): + raise NotImplementedError() + + def write_mzML(self, analysis_name, outfile): + writer = MzmlWriter(analysis_name, self.scans, precursor_information=self.mass_spec.precursor_information) + writer.write_mzML(outfile) + + def _process_scan(self, scan): + raise NotImplementedError() + + def _update_parameters(self, scan): + raise NotImplementedError() + + def run(self, min_time, max_time, progress_bar=True): + raise NotImplementedError() + + def _plot_scan(self, scan): + if self.make_plot: + plt.figure() + for i in range(scan.num_peaks): + x1 = scan.mzs[i] + x2 = scan.mzs[i] + y1 = 0 + y2 = scan.intensities[i] + a = [[x1, y1], [x2, y2]] + plt.plot(*zip(*a), marker='', color='r', ls='-', lw=1) + plt.title('Scan {0} {1}s -- {2} peaks'.format(scan.scan_id, scan.rt, scan.num_peaks)) + plt.show() + + +class SimpleMs1Controller(Controller): + # CJ.. this is the one we are actually using + def __init__(self, mass_spec, upper_mz=None): + super().__init__(mass_spec) + default_scan = ScanParameters() + default_scan.set(ScanParameters.MS_LEVEL, 1) + if upper_mz: + default_scan.set(ScanParameters.ISOLATION_WINDOWS, [[(0, upper_mz) ]]) + else: + default_scan.set(ScanParameters.ISOLATION_WINDOWS, [[DEFAULT_MS1_SCAN_WINDOW]]) + + self.eics = list() + + mass_spec.reset() + mass_spec.current_N = 0 + mass_spec.current_DEW = 0 + + mass_spec.set_repeating_scan(default_scan) + mass_spec.register(IndependentMassSpectrometer.MS_SCAN_ARRIVED, self.handle_scan) + mass_spec.register(IndependentMassSpectrometer.ACQUISITION_STREAM_OPENING, self.handle_acquisition_open) + mass_spec.register(IndependentMassSpectrometer.ACQUISITION_STREAM_CLOSING, self.handle_acquisition_closing) + + def peak_recorder( self ): + return self.mass_spec.peak_recorder + + def run(self, min_time, max_time, progress_bar=True): + if progress_bar: + with tqdm(total=max_time - min_time, initial=0) as pbar: + self.mass_spec.run(min_time, max_time, pbar=pbar) + else: + self.mass_spec.run(min_time, max_time ) + + def handle_acquisition_open(self): + self.logger.info('Acquisition open') + + def handle_acquisition_closing(self): + self.logger.info('Acquisition closing') + + def _process_scan(self, scan): + if scan.num_peaks > 0: + self.logger.info('Time %f Received %s' % (self.mass_spec.time, scan)) + self._plot_scan(scan) + + def _update_parameters(self, scan): + pass # do nothing + + def create_eics( self, mapper ): + # TODO should probably check for the ms level + eics = [] + for chem, values in self.peak_recorder().items(): + # so we loop through all of the peaks and + # determine the unique mzs + unique_mzs = FloatSet( [ pr[1] for pr in values ] ) + for currmz in unique_mzs: + sliced = list(filter(lambda pr: roughly_equal( pr[1], currmz, 0.05), values )) + curr_rts = np.array( mapper.rts() ) + curr_mz = np.repeat( currmz, mapper.length() ) + curr_it = np.zeros( mapper.length() ) + for (rt, mz, it ) in sliced: + idx = mapper.rt_to_idx( rt ) + curr_it[ idx ] = it + eics.append( + EIC( name=chem, rts=curr_rts, mzs=curr_mz, its=curr_it ) + ) + return eics + + def _reset_scans( self, level ): + num_scans = len( self.scans[level] ) + for idx in range( num_scans ): + self.scans[ level ][ idx ].mzs = [] + self.scans[ level ][ idx ].intensities = [] + self.scans[ level ][ idx ].num_peaks = 0 + + def update_scans( self, finalized_eics, mapper ): + finalized_eics.sort( key=lambda eic: eic.mzs[0] ) + # first. reset each scan. we assume level is 1 + self._reset_scans( 1 ) + for eic in finalized_eics: + assert len(eic.rts) == len(self.scans[1]) + for idx, (rt, mz, it) in enumerate(zip(eic.rts, eic.mzs, eic.its)): + self.scans[ 1 ][ idx ].mzs.append( mz ) + self.scans[ 1 ][ idx ].intensities.append( it ) + self.scans[ 1 ][ idx ].num_peaks += 1 + + num_scans = len( self.scans[1] ) + for idx in range( num_scans ): + self.scans[ 1 ][ idx ].mzs = np.array( self.scans[ 1 ][ idx ].mzs ) + self.scans[ 1 ][ idx ].intensities = np.array( self.scans[ 1 ][ idx ].intensities ) + + +class Precursor(object): + def __init__(self, precursor_mz, precursor_intensity, precursor_charge, precursor_scan_id): + self.precursor_mz = precursor_mz + self.precursor_intensity = precursor_intensity + self.precursor_charge = precursor_charge + self.precursor_scan_id = precursor_scan_id + + def __str__(self): + return 'Precursor mz %f intensity %f charge %d scan_id %d' % ( + self.precursor_mz, self.precursor_intensity, self.precursor_charge, self.precursor_scan_id) + + +class TopNController(Controller): + def __init__(self, mass_spec, N, isolation_window, mz_tol, rt_tol, min_ms1_intensity): + super().__init__(mass_spec) + self.last_ms1_scan = None + self.N = N + self.isolation_window = isolation_window # the isolation window (in Dalton) to select a precursor ion + self.mz_tol = mz_tol # the m/z window (ppm) to prevent the same precursor ion to be fragmented again + self.rt_tol = rt_tol # the rt window to prevent the same precursor ion to be fragmented again + self.min_ms1_intensity = min_ms1_intensity # minimum ms1 intensity to fragment + + mass_spec.reset() + mass_spec.current_N = N + mass_spec.current_DEW = rt_tol + + default_scan = ScanParameters() + default_scan.set(ScanParameters.MS_LEVEL, 1) + default_scan.set(ScanParameters.ISOLATION_WINDOWS, [[DEFAULT_MS1_SCAN_WINDOW]]) + mass_spec.set_repeating_scan(default_scan) + + # register new event handlers under this controller + mass_spec.register(IndependentMassSpectrometer.MS_SCAN_ARRIVED, self.handle_scan) + mass_spec.register(IndependentMassSpectrometer.ACQUISITION_STREAM_OPENING, self.handle_acquisition_open) + mass_spec.register(IndependentMassSpectrometer.ACQUISITION_STREAM_CLOSING, self.handle_acquisition_closing) + + def run(self, min_time=None, max_time=None, progress_bar=True): + if min_time is None and max_time is None: + min_time = self.mass_spec.schedule["targetTime"].values[0] + max_time = self.mass_spec.schedule["targetTime"].values[-1] + if progress_bar: + with tqdm(total=max_time - min_time, initial=0) as pbar: + self.mass_spec.run(min_time, max_time, pbar=pbar) + else: + self.mass_spec.run(min_time, max_time) + + def handle_acquisition_open(self): + self.logger.info('Time %f Acquisition open' % self.mass_spec.time) + + def handle_acquisition_closing(self): + self.logger.info('Time %f Acquisition closing' % self.mass_spec.time) + + def _process_scan(self, scan): + self.logger.info('Time %f Received from mass spec %s' % (self.mass_spec.time, scan)) + if scan.ms_level == 1: # we get an ms1 scan, if it has a peak, then store it for fragmentation next time + if scan.num_peaks > 0: + self.last_ms1_scan = scan + else: + self.last_ms1_scan = None + + elif scan.ms_level == 2: # if we get ms2 scan, then do something with it + # scan.filter_intensity(self.min_ms2_intensity) + if scan.num_peaks > 0: + self._plot_scan(scan) + + def _update_parameters(self, scan): + + # if there's a previous ms1 scan to process + if self.last_ms1_scan is not None: + + mzs = self.last_ms1_scan.mzs + intensities = self.last_ms1_scan.intensities + rt = self.last_ms1_scan.rt + + # loop over points in decreasing intensity + fragmented_count = 0 + idx = np.argsort(intensities)[::-1] + for i in idx: + mz = mzs[i] + intensity = intensities[i] + + # stopping criteria is after we've fragmented N ions or we found ion < min_intensity + if fragmented_count >= self.N: + self.logger.debug('Time %f Top-%d ions have been selected' % (self.mass_spec.time, self.N)) + break + + if intensity < self.min_ms1_intensity: + self.logger.debug( + 'Time %f Minimum intensity threshold %f reached at %f, %d' % ( + self.mass_spec.time, self.min_ms1_intensity, intensity, fragmented_count)) + break + + # skip ion in the dynamic exclusion list of the mass spec + if self.mass_spec.is_excluded(mz, rt): + continue + + # send a new ms2 scan parameter to the mass spec + dda_scan_params = ScanParameters() + dda_scan_params.set(ScanParameters.MS_LEVEL, 2) + + # create precursor object, assume it's all singly charged + precursor_charge = +1 if (self.mass_spec.ionisation_mode == POSITIVE) else -1 + precursor = Precursor(precursor_mz=mz, precursor_intensity=intensity, + precursor_charge=precursor_charge, precursor_scan_id=self.last_ms1_scan.scan_id) + mz_lower = mz - self.isolation_window # Da + mz_upper = mz + self.isolation_window # Da + isolation_windows = [[(mz_lower, mz_upper)]] + dda_scan_params.set(ScanParameters.ISOLATION_WINDOWS, isolation_windows) + dda_scan_params.set(ScanParameters.PRECURSOR, precursor) + + # save dynamic exclusion parameters too + dda_scan_params.set(ScanParameters.DYNAMIC_EXCLUSION_MZ_TOL, self.mz_tol) + dda_scan_params.set(ScanParameters.DYNAMIC_EXCLUSION_RT_TOL, self.rt_tol) + + # push this dda scan parameter to the mass spec queue + self.mass_spec.add_to_processing_queue(dda_scan_params) + fragmented_count += 1 + + for param in self.mass_spec.get_processing_queue(): + precursor = param.get(ScanParameters.PRECURSOR) + if precursor is not None: + self.logger.debug('- %s' % str(precursor)) + + # set this ms1 scan as has been processed + self.last_ms1_scan = None + + +class TreeController(Controller): + def __init__(self, mass_spec, dia_design, window_type, kaufmann_design, extra_bins, num_windows=None): + super().__init__(mass_spec) + self.last_ms1_scan = None + self.dia_design = dia_design + self.window_type = window_type + self.kaufmann_design = kaufmann_design + self.extra_bins = extra_bins + self.num_windows = num_windows + + mass_spec.reset() + default_scan = ScanParameters() + default_scan.set(ScanParameters.MS_LEVEL, 1) + default_scan.set(ScanParameters.ISOLATION_WINDOWS, [[DEFAULT_MS1_SCAN_WINDOW]]) + mass_spec.set_repeating_scan(default_scan) + + mass_spec.register(IndependentMassSpectrometer.MS_SCAN_ARRIVED, self.handle_scan) + mass_spec.register(IndependentMassSpectrometer.ACQUISITION_STREAM_OPENING, self.handle_acquisition_open) + mass_spec.register(IndependentMassSpectrometer.ACQUISITION_STREAM_CLOSING, self.handle_acquisition_closing) + + def run(self, min_time, max_time, progress_bar=True): + if progress_bar: + with tqdm(total=max_time - min_time, initial=0) as pbar: + self.mass_spec.run(min_time, max_time, pbar=pbar) + else: + self.mass_spec.run(min_time, max_time) + + def handle_acquisition_open(self): + self.logger.info('Acquisition open') + + def handle_acquisition_closing(self): + self.logger.info('Acquisition closing') + + def _process_scan(self, scan): + self.logger.info('Received scan {}'.format(scan)) + if scan.ms_level == 1: # if we get a non-empty ms1 scan + if scan.num_peaks > 0: + self.last_ms1_scan = scan + else: + self.last_ms1_scan = None + + elif scan.ms_level == 2: # if we get ms2 scan, then do something with it + if scan.num_peaks > 0: + self._plot_scan(scan) + + def _update_parameters(self, scan): + + # if there's a previous ms1 scan to process + if self.last_ms1_scan is not None: + + rt = self.last_ms1_scan.rt + + # then get the last ms1 scan, select bin walls and create scan locations + mzs = self.last_ms1_scan.mzs + default_range = [DEFAULT_MS1_SCAN_WINDOW] # TODO: this should maybe come from somewhere else? + locations = DiaWindows(mzs, default_range, self.dia_design, self.window_type, self.kaufmann_design, + self.extra_bins, self.num_windows).locations + self.logger.debug('Window locations {}'.format(locations)) + for i in range(len(locations)): # define isolation window around the selected precursor ions + isolation_windows = locations[i] + dda_scan_params = ScanParameters() + dda_scan_params.set(ScanParameters.MS_LEVEL, 2) + dda_scan_params.set(ScanParameters.ISOLATION_WINDOWS, isolation_windows) + self.mass_spec.add_to_processing_queue(dda_scan_params) # push this dda scan to the mass spec queue + + # set this ms1 scan as has been processed + self.last_ms1_scan = None + + +class KaufmannWindows(object): + """ + Method for creating window designs based on Kaufmann paper - https://www.ncbi.nlm.nih.gov/pubmed/27188447 + """ + + def __init__(self, bin_walls, bin_walls_extra, kaufmann_design, extra_bins=0): + self.locations = [] + if kaufmann_design == "nested": + n_locations_internal = 4 + for i in range(0, 8): + self.locations.append([[(bin_walls[(0 + i * 8)], bin_walls[(8 + i * 8)])]]) + elif kaufmann_design == "tree": + n_locations_internal = 3 + self.locations.append([[(bin_walls[0], bin_walls[32])]]) + self.locations.append([[(bin_walls[32], bin_walls[64])]]) + self.locations.append([[(bin_walls[16], bin_walls[48])]]) + self.locations.append([[(bin_walls[8], bin_walls[24]), (bin_walls[40], bin_walls[56])]]) + else: + raise ValueError("not a valid design") + locations_internal = [[[]] for i in range(n_locations_internal + extra_bins)] + for i in range(0, 4): + locations_internal[0][0].append((bin_walls[(4 + i * 16)], bin_walls[(12 + i * 16)])) + locations_internal[1][0].append((bin_walls[(2 + i * 16)], bin_walls[(6 + i * 16)])) + locations_internal[1][0].append((bin_walls[(10 + i * 16)], bin_walls[(14 + i * 16)])) + locations_internal[2][0].append((bin_walls[(1 + i * 16)], bin_walls[(3 + i * 16)])) + locations_internal[2][0].append((bin_walls[(9 + i * 16)], bin_walls[(11 + i * 16)])) + if kaufmann_design == "nested": + locations_internal[3][0].append((bin_walls[(5 + i * 16)], bin_walls[(7 + i * 16)])) + locations_internal[3][0].append((bin_walls[(13 + i * 16)], bin_walls[(15 + i * 16)])) + else: + locations_internal[2][0].append((bin_walls[(5 + i * 16)], bin_walls[(7 + i * 16)])) + locations_internal[2][0].append((bin_walls[(13 + i * 16)], bin_walls[(15 + i * 16)])) + if extra_bins > 0: # TODO: fix this + for j in range(extra_bins): + for i in range(64 * (2 ** j)): + locations_internal[n_locations_internal + j][0].append((bin_walls_extra[int( + 0 + i * ((2 ** extra_bins) / (2 ** j)))], bin_walls_extra[int( + ((2 ** extra_bins) / (2 ** j)) / 2 + i * ((2 ** extra_bins) / (2 ** j)))])) + self.locations.extend(locations_internal) + + +class DiaWindows(object): + """ + Create DIA window design + """ + + def __init__(self, ms1_mzs, ms1_range, dia_design, window_type, kaufmann_design, extra_bins, num_windows=None, + range_slack=0.01): + ms1_range_difference = ms1_range[0][1] - ms1_range[0][0] + # set the number of windows for kaufmann method + if dia_design == "kaufmann": + num_windows = 64 + # dont allow extra bins for basic method + if dia_design == "basic" and extra_bins > 0: + sys.exit("Cannot have extra bins with 'basic' dia design.") + # find bin walls and extra bin walls + if window_type == "even": + internal_bin_walls = [ms1_range[0][0]] + for window_index in range(0, num_windows): + internal_bin_walls.append(ms1_range[0][0] + ((window_index + 1) / num_windows) * ms1_range_difference) + internal_bin_walls[0] = internal_bin_walls[0] - range_slack * ms1_range_difference + internal_bin_walls[-1] = internal_bin_walls[-1] + range_slack * ms1_range_difference + internal_bin_walls_extra = None + if extra_bins > 0: + internal_bin_walls_extra = [ms1_range[0][0]] + for window_index in range(0, num_windows * (2 ** extra_bins)): + internal_bin_walls_extra.append(ms1_range[0][0] + ( + (window_index + 1) / (num_windows * (2 ** extra_bins))) * ms1_range_difference) + internal_bin_walls_extra[0] = internal_bin_walls_extra[0] - range_slack * ms1_range_difference + internal_bin_walls_extra[-1] = internal_bin_walls_extra[-1] + range_slack * ms1_range_difference + elif window_type == "percentile": + internal_bin_walls = np.percentile(ms1_mzs, + np.arange(0, 100 + 100 / num_windows, 100 / num_windows)).tolist() + internal_bin_walls[0] = internal_bin_walls[0] - range_slack * ms1_range_difference + internal_bin_walls[-1] = internal_bin_walls[-1] + range_slack * ms1_range_difference + internal_bin_walls_extra = None + if extra_bins > 0: + internal_bin_walls_extra = np.percentile(ms1_mzs, + np.arange(0, 100 + 100 / (num_windows * (2 ** extra_bins)), + 100 / (num_windows * (2 ** extra_bins)))).tolist() + internal_bin_walls_extra[0] = internal_bin_walls_extra[0] - range_slack * ms1_range_difference + internal_bin_walls_extra[-1] = internal_bin_walls_extra[-1] + range_slack * ms1_range_difference + else: + raise ValueError("Incorrect window_type selected. Must be 'even' or 'percentile'.") + # convert bin walls and extra bin walls into locations to scan + if dia_design == "basic": + self.locations = [] + for window_index in range(0, num_windows): + self.locations.append([[(internal_bin_walls[window_index], internal_bin_walls[window_index + 1])]]) + elif dia_design == "kaufmann": + self.locations = KaufmannWindows(internal_bin_walls, internal_bin_walls_extra, kaufmann_design, + extra_bins).locations + else: + raise ValueError("Incorrect dia_design selected. Must be 'basic' or 'kaufmann'.") + + +class DsDAController(Controller): + def __init__(self, mass_spec, N, isolation_window, rt_tol, min_ms1_intensity): + super().__init__(mass_spec) + self.last_ms1_scan = None + self.N = N + self.isolation_window = isolation_window # the isolation window (in Dalton) around a precursor ion to be fragmented + self.rt_tol = rt_tol # the rt window to prevent the same precursor ion to be fragmented again + self.min_ms1_intensity = min_ms1_intensity # minimum ms1 intensity to fragment + + mass_spec.reset() + mass_spec.current_N = N + mass_spec.current_DEW = rt_tol + + # register new event handlers under this controller + mass_spec.register(IndependentMassSpectrometer.MS_SCAN_ARRIVED, self.handle_scan) + mass_spec.register(IndependentMassSpectrometer.ACQUISITION_STREAM_OPENING, self.handle_acquisition_open) + mass_spec.register(IndependentMassSpectrometer.ACQUISITION_STREAM_CLOSING, self.handle_acquisition_closing) + + def run(self, schedule_file, progress_bar=True): + self.schedule = pd.read_csv(schedule_file) + for idx, row in self.schedule.iterrows(): + target_mass = row.targetMass + target_time = row.targetTime + + if np.isnan(target_mass): + ms_level = 1 + isolation_windows = [[(0, 1000)]] + precursor = None + else: + ms_level = 2 + mz_lower = target_mass - self.isolation_window + mz_upper = target_mass + self.isolation_window + isolation_windows = [[(mz_lower, mz_upper)]] + precursor_charge = +1 if (self.mass_spec.ionisation_mode == POSITIVE) else -1 + scan_id = 0 + precursor = Precursor(precursor_mz=target_mass, precursor_intensity=0, + precursor_charge=precursor_charge, precursor_scan_id=scan_id) + + dda_scan_params = ScanParameters() + dda_scan_params.set(ScanParameters.MS_LEVEL, ms_level) + dda_scan_params.set(ScanParameters.ISOLATION_WINDOWS, isolation_windows) + dda_scan_params.set(ScanParameters.TIME, target_time) + if precursor: + dda_scan_params.set(ScanParameters.PRECURSOR, precursor) + self.mass_spec.add_to_processing_queue(dda_scan_params) # push this scan to the mass spec queue + + if progress_bar: + with tqdm(total=target_time, + initial=0) as pbar: + self.mass_spec.run(self.schedule, pbar=pbar) + else: + self.mass_spec.run(self.schedule) + + def handle_acquisition_open(self): + self.logger.info('Acquisition open') + + def handle_acquisition_closing(self): + self.logger.info('Acquisition closing') + + def _process_scan(self, scan): + self.logger.info('Received {}'.format(scan)) + if scan.ms_level == 1: # we get an ms1 scan, store it for fragmentation next time + self.last_ms1_scan = scan + elif scan.ms_level == 2: # if we get ms2 scan, then do something with it + # scan.filter_intensity(self.min_ms2_intensity) + if scan.num_peaks > 0: + self._plot_scan(scan) + + def _update_parameters(self, scan): + pass + + +class HybridController(Controller): + def __init__(self, mass_spec, N, scan_param_changepoints, isolation_window, mz_tol, rt_tol, min_ms1_intensity, + n_purity_scans=None, purity_shift=None, purity_threshold=0): + super().__init__(mass_spec) + self.last_ms1_scan = None + self.N = np.array(N) + if scan_param_changepoints is not None: + self.scan_param_changepoints = np.array([0] + scan_param_changepoints) + else: + self.scan_param_changepoints = np.array([0]) + self.isolation_window = np.array(isolation_window) # the isolation window (in Dalton) to select a precursor ion + self.mz_tol = np.array(mz_tol) # the m/z window (ppm) to prevent the same precursor ion to be fragmented again + self.rt_tol = np.array(rt_tol) # the rt window to prevent the same precursor ion to be fragmented again + self.min_ms1_intensity = min_ms1_intensity # minimum ms1 intensity to fragment + + self.n_purity_scans = n_purity_scans + self.purity_shift = purity_shift + self.purity_threshold = purity_threshold + + # make sure the input are all correct + assert len(self.N) == len(self.scan_param_changepoints) == len(self.isolation_window) == len(self.mz_tol) == len(self.rt_tol) + if self.purity_threshold != 0: + assert all(self.n_purity_scans < np.array(self.N)) + + mass_spec.reset() + current_N, current_rt_tol, idx = self._get_current_N_DEW() + mass_spec.current_N = current_N + mass_spec.current_DEW = current_rt_tol + + default_scan = ScanParameters() + default_scan.set(ScanParameters.MS_LEVEL, 1) + default_scan.set(ScanParameters.ISOLATION_WINDOWS, [[DEFAULT_MS1_SCAN_WINDOW]]) + mass_spec.set_repeating_scan(default_scan) + + # register new event handlers under this controller + mass_spec.register(IndependentMassSpectrometer.MS_SCAN_ARRIVED, self.handle_scan) + mass_spec.register(IndependentMassSpectrometer.ACQUISITION_STREAM_OPENING, self.handle_acquisition_open) + mass_spec.register(IndependentMassSpectrometer.ACQUISITION_STREAM_CLOSING, self.handle_acquisition_closing) + + def run(self, min_time=None, max_time=None, progress_bar=True): + if min_time is None and max_time is None: + min_time = self.mass_spec.schedule["targetTime"].values[0] + max_time = self.mass_spec.schedule["targetTime"].values[-1] + if progress_bar: + with tqdm(total=max_time - min_time, initial=0) as pbar: + self.mass_spec.run(min_time, max_time, pbar=pbar) + else: + self.mass_spec.run(min_time, max_time) + + def handle_acquisition_open(self): + self.logger.info('Time %f Acquisition open' % self.mass_spec.time) + + def handle_acquisition_closing(self): + self.logger.info('Time %f Acquisition closing' % self.mass_spec.time) + + def _process_scan(self, scan): + self.logger.info('Time %f Received from mass spec %s' % (self.mass_spec.time, scan)) + if scan.ms_level == 1: # we get an ms1 scan, if it has a peak, then store it for fragmentation next time + if scan.num_peaks > 0: + self.last_ms1_scan = scan + else: + self.last_ms1_scan = None + + elif scan.ms_level == 2: # if we get ms2 scan, then do something with it + # scan.filter_intensity(self.min_ms2_intensity) + if scan.num_peaks > 0: + self._plot_scan(scan) + + def _update_parameters(self, scan): + + # if there's a previous ms1 scan to process + if self.last_ms1_scan is not None: + + mzs = self.last_ms1_scan.mzs + intensities = self.last_ms1_scan.intensities + rt = self.last_ms1_scan.rt + + # set up current scan parameters + current_N, current_rt_tol, idx = self._get_current_N_DEW() + current_isolation_window = self.isolation_window[idx] + current_mz_tol = self.mz_tol[idx] + + # calculate purities + purities = [] + for mz_idx in range(len(self.last_ms1_scan.mzs)): + nearby_mzs_idx = np.where(abs(self.last_ms1_scan.mzs - self.last_ms1_scan.mzs[mz_idx]) < current_isolation_window) + if len(nearby_mzs_idx[0]) == 1: + purities.append(1) + else: + total_intensity = sum(self.last_ms1_scan.intensities[nearby_mzs_idx]) + purities.append(self.last_ms1_scan.intensities[mz_idx] / total_intensity) + + # loop over points in decreasing intensity + fragmented_count = 0 + idx = np.argsort(intensities)[::-1] + for i in idx: + mz = mzs[i] + intensity = intensities[i] + purity = purities[i] + + # stopping criteria is after we've fragmented N ions or we found ion < min_intensity + if fragmented_count >= current_N: + self.logger.debug('Time %f Top-%d ions have been selected' % (self.mass_spec.time, current_N)) + break + + if intensity < self.min_ms1_intensity: + self.logger.debug( + 'Time %f Minimum intensity threshold %f reached at %f, %d' % ( + self.mass_spec.time, self.min_ms1_intensity, intensity, fragmented_count)) + break + + # skip ion in the dynamic exclusion list of the mass spec + if self.mass_spec.is_excluded(mz, rt): + continue + + if purity < self.purity_threshold: + purity_shift_amounts = [self.purity_shift * (i - (self.n_purity_scans - 1) / 2) for i in range(self.n_purity_scans)] + for purity_idx in range(self.n_purity_scans): + # send a new ms2 scan parameter to the mass spec + dda_scan_params = ScanParameters() + dda_scan_params.set(ScanParameters.MS_LEVEL, 2) + dda_scan_params.set(ScanParameters.N, current_N) + + # create precursor object, assume it's all singly charged + precursor_charge = +1 if (self.mass_spec.ionisation_mode == POSITIVE) else -1 + precursor = Precursor(precursor_mz=mz, precursor_intensity=intensity, + precursor_charge=precursor_charge, + precursor_scan_id=self.last_ms1_scan.scan_id) + mz_lower = mz + purity_shift_amounts[purity_idx] - current_isolation_window # Da + mz_upper = mz + purity_shift_amounts[purity_idx] + current_isolation_window # Da + isolation_windows = [[(mz_lower, mz_upper)]] + dda_scan_params.set(ScanParameters.ISOLATION_WINDOWS, isolation_windows) + dda_scan_params.set(ScanParameters.PRECURSOR, precursor) + + # save dynamic exclusion parameters too + dda_scan_params.set(ScanParameters.DYNAMIC_EXCLUSION_MZ_TOL, current_mz_tol) + dda_scan_params.set(ScanParameters.DYNAMIC_EXCLUSION_RT_TOL, current_rt_tol) + + # push this dda scan parameter to the mass spec queue + self.mass_spec.add_to_processing_queue(dda_scan_params) + fragmented_count += 1 + # need to work out what we want to do here + else: + # send a new ms2 scan parameter to the mass spec + dda_scan_params = ScanParameters() + dda_scan_params.set(ScanParameters.MS_LEVEL, 2) + dda_scan_params.set(ScanParameters.N, current_N) + + # create precursor object, assume it's all singly charged + precursor_charge = +1 if (self.mass_spec.ionisation_mode == POSITIVE) else -1 + precursor = Precursor(precursor_mz=mz, precursor_intensity=intensity, + precursor_charge=precursor_charge, precursor_scan_id=self.last_ms1_scan.scan_id) + mz_lower = mz - current_isolation_window # Da + mz_upper = mz + current_isolation_window # Da + isolation_windows = [[(mz_lower, mz_upper)]] + dda_scan_params.set(ScanParameters.ISOLATION_WINDOWS, isolation_windows) + dda_scan_params.set(ScanParameters.PRECURSOR, precursor) + + # save dynamic exclusion parameters too + dda_scan_params.set(ScanParameters.DYNAMIC_EXCLUSION_MZ_TOL, current_mz_tol) + dda_scan_params.set(ScanParameters.DYNAMIC_EXCLUSION_RT_TOL, current_rt_tol) + + # push this dda scan parameter to the mass spec queue + self.mass_spec.add_to_processing_queue(dda_scan_params) + fragmented_count += 1 + + for param in self.mass_spec.get_processing_queue(): + precursor = param.get(ScanParameters.PRECURSOR) + if precursor is not None: + self.logger.debug('- %s' % str(precursor)) + + # set this ms1 scan as has been processed + self.last_ms1_scan = None + + def _get_current_N_DEW(self): + idx = np.nonzero(self.scan_param_changepoints <= self.mass_spec.time)[0][-1] + current_N = self.N[idx] + current_rt_tol = self.rt_tol[idx] + return current_N, current_rt_tol, idx diff --git a/Synthetic data creation scripts/vimms/DIA.py b/Synthetic data creation scripts/vimms/DIA.py new file mode 100644 index 00000000..a7698478 --- /dev/null +++ b/Synthetic data creation scripts/vimms/DIA.py @@ -0,0 +1,349 @@ +import numpy as np +from tqdm import tqdm +import math +import sys +import copy + +from vimms.Controller import * +from vimms.MassSpec import * +from vimms.Common import POSITIVE, DEFAULT_MS1_SCAN_WINDOW, LoggerMixin + + +def DiaRestrictedScanner(dataset, ps, dia_design, window_type, kaufmann_design, extra_bins, num_windows=None, pbar=False): + mass_spec = IndependentMassSpectrometer(POSITIVE, dataset, ps) + controller = TreeController(mass_spec, dia_design, window_type, kaufmann_design, extra_bins, num_windows) + controller.run(10, 20, pbar) + controller.scans[2] = controller.scans[2][0:(controller.scans[1][1].scan_id-1)] + controller.scans[1] = controller.scans[1][0:2] + return controller + + +class DiaAnalyser(object): + def __init__(self, controller, min_intensity=0): + self.controller = controller + self.scans = controller.scans + self.dataset = controller.mass_spec.chemicals + self.chemicals_identified = 0 + self.ms2_matched = 0 + self.entropy = 0 + self.ms1_range = np.array([0,1000]) #TODO: fix this (ie make it so it can be controller in controller and then here + self.min_intensity = min_intensity + + self.ms1_scan_times = np.array([scan.rt for scan in self.scans[1]]) + self.ms2_scan_times = np.array([scan.rt for scan in self.scans[2]]) + self.ms1_mzs = [self.controller.mass_spec._get_all_mz_peaks(self.dataset[i], self.dataset[i].rt + 0.01, 1, [[(0, 1000)]])[0][0] for i in range(len(self.dataset))] + self.ms1_start_rt = np.array([data.rt for data in self.dataset]) + self.ms1_end_rt = np.array([data.rt + data.chromatogram.max_rt for data in self.dataset]) + self.first_scans, self.last_scans = self._get_scan_times() + + self.chemical_locations = [] + + with tqdm(total=len(self.dataset)) as pbar: + for chem_num in range(len(self.dataset)): + chemical_location = self._get_chemical_location(chem_num) + chemical_time = [(self.first_scans[chem_num], self.last_scans[chem_num])] + self.chemical_locations.append(chemical_location) + num_ms1_options = 0 + for i in range(len(chemical_location)): + mz_location = np.logical_and(np.array(self.ms1_mzs) > chemical_location[i][0], + np.array(self.ms1_mzs) <= chemical_location[i][1]) + time_location = np.logical_and(self.ms1_start_rt <= chemical_time[0][1], + self.ms1_end_rt >= chemical_time[0][0]) + num_ms1_options += sum(mz_location * time_location) + if num_ms1_options == 0: + self.entropy += -len(self.dataset[chem_num].children) * len(self.dataset) * math.log(1 / len(self.dataset)) + else: + self.entropy += -len(self.dataset[chem_num].children) * num_ms1_options * math.log(1 / num_ms1_options) + if num_ms1_options == 1: + self.chemicals_identified += 1 + self.ms2_matched += len(self.dataset[chem_num].children) + pbar.update(1) + pbar.close() + + def _get_scan_times(self): + max_time = self.scans[1][-1].rt + if self.scans[2] != []: + max_time = max(self.scans[1][-1].rt, self.scans[2][-1].rt) + 1 + first_scans = [max_time for i in self.dataset] + last_scans = [max_time for i in self.dataset] + for chem_num in range(len(self.dataset)): + relevant_times = self.ms1_scan_times[(self.ms1_start_rt[chem_num] < self.ms1_scan_times) & (self.ms1_scan_times < self.ms1_end_rt[chem_num])] + for time in relevant_times: + intensity = self.controller.mass_spec._get_all_mz_peaks(self.dataset[chem_num], time, 1, [[(0,1000)]])[0][1] #TODO: Make MS1 range more general + if intensity > self.min_intensity: + first_scans[chem_num] = min(first_scans[chem_num], time) + last_scans[chem_num] = time + return first_scans, last_scans + # + # def _get_ms1_mzs(self): + # # get list of ms1s + # ms1_mzs = [] + # if isinstance(self.scans[1], list): + # for j in range(len(self.scans[1])): + # ms1_mzs.extend(self.scans[1][j].mzs) + # ms1_mzs = np.unique(np.array(ms1_mzs)) + # else: + # ms1_mzs = self.scans[1].mzs + # return ms1_mzs + + def _get_chemical_location(self, chem_num): + # find location where ms2s of chemical can be narrowed down to + which_scans = np.where(np.logical_and(np.array(self.ms2_scan_times) > self.first_scans[chem_num], + np.array(self.ms2_scan_times) < self.last_scans[chem_num])) + chemical_scans = np.array(self.scans[2])[which_scans] + if chemical_scans.size == 0: + possible_locations = [(0,1000)] # TODO: Make this more general + else: + locations = [scan.isolation_windows for scan in chemical_scans] + scan_times = [scan.rt for scan in chemical_scans] + split_points = np.unique(np.array(list(sum(sum(sum(locations, []), []), ())))) + split_points = np.unique(np.concatenate((split_points, self.ms1_range))) + mid_points = [(split_points[i] + split_points[i + 1]) / 2 for i in range(len(split_points) - 1)] + possible_mid_points = self._get_mid_points_in_location(chem_num, mid_points, locations, scan_times) + possible_locations = self._get_possible_locations(possible_mid_points, split_points) + return possible_locations + + def _get_mid_points_in_location(self, chem_num, mid_points, locations, scan_times): + # find mid points which satisfying scans locations + current_mid_points = mid_points + for i in range(len(locations)): + chem_scanned = isinstance( + self.controller.mass_spec._get_all_mz_peaks(self.dataset[chem_num], scan_times[i], 2, locations[i]), + list) + new_mid_points = [] + for j in range(len(current_mid_points)): + if chem_scanned == self._in_window(current_mid_points[j], locations[i]): + new_mid_points.append(current_mid_points[j]) + current_mid_points = new_mid_points + return current_mid_points + + def _get_possible_locations(self, possible_mid_points, split_points): + # find locations where possible mid points can be in, then simplify locations + possible_locations = [] + for i in range(len(possible_mid_points)): + min_location = max( + np.array(split_points)[np.where(np.array(split_points) < possible_mid_points[i])].tolist()) + max_location = min( + np.array(split_points)[np.where(np.array(split_points) >= possible_mid_points[i])].tolist()) + possible_locations.extend([(min_location, max_location)]) + # TODO: need to simplify still + return possible_locations + + def _in_window(self, mid_point, locations): + for window in locations[0]: + if (mid_point > window[0] and mid_point <= window[1]): + return True + return False + + +class RestrictedDiaAnalyser(object): + def __init__(self, controller): + self.entropy = [] + self.chemicals_identified = [] + self.ms2_matched = [] + self.scan_num = [] + temp_controller = copy.deepcopy(controller) + start = len(temp_controller.scans[2]) + for num_ms2_scans in range(start, -1, -1): + temp_controller.scans[2] = temp_controller.scans[2][0:num_ms2_scans] + analyser = DiaAnalyser(temp_controller) + self.entropy.append(analyser.entropy) + self.chemicals_identified.append(analyser.chemicals_identified) + self.ms2_matched.append(analyser.ms2_matched) + self.scan_num.append(num_ms2_scans + 1) + self.entropy.reverse() + self.chemicals_identified.reverse() + self.ms2_matched.reverse() + self.scan_num.reverse() + + + +############################# ok up to here ##################################### + + +class Scan_Results_Calculator(object): + """ + Method for taking raw results, grouping ms2 fragments and determining in which scans they were found + """ + + def __init__(self, dia_results, ms2_mz_slack=0.00001, ms2_intensity_slack=0.1): + self.intensities_in_scans = dia_results.intensities_in_scans + self.mz_in_scans = dia_results.mz_in_scans + self.locations = dia_results.locations + self.bin_walls = dia_results.bin_walls + self.ms1_values = dia_results.ms1_values + self.results = [[] for i in range(len(dia_results.locations))] + unlisted_mz_in_scans = np.concatenate(dia_results.mz_in_scans) + unlisted_intensities_in_scans = np.concatenate(dia_results.intensities_in_scans) + # find unique mz + unique_mz = [[unlisted_mz_in_scans[0]]] + unique_intensities = [[unlisted_intensities_in_scans[0]]] + for unlisted_mz_index in range(1, len(unlisted_mz_in_scans)): + unique_mz_min = math.inf + for unique_mz_index in range(len(unique_mz)): + unique_dist = abs( + sum(unique_mz[unique_mz_index]) / len(unique_mz[unique_mz_index]) - unlisted_mz_in_scans[ + unlisted_mz_index]) + if (unique_dist < unique_mz_min): + unique_mz_min = unique_dist + unique_mz_which = unique_mz_index + if unique_mz_min < ms2_mz_slack: + unique_mz[unique_mz_which].append(unlisted_mz_in_scans[unlisted_mz_index]) + unique_intensities[unique_mz_which].append(unlisted_intensities_in_scans[unlisted_mz_index]) + else: + unique_mz.append([unlisted_mz_in_scans[unlisted_mz_index]]) + unique_intensities.append([unlisted_intensities_in_scans[unlisted_mz_index]]) + self.ms2_intensities = unique_intensities + self.ms2_mz = unique_mz + # find where intensities are unique and assign them a scan result + for unique_mz_index in range(len(unique_mz)): + if max(abs(unique_intensities[0] - sum(unique_intensities[0]) / len( + unique_intensities[0]))) > ms2_intensity_slack: + print("not ready yet") + else: + for location_index in range(len(dia_results.locations)): + TF_in_location = [] + for unique_index in range(len(unique_mz[unique_mz_index])): + TF_in_location.append( + unique_mz[unique_mz_index][unique_index] in dia_results.mz_in_scans[location_index] and + unique_intensities[unique_mz_index][unique_index] in dia_results.intensities_in_scans[ + location_index]) + if any(TF_in_location): + self.results[location_index].append(1) + else: + self.results[location_index].append(0) + + +class Dia_Location_Finder(object): + """ + Method for finding location of ms2 fragments based on which DIA scans they are seen in + """ + + def __init__(self, scan_results): + self.locations = scan_results.locations + self.results = scan_results.results + self.bin_walls = scan_results.bin_walls + self.ms1_values = scan_results.ms1_values + self.ms2_intensities = scan_results.ms2_intensities + self.ms2_mz = scan_results.ms2_mz + self.location_all = [] + bin_mid_points = list((np.array(self.bin_walls[1:]) + np.array(self.bin_walls[:(len(self.bin_walls) - 1)])) / 2) + for sample_index in range(0, len(self.results[0])): + mid_point_TF = [] + for mid_points_index in range(0, len(bin_mid_points)): + mid_point_TF.append(self._mid_point_in_location(bin_mid_points[mid_points_index], sample_index)) + self.location_all.append([(list(np.array(self.bin_walls)[np.where(np.array(mid_point_TF) == True)])[0], + list(np.array(self.bin_walls[1:])[np.where(np.array(mid_point_TF) == True)])[ + 0])]) + + def _mid_point_in_location(self, mid_point, sample_index): + for locations_index in range(0, len(self.locations)): + if self._in_window(mid_point, self.locations[locations_index]) == True and self.results[locations_index][ + sample_index] == 0: + return False + if self._in_window(mid_point, self.locations[locations_index]) == False and self.results[locations_index][ + sample_index] == 1: + return False + else: + return True + + def _in_window(self, mid_point, locations): + for window in locations: + if (mid_point > window[0] and mid_point <= window[1]): + return True + return False + + +class Entropy(object): + """ + Method for calculating entropy based on locations of ms2 components + """ + + def __init__(self, dia_location_finder): + self.entropy = [] + self.components_determined = [] + ms1_vec = [] + ms2_vec = [] + for i in range(0, len(dia_location_finder.bin_walls) - 1): + ms2_vec.extend([0]) + ms1_vec.extend([len(np.where( + np.logical_and(np.array(dia_location_finder.ms1_values) > dia_location_finder.bin_walls[i], + np.array(dia_location_finder.ms1_values) <= dia_location_finder.bin_walls[i + 1]))[0])]) + # fix this + for j in range(0, len(dia_location_finder.location_all)): + if [(dia_location_finder.bin_walls[i], dia_location_finder.bin_walls[i + 1])] == \ + dia_location_finder.location_all[j]: + ms2_vec[i] += 1 + ms1_vec_nozero = [value for value in ms1_vec if value != 0] + ms2_vec_nozero = [value for value in ms1_vec if value != 0] + entropy_vec = [] + for j in range(0, len(ms2_vec_nozero)): + entropy_vec.append(-ms2_vec_nozero[j] * ms1_vec_nozero[j] * math.log(1 / ms1_vec_nozero[j])) + self.entropy = sum(entropy_vec) + self.components_determined = sum(np.extract(np.array(ms1_vec_nozero) == 1, ms2_vec_nozero)) + self.components = sum(ms2_vec_nozero) + + +class Entropy_List(object): + """ + Method for calculating entropy on multiple subsets of the DIA results. Useful for creating plots and monitoring performance over multiple scans + """ + + def __init__(self, dataset, ms_level, rt, dia_design, window_type, kaufmann_design, extra_bins=0, range_slack=0.01, + ms1_range=[(None, None)], num_windows=None, ms2_mz_slack=0.00001, ms2_intensity_slack=0.1): + self.entropy = [] + self.components_determined = [] + if (dia_design != "kaufmann"): + sys.exit("Only the 'kaufmann' method can be used with Entropy_List") + if (kaufmann_design == "tree"): + self.start_subsample_scans = 2 + self.end_subsample_scans = 7 + extra_bins + elif (kaufmann_design == "nested"): + self.start_subsample_scans = 8 + self.end_subsample_scans = 12 + extra_bins + else: + sys.exit("Cannot use Entropy_List with this design. Kaufmann 'nested' or 'tree' only.") + for i in range(self.start_subsample_scans, self.end_subsample_scans): + dia = Dia_Methods_Subsample( + Dia_Methods(dataset, ms_level, rt, dia_design, window_type, kaufmann_design, extra_bins, range_slack, + ms1_range, num_windows), i) + results = Entropy(Dia_Location_Finder(Scan_Results_Calculator(dia, ms2_mz_slack, ms2_intensity_slack))) + self.entropy.append(results.entropy) + self.components_determined.append(results.components_determined) + self.components = results.components + + + + +class Dia_Methods(object): + """ + Method for doing DIA on a dataset of ms1 and ms2 peaks. Creates windows and then return attributes of scan results + """ + + def __init__(self, dataset, ms_level, rt, dia_design, window_type, kaufmann_design=None, extra_bins=0, + range_slack=0.01, ms1_range=[(None, None)], num_windows=None): + dia_windows = Dia_Windows(dataset, dia_design, window_type, kaufmann_design, extra_bins, range_slack, ms1_range, + num_windows) + self.bin_walls = dia_windows.bin_walls + self.locations = dia_windows.locations + self.ms1_values = dia_windows.ms1_values + self.mz_in_scans = [] + self.intensities_in_scans = [] + for window_index in range(0, len(self.locations)): + data_scan = Dataset_Scan(dataset, ms_level, rt, self.locations[window_index]) + self.mz_in_scans.append(np.array(data_scan.mz_in_scan)) + self.intensities_in_scans.append(np.array(data_scan.scan_intensities)) + + +class Dia_Methods_Subsample(object): + """ + Method for taking a sumsample of DIA results. Helpful for visualising results as scans progress + """ + + def __init__(self, dia_methods, num_scans): + self.bin_walls = list(set(np.array(sum(dia_methods.locations[0:num_scans], [])).flatten())) + self.bin_walls.sort() + self.locations = dia_methods.locations[0:num_scans] + self.ms1_values = dia_methods.ms1_values + self.mz_in_scans = dia_methods.mz_in_scans[0:num_scans] + self.intensities_in_scans = dia_methods.intensities_in_scans[0:num_scans] \ No newline at end of file diff --git a/Synthetic data creation scripts/vimms/DataGenerator.py b/Synthetic data creation scripts/vimms/DataGenerator.py new file mode 100644 index 00000000..7d46525e --- /dev/null +++ b/Synthetic data creation scripts/vimms/DataGenerator.py @@ -0,0 +1,656 @@ +import copy +import glob +import os +import xml.etree.ElementTree + +import numpy as np +import pandas as pd +import pylab as plt +import pymzml +from sklearn.neighbors import KernelDensity +import zipfile + +from vimms.Chemicals import DatabaseCompound +from vimms.Common import LoggerMixin, MZ, INTENSITY, RT, N_PEAKS, SCAN_DURATION, MZ_INTENSITY_RT, save_obj +from vimms.MassSpec import Peak, Scan +from vimms.SpectralUtils import get_precursor_info + +import matplotlib.pyplot as plt + + +def extract_hmdb_metabolite(in_file, delete=True): + print('Extracting HMDB metabolites from %s' % in_file) + + # if out_file is zipped then extract the xml file inside + try: + # extract from zip file + zf = zipfile.ZipFile(in_file, 'r') + metabolite_xml_file = zf.namelist()[0] # assume there's only a single file inside the zip file + f = zf.open(metabolite_xml_file) + except zipfile.BadZipFile: # oops not a zip file + zf = None + f = in_file + + # loops through file and extract the necessary element text to create a DatabaseCompound + db = xml.etree.ElementTree.parse(f).getroot() + compounds = [] + prefix = '{http://www.hmdb.ca}' + for metabolite_element in db: + row = [None, None, None, None, None, None] + for element in metabolite_element: + if element.tag == (prefix + 'name'): + row[0] = element.text + elif element.tag == (prefix + 'chemical_formula'): + row[1] = element.text + elif element.tag == (prefix + 'monisotopic_molecular_weight'): + row[2] = element.text + elif element.tag == (prefix + 'smiles'): + row[3] = element.text + elif element.tag == (prefix + 'inchi'): + row[4] = element.text + elif element.tag == (prefix + 'inchikey'): + row[5] = element.text + + # if all fields are present, then add them as a DatabaseCompound + if None not in row: + compound = DatabaseCompound(row[0], row[1], row[2], row[3], row[4], row[5]) + compounds.append(compound) + print('Loaded %d DatabaseCompounds from %s' % (len(compounds), in_file)) + + f.close() + if zf is not None: + zf.close() + + if delete: + print('Deleting %s' % in_file) + os.remove(in_file) + + return compounds + + +def get_data_source(mzml_path, filename, xcms_output=None): + """ + Load a `DataSource` object that stores information on a set of .mzML files. + :param mzml_path: the location of .mzML files to train the KDEs. + :param filename: a particular .mzML file to be used. If None then all files in `mzml_path` will be used. + :param xcms_output: As an option, we can use XCMS peak picking results to train the (mz, RT, intensity) densities. + This makes the generated spectra more similar to real ones after peak picking. If not available, leave this as None. + :return: a DataSource object. + """ + ds = DataSource() + ds.load_data(mzml_path, filename) + if xcms_output is not None: + ds.load_xcms_output(xcms_output) + return ds + + +def get_spectral_feature_database(ds, filename, min_ms1_intensity, min_ms2_intensity, min_rt, max_rt, + bandwidth_mz_intensity_rt, bandwidth_n_peaks, out_file=None): + """ + Generate spectral feature database on the .mzML files that have been loaded into the DataSource + :param ds: the `DataSource` object that contains loaded .mzML files. + :param filename: a particular .mzML file to be used. If None then all loaded files in `ds` will be used. + :param min_ms1_intensity: minimum MS1 intensity to include a data point to train the KDEs. + :param min_ms2_intensity: minimum MS2 intensity to include a data point to train the KDEs. + :param min_rt: minimum RT to include a data point to train the KDEs. + :param max_rt: maximum RT to include a data point to train the KDEs. + :param bandwidth_mz_intensity_rt: the bandwidth of the kernel to train the KDEs for (mz, RT, intensity) values. + :param bandwidth_n_peaks: the bandwidth of the kernel to train the KDEs for the number of peaks per scan. + :param out_file: the resulting output file to store the trained KDEs (in form of `PeakSampler` object). + :return: a PeakSampler object that can be used to draw samples for simulation. + """ + ps = PeakSampler(ds, min_rt, max_rt, min_ms1_intensity, min_ms2_intensity, filename, False, + bandwidth_mz_intensity_rt, bandwidth_n_peaks) + if out_file is not None: + save_obj(ps, out_file) + return ps + + +def filter_df(df, min_ms1_intensity, rt_range, mz_range): + # filter by rt range + if rt_range is not None: + df = df[(df['rt'] > rt_range[0][0]) & (df['rt'] < rt_range[0][1])] + + # filter by mz range + if mz_range is not None: + df = df[(df['rt'] > mz_range[0][0]) & (df['rt'] < mz_range[0][1])] + + # filter by min intensity + intensity_col = 'maxo' + if min_ms1_intensity is not None: + df = df[(df[intensity_col] > min_ms1_intensity)] + return df + + +class DataSource(LoggerMixin): + """ + A class to load and extract centroided peaks from CSV and mzML files. + :param min_ms1_intensity: minimum ms1 intensity for filtering + :param min_ms2_intensity: maximum ms2 intensity for filtering + :param min_rt: minimum RT for filtering + :param max_rt: maximum RT for filtering + """ + + def __init__(self): + # A dictionary that stores the actual pymzml spectra for each filename + self.file_spectra = {} # key: filename, value: a dict where key is scan_number and value is spectrum + + # A dictionary to store the distribution on scan durations for each ms_level in each file + self.file_scan_durations = {} # key: filename, value: a dict with key ms level and value scan durations + + # A dictionary to store extracted MS2 scans + self.precursor_info = {} # key: filename, value: a dataframe of precursor info + + # pymzml parameters + self.ms1_precision = 5e-6 + self.obo_version = '4.0.1' + + # xcms peak picking results, if any + self.df = None + + def load_data(self, mzml_path, file_name=None): + """ + Loads data and generate peaks from mzML files. The resulting peak objects will not have chromatographic peak + shapes, because no peak picking has been performed yet. + :param mzml_path: the input folder containing the mzML files + :return: nothing, but the instance variable file_spectra and scan_durations are populated + """ + for filename in glob.glob(os.path.join(mzml_path, '*.mzML')): + fname = os.path.basename(filename) + if file_name is not None and fname != file_name: + continue + self.logger.info('Loading %s' % fname) + + # TODO: inefficient because we have to parse the mzML file multiple times + self.file_spectra[fname] = self.extract_all_scans(filename) + self.precursor_info[fname] = self.extract_precursor_info(filename) + self.file_scan_durations[fname] = self.extract_scan_durations(filename) + + def extract_all_scans(self, filename): + scans = {} + run = pymzml.run.Reader(filename, obo_version=self.obo_version, + MS1_Precision=self.ms1_precision, + extraAccessions=[('MS:1000016', ['value', 'unitName'])]) + for scan_no, scan in enumerate(run): + scans[scan_no] = scan + return scans + + def extract_precursor_info(self, filename): + df = get_precursor_info(filename) + return df + + def extract_scan_durations(self, filename): + transitions = { + (1, 1): [], + (1, 2): [], + (2, 1): [], + (2, 2): [] + } + run = pymzml.run.Reader(filename, obo_version=self.obo_version, + MS1_Precision=self.ms1_precision, + extraAccessions=[('MS:1000016', ['value', 'unitName'])]) + for scan_no, scan in enumerate(run): + if scan_no == 0: + previous_level = scan['ms level'] + old_rt = self._get_rt(scan) + continue + rt = self._get_rt(scan) + current_level = scan['ms level'] + previous_duration = rt - old_rt + transitions[(previous_level, current_level)].append(previous_duration) + previous_level = current_level + old_rt = rt + return transitions + + def load_xcms_output(self, xcms_filename): + self.df = pd.read_csv(xcms_filename) + + def plot_data(self, file_name, ms_level=1, min_rt=None, max_rt=None, max_data=100000): + data_types = [MZ, INTENSITY, RT, N_PEAKS, SCAN_DURATION] + for data_type in data_types: + if data_type == SCAN_DURATION: + X = self.get_scan_durations(file_name) + self.plot_histogram(X, data_type) + elif data_type == N_PEAKS: + X = self.get_n_peaks(file_name, ms_level, min_rt=min_rt, max_rt=max_rt) + else: + X = self.get_data(data_type, file_name, ms_level, min_rt=min_rt, max_rt=max_rt, max_data=max_data) + if data_type == INTENSITY: + X = np.log(X) + self.plot_histogram(X, data_type) + self.plot_boxplot(X, data_type) + + def plot_histogram(self, X, data_type, n_bins=100): + """ + Makes a histogram plot on the distribution of the item of interest + :param X: a numpy array + :param bins: number of histogram bins + :return: nothing. A plot is shown. + """ + if data_type == SCAN_DURATION: + rt_steps = X + for key, rt_list in rt_steps.items(): + try: + bins = np.linspace(min(rt_list), max(rt_list), n_bins) + plt.figure() + plt.hist(rt_list, bins=bins) + plt.title(key) + plt.show() + except ValueError: + continue + else: + plt.figure() + _ = plt.hist(X, bins=n_bins) + plt.plot(X[:, 0], np.full(X.shape[0], -0.01), '|k') + plt.title('Histogram for %s -- shape %s' % (data_type, str(X.shape))) + plt.show() + + def plot_boxplot(self, X, data_type): + """ + Makes a boxplot on the distribution of the item of interest + :param X: a numpy array + :return: nothing. A plot is shown. + """ + plt.figure() + _ = plt.boxplot(X) + plt.title('Boxplot for %s -- shape %s' % (data_type, str(X.shape))) + plt.show() + + def plot_peak(self, peak): + f, axarr = plt.subplots(2, sharex=True) + axarr[0].plot(peak.rt_values, peak.intensity_values) + axarr[1].plot(peak.rt_values, peak.mz_values, linestyle='None', marker='o', markersize=1.0, color='b') + + def get_data(self, data_type, filename, ms_level, min_intensity=None, + min_rt=None, max_rt=None, log=False, max_data=100000): + """ + Retrieves values as numpy array + :param data_type: data_type is 'mz', 'rt', 'intensity' or 'n_peaks' + :param filename: the mzml filename or None for all files + :param ms_level: level 1 or 2 + :param min_intensity: minimum ms2 intensity for thresholding + :param min_rt: minimum RT value for thresholding + :param max_rt: max RT value for thresholding + :param log: if true, the returned values will be logged + :return: an Nx1 numpy array of all the values requested + """ + # if xcms peak picking results are provided, use that instead + if ms_level == 1 and self.df is not None: + self.logger.info('Using values from XCMS peaklist') + + # remove rows in the peak picked dataframe that are outside threshold values + df = filter_df(self.df, min_intensity, [[min_rt, max_rt]], None) + + # extract the values we need + if data_type == MZ: + X = df['mz'].values + elif data_type == RT: + # we use rt for the starting value for the chemical to elute + X = df['rt'].values + elif data_type == INTENSITY: + X = df['maxo'].values + elif data_type == MZ_INTENSITY_RT: + X = df[['mz', 'maxo', 'rt']].values + + else: # else we get the values by reading from the scans in mzML files directly + self.logger.info('Using values from scans') + + # get spectra from either one file or all files + if filename is None: # use all spectra + all_spectra = [] + for f in self.file_spectra: + spectra_for_f = list(self.file_spectra[f].values()) + all_spectra.extend(spectra_for_f) + else: # use spectra for that file only + all_spectra = self.file_spectra[filename].values() + + # loop through spectrum and get all peaks above threshold + values = [] + for spectrum in all_spectra: + # if wrong ms level, skip this spectrum + if spectrum.ms_level != ms_level: + continue + + # collect all valid Peak objects in a spectrum + spectrum_peaks = [] + for mz, intensity in spectrum.peaks('raw'): + rt = self._get_rt(spectrum) + p = Peak(mz, rt, intensity, spectrum.ms_level) + if self._valid_peak(p, min_intensity, min_rt, max_rt): + spectrum_peaks.append(p) + + if data_type == MZ_INTENSITY_RT: # used when fitting m/z, rt and intensity together for the manuscript + mzs = list(getattr(x, MZ) for x in spectrum_peaks) + intensities = list(getattr(x, INTENSITY) for x in spectrum_peaks) + rts = list(getattr(x, RT) for x in spectrum_peaks) + values.extend(list(zip(mzs, intensities, rts))) + + else: # MZ, INTENSITY or RT separately + attrs = list(getattr(x, data_type) for x in spectrum_peaks) + values.extend(attrs) + + X = np.array(values) + + # log-transform if necessary + if log: + if data_type == MZ_INTENSITY_RT: # just log the intensity part + X[:, 1] = np.log(X[:, 1]) + else: + X = np.log(X) + + # pick random samples + try: + idx = np.arange(len(X)) + rnd_idx = np.random.choice(idx, size=int(max_data), replace=False) + sampled_X = X[rnd_idx] + except ValueError: + sampled_X = X + + # return values + if data_type == MZ_INTENSITY_RT: + return sampled_X # it's already a Nx2 or Nx3 array + else: + # convert into Nx1 array + return sampled_X[:, np.newaxis] + + def get_n_peaks(self, filename, ms_level, min_intensity=None, min_rt=None, max_rt=None): + # get spectra from either one file or all files + if filename is None: # use all spectra + all_spectra = [] + for f in self.file_spectra: + spectra_for_f = list(self.file_spectra[f].values()) + all_spectra.extend(spectra_for_f) + else: # use spectra for that file only + all_spectra = self.file_spectra[filename].values() + + # loop through spectrum and get all peaks above threshold + values = [] + for spectrum in all_spectra: + # if wrong ms level, skip this spectrum + if spectrum.ms_level != ms_level: + continue + + # collect all valid Peak objects in a spectrum + spectrum_peaks = [] + for mz, intensity in spectrum.peaks('raw'): + rt = self._get_rt(spectrum) + p = Peak(mz, rt, intensity, spectrum.ms_level) + if self._valid_peak(p, min_intensity, min_rt, max_rt): + spectrum_peaks.append(p) + + # collect the data points we need into a list + n_peaks = len(spectrum_peaks) + if n_peaks > 0: + values.append(n_peaks) + + # convert into Nx1 array + X = np.array(values) + return X[:, np.newaxis] + + def get_scan_durations(self, fname): + if fname is None: # if no filename, then combine all the dictionary keys + combined = None + for f in self.file_scan_durations: + if combined is None: # copy first one + combined = copy.deepcopy(self.file_scan_durations[f]) + else: # and extend with the subsequent ones + for key in combined: + combined[key].extend(self.file_scan_durations[f][key]) + else: + combined = self.file_scan_durations[fname] + return combined + + def _get_rt(self, spectrum): + rt, units = spectrum.scan_time + if units == 'minute': + rt *= 60.0 + return rt + + def _valid_peak(self, peak, min_intensity, min_rt, max_rt): + if min_intensity is not None and peak.intensity < min_intensity: + return False + elif min_rt is not None and peak.rt < min_rt: + return False + elif max_rt is not None and peak.rt > max_rt: + return False + else: + return True + + +class PeakSampler(LoggerMixin): + """A class to sample peaks from a trained density estimator""" + + # TODO: add min intensity threshold here so we don't store everything??!!! + def __init__(self, data_source, min_rt, max_rt, min_ms1_intensity, min_ms2_intensity, + filename=None, plot=False, + bandwidth_mz_intensity_rt=1.0, bandwidth_n_peaks=1.0, filename_to_N_DEW=None): + self.min_rt = min_rt + self.max_rt = max_rt + self.min_ms1_intensity = min_ms1_intensity + self.min_ms2_intensity = min_ms2_intensity + self.filename = filename + self.plot = plot + self.filename_to_N_DEW = filename_to_N_DEW # a dictionary that maps from filename to (N, DEW) + + # get all the scan dataframes across all files and combine them all + self.all_ms2_scans = self._extract_ms2_scans(data_source) + self.logger.debug('Extracted %d MS2 scans' % len(self.all_ms2_scans)) + + # compute sum(ms2 peak intensities) / ms1.intensity + self.intensity_props = self._compute_intensity_props() + + # extract scan durations + self.file_scan_durations = {} # key: (N, DEW), value: a list of scan durations for (N, DEW) + if filename_to_N_DEW is None: + # no mapping between filename to N is specified, so we just assign it a default key of 0 + self.logger.debug('Extracting scan durations') + N_DEW = (0, 0) # default value if not specified + self.file_scan_durations[N_DEW] = data_source.get_scan_durations(filename) + else: + # store the scan durations for the different Ns + for filename, v in filename_to_N_DEW.items(): + N, DEW = v + self.logger.debug('Extracting scan durations for N=%d DEW=%d from %s' % (N, DEW, filename)) + self.file_scan_durations[v] = data_source.get_scan_durations(filename) + + # train KDEs for each ms-level + max_data = 100000 + self.kdes = {} + self.kernel = 'gaussian' + self._kde(data_source, filename, 1, bandwidth_mz_intensity_rt, bandwidth_n_peaks, max_data) + try: # exceptions if data_source only contains fullscan data but we try to train kde on ms level 2 + self._kde(data_source, filename, 2, bandwidth_mz_intensity_rt, bandwidth_n_peaks, max_data) + except ValueError: + pass + except IndexError: + pass + + #################################################################################################################### + # Public methods + #################################################################################################################### + + def scan_durations(self, previous_level, current_level, n_sample, N, DEW): + # the scan durations is stored for each N and DEW combination + file_scan_durations = self.file_scan_durations[(N, DEW)] + key = (previous_level, current_level,) + values = file_scan_durations[key] + try: + return np.random.choice(values, replace=False, size=n_sample) + except ValueError: + return np.array([]) + + def get_peak(self, ms_level, N=None, min_mz=None, max_mz=None, min_rt=None, max_rt=None, min_intensity=None): + if N is None: + N = max(self.n_peaks(ms_level, 1).astype(int)[0][0], 0) + + peaks = [] + while len(peaks) < N: + vals = self.sample(ms_level, 1) + intensity = np.exp(vals[0, 1]) + mz = vals[0, 0] + rt = vals[0, 2] + p = Peak(mz, rt, intensity, ms_level) + if self._is_valid(p, min_mz, max_mz, min_rt, max_rt, min_intensity): # othwerise we just keep rejecting + peaks.append(p) + return peaks + + def sample(self, ms_level, n_sample): + vals = self.kdes[(MZ_INTENSITY_RT, ms_level)].sample(n_sample) + return vals + + def n_peaks(self, ms_level, n_sample): + return self.kdes[(N_PEAKS, ms_level)].sample(n_sample) + + def get_ms2_spectra(self, N=1): + spectra = [] + total = len(self.all_ms2_scans) + + if total > 0: + # select N random spectra + idx = np.random.choice(total, replace=False, size=N) + samples = self.all_ms2_scans.iloc[idx, :] + + # convert to Scan objects + for idx, row in samples.iterrows(): + # create precursor (MS1) peak + parent_ms_level = 1 + parent_mz = row['ms1_mz'] + parent_rt = row['ms1_scan_rt'] + parent_intensity = row['ms1_intensity'] + parent_peak = Peak(parent_mz, parent_rt, parent_intensity, parent_ms_level) + + # create MS2 scan + ms_level = 2 + ms2_peaks = row['ms2_peaklist'] + ms2_scan_id = idx + ms2_mzs = ms2_peaks[:, 0] + ms2_rt = ms2_peaks[0, 1] # all the values are the same, so we can take the first one + ms2_intensities = ms2_peaks[:, 2] # TODO: filter by min_ms2_intensity here + ms2_scan = Scan(ms2_scan_id, ms2_mzs, ms2_intensities, ms_level, ms2_rt, parent=parent_peak) + spectra.append(ms2_scan) + return spectra + + def get_noise_sample(self): + # TODO: finish this + # need to choose number of noise fragments from get_num_noisy_samples() below + # then draw n noise fragments + # returns list of ms2 noise fragments. type = MSN + # noise fragment here is defined as ms2 peaks below some intensity threshold + return [] + + def get_num_noisy_samples(self): + # TODO: finish this + # returns a distribution of the number of noise fragments + return 0.0 + + def get_msn_noisy_intensity(self, intensity, ms_level, max_intensity, pct): + # CJ: for now, we will just be doing it where we randomly take a percentage of the + # max intensity... so we will need to have two extra arguments here + # TODO: until we characterise the noise properly, just return the original value for now + # takes intensity + # adds noise, but ensures its positive value + # returns list with one numeric value + # ignores ms_level for now + assert 0 <= pct and pct <= 100, f"pct value must be on the range of [0, 100], {pct} is invalid." + + mult = (np.random.rand()*float(pct)) / float(100.) + noise = max_intensity * mult + + return intensity + noise + + def get_msn_noisy_mz(self, mz, ms_level): + # TODO: finish this + # same as above, but for m/z + # Simon: We can characterise mz noise from the chromatographic peaks we extract. + # I suggest a constant variance for now, but we might want to fit models where we account for + # variability in noise variance as a function of mz itself, and intensity. + return mz + + def get_parent_intensity_proportion(self, N=1): + # this is the proportion of all fragment intensities in a spectra over the parent intensity + # returns number between 0 and 1 + if len(self.all_ms2_scans) > 0: + return np.random.choice(self.intensity_props, replace=False, size=N) + return None + + #################################################################################################################### + # Private methods used in the constructor + #################################################################################################################### + + def _extract_ms2_scans(self, data_source): + combined_dfs = pd.concat(data_source.precursor_info.values()) + # select only the column we need + # 'ms2_peaklist' is a 2d-array, where each row is an ms2 peak, and columns are mz, rt, intensity + col_names = ['ms1_mz', 'ms1_scan_rt', 'ms1_intensity', 'ms2_peaklist'] + return combined_dfs[col_names] + + def _compute_intensity_props(self): + self.logger.debug('Computing parent intensity proportions') + intensity_props = [] + for idx, row in self.all_ms2_scans.iterrows(): + parent_intensity = row['ms1_intensity'] + ms2_peaks = row['ms2_peaklist'] + ms2_intensities = ms2_peaks[:, 2] + prop = np.sum(ms2_intensities) / parent_intensity + if prop <= 1: + intensity_props.append(prop) + return np.array(intensity_props) + + def _kde(self, data_source, filename, ms_level, bandwidth_mz_intensity_rt, bandwidth_n_peaks, max_data): + self.logger.debug('Training KDEs for ms_level=%d' % ms_level) + params = [ + {'data_type': MZ_INTENSITY_RT, 'bandwidth': bandwidth_mz_intensity_rt}, + {'data_type': N_PEAKS, 'bandwidth': bandwidth_n_peaks} + ] + + for param in params: + data_type = param['data_type'] + min_intensity = self.min_ms1_intensity if ms_level == 1 else self.min_ms2_intensity + + # get data + self.logger.debug('Retrieving %s values from %s' % (data_type, data_source)) + if data_type == N_PEAKS: + X = data_source.get_n_peaks(filename, ms_level, min_intensity=min_intensity, + min_rt=self.min_rt, max_rt=self.max_rt) + else: + log = True if data_type == MZ_INTENSITY_RT else False + X = data_source.get_data(data_type, filename, ms_level, min_intensity=min_intensity, + min_rt=self.min_rt, max_rt=self.max_rt, log=log, max_data=max_data) + + # fit kde + bandwidth = param['bandwidth'] + kde = KernelDensity(kernel=self.kernel, bandwidth=bandwidth).fit(X) + self.kdes[(data_type, ms_level)] = kde + + # plot if necessary + self._plot(kde, X, data_type, filename, bandwidth) + + def _is_valid(self, peak, min_mz, max_mz, min_rt, max_rt, min_intensity): + if peak.intensity < 0: + return False + if min_mz is not None and min_mz > peak.mz: + return False + if max_mz is not None and max_mz < peak.mz: + return False + if min_rt is not None and min_rt > peak.rt: + return False + if max_rt is not None and max_rt < peak.rt: + return False + if min_intensity is not None and min_intensity > peak.intensity: + return False + return True + + def _plot(self, kde, X, data_type, filename, bandwidth): + if self.plot: + if data_type == MZ_INTENSITY_RT: + self.logger.debug('3D plotting for %s not implemented' % MZ_INTENSITY_RT) + else: + fname = 'All' if filename is None else filename + title = '%s density estimation for %s - bandwidth %.3f' % (data_type, fname, bandwidth) + X_plot = np.linspace(np.min(X), np.max(X), 1000)[:, np.newaxis] + log_dens = kde.score_samples(X_plot) + plt.figure() + plt.fill_between(X_plot[:, 0], np.exp(log_dens), alpha=0.5) + plt.plot(X[:, 0], np.full(X.shape[0], -0.01), '|k') + plt.title(title) + plt.show() diff --git a/Synthetic data creation scripts/vimms/DsDA.py b/Synthetic data creation scripts/vimms/DsDA.py new file mode 100644 index 00000000..18c89560 --- /dev/null +++ b/Synthetic data creation scripts/vimms/DsDA.py @@ -0,0 +1,112 @@ +import glob +import os + +import numpy as np +import pandas as pd + +from vimms.Common import load_obj + + +def get_schedule(n, schedule_dir): + while True: + files = sorted(glob.glob(os.path.join(schedule_dir, '*.csv'))) + if len(files) == n: + last_file = files[-1] + try: + schedule = pd.read_csv(last_file) + if schedule.shape[0] == 11951: + print("Schedule Found") + return last_file + except: + pass + + +def fragmentation_performance_chemicals(controller_directory, min_acceptable_intensity, controller_file_spec="*.p"): + global total_matched_chemicals + os.chdir(controller_directory) + file_names = glob.glob(controller_file_spec) + n_samples = len(file_names) + controllers = [] + all_chemicals = [] + for controller_index in range(n_samples): + controller = load_obj(file_names[controller_index]) + controllers.append(controller) + all_chemicals.extend(controller.mass_spec.chemicals) + all_rts = [chem.rt for chem in all_chemicals] + chemicals_found_total = np.unique(all_rts) + sample_chemical_start_rts = [[] for i in range(n_samples)] + sample_chemical_start_rts_total = [] + for i in range(n_samples): + for event in controllers[i].mass_spec.fragmentation_events: + if event.ms_level == 2: + if controllers[i].mass_spec._get_intensity(event.chem, event.query_rt, 0, + 0) > min_acceptable_intensity: + sample_chemical_start_rts[i].append(event.chem.rt) + sample_chemical_start_rts[i] = np.unique(np.array(sample_chemical_start_rts[i])).tolist() + # at this point we have collected the RTs of the all the chemicals that + # have been fragmented above the min_intensity threshold + flatten_rts = [] + for l in sample_chemical_start_rts[0:(i + 1)]: + flatten_rts.extend(l) + sample_chemical_start_rts_total.append(len(np.unique(np.array(flatten_rts)))) + total_matched_chemicals = sample_chemical_start_rts_total + print("Completed Controller", i + 1) + return chemicals_found_total, total_matched_chemicals + + +def create_frag_dicts(controller_directory, aligned_chemicals_location, min_acceptable_intensity, + controller_file_spec="*.p"): + os.chdir(controller_directory) + file_names = glob.glob(controller_file_spec) + params = [] + for controller_index in range(len(file_names)): + params.append({ + 'controller_directory': controller_directory + file_names[controller_index], + 'min_acceptable_intensity': min_acceptable_intensity, + 'aligned_chemicals_location': aligned_chemicals_location + }) + return params + + +def fragmentation_performance_aligned(param_dict): + controller = load_obj(param_dict["controller_directory"]) + min_acceptable_intensity = param_dict["min_acceptable_intensity"] + aligned_chemicals = pd.read_csv(param_dict["aligned_chemicals_location"]) + n_chemicals_aligned = len(aligned_chemicals["mzmed"]) + chemicals_found = 0 + + events = np.array([event for event in controller.mass_spec.fragmentation_events if event.ms_level == 2]) + event_query_rts = np.array([event.query_rt for event in events]) + event_query_mzs = np.array([controller.mass_spec._get_mz(event.chem, event.query_rt, 0, 0) for event in events]) + + chemicals_found = [0 for i in range(n_chemicals_aligned)] + + for aligned_index in range(n_chemicals_aligned): + + rtmin = aligned_chemicals['peak_rtmin'][aligned_index] + rtmax = aligned_chemicals['peak_rtmax'][aligned_index] + mzmin = aligned_chemicals['peak_mzmin'][aligned_index] + mzmax = aligned_chemicals['peak_mzmax'][aligned_index] + rtmin_check = event_query_rts > rtmin + rtmax_check = event_query_rts < rtmax + mzmin_check = event_query_mzs > mzmin + mzmax_check = event_query_mzs < mzmax + idx = np.nonzero(rtmin_check & rtmax_check & mzmin_check & mzmax_check)[0] + + for i in idx: + event = events[i] + inten = controller.mass_spec._get_intensity(event.chem, event.query_rt, 0, 0) + if inten > min_acceptable_intensity: + chemicals_found[aligned_index] = 1 + break + return chemicals_found + + +def multi_sample_fragmentation_performance_aligned(params): + chemicals_found_multi = np.array(list(map(fragmentation_performance_aligned, params))) + total_chemicals_found = [] + + for i in range(len(chemicals_found_multi)): + total_chemicals_found.append((chemicals_found_multi[0:(1 + i)].sum(axis=0) > 0).sum()) + + return total_chemicals_found diff --git a/Synthetic data creation scripts/vimms/Evaluation.py b/Synthetic data creation scripts/vimms/Evaluation.py new file mode 100644 index 00000000..bae247e7 --- /dev/null +++ b/Synthetic data creation scripts/vimms/Evaluation.py @@ -0,0 +1,58 @@ +from vimms.PlotsForPaper import compute_performance_scenario_2 + + +class Evaluation(object): + """ + A class to compute performance evaluation + """ + + def __init__(self, controller, evaluation_strategy): + """ + Initialises an evaluation object + :param controller: a controller + :param evaluation_strategy: a strategy to evaluate performance + """ + self.controller = controller + self.strategy = evaluation_strategy + + def compute_performance(self): + """ + Evaluates a controller performance based on the provided evaluation strategy + :return: performance values + """ + return self.strategy.evaluate(self.controller) + + +class TopNEvaluationStrategy(object): + """ + A class to compute Top-N performance acccording to Section 3.3 of the paper + """ + + def __init__(self, **params): + """ + Evaluation parameters + :param params: multiple keyword arguments of parameters that we need + + For Top-N these parameters are: + - dataset = the list of chemicals to evaluate performance on + - min_ms1_intensity = minimum MS1 intensity to fragment + - fullscan_peaks_df = a dataframe of XCMS peak-picking result on the full-scan file + - fragmentation_peaks_df = a dataframe of XCMS peak-picking result on the fragmentation file + - fullscan_filename = an optional filename to filter fullscan_peaks_df, otherwise None + - fragfile_filename = an optional filename to filter fragmentation_peaks_df, otherwise None + - matching_mz_tol = matching tolerance (in ppm) to match peaks + - matching_rt_tol = matching tolerance (in seconds) to match peaks + """ + self.params = params + + def evaluate(self, controller): + return compute_performance_scenario_2(controller, + self.params['dataset'], + self.params['min_ms1_intensity'], + self.params['fullscan_filename'], + self.params['fragfile_filename'], + self.params['fullscan_peaks_df'], + self.params['fragmentation_peaks_df'], + self.params['matching_mz_tol'], + self.params['matching_rt_tol'], + chem_to_frag_events=None) diff --git a/Synthetic data creation scripts/vimms/MassSpec.py b/Synthetic data creation scripts/vimms/MassSpec.py new file mode 100644 index 00000000..f77829ee --- /dev/null +++ b/Synthetic data creation scripts/vimms/MassSpec.py @@ -0,0 +1,769 @@ +import math +from collections import defaultdict +from collections import namedtuple + +import numpy as np +import pandas as pd +from events import Events + +from vimms.Common import LoggerMixin, adduct_transformation +import matplotlib.pyplot as plt + +class Peak(object): + """ + A class to represent an empirical or sampled scan-level peak object + """ + + def __init__(self, mz, rt, intensity, ms_level): + """ + Creates a peak object + :param mz: mass-to-charge value + :param rt: retention time value + :param intensity: intensity value + :param ms_level: MS level + """ + self.mz = mz + self.rt = rt + self.intensity = intensity + self.ms_level = ms_level + + def __repr__(self): + return 'Peak mz=%.4f rt=%.2f intensity=%.2f ms_level=%d' % (self.mz, self.rt, self.intensity, self.ms_level) + + def __eq__(self, other): + if not isinstance(other, Peak): + # don't attempt to compare against unrelated types + return NotImplemented + + return math.isclose(self.mz, other.mz) and \ + math.isclose(self.rt, other.rt) and \ + math.isclose(self.intensity, other.intensity) and \ + self.ms_level == other.ms_level + + +class Scan(object): + """ + A class to store scan information + """ + + def __init__(self, scan_id, mzs, intensities, ms_level, rt, scan_duration=None, isolation_windows=None, + parent=None): + """ + Creates a scan + :param scan_id: current scan id + :param mzs: an array of mz values + :param intensities: an array of intensity values + :param ms_level: the ms level of this scan + :param rt: the retention time of this scan + :param scan_duration: how long this scan takes, if known. + :param isolation_windows: the window to isolate precursor peak, if known + :param parent: parent precursor peak, if known + """ + assert len(mzs) == len(intensities) + self.scan_id = scan_id + + # ensure that mzs and intensites are sorted by their mz values + p = mzs.argsort() + self.mzs = mzs[p] + self.intensities = intensities[p] + + self.ms_level = ms_level + self.rt = rt + self.num_peaks = len(mzs) + + self.scan_duration = scan_duration + self.isolation_windows = isolation_windows + self.parent = parent + + def __repr__(self): + return 'Scan %d num_peaks=%d rt=%.2f ms_level=%d' % (self.scan_id, self.num_peaks, self.rt, self.ms_level) + + # TODO maybe add the noise here? + +class ScanParameters(object): + """ + A class to store parameters used to instruct the mass spec how to generate a scan. + This object is usually created by the controllers. + """ + + # possible scan parameter names + MS_LEVEL = 'ms_level' + ISOLATION_WINDOWS = 'isolation_windows' + PRECURSOR = 'precursor' + DYNAMIC_EXCLUSION_MZ_TOL = 'mz_tol' + DYNAMIC_EXCLUSION_RT_TOL = 'rt_tol' + TIME = 'time' + N = 'N' + + def __init__(self): + """ + Creates a scan parameter object + """ + self.params = {} + + def set(self, key, value): + """ + Sets scan parameter value + :param key: a scan parameter name + :param value: a scan parameter value + :return: + """ + self.params[key] = value + + def get(self, key): + """ + Gets scan parameter value + :param key: + :return: + """ + if key in self.params: + return self.params[key] + else: + return None + + def __repr__(self): + return 'ScanParameters %s' % (self.params) + + +class FragmentationEvent(object): + """ + A class to store fragmentation events. Mostly used for benchmarking purpose. + """ + + def __init__(self, chem, query_rt, ms_level, peaks, scan_id): + """ + Creates a fragmentation event + :param chem: the chemical that were fragmented + :param query_rt: the time when fragmentation occurs + :param ms_level: MS level of fragmentation + :param peaks: the set of peaks produced during the fragmentation event + :param scan_id: the scan id linked to this fragmentation event + """ + self.chem = chem + self.query_rt = query_rt + self.ms_level = ms_level + self.peaks = peaks + self.scan_id = scan_id + + def __repr__(self): + return 'MS%d FragmentationEvent for %s at %f' % (self.ms_level, self.chem, self.query_rt) + + +class ExclusionItem(object): + """ + A class to store the item to exclude when computing dynamic exclusion window + """ + + def __init__(self, from_mz, to_mz, from_rt, to_rt): + """ + Creates a dynamic exclusion item + :param from_mz: m/z lower bounding box + :param to_mz: m/z upper bounding box + :param from_rt: RT lower bounding box + :param to_rt: RT upper bounding box + """ + self.from_mz = from_mz + self.to_mz = to_mz + self.from_rt = from_rt + self.to_rt = to_rt + + +class IndependentMassSpectrometer(LoggerMixin): + """ + A class that represents (synchronous) mass spectrometry process. + Independent here refers to how the intensity of each peak in a scan is independent of each other + i.e. there's no ion supression effect. + """ + MS_SCAN_ARRIVED = 'MsScanArrived' + ACQUISITION_STREAM_OPENING = 'AcquisitionStreamOpening' + ACQUISITION_STREAM_CLOSING = 'AcquisitionStreamClosing' + + def __init__(self, ionisation_mode, chemicals, peak_sampler, sig, + schedule_file=None, add_noise=False, dynamic_exclusion=True): + """ + Creates a mass spec object. + :param ionisation_mode: POSITIVE or NEGATIVE + :param chemicals: a list of Chemical objects in the dataset + :param peak_sampler: an instance of DataGenerator.PeakSampler object + :param schedule_file: path to schedule (CSV) file in DsDA format + :param add_noise: a flag to indicate whether to add noise + :param dynamic_exclusion: a flag to indicate whether to perform dynamic exclusion + """ + self.peak_recorder = defaultdict(list) # Added by CJ... helps keep track of peaks + self.grp = 0 + # current scan index, internal time and schedule file if provided + self.idx = 0 + self.time = 0 + self.schedule_file = schedule_file + if self.schedule_file is not None: + self.schedule = pd.read_csv(schedule_file) + self.sig = sig + # current task queue + self.processing_queue = [] + self.repeating_scan_parameters = None + + # the events here follows IAPI events + self.events = Events((self.MS_SCAN_ARRIVED, self.ACQUISITION_STREAM_OPENING, self.ACQUISITION_STREAM_CLOSING,)) + self.event_dict = { + self.MS_SCAN_ARRIVED: self.events.MsScanArrived, + self.ACQUISITION_STREAM_OPENING: self.events.AcquisitionStreamOpening, + self.ACQUISITION_STREAM_CLOSING: self.events.AcquisitionStreamClosing + } + + # the list of all chemicals in the dataset + self.chemicals = chemicals + self.ionisation_mode = ionisation_mode # currently unused + + # stores the chromatograms start and end rt for quick retrieval + chem_rts = np.array([chem.rt for chem in self.chemicals]) + self.chrom_min_rts = np.array([chem.chromatogram.min_rt for chem in self.chemicals]) + chem_rts + self.chrom_max_rts = np.array([chem.chromatogram.max_rt for chem in self.chemicals]) + chem_rts + + # here's where we store all the stuff to sample from + self.peak_sampler = peak_sampler + + # required to sample for different scan durations based on (N, DEW) in the hybrid controller + self.current_N = 0 + self.current_DEW = 0 + + # stores the mapping between precursor peak to ms2 scans + self.precursor_information = defaultdict(list) # key: Precursor object, value: ms2 scans + self.add_noise = add_noise # whether to add noise to the generated fragment peaks + self.fragmentation_events = [] # which chemicals produce which peaks + + # for dynamic exclusion window + self.dynamic_exclusion = dynamic_exclusion + self.exclusion_list = [] # a list of ExclusionItem + self.noise_level_ = 0 + + #################################################################################################################### + # Public methods + #################################################################################################################### + def noise_level( self, nl ): + self.noise_level_ = nl + + def set_group( self, grp ): + self.grp = grp + + def run(self, min_time, max_time, pbar=None): + """ + Simulates running the mass spec from min_time to max_time + :param min_time: start time + :param max_time: end time + :param pbar: progress bar + :return: None + """ + max_time = self._init_time(max_time, min_time) + self.fire_event(IndependentMassSpectrometer.ACQUISITION_STREAM_OPENING) + + try: + while self.time < max_time: + + # get scan param from the processing queue and do one scan + param = self._get_param() + scan = self._get_scan(self.time, param) + + # notify the controller that a new scan has been generated + # at this point, the MS_SCAN_ARRIVED event handler in the controller is called + # and the processing queue will be updated with new sets of scan parameters to do + self.fire_event(self.MS_SCAN_ARRIVED, scan) + + # sample scan duration and increase internal time + current_level = scan.ms_level + current_N = self.current_N + current_DEW = self.current_DEW + try: + next_scan_param = self.get_processing_queue()[0] + except IndexError: + next_scan_param = None + current_scan_duration = self._increase_time(current_level, current_N, current_DEW, + next_scan_param) + scan.scan_duration = current_scan_duration + + # add precursor and DEW information based on the current scan produced + # the DEW list update must be done after time has been increased + self._add_precursor_info(param, scan) + if self.dynamic_exclusion: + self._manage_dynamic_exclusion_list(param, scan) + + # stores the updated value of N and DEW + self._store_next_N_DEW(next_scan_param) + + # update progress bar + self._update_progress_bar(current_scan_duration, pbar, scan) + finally: + self.fire_event(IndependentMassSpectrometer.ACQUISITION_STREAM_CLOSING) + if pbar is not None: + pbar.close() + + def get_processing_queue(self): + """ + Returns the current processing queue + :return: + """ + return self.processing_queue + + def add_to_processing_queue(self, param): + """ + Adds a new scan parameters to the processing queue of scan parameters. Usually done by the controllers. + :param param: the scan parameters to add + :return: None + """ + self.processing_queue.append(param) + + def disable_repeating_scan(self): + """ + Disable repeating scan + :return: None + """ + self.set_repeating_scan(None) + + def set_repeating_scan(self, params): + """ + Sets the parameters for the default repeating scans that will be done when the processing queue is empty. + :param params: + :return: + """ + self.repeating_scan_parameters = params + + def reset(self): + """ + Resets the mass spec state so we can reuse it again + :return: None + """ + for key in self.event_dict: # clear event handlers + self.clear(key) + self.time = 0 + self.idx = 0 + self.processing_queue = [] + self.repeating_scan_parameters = None + self.current_N = 0 + self.current_DEW = 0 + self.precursor_information = defaultdict(list) + self.fragmentation_events = [] + self.exclusion_list = [] + + def is_excluded(self, mz, rt): + """ + Checks if a pair of (mz, rt) value is currently excluded by dynamic exclusion window + :param mz: m/z value + :param rt: RT value + :return: True if excluded, False otherwise + """ + # TODO: make this faster? + for x in self.exclusion_list: + exclude_mz = x.from_mz <= mz <= x.to_mz + exclude_rt = x.from_rt <= rt <= x.to_rt + if exclude_mz and exclude_rt: + self.logger.debug( + 'Time {:.6f} Excluded precursor ion mz {:.4f} rt {:.2f} because of {}'.format(self.time, mz, rt, x)) + return True + return False + + def fire_event(self, event_name, arg=None): + """ + Simulates sending an event + :param event_name: the event name + :param arg: the event parameter + :return: None + """ + if event_name not in self.event_dict: + raise ValueError('Unknown event name') + + # pretend to fire the event + # actually here we just runs the event handler method directly + e = self.event_dict[event_name] + if arg is not None: + e(arg) + else: + e() + + def register(self, event_name, handler): + """ + Register event handler + :param event_name: the event name + :param handler: the event handler + :return: None + """ + if event_name not in self.event_dict: + raise ValueError('Unknown event name') + e = self.event_dict[event_name] + e += handler # register a new event handler for e + + def clear(self, event_name): + """ + Clears event handler for a given event name + :param event_name: the event name + :return: None + """ + if event_name not in self.event_dict: + raise ValueError('Unknown event name') + e = self.event_dict[event_name] + e.targets = [] + + #################################################################################################################### + # Private methods + #################################################################################################################### + + def _init_time(self, max_time, min_time): + """ + Sets initial mass spec time + :param max_time: end time + :param min_time: start time + :return: a new end time, if it's read from DsDA CSV file, otherwise it's the same + """ + if self.schedule_file is None: + self.time = min_time + else: + self.time = self.schedule["targetTime"].values[0] + max_time = self.schedule["targetTime"].values[-1] + return max_time + + def _get_param(self): + """ + Retrieves a new set of scan parameters from the processing queue + :return: A new set of scan parameters from the queue if available, otherwise it returns the default scan params. + """ + # if the processing queue is empty, then just do the repeating scan + if len(self.processing_queue) == 0: + param = self.repeating_scan_parameters + else: + # otherwise pop the parameter for the next scan from the queue + param = self.processing_queue.pop(0) + return param + + def _increase_time(self, current_level, current_N, current_DEW, next_scan_param): + # look into the queue, find out what the next scan ms_level is, and compute the scan duration + # only applicable for simulated mass spec, since the real mass spec can generate its own scan duration. + self.idx += 1 + if self.schedule_file is None: + if next_scan_param is None: # if queue is empty, the next one is an MS1 scan by default + next_level = 1 + else: + next_level = next_scan_param.get(ScanParameters.MS_LEVEL) + + # sample current scan duration based on current_DEW, current_N, current_level and next_level + current_scan_duration = self._sample_scan_duration(current_DEW, current_N, + current_level, next_level) + else: + new_time = self.schedule["targetTime"][self.idx] + current_scan_duration = new_time - self.time + + self.time += current_scan_duration + self.logger.info('Time %f Len(queue)=%d' % (self.time, len(self.processing_queue))) + return current_scan_duration + + def _sample_scan_duration(self, current_DEW, current_N, current_level, next_level): + # get scan duration based on current and next level + if current_level == 1 and next_level == 1: + # special case: for the transition (1, 1), we can try to get the times for the + # fullscan data (N=0, DEW=0) if it's stored + try: + current_scan_duration = self.peak_sampler.scan_durations(current_level, next_level, 1, N=0, DEW=0) + except KeyError: ## ooops not found + current_scan_duration = self.peak_sampler.scan_durations(current_level, next_level, 1, + N=current_N, DEW=current_DEW) + else: # for (1, 2), (2, 1) and (2, 2) + current_scan_duration = self.peak_sampler.scan_durations(current_level, next_level, 1, + N=current_N, DEW=current_DEW) + current_scan_duration = current_scan_duration.flatten()[0] + return current_scan_duration + + def _add_precursor_info(self, param, scan): + """ + Adds precursor ion information. + If MS2 and above, and controller tells us which precursor ion the scan is coming from, store it + :param param: a scan parameter object + :param scan: the newly generated scan + :return: None + """ + precursor = param.get(ScanParameters.PRECURSOR) + if scan.ms_level >= 2 and precursor is not None: + isolation_windows = param.get(ScanParameters.ISOLATION_WINDOWS) + iso_min = isolation_windows[0][0][0] + iso_max = isolation_windows[0][0][1] + self.logger.debug('Time {:.6f} Isolated precursor ion {:.4f} at ({:.4f}, {:.4f})'.format(self.time, + precursor.precursor_mz, + iso_min, + iso_max)) + self.precursor_information[precursor].append(scan) + + def _manage_dynamic_exclusion_list(self, param, scan): + """ + Manages dynamic exclusion list + :param param: a scan parameter object + :param scan: the newly generated scan + :return: None + """ + precursor = param.get(ScanParameters.PRECURSOR) + if scan.ms_level >= 2 and precursor is not None: + # add dynamic exclusion item to the exclusion list to prevent the same precursor ion being fragmented + # multiple times in the same mz and rt window + # Note: at this point, fragmentation has occurred and time has been incremented! so the time when + # items are checked for dynamic exclusion is the time when MS2 fragmentation occurs + # TODO: we need to add a repeat count too, i.e. how many times we've seen a fragment peak before + # it gets excluded (now it's basically 1) + mz = precursor.precursor_mz + mz_tol = param.get(ScanParameters.DYNAMIC_EXCLUSION_MZ_TOL) + rt_tol = param.get(ScanParameters.DYNAMIC_EXCLUSION_RT_TOL) + mz_lower = mz * (1 - mz_tol / 1e6) + mz_upper = mz * (1 + mz_tol / 1e6) + rt_lower = self.time + rt_upper = self.time + rt_tol + x = ExclusionItem(from_mz=mz_lower, to_mz=mz_upper, from_rt=rt_lower, to_rt=rt_upper) + self.logger.debug('Time {:.6f} Created dynamic exclusion window mz ({}-{}) rt ({}-{})'.format( + self.time, + x.from_mz, x.to_mz, x.from_rt, x.to_rt + )) + self.exclusion_list.append(x) + + # remove expired items from dynamic exclusion list + self.exclusion_list = list(filter(lambda x: x.to_rt > self.time, self.exclusion_list)) + + def _store_next_N_DEW(self, next_scan_param): + """ + Stores the N and DEW parameter values for the next scan params + :param next_scan_param: A new set of scan parameters + :return: None + """ + if next_scan_param is not None: + # Only the hybrid controller sends these N and DEW parameters. For other controllers they will be None + next_N = next_scan_param.get(ScanParameters.N) + next_DEW = next_scan_param.get(ScanParameters.DYNAMIC_EXCLUSION_RT_TOL) + else: + next_N = None + next_DEW = None + + # keep track of the N and DEW values for the next scan if they have been changed by the Hybrid Controller + if next_N is not None: + self.current_N = next_N + if next_DEW is not None: + self.current_DEW = next_DEW + + def _update_progress_bar(self, elapsed, pbar, scan): + """ + Updates progress bar based on elapsed time + :param elapsed: Elapsed time to increment the progress bar + :param pbar: progress bar object + :param scan: the newly generated scan + :return: None + """ + if pbar is not None: + if self.current_N > 0 and self.current_DEW > 0: + msg = '(%.3fs) ms_level=%d N=%d DEW=%d' % (self.time, scan.ms_level, + self.current_N, self.current_DEW) + else: + msg = '(%.3fs) ms_level=%d' % (self.time, scan.ms_level) + pbar.update(elapsed) + pbar.set_description(msg) + + #################################################################################################################### + # Scan generation methods + #################################################################################################################### + + def _get_scan(self, scan_time, param, max_intensity=0): + """ + Constructs a scan at a particular timepoint + :param time: the timepoint + :return: a mass spectrometry scan at that time + """ + scan_mzs = [] # all the mzs values in this scan + scan_intensities = [] # all the intensity values in this scan + ms_level = param.get(ScanParameters.MS_LEVEL) + isolation_windows = param.get(ScanParameters.ISOLATION_WINDOWS) + scan_id = self.idx + + # for all chemicals that come out from the column coupled to the mass spec + idx = self._get_chem_indices(scan_time) + last = -1 + for i in idx: + chemical = self.chemicals[i] + # mzs is a list of (mz, intensity) for the different adduct/isotopes combinations of a chemical + mzs = self._get_all_mz_peaks(chemical, scan_time, ms_level, isolation_windows) + + peaks = [] + if mzs is not None: + new_data = [] + for (m,it) in mzs: + new_data.append((scan_time, m, it )) + self.peak_recorder[ str(chemical.formula) ].extend( new_data ) + chem_mzs = [] + chem_intensities = [] + for peak_mz, peak_intensity in mzs: + if peak_intensity > 0: + chem_mzs.append(peak_mz) + chem_intensities.append(peak_intensity) + p = Peak(peak_mz, scan_time, peak_intensity, ms_level) + peaks.append(p) + + scan_mzs.extend(chem_mzs) + scan_intensities.extend(chem_intensities) + + curr = len( scan_mzs ) + if curr != last: + last = curr + # for benchmarking purpose + if len(peaks) > 0: + frag = FragmentationEvent(chemical, scan_time, ms_level, peaks, scan_id) + self.fragmentation_events.append(frag) + + scan_mzs = np.array(scan_mzs) + scan_intensities = np.array(scan_intensities) + if self.grp == 2: + scan_intensities *= 2 + # Note: at this point, the scan duration is not set yet because we don't know what the next scan is going to be + # We will set it later in the get_next_scan() method after we've notified the controller that this scan is produced. + return Scan(scan_id, scan_mzs, scan_intensities, ms_level, scan_time, + scan_duration=None, isolation_windows=isolation_windows) + + def _get_chem_indices(self, query_rt): + rtmin_check = self.chrom_min_rts <= query_rt + rtmax_check = query_rt <= self.chrom_max_rts + idx = np.nonzero(rtmin_check & rtmax_check)[0] + + return idx + + + def _get_all_mz_peaks(self, chemical, query_rt, ms_level, isolation_windows): + if not self._rt_match(chemical, query_rt): + return None + mz_peaks = [] + for which_isotope in range(len(chemical.isotopes)): + for which_adduct, adduct in enumerate(self._get_adducts(chemical)): + if adduct[0] != 'M+H': + continue + mz_peaks.extend( self._get_mz_peaks(chemical, query_rt, ms_level, isolation_windows, which_isotope, which_adduct) ) + if mz_peaks == []: + return None + else: + return mz_peaks + + def _get_mz_peaks(self, chemical, query_rt, ms_level, isolation_windows, which_isotope, which_adduct): + # EXAMPLE OF USE OF DEFINITION: if we wants to do an ms2 scan on a chemical. we would first have ms_level=2 and the chemicals + # ms_level =1. So we would go to the "else". We then check the ms1 window matched. It then would loop through + # the children who have ms_level = 2. So we then go to second elif and return the mz and intensity of each ms2 fragment + mz_peaks = [] + if ms_level == 1 and chemical.ms_level == 1: # fragment ms1 peaks + # returns ms1 peaks if chemical is has ms_level = 1 and scan is an ms1 scan + if not (which_isotope > 0 and which_adduct > 0): + # rechecks isolations window if not monoisotopic and "M + H" adduct + if self._isolation_match(chemical, query_rt, isolation_windows[0], which_isotope, which_adduct): + intensity = self._get_intensity(chemical, query_rt, which_isotope, which_adduct) + mz = self._get_mz(chemical, query_rt, which_isotope, which_adduct) + mz_peaks.extend([(mz, intensity)]) + assert len( mz_peaks ) == 1 + else: + pass + elif ms_level == chemical.ms_level: + # returns ms2 fragments if chemical and scan are both ms2, + # returns ms3 fragments if chemical and scan are both ms3, etc, etc + intensity = self._get_intensity(chemical, query_rt, which_isotope, which_adduct) + mz = self._get_mz(chemical, query_rt, which_isotope, which_adduct) + return [(mz, intensity)] + # TODO: Potential improve how the isotope spectra are generated + else: + # check isolation window for ms2+ scans, queries children if isolation windows ok + if self._isolation_match(chemical, query_rt, isolation_windows[chemical.ms_level - 1], which_isotope, + which_adduct) and chemical.children is not None: + for i in range(len(chemical.children)): + mz_peaks.extend(self._get_mz_peaks(chemical.children[i], query_rt, ms_level, isolation_windows, + which_isotope, which_adduct)) + else: + return [] + return mz_peaks + + def _get_adducts(self, chemical): + if chemical.ms_level == 1: + return chemical.adducts + else: + return self._get_adducts(chemical.parent) + + def _rt_match(self, chemical, query_rt): + return chemical.ms_level != 1 or chemical.chromatogram._rt_match(query_rt - chemical.rt) + + def _get_intensity(self, chemical, query_rt, which_isotope, which_adduct): + # TODO this is the part where I will set the intensity + if chemical.ms_level == 1: + intensity = chemical.isotopes[which_isotope][1] * self._get_adducts(chemical)[which_adduct][1] * \ + chemical.max_intensity + return intensity * chemical.chromatogram.get_relative_intensity(query_rt - chemical.rt) + else: + return self._get_intensity(chemical.parent, query_rt, which_isotope, which_adduct) * \ + chemical.parent_mass_prop * chemical.prop_ms2_mass + + def _get_mz(self, chemical, query_rt, which_isotope, which_adduct): + if chemical.ms_level == 1: + return (adduct_transformation(chemical.isotopes[which_isotope][0], + self._get_adducts(chemical)[which_adduct][0]) + + chemical.chromatogram.get_relative_mz(query_rt - chemical.rt)) + else: + ms1_parent = chemical + while ms1_parent.ms_level != 1: + ms1_parent = chemical.parent + isotope_transformation = ms1_parent.isotopes[which_isotope][0] - ms1_parent.isotopes[0][0] + # TODO: Needs improving + return (adduct_transformation(chemical.isotopes[0][0], + self._get_adducts(chemical)[which_adduct][0]) + isotope_transformation) + + def _isolation_match(self, chemical, query_rt, isolation_windows, which_isotope, which_adduct): + # assumes list is formated like: + # [(min_1,max_1),(min_2,max_2),...] + for window in isolation_windows: + if window[0] < self._get_mz(chemical, query_rt, which_isotope, which_adduct) <= window[1]: + return True + return False + + +class DsDAMassSpec(IndependentMassSpectrometer): + """ + A mass spec class with fixed schedule time, used during DsDA experiment in the paper. + TODO: Could probably be removed. + """ + + def run(self, schedule, pbar=None): + self.schedule = schedule + self.time = schedule["targetTime"][0] + self.fire_event(IndependentMassSpectrometer.ACQUISITION_STREAM_OPENING) + + try: + last_ms1_id = 0 + while len(self.processing_queue) != 0: + scan_params = self.processing_queue.pop(0) + + # make a scan + target_time = scan_params.get(ScanParameters.TIME) + scan = self._get_scan(target_time, scan_params, max_intensity, noise) + + # set scan duration + try: + next_time = self.processing_queue[0].get(ScanParameters.TIME) + except IndexError: + next_time = 1 + scan.scan_duration = next_time - target_time + + # update precursor scan id + if scan.ms_level == 1: + last_ms1_id = scan.scan_id + else: + precursor = scan_params.get(ScanParameters.PRECURSOR) + if precursor is not None: + precursor.precursor_scan_id = last_ms1_id + self.precursor_information[precursor].append(scan) + + # notify controller about this scan + self.fire_event(self.MS_SCAN_ARRIVED, scan) + + # increase mass spec time + self.idx += 1 + self.time += scan.scan_duration + + # print a progress bar if provided + if pbar is not None: + elapsed = self.time + pbar.update(elapsed) + # TODO: fix error bar + + finally: + self.fire_event(IndependentMassSpectrometer.ACQUISITION_STREAM_CLOSING) + if pbar is not None: + pbar.close() diff --git a/Synthetic data creation scripts/vimms/MatrixFactorisation.py b/Synthetic data creation scripts/vimms/MatrixFactorisation.py new file mode 100644 index 00000000..c6625d05 --- /dev/null +++ b/Synthetic data creation scripts/vimms/MatrixFactorisation.py @@ -0,0 +1,242 @@ +import bisect +import copy +import numpy as np +import pylab as plt +import math +import scipy + + +class BlockData(object): + def __init__(self, datasets, mz_step, rt_step, rt_range=[(0, 1450)], mz_range=[(50, 1070)]): + self.datasets = datasets + self.mz_step = mz_step + self.rt_step = rt_step + self.rt_range = rt_range + self.mz_range = mz_range + self.keys = [] + for j in range(len(self.datasets)): + self.keys.append(list(self.datasets[j].file_spectra.keys())) + self.mz_bin_lower = np.arange(mz_range[0][0], mz_range[0][1], mz_step) + self.rt_bin_lower = np.arange(rt_range[0][0], rt_range[0][1], rt_step) + self.n_mz_bin_lower = len(self.mz_bin_lower) + self.n_rt_bin_lower = len(self.rt_bin_lower) + + self.intensity_mats = [] + for j in range(len(self.datasets)): + for i in range(len(self.keys[j])): + self.intensity_mats.append(self._block_file(i, j)) + print("Processed", self.keys[j][i]) + + def _block_file(self, num, data_num): + intensity_mat = np.zeros((self.n_mz_bin_lower, self.n_rt_bin_lower), np.double) + spectra = self.datasets[data_num].file_spectra[self.keys[data_num][num]] + c1 = 0 + for scan_num in spectra: + scan_rt = spectra[scan_num].scan_time[0] + if scan_rt < self.rt_bin_lower[0]: + continue + if scan_rt > self.rt_bin_lower[-1] + self.rt_step: + continue + else: + rt_pos = bisect.bisect_right(self.rt_bin_lower, scan_rt) + rt_pos -= 1 + for peak_num in range(len(spectra[scan_num].mz)): + mz = spectra[scan_num].mz[peak_num] + intensity = spectra[scan_num].i[peak_num] + if mz < self.mz_bin_lower[0]: + continue + if mz > self.mz_bin_lower[-1] + self.mz_step: + break + mz_pos = bisect.bisect_right(self.mz_bin_lower, mz) + mz_pos -= 1 + + intensity_mat[mz_pos, rt_pos] += intensity + return intensity_mat + + def plot(self, data_num): + print("Warning: Python prints plots in a stupid stupid way!") + plt.imshow(np.log(self.intensity_mats[data_num][-1:0:-1] + 1), aspect='auto') + + def combine(self, plot=True): + combined = [] + for mat in self.intensity_mats: + combined.append(list(mat.flatten())) + return combined + + +def gibbs_sampler(X, observed, R, prior_u, prec_u, prior_v, prec_v, alpha, n_its = 1000, burn_in = 100, true_V=[], sample_known = True): + # initialise + N, M = X.shape + U = np.random.normal(size=(N, R)) + if len(true_V) == 0: + V = np.random.normal(size=(M, R)) + else: + V = true_V + tot_U = np.zeros((N, R)) + tot_V = np.zeros((M, R)) + samples_U = [] + samples_V = [] + all_err = [] + range_U = range(N) + if sample_known is False: + range_U = np.where(np.sum(observed, axis=1) != len(observed[0, :]))[0].tolist() + for it in range(n_its): + # loop over u, updating them + # first compute the covariance - shared if all data observed + prec_mat = prec_u + alpha * np.dot(V.T, V) + cov_mat = np.linalg.inv(prec_mat) + for n in range_U: + if observed[n, :].sum() < M: + # not all data observed, compute specific precision + this_prec_mat = prec_u + alpha * np.dot(np.dot(V.T, np.diag(observed[n, :])), V) + this_cov_mat = np.linalg.inv(this_prec_mat) + else: + this_prec_mat = prec_mat + this_cov_mat = cov_mat + s = np.zeros(R) + for m in range(M): + if observed[n, m]: + s += X[n, m] * V[m, :] + s *= alpha + s += np.dot(prec_u, prior_u) + cond_mu = np.dot(this_cov_mat, s) + U[n, :] = np.random.multivariate_normal(cond_mu, this_cov_mat) + + # loop over v updating them + # first covariance + if len(true_V) == 0: + prec_mat = prec_v + alpha * np.dot(U.T, U) + cov_mat = np.linalg.inv(prec_mat) + for m in range(M): + if observed[:, m].sum() < N: + this_prec_mat = prec_v + alpha * np.dot(np.dot(U.T, np.diag(observed[:, m])), U) + this_cov_mat = np.linalg.inv(this_prec_mat) + else: + this_prec_mat = prec_mat + this_cov_mat = cov_mat + + s = np.zeros(R) + for n in range(N): + if observed[n, m]: + s += X[n, m] * U[n, :] + s *= alpha + s += np.dot(prec_v, prior_v) + cond_mu = np.dot(this_cov_mat, s) + V[m, :] = np.random.multivariate_normal(cond_mu, this_cov_mat) + if it > burn_in: + tot_U += U + tot_V += V + samples_U.append(copy.deepcopy(U)) + samples_V.append(copy.deepcopy(V)) + recon_error = np.sqrt(((X - np.dot(U, V.T)) ** 2).mean()) + all_err.append(recon_error) + if len(true_V) == 0: + return tot_U / (n_its - burn_in), tot_V / (n_its - burn_in) + else: + if sample_known is True: + return samples_U + else: + updated_samples_U = [] + for i in range(len(samples_U)): + updated_samples_U.append(samples_U[i][range_U,:]) + return range_U, updated_samples_U + + +class VB_PCA(object): + def __init__(self, Y, Z, D, MaxIts=100, a=1, b=1, tol=1e-3, compute_LB=False, VB_PCA_model=None): + + # intialise parameters + self.Y = Y + self.Z = Z + self.D = D + ZY = Z * Y + self.B = [] + self.e_tau = [] + e_tau = a / b + self.e_tau.append(e_tau) + self.e_log_tau = [np.log(e_tau)] + self.N = Y.shape[0] + self.M = Y.shape[1] + self.e_w = np.random.normal(0, 1, (self.M, self.D)) + self.e_X = np.random.normal(0, 1, (self.N, self.D)) + self.e_wwt = [] + self.e_XXt = [] + for m in range(self.M): + self.e_wwt.append(np.identity(self.D) + np.matmul(self.e_w[m, :][np.newaxis].T, self.e_w[m, :][np.newaxis])) + for n in range(self.N): + self.e_XXt.append(np.identity(self.D) + np.matmul(self.e_X[n, :][np.newaxis].T, self.e_X[n, :][np.newaxis])) + self.sigx = [[] for i in range(self.N)] + self.sigw = [[] for i in range(self.M)] + # run code + for it in range(MaxIts): + + # update X + for n in range(self.N): + Zlist = [vec * np.ones((self.D, self.D)) for vec in Z[n,:]] + self.sigx[n] = np.linalg.inv(np.identity(self.D) + e_tau * sum(np.array(self.e_wwt) * np.array(Zlist))) + self.e_X[n, :] = e_tau * np.matmul(self.sigx[n], np.sum( + np.multiply(self.e_w, np.array([(ZY[n, :]).tolist() for i in range(self.D)]).T), axis=0)) + self.e_XXt[n] = self.sigx[n] + np.matmul(self.e_X[n, :][np.newaxis].T, self.e_X[n, :][np.newaxis]) + + # update W + for m in range(self.M): + Zlist = [vec * np.ones((self.D, self.D)) for vec in Z[:,m]] + self.sigw[m] = np.linalg.inv(np.identity(self.D) + e_tau * sum(np.array(self.e_XXt) * np.array(Zlist))) + self.e_w[m, :] = e_tau * np.matmul(self.sigw[m], np.sum( + np.multiply(self.e_X, np.array([(Y[:, m] * Z[:, m]).tolist() for i in range(self.D)]).T), axis=0)) + self.e_wwt[m] = self.sigw[m] + np.matmul(self.e_w[m, :][np.newaxis].T, self.e_w[m, :][np.newaxis]) + + # update tau + e = a + sum(sum(Z)) / 2 + outer_expect = 0 + RSS = 0 + for n in range(self.N): + for m in range(self.M): + outer_expect += Z[n,m] * (np.trace(np.matmul(self.e_wwt[m], self.sigx[n])) + np.matmul( + np.matmul(self.e_X[n, :], self.e_wwt[m]), self.e_X[n, :][np.newaxis].T)) + RSS += (ZY[n, m] ** 2) - 2 * np.matmul(self.e_w[m].T, self.e_X[n]) * (ZY[n, m]) + f = b + 0.5 * RSS + 0.5 * outer_expect + e_tau = e / f + self.e_tau.append(e_tau[0]) + e_log_tau = np.mean(np.log(np.random.gamma(shape=e, scale=1 / f, size=1000))) + self.e_log_tau.append(e_log_tau) + + # Compute the bound + if compute_LB is True: + LB = a * np.log(b) + (a - 1) * e_log_tau - b * e_tau - scipy.special.loggamma(a) + LB -= (e * np.log(f) + (e - 1) * e_log_tau - f * e_tau - scipy.special.loggamma(e)) + + for n in range(self.N): + LB += (-(self.D / 2) * np.log(2 * math.pi) - 0.5 * sum(np.diag(self.sigx[n])) + sum(self.e_X[n, :] ** 2)) + LB -= (-(self.D / 2) * np.log(2 * math.pi) - 0.5 * np.log(np.linalg.det(self.sigx[n])) - 0.5 * self.D) + + for m in range(self.M): + LB += (-(self.D / 2) * np.log(2 * math.pi) - 0.5 * sum(np.diag(self.sigw[m])) + sum(self.e_w[m, :] ** 2)) + print((-(self.D / 2) * np.log(2 * math.pi) - 0.5 * np.log(np.linalg.det(self.sigw[m])) - 0.5 * self.D)) + LB -= (-(self.D / 2) * np.log(2 * math.pi) - 0.5 * np.log(np.linalg.det(self.sigw[m])) - 0.5 * self.D) + + # likelihood bit + LB += (-(self.N * self.M / 2) * np.log(2 * math.pi) + (self.N * self.M / 2) * e_log_tau - 0.5 * e_tau * sum( + sum((ZY ** 2))) - 2 * sum(sum(Z * (np.multiply(np.matmul(self.e_w, self.e_X.T).T, Y)))) + outer_expect) + self.B.append(LB) + + # break if change in bound is less than the tolerance + if it > 2: + if abs(self.B[-1] - self.B[-2]) < tol: + break + # reconstruct Y + self.Y_reconstructed = np.matmul(self.e_X, self.e_w.T) + + def update(self, new_Z, new_Y): + # assume last Y is only new observations + self.N = new_Y.shape[0] + self.M = new_Y.shape[1] + if self.Z.shape[0] < new_Z.shape[0]: + self.e_X = np.concatenate((self.e_X, np.array([[0 for i in range(self.D)]]))) + self.sigx.append(np.identity(self.D)) + prec_x = np.identity(self.D) + np.dot(np.dot(self.e_w.T, np.diag(new_Z[-1, :])), self.e_w) * self.e_tau[-1] + self.sigx[-1] = np.linalg.inv(prec_x) + self.e_X[-1] = np.dot(self.sigx[-1], np.dot(self.e_w.T, (new_Y[-1,]*new_Z[-1,]))) + self.Y = new_Y + self.Z = new_Z + self.Y_reconstructed = np.matmul(self.e_X, self.e_w.T) diff --git a/Synthetic data creation scripts/vimms/MzmlWriter.py b/Synthetic data creation scripts/vimms/MzmlWriter.py new file mode 100644 index 00000000..4fa89f8c --- /dev/null +++ b/Synthetic data creation scripts/vimms/MzmlWriter.py @@ -0,0 +1,160 @@ +import os + +import numpy as np +from psims.mzml.writer import MzMLWriter as PsimsMzMLWriter + +from vimms.Common import DEFAULT_MS1_SCAN_WINDOW, create_if_not_exist + + +class MzmlWriter(object): + """A class to write peak data to mzML file""" + + def __init__(self, analysis_name, scans, precursor_information=None): + """ + Initialises the mzML writer class. + :param analysis_name: Name of the analysis. + :param scans: A dictionary where key is scan level, value is a list of Scans object for that level. + :param precursor_information: A dictionary where key is Precursor object, value is a list of ms2 scans only + """ + self.analysis_name = analysis_name + self.scans = scans + self.precursor_information = precursor_information + + def write_mzML(self, out_file): + # if directory doesn't exist, create it + out_dir = os.path.dirname(out_file) + create_if_not_exist(out_dir) + + # start writing mzML here + with PsimsMzMLWriter(open(out_file, 'wb')) as writer: + # add default controlled vocabularies + writer.controlled_vocabularies() + + # write other fields like sample list, software list, etc. + self._write_info(writer) + + # open the run + with writer.run(id=self.analysis_name): + self._write_spectra(writer, self.scans, self.precursor_information) + + # open chromatogram list sections + with writer.chromatogram_list(count=1): + tic_rts, tic_intensities = self._get_tic_chromatogram(self.scans) + writer.write_chromatogram(tic_rts, tic_intensities, id='tic', + chromatogram_type='total ion current chromatogram', + time_unit='second') + + writer.close() + + def _write_info(self, out): + # check file contains what kind of spectra + has_ms1_spectrum = 1 in self.scans + has_msn_spectrum = 1 in self.scans and len(self.scans) > 1 + file_contents = [ + 'profile spectrum' + ] + if has_ms1_spectrum: + file_contents.append('MS1 spectrum') + if has_msn_spectrum: + file_contents.append('MSn spectrum') + out.file_description( + file_contents=file_contents, + source_files=[] + ) + out.sample_list(samples=[]) + out.software_list(software_list={ + 'id': 'VMS', + 'version': '1.0.0' + }) + out.scan_settings_list(scan_settings=[]) + out.instrument_configuration_list(instrument_configurations={ + 'id': 'VMS', + 'component_list': [] + }) + out.data_processing_list({'id': 'VMS'}) + + def sort_filter(self, all_scans): + all_scans = sorted(all_scans, key=lambda x: x.scan_id) + # all_scans = [x for x in all_scans if x.num_peaks > 0] + + # add a single peak to empty scans + empty = [x for x in all_scans if x.num_peaks == 0] + for scan in empty: + scan.mzs = np.array([100.0]) + scan.intensities = np.array([1.0]) + scan.num_peaks = 1 + return all_scans + + def _write_spectra(self, writer, scans, precursor_information): + assert len(scans) <= 3 # NOTE: we only support writing up to ms2 scans for now + + # get all scans across different ms_levels and sort them by scan_id + all_scans = [] + for ms_level in scans: + all_scans.extend(scans[ms_level]) + all_scans = self.sort_filter(all_scans) + spectrum_count = len(all_scans) + + # get precursor information for each scan, if available + scan_precursor = {} + for precursor, ms2_scans in precursor_information.items(): + assert len(ms2_scans) == 1 + ms2_scan = ms2_scans[0] + scan_precursor[ms2_scan.scan_id] = precursor + + # write scans + with writer.spectrum_list(count=spectrum_count): + for scan in all_scans: + precursor = None + if scan.scan_id in scan_precursor: + precursor = scan_precursor[scan.scan_id] + self._write_scan(writer, scan, precursor) + + def _get_scan_id(self, scan_id): + return scan_id + + def _write_scan(self, out, scan, precursor): + assert scan.num_peaks > 0 + label = 'MS1 Spectrum' if scan.ms_level == 1 else 'MSn Spectrum' + precursor_information = None + if precursor is not None: + precursor_information = { + "mz": precursor.precursor_mz, + "intensity": precursor.precursor_intensity, + "charge": precursor.precursor_charge, + "spectrum_reference": self._get_scan_id(precursor.precursor_scan_id), + "activation": ["HCD", {"collision energy": 25.0}] + } + lowest_observed_mz = min(scan.mzs) + highest_observed_mz = max(scan.mzs) + bp_pos = np.argmax(scan.intensities) + bp_intensity = scan.intensities[bp_pos] + bp_mz = scan.mzs[bp_pos] + + out.write_spectrum( + scan.mzs, scan.intensities, + id=self._get_scan_id(scan.scan_id), + centroided=True, + scan_start_time=scan.rt / 60.0, + scan_window_list=[DEFAULT_MS1_SCAN_WINDOW], + params=[ + {label: ''}, + {'ms level': scan.ms_level}, + {'total ion current': np.sum(scan.intensities)}, + {'lowest observed m/z': lowest_observed_mz}, + {'highest observed m/z': highest_observed_mz}, + # {'base peak m/z', bp_mz}, + # {'base peak intensity', bp_intensity} + ], + precursor_information=precursor_information + ) + + def _get_tic_chromatogram(self, scans): + time_array = [] + intensity_array = [] + for ms1_scan in scans[1]: + time_array.append(ms1_scan.rt) + intensity_array.append(np.sum(ms1_scan.intensities)) + time_array = np.array(time_array) + intensity_array = np.array(intensity_array) + return time_array, intensity_array diff --git a/Synthetic data creation scripts/vimms/PlotsForPaper.py b/Synthetic data creation scripts/vimms/PlotsForPaper.py new file mode 100644 index 00000000..58ecbb64 --- /dev/null +++ b/Synthetic data creation scripts/vimms/PlotsForPaper.py @@ -0,0 +1,718 @@ +import os +from collections import defaultdict + +import numpy as np +import pandas as pd +import pylab as plt +import seaborn as sns +import matplotlib.patches as mpatches +import pymzml + +from vimms.Chemicals import UnknownChemical, get_absolute_intensity, get_key +from vimms.Common import load_obj, PROTON_MASS, find_nearest_index_in_array +from vimms.MassSpec import FragmentationEvent +from vimms.Roi import make_roi, RoiToChemicalCreator +from vimms.SpectralUtils import get_precursor_info, get_chemicals + + +def get_N(row): + if 'T10' in row['filename']: + return 10 + else: + return row['filename'].split('_')[3] + + +def get_dew(row): + if 'T10' in row['filename']: + return 15 + else: + tok = row['filename'].split('_')[5] # get the dew value in the filename + return tok.split('.')[0] # get the part before '.mzML' + + +def experiment_group(row): + if 'experiment' in row: + col_to_check = 'experiment' + else: + col_to_check = 'filename' + + if 'beer' in row[col_to_check]: + return 'beer' + else: + return 'urine' + + +def add_group_column(df): + df['group'] = df.apply(lambda row: experiment_group(row), axis=1) + + +def get_df(csv_file, min_ms1_intensity, rt_range, mz_range): + df = pd.read_csv(csv_file) + return filter_df(df, min_ms1_intensity, rt_range, mz_range) + + +def filter_df(df, min_ms1_intensity, rt_range, mz_range): + # filter by rt range + if rt_range is not None: + df = df[(df['rt'] > rt_range[0][0]) & (df['rt'] < rt_range[0][1])] + + # filter by mz range + if mz_range is not None: + df = df[(df['rt'] > mz_range[0][0]) & (df['rt'] < mz_range[0][1])] + + # filter by min intensity + intensity_col = 'maxo' + if min_ms1_intensity is not None: + df = df[(df[intensity_col] > min_ms1_intensity)] + + # add log intensity column + df['log_intensity'] = df.apply(lambda row: np.log(row[intensity_col]), axis=1) + + # add N column + try: + df['N'] = df.apply(lambda row: get_N(row), axis=1) + df[['N']] = df[['N']].astype('int') + except IndexError: + pass + except ValueError: + df['N'] = df.apply(lambda row: np.nan, axis=1) + + # add group column + df['group'] = df.apply(lambda row: experiment_group(row), axis=1) + return df + + +def make_boxplot(df, x, y, xticklabels, title, outfile=None): + g = sns.catplot(x=x, y=y, kind='box', data=df) + g.fig.set_size_inches(10, 3) + if xticklabels is not None: + g.set_xticklabels(xticklabels, rotation=90) + else: + g.set_xticklabels(rotation=90) + plt.title(title) + plt.tight_layout() + if outfile is not None: + plt.savefig(outfile, dpi=300) + plt.show() + + +def make_hist(df, col_name, file_name, title): + gb = df.groupby('filename') + group_df = gb.get_group(file_name) + vals = group_df[col_name].values + print(vals, len(vals)) + _ = plt.hist(vals, bins=100) + plt.title(title) + plt.tight_layout() + plt.show() + + +def to_chemical(row): + mz = row['mz'] - PROTON_MASS + rt = row['rt'] + max_intensity = row['maxo'] + chrom = None + chem = UnknownChemical(mz, rt, max_intensity, chrom, children=None) + return chem + + +def df_to_chemicals(df, filename=None): + if filename is not None: + filtered_df = df.loc[df['filename'] == filename] + else: + filtered_df = df + chems = filtered_df.apply(lambda row: to_chemical(row), axis=1).values + return chems + + +def find_chem(to_find, min_rts, max_rts, min_mzs, max_mzs, chem_list): + query_mz = to_find.isotopes[0][0] + query_rt = to_find.rt + min_rt_check = min_rts <= query_rt + max_rt_check = query_rt <= max_rts + min_mz_check = min_mzs <= query_mz + max_mz_check = query_mz <= max_mzs + idx = np.nonzero(min_rt_check & max_rt_check & min_mz_check & max_mz_check)[0] + matches = chem_list[idx] + + # pick a match + if len(matches) == 0: + return None + elif len(matches) == 1: + return matches[0] + else: # multiple matches, take the closest in rt + diffs = [np.abs(chem.rt - to_find.rt) for chem in matches] + idx = np.argmin(diffs) + return matches[idx] + + +def match(chemical_list_1, chemical_list_2, mz_tol, rt_tol, verbose=False): + matches = {} + chem_list = np.array(chemical_list_2) + min_rts = np.array([chem.rt - rt_tol for chem in chem_list]) + max_rts = np.array([chem.rt + rt_tol for chem in chem_list]) + min_mzs = np.array([chem.isotopes[0][0] * (1 - mz_tol / 1e6) for chem in chem_list]) + max_mzs = np.array([chem.isotopes[0][0] * (1 + mz_tol / 1e6) for chem in chem_list]) + for i in range(len(chemical_list_1)): + to_find = chemical_list_1[i] + if i % 1000 == 0 and verbose: + print('%d/%d found %d' % (i, len(chemical_list_1), len(matches))) + match = find_chem(to_find, min_rts, max_rts, min_mzs, max_mzs, chem_list) + if match: + matches[to_find] = match + return matches + + +def match_peaklist(mz_list_1, rt_list_1, intensity_list_1, mz_list_2, rt_list_2, intensity_list_2, mz_tol, rt_tol): + if mz_tol is not None: # create mz range for matching in ppm + min_mzs = np.array([mz * (1 - mz_tol / 1e6) for mz in mz_list_2]) + max_mzs = np.array([mz * (1 + mz_tol / 1e6) for mz in mz_list_2]) + + else: # create mz ranges by rounding to 2dp + min_mzs = np.around(mz_list_2, decimals=2) + max_mzs = np.around(mz_list_2, decimals=2) + mz_list_1 = np.around(mz_list_1, decimals=2) + + # create rt ranges for matching + min_rts = np.array([rt - rt_tol for rt in rt_list_2]) + max_rts = np.array([rt + rt_tol for rt in rt_list_2]) + + matches = {} + for i in range(len(mz_list_1)): # loop over query and find a match + query = (mz_list_1[i], rt_list_1[i], intensity_list_1[i],) + match = find_match(query, min_rts, max_rts, min_mzs, max_mzs, mz_list_2, rt_list_2, intensity_list_2) + matches[query] = match + return matches + + +def check_found_matches(matches, left_label, right_label, N=20): + found = [key for key in matches if matches[key] is not None] + print('Found %d/%d (%f)' % (len(found), len(matches), len(found) / len(matches))) + + print('%s\t\t\t\t\t\t%s' % (left_label, right_label)) + for key, value in list(matches.items())[0:N]: + if value is not None: + print('mz %.2f rt %.4f intensity %.4f\tmz %.2f rt %.4f intensity %.4f' % ( + key[0], key[1], key[2], value[0], value[1], value[2])) + + +def plot_matched_precursors(matches, min_mz, max_mz, min_rt, max_rt, out_file=None): + plt.figure(figsize=(12, 6)) + plt.rcParams.update({'font.size': 24}) + for key in matches: + mz, rt, intensity = key + if min_mz < mz < max_mz and min_rt < rt < max_rt: + if matches[key] is not None: + plt.plot([rt], [mz], marker='.', markersize=5, color='blue', alpha=0.1) + else: + plt.plot([rt], [mz], marker='.', markersize=5, color='red', alpha=0.1) + + blue_patch = mpatches.Patch(color='blue', label='Matched') + red_patch = mpatches.Patch(color='red', label='Unmatched') + plt.legend(handles=[blue_patch, red_patch]) + plt.title('Matched fragmentation events', fontsize=30) + plt.xlabel('Retention Time (s)') + plt.ylabel('m/z') + plt.tight_layout() + if out_file is not None: + plt.savefig(out_file, dpi=300) + + +def count_stuff(input_file, min_rt, max_rt): + run = pymzml.run.Reader(input_file, MS1_Precision=5e-6, + extraAccessions=[('MS:1000016', ['value', 'unitName'])], + obo_version='4.0.1') + mzs = [] + rts = [] + intensities = [] + count_ms1_scans = 0 + count_ms2_scans = 0 + cumsum_ms1_scans = [] + cumsum_ms2_scans = [] + count_selected_precursors = 0 + for spectrum in run: + ms_level = spectrum['ms level'] + current_scan_rt, units = spectrum.scan_time + if units == 'minute': + current_scan_rt *= 60.0 + if min_rt < current_scan_rt < max_rt: + if ms_level == 1: + count_ms1_scans += 1 + cumsum_ms1_scans.append((current_scan_rt, count_ms1_scans,)) + elif ms_level == 2: + try: + selected_precursors = spectrum.selected_precursors + count_selected_precursors += len(selected_precursors) + mz = selected_precursors[0]['mz'] + intensity = selected_precursors[0]['i'] + + count_ms2_scans += 1 + mzs.append(mz) + rts.append(current_scan_rt) + intensities.append(intensity) + cumsum_ms2_scans.append((current_scan_rt, count_ms2_scans,)) + except KeyError: + # print(selected_precursors) + pass + + print('Number of ms1 scans =', count_ms1_scans) + print('Number of ms2 scans =', count_ms2_scans) + print('Total scans =', count_ms1_scans + count_ms2_scans) + print('Number of selected precursors =', count_selected_precursors) + return np.array(mzs), np.array(rts), np.array(intensities), np.array(cumsum_ms1_scans), np.array(cumsum_ms2_scans) + + +def find_match(query, min_rts, max_rts, min_mzs, max_mzs, mz_list, rt_list, intensity_list): + # check ranges + query_mz, query_rt, query_intensity = query + min_rt_check = min_rts <= query_rt + max_rt_check = query_rt <= max_rts + min_mz_check = min_mzs <= query_mz + max_mz_check = query_mz <= max_mzs + idx = np.nonzero(min_rt_check & max_rt_check & min_mz_check & max_mz_check)[0] + + # get mz, rt and intensity of matching indices + matches_mz = mz_list[idx] + matches_rt = rt_list[idx] + matches_intensity = intensity_list[idx] + + if len(idx) == 0: # no match + return None + + elif len(idx) == 1: # single match + return (matches_mz[0], matches_rt[0], matches_intensity[0],) + + else: # multiple matches, take the closest in rt + diffs = [np.abs(rt - query_rt) for rt in matches_rt] + idx = np.argmin(diffs) + return (matches_mz[idx], matches_rt[idx], matches_intensity[idx],) + + +def plot_num_scans(real_cumsum_ms1, real_cumsum_ms2, simulated_cumsum_ms1, simulated_cumsum_ms2, out_file=None): + plt.plot(real_cumsum_ms1[:, 0], real_cumsum_ms1[:, 1], 'r') + plt.plot(real_cumsum_ms2[:, 0], real_cumsum_ms2[:, 1], 'b') + plt.plot(simulated_cumsum_ms1[:, 0], simulated_cumsum_ms1[:, 1], 'r--') + plt.plot(simulated_cumsum_ms2[:, 0], simulated_cumsum_ms2[:, 1], 'b--') + + plt.legend(['Actual MS1', 'Actual MS2', 'Simulated MS1', 'Simulated MS2']) + plt.xlabel('Retention Time (s)') + plt.ylabel('Cumulative sum') + plt.title('Cumulative number of MS1 and MS2 scans', fontsize=18) + plt.tight_layout() + + if out_file is not None: + plt.savefig(out_file, dpi=300) + + +def plot_matched_intensities(matched_intensities, unmatched_intensities, out_file=None): + plt.figure() + temp1 = plt.hist(np.log(matched_intensities), bins = np.linspace(10,20,50), color='blue') + temp2 = plt.hist(np.log(unmatched_intensities), bins = np.linspace(10,20,50), color='red') + plt.title('Matched precursor intensities') + + blue_patch = mpatches.Patch(color='blue', label='Matched') + red_patch = mpatches.Patch(color='red', label='Unmatched') + plt.legend(handles=[blue_patch, red_patch]) + plt.xlabel('log(intensity)') + plt.ylabel('Precursor count') + plt.tight_layout() + + if out_file is not None: + plt.savefig(out_file, dpi=300) + + +def load_controller(results_dir, experiment_name, N, rt_tol): + analysis_name = 'experiment_%s_N_%d_rttol_%d' % (experiment_name, N, rt_tol) + pickle_in = '%s/%s.p' % (results_dir, analysis_name) + print('Loading %s' % analysis_name) + try: + controller = load_obj(pickle_in) + except FileNotFoundError: + controller = None + return controller + + +def load_controllers(results_dir, Ns, rt_tols): + controllers = [] + for N in Ns: + for rt_tol in rt_tols: + controller = load_controller(results_dir, N, rt_tol) + if controller is not None: + controllers.append(controller) + return controllers + + +def compute_performance_scenario_1(controller, dataset, min_ms1_intensity, + fullscan_filename, P_peaks_df, + matching_mz_tol, matching_rt_tol, + chem_to_frag_events=None): + if chem_to_frag_events is None: # read MS2 fragmentation events from pickled controller + chem_to_frag_events = get_frag_events(controller, 2) + + # match with xcms peak-picked ms1 data + detected_ms1 = df_to_chemicals(P_peaks_df, fullscan_filename) + matches_fullscan = match(dataset, detected_ms1, matching_mz_tol, matching_rt_tol, verbose=False) + + # check if matched and set a flag to indicate that + update_matched_status(dataset, matches_fullscan, None) + + # positive instances are ground truth MS1 peaks found by XCMS + # negative instances are chemicals that cannot be matched to XCMS output + positives = list(filter(lambda x: x.found_in_fullscan, dataset)) + negatives = list(filter(lambda x: not x.found_in_fullscan, dataset)) + + # for both positive and negative instances, count how many frag events they have + # and whether it's above (good) or below (bad) the minimum ms1 intensity at the time of fragmentation. + positives_count = get_chem_frag_counts(positives, chem_to_frag_events, min_ms1_intensity) + negatives_count = get_chem_frag_counts(negatives, chem_to_frag_events, min_ms1_intensity) + + # TP = positive instances that are good only + tp = [chem for chem in positives if positives_count[chem]['good'] > 0 and positives_count[chem]['bad'] == 0] + + # FP = negative instances that are fragmented (both good + bad) + fp = [chem for chem in negatives if negatives_count[chem]['good'] > 0 or negatives_count[chem]['bad'] > 0] + + # FN = positive instances that are not fragmented at all + positive instances that are bad only + fn = [chem for chem in positives if \ + (positives_count[chem]['good'] == 0 and positives_count[chem]['bad'] == 0) or \ + (positives_count[chem]['good'] == 0 and positives_count[chem]['bad'] > 0)] + + tp = len(tp) + fp = len(fp) + fn = len(fn) + prec, rec, f1 = compute_pref_rec_f1(tp, fp, fn) + return tp, fp, fn, prec, rec, f1 + + +# def compute_performance_scenario_1(controller, chemicals, min_ms1_intensity, +# fullscan_filename, P_peaks_df, +# matching_mz_tol, matching_rt_tol, +# chem_to_frag_events=None): + +# if chem_to_frag_events is None: # read MS2 fragmentation events from pickled controller +# chem_to_frag_events = get_frag_events(controller, 2) + +# # match xcms picked ms1 peaks to fragmentation peaks +# detected_ms1 = df_to_chemicals(P_peaks_df, fullscan_filename) +# matches_fullscan = match(detected_ms1, chemicals, matching_mz_tol, matching_rt_tol, verbose=False) +# matched_frags = set(matches_fullscan.values()) +# print('%d/%d %d/%d' % (len(matches_fullscan), len(detected_ms1), len(matched_frags), len(chemicals))) + +# # ms1 peaks that are also fragmented +# positives = [] +# for ms1_peak in matches_fullscan: +# frag_peak = matches_fullscan[ms1_peak] +# frag_events = chem_to_frag_events[frag_peak] +# if len(frag_events) > 0: +# positives.append(frag_peak) + +# # fragmentation peaks that are not in ms1 peaks +# negatives = [] +# for frag_peak in chemicals: +# if frag_peak not in matched_frags: +# frag_events = chem_to_frag_events[frag_peak] +# if len(frag_events) > 0: +# negatives.append(frag_peak) + +# positives_count = get_chem_frag_counts(positives, chem_to_frag_events, min_ms1_intensity) +# negatives_count = get_chem_frag_counts(negatives, chem_to_frag_events, min_ms1_intensity) + +# # peaks from ground truth (found in full-scan files) that are fragmented above the minimum intensity threshold +# tp = [chem for chem in positives if positives_count[chem]['good'] > 0 and positives_count[chem]['bad'] == 0] +# tp = len(tp) + +# # peaks from ground truth that are not fragmented + peaks from ground truth that are fragmented below the minimum intensity threshold. +# fp = len(detected_ms1) - tp + +# # peaks not from ground truth that are fragmented above the minimum intensity threshold. +# fn = [chem for chem in negatives if negatives_count[chem]['good'] > 0 and negatives_count[chem]['bad'] == 0] +# fn = len(fn) + +# prec, rec, f1 = compute_pref_rec_f1(tp, fp, fn) +# return tp, fp, fn, prec, rec, f1 + + +def compute_performance_scenario_2(controller, dataset, min_ms1_intensity, + fullscan_filename, fragfile_filename, + fullscan_peaks_df, fragmentation_peaks_df, + matching_mz_tol, matching_rt_tol, + chem_to_frag_events=None): + if chem_to_frag_events is None: # read MS2 fragmentation events from pickled controller + chem_to_frag_events = get_frag_events(controller, 2) + + # load the list of xcms-picked peaks + detected_from_fullscan = df_to_chemicals(fullscan_peaks_df, fullscan_filename) + detected_from_fragfile = df_to_chemicals(fragmentation_peaks_df, fragfile_filename) + + # match with xcms peak-picked ms1 data from fullscan file + matches_fullscan = match(dataset, detected_from_fullscan, matching_mz_tol, matching_rt_tol, verbose=False) + + # match with xcms peak-picked ms1 data from fragmentation file + matches_fragfile = match(dataset, detected_from_fragfile, matching_mz_tol, matching_rt_tol, verbose=False) + + # check if matched and set a flag to indicate that + update_matched_status(dataset, matches_fullscan, matches_fragfile) + + # True positive: a peak that is fragmented above the minimum MS1 intensity and is picked by XCMS from + # the MS1 information in the DDA file and is picked in the fullscan file. + found_in_both = list(filter(lambda x: x.found_in_fullscan and x.found_in_fragfile, dataset)) + frag_count = get_chem_frag_counts(found_in_both, chem_to_frag_events, min_ms1_intensity) + tp = [chem for chem in found_in_both if frag_count[chem]['good'] > 0 and frag_count[chem]['bad'] == 0] + tp = len(tp) + + # False positive: any peak that is above minimum intensity and is picked by XCMS + # from the DDA file but is not picked from the fullscan. + found_in_dda_only = list(filter(lambda x: not x.found_in_fullscan and x.found_in_fragfile, dataset)) + frag_count = get_chem_frag_counts(found_in_dda_only, chem_to_frag_events, min_ms1_intensity) + fp = [chem for chem in found_in_dda_only if frag_count[chem]['good'] > 0 and frag_count[chem]['bad'] == 0] + fp = len(fp) + + # False negative: any peak that is picked from fullscan data, and is not fragmented, or + # is fragmented below the minimum intensity. + found_in_fullscan = list(filter(lambda x: x.found_in_fullscan, dataset)) + fn = len(found_in_fullscan) - tp + + prec, rec, f1 = compute_pref_rec_f1(tp, fp, fn) + return tp, fp, fn, prec, rec, f1 + + +def get_frag_events(controller, ms_level): + ''' + Gets the fragmentation events for all chemicals for an ms level from the controller + :param controller: A Top-N controller object + :param ms_level: The MS-level (usually 2) + :return: A dictionary where keys are chemicals and values are a list of fragmentation events + ''' + filtered_frag_events = list(filter(lambda x: x.ms_level == ms_level, controller.mass_spec.fragmentation_events)) + chem_to_frag_events = defaultdict(list) + for frag_event in filtered_frag_events: + key = frag_event.chem + chem_to_frag_events[key].append(frag_event) + return dict(chem_to_frag_events) + + +def count_frag_events(chem, chem_to_frag_events, min_ms1_intensity): + ''' + Counts how many good and bad fragmentation events for each chemical (key). + Good fragmentation events are defined as fragmentation events that occur when at the time of fragmentation, + the chemical MS1 intensity is above the min_ms1_intensity threshold. + :param chem: the chemical to count + :param chem_to_frag_events: a dictionary of chemicals to frag events (from get_frag_events above()) + :return: a tuple of good and bad fragmentation event counts + ''' + frag_events = chem_to_frag_events[chem] + good_count = 0 + bad_count = 0 + for frag_event in frag_events: + chem = frag_event.chem + query_rt = frag_event.query_rt + if get_absolute_intensity(chem, query_rt) < min_ms1_intensity: + bad_count += 1 + else: + good_count += 1 + return good_count, bad_count + + +def get_chem_frag_counts(chem_list, chem_to_frag_events, min_ms1_intensity): + # get the count of good/bad fragmentation events for all chemicals in chem_list + results = {} + for i in range(len(chem_list)): + chem = chem_list[i] + try: + good_count, bad_count = count_frag_events(chem, chem_to_frag_events, min_ms1_intensity) + except KeyError: + good_count = 0 + bad_count = 0 + results[chem] = { + 'good': good_count, + 'bad': bad_count + } + return results + + +def update_matched_status(dataset, matches_fullscan, matches_fragfile): + ''' + Update a boolean flag in the Chemical object that tells us if it is found in fullscan or fragmentation data + :param dataset: a list of Chemicals + :param matches_fullscan: the result of matching Chemicals in dataset to fullscan file + :param matches_fragfile: the result of matching Chemicals in dataset to fragmentation file + :return: None, but the Chemical objects in dataset is modified + ''' + found_in_fullscan = 0 + found_in_fragfile = 0 + for chem in dataset: + if matches_fullscan is not None: # check if a match is found in fullscan mzML + if chem in matches_fullscan: + chem.found_in_fullscan = True + found_in_fullscan += 1 + else: + chem.found_in_fullscan = False + + if matches_fragfile is not None: # check if a match is found in fragmentation mzML + if chem in matches_fragfile: + chem.found_in_fragfile = True + found_in_fragfile += 1 + else: + chem.found_in_fragfile = False + + print('Matched %d/%d in fullscan data, %d/%d in fragmentation data' % (found_in_fullscan, len(dataset), + found_in_fragfile, len(dataset))) + + +def compute_pref_rec_f1(tp, fp, fn): + prec = tp / (tp + fp) + rec = tp / (tp + fn) + f1 = (2 * prec * rec) / (prec + rec) + return prec, rec, f1 + + +def calculate_performance(params): + # get parameters + fragfile = params['fragfile'] + N = params['N'] + rt_tol = params['rt_tol'] + roi_mz_tol = params['roi_mz_tol'] + roi_min_ms1_intensity = params['roi_min_ms1_intensity'] + fragmentation_min_ms1_intensity = params['fragmentation_min_ms1_intensity'] + min_rt = params['min_rt'] + max_rt = params['max_rt'] + roi_min_length = params['roi_min_length'] + fullscan_filename = params['fullscan_filename'] + P_peaks_df = params['P_peaks_df'] + Q_peaks_df = params['Q_peaks_df'] + matching_mz_tol = params['matching_mz_tol'] + matching_rt_tol = params['matching_rt_tol'] + scenario = params['scenario'] + + controller_file = params['controller_file'] + chemicals_file = params['chemicals_file'] + + if chemicals_file.endswith('.p'): + print('Loading chemicals') + chemicals = load_obj(chemicals_file) + else: + print('Extracting chemicals') + chemicals = get_chemicals(chemicals_file, roi_mz_tol, roi_min_ms1_intensity, min_rt, max_rt, + min_length=roi_min_length) + + if type(chemicals) == list: + chemicals = np.array(chemicals) + + if controller_file.endswith('.p'): + print('Loading fragmentation events') + controller = load_obj(controller_file) + chem_to_frag_events = None + else: + print('Extracting fragmentation events') + controller = None + precursor_df = get_precursor_info(controller_file) + chem_to_frag_events = get_chem_to_frag_events(chemicals, precursor_df) + + # compute performance under each scenario + print('Computing performance under scenario %d' % scenario) + tp, fp, fn, prec, rec, f1 = 0, 0, 0, 0, 0, 0 + if scenario == 1: + tp, fp, fn, prec, rec, f1 = compute_performance_scenario_1(controller, chemicals, + fragmentation_min_ms1_intensity, + fullscan_filename, P_peaks_df, + matching_mz_tol, matching_rt_tol, + chem_to_frag_events=chem_to_frag_events) + elif scenario == 2: + fragfile_filename = os.path.basename(fragfile) + tp, fp, fn, prec, rec, f1 = compute_performance_scenario_2(controller, chemicals, + fragmentation_min_ms1_intensity, + fullscan_filename, fragfile_filename, + P_peaks_df, Q_peaks_df, matching_mz_tol, + matching_rt_tol, + chem_to_frag_events=chem_to_frag_events) + + return N, rt_tol, scenario, tp, fp, fn, prec, rec, f1 + + +def get_chem_to_frag_events(chemicals, ms1_df): + # used for searching later + min_rts = np.array([min(chem.chromatogram.raw_rts) for chem in chemicals]) + max_rts = np.array([max(chem.chromatogram.raw_rts) for chem in chemicals]) + min_mzs = np.array([min(chem.chromatogram.raw_mzs) for chem in chemicals]) + max_mzs = np.array([max(chem.chromatogram.raw_mzs) for chem in chemicals]) + + # loop over each fragmentation event in ms1_df, attempt to match it to chemicals + chem_to_frag_events = defaultdict(list) + for idx, row in ms1_df.iterrows(): + query_rt = row['ms1_scan_rt'] + query_mz = row['ms1_mz'] + query_intensity = row['ms1_intensity'] + scan_id = row['ms2_scan_id'] + + chem = None + idx = _get_chem_indices(query_mz, query_rt, min_mzs, max_mzs, min_rts, max_rts) + if len(idx) == 1: # single match + chem = chemicals[idx][0] + + elif len( + idx) > 1: # multiple matches, find the closest in intensity to query_intensity at the time of fragmentation + matches = chemicals[idx] + possible_intensities = np.array([get_absolute_intensity(chem, query_rt) for chem in matches]) + closest = find_nearest_index_in_array(possible_intensities, query_intensity) + chem = matches[closest] + + # create frag event for the given chem + if chem is not None: + ms_level = 2 + peaks = [] # we don't know which ms2 peaks are linked to this chem object + # key = get_key(chem) + frag_event = FragmentationEvent(chem, query_rt, ms_level, peaks, scan_id) + chem_to_frag_events[chem].append(frag_event) + return dict(chem_to_frag_events) + + +def get_chemicals(mzML_file, mz_tol, min_ms1_intensity, start_rt, stop_rt, min_length=1): + ''' + Extract ROI from an mzML file and turn them into UnknownChemical objects + :param mzML_file: input mzML file + :param mz_tol: mz tolerance for ROI extraction + :param min_ms1_intensity: ROI will only be kept if it has one point above this threshold + :param start_rt: start RT to extract ROI + :param stop_rt: end RT to extract ROI + :return: a list of UnknownChemical objects + ''' + min_intensity = 0 + good_roi, junk = make_roi(mzML_file, mz_tol=mz_tol, mz_units='ppm', min_length=min_length, + min_intensity=min_intensity, start_rt=start_rt, stop_rt=stop_rt) + + # keep ROI that have at least one point above the minimum to fragment threshold + keep = [] + for roi in good_roi: + if np.count_nonzero(np.array(roi.intensity_list) > min_ms1_intensity) > 0: + keep.append(roi) + + ps = None # unused + rtcc = RoiToChemicalCreator(ps, keep) + chemicals = np.array(rtcc.chemicals) + return chemicals + + +def evaluate_serial(all_params): + results = [] + for params in all_params: + res = calculate_performance(params) + results.append(res) + print('N=%d rt_tol=%d scenario=%d tp=%d fp=%d fn=%d prec=%.3f rec=%.3f f1=%.3f\n' % res) + result_df = pd.DataFrame(results, columns=['N', 'rt_tol', 'scenario', 'TP', 'FP', 'FN', 'Prec', 'Rec', 'F1']) + return result_df + + +def evaluate_parallel(all_params, pushed_dict=None): + import ipyparallel as ipp + rc = ipp.Client() + dview = rc[:] # use all engines​ + with dview.sync_imports(): + import os + from vimms.Common import load_obj + + if pushed_dict is not None: + dview.push(pushed_dict) + + results = dview.map_sync(calculate_performance, all_params) + result_df = pd.DataFrame(results, columns=['N', 'rt_tol', 'scenario', 'TP', 'FP', 'FN', 'Prec', 'Rec', 'F1']) + return result_df \ No newline at end of file diff --git a/Synthetic data creation scripts/vimms/Roi.py b/Synthetic data creation scripts/vimms/Roi.py new file mode 100644 index 00000000..054d0704 --- /dev/null +++ b/Synthetic data creation scripts/vimms/Roi.py @@ -0,0 +1,336 @@ +import bisect +import math +from collections import OrderedDict + +import numpy as np +import pylab as plt +import pymzml +from scipy.stats import pearsonr +import os + +from vimms.Chemicals import ChemicalCreator, UnknownChemical, GET_MS2_BY_PEAKS +from vimms.Chromatograms import EmpiricalChromatogram +from vimms.Common import PROTON_MASS, CHEM_NOISE, save_obj + +POS_TRANSFORMATIONS = OrderedDict() +POS_TRANSFORMATIONS['M+H'] = lambda mz: (mz + PROTON_MASS) +POS_TRANSFORMATIONS['[M+ACN]+H'] = lambda mz: (mz + 42.033823) +POS_TRANSFORMATIONS['[M+CH3OH]+H'] = lambda mz: (mz + 33.033489) +POS_TRANSFORMATIONS['[M+NH3]+H'] = lambda mz: (mz + 18.033823) +POS_TRANSFORMATIONS['M+Na'] = lambda mz: (mz + 22.989218) +POS_TRANSFORMATIONS['M+K'] = lambda mz: (mz + 38.963158) +POS_TRANSFORMATIONS['M+2Na-H'] = lambda mz: (mz + 44.971160) +POS_TRANSFORMATIONS['M+ACN+Na'] = lambda mz: (mz + 64.015765) +POS_TRANSFORMATIONS['M+2Na-H'] = lambda mz: (mz + 44.971160) +POS_TRANSFORMATIONS['M+2K+H'] = lambda mz: (mz + 76.919040) +POS_TRANSFORMATIONS['[M+DMSO]+H'] = lambda mz: (mz + 79.02122) +POS_TRANSFORMATIONS['[M+2ACN]+H'] = lambda mz: (mz + 83.060370) +POS_TRANSFORMATIONS['2M+H'] = lambda mz: (mz * 2) + 1.007276 +POS_TRANSFORMATIONS['M+ACN+Na'] = lambda mz: (mz + 64.015765) +POS_TRANSFORMATIONS['2M+NH4'] = lambda mz: (mz * 2) + 18.033823 + + +# Object to store a RoI +# Maintains 3 lists -- mz, rt and intensity +# When a new point (mz,rt,intensity) is added, it updates the +# list and the mean mz which is required. +class Roi(object): + def __init__(self, mz, rt, intensity): + self.mz_list = [mz] + self.rt_list = [rt] + self.intensity_list = [intensity] + self.n = 1 + self.mz_sum = mz + + def get_mean_mz(self): + return self.mz_sum / self.n + + def get_max_intensity(self): + return max(self.intensity_list) + + def add(self, mz, rt, intensity): + self.mz_list.append(mz) + self.rt_list.append(rt) + self.intensity_list.append(intensity) + self.mz_sum += mz + self.n += 1 + + def __lt__(self, other): + return self.get_mean_mz() <= other.get_mean_mz() + + def to_chromatogram(self): + if self.n == 0: + return None + chrom = EmpiricalChromatogram(np.array(self.rt_list), np.array(self.mz_list), np.array(self.intensity_list)) + return chrom + + def __repr__(self): + return 'ROI with data points=%d mz (%.4f-%.4f) rt (%.4f-%.4f)' % ( + self.n, + self.mz_list[0], self.mz_list[-1], + self.rt_list[0], self.rt_list[-1]) + + +# Find the RoI that a particular mz falls into +# If it falls into nothing, return None +# mz_tol is the window above and below the +# mean_mz of the RoI. E.g. if mz_tol = 1 Da, then it looks +# plus and minus 1Da +def match(mz, roi_list, mz_tol, mz_units='Da'): + if len(roi_list) == 0: + return None + pos = bisect.bisect_right(roi_list, mz) + if pos == len(roi_list): + return None + if pos == 0: + return None + + if mz_units == 'Da': + dist_left = mz.get_mean_mz() - roi_list[pos - 1].get_mean_mz() + dist_right = roi_list[pos].get_mean_mz() - mz.get_mean_mz() + else: # ppm + dist_left = 1e6 * (mz.get_mean_mz() - roi_list[pos - 1].get_mean_mz()) / mz.get_mean_mz() + dist_right = 1e6 * (roi_list[pos].get_mean_mz() - mz.get_mean_mz()) / mz.get_mean_mz() + + if dist_left < mz_tol and dist_right > mz_tol: + return roi_list[pos - 1] + elif dist_left > mz_tol and dist_right < mz_tol: + return roi_list[pos] + elif dist_left < mz_tol and dist_right < mz_tol: + if dist_left <= dist_right: + return roi_list[pos - 1] + else: + return roi_list[pos] + else: + return None + + +def roi_correlation(roi1, roi2, min_rt_point_overlap=5, method='pearson'): + # flip around so that roi1 starts earlier (or equal) + if roi2.rt_list[0] < roi1.rt_list[0]: + temp = roi2 + roi2 = roi1 + roi1 = temp + + # check that they meet the min_rt_point overlap + if roi1.rt_list[-1] < roi2.rt_list[0]: + # no overlap at all + return 0.0 + + # find the position of the first element in roi2 in roi1 + pos = roi1.rt_list.index(roi2.rt_list[0]) + + # print roi1.rt_list + # print roi2.rt_list + # print pos + + total_length = max([len(roi1.rt_list), len(roi2.rt_list) + pos]) + # print total_length + + r1 = np.zeros((total_length), np.double) + r2 = np.zeros_like(r1) + + r1[:len(roi1.rt_list)] = roi1.intensity_list + r2[pos:pos + len(roi2.rt_list)] = roi2.intensity_list + + # print + # for i,a in enumerate(r1): + # print "{:10.4f}\t{:10.4f}".format(a,r2[i]) + if method == 'pearson': + r, _ = pearsonr(r1, r2) + else: + r = cosine_score(r1, r2) + + return r + + +def cosine_score(u, v): + numerator = (u * v).sum() + denominator = np.sqrt((u * u).sum()) * np.sqrt((v * v).sum()) + return numerator / denominator + + +# Make the RoI from an input file +# mz_units = Da for Daltons +# mz_units = ppm for ppm +def make_roi(input_file, mz_tol=0.001, mz_units='Da', min_length=10, min_intensity=50000, start_rt=0, stop_rt=10000000): + # input_file = 'Beer_multibeers_1_fullscan1.mzML' + + if not mz_units == 'Da' and not mz_units == 'ppm': + print("Unknown mz units, use Da or ppm") + return None, None + + run = pymzml.run.Reader(input_file, MS1_Precision=5e-6, + extraAccessions=[('MS:1000016', ['value', 'unitName'])], + obo_version='4.0.1') + + live_roi = [] + dead_roi = [] + junk_roi = [] + + for spectrum in run: + # print spectrum['centroid_peaks'] + if spectrum['ms level'] == 1: + live_roi.sort() + # current_ms1_scan_rt, units = spectrum['scan start time'] # this no longer works + current_ms1_scan_rt, units = spectrum.scan_time + if units == 'minute': + current_ms1_scan_rt *= 60.0 + + if current_ms1_scan_rt < start_rt: + continue + if current_ms1_scan_rt > stop_rt: + break + + # print current_ms1_scan_rt + # print spectrum.peaks + not_grew = set(live_roi) + for mz, intensity in spectrum.peaks('raw'): + if intensity >= min_intensity: + match_roi = match(Roi(mz, 0, 0), live_roi, mz_tol, mz_units=mz_units) + if match_roi: + match_roi.add(mz, current_ms1_scan_rt, intensity) + if match_roi in not_grew: + not_grew.remove(match_roi) + else: + bisect.insort_right(live_roi, Roi(mz, current_ms1_scan_rt, intensity)) + + for roi in not_grew: + if roi.n >= min_length: + dead_roi.append(roi) + else: + junk_roi.append(roi) + pos = live_roi.index(roi) + del live_roi[pos] + + # print("Scan @ {}, {} live ROIs".format(current_ms1_scan_rt, len(live_roi))) + + # process all the live ones - keeping only those that + # are longer than the minimum length + good_roi = dead_roi + for roi in live_roi: + if roi.n >= min_length: + good_roi.append(roi) + else: + junk_roi.append(roi) + return good_roi, junk_roi + + +def greedy_roi_cluster(roi_list, corr_thresh=0.75, corr_type='cosine'): + # sort in descending intensity + roi_list_copy = [r for r in roi_list] + roi_list_copy.sort(key=lambda x: max(x.intensity_list), reverse=True) + roi_clusters = [] + while len(roi_list_copy) > 0: + roi_clusters.append([roi_list_copy[0]]) + remove_idx = [0] + if len(roi_list_copy) > 1: + for i, r in enumerate(roi_list_copy[1:]): + corr = roi_correlation(roi_list_copy[0], r) + if corr > corr_thresh: + roi_clusters[-1].append(r) + remove_idx.append(i + 1) + remove_idx.sort(reverse=True) + for r in remove_idx: + del roi_list_copy[r] + + return roi_clusters + + +class RoiToChemicalCreator(ChemicalCreator): + """ + Turns ROI to Chemical objects + """ + + def __init__(self, peak_sampler, all_roi): + super().__init__(peak_sampler) + self.rois_data = all_roi + self.ms_levels = 2 + self.crp_samples = [[] for i in range(self.ms_levels)] + self.crp_index = [[] for i in range(self.ms_levels)] + self.alpha = math.inf + self.counts = [[] for i in range(self.ms_levels)] + if self.ms_levels > 2: + self.logger.warning( + "Warning ms_level > 3 not implemented properly yet. Uses scaled ms_level = 2 information for now") + + self.chromatograms = [] + self.chemicals = [] + for i in range(len(self.rois_data)): + if i % 50000 == 0: + self.logger.debug('%6d/%6d' % (i, len(self.rois_data))) + roi = self.rois_data[i] + + # raise numpy warning as exception, see https://stackoverflow.com/questions/15933741/how-do-i-catch-a-numpy-warning-like-its-an-exception-not-just-for-testing + chrom = None + with np.errstate(divide='raise'): + try: + chrom = roi.to_chromatogram() + except FloatingPointError: + self.logger.debug('Invalid chromatogram {}'.format(i)) + except ZeroDivisionError: + self.logger.debug('Invalid chromatogram {}'.format(i)) + + if chrom is not None: + chem = self._to_unknown_chemical(chrom) + if self.peak_sampler is not None: + try: + # TODO: initialise chemical with only 1 child for the purpose of experiment, we might need to improve this + chem.children = self._get_children(GET_MS2_BY_PEAKS, chem, n_peaks=1) + except KeyError: + pass + self.chromatograms.append(chrom) + self.chemicals.append(chem) + assert len(self.chromatograms) == len(self.chemicals) + self.logger.info('Found %d ROIs above thresholds' % len(self.chromatograms)) + + def sample(self, chromatogram_creator, mz_range, rt_range, min_ms1_intensity, n_ms1_peaks, ms_levels=2, + chemical_type=None, + formula_list=None, compound_list=None, alpha=math.inf, fixed_mz=False, adduct_proportion_cutoff=0.05): + return NotImplementedError() + + def sample_from_chromatograms(self, chromatogram_creator, min_rt, max_rt, min_ms1_intensity, ms_levels=2): + return NotImplementedError() + + def _to_unknown_chemical(self, chrom): + idx = np.argmax(chrom.raw_intensities) # find intensity apex + mz = chrom.raw_mzs[idx] + + # In the MassSpec, we assume that chemical starts eluting from chem.rt + chem.chromatogram.rts (normalised to start from 0) + # So here, we have to set set chemical rt to start from the minimum of chromatogram raw rts, so it elutes correct. + # rt = chrom.raw_rts[idx] + rt = min(chrom.raw_rts) + + max_intensity = chrom.raw_intensities[idx] + mz = mz - PROTON_MASS + chem = UnknownChemical(mz, rt, max_intensity, chrom, None) + chem.type = CHEM_NOISE + return chem + + def plot_chems(self, n_plots, reverse=False): + sorted_chems = sorted(self.chemicals, key=lambda chem: chem.chromatogram.roi.num_scans()) + if reverse: + sorted_chems.reverse() + for c in sorted_chems[0:n_plots]: + chrom = c.chromatogram + plt.plot(chrom.raw_rts, chrom.raw_intensities) + plt.show() + + +def extract_roi(file_names, out_dir, pattern, mzml_path, ps, roi_mz_tol=10, roi_min_length=2, roi_min_intensity=1.75E5, roi_start_rt=0, + roi_stop_rt=1440): + for i in range(len(file_names)): # for all mzML files in file_names + # extract ROI + mzml_file = os.path.join(mzml_path, file_names[i]) + good_roi, junk = make_roi(mzml_file, mz_tol=roi_mz_tol, mz_units='ppm', min_length=roi_min_length, + min_intensity=roi_min_intensity, start_rt=roi_start_rt, stop_rt=roi_stop_rt) + all_roi = good_roi + + # turn ROI to chemicals + rtcc = RoiToChemicalCreator(ps, all_roi) + data = rtcc.chemicals + + # save extracted chemicals + basename = os.path.basename(file_names[i]) + out_name = pattern % int(basename.split('_')[2]) + save_obj(data, os.path.join(out_dir, out_name)) \ No newline at end of file diff --git a/Synthetic data creation scripts/vimms/SpectralUtils.py b/Synthetic data creation scripts/vimms/SpectralUtils.py new file mode 100644 index 00000000..e84053c2 --- /dev/null +++ b/Synthetic data creation scripts/vimms/SpectralUtils.py @@ -0,0 +1,136 @@ +# Collection of methods to deal with mass spectra mzML files +import numpy as np +import pandas as pd +import pymzml + +from vimms.Common import get_rt +from vimms.Roi import make_roi, RoiToChemicalCreator + +######################################################################################################################## +# Data extraction methods +######################################################################################################################## + + +def get_chemicals(mzML_file, mz_tol, min_ms1_intensity, start_rt, stop_rt, min_length=1): + ''' + Extract ROI from an mzML file and turn them into UnknownChemical objects + :param mzML_file: input mzML file + :param mz_tol: mz tolerance for ROI extraction + :param min_ms1_intensity: ROI will only be kept if it has one point above this threshold + :param start_rt: start RT to extract ROI + :param stop_rt: end RT to extract ROI + :return: a list of UnknownChemical objects + ''' + min_intensity = 0 + good_roi, junk = make_roi(mzML_file, mz_tol=mz_tol, mz_units='ppm', min_length=min_length, + min_intensity=min_intensity, start_rt=start_rt, stop_rt=stop_rt) + + # keep ROI that have at least one point above the minimum to fragment threshold + keep = [] + for roi in good_roi: + if np.count_nonzero(np.array(roi.intensity_list) > min_ms1_intensity) > 0: + keep.append(roi) + + ps = None # unused + rtcc = RoiToChemicalCreator(ps, keep) + chemicals = np.array(rtcc.chemicals) + return chemicals + + +def get_precursor_info(fragfile): + """ + Get (MS1) precursor peaks and their associated MS2 scans from an mzML file + :param fragfile: path to an mzML file + :return: a pandas dataframe that contains all the ms1 and ms2 information + """ + run = pymzml.run.Reader(fragfile, obo_version='4.0.1', + MS1_Precision=5e-6, + extraAccessions=[('MS:1000016', ['value', 'unitName'])]) + + last_ms1_peaklist = None + last_ms1_scan_no = 0 + isolation_window = 0.5 # Dalton + data = [] + for scan_no, scan in enumerate(run): + if scan.ms_level == 1: # save the last ms1 scan that we've seen + last_ms1_peaklist = _get_peaks(scan) + last_ms1_scan_no = scan_no + + # TODO: it's better to use the "isolation window target m/z" field in the mzML file for matching + precursors = scan.selected_precursors + if len(precursors) > 0: + assert len(precursors) == 1 # assume exactly 1 precursor peak for each ms2 scan + precursor = precursors[0] + + try: + scan_rt = get_rt(scan) + precursor_mz = precursor['mz'] + precursor_intensity = precursor['i'] + res = _find_precursor_peaks(precursor, last_ms1_peaklist, last_ms1_scan_no, + isolation_window=isolation_window) + ms2_peaklist = _get_peaks(scan) + row = [scan_no, scan_rt, precursor_mz, precursor_intensity, ms2_peaklist] + row.extend(res) + data.append(row) + except ValueError as e: + print(e) + except KeyError as e: + continue # sometimes we can't find the intensity value precursor['i'] in precursors + + columns = ['ms2_scan_id', 'ms2_scan_rt', 'ms2_precursor_mz', 'ms2_precursor_intensity', 'ms2_peaklist', + 'ms1_scan_id', 'ms1_scan_rt', 'ms1_mz', 'ms1_intensity'] + df = pd.DataFrame(data, columns=columns) + + # select only rows where we are sure of the matching, i.e. the intensity values aren't too different + df['intensity_diff'] = np.abs(df['ms2_precursor_intensity'] - df['ms1_intensity']) + idx = (df['intensity_diff'] < 0.1) + ms1_df = df[idx] + return ms1_df + + +######################################################################################################################## +# Private methods +######################################################################################################################## + +def _get_peaks(spectrum): + mzs = spectrum.mz + rts = [get_rt(spectrum)] * len(mzs) + intensities = spectrum.i + peaklist = np.stack([mzs, rts, intensities], axis=1) + return peaklist + + +def _find_precursor_peaks(precursor, last_ms1_peaklist, last_ms1_scan_no, isolation_window=0.5): + selected_ms1, selected_ms1_idx = _find_precursor_ms1(precursor, last_ms1_peaklist, + last_ms1_scan_no, isolation_window) + selected_ms1_mz = selected_ms1[0] + selected_ms1_rt = selected_ms1[1] + selected_ms1_intensity = selected_ms1[2] + res = [last_ms1_scan_no, selected_ms1_rt, selected_ms1_mz, selected_ms1_intensity] + return res + + +def _find_precursor_ms1(precursor, last_ms1_peaklist, last_ms1_scan_no, isolation_window): + precursor_mz = precursor['mz'] + precursor_intensity = precursor['i'] + + # find mz in the last ms1 scan that fall within isolation window + mzs = last_ms1_peaklist[:, 0] + diffs = abs(mzs - precursor_mz) < isolation_window + idx = np.nonzero(diffs)[0] + + if len(idx) == 0: # should never happen!? + raise ValueError('Cannot find precursor peak (%f, %f) in the last ms1 scan %d' % + (precursor_mz, precursor_intensity, last_ms1_scan_no)) + + elif len(idx) == 1: # only one is found + selected_ms1_idx = idx[0] + + else: # found multilple possible ms1 peak, select the largest intensity + possible_ms1 = last_ms1_peaklist[idx, :] + possible_intensities = possible_ms1[:, 2] + closest = np.argmax(possible_intensities) + selected_ms1_idx = idx[closest] + + selected_ms1 = last_ms1_peaklist[selected_ms1_idx, :] + return selected_ms1, selected_ms1_idx \ No newline at end of file diff --git a/Synthetic data creation scripts/vimms/TopNExperiment.py b/Synthetic data creation scripts/vimms/TopNExperiment.py new file mode 100644 index 00000000..620a3447 --- /dev/null +++ b/Synthetic data creation scripts/vimms/TopNExperiment.py @@ -0,0 +1,147 @@ +import os + +from vimms.Common import save_obj, create_if_not_exist +from vimms.Controller import TopNController +from vimms.DataGenerator import DataSource, PeakSampler +from vimms.MassSpec import IndependentMassSpectrometer + + +######################################################################################################################## +# Codes to set up experiments +######################################################################################################################## + + +def run_experiment(param): + ''' + Runs a Top-N experiment + :param param: the experimental parameters + :return: the analysis name that has been successfully ran + ''' + analysis_name = param['analysis_name'] + mzml_out = param['mzml_out'] + pickle_out = param['pickle_out'] + N = param['N'] + rt_tol = param['rt_tol'] + + if os.path.isfile(mzml_out) and os.path.isfile(pickle_out): + print('Skipping %s' % (analysis_name)) + else: + print('Processing %s' % (analysis_name)) + peak_sampler = param['peak_sampler'] + if peak_sampler is None: # extract density from the fragmenatation file + mzml_path = param['mzml_path'] + fragfiles = param['fragfiles'] + fragfile = fragfiles[(N, rt_tol,)] + min_rt = param['min_rt'] + max_rt = param['max_rt'] + peak_sampler = get_peak_sampler(mzml_path, fragfile, min_rt, max_rt) + + mass_spec = IndependentMassSpectrometer(param['ionisation_mode'], param['data'], peak_sampler) + controller = TopNController(mass_spec, param['N'], param['isolation_window'], + param['mz_tol'], param['rt_tol'], param['min_ms1_intensity']) + controller.run(param['min_rt'], param['max_rt'], progress_bar=param['pbar']) + controller.write_mzML(analysis_name, mzml_out) + save_obj(controller, pickle_out) + return analysis_name + + +def get_peak_sampler(mzml_path, fragfile, min_rt, max_rt): + ds = DataSource() + ds.load_data(mzml_path, file_name=fragfile) + kde_min_ms1_intensity = 0 # min intensity to be selected for kdes + kde_min_ms2_intensity = 0 + peak_sampler = PeakSampler(ds, kde_min_ms1_intensity, kde_min_ms2_intensity, min_rt, max_rt) + return peak_sampler + + +def run_parallel_experiment(params): + ''' + Runs experiments in parallel using iParallel library + :param params: the experimental parameter + :return: None + ''' + import ipyparallel as ipp + rc = ipp.Client() + dview = rc[:] # use all engines​ + with dview.sync_imports(): + pass + + analysis_names = dview.map_sync(run_experiment, params) + for analysis_name in analysis_names: + print(analysis_name) + + +def run_serial_experiment(params): + ''' + Runs experiments serially + :param params: the experimental parameter + :return: None + ''' + total = len(params) + for i in range(len(params)): + param = params[i] + print('Processing \t%d/%d\t%s' % (i + 1, total, param['analysis_name'])) + run_experiment(param) + + +def get_params(experiment_name, Ns, rt_tols, mz_tol, isolation_window, ionisation_mode, data, peak_sampler, + min_ms1_intensity, min_rt, max_rt, + out_dir, pbar, mzml_path=None, fragfiles=None): + ''' + Creates a list of experimental parameters + :param experiment_name: current experimental name + :param Ns: possible values of N in top-N to test + :param rt_tols: possible values of DEW to test + :param mz_tol: Top-N controller parameter: the m/z window (ppm) to prevent the same precursor ion to be fragmented again + :param isolation_window: Top-N controller parameter: the m/z window (ppm) to prevent the same precursor ion to be fragmented again + :param ionisation_mode: Top-N controller parameter: either positive or negative + :param data: chemicals to fragment + :param peak sampler: trained densities to sample values during simulatin + :param min_ms1_intensity: Top-N controller parameter: minimum ms1 intensity to fragment + :param min_rt: start RT to simulate + :param max_rt: end RT to simulate + :param out_dir: output directory + :param pbar: progress bar to update + :return: a list of parameters + ''' + create_if_not_exist(out_dir) + print('N =', Ns) + print('rt_tol =', rt_tols) + params = [] + for N in Ns: + for rt_tol in rt_tols: + analysis_name = 'experiment_%s_N_%d_rttol_%d' % (experiment_name, N, rt_tol) + mzml_out = os.path.join(out_dir, '%s.mzML' % analysis_name) + pickle_out = os.path.join(out_dir, '%s.p' % analysis_name) + param_dict = { + 'N': N, + 'mz_tol': mz_tol, + 'rt_tol': rt_tol, + 'min_ms1_intensity': min_ms1_intensity, + 'isolation_window': isolation_window, + 'ionisation_mode': ionisation_mode, + 'data': data, + 'peak_sampler': peak_sampler, + 'min_rt': min_rt, + 'max_rt': max_rt, + 'analysis_name': analysis_name, + 'mzml_out': mzml_out, + 'pickle_out': pickle_out, + 'pbar': pbar + } + if mzml_path is not None: + param_dict['mzml_path'] = mzml_path + if fragfiles is not None: + param_dict['fragfiles'] = fragfiles + params.append(param_dict) + print('len(params) =', len(params)) + return params + + +def get_N_rt_tol_from_qcb_filename(fragfile): + base = os.path.basename(fragfile) + base = os.path.splitext(base)[0] + tokens = base.split('_') + N = int(tokens[1][1:]) + rt_tol = int(tokens[2][3:]) + return N, rt_tol diff --git a/Synthetic data creation scripts/vimms/__init__.py b/Synthetic data creation scripts/vimms/__init__.py new file mode 100644 index 00000000..57dbee80 --- /dev/null +++ b/Synthetic data creation scripts/vimms/__init__.py @@ -0,0 +1 @@ +name = 'vimms' diff --git a/Synthetic data creation scripts/vimms_data_generation/01. Download Data.ipynb b/Synthetic data creation scripts/vimms_data_generation/01. Download Data.ipynb new file mode 100644 index 00000000..d7c1c3b6 --- /dev/null +++ b/Synthetic data creation scripts/vimms_data_generation/01. Download Data.ipynb @@ -0,0 +1,1174 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 1. Download Data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook downloads the necessary example data that will be used in other notebooks. In particular, the notebook does the following:\n", + "\n", + "- Download beer and urine .mzML files used as examples in the paper\n", + "- Download the HMDB database and extract metabolites.\n", + "- Trains kernel density estimators on the mzML files.\n", + "- Extract regions of interests from the mzML files.\n", + "\n", + "**Please run this notebook first to make sure the data files are available for subsequent notebooks.**" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import glob" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.append('..')" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from vimms.DataGenerator import extract_hmdb_metabolite, get_data_source, get_spectral_feature_database\n", + "from vimms.MassSpec import IndependentMassSpectrometer\n", + "from vimms.Controller import SimpleMs1Controller\n", + "from vimms.Common import *\n", + "from vimms.Roi import make_roi, RoiToChemicalCreator, extract_roi" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# set_log_level_info()\n", + "set_log_level_debug()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## a. Download beer and urine files" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here we download the beer and urine .mzML files used as examples in the paper if they don't exist." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "url = 'http://researchdata.gla.ac.uk/870/2/example_data.zip'\n", + "base_dir = os.path.join(os.getcwd(), 'example_data')" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found /home/cjurich/projects/vimms/examples/example_data\n" + ] + } + ], + "source": [ + "if not os.path.isdir(base_dir): # if not exist then download the example data and extract it\n", + " print('Creating %s' % base_dir) \n", + " out_file = 'example_data.zip'\n", + " download_file(url, out_file)\n", + " extract_zip_file(out_file, delete=True)\n", + "else:\n", + " print('Found %s' % base_dir)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## b. Download metabolites from HMDB" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next we load a pre-processed pickled file of database metabolites in the `data_dir` folder. If it is not found, then create the file by downloading and extracting the metabolites from HMDB." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded 114202 DatabaseCompounds from /home/cjurich/projects/vimms/examples/example_data/hmdb_compounds.p\n" + ] + } + ], + "source": [ + "compound_file = Path(base_dir, 'hmdb_compounds.p')\n", + "hmdb_compounds = load_obj(compound_file)\n", + "if hmdb_compounds is None: # if file does not exist\n", + "\n", + " # download the entire HMDB metabolite database\n", + " url = 'http://www.hmdb.ca/system/downloads/current/hmdb_metabolites.zip'\n", + "\n", + " out_file = download_file(url)\n", + " compounds = extract_hmdb_metabolite(out_file, delete=True)\n", + " save_obj(compounds, compound_file)\n", + "\n", + "else:\n", + " print('Loaded %d DatabaseCompounds from %s' % (len(hmdb_compounds), compound_file))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## c. Generate Spectral Feature Database" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this section we demonstrate how ViMMS constructs the spectral feature database containing information, such as the densities of m/z, RT and intensities, scan durations, MS2 peaks, from the example Beer mzML files. The spectral feature database will be used to sample for various features during the simulation later.\n", + "\n", + "The following two methods `get_data_source` and `get_spectral_feature_database` from ViMMS will be used. \n", + "- `get_data_source` loads a `DataSource` object that stores information on a set of .mzML files\n", + "- `get_spectral_feature_database` extracts relevant features from .mzML files that have been loaded into the DataSource. \n", + "\n", + "The parameter below should work for most cases, however for different data, it might be necessary to adjust the `min_rt` and `max_rt` values." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "filename = None # if None, use all mzML files found\n", + "min_ms1_intensity = 0 # min MS1 intensity threshold to include a data point for density estimation\n", + "min_ms2_intensity = 0 # min MS2 intensity threshold to include a data point for density estimation\n", + "min_rt = 0 # min RT to include a data point for density estimation\n", + "max_rt = 1440 # max RT to include a data point for density estimation\n", + "bandwidth_mz_intensity_rt = 1.0 # kernel bandwidth parameter to sample (mz, RT, intensity) values during simulation\n", + "bandwidth_n_peaks = 1.0 # kernel bandwidth parameter to sample number of peaks per scan during simulation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load fullscan data and train spectral feature database" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "mzml_path = Path(base_dir, 'beers', 'fullscan', 'mzML')\n", + "xcms_output = Path(mzml_path, 'extracted_peaks_ms1.csv')\n", + "out_file = Path(base_dir, 'peak_sampler_mz_rt_int_19_beers_fullscan.p')" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO : DataSource : Loading Beer_multibeers_7_fullscan1.mzML\n", + "INFO : numexpr.utils : NumExpr defaulting to 8 threads.\n", + "INFO : DataSource : Loading Beer_multibeers_6_fullscan1.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_19_fullscan1.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_4_fullscan1.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_10_fullscan1.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_17_fullscan1.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_3_fullscan1.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_8_fullscan1.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_16_fullscan1.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_12_fullscan1.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_13_fullscan1.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_5_fullscan1.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_2_fullscan1.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_9_fullscan1.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_15_fullscan1.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_14_fullscan1.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_1_fullscan1.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_11_fullscan1.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_18_fullscan1.mzML\n" + ] + } + ], + "source": [ + "ds_fullscan = get_data_source(mzml_path, filename, xcms_output)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : PeakSampler : Extracted 0 MS2 scans\n", + "DEBUG : PeakSampler : Computing parent intensity proportions\n", + "DEBUG : PeakSampler : Extracting scan durations\n", + "DEBUG : PeakSampler : Training KDEs for ms_level=1\n", + "DEBUG : PeakSampler : Retrieving mz_intensity_rt values from <vimms.DataGenerator.DataSource object at 0x7f2e3ec14640>\n", + "INFO : DataSource : Using values from XCMS peaklist\n", + "DEBUG : PeakSampler : Retrieving n_peaks values from <vimms.DataGenerator.DataSource object at 0x7f2e3ec14640>\n", + "DEBUG : PeakSampler : Training KDEs for ms_level=2\n", + "DEBUG : PeakSampler : Retrieving mz_intensity_rt values from <vimms.DataGenerator.DataSource object at 0x7f2e3ec14640>\n", + "INFO : DataSource : Using values from scans\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'vimms.DataGenerator.PeakSampler'> to /home/cjurich/projects/vimms/examples/example_data/peak_sampler_mz_rt_int_19_beers_fullscan.p\n" + ] + } + ], + "source": [ + "ps = get_spectral_feature_database(ds_fullscan, filename, min_ms1_intensity, min_ms2_intensity, min_rt, max_rt,\n", + " bandwidth_mz_intensity_rt, bandwidth_n_peaks, out_file)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Peak mz=428.7252 rt=1081.71 intensity=2123572.19 ms_level=1,\n", + " Peak mz=411.0711 rt=602.91 intensity=12226691.90 ms_level=1,\n", + " Peak mz=126.0966 rt=267.76 intensity=93690.81 ms_level=1,\n", + " Peak mz=206.4157 rt=480.98 intensity=62336.55 ms_level=1,\n", + " Peak mz=495.0441 rt=546.91 intensity=907325.70 ms_level=1,\n", + " Peak mz=210.3872 rt=289.68 intensity=582443.59 ms_level=1,\n", + " Peak mz=249.6236 rt=399.33 intensity=3002228.13 ms_level=1,\n", + " Peak mz=122.2433 rt=76.53 intensity=16820.22 ms_level=1,\n", + " Peak mz=150.6783 rt=225.97 intensity=332209.90 ms_level=1,\n", + " Peak mz=150.3422 rt=521.56 intensity=1986196.81 ms_level=1]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ps.get_peak(1, 10) # try to sample 10 MS1 peaks" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load fragmentation data and train spectral feature database" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "mzml_path = Path(base_dir, 'beers', 'fragmentation', 'mzML')\n", + "xcms_output = Path(mzml_path, 'extracted_peaks_ms1.csv')\n", + "out_file = Path(base_dir, 'peak_sampler_mz_rt_int_19_beers_fragmentation.p')" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO : DataSource : Loading Beer_multibeers_8_T10_POS.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_12_T10_POS.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_9_T10_POS.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_1_T10_POS.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_10_T10_POS.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_19_T10_POS.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_6_T10_POS.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_3_T10_POS.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_15_T10_POS.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_2_T10_POS.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_17_T10_POS.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_4_T10_POS.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_16_T10_POS.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_13_T10_POS.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_11_T10_POS.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_18_T10_POS.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_7_T10_POS.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_14_T10_POS.mzML\n", + "INFO : DataSource : Loading Beer_multibeers_5_T10_POS.mzML\n" + ] + } + ], + "source": [ + "ds_fragmentation = get_data_source(mzml_path, filename, xcms_output)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : PeakSampler : Extracted 138969 MS2 scans\n", + "DEBUG : PeakSampler : Computing parent intensity proportions\n", + "DEBUG : PeakSampler : Extracting scan durations\n", + "DEBUG : PeakSampler : Training KDEs for ms_level=1\n", + "DEBUG : PeakSampler : Retrieving mz_intensity_rt values from <vimms.DataGenerator.DataSource object at 0x7f2e1bef3d60>\n", + "INFO : DataSource : Using values from XCMS peaklist\n", + "DEBUG : PeakSampler : Retrieving n_peaks values from <vimms.DataGenerator.DataSource object at 0x7f2e1bef3d60>\n", + "DEBUG : PeakSampler : Training KDEs for ms_level=2\n", + "DEBUG : PeakSampler : Retrieving mz_intensity_rt values from <vimms.DataGenerator.DataSource object at 0x7f2e1bef3d60>\n", + "INFO : DataSource : Using values from scans\n", + "DEBUG : PeakSampler : Retrieving n_peaks values from <vimms.DataGenerator.DataSource object at 0x7f2e1bef3d60>\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'vimms.DataGenerator.PeakSampler'> to /home/cjurich/projects/vimms/examples/example_data/peak_sampler_mz_rt_int_19_beers_fragmentation.p\n" + ] + } + ], + "source": [ + "ps = get_spectral_feature_database(ds_fragmentation, filename, min_ms1_intensity, min_ms2_intensity, min_rt, max_rt,\n", + " bandwidth_mz_intensity_rt, bandwidth_n_peaks, out_file)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Peak mz=221.7807 rt=214.06 intensity=320377.74 ms_level=1,\n", + " Peak mz=242.0755 rt=342.85 intensity=457720.15 ms_level=1,\n", + " Peak mz=601.3200 rt=420.90 intensity=3570.79 ms_level=1,\n", + " Peak mz=436.5792 rt=801.95 intensity=87740.36 ms_level=1,\n", + " Peak mz=301.6364 rt=249.27 intensity=61586.19 ms_level=1,\n", + " Peak mz=246.0578 rt=262.69 intensity=278773.64 ms_level=1,\n", + " Peak mz=473.5991 rt=633.66 intensity=342773.23 ms_level=1,\n", + " Peak mz=178.9170 rt=1261.88 intensity=25533.11 ms_level=1,\n", + " Peak mz=254.8886 rt=375.12 intensity=718706.28 ms_level=1,\n", + " Peak mz=679.5092 rt=218.24 intensity=361896.01 ms_level=1]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ps.get_peak(1, 10)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Peak mz=70.5123 rt=769.84 intensity=12155.36 ms_level=2,\n", + " Peak mz=306.8993 rt=948.28 intensity=3623.41 ms_level=2,\n", + " Peak mz=97.8524 rt=1016.22 intensity=3337.43 ms_level=2,\n", + " Peak mz=103.5200 rt=342.68 intensity=1281.02 ms_level=2,\n", + " Peak mz=111.4647 rt=543.28 intensity=21201.48 ms_level=2,\n", + " Peak mz=118.6177 rt=437.24 intensity=86636.24 ms_level=2,\n", + " Peak mz=85.0430 rt=1161.27 intensity=557.34 ms_level=2,\n", + " Peak mz=272.8699 rt=253.92 intensity=36855.75 ms_level=2,\n", + " Peak mz=94.4220 rt=368.48 intensity=1134.53 ms_level=2,\n", + " Peak mz=52.8812 rt=923.67 intensity=1059.23 ms_level=2]" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ps.get_peak(2, 10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## d. Extract the ROIs for DsDA Experiments" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "roi_mz_tol = 10\n", + "roi_min_length = 2\n", + "roi_min_intensity = 1.75E5\n", + "roi_start_rt = min_rt\n", + "roi_stop_rt = max_rt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Extract beer ROIs" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 13179\n", + "INFO : RoiToChemicalCreator : Found 13179 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Beer/beer_t10_simulator_files/beer_8.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 11249\n", + "INFO : RoiToChemicalCreator : Found 11249 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Beer/beer_t10_simulator_files/beer_12.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 14842\n", + "INFO : RoiToChemicalCreator : Found 14842 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Beer/beer_t10_simulator_files/beer_9.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 12611\n", + "INFO : RoiToChemicalCreator : Found 12611 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Beer/beer_t10_simulator_files/beer_1.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 11925\n", + "INFO : RoiToChemicalCreator : Found 11925 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Beer/beer_t10_simulator_files/beer_10.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 12945\n", + "INFO : RoiToChemicalCreator : Found 12945 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Beer/beer_t10_simulator_files/beer_19.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 11636\n", + "INFO : RoiToChemicalCreator : Found 11636 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Beer/beer_t10_simulator_files/beer_6.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 9716\n", + "INFO : RoiToChemicalCreator : Found 9716 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Beer/beer_t10_simulator_files/beer_3.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 13068\n", + "INFO : RoiToChemicalCreator : Found 13068 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Beer/beer_t10_simulator_files/beer_15.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 14839\n", + "INFO : RoiToChemicalCreator : Found 14839 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Beer/beer_t10_simulator_files/beer_2.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 14778\n", + "INFO : RoiToChemicalCreator : Found 14778 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Beer/beer_t10_simulator_files/beer_17.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 12029\n", + "INFO : RoiToChemicalCreator : Found 12029 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Beer/beer_t10_simulator_files/beer_4.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 15556\n", + "INFO : RoiToChemicalCreator : Found 15556 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Beer/beer_t10_simulator_files/beer_16.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 10489\n", + "INFO : RoiToChemicalCreator : Found 10489 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Beer/beer_t10_simulator_files/beer_13.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 9971\n", + "INFO : RoiToChemicalCreator : Found 9971 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Beer/beer_t10_simulator_files/beer_11.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 13742\n", + "INFO : RoiToChemicalCreator : Found 13742 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Beer/beer_t10_simulator_files/beer_18.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 12181\n", + "INFO : RoiToChemicalCreator : Found 12181 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Beer/beer_t10_simulator_files/beer_7.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 12840\n", + "INFO : RoiToChemicalCreator : Found 12840 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Beer/beer_t10_simulator_files/beer_14.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 10502\n", + "INFO : RoiToChemicalCreator : Found 10502 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Beer/beer_t10_simulator_files/beer_5.p\n" + ] + } + ], + "source": [ + "file_names = Path(base_dir, 'beers', 'fragmentation', 'mzML').glob('*.mzML')\n", + "out_dir = Path(base_dir,'DsDA', 'DsDA_Beer', 'beer_t10_simulator_files')\n", + "mzml_path = Path(base_dir, 'beers', 'fragmentation', 'mzML')\n", + "\n", + "extract_roi(list(file_names), out_dir, 'beer_%d.p', mzml_path, ps)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Extract urine ROIs" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 16320\n", + "INFO : RoiToChemicalCreator : Found 16320 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Created /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files\n", + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_97.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 16294\n", + "INFO : RoiToChemicalCreator : Found 16294 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_85.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 16321\n", + "INFO : RoiToChemicalCreator : Found 16321 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_2.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 16100\n", + "INFO : RoiToChemicalCreator : Found 16100 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_8.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 15895\n", + "INFO : RoiToChemicalCreator : Found 15895 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_53.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 16885\n", + "INFO : RoiToChemicalCreator : Found 16885 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_72.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 18395\n", + "INFO : RoiToChemicalCreator : Found 18395 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_3.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 13836\n", + "INFO : RoiToChemicalCreator : Found 13836 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_58.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 10211\n", + "INFO : RoiToChemicalCreator : Found 10211 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_32.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 17938\n", + "INFO : RoiToChemicalCreator : Found 17938 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_49.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 17424\n", + "INFO : RoiToChemicalCreator : Found 17424 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_80.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 15601\n", + "INFO : RoiToChemicalCreator : Found 15601 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_54.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 14048\n", + "INFO : RoiToChemicalCreator : Found 14048 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_93.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 11073\n", + "INFO : RoiToChemicalCreator : Found 11073 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_9.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 18560\n", + "INFO : RoiToChemicalCreator : Found 18560 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_105.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 16681\n", + "INFO : RoiToChemicalCreator : Found 16681 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_38.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 20280\n", + "INFO : RoiToChemicalCreator : Found 20280 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_57.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 15677\n", + "INFO : RoiToChemicalCreator : Found 15677 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_51.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 16354\n", + "INFO : RoiToChemicalCreator : Found 16354 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_28.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 13089\n", + "INFO : RoiToChemicalCreator : Found 13089 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_17.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 17858\n", + "INFO : RoiToChemicalCreator : Found 17858 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_52.p\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 13999\n", + "INFO : RoiToChemicalCreator : Found 13999 ROIs above thresholds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/DsDA/DsDA_Urine/urine_t10_simulator_files/urine_18.p\n" + ] + } + ], + "source": [ + "file_names = Path(base_dir, 'urines', 'fragmentation', 'mzML').glob('*.mzML')\n", + "out_dir = Path(base_dir,'DsDA', 'DsDA_Urine', 'urine_t10_simulator_files')\n", + "mzml_path = Path(base_dir, 'urines', 'fragmentation', 'mzML')\n", + "\n", + "extract_roi(list(file_names), out_dir, 'urine_%d.p', mzml_path, ps)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/Synthetic data creation scripts/vimms_data_generation/02. MS1 Simulations.ipynb b/Synthetic data creation scripts/vimms_data_generation/02. MS1 Simulations.ipynb new file mode 100644 index 00000000..2af7ae0a --- /dev/null +++ b/Synthetic data creation scripts/vimms_data_generation/02. MS1 Simulations.ipynb @@ -0,0 +1,321 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 2. Generating a Sample using MS1 Controller" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this notebook, we demonstrate how ViMMS can be used to generate a full-scan mzML file from a single sample. This corresponds to Section 3.1 of the paper." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.append('..')" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from vimms.Chemicals import ChemicalCreator\n", + "from vimms.MassSpec import IndependentMassSpectrometer\n", + "from vimms.Controller import SimpleMs1Controller\n", + "from vimms.Common import *" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Load previously trained spectral feature database and the list of extracted metabolites, created in **01. Download Data.ipynb**." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "base_dir = os.path.abspath('example_data')\n", + "ps = load_obj(Path(base_dir, 'peak_sampler_mz_rt_int_19_beers_fullscan.p'))\n", + "hmdb = load_obj(Path(base_dir, 'hmdb_compounds.p'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Set ViMMS logging level" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "set_log_level_debug()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create Chemicals" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Define an output folder containing our results" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "out_dir = Path(base_dir, 'results', 'MS1_single')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here we generate the chemical objects that will be used in the sample. The chemical objects are generated by sampling from metabolites in the HMDB database." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# the list of ROI sources created in the previous notebook '01. Download Data.ipynb'\n", + "ROI_Sources = [str(Path(base_dir,'DsDA', 'DsDA_Beer', 'beer_t10_simulator_files'))]\n", + "\n", + "# minimum MS1 intensity of chemicals\n", + "min_ms1_intensity = 1.75E5\n", + "\n", + "# m/z and RT range of chemicals\n", + "rt_range = [(0, 1440)]\n", + "mz_range = [(0, 1050)]\n", + "\n", + "# the number of chemicals in the sample\n", + "n_chems = 6500\n", + "\n", + "# maximum MS level (we do not generate fragmentation peaks when this value is 1)\n", + "ms_level = 1" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : ChemicalCreator : Sorting database compounds by masses\n", + "DEBUG : ChemicalCreator : 6500 chemicals to be created.\n", + "DEBUG : ChemicalCreator : Sampling formula 0/6500\n", + "DEBUG : ChemicalCreator : Sampling formula 500/6500\n", + "DEBUG : ChemicalCreator : Sampling formula 1000/6500\n", + "DEBUG : ChemicalCreator : Sampling formula 1500/6500\n", + "DEBUG : ChemicalCreator : Sampling formula 2000/6500\n", + "DEBUG : ChemicalCreator : Sampling formula 2500/6500\n", + "DEBUG : ChemicalCreator : Sampling formula 3000/6500\n", + "DEBUG : ChemicalCreator : Sampling formula 3500/6500\n", + "DEBUG : ChemicalCreator : Sampling formula 4000/6500\n", + "DEBUG : ChemicalCreator : Sampling formula 4500/6500\n", + "DEBUG : ChemicalCreator : Sampling formula 5000/6500\n", + "DEBUG : ChemicalCreator : Sampling formula 5500/6500\n", + "DEBUG : ChemicalCreator : Sampling formula 6000/6500\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Created /home/cjurich/projects/vimms/examples/example_data/results/MS1_single\n", + "Saving <class 'list'> to /home/cjurich/projects/vimms/examples/example_data/results/MS1_single/dataset.p\n" + ] + } + ], + "source": [ + "chems = ChemicalCreator(ps, ROI_Sources, hmdb)\n", + "dataset = chems.sample(mz_range, rt_range, min_ms1_intensity, n_chems, ms_level)\n", + "save_obj(dataset, Path(out_dir, 'dataset.p'))" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "KnownChemical - 'C13H12O3' rt=787.87 max_intensity=23718771.78\n", + "KnownChemical - 'C9H14N2O3S2' rt=224.35 max_intensity=6824500.18\n", + "KnownChemical - 'C19H12O2' rt=894.10 max_intensity=195695.90\n", + "KnownChemical - 'C16H19NO10' rt=785.77 max_intensity=1533328.27\n", + "KnownChemical - 'C3H5O7P' rt=204.98 max_intensity=737461.08\n", + "KnownChemical - 'C16H10N2O8S2' rt=538.08 max_intensity=21184668.53\n", + "KnownChemical - 'C10H17N3O8' rt=610.93 max_intensity=1315789.26\n", + "KnownChemical - 'C29H40O8' rt=537.58 max_intensity=1119299.33\n", + "KnownChemical - 'C19H16O9' rt=333.55 max_intensity=251170.17\n", + "KnownChemical - 'C23H48NO7P' rt=325.95 max_intensity=260464.78\n" + ] + } + ], + "source": [ + "for chem in dataset[0:10]:\n", + " print(chem)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Run MS1 controller on the samples and generate .mzML files" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "set_log_level_warning()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "min_rt = rt_range[0][0]\n", + "max_rt = rt_range[0][1]" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "(1440.580s) ms_level=1: : 1440.5803710000002it [00:59, 24.09it/s] \n" + ] + } + ], + "source": [ + "mass_spec = IndependentMassSpectrometer(POSITIVE, dataset, ps)\n", + "controller = SimpleMs1Controller(mass_spec)\n", + "controller.run(min_rt, max_rt)\n", + "\n", + "mzml_filename = Path(out_dir, 'ms1_controller.mzML')\n", + "controller.write_mzML('my_analysis', mzml_filename)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Simulated results have been saved to the following .mzML file and can be viewed in tools like [ToppView](https://pubs.acs.org/doi/abs/10.1021/pr900171m) or using other mzML file viewers." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "PosixPath('/home/cjurich/projects/vimms/examples/example_data/results/MS1_single/ms1_controller.mzML')" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mzml_filename" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/Synthetic data creation scripts/vimms_data_generation/03. Multiple Samples Example.ipynb b/Synthetic data creation scripts/vimms_data_generation/03. Multiple Samples Example.ipynb new file mode 100644 index 00000000..af910cbe --- /dev/null +++ b/Synthetic data creation scripts/vimms_data_generation/03. Multiple Samples Example.ipynb @@ -0,0 +1,1503 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 3. Generating Multiple Samples using MS1 Controller" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this notebook, we demonstrate how ViMMS can be used to generate multiple samples (sets of chemicals) that are biological and technical replicates. The MS1 controller is then used to produce mass spectral data in form of .mzML files for the multiple samples." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "from collections import defaultdict\n", + "import os\n", + "from pathlib import Path" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.append('..')" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from vimms.Chemicals import ChemicalCreator, MultiSampleCreator\n", + "from vimms.MassSpec import IndependentMassSpectrometer\n", + "from vimms.Controller import SimpleMs1Controller\n", + "from vimms.Common import *" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Load previously trained KDEs in `PeakSampler` and the list of extracted metabolites, created in **01. Download Data.ipynb**." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "base_dir = os.path.abspath('example_data')\n", + "ps = load_obj(Path(base_dir, 'peak_sampler_mz_rt_int_19_beers_fullscan.p'))\n", + "hmdb = load_obj(Path(base_dir, 'hmdb_compounds.p'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Set ViMMS logging level" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "set_log_level_warning()\n", + "# set_log_level_info()\n", + "# set_log_level_debug()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create Initial Chemical" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Define an output folder containing our results" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "out_dir = Path(base_dir, 'results', 'MS1_multiple')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here we generate multiple chemical objects that will be used across samples. The chemical objects are generated by sampling from metabolites in the HMDB database." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# the list of ROI sources created in the previous notebook '01. Download Data.ipynb'\n", + "ROI_Sources = [str(Path(base_dir,'DsDA', 'DsDA_Beer', 'beer_t10_simulator_files'))]\n", + "\n", + "# minimum MS1 intensity of chemicals\n", + "min_ms1_intensity = 1.75E5\n", + "\n", + "# m/z and RT range of chemicals\n", + "rt_range = [(400, 800)]\n", + "mz_range = [(100, 400)]\n", + "\n", + "# the number of chemicals in the sample\n", + "n_chems = 1000\n", + "\n", + "# maximum MS level (we do not generate fragmentation peaks when this value is 1)\n", + "ms_level = 1\n", + "\n", + "# for this experiment, we restrict the sampled chromatograms to be within 20 - 40s in length\n", + "# so they are not too big and too small\n", + "roi_rt_range = [20, 40]" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\BaseDataset\\dataset.p\n" + ] + } + ], + "source": [ + "chems = ChemicalCreator(ps, ROI_Sources, hmdb)\n", + "dataset = chems.sample(mz_range, rt_range, min_ms1_intensity, n_chems, ms_level, roi_rt_range=roi_rt_range)\n", + "save_obj(dataset, Path(out_dir, 'BaseDataset', 'dataset.p'))" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "KnownChemical - 'C21H28O2' rt=732.88 max_intensity=268684.17\n", + "KnownChemical - 'C9H15NO3S' rt=626.68 max_intensity=2101106.53\n", + "KnownChemical - 'C12H22O4' rt=548.52 max_intensity=743724.75\n", + "KnownChemical - 'C16H38N2' rt=577.29 max_intensity=1230168.35\n", + "KnownChemical - 'C5H9NO2' rt=382.06 max_intensity=8778690.16\n", + "KnownChemical - 'C9H15N5O' rt=429.14 max_intensity=402739.66\n", + "KnownChemical - 'C15H21N3O' rt=526.60 max_intensity=8337695.01\n", + "KnownChemical - 'C10H15N3O2' rt=422.79 max_intensity=311677.09\n", + "KnownChemical - 'C12H14N4O2S' rt=658.45 max_intensity=65078397.89\n", + "KnownChemical - 'C6H14O6S2' rt=428.27 max_intensity=933174.98\n" + ] + } + ], + "source": [ + "for chem in dataset[0:10]:\n", + " print(chem)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create Multiple Samples" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The next section allows us to define classes of biological replicates, each having multiple technical replicates. \n", + "\n", + "Below we create two biological classes ('class0', 'class1'), each having 10 technical replicates with some noise on the chemical's intensity." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "n_samples = [10, 10] # number of files per class\n", + "classes = [\"class%d\" % i for i in range(len(n_samples))] # creates default list of classes\n", + "intensity_noise_sd = [1000] # noise on max intensity" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['class0', 'class1']" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "classes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Add intensity changes between different classes" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "change_probabilities = [0 for i in range(len(n_samples))] # probability of intensity changes between different classes\n", + "change_differences_means = [0 for i in range(len(n_samples))] # mean of those intensity changes\n", + "change_differences_sds = [0 for i in range(len(n_samples))] # SD of those intensity changes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Add experimental variables (examples in comments)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "experimental_classes = None # [[\"male\",\"female\"],[\"Positive\",\"Negative\",\"Unknown\"]]\n", + "experimental_probabilitities = None # [[0.5,0.5],[0.33,0.33,0.34]]\n", + "experimental_sds = None # [[250],[250]]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Dropout chemicals in different classes" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "# drop-out chemicals by their probabilities\n", + "dropout_probability = 0.2\n", + "dropout_probabilities = [dropout_probability for i in range(len(n_samples))]\n", + "dropout_numbers = None # drop-out chemicals by an absolute number\n", + "\n", + "# dropout_probabilities = None\n", + "# dropout_numbers = 2 # number of chemicals dropped out in each class" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Generate multiple samples" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "save_location = os.path.join(out_dir, 'ChemicalFiles')" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\ChemicalFiles\\sample_0.p\n", + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\ChemicalFiles\\sample_1.p\n", + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\ChemicalFiles\\sample_2.p\n", + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\ChemicalFiles\\sample_3.p\n", + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\ChemicalFiles\\sample_4.p\n", + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\ChemicalFiles\\sample_5.p\n", + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\ChemicalFiles\\sample_6.p\n", + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\ChemicalFiles\\sample_7.p\n", + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\ChemicalFiles\\sample_8.p\n", + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\ChemicalFiles\\sample_9.p\n", + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\ChemicalFiles\\sample_10.p\n", + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\ChemicalFiles\\sample_11.p\n", + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\ChemicalFiles\\sample_12.p\n", + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\ChemicalFiles\\sample_13.p\n", + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\ChemicalFiles\\sample_14.p\n", + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\ChemicalFiles\\sample_15.p\n", + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\ChemicalFiles\\sample_16.p\n", + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\ChemicalFiles\\sample_17.p\n", + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\ChemicalFiles\\sample_18.p\n", + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\ChemicalFiles\\sample_19.p\n" + ] + } + ], + "source": [ + "multiple_samples = MultiSampleCreator(dataset, n_samples, classes, intensity_noise_sd, \n", + " change_probabilities, change_differences_means, change_differences_sds, dropout_probabilities, dropout_numbers,\n", + " experimental_classes, experimental_probabilitities, experimental_sds, save_location=save_location)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "20" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "total_samples = np.sum(multiple_samples.n_samples)\n", + "total_samples" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also print the chemicals that are missing (removed by drop-out) in each class." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\MissingChemicals\\missing_chemicals.p\n" + ] + }, + { + "data": { + "text/plain": [ + "[[KnownChemical - 'C21H28O2' rt=732.88 max_intensity=268684.17,\n", + " KnownChemical - 'C12H22O4' rt=548.52 max_intensity=743724.75,\n", + " KnownChemical - 'C9H15N5O' rt=429.14 max_intensity=402739.66,\n", + " KnownChemical - 'C10H15N3O2' rt=422.79 max_intensity=311677.09,\n", + " KnownChemical - 'C16H19NOS' rt=664.34 max_intensity=498105.07,\n", + " KnownChemical - 'C10H15N3O5' rt=633.61 max_intensity=531359.64,\n", + " KnownChemical - 'C6H13NO2' rt=371.39 max_intensity=3724193.90,\n", + " KnownChemical - 'C6HCl5O' rt=414.97 max_intensity=410884.25,\n", + " KnownChemical - 'C16H14ClN3O' rt=577.63 max_intensity=263971.98,\n", + " KnownChemical - 'C17H21N' rt=456.76 max_intensity=588754.25,\n", + " KnownChemical - 'C18H24O3' rt=736.21 max_intensity=209830.14,\n", + " KnownChemical - 'C7H8O5S' rt=412.30 max_intensity=2309042.63,\n", + " KnownChemical - 'C9H11N5O3' rt=668.43 max_intensity=5617453.25,\n", + " KnownChemical - 'C15H15NO3' rt=682.22 max_intensity=394247.65,\n", + " KnownChemical - 'C9H21N3O' rt=574.84 max_intensity=1280653.26,\n", + " KnownChemical - 'C13H16N2O2' rt=691.56 max_intensity=562294.37,\n", + " KnownChemical - 'C6H10N6O' rt=519.99 max_intensity=502432.70,\n", + " KnownChemical - 'C9H10N4O2S2' rt=418.25 max_intensity=15487155.81,\n", + " KnownChemical - 'C10H16O' rt=498.36 max_intensity=8125012.33,\n", + " KnownChemical - 'C11H12N2O3S' rt=630.16 max_intensity=243343.91,\n", + " KnownChemical - 'C6H4Cl2O' rt=571.80 max_intensity=240326.54,\n", + " KnownChemical - 'C11H15N4O7PS' rt=445.30 max_intensity=838657.41,\n", + " KnownChemical - 'C9H11ClO3' rt=431.82 max_intensity=422349.71,\n", + " KnownChemical - 'C18H22O2' rt=677.54 max_intensity=179998.55,\n", + " KnownChemical - 'C7H13NO2' rt=603.42 max_intensity=1053665.42,\n", + " KnownChemical - 'C8H16N4O3' rt=738.43 max_intensity=4786603.69,\n", + " KnownChemical - 'C11H18N2O3' rt=496.44 max_intensity=470069.27,\n", + " KnownChemical - 'C10H7NO4' rt=490.82 max_intensity=1618315.43,\n", + " KnownChemical - 'C10H10N2O4S' rt=467.25 max_intensity=1474129.08,\n", + " KnownChemical - 'C12H13NO3' rt=674.18 max_intensity=308063.43,\n", + " KnownChemical - 'C3H2ClF5O' rt=495.82 max_intensity=333064.35,\n", + " KnownChemical - 'C13H13N3O3' rt=747.02 max_intensity=37506537.67,\n", + " KnownChemical - 'C8H11NO2' rt=511.73 max_intensity=616561.49,\n", + " KnownChemical - 'C8H11N' rt=647.43 max_intensity=7526641.04,\n", + " KnownChemical - 'C19H25NO' rt=667.03 max_intensity=301184.19,\n", + " KnownChemical - 'C7H11NO7P2' rt=425.89 max_intensity=3206938.17,\n", + " KnownChemical - 'C6H7N5' rt=738.73 max_intensity=1066550.16,\n", + " KnownChemical - 'C6H4O5' rt=659.22 max_intensity=431609.33,\n", + " KnownChemical - 'C5H11Cl2N' rt=486.41 max_intensity=330500.12,\n", + " KnownChemical - 'C6H8S' rt=459.06 max_intensity=2414333.00,\n", + " KnownChemical - 'C19H30O3' rt=424.22 max_intensity=280506.40,\n", + " KnownChemical - 'C12H19NO3' rt=423.93 max_intensity=447323.93,\n", + " KnownChemical - 'C4H10NO6P' rt=576.86 max_intensity=530520.82,\n", + " KnownChemical - 'C8H11NO6S' rt=541.48 max_intensity=5418948.45,\n", + " KnownChemical - 'C9H18O' rt=733.79 max_intensity=1076025.65,\n", + " KnownChemical - 'C12H18' rt=633.73 max_intensity=205268.18,\n", + " KnownChemical - 'C4H6O5' rt=652.99 max_intensity=3882543.31,\n", + " KnownChemical - 'C11H15NO' rt=550.99 max_intensity=591309.97,\n", + " KnownChemical - 'C11H21N' rt=779.35 max_intensity=214960.02,\n", + " KnownChemical - 'C16H25NO4' rt=790.25 max_intensity=1409625.44,\n", + " KnownChemical - 'C10H22N4' rt=479.53 max_intensity=2385621.58,\n", + " KnownChemical - 'C21H28O5' rt=648.31 max_intensity=1022658.42,\n", + " KnownChemical - 'C12H15N3O' rt=703.97 max_intensity=832271.78,\n", + " KnownChemical - 'C5H4N2O4' rt=429.89 max_intensity=270859.15,\n", + " KnownChemical - 'C22H44O2' rt=598.82 max_intensity=374192.97,\n", + " KnownChemical - 'C9H17NO5' rt=562.37 max_intensity=868753.60,\n", + " KnownChemical - 'C7H7ClN2O4S' rt=752.38 max_intensity=9118598.67,\n", + " KnownChemical - 'C3H8NO6P' rt=710.13 max_intensity=351596.35,\n", + " KnownChemical - 'C6H11NO2' rt=686.97 max_intensity=434109.46,\n", + " KnownChemical - 'C16H11ClN4O' rt=672.93 max_intensity=189608.42,\n", + " KnownChemical - 'C7H8N2O2' rt=432.09 max_intensity=818248.42,\n", + " KnownChemical - 'C5H11NO3S' rt=589.52 max_intensity=205256.80,\n", + " KnownChemical - 'C5H8O3' rt=654.83 max_intensity=535188.33,\n", + " KnownChemical - 'C13H19NO' rt=715.87 max_intensity=311911.23,\n", + " KnownChemical - 'C12H14O5' rt=641.98 max_intensity=7157886.23,\n", + " KnownChemical - 'C8H17NO2' rt=398.12 max_intensity=181902.35,\n", + " KnownChemical - 'C17H18N2O6S' rt=684.56 max_intensity=374322.36,\n", + " KnownChemical - 'C14H14ClNOS' rt=764.50 max_intensity=111436622.82,\n", + " KnownChemical - 'C12H12N2O2S' rt=682.35 max_intensity=1024654.94,\n", + " KnownChemical - 'C14H22ClNO2' rt=403.77 max_intensity=944853.27,\n", + " KnownChemical - 'C5H9NO3S' rt=448.67 max_intensity=1158111.31,\n", + " KnownChemical - 'C8H11NO' rt=611.11 max_intensity=250852.45,\n", + " KnownChemical - 'C15H12O4' rt=641.91 max_intensity=1515666.43,\n", + " KnownChemical - 'C16H21N3' rt=750.11 max_intensity=2869315.16,\n", + " KnownChemical - 'C6H5N5O2' rt=416.88 max_intensity=747785.86,\n", + " KnownChemical - 'C9H13NO3' rt=406.94 max_intensity=238145.31,\n", + " KnownChemical - 'C13H17N' rt=490.88 max_intensity=1569691.62,\n", + " KnownChemical - 'C14H19N5O4' rt=714.78 max_intensity=1042507.95,\n", + " KnownChemical - 'C10H11ClFN5O3' rt=610.71 max_intensity=1210228.45,\n", + " KnownChemical - 'C17H23NO3' rt=767.35 max_intensity=1093116.31,\n", + " KnownChemical - 'C8H8N4O3' rt=461.84 max_intensity=1824876.81,\n", + " KnownChemical - 'C8H12N2O3S' rt=713.20 max_intensity=761679.93,\n", + " KnownChemical - 'C2H7O2PS2' rt=433.48 max_intensity=4840608.73,\n", + " KnownChemical - 'C12H15N3O3S' rt=527.02 max_intensity=1565062.17,\n", + " KnownChemical - 'C5H4N4O3' rt=480.92 max_intensity=501374.75,\n", + " KnownChemical - 'C6H8O4' rt=464.52 max_intensity=3153279.28,\n", + " KnownChemical - 'C9H12' rt=570.47 max_intensity=9404500.19,\n", + " KnownChemical - 'C9H8O2' rt=670.91 max_intensity=913535.72,\n", + " KnownChemical - 'C18H34O2' rt=392.44 max_intensity=9110098.17,\n", + " KnownChemical - 'C10H12O3' rt=457.39 max_intensity=289902.71,\n", + " KnownChemical - 'C12H14O9S' rt=798.22 max_intensity=400196.91,\n", + " KnownChemical - 'C5H7N5O' rt=400.59 max_intensity=452837.14,\n", + " KnownChemical - 'C4H9NO3' rt=651.38 max_intensity=3004717.10,\n", + " KnownChemical - 'C19H26O8' rt=696.28 max_intensity=1006653.84,\n", + " KnownChemical - 'C6H14N2O2' rt=767.16 max_intensity=5296426.89,\n", + " KnownChemical - 'C13H17NO' rt=401.00 max_intensity=5124308.38,\n", + " KnownChemical - 'C9H6O3' rt=619.80 max_intensity=4507455.53,\n", + " KnownChemical - 'C10H11N3O4S' rt=485.32 max_intensity=285420.40,\n", + " KnownChemical - 'C7H13NO4' rt=690.26 max_intensity=1032994.16,\n", + " KnownChemical - 'C24H38O3' rt=732.51 max_intensity=357802.46,\n", + " KnownChemical - 'C8H14N2O5S' rt=429.96 max_intensity=11007582.80,\n", + " KnownChemical - 'C8H14O2' rt=655.77 max_intensity=19450570.99,\n", + " KnownChemical - 'C16H26N2O3' rt=595.49 max_intensity=401097.22,\n", + " KnownChemical - 'C10H9NO3' rt=598.88 max_intensity=218760.62,\n", + " KnownChemical - 'C10H8O' rt=612.73 max_intensity=39386911.13,\n", + " KnownChemical - 'C11H11N5' rt=438.39 max_intensity=333599.76,\n", + " KnownChemical - 'C13H20N2O2' rt=583.06 max_intensity=972039.85,\n", + " KnownChemical - 'C18H29NO3' rt=605.88 max_intensity=57893824.32,\n", + " KnownChemical - 'C10H12O7S' rt=494.39 max_intensity=209149.88,\n", + " KnownChemical - 'C10H16O4' rt=430.29 max_intensity=24570465.10,\n", + " KnownChemical - 'C6H12O2' rt=757.23 max_intensity=315928.28,\n", + " KnownChemical - 'C19H17NO2S' rt=652.91 max_intensity=3565226.06,\n", + " KnownChemical - 'C8H8N2O3S' rt=545.36 max_intensity=419531.73,\n", + " KnownChemical - 'C22H30N2' rt=421.11 max_intensity=317806.27,\n", + " KnownChemical - 'C22H34O2' rt=603.16 max_intensity=313232.44,\n", + " KnownChemical - 'C16H24N2O3' rt=526.49 max_intensity=227457.40,\n", + " KnownChemical - 'C6H8OS' rt=395.19 max_intensity=403740.30,\n", + " KnownChemical - 'C10H20N2S4' rt=464.66 max_intensity=50590970.03,\n", + " KnownChemical - 'C9H7NO5S' rt=557.53 max_intensity=450036.33,\n", + " KnownChemical - 'C6H12O4' rt=606.23 max_intensity=44589301.94,\n", + " KnownChemical - 'C18H23N3O' rt=441.36 max_intensity=1143101.37,\n", + " KnownChemical - 'C6H9N3O2' rt=766.83 max_intensity=412636306.69,\n", + " KnownChemical - 'C6H13O9P' rt=679.35 max_intensity=215595.51,\n", + " KnownChemical - 'C11H12N2O' rt=621.77 max_intensity=424192.10,\n", + " KnownChemical - 'C10H15N5' rt=654.25 max_intensity=14897770.19,\n", + " KnownChemical - 'C8H7Cl2N3' rt=455.45 max_intensity=406492.97,\n", + " KnownChemical - 'C11H16O4' rt=413.23 max_intensity=9675703.23,\n", + " KnownChemical - 'C15H21N3O2' rt=742.95 max_intensity=3485444.69,\n", + " KnownChemical - 'C8H18NO2' rt=715.11 max_intensity=1308542.60,\n", + " KnownChemical - 'C17H21NO' rt=549.09 max_intensity=1013581.18,\n", + " KnownChemical - 'C18H22N2' rt=678.22 max_intensity=5604308.23,\n", + " KnownChemical - 'C13H18ClNO2' rt=679.25 max_intensity=3650145.68,\n", + " KnownChemical - 'C10H16N4O3' rt=432.26 max_intensity=545845.21,\n", + " KnownChemical - 'C6H6N2O2' rt=663.91 max_intensity=1084353.62,\n", + " KnownChemical - 'C14H14ClNS' rt=558.12 max_intensity=1339579.44,\n", + " KnownChemical - 'C16H13ClN2O3' rt=411.33 max_intensity=894436.37,\n", + " KnownChemical - 'C18H19N' rt=537.16 max_intensity=951383.74,\n", + " KnownChemical - 'C18H13ClFN3' rt=405.52 max_intensity=20709928.79,\n", + " KnownChemical - 'C5H5N5' rt=487.52 max_intensity=1418998.22,\n", + " KnownChemical - 'C4H8N2O2' rt=612.59 max_intensity=2523861.70,\n", + " KnownChemical - 'C16H25NO2' rt=721.36 max_intensity=5410419.20,\n", + " KnownChemical - 'C4H7NO3' rt=613.34 max_intensity=673127.88,\n", + " KnownChemical - 'C4H11O3PS' rt=404.34 max_intensity=782686.31,\n", + " KnownChemical - 'C16H23NO2' rt=438.46 max_intensity=17597743.59,\n", + " KnownChemical - 'C5H4N4O' rt=417.01 max_intensity=438570.10,\n", + " KnownChemical - 'C8H15N3O3' rt=545.84 max_intensity=796822.91,\n", + " KnownChemical - 'C12H19NO6' rt=489.60 max_intensity=8214409.86,\n", + " KnownChemical - 'C23H26N2O3' rt=666.41 max_intensity=1386317.20,\n", + " KnownChemical - 'C9H12O4' rt=713.34 max_intensity=3469357.72,\n", + " KnownChemical - 'C18H30O2' rt=726.66 max_intensity=78179100.24,\n", + " KnownChemical - 'C10H12N2O7' rt=397.83 max_intensity=1001140.18,\n", + " KnownChemical - 'C10H17N3S' rt=489.65 max_intensity=5285547.62,\n", + " KnownChemical - 'C15H13N3O4S' rt=489.28 max_intensity=1661798.50,\n", + " KnownChemical - 'C7H8O' rt=574.60 max_intensity=1364246.38,\n", + " KnownChemical - 'C11H12O8S' rt=423.42 max_intensity=534892.58,\n", + " KnownChemical - 'C6H4O2' rt=542.41 max_intensity=1037189.99,\n", + " KnownChemical - 'C19H16O4' rt=761.36 max_intensity=752401.42,\n", + " KnownChemical - 'C7H8ClN3O4S2' rt=741.47 max_intensity=431068.59,\n", + " KnownChemical - 'C10H7N3S' rt=631.66 max_intensity=858145.50,\n", + " KnownChemical - 'C8H12N2O4Pt' rt=714.08 max_intensity=691460.41,\n", + " KnownChemical - 'C7H9N' rt=428.60 max_intensity=2992619.91,\n", + " KnownChemical - 'C5H9N3O4' rt=552.81 max_intensity=430667.01,\n", + " KnownChemical - 'C9H17NO4' rt=548.84 max_intensity=1585214.38,\n", + " KnownChemical - 'C21H28O6' rt=725.33 max_intensity=535020.65,\n", + " KnownChemical - 'C5H4N4O2S' rt=666.41 max_intensity=6147430.47,\n", + " KnownChemical - 'C17H18N2O6' rt=760.03 max_intensity=2548246.09,\n", + " KnownChemical - 'C5H10O4' rt=718.44 max_intensity=3299138.29,\n", + " KnownChemical - 'C6H9NO5' rt=488.72 max_intensity=434793.91,\n", + " KnownChemical - 'C8H10O7S' rt=535.68 max_intensity=14707248.45,\n", + " KnownChemical - 'C8H10O8' rt=420.71 max_intensity=307837.78,\n", + " KnownChemical - 'C12H12' rt=598.70 max_intensity=2135157.97,\n", + " KnownChemical - 'C12H17NO3' rt=764.74 max_intensity=973574.98,\n", + " KnownChemical - 'C11H20NO12P' rt=767.39 max_intensity=963879.53,\n", + " KnownChemical - 'C10H16N2O4' rt=536.86 max_intensity=5046407.27,\n", + " KnownChemical - 'C20H24N2O' rt=666.04 max_intensity=334453.84,\n", + " KnownChemical - 'C4H7FN2O3' rt=420.99 max_intensity=5593047.08,\n", + " KnownChemical - 'C8H7NO5' rt=480.48 max_intensity=1214024.74,\n", + " KnownChemical - 'C15H14O3' rt=551.55 max_intensity=30530950.70,\n", + " KnownChemical - 'C20H21N' rt=610.04 max_intensity=10173146.26,\n", + " KnownChemical - 'C8H6O4' rt=702.47 max_intensity=379137.14,\n", + " KnownChemical - 'C15H11N3O3' rt=617.55 max_intensity=197807.47,\n", + " KnownChemical - 'C9H13NO' rt=600.54 max_intensity=329869.10,\n", + " KnownChemical - 'C6H6O4' rt=465.54 max_intensity=831876.73,\n", + " KnownChemical - 'C23H27NO3' rt=753.81 max_intensity=660616.06,\n", + " KnownChemical - 'C3H8N2O2' rt=603.59 max_intensity=2322167.36,\n", + " KnownChemical - 'C8H12O7' rt=401.39 max_intensity=1410977.31,\n", + " KnownChemical - 'C9H16N4O7' rt=490.93 max_intensity=182023.26,\n", + " KnownChemical - 'C21H34O3' rt=482.65 max_intensity=1248462.12,\n", + " KnownChemical - 'C3H2F6O' rt=560.99 max_intensity=1337844.10,\n", + " KnownChemical - 'C6H8O5' rt=410.09 max_intensity=3945799.83,\n", + " KnownChemical - 'C12H16N2O4' rt=415.76 max_intensity=214303.00,\n", + " KnownChemical - 'C12H15ClO3' rt=394.44 max_intensity=3366345.98,\n", + " KnownChemical - 'C10H12N2O4' rt=409.85 max_intensity=525239.21,\n", + " KnownChemical - 'C12H16F3N' rt=698.73 max_intensity=328955.91,\n", + " KnownChemical - 'C12H14O7' rt=513.76 max_intensity=658346.70,\n", + " KnownChemical - 'C10H12N2' rt=673.49 max_intensity=1722883.32,\n", + " KnownChemical - 'C5H5N3O' rt=645.22 max_intensity=8046071.09,\n", + " KnownChemical - 'C14H20O' rt=723.73 max_intensity=360069.63,\n", + " KnownChemical - 'C14H15NO3' rt=386.72 max_intensity=1266889.65,\n", + " KnownChemical - 'C3H7O4P' rt=635.82 max_intensity=310941.29,\n", + " KnownChemical - 'C7H11N3O2' rt=425.66 max_intensity=1105918.75,\n", + " KnownChemical - 'C6H14O6' rt=662.02 max_intensity=2022789.04,\n", + " KnownChemical - 'C8H8N4' rt=410.00 max_intensity=42509865.69,\n", + " KnownChemical - 'C10H12O5S' rt=441.89 max_intensity=2216622.88,\n", + " KnownChemical - 'C9H8O' rt=789.56 max_intensity=2206330.94,\n", + " KnownChemical - 'C10H21N3O2' rt=406.35 max_intensity=998566.79,\n", + " KnownChemical - 'C8H12N2' rt=701.97 max_intensity=184239.83,\n", + " KnownChemical - 'C15H10O2' rt=405.96 max_intensity=606950.09,\n", + " KnownChemical - 'C8H10N2O' rt=669.49 max_intensity=1571835.79,\n", + " KnownChemical - 'C8H12N4O5' rt=525.09 max_intensity=741152.99,\n", + " KnownChemical - 'C3H6N2O2' rt=734.58 max_intensity=383783.71,\n", + " KnownChemical - 'C24H40O4' rt=583.10 max_intensity=681116.55,\n", + " KnownChemical - 'C4H6N4O3S2' rt=423.48 max_intensity=2047253.42,\n", + " KnownChemical - 'C13H10O' rt=535.31 max_intensity=740828.93,\n", + " KnownChemical - 'C7H10O7' rt=740.36 max_intensity=4263954.45,\n", + " KnownChemical - 'C9H7N7O2S' rt=539.57 max_intensity=6130110.21,\n", + " KnownChemical - 'C10H16N2O3S' rt=390.88 max_intensity=21566188.88,\n", + " KnownChemical - 'C23H34' rt=615.96 max_intensity=351347.44,\n", + " KnownChemical - 'C5H11NO4S' rt=436.60 max_intensity=1320867.63,\n", + " KnownChemical - 'C8H10FN3O3S' rt=395.89 max_intensity=499137.59,\n", + " KnownChemical - 'C11H12N2OS' rt=473.31 max_intensity=219750.47],\n", + " [KnownChemical - 'C9H15NO3S' rt=626.68 max_intensity=2101106.53,\n", + " KnownChemical - 'C10H17N' rt=518.94 max_intensity=3471723.43,\n", + " KnownChemical - 'C16H12O7' rt=524.40 max_intensity=857009.34,\n", + " KnownChemical - 'C4H5N3O2' rt=493.13 max_intensity=427065.01,\n", + " KnownChemical - 'C6HCl5O' rt=414.97 max_intensity=410884.25,\n", + " KnownChemical - 'C17H12Cl2N4O' rt=752.14 max_intensity=2939629.28,\n", + " KnownChemical - 'C9H8O4' rt=489.90 max_intensity=1988935.55,\n", + " KnownChemical - 'C18H27NO2' rt=675.88 max_intensity=186811.18,\n", + " KnownChemical - 'C2H8O7P2' rt=748.60 max_intensity=2000241.65,\n", + " KnownChemical - 'C10H20O2' rt=658.95 max_intensity=1987739.83,\n", + " KnownChemical - 'C7H8O5S' rt=412.30 max_intensity=2309042.63,\n", + " KnownChemical - 'C11H10N4O2' rt=632.70 max_intensity=429439.78,\n", + " KnownChemical - 'C11H19N5O2S2' rt=622.35 max_intensity=360919.28,\n", + " KnownChemical - 'C10H15N5O3' rt=438.81 max_intensity=1466022.86,\n", + " KnownChemical - 'C13H16N2O2' rt=691.56 max_intensity=562294.37,\n", + " KnownChemical - 'C13H25NO4' rt=441.24 max_intensity=7863378.39,\n", + " KnownChemical - 'C4H3F7O' rt=668.21 max_intensity=2205370.01,\n", + " KnownChemical - 'C9H12N2O2' rt=421.05 max_intensity=3017089.33,\n", + " KnownChemical - 'C6H10O5' rt=418.72 max_intensity=235048.56,\n", + " KnownChemical - 'C4H3FN2O2' rt=448.29 max_intensity=469152.81,\n", + " KnownChemical - 'C8H16N4O3' rt=738.43 max_intensity=4786603.69,\n", + " KnownChemical - 'C10H7NO4' rt=490.82 max_intensity=1618315.43,\n", + " KnownChemical - 'C3H2ClF5O' rt=495.82 max_intensity=333064.35,\n", + " KnownChemical - 'C3H6O4' rt=748.81 max_intensity=1870882.49,\n", + " KnownChemical - 'C9H13N3O4' rt=502.08 max_intensity=41047899.76,\n", + " KnownChemical - 'C7H17N2O2' rt=743.92 max_intensity=2702220.70,\n", + " KnownChemical - 'C8H6N4O5' rt=657.02 max_intensity=5657036.21,\n", + " KnownChemical - 'C10H12ClN3O3S' rt=597.10 max_intensity=2004007.99,\n", + " KnownChemical - 'C5H10N2O' rt=484.04 max_intensity=888219.45,\n", + " KnownChemical - 'C22H30O3' rt=525.96 max_intensity=2651568.40,\n", + " KnownChemical - 'C12H14N2O5S' rt=560.02 max_intensity=5932071.16,\n", + " KnownChemical - 'C6H8S' rt=459.06 max_intensity=2414333.00,\n", + " KnownChemical - 'C12H19NO3' rt=423.93 max_intensity=447323.93,\n", + " KnownChemical - 'C12H18O2' rt=540.02 max_intensity=730065.84,\n", + " KnownChemical - 'C9H9N3O2S2' rt=757.70 max_intensity=775341.61,\n", + " KnownChemical - 'C15H17NO2' rt=627.96 max_intensity=442922.38,\n", + " KnownChemical - 'C11H14ClNO3S' rt=576.65 max_intensity=7613172.04,\n", + " KnownChemical - 'C8H10O3' rt=614.36 max_intensity=567209.42,\n", + " KnownChemical - 'C8H11NO5S' rt=735.56 max_intensity=199493.09,\n", + " KnownChemical - 'C13H23NO4' rt=687.18 max_intensity=439986.85,\n", + " KnownChemical - 'C11H12N4O3S' rt=579.26 max_intensity=676147.99,\n", + " KnownChemical - 'C17H26ClN' rt=600.11 max_intensity=9556158.31,\n", + " KnownChemical - 'C10H21NO7' rt=574.11 max_intensity=604728.00,\n", + " KnownChemical - 'C11H21NO4' rt=693.64 max_intensity=372915.28,\n", + " KnownChemical - 'C5H5N5O' rt=484.64 max_intensity=456377.07,\n", + " KnownChemical - 'C14H14N2O' rt=570.31 max_intensity=239509.85,\n", + " KnownChemical - 'C5H4N2O4' rt=429.89 max_intensity=270859.15,\n", + " KnownChemical - 'C7H9ClO' rt=741.87 max_intensity=189972.95,\n", + " KnownChemical - 'C13H16N2O3S' rt=673.82 max_intensity=61073923.41,\n", + " KnownChemical - 'C22H44O2' rt=598.82 max_intensity=374192.97,\n", + " KnownChemical - 'C12H18N2O3' rt=610.64 max_intensity=1132662.08,\n", + " KnownChemical - 'C10H16NO' rt=643.11 max_intensity=216324.95,\n", + " KnownChemical - 'C9H17NO5' rt=562.37 max_intensity=868753.60,\n", + " KnownChemical - 'C4H11O4P' rt=555.30 max_intensity=715163.75,\n", + " KnownChemical - 'C3H7O7P' rt=430.83 max_intensity=293934.13,\n", + " KnownChemical - 'C4H9NO2' rt=599.40 max_intensity=354130.89,\n", + " KnownChemical - 'C5H11NO3S' rt=589.52 max_intensity=205256.80,\n", + " KnownChemical - 'C13H19NO' rt=715.87 max_intensity=311911.23,\n", + " KnownChemical - 'C21H36O5' rt=427.22 max_intensity=233023.87,\n", + " KnownChemical - 'C17H20N4OS' rt=439.75 max_intensity=2025653.92,\n", + " KnownChemical - 'C14H14ClNOS' rt=764.50 max_intensity=111436622.82,\n", + " KnownChemical - 'C12H12N2O2S' rt=682.35 max_intensity=1024654.94,\n", + " KnownChemical - 'C12H21NO' rt=715.62 max_intensity=387451.65,\n", + " KnownChemical - 'C8H11NO' rt=611.11 max_intensity=250852.45,\n", + " KnownChemical - 'C4H8O5' rt=673.58 max_intensity=508429.41,\n", + " KnownChemical - 'C10H8N6O' rt=413.01 max_intensity=4576973.93,\n", + " KnownChemical - 'C6H14N4O2' rt=769.58 max_intensity=22741497.85,\n", + " KnownChemical - 'C6H10O4' rt=545.84 max_intensity=508262.69,\n", + " KnownChemical - 'C12H23NO2S' rt=718.10 max_intensity=210642.11,\n", + " KnownChemical - 'C5H11NO2Se' rt=699.47 max_intensity=6651243.08,\n", + " KnownChemical - 'C8H8O3' rt=646.00 max_intensity=677277.86,\n", + " KnownChemical - 'C14H21NO2' rt=745.54 max_intensity=2211652.62,\n", + " KnownChemical - 'C14H12O6S' rt=733.04 max_intensity=284539.30,\n", + " KnownChemical - 'C5H4N4O3' rt=480.92 max_intensity=501374.75,\n", + " KnownChemical - 'I2' rt=453.89 max_intensity=166933849.71,\n", + " KnownChemical - 'C8H7NO2' rt=425.28 max_intensity=1187800.06,\n", + " KnownChemical - 'C18H32O2' rt=533.88 max_intensity=2099293.85,\n", + " KnownChemical - 'C11H16N2O' rt=555.84 max_intensity=516183.71,\n", + " KnownChemical - 'C12H12N2O4' rt=408.72 max_intensity=514493.97,\n", + " KnownChemical - 'C6H14N2O' rt=593.92 max_intensity=453001.90,\n", + " KnownChemical - 'C10H26N4' rt=737.62 max_intensity=12346129.24,\n", + " KnownChemical - 'C15H10O10S' rt=416.58 max_intensity=414072.51,\n", + " KnownChemical - 'C7H11NO2' rt=424.34 max_intensity=471025.60,\n", + " KnownChemical - 'C14H16ClN3O4S2' rt=739.94 max_intensity=193303.86,\n", + " KnownChemical - 'C11H16ClN5' rt=451.98 max_intensity=22034454.67,\n", + " KnownChemical - 'C8H14O2' rt=655.77 max_intensity=19450570.99,\n", + " KnownChemical - 'C16H26N2O3' rt=595.49 max_intensity=401097.22,\n", + " KnownChemical - 'C9H16N2O2' rt=678.27 max_intensity=354111.46,\n", + " KnownChemical - 'C11H11N5' rt=438.39 max_intensity=333599.76,\n", + " KnownChemical - 'C19H27N5O4' rt=444.58 max_intensity=2970179.83,\n", + " KnownChemical - 'C19H25N5O4' rt=414.11 max_intensity=3545121.49,\n", + " KnownChemical - 'C21H23ClFN3O' rt=524.18 max_intensity=427821.38,\n", + " KnownChemical - 'C8H18O' rt=587.52 max_intensity=1031830.05,\n", + " KnownChemical - 'C12H20O4' rt=718.43 max_intensity=2079237.51,\n", + " KnownChemical - 'C11H12N4' rt=511.07 max_intensity=184316.02,\n", + " KnownChemical - 'C9H9Cl2N3' rt=549.73 max_intensity=2807066.89,\n", + " KnownChemical - 'C5H10N2O3' rt=495.50 max_intensity=430950.41,\n", + " KnownChemical - 'C8H16O2' rt=393.42 max_intensity=322097.12,\n", + " KnownChemical - 'C12H9N3O5S' rt=414.29 max_intensity=1124232.91,\n", + " KnownChemical - 'C10H16O4' rt=430.29 max_intensity=24570465.10,\n", + " KnownChemical - 'C15H21N' rt=551.42 max_intensity=851267.32,\n", + " KnownChemical - 'C12H13N3O2' rt=638.96 max_intensity=4010414.36,\n", + " KnownChemical - 'C4H8S2' rt=463.97 max_intensity=772632.68,\n", + " KnownChemical - 'C22H34O2' rt=603.16 max_intensity=313232.44,\n", + " KnownChemical - 'C10H20N2S4' rt=464.66 max_intensity=50590970.03,\n", + " KnownChemical - 'C17H20FN3O3' rt=685.61 max_intensity=629690.40,\n", + " KnownChemical - 'C17H20N4O6' rt=422.90 max_intensity=1147997.66,\n", + " KnownChemical - 'C8H7Cl2N3' rt=455.45 max_intensity=406492.97,\n", + " KnownChemical - 'C21H25NO' rt=612.11 max_intensity=3265372.01,\n", + " KnownChemical - 'C11H16O4' rt=413.23 max_intensity=9675703.23,\n", + " KnownChemical - 'C11H12Cl2N2O5' rt=426.66 max_intensity=1341168.16,\n", + " KnownChemical - 'C15H21N3O2' rt=742.95 max_intensity=3485444.69,\n", + " KnownChemical - 'C8H18NO2' rt=715.11 max_intensity=1308542.60,\n", + " KnownChemical - 'C12H16O5' rt=672.56 max_intensity=632244.16,\n", + " KnownChemical - 'C6H7NO' rt=585.96 max_intensity=430059.12,\n", + " KnownChemical - 'C15H12N2O' rt=417.17 max_intensity=1733868.52,\n", + " KnownChemical - 'C14H11ClN2O4S' rt=682.54 max_intensity=718246.73,\n", + " KnownChemical - 'C17H17N3O3S' rt=687.57 max_intensity=244878.44,\n", + " KnownChemical - 'C4H8O4' rt=414.94 max_intensity=3985435.24,\n", + " KnownChemical - 'C18H19N' rt=537.16 max_intensity=951383.74,\n", + " KnownChemical - 'C5H5N5' rt=487.52 max_intensity=1418998.22,\n", + " KnownChemical - 'C16H25NO2' rt=721.36 max_intensity=5410419.20,\n", + " KnownChemical - 'C5H4N4O' rt=417.01 max_intensity=438570.10,\n", + " KnownChemical - 'C6H10S' rt=673.34 max_intensity=1181581.81,\n", + " KnownChemical - 'C12H19NO6' rt=489.60 max_intensity=8214409.86,\n", + " KnownChemical - 'C15H13N3O4S' rt=489.28 max_intensity=1661798.50,\n", + " KnownChemical - 'C5H10O2' rt=464.51 max_intensity=4729035.21,\n", + " KnownChemical - 'C17H20FN3O4' rt=742.88 max_intensity=465376.23,\n", + " KnownChemical - 'C18H21ClN2O' rt=624.99 max_intensity=319533.30,\n", + " KnownChemical - 'C11H12Cl2N2O' rt=426.84 max_intensity=212978.90,\n", + " KnownChemical - 'C13H10N2O4' rt=760.79 max_intensity=3885782688.76,\n", + " KnownChemical - 'C10H17NOS' rt=505.31 max_intensity=467520.05,\n", + " KnownChemical - 'C7H7NO4' rt=422.82 max_intensity=220877.98,\n", + " KnownChemical - 'C14H20N2O2' rt=490.24 max_intensity=181082.59,\n", + " KnownChemical - 'C7H9N' rt=428.60 max_intensity=2992619.91,\n", + " KnownChemical - 'C5H10O4' rt=718.44 max_intensity=3299138.29,\n", + " KnownChemical - 'C14H10O5' rt=554.89 max_intensity=1443864.49,\n", + " KnownChemical - 'C20H27N' rt=421.89 max_intensity=1825335.98,\n", + " KnownChemical - 'C15H17F3N2O4' rt=712.12 max_intensity=6459473.72,\n", + " KnownChemical - 'C12H12' rt=598.70 max_intensity=2135157.97,\n", + " KnownChemical - 'C11H20NO12P' rt=767.39 max_intensity=963879.53,\n", + " KnownChemical - 'C7H14N2O3' rt=469.75 max_intensity=7653059.83,\n", + " KnownChemical - 'C7H6O2' rt=549.70 max_intensity=216294.58,\n", + " KnownChemical - 'C22H19ClO3' rt=616.17 max_intensity=10800178.50,\n", + " KnownChemical - 'C11H11NO3' rt=592.35 max_intensity=729313.56,\n", + " KnownChemical - 'C10H11F3N2O5' rt=718.04 max_intensity=17880991.02,\n", + " KnownChemical - 'C12H14ClNO' rt=541.22 max_intensity=481831.46,\n", + " KnownChemical - 'C16H15N3' rt=675.55 max_intensity=1287028.92,\n", + " KnownChemical - 'C12H14N2O3' rt=418.29 max_intensity=418187.22,\n", + " KnownChemical - 'C11H15NO5' rt=609.66 max_intensity=1586316.60,\n", + " KnownChemical - 'C19H28O2' rt=679.99 max_intensity=1021449.94,\n", + " KnownChemical - 'C2H7O3PS' rt=734.74 max_intensity=256371.94,\n", + " KnownChemical - 'C15H10N2O' rt=783.17 max_intensity=5513242.69,\n", + " KnownChemical - 'C8H10O' rt=450.50 max_intensity=2973122.63,\n", + " KnownChemical - 'C10H12N2' rt=673.49 max_intensity=1722883.32,\n", + " KnownChemical - 'C15H16O2' rt=537.18 max_intensity=320802.95,\n", + " KnownChemical - 'C2H8NO4P' rt=583.75 max_intensity=2735449.71,\n", + " KnownChemical - 'C5H5N3O' rt=645.22 max_intensity=8046071.09,\n", + " KnownChemical - 'C8H14O4' rt=614.27 max_intensity=651798.11,\n", + " KnownChemical - 'C10H12N4O4S' rt=793.94 max_intensity=244338.24,\n", + " KnownChemical - 'C13H18O3' rt=415.14 max_intensity=959573.88,\n", + " KnownChemical - 'C7H12O4' rt=626.10 max_intensity=177177.68,\n", + " KnownChemical - 'C9H20N2O2' rt=504.15 max_intensity=826317.38,\n", + " KnownChemical - 'C15H23NO3' rt=417.30 max_intensity=1863502.20,\n", + " KnownChemical - 'C2H8NO3P' rt=768.48 max_intensity=188501.80,\n", + " KnownChemical - 'C12H14O4' rt=533.36 max_intensity=1616412.72,\n", + " KnownChemical - 'C8H14O7' rt=562.71 max_intensity=4202246.40,\n", + " KnownChemical - 'C7H11N3O2' rt=425.66 max_intensity=1105918.75,\n", + " KnownChemical - 'C7H8N4O3' rt=492.67 max_intensity=533517.76,\n", + " KnownChemical - 'C8H8N4' rt=410.00 max_intensity=42509865.69,\n", + " KnownChemical - 'C6H7N3O' rt=475.60 max_intensity=3760136.05,\n", + " KnownChemical - 'C2H6O4S' rt=433.27 max_intensity=8958016.61,\n", + " KnownChemical - 'C21H29NO2' rt=544.39 max_intensity=255465.65,\n", + " KnownChemical - 'C8H12N2' rt=701.97 max_intensity=184239.83,\n", + " KnownChemical - 'C6H12O3' rt=591.53 max_intensity=2758937.95,\n", + " KnownChemical - 'C12H10N2O5' rt=558.98 max_intensity=602319.18,\n", + " KnownChemical - 'C14H30NO7P' rt=471.42 max_intensity=782748.15,\n", + " KnownChemical - 'C11H16N5O7PS' rt=578.04 max_intensity=351374.90,\n", + " KnownChemical - 'CH3O5P' rt=714.29 max_intensity=17422591.51,\n", + " KnownChemical - 'C6H11NO4' rt=516.85 max_intensity=4559452.41,\n", + " KnownChemical - 'C7H6ClN3O4S2' rt=449.88 max_intensity=336501.37,\n", + " KnownChemical - 'C7H10O7' rt=740.36 max_intensity=4263954.45,\n", + " KnownChemical - 'C10H16N2O3S' rt=390.88 max_intensity=21566188.88,\n", + " KnownChemical - 'C6H6N4S' rt=452.09 max_intensity=412529.32,\n", + " KnownChemical - 'C7H15NO3' rt=488.06 max_intensity=998140.46,\n", + " KnownChemical - 'C19H23NO4' rt=552.61 max_intensity=892392.75,\n", + " KnownChemical - 'C11H15N5O3S' rt=401.34 max_intensity=723652.11]]" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "save_obj(multiple_samples.missing_chemicals, Path(out_dir, 'MissingChemicals', 'missing_chemicals.p'))\n", + "multiple_samples.missing_chemicals" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Run MS1 controller on the samples and generate .mzML files" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now take the multiple samples created above and generate mass spectral data (.mzML files) using the MS1 controller in ViMMS." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\\number_0_class_0.mzML\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "400.81993000000045it [00:04, 92.48it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Created C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\n", + "Generating C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\\number_1_class_0.mzML\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "400.7331000000005it [00:04, 95.30it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\\number_2_class_0.mzML\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "400.23352599999976it [00:04, 95.66it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\\number_3_class_0.mzML\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "400.06701999999984it [00:04, 93.63it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\\number_4_class_0.mzML\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "400.8218000000011it [00:04, 96.63it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\\number_5_class_0.mzML\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "400.67342999999937it [00:04, 93.35it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\\number_6_class_0.mzML\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "400.9388760000012it [00:04, 91.21it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\\number_7_class_0.mzML\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "400.7767300000007it [00:04, 97.70it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\\number_8_class_0.mzML\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "401.0148999999999it [00:04, 96.84it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\\number_9_class_0.mzML\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "400.10175700000036it [00:04, 90.83it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\\number_0_class_1.mzML\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "401.1556000000006it [00:04, 92.73it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\\number_1_class_1.mzML\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "400.1441599999994it [00:04, 86.05it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\\number_2_class_1.mzML\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "400.8101270000003it [00:04, 93.60it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\\number_3_class_1.mzML\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "400.6406600000005it [00:04, 91.75it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\\number_4_class_1.mzML\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "400.2084600000003it [00:04, 89.61it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\\number_5_class_1.mzML\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "401.14851999999894it [00:04, 81.52it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\\number_6_class_1.mzML\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "400.3037899999998it [00:04, 92.11it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\\number_7_class_1.mzML\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "400.75644999999827it [00:04, 88.37it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\\number_8_class_1.mzML\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "400.00880000000143it [00:04, 90.58it/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\MS1_multiple\\mzMLFiles\\number_9_class_1.mzML\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "400.82949000000076it [00:04, 80.46it/s] \n" + ] + } + ], + "source": [ + "min_rt = rt_range[0][0]\n", + "max_rt = rt_range[0][1]\n", + "controllers = defaultdict(list)\n", + "controller_to_mzml = {}\n", + "\n", + "mzml_dir = Path(out_dir, 'mzMLFiles')\n", + "num_classes = len(n_samples)\n", + "sample_idx = 0\n", + "for j in range(num_classes): # loop over classes\n", + " num_samples = n_samples[j]\n", + " for i in range(num_samples): # loop over samples for each class\n", + " \n", + " # load the sample\n", + " fname = Path(save_location, 'sample_%d.p' % sample_idx) \n", + " sample = load_obj(fname)\n", + " sample_idx += 1\n", + " \n", + " # define output .mzML filename\n", + " out_file = 'number_%d_class_%d.mzML' % (i, j)\n", + " out_path = Path(mzml_dir, out_file)\n", + " print('Generating %s' % out_path)\n", + "\n", + " # run it through the MS1 controller \n", + " mass_spec = IndependentMassSpectrometer(POSITIVE, sample, ps)\n", + " controller = SimpleMs1Controller(mass_spec)\n", + " controller.run(min_rt, max_rt)\n", + " controller.write_mzML('my_analysis', out_path)\n", + "\n", + " # save the resulting controller\n", + " controllers[j].append(controller)\n", + " controller_to_mzml[controller] = (j, out_file, )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Print out the missing peaks" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The controller object contains all the information about the state of the mass spectrometry process over time. Below we demonstrate this by generating a report of peaks corresponding to a chemical that are present in one class but is missing from the other class. This can be useful in the benchmark evaluation of peak picking or alignment algorithms." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "def get_chem_to_peaks(controller):\n", + " chem_to_peaks = defaultdict(list)\n", + " frag_events = controller.mass_spec.fragmentation_events\n", + " for frag_event in frag_events:\n", + " chem = frag_event.chem\n", + " peaks = frag_event.peaks\n", + " chem_to_peaks[chem].extend(peaks)\n", + " return chem_to_peaks" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "for controller, (current_class, mzml_filename) in controller_to_mzml.items():\n", + " controller_peaks = get_chem_to_peaks(controller)\n", + " basename = os.path.basename(mzml_filename)\n", + " front, back = os.path.splitext(mzml_filename)\n", + " outfile = front + '.csv'\n", + "\n", + " missing_peaks = [] \n", + " for other_class in range(num_classes):\n", + " if current_class == other_class:\n", + " continue\n", + "\n", + " # get the peaks that are present in current_class but missing in other_class\n", + " missing_chems = multiple_samples.missing_chemicals[other_class]\n", + " for chem in missing_chems:\n", + " peaks = controller_peaks[chem]\n", + " for peak in peaks:\n", + " row = (chem.formula.formula_string, current_class, other_class, peak.mz, peak.rt, peak.intensity)\n", + " missing_peaks.append(row)\n", + " \n", + " # convert to dataframe\n", + " columns = ['formula', 'present_in', 'missing_in', 'mz', 'RT', 'intensity']\n", + " missing_df = pd.DataFrame(missing_peaks, columns=columns)\n", + " missing_df.to_csv(os.path.join(out_dir, 'MissingChemicals', os.path.basename(outfile)))" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "<div>\n", + "<style scoped>\n", + " .dataframe tbody tr th:only-of-type {\n", + " vertical-align: middle;\n", + " }\n", + "\n", + " .dataframe tbody tr th {\n", + " vertical-align: top;\n", + " }\n", + "\n", + " .dataframe thead th {\n", + " text-align: right;\n", + " }\n", + "</style>\n", + "<table border=\"1\" class=\"dataframe\">\n", + " <thead>\n", + " <tr style=\"text-align: right;\">\n", + " <th></th>\n", + " <th>formula</th>\n", + " <th>present_in</th>\n", + " <th>missing_in</th>\n", + " <th>mz</th>\n", + " <th>RT</th>\n", + " <th>intensity</th>\n", + " </tr>\n", + " </thead>\n", + " <tbody>\n", + " <tr>\n", + " <td>0</td>\n", + " <td>C21H28O2</td>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " <td>313.216480</td>\n", + " <td>733.48489</td>\n", + " <td>190887.063824</td>\n", + " </tr>\n", + " <tr>\n", + " <td>1</td>\n", + " <td>C21H28O2</td>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " <td>330.243026</td>\n", + " <td>733.48489</td>\n", + " <td>36691.272425</td>\n", + " </tr>\n", + " <tr>\n", + " <td>2</td>\n", + " <td>C21H28O2</td>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " <td>389.128243</td>\n", + " <td>733.48489</td>\n", + " <td>89999.078686</td>\n", + " </tr>\n", + " <tr>\n", + " <td>3</td>\n", + " <td>C21H28O2</td>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " <td>642.451956</td>\n", + " <td>733.48489</td>\n", + " <td>52228.312111</td>\n", + " </tr>\n", + " <tr>\n", + " <td>4</td>\n", + " <td>C21H28O2</td>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " <td>314.219835</td>\n", + " <td>733.48489</td>\n", + " <td>44585.350600</td>\n", + " </tr>\n", + " <tr>\n", + " <td>5</td>\n", + " <td>C21H28O2</td>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " <td>315.223189</td>\n", + " <td>733.48489</td>\n", + " <td>4958.936872</td>\n", + " </tr>\n", + " <tr>\n", + " <td>6</td>\n", + " <td>C21H28O2</td>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " <td>313.216438</td>\n", + " <td>734.96489</td>\n", + " <td>201067.826579</td>\n", + " </tr>\n", + " <tr>\n", + " <td>7</td>\n", + " <td>C21H28O2</td>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " <td>330.242984</td>\n", + " <td>734.96489</td>\n", + " <td>38648.163229</td>\n", + " </tr>\n", + " <tr>\n", + " <td>8</td>\n", + " <td>C21H28O2</td>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " <td>389.128201</td>\n", + " <td>734.96489</td>\n", + " <td>94799.085820</td>\n", + " </tr>\n", + " <tr>\n", + " <td>9</td>\n", + " <td>C21H28O2</td>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " <td>642.451914</td>\n", + " <td>734.96489</td>\n", + " <td>55013.854746</td>\n", + " </tr>\n", + " <tr>\n", + " <td>10</td>\n", + " <td>C21H28O2</td>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " <td>314.219792</td>\n", + " <td>734.96489</td>\n", + " <td>46963.263842</td>\n", + " </tr>\n", + " <tr>\n", + " <td>11</td>\n", + " <td>C21H28O2</td>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " <td>315.223147</td>\n", + " <td>734.96489</td>\n", + " <td>5223.416605</td>\n", + " </tr>\n", + " <tr>\n", + " <td>12</td>\n", + " <td>C21H28O2</td>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " <td>313.216161</td>\n", + " <td>736.20189</td>\n", + " <td>213513.273571</td>\n", + " </tr>\n", + " <tr>\n", + " <td>13</td>\n", + " <td>C21H28O2</td>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " <td>330.242708</td>\n", + " <td>736.20189</td>\n", + " <td>41040.359310</td>\n", + " </tr>\n", + " <tr>\n", + " <td>14</td>\n", + " <td>C21H28O2</td>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " <td>389.127925</td>\n", + " <td>736.20189</td>\n", + " <td>100666.842077</td>\n", + " </tr>\n", + " <tr>\n", + " <td>15</td>\n", + " <td>C21H28O2</td>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " <td>642.451638</td>\n", + " <td>736.20189</td>\n", + " <td>58419.034106</td>\n", + " </tr>\n", + " <tr>\n", + " <td>16</td>\n", + " <td>C21H28O2</td>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " <td>314.219516</td>\n", + " <td>736.20189</td>\n", + " <td>49870.137710</td>\n", + " </tr>\n", + " <tr>\n", + " <td>17</td>\n", + " <td>C21H28O2</td>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " <td>315.222871</td>\n", + " <td>736.20189</td>\n", + " <td>5546.729169</td>\n", + " </tr>\n", + " <tr>\n", + " <td>18</td>\n", + " <td>C21H28O2</td>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " <td>313.216065</td>\n", + " <td>737.46289</td>\n", + " <td>208088.886377</td>\n", + " </tr>\n", + " <tr>\n", + " <td>19</td>\n", + " <td>C21H28O2</td>\n", + " <td>1</td>\n", + " <td>0</td>\n", + " <td>330.242612</td>\n", + " <td>737.46289</td>\n", + " <td>39997.713128</td>\n", + " </tr>\n", + " </tbody>\n", + "</table>\n", + "</div>" + ], + "text/plain": [ + " formula present_in missing_in mz RT intensity\n", + "0 C21H28O2 1 0 313.216480 733.48489 190887.063824\n", + "1 C21H28O2 1 0 330.243026 733.48489 36691.272425\n", + "2 C21H28O2 1 0 389.128243 733.48489 89999.078686\n", + "3 C21H28O2 1 0 642.451956 733.48489 52228.312111\n", + "4 C21H28O2 1 0 314.219835 733.48489 44585.350600\n", + "5 C21H28O2 1 0 315.223189 733.48489 4958.936872\n", + "6 C21H28O2 1 0 313.216438 734.96489 201067.826579\n", + "7 C21H28O2 1 0 330.242984 734.96489 38648.163229\n", + "8 C21H28O2 1 0 389.128201 734.96489 94799.085820\n", + "9 C21H28O2 1 0 642.451914 734.96489 55013.854746\n", + "10 C21H28O2 1 0 314.219792 734.96489 46963.263842\n", + "11 C21H28O2 1 0 315.223147 734.96489 5223.416605\n", + "12 C21H28O2 1 0 313.216161 736.20189 213513.273571\n", + "13 C21H28O2 1 0 330.242708 736.20189 41040.359310\n", + "14 C21H28O2 1 0 389.127925 736.20189 100666.842077\n", + "15 C21H28O2 1 0 642.451638 736.20189 58419.034106\n", + "16 C21H28O2 1 0 314.219516 736.20189 49870.137710\n", + "17 C21H28O2 1 0 315.222871 736.20189 5546.729169\n", + "18 C21H28O2 1 0 313.216065 737.46289 208088.886377\n", + "19 C21H28O2 1 0 330.242612 737.46289 39997.713128" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "missing_df.head(20)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/Synthetic data creation scripts/vimms_data_generation/04. Top-N Simulations.ipynb b/Synthetic data creation scripts/vimms_data_generation/04. Top-N Simulations.ipynb new file mode 100644 index 00000000..41810b5c --- /dev/null +++ b/Synthetic data creation scripts/vimms_data_generation/04. Top-N Simulations.ipynb @@ -0,0 +1,791 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 4. Top-N Simulations from Actual Experimental Data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook loads an existing Beer1pos data, runs it through the simulator and compares the simulated results to the initial input data. The results here correspond to Section 3.2 in the paper." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import pylab as plt\n", + "import pymzml" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.append('..')" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from vimms.Roi import RoiToChemicalCreator, make_roi\n", + "from vimms.DataGenerator import DataSource, PeakSampler, get_spectral_feature_database\n", + "from vimms.MassSpec import IndependentMassSpectrometer\n", + "from vimms.Controller import TopNController\n", + "from vimms.PlotsForPaper import count_stuff, plot_num_scans, match_peaklist, check_found_matches, \\\n", + "plot_matched_intensities, plot_matched_precursors\n", + "from vimms.Common import *" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "set_log_level_debug()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Parameters" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "base_dir = 'example_data'\n", + "mzml_path = os.path.join(base_dir, 'beers', 'fragmentation', 'mzML')\n", + "file_name = 'Beer_multibeers_1_T10_POS.mzML'\n", + "\n", + "experiment_name = 'mzml_compare'\n", + "experiment_out_dir = os.path.join(base_dir, 'results', experiment_name)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "min_rt = 0\n", + "max_rt = 1441" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "kde_min_ms1_intensity = 0 # min intensity to be selected for kdes\n", + "kde_min_ms2_intensity = 0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### a. ROI extraction parameters" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "roi_mz_tol = 10\n", + "roi_min_length = 1\n", + "roi_min_intensity = 0\n", + "roi_start_rt = min_rt\n", + "roi_stop_rt = max_rt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### b. Top-N parameters" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "isolation_window = 1 # the isolation window in Dalton around a selected precursor ion\n", + "ionisation_mode = POSITIVE\n", + "N = 10\n", + "rt_tol = 15\n", + "mz_tol = 10\n", + "min_ms1_intensity = 1.75E5 # minimum ms1 intensity to fragment" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "mzml_out = os.path.join(experiment_out_dir, 'simulated.mzML')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Train densities" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO : DataSource : Loading Beer_multibeers_1_T10_POS.mzML\n" + ] + } + ], + "source": [ + "ds = DataSource()\n", + "ds.load_data(mzml_path, file_name=file_name)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : PeakSampler : Extracted 7647 MS2 scans\n", + "DEBUG : PeakSampler : Computing parent intensity proportions\n", + "DEBUG : PeakSampler : Extracting scan durations\n", + "DEBUG : PeakSampler : Training KDEs for ms_level=1\n", + "DEBUG : PeakSampler : Retrieving mz_intensity_rt values from <vimms.DataGenerator.DataSource object at 0x00000172559A4630>\n", + "INFO : DataSource : Using values from scans\n", + "DEBUG : PeakSampler : Retrieving n_peaks values from <vimms.DataGenerator.DataSource object at 0x00000172559A4630>\n", + "DEBUG : PeakSampler : Training KDEs for ms_level=2\n", + "DEBUG : PeakSampler : Retrieving mz_intensity_rt values from <vimms.DataGenerator.DataSource object at 0x00000172559A4630>\n", + "INFO : DataSource : Using values from scans\n", + "DEBUG : PeakSampler : Retrieving n_peaks values from <vimms.DataGenerator.DataSource object at 0x00000172559A4630>\n" + ] + } + ], + "source": [ + "bandwidth_mz_intensity_rt=1.0\n", + "bandwidth_n_peaks=1.0\n", + "ps = get_spectral_feature_database(ds, file_name, kde_min_ms1_intensity, kde_min_ms2_intensity, min_rt, max_rt,\n", + " bandwidth_mz_intensity_rt, bandwidth_n_peaks)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Extract all ROIs" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "mzml_file = os.path.join(mzml_path, file_name)\n", + "good_roi, junk = make_roi(mzml_file, mz_tol=roi_mz_tol, mz_units='ppm', min_length=roi_min_length,\n", + " min_intensity=roi_min_intensity, start_rt=roi_start_rt, stop_rt=roi_stop_rt)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "512540" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "all_roi = good_roi + junk\n", + "len(all_roi)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "How many singleton and non-singleton ROIs?" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "352967" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len([roi for roi in all_roi if roi.n == 1])" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "159573" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len([roi for roi in all_roi if roi.n > 1])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Keep only the ROIs that can be fragmented above **min_ms1_intensity threshold**." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "175000.0" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "min_ms1_intensity" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "10190" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "keep = []\n", + "for roi in all_roi:\n", + " if np.count_nonzero(np.array(roi.intensity_list) > min_ms1_intensity) > 0:\n", + " keep.append(roi)\n", + "\n", + "all_roi = keep\n", + "len(keep)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Turn ROIs into chromatograms/chemicals" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 10190\n", + "INFO : RoiToChemicalCreator : Found 10190 ROIs above thresholds\n" + ] + } + ], + "source": [ + "set_log_level_debug()\n", + "rtcc = RoiToChemicalCreator(ps, all_roi)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to example_data\\results\\mzml_compare\\dataset.p\n" + ] + } + ], + "source": [ + "data = rtcc.chemicals\n", + "save_obj(data, os.path.join(experiment_out_dir, 'dataset.p'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Run Top-N Controller" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "set_log_level_warning()\n", + "pbar = True" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "1441.1996749999914it [01:41, 14.17it/s] \n" + ] + } + ], + "source": [ + "mass_spec = IndependentMassSpectrometer(ionisation_mode, data, ps)\n", + "controller = TopNController(mass_spec, N, isolation_window, mz_tol,\n", + " rt_tol, min_ms1_intensity)\n", + "controller.run(min_rt, max_rt, pbar)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "controller.write_mzML('my_analysis', mzml_out)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'example_data\\\\results\\\\mzml_compare\\\\simulated.mzML'" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mzml_out" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Compare Results" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load simulated and real data." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of ms1 scans = 1258\n", + "Number of ms2 scans = 8619\n", + "Total scans = 9877\n", + "Number of selected precursors = 8619\n" + ] + } + ], + "source": [ + "simulated_input_file = mzml_out\n", + "simulated_mzs, simulated_rts, simulated_intensities, simulated_cumsum_ms1, simulated_cumsum_ms2 = count_stuff(\n", + " simulated_input_file, min_rt, max_rt)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of ms1 scans = 1751\n", + "Number of ms2 scans = 7655\n", + "Total scans = 9406\n", + "Number of selected precursors = 7672\n" + ] + } + ], + "source": [ + "real_input_file = mzml_file\n", + "real_mzs, real_rts, real_intensities, real_cumsum_ms1, real_cumsum_ms2 = count_stuff(\n", + " real_input_file, min_rt, max_rt)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot number of scans" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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VZCWvKdjx44cPnyYJUuW0K5dO9atW0ezZs0YP358luUi3Nb1DgoKIiAggIYNG6alVapUCTBWxpXw9ttvExkZScWKFQkKCmLKlCkcOHAgyzKbNm1i3759BAcHp1mLISEhnDhxIm35+V27dtG8efN05Tx/Z0W3bt3YsmULu3fvZubMmfTt29er3Lhx4zhy5AjTp0+nYcOGxMbGUr9+fb788ssMskePHuWuu+4iOjqaZ555xue25CXnz8PevfDf/8KCBZkHdk1IMCvqhoXBhAnQvr0JafTii3YZDUve4qvjxCJgpLO8BoCKSBgmnt9HedCuIsPq1ZnnBQRknV+hQtb51atnne8rfn5+REdHEx0dzYgRI+jXrx+jRo1iyJAhlCpVymuZkiVLpvstIunSxBlJT01NBaCYE9dG3bq9Ll68mGW75s+fz6BBg5g8eTItWrSgTJkyTJs2jYULF2ZZLjU1lcaNGzNv3rwMebnlBBISEkKHDh144okniI+P56GHHspUtnz58nTq1IlOnToxYcIEmjRpwosvvkhUVFSazJEjR2jbti0NGjTgvffeSzt/BcHhw8alfNAguPlm2L8fvN0G58+bLry5c80k3JQUuP9+o5gaNcr/dluuTXxVUkOAT4FEIABYi4mA/i0wPG+aZskr6tevT3JyMufOnctUSWWXis4reHx8fNr3rVu3Zllm7dq13HbbbenmaLksIRelSpUiJSUlXVrTpk15//33qVChAqGhoV7rrlevHuvXrycmJiYtbf369V5lM6Nv3760bduWJ598Ej8fzYVSpUpRq1YtDh8+nJYWHx9PmzZtuPnmm3n//fczeAnmF6rGYho40Cig+++HWrUyKqg//oBXXzVzpH7/3bxM/eMf0LevUWoWS37i079FVU8CfxWRtkBTzPyqzar6RV42znJlJCUl0alTJ2JiYoiIiCA4OJiNGzcyadIkoqKiKJOLsyvDw8OpXr06o0aNYuLEicTFxTF27Ngsy9SpU4fZs2ezfPlywsPDmTdvHmvWrEnnVBAWFsby5cvZvXs35cuXJyQkhO7duzN58mTat2/PmDFjqFGjBgcPHmTRokU88cQT1K5dm6effpqePXtyyy230Lp1axYsWMD333+fLUurTZs2JCYmEhwc7DV/6dKlzJs3j65du1KnTh1UlSVLlvDpp58yevRoAA4fPkzr1q2p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\n", + "text/plain": [ + "<Figure size 432x288 with 1 Axes>" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "plt.rcParams.update({'font.size': 14})\n", + "out_file = os.path.join(base_dir, 'results', 'topN_num_scans.png')\n", + "plot_num_scans(real_cumsum_ms1, real_cumsum_ms2, simulated_cumsum_ms1, simulated_cumsum_ms2, out_file)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Check the number of precursors that could be matched at different m/z and RT tolerances" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found 4875/7655 (0.636839)\n", + "Real\t\t\t\t\t\tSimulated\n", + "mz 144.98 rt 0.6005 intensity 1548081.2500\tmz 144.98 rt 0.5394 intensity 1735726.1927\n", + "mz 207.16 rt 0.7352 intensity 1443976.8750\tmz 207.16 rt 0.6714 intensity 1517306.5950\n", + "mz 126.05 rt 0.8693 intensity 1087971.0000\tmz 126.05 rt 0.8114 intensity 1324472.7979\n", + "mz 146.98 rt 1.0029 intensity 727259.1875\tmz 146.98 rt 0.8914 intensity 808759.8595\n", + "mz 224.19 rt 1.1375 intensity 395470.6562\tmz 224.19 rt 1.3004 intensity 406930.1763\n", + "mz 338.34 rt 1.2750 intensity 385685.9688\tmz 338.34 rt 1.1644 intensity 415747.1931\n", + "mz 116.07 rt 1.4077 intensity 366917.8750\tmz 116.07 rt 1.0274 intensity 488422.7070\n", + "mz 131.13 rt 1.5412 intensity 322462.5000\tmz 131.13 rt 1.4334 intensity 381448.3994\n", + "mz 104.11 rt 1.6740 intensity 267410.7188\tmz 104.11 rt 1.5724 intensity 329901.3222\n", + "mz 228.20 rt 2.0738 intensity 605814.3125\tmz 228.20 rt 2.3074 intensity 600740.7569\n", + "mz 162.08 rt 2.2073 intensity 955391.5000\tmz 162.08 rt 1.7074 intensity 324396.9502\n", + "mz 128.95 rt 2.4319 intensity 846180.3125\tmz 128.95 rt 2.0474 intensity 836551.3300\n", + "mz 144.93 rt 2.5655 intensity 3498059.0000\tmz 144.93 rt 2.4424 intensity 509680.6810\n", + "mz 125.07 rt 2.6990 intensity 506922.0938\tmz 125.07 rt 2.5504 intensity 501693.7715\n", + "mz 144.07 rt 2.8327 intensity 501226.1562\tmz 144.07 rt 2.6274 intensity 495539.7049\n", + "mz 83.06 rt 2.9652 intensity 430810.1875\tmz 83.06 rt 2.7604 intensity 426244.7156\n", + "mz 149.01 rt 3.0988 intensity 392989.6250\tmz 149.01 rt 2.8944 intensity 389704.9924\n", + "mz 88.08 rt 3.2314 intensity 388301.0312\tmz 88.08 rt 3.0454 intensity 383107.1695\n", + "mz 102.13 rt 3.6555 intensity 343353.2812\tmz 102.13 rt 3.1804 intensity 358989.9052\n" + ] + } + ], + "source": [ + "mz_tol = None # in ppm. if None, then 2 decimal places is used for matching the m/z\n", + "rt_tol = 5 # seconds\n", + "matches = match_peaklist(real_mzs, real_rts, real_intensities, simulated_mzs, simulated_rts, simulated_intensities, mz_tol, rt_tol)\n", + "check_found_matches(matches, 'Real', 'Simulated')" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found 6546/7655 (0.855127)\n", + "Real\t\t\t\t\t\tSimulated\n", + "mz 144.98 rt 0.6005 intensity 1548081.2500\tmz 144.98 rt 0.5394 intensity 1735726.1927\n", + "mz 207.16 rt 0.7352 intensity 1443976.8750\tmz 207.16 rt 0.6714 intensity 1517306.5950\n", + "mz 126.05 rt 0.8693 intensity 1087971.0000\tmz 126.05 rt 0.8114 intensity 1324472.7979\n", + "mz 146.98 rt 1.0029 intensity 727259.1875\tmz 146.98 rt 0.8914 intensity 808759.8595\n", + "mz 224.19 rt 1.1375 intensity 395470.6562\tmz 224.19 rt 1.3004 intensity 406930.1763\n", + "mz 338.34 rt 1.2750 intensity 385685.9688\tmz 338.34 rt 1.1644 intensity 415747.1931\n", + "mz 116.07 rt 1.4077 intensity 366917.8750\tmz 116.07 rt 1.0274 intensity 488422.7070\n", + "mz 131.13 rt 1.5412 intensity 322462.5000\tmz 131.13 rt 1.4334 intensity 381448.3994\n", + "mz 104.11 rt 1.6740 intensity 267410.7188\tmz 104.11 rt 1.5724 intensity 329901.3222\n", + "mz 228.20 rt 2.0738 intensity 605814.3125\tmz 228.20 rt 2.3074 intensity 600740.7569\n", + "mz 162.08 rt 2.2073 intensity 955391.5000\tmz 162.08 rt 1.7074 intensity 324396.9502\n", + "mz 128.95 rt 2.4319 intensity 846180.3125\tmz 128.95 rt 2.0474 intensity 836551.3300\n", + "mz 144.93 rt 2.5655 intensity 3498059.0000\tmz 144.93 rt 2.4424 intensity 509680.6810\n", + "mz 125.07 rt 2.6990 intensity 506922.0938\tmz 125.07 rt 2.5504 intensity 501693.7715\n", + "mz 144.07 rt 2.8327 intensity 501226.1562\tmz 144.07 rt 2.6274 intensity 495539.7049\n", + "mz 83.06 rt 2.9652 intensity 430810.1875\tmz 83.06 rt 2.7604 intensity 426244.7156\n", + "mz 149.01 rt 3.0988 intensity 392989.6250\tmz 149.01 rt 2.8944 intensity 389704.9924\n", + "mz 88.08 rt 3.2314 intensity 388301.0312\tmz 88.08 rt 3.0454 intensity 383107.1695\n", + "mz 102.13 rt 3.6555 intensity 343353.2812\tmz 102.13 rt 3.1804 intensity 358989.9052\n" + ] + } + ], + "source": [ + "mz_tol = None\n", + "rt_tol = 10\n", + "matches = match_peaklist(real_mzs, real_rts, real_intensities, simulated_mzs, simulated_rts, simulated_intensities, mz_tol, rt_tol)\n", + "check_found_matches(matches, 'Real', 'Simulated')" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found 6759/7655 (0.882952)\n", + "Real\t\t\t\t\t\tSimulated\n", + "mz 144.98 rt 0.6005 intensity 1548081.2500\tmz 144.98 rt 0.5394 intensity 1735726.1927\n", + "mz 207.16 rt 0.7352 intensity 1443976.8750\tmz 207.16 rt 0.6714 intensity 1517306.5950\n", + "mz 126.05 rt 0.8693 intensity 1087971.0000\tmz 126.05 rt 0.8114 intensity 1324472.7979\n", + "mz 146.98 rt 1.0029 intensity 727259.1875\tmz 146.98 rt 0.8914 intensity 808759.8595\n", + "mz 224.19 rt 1.1375 intensity 395470.6562\tmz 224.19 rt 1.3004 intensity 406930.1763\n", + "mz 338.34 rt 1.2750 intensity 385685.9688\tmz 338.34 rt 1.1644 intensity 415747.1931\n", + "mz 116.07 rt 1.4077 intensity 366917.8750\tmz 116.07 rt 1.0274 intensity 488422.7070\n", + "mz 131.13 rt 1.5412 intensity 322462.5000\tmz 131.13 rt 1.4334 intensity 381448.3994\n", + "mz 104.11 rt 1.6740 intensity 267410.7188\tmz 104.11 rt 1.5724 intensity 329901.3222\n", + "mz 228.20 rt 2.0738 intensity 605814.3125\tmz 228.20 rt 2.3074 intensity 600740.7569\n", + "mz 162.08 rt 2.2073 intensity 955391.5000\tmz 162.08 rt 1.7074 intensity 324396.9502\n", + "mz 128.95 rt 2.4319 intensity 846180.3125\tmz 128.95 rt 2.0474 intensity 836551.3300\n", + "mz 144.93 rt 2.5655 intensity 3498059.0000\tmz 144.93 rt 2.4424 intensity 509680.6810\n", + "mz 125.07 rt 2.6990 intensity 506922.0938\tmz 125.07 rt 2.5504 intensity 501693.7715\n", + "mz 144.07 rt 2.8327 intensity 501226.1562\tmz 144.07 rt 2.6274 intensity 495539.7049\n", + "mz 83.06 rt 2.9652 intensity 430810.1875\tmz 83.06 rt 2.7604 intensity 426244.7156\n", + "mz 149.01 rt 3.0988 intensity 392989.6250\tmz 149.01 rt 2.8944 intensity 389704.9924\n", + "mz 88.08 rt 3.2314 intensity 388301.0312\tmz 88.08 rt 3.0454 intensity 383107.1695\n", + "mz 102.13 rt 3.6555 intensity 343353.2812\tmz 102.13 rt 3.1804 intensity 358989.9052\n" + ] + } + ], + "source": [ + "mz_tol = None\n", + "rt_tol = 15\n", + "matches = match_peaklist(real_mzs, real_rts, real_intensities, simulated_mzs, simulated_rts, simulated_intensities, mz_tol, rt_tol)\n", + "check_found_matches(matches, 'Real', 'Simulated')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot the matches" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "unmatched_intensities = []\n", + "matched_intensities = []\n", + "for key, value in list(matches.items()):\n", + " intensity = key[2]\n", + " if value is None:\n", + " unmatched_intensities.append(intensity)\n", + " else:\n", + " matched_intensities.append(intensity)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [], + "source": [ + "plt.rcParams.update({'font.size': 18}) " + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "<Figure size 432x288 with 1 Axes>" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "out_file = os.path.join(base_dir, 'results', 'topN_matched_intensities.png')\n", + "plot_matched_intensities(matched_intensities, unmatched_intensities, out_file)" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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zLp165g7dy59+vRhwIAB7ZU1EZG2VVwMpaVQWRnmxcUdnSMRkRZTQUckwUEHHcQuu+zCggULeO+995KGuf/++6mpqeGkk06id+/e7ZxDEZE2YgYjRsCYMWGuQXBFpAtTQUckiTPPPBNIXasTWz558uSUaSxZsoTrr7+eww8/nJ133plevXpRXl7OhAkTuP7669m8eXOTOKNHj2bixIkALFu2bLtngFI9B7Ro0SIuuOACdt99d0pLSykvL2ffffdl6tSpvPHGGynzV1dXx8yZM9l///0pKSlhwIABHHfcccyfPz9lHICqqiquueYavvzlL9OvXz969erFbrvtxtSpU1m+fHnauPfccw8TJkxoqAk77LDDmDt3bto4ItLOzKBHDxVyRKTrc3dNnXgaN26cN8fChQszhglPl3btqS2cddZZDvhpp53mixcvdsCHDx/udXV124X74IMPHPCRI0d6XV2dDx8+3AG/8847tws3btw4JwxU62bm5eXlbmYNy8aPH++VlZXbxRk/frz379/fAS8oKPChQ4duN91///3bhb/pppu8sLCwIc3S0lLv3bt3w/tDDjkk6T5eeeWVfvTRRzvgxcXF3qdPn4Y4vXr18pdffjnpZ7Rw4UIfNWpUQ9iioiIvLS1teN+/f3//xz/+kTTuj370o4ZwBQUF230eN954Y0O6zz//fOYvqxPL5jcoIiIiuQHM9xTX0arREUlizJgxfO1rX2PlypU899xz262L1eZ897vfpaAg9U/owAMPZObMmXz00Uds2bKF9evXs3nzZv7617+y++67M3/+fC677LLt4rz++us8/PDDAIwcOZLVq1dvN512WuMYtg8++CBTp06lrq6Ok08+mYULF1JVVUV1dTWffvopf/7znxk3blzSvP3+97/ntdde44EHHqCqqoqNGzfy9ttvs88++7BlyxYuvPDCJnEqKio45phjWLZsGSeccAJvvvkmmzdvpqqqio8//pgzzzyT9evXc9JJJ7Fhw4bt4t5zzz38/ve/B+CSSy5h7dq1rF+/nlWrVjF58mQuueQS1qxZk/KzFBEREWm2VCUgTZ1jUo1O8qktxNfouLvfeuutDviZZ57ZEKa+vr6h5iH2Waeq0Uln8eLFXlRU5CUlJV5dXb3duueff94BHzVqVMr4NTU1PmLECAf89NNPb/Y+Av73v/+9yfr58+c3rF+6dOl266688koHfNKkSV5fX580/WOOOcYBv+666xqW1dfX+6677uqAn3XWWU3i1NfX+xFHHNGwXdXoiIiISLZQjY5I85166qn06tWLhx9+mOrqagBeeOEFli1bxvjx49lrr71anPaYMWPYe++92bRpE2+99Vaz4z/77LOsWLGCwsJCrrvuumbH/8Y3vsHXv/71JsvHjRvHiBEjAJp0xDB79mwALr74YixF2/3TTz8dgKeffrph2VtvvcVHH30EwOWXX94kjplxxRVXNHsfRERERNIp6ugMiHRW5eXlHH/88Tz44IM89NBDTJ48OZw7qJIAACAASURBVKtOCOI9/fTT3HHHHbz22musWrUqaQcEn376abPz9s9//hOA/fffn+HDhzc7/pe//OWU64YPH86KFStYv359w7Lly5ezYsUKAE455ZSUTfZqamoawse8+eabAAwZMoQ99tgjabyDDz6YoqIitm3b1rwdEREREUlBBR2RNCZPnsyDDz7I3XffzSmnnMJDDz1EcXFxQ81FOlOnTuV3v/tdw/vi4mIGDBhAcTQuxbp166itrW2oLWqOz6JRU2Nj/jRXWVlZynW9evUCoDZuoMBVq1Y1vM7mWZpNmzY1CZ+uQNazZ08GDRrE6tWrM6YtIiIikg01XRNJ4+ijj2bIkCE899xz3HzzzVRWVvKtb32LQYMGpY335JNP8rvf/Y7CwkKmT5/ORx99xNatW1m7dm1DxwJf/epXgfCcXHO1JE5r1NfXN7yuqKjI+GzZ0qVLm72N9t4nERERyW8q6IikUVRUxHe+8x3q6+u58sorgcYxdtJ58MEHATj33HOZNm0au+yyS5PnWmK1Mi0xbNgwIIy10x6GDh3a8HrhwoXNijt48GAgfRO9mpoa1q5d27LMiYiIiCShgo5IBrHncWpra+nfvz/HH398xjix51kOPPDApOuXLVvW8IB+otjzL+lqOCZMmADAO++8w8qVKzPmp7V23nnnhsJOrPvrbH3pS18CQsHuww8/TBrm5Zdf1vM5IiIiklMq6IhkMG7cOKZPn85Pf/pTZs6cSc+ePTPG6devHwD/8z//k3T9FVdckbIg07dvXyA0EUvl8MMPZ/jw4dTV1fGzn/0sY35yYcqUKQDccsstLFq0KGU4d98u7wcccAC77rorANdee23S8L/61a9ym1kRERHp9lTQEcnCtGnT+M1vfpN1b2tHHnkkALfeeit33HFHQ29kn3zyCWeddRb33Xcf/fv3Txp3t912o7i4mIqKCh566KGkYYqLi7n++usBuO+++zj11FN5//33G9avWrWK22+/nalTp2a9j5lcdtlljBkzhurqag455BBmz55NVVVVw/rly5dz++23M27cOB555JGG5WbG9OnTAbjjjju49NJLGwYU/eyzzzjnnHN47rnnKCkpyVleRURERFTQEWkDU6ZMYcKECWzbto3vf//7lJSU0L9/f0aNGsVdd93FjBkz2G+//ZLGLS0tbejV7eSTT6a8vJzRo0czevRo/vKXvzSEO+2007j++uspKCjgwQcfZK+99qKsrIySkhJ23HFHzj//fN55552c7VN5eTlPPfUUe+21F2vWrGHKlCn069ePgQMHUlJSwk477cT555/PggULmjyP9N3vfpcf/ehHAPz6179m0KBBDBgwgB122IFZs2bxm9/8puFZHhEREZFcUEGnG4p7rrxL6gr579GjB88880xDLUhBQQFFRUUceeSRPPbYY/ziF79IG/+Pf/wjl19+OXvssQdbt25l2bJlLFu2bLsaFICf/OQnLFiwgLPPPpvRo0dTW1tLr1692G+//bjwwgu54YYbcrpfu+66KwsWLOCWW25h4sSJDBgwgMrKSoqKithvv/348Y9/zAsvvJC0w4abb76ZP//5z3z1q1+lZ8+euDuHHHIIjz/+eE5rnkREREQATF26dm7jx4/3+fPnZx1+0aJF7LXXXm2YIxFJR79BERGR9mNmb7j7+GTrVKMjIiIiIiJ5RwUdERERERHJOyroiIiIiIhI3lFBR0RERERE8o4KOiIiIl2QO9TUhLmIiDRV1NEZEBERkeZxhxUroLoaSkthxAhIGL6q23KH2looLtZnItLdqUZHRESki6mtDYWcvn3DvLa2o3OUXHvXOsUKgEuWhLlqu0S6N9XoiIiIdDHFxaEmp7IyzIuLOzpHTXVErVN8AbCyMrzv0aNttykinZcKOiIiIl2MWSg4dOYmWh1R6OgKBUARaT8q6IiIiHRBZp27tqK9Ch2Jz+R09gKgiLQfFXREREQk59qj0JGqeVxnLgCKSPtRZwQiIiJdSRfqVzpW6GhuISebXXQPBZyqqvSdMnShj0tEckw1OiIiIl1FN+hXOptdjIWpqgpN4wD69GnaPK4bfFwikoYKOiIiIl1FN+hWLJtdjIXp1y+8HzkyFGQSCzHd4OMSkTTUdE1ERKSr6AbdimWzi/Fh+vRJXsjJNi0RyV+q0REREekqukG3YtnsYrYfQzf4uEQkDdXoiEiHmDVrFmbGoYce2tFZaWLKlCmYGdOnT+/orIg01dIn/LuQxF1M1qFANmGShROR7kMFne5o2LBwxu+q07BhbfKxjB49OuuL2+aE7eqmT5/O9OnT2bBhQ0dnRUS6oViHAkuWhHmy3tOyCSMi3U/eFnTMrMDMzjazZ8xsjZnVmtkGM3vVzK40s7I0cXuY2c/N7C0zq4rivWJm55tlvidkZkeY2WNm9rmZbTGzxWZ2o5kNze1ettBnn3V0Dlqnq+e/i5kxYwYzZsxQQUdEOkR8hwKpupDOJoyIdD95+YyOmZUAjwGHxS2uBPoCX4mm88zsMHdfkhC3L/AcMC5atAnoDUyIpuPN7Nvuvi3Ftq8Ero7e1gNVwBhgKnB6tM13W7+XIiIi+a+5nROo0wERicnXGp1fEAo5DlwBlLt7P6AXcDqwARgF/ClJ3NsJhZx1wPFAH6AEmAJsAY4DZiTbqJkdQ2Mh5/q47e4DvAUMBuaYWc9W76GIiHRP3WwEzFiHAmPGpB4HJ5swItL95GtB54xofqe7/4e7VwC4e4273w9cHK2faGb9Y5HM7EDg1Ojt2e7+uAd17j4buCxad7GZDUmy3Wui+aPufom7b4y2+x6h0BSr3Tk/R/spnci8efMwM0aPHg3ASy+9xHHHHcegQYPo3bs3+++/PzfffDOe5OJk6dKlmBmxlpGvvfYakyZNYvDgwZSVlXHwwQfzxBNPNISvqanh2muvZZ999qGkpIShQ4fygx/8gHXr1iXN27p165g9ezYnnXQSe+65J2VlZZSWljJ27Fh+8pOf8OmnnzaJE3sgP2bnnXduyKOZMWXKlCZx1q5dy7Rp0xg3bhzl5eWUlJSw++67853vfIc5c+ak/fwee+wxJk6cSHl5OX369GHChAncd999aePU19dz9913c+SRRzJ48GB69OjBjjvuyGmnncarr76aNu6rr77K8ccfz4ABA+jTpw8HHHAAN954I/X19WnjiXSobvowSjYdCqjTARFpwt3zbiLUvDjwf1Os3zda78DwuOXXRcveTxGvN6E2yIELEtbtHZfmQSni3xat/2e2+zJu3DhvjoULF2YOFP4au/bUBkaNGuWAT5s2rUVhn3/+eQd81KhRfuedd3phYaGbmffr18/jjg2/8MILm6T38ccfN6yfM2eOFxcXN4lbUFDg//Vf/+WbN2/2Qw891AHv1auX9+7duyHMgQce6Fu3bm2S/k9/+tPt8tC3b18vLCxseD948GB/++23t4szdepUHzp0aEOYQYMG+dChQxumqVOnbhf+xRdf9IEDBzaE79Gjh/ft23e77ca78847HfBDDjnEr7rqqoZ9TPy8brjhhqTfQWVlpR9xxBEN4cxsu+0VFBT47373u6Rx77vvvu32v7y83IuKihzwk046ySdPnpz1sZAoq9+gSEtt3eq+aJH7ypVhnuT3LiLSnQDzPcV1dL7W6CyN5gemWB97/uYzIP5W9sRo/rdkkdx9M/D36O1hCatjcSuAVLeSn4rmXzGzPinCSBe3Zs0afvCDH/DDH/6QVatWsWHDBtavX8+Pf/xjAG666Sbee++9lPEnT57M5MmTG+J+/vnnTJo0ifr6ei6++GIuueQS3n//fR5//HGqqqrYuHEjc+bMoaysjAULFvCnPzVtkTl8+HAuu+wy3nzzTTZu3EhFRQVbt25l/vz5fPOb32TNmjWcccYZsQI5ADfeeCOrV69ueP/666+zevXqhunGG29sWLd48WKOO+441q5dywEHHMBzzz3Hpk2bqKioYN26dTz11FOceOKJSff37bffZsaMGfzyl79k7dq1bNiwgdWrV3PyyScDcPnllyetqZo8eTLPPPMM++23H3PnzqW6upqKigrWr1/PNddcQ1FRERdeeCEvvfTSdvEWL17M2WefTV1dHUcddRSLFy9m/fr1VFRUcP311/Poo49mrH0S6TB6GEVEJHupSkBdeQJ+SrhTW09obtYvWt4DOA1YH637XlwcIzQtc+AHadK+NgrzbsLyW6Llr6SJuxeNd6rHZ7MvqtFJMbWBXNXoAH7uuecmjbfvvvs64DNmzNhueXyNzsSJE5vEq6qq2q62Yt68eU3CxGpFksVPZ8uWLT527NiU6ca2+fHHH6dM45RTTnHAd999d6+srMxqu7EaHcCvvvrqJus3b97sgwcPdsBnz5693bqnn37aAR89erSvXbs2afrXXnutA37sscdut/ycc85xwPfYYw/fvHlzk3i//OUvG/KlGh3plOrrQ01OfX1H50REpMPR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\n", + "text/plain": [ + "<Figure size 864x432 with 1 Axes>" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "out_file = os.path.join(base_dir, 'results', 'topN_matched_precursors.png')\n", + "plot_matched_precursors(matches, 50, 1000, 180, 1260, out_file)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/Synthetic data creation scripts/vimms_data_generation/05. Varying N in Top-N Simulations.ipynb b/Synthetic data creation scripts/vimms_data_generation/05. Varying N in Top-N Simulations.ipynb new file mode 100644 index 00000000..ff2d57f9 --- /dev/null +++ b/Synthetic data creation scripts/vimms_data_generation/05. Varying N in Top-N Simulations.ipynb @@ -0,0 +1,897 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 5. Varying N in Top-N Simulations" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook loads an existing Beer1pos data and runs it through the simulator with varying N (the number of precursor peaks selected for fragmentations) for Top-N DDA fragmentation. The results here correspond to Section 3.3 in the paper for the Beer1pos data." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import pylab as plt\n", + "import pymzml\n", + "import math\n", + "import seaborn as sns" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.append('..')" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from vimms.Roi import RoiToChemicalCreator, make_roi\n", + "from vimms.DataGenerator import DataSource, PeakSampler, get_spectral_feature_database\n", + "from vimms.MassSpec import IndependentMassSpectrometer\n", + "from vimms.Controller import TopNController\n", + "from vimms.TopNExperiment import get_params, run_serial_experiment, run_parallel_experiment\n", + "from vimms.PlotsForPaper import get_df, load_controller, compute_performance_scenario_2\n", + "from vimms.Common import *" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "set_log_level_debug()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Parameters" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "base_dir = 'example_data'\n", + "mzml_path = os.path.join(base_dir, 'beers', 'fragmentation', 'mzML')\n", + "file_name = 'Beer_multibeers_1_T10_POS.mzML'\n", + "\n", + "experiment_name = 'beer1pos'\n", + "experiment_out_dir = os.path.abspath(os.path.join(base_dir, 'results', experiment_name, 'mzML'))" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'C:\\\\Users\\\\joewa\\\\Work\\\\git\\\\vimms\\\\examples\\\\example_data\\\\results\\\\beer1pos\\\\mzML'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "experiment_out_dir" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "min_rt = 3*60 # start time when compounds begin to elute in the mzML file\n", + "max_rt = 21*60" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "kde_min_ms1_intensity = 0 # min intensity to be selected for kdes\n", + "kde_min_ms2_intensity = 0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### a. ROI extraction parameters" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "roi_mz_tol = 10\n", + "roi_min_length = 1\n", + "roi_min_intensity = 0\n", + "roi_start_rt = min_rt\n", + "roi_stop_rt = max_rt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### b. Top-N parameters" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "isolation_window = 1 # the isolation window in Dalton around a selected precursor ion\n", + "ionisation_mode = POSITIVE\n", + "N = 10\n", + "rt_tol = 15\n", + "mz_tol = 10\n", + "min_ms1_intensity = 1.75E5 # minimum ms1 intensity to fragment" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "mzml_out = os.path.join(experiment_out_dir, 'simulated.mzML')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Train densities" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO : DataSource : Loading Beer_multibeers_1_T10_POS.mzML\n" + ] + } + ], + "source": [ + "ds = DataSource()\n", + "ds.load_data(mzml_path, file_name=file_name)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : PeakSampler : Extracted 7647 MS2 scans\n", + "DEBUG : PeakSampler : Computing parent intensity proportions\n", + "DEBUG : PeakSampler : Extracting scan durations\n", + "DEBUG : PeakSampler : Training KDEs for ms_level=1\n", + "DEBUG : PeakSampler : Retrieving mz_intensity_rt values from <vimms.DataGenerator.DataSource object at 0x00000233E649CA20>\n", + "INFO : DataSource : Using values from scans\n", + "DEBUG : PeakSampler : Retrieving n_peaks values from <vimms.DataGenerator.DataSource object at 0x00000233E649CA20>\n", + "DEBUG : PeakSampler : Training KDEs for ms_level=2\n", + "DEBUG : PeakSampler : Retrieving mz_intensity_rt values from <vimms.DataGenerator.DataSource object at 0x00000233E649CA20>\n", + "INFO : DataSource : Using values from scans\n", + "DEBUG : PeakSampler : Retrieving n_peaks values from <vimms.DataGenerator.DataSource object at 0x00000233E649CA20>\n" + ] + } + ], + "source": [ + "bandwidth_mz_intensity_rt=1.0\n", + "bandwidth_n_peaks=1.0\n", + "ps = get_spectral_feature_database(ds, file_name, kde_min_ms1_intensity, kde_min_ms2_intensity, min_rt, max_rt,\n", + " bandwidth_mz_intensity_rt, bandwidth_n_peaks)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Extract all ROIs" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "mzml_file = os.path.join(mzml_path, file_name)\n", + "good_roi, junk = make_roi(mzml_file, mz_tol=roi_mz_tol, mz_units='ppm', min_length=roi_min_length,\n", + " min_intensity=roi_min_intensity, start_rt=roi_start_rt, stop_rt=roi_stop_rt)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "266107" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "all_roi = good_roi + junk\n", + "len(all_roi)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "How many singleton and non-singleton ROIs?" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "185119" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len([roi for roi in all_roi if roi.n == 1])" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "80988" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len([roi for roi in all_roi if roi.n > 1])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Keep only the ROIs that can be fragmented above **min_ms1_intensity threshold**." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "175000.0" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "min_ms1_intensity" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "10079" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "keep = []\n", + "for roi in all_roi:\n", + " if np.count_nonzero(np.array(roi.intensity_list) > min_ms1_intensity) > 0:\n", + " keep.append(roi)\n", + "\n", + "all_roi = keep\n", + "len(keep)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Turn ROIs into chromatograms/chemicals" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG : RoiToChemicalCreator : 0/ 10079\n", + "INFO : RoiToChemicalCreator : Found 10079 ROIs above thresholds\n" + ] + } + ], + "source": [ + "set_log_level_debug()\n", + "rtcc = RoiToChemicalCreator(ps, all_roi)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to C:\\Users\\joewa\\Work\\git\\vimms\\examples\\example_data\\results\\beer1pos\\mzML\\dataset.p\n" + ] + } + ], + "source": [ + "data = rtcc.chemicals\n", + "save_obj(data, os.path.join(experiment_out_dir, 'dataset.p'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Run Top-N Controller" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "set_log_level_warning()\n", + "pbar = False # turn off progress bar" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "Ns = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100]\n", + "rt_tols = [15]" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "N = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100]\n", + "rt_tol = [15]\n", + "len(params) = 28\n" + ] + } + ], + "source": [ + "params = get_params(experiment_name, Ns, rt_tols, mz_tol, isolation_window, ionisation_mode, data, ps, \n", + " min_ms1_intensity, min_rt, max_rt, experiment_out_dir, pbar)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'C:\\\\Users\\\\joewa\\\\Work\\\\git\\\\vimms\\\\examples\\\\example_data\\\\results\\\\beer1pos\\\\mzML'" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "experiment_out_dir" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Run the experiments." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# %time run_serial_experiment(params)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively since each simulated run is completely independent of the others, we can save time by running the different values of N in parallel. Here we use the [iparallel](https://ipyparallel.readthedocs.io/en/latest/) package. To do this, start a local parallel cluster with the following command:\n", + "\n", + "$ ipcluster start -n 5\n", + "\n", + "where 5 is the number of cores to use (for example)." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "experiment_beer1pos_N_1_rttol_15\n", + "experiment_beer1pos_N_2_rttol_15\n", + "experiment_beer1pos_N_3_rttol_15\n", + "experiment_beer1pos_N_4_rttol_15\n", + "experiment_beer1pos_N_5_rttol_15\n", + "experiment_beer1pos_N_6_rttol_15\n", + "experiment_beer1pos_N_7_rttol_15\n", + "experiment_beer1pos_N_8_rttol_15\n", + "experiment_beer1pos_N_9_rttol_15\n", + "experiment_beer1pos_N_10_rttol_15\n", + "experiment_beer1pos_N_15_rttol_15\n", + "experiment_beer1pos_N_20_rttol_15\n", + "experiment_beer1pos_N_25_rttol_15\n", + "experiment_beer1pos_N_30_rttol_15\n", + "experiment_beer1pos_N_35_rttol_15\n", + "experiment_beer1pos_N_40_rttol_15\n", + "experiment_beer1pos_N_45_rttol_15\n", + "experiment_beer1pos_N_50_rttol_15\n", + "experiment_beer1pos_N_55_rttol_15\n", + "experiment_beer1pos_N_60_rttol_15\n", + "experiment_beer1pos_N_65_rttol_15\n", + "experiment_beer1pos_N_70_rttol_15\n", + "experiment_beer1pos_N_75_rttol_15\n", + "experiment_beer1pos_N_80_rttol_15\n", + "experiment_beer1pos_N_85_rttol_15\n", + "experiment_beer1pos_N_90_rttol_15\n", + "experiment_beer1pos_N_95_rttol_15\n", + "experiment_beer1pos_N_100_rttol_15\n", + "Wall time: 17min 50s\n" + ] + } + ], + "source": [ + "%time run_parallel_experiment(params)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Analyse Results" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we need to load the ground truth peaks found by xcms from each mzML file.\n", + "- P = peaks picked by XCMS from the full-scan file\n", + "- Q = peaks picked by XCMS from the fragmentation file\n", + "\n", + "Peak picking was done using the script `extract_peaks.R` in the `example_data/results/ground_truth` folder. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Manual step: to generate the lists of ground truth peaks for evaluation, please run the R script on both the full-scan and simulated fragmentation files.**\n", + "\n", + "Requirements:\n", + "- Ensure that XCMS3 has been installed: https://bioconductor.org/packages/release/bioc/html/xcms.html.\n", + "\n", + "Steps for peak picking on simulated fragmentation files:\n", + "1. Ensure that fragmentation .mzML file are located in `examples\\example_data\\results\\beer1pos`.\n", + "2. Open a new R window and run the R script `examples\\example_data\\results\\beer1pos\\extract_peaks.R`. The script will process any files found in an `mzML` folder relative to its current location.\n", + "3. The file `extracted_peaks_ms1.csv` will be created in the folder of step 2.\n", + "\n", + "We have provided the peak-picking result for the full-scan file, but to do it manually, follow the same steps as above. \n", + "1. Place your full-scan .mzML file in `examples\\example_data\\results\\ground_truth\\mzML`.\n", + "2. Open a new R window and run the R script `examples\\example_data\\results\\ground_truth\\extract_peaks.R`. The script will process any files found in an `mzML` folder relative to its current location.\n", + "3. The file `extracted_peaks_ms1.csv` will be created in the folder of step 2." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [], + "source": [ + "min_ms1_intensity = 0\n", + "rt_range = [(min_rt, max_rt)]\n", + "mz_range = [(0, math.inf)]\n", + "results_dir = os.path.join(base_dir, 'results', 'ground_truth', 'mzML') \n", + "csv_file = os.path.join(results_dir, 'extracted_peaks_ms1.csv')\n", + "P_peaks_df = get_df(csv_file, min_ms1_intensity, rt_range, mz_range)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "csv_file = os.path.join(experiment_out_dir, 'extracted_peaks_ms1.csv')\n", + "Q_peaks_df = get_df(csv_file, min_ms1_intensity, rt_range, mz_range)" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "fullscan_filename = 'Beer_multibeers_1_fullscan1.mzML' \n", + "matching_mz_tol = 10 # ppm\n", + "matching_rt_tol = 30 # seconds" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading experiment_beer1pos_N_1_rttol_15\n", + "Matched 3678/10079 in fullscan data, 6573/10079 in fragmentation data\n", + "beer1pos N=1 rt_tol=15 tp=1183 fp=916 fn=2495 prec=0.564 rec=0.322 f1=0.410\n", + "Loading experiment_beer1pos_N_2_rttol_15\n", + "Matched 3678/10079 in fullscan data, 6552/10079 in fragmentation data\n", + "beer1pos N=2 rt_tol=15 tp=1532 fp=1155 fn=2146 prec=0.570 rec=0.417 f1=0.481\n", + "Loading experiment_beer1pos_N_3_rttol_15\n", + "Matched 3678/10079 in fullscan data, 6276/10079 in fragmentation data\n", + "beer1pos N=3 rt_tol=15 tp=1674 fp=1193 fn=2004 prec=0.584 rec=0.455 f1=0.512\n", + "Loading experiment_beer1pos_N_4_rttol_15\n", + "Matched 3678/10079 in fullscan data, 6025/10079 in fragmentation data\n", + "beer1pos N=4 rt_tol=15 tp=1741 fp=1195 fn=1937 prec=0.593 rec=0.473 f1=0.526\n", + "Loading experiment_beer1pos_N_5_rttol_15\n", + "Matched 3678/10079 in fullscan data, 5762/10079 in fragmentation data\n", + "beer1pos N=5 rt_tol=15 tp=1817 fp=1166 fn=1861 prec=0.609 rec=0.494 f1=0.546\n", + "Loading experiment_beer1pos_N_6_rttol_15\n", + "Matched 3678/10079 in fullscan data, 5400/10079 in fragmentation data\n", + "beer1pos N=6 rt_tol=15 tp=1833 fp=1152 fn=1845 prec=0.614 rec=0.498 f1=0.550\n", + "Loading experiment_beer1pos_N_7_rttol_15\n", + "Matched 3678/10079 in fullscan data, 5253/10079 in fragmentation data\n", + "beer1pos N=7 rt_tol=15 tp=1823 fp=1117 fn=1855 prec=0.620 rec=0.496 f1=0.551\n", + "Loading experiment_beer1pos_N_8_rttol_15\n", + "Matched 3678/10079 in fullscan data, 5072/10079 in fragmentation data\n", + "beer1pos N=8 rt_tol=15 tp=1849 fp=1093 fn=1829 prec=0.628 rec=0.503 f1=0.559\n", + "Loading experiment_beer1pos_N_9_rttol_15\n", + "Matched 3678/10079 in fullscan data, 4874/10079 in fragmentation data\n", + "beer1pos N=9 rt_tol=15 tp=1850 fp=1043 fn=1828 prec=0.639 rec=0.503 f1=0.563\n", + "Loading experiment_beer1pos_N_10_rttol_15\n", + "Matched 3678/10079 in fullscan data, 4704/10079 in fragmentation data\n", + "beer1pos N=10 rt_tol=15 tp=1867 fp=1001 fn=1811 prec=0.651 rec=0.508 f1=0.570\n", + "Loading experiment_beer1pos_N_15_rttol_15\n", + "Matched 3678/10079 in fullscan data, 2934/10079 in fragmentation data\n", + "beer1pos N=15 rt_tol=15 tp=1519 fp=501 fn=2159 prec=0.752 rec=0.413 f1=0.533\n", + "Loading experiment_beer1pos_N_20_rttol_15\n", + "Matched 3678/10079 in fullscan data, 2343/10079 in fragmentation data\n", + "beer1pos N=20 rt_tol=15 tp=1264 fp=435 fn=2414 prec=0.744 rec=0.344 f1=0.470\n", + "Loading experiment_beer1pos_N_25_rttol_15\n", + "Matched 3678/10079 in fullscan data, 1924/10079 in fragmentation data\n", + "beer1pos N=25 rt_tol=15 tp=1060 fp=387 fn=2618 prec=0.733 rec=0.288 f1=0.414\n", + "Loading experiment_beer1pos_N_30_rttol_15\n", + "Matched 3678/10079 in fullscan data, 1488/10079 in fragmentation data\n", + "beer1pos N=30 rt_tol=15 tp=809 fp=329 fn=2869 prec=0.711 rec=0.220 f1=0.336\n", + "Loading experiment_beer1pos_N_35_rttol_15\n", + "Matched 3678/10079 in fullscan data, 1160/10079 in fragmentation data\n", + "beer1pos N=35 rt_tol=15 tp=654 fp=249 fn=3024 prec=0.724 rec=0.178 f1=0.286\n", + "Loading experiment_beer1pos_N_40_rttol_15\n", + "Matched 3678/10079 in fullscan data, 992/10079 in fragmentation data\n", + "beer1pos N=40 rt_tol=15 tp=543 fp=252 fn=3135 prec=0.683 rec=0.148 f1=0.243\n", + "Loading experiment_beer1pos_N_45_rttol_15\n", + "Matched 3678/10079 in fullscan data, 838/10079 in fragmentation data\n", + "beer1pos N=45 rt_tol=15 tp=445 fp=223 fn=3233 prec=0.666 rec=0.121 f1=0.205\n", + "Loading experiment_beer1pos_N_50_rttol_15\n", + "Matched 3678/10079 in fullscan data, 660/10079 in fragmentation data\n", + "beer1pos N=50 rt_tol=15 tp=343 fp=184 fn=3335 prec=0.651 rec=0.093 f1=0.163\n", + "Loading experiment_beer1pos_N_55_rttol_15\n", + "Matched 3678/10079 in fullscan data, 628/10079 in fragmentation data\n", + "beer1pos N=55 rt_tol=15 tp=301 fp=192 fn=3377 prec=0.611 rec=0.082 f1=0.144\n", + "Loading experiment_beer1pos_N_60_rttol_15\n", + "Matched 3678/10079 in fullscan data, 199/10079 in fragmentation data\n", + "beer1pos N=60 rt_tol=15 tp=114 fp=54 fn=3564 prec=0.679 rec=0.031 f1=0.059\n", + "Loading experiment_beer1pos_N_65_rttol_15\n", + "Matched 3678/10079 in fullscan data, 162/10079 in fragmentation data\n", + "beer1pos N=65 rt_tol=15 tp=72 fp=58 fn=3606 prec=0.554 rec=0.020 f1=0.038\n", + "Loading experiment_beer1pos_N_70_rttol_15\n", + "Matched 3678/10079 in fullscan data, 96/10079 in fragmentation data\n", + "beer1pos N=70 rt_tol=15 tp=48 fp=38 fn=3630 prec=0.558 rec=0.013 f1=0.026\n", + "Loading experiment_beer1pos_N_75_rttol_15\n", + "Matched 3678/10079 in fullscan data, 104/10079 in fragmentation data\n", + "beer1pos N=75 rt_tol=15 tp=44 fp=44 fn=3634 prec=0.500 rec=0.012 f1=0.023\n", + "Loading experiment_beer1pos_N_80_rttol_15\n", + "Matched 3678/10079 in fullscan data, 80/10079 in fragmentation data\n", + "beer1pos N=80 rt_tol=15 tp=33 fp=39 fn=3645 prec=0.458 rec=0.009 f1=0.018\n", + "Loading experiment_beer1pos_N_85_rttol_15\n", + "Matched 3678/10079 in fullscan data, 92/10079 in fragmentation data\n", + "beer1pos N=85 rt_tol=15 tp=32 fp=47 fn=3646 prec=0.405 rec=0.009 f1=0.017\n", + "Loading experiment_beer1pos_N_90_rttol_15\n", + "Matched 3678/10079 in fullscan data, 92/10079 in fragmentation data\n", + "beer1pos N=90 rt_tol=15 tp=36 fp=45 fn=3642 prec=0.444 rec=0.010 f1=0.019\n", + "Loading experiment_beer1pos_N_95_rttol_15\n", + "Matched 3678/10079 in fullscan data, 44/10079 in fragmentation data\n", + "beer1pos N=95 rt_tol=15 tp=20 fp=17 fn=3658 prec=0.541 rec=0.005 f1=0.011\n", + "Loading experiment_beer1pos_N_100_rttol_15\n", + "Matched 3678/10079 in fullscan data, 76/10079 in fragmentation data\n", + "beer1pos N=100 rt_tol=15 tp=28 fp=37 fn=3650 prec=0.431 rec=0.008 f1=0.015\n" + ] + } + ], + "source": [ + "results = []\n", + "for N in Ns:\n", + " for rt_tol in rt_tols:\n", + "\n", + " # load chemicals and check for matching\n", + " chemicals = load_obj(os.path.join(experiment_out_dir, 'dataset.p')) \n", + " fragfile_filename = 'experiment_%s_N_%d_rttol_%d.mzML' % (experiment_name, N, rt_tol) \n", + "\n", + " # load controller and compute performance\n", + " controller = load_controller(experiment_out_dir, experiment_name, N, rt_tol)\n", + " if controller is not None:\n", + " tp, fp, fn, prec, rec, f1 = compute_performance_scenario_2(controller, chemicals, min_ms1_intensity,\n", + " fullscan_filename, fragfile_filename,\n", + " P_peaks_df, Q_peaks_df, matching_mz_tol, matching_rt_tol)\n", + " print('%s N=%d rt_tol=%d tp=%d fp=%d fn=%d prec=%.3f rec=%.3f f1=%.3f' % (experiment_name, \n", + " N, rt_tol, tp, fp, fn, prec, rec, f1))\n", + " res = (experiment_name, N, rt_tol, tp, fp, fn, prec, rec, f1) \n", + " results.append(res) " + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [], + "source": [ + "result_df = pd.DataFrame(results, columns=['experiment', 'N', 'rt_tol', 'TP', 'FP', 'FN', 'Prec', 'Rec', 'F1'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot precision, recall, f1" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "<Figure size 864x432 with 1 Axes>" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "plt.figure(figsize=(12, 6))\n", + "ax = sns.lineplot(x='N', y='Prec', hue='experiment', legend='brief', data=result_df)\n", + "plt.title('Top-N Precision')\n", + "for l in ax.lines:\n", + " plt.setp(l, linewidth=5)\n", + "plt.ylabel('Precision')\n", + "plt.xlabel(r'Top-$N$')\n", + "plt.legend(prop={'size': 20})\n", + "plt.tight_layout()\n", + "\n", + "fig_out = os.path.join(experiment_out_dir, 'topN_precision.png')\n", + "plt.savefig(fig_out, dpi=300)" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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AQAA3Ofjjz/Wvffeq9DQUPXs2VO1atXSpk2bNG7cOH333XdatGiRYmNjdcMNN+jvf/+7Ro8erX//+996+eWXJUlHjx5V3759lZmZqc8++0xVqlQpdPysrCxdeeWVSk1NVb9+/ZSVlaX//e9/euSRR7RhwwaNHj36vOo53Y033qglS5aoR48e6t27t0MdzmRnZ+uqq65ScnKyevXqpaysLE2aNEk33nijfv75Z7377rtavHixevToodDQUH355Zd66KGHVLlyZfXt27fQsUaOHKlnn31WUVFRuv7661WlShWtWrVKr776qn788UctXLhQERERDue/+uqrtWfPHvXo0UNBQUH65ptvNHToUB0/flzPPvuspPwQmZqaqrfeeksXX3yxevfufeoYcXEeeoSQtdZtX5KuVf69VVsk/fvE2EhJPU/8+UVJayStlPSbpCZnO2abNm1safXCj2tt7Se/P+NXx5dm2td+3mBfnr7O3vzeAtvwXz8WufaS53+xP6zaY/Py8lxa56TF252e74O5W1x6HgAA4Blr1671dglet2HDBhscHGzr169vd+3aVWhu5syZNiAgwPbu3fvU2PHjx22rVq2sMcb+9NNP1lprBw0aZCXZYcOGORy/du3aVpLt2LGjPX78+KnxQ4cO2Xr16llJds6cOeddj7XWXn755VaSveiii2xSUpJDDR9//LGVZD/++GOntV1//fWFaps7d66VZCtWrGjj4+NtSkrKqbktW7bY4OBgGxcXV+hYs2bNspJshw4dCq0veP5//OMfTs/fo0cPe/To0VPj+/fvt5GRkTYyMtJmZWWdGt+2bZuVZO+44w6Hz1gcxf1+l5RgneQVt15OsNb+aK1tZK2tb639z4mxYdbab0/8+SlrbXNr7cXW2iustevdWY8/25VyVB//nuh0LjQoQL3jYvT5Pe009/Er9OhVjfT4NU30xf0dtGr41frivg56/JrGurxRZUWWDf6r+99jl+vai6rLGGe3y52/W+Jr6eJaFRzG3/x1k/anH3fpuQAAADzhvffeU3Z2tt566y3VqFGj0FzXrl3Vs2dPfffdd8rIyJAkhYaGasqUKSpfvrwGDhyoV199VZ988okuu+wyDRs2rMjzvPjiiwoNDT31OioqSs8884yk/CtW51tPQc8995yio6PP+e/gzTffLFRb586dVbduXaWkpOi///2vKlT46+e/evXqqWPHjvrzzz+Vm5t7anzUqFGSpA8++KDQekkaNGiQ4uLiNHHiRKfnHzVqlMqWLXvqdZUqVdSrVy+lpaVpw4YN5/x53MWtDxqG67z280Zl5RR+plRoUICevr6Zel4co8iywU7fVyY4UG3rRqlt3Sj9/QpPVJrf8OL5Xi3Uc/R8pw0v3qLhBQAA8DMLFy6UJM2ZM0dLlixxmD9w4IByc3O1ceNGtWmT31agYcOGGjNmjPr376/HH39c0dHR+vzzzxUYGOj0HEFBQbr00ksdxrt06SJJWr58+QXVc1Lbtm2L8YkLq1ChgurXr+8wHhMTo23btjmcQ5Jq1Kih3Nxc7du371QIXLhwoYKDg/Xll1/qyy+/dHhPVlaWkpKSdOjQIVWqVOnUeGRkpBo0aOCwvlat/JYPKSkp5/yZ3IWA5QdW707T18t3O4zf1amubm9f2wsVnd1FNSN1W9tYTVy8o9D4tBV71O+SWHWoX6mIdwIAAPieQ4fyG3a98sorZ1x3+PDhQq+vuuoqRUREKD09XTfffLPD1aaCoqOjnYavatWqSZLS0tIuuJ6CxzsXRXUXDAoKKnL+5FzBRh+HDh1STk6ORowYccbzHT58uFDAOv1q1+nnKHiVzNvoOODjrLV64UfH3h9R5UP0f10cf4vgSx6/prEqlnO8sjZs2mpl5+Y5eQcAAIBvOhkg0tLSztgv4PLLLz/1HmutBg4cqPT0dEVHR2vs2LGaO3dukec4ePCg06Cwb9++QjWcbz0nufr2kHMRGRmpihUrnrWXQ+3avnkRoTgIWD5u9sYkLdji2OL84a4NFFHG+bZAX1GhXIiG9mjiML7pwGGNX5Do+YIAAADOU/v27SVJ8+bNK/Z7XnnlFU2fPl39+/fXrFmzFBwcrNtuu00HDx50uj4nJ0cLFixwGJ89e7YkqVWrv26zOJ96fEH79u2VkpKiNWvWuO0cJ68CeuuqFgHLh+XmWb30o2PfjzqVyum2dv6R6m9uU0txThpevPHLRhpeAAAAv/Hggw8qODhYQ4YM0caNGx3ms7KyCoWdxYsX6+mnn1aDBg303nvv6aKLLtIbb7yh3bt3a9CgQUU+1+qpp55SZuZfj+RJTk7W888/L0m68847z7seXzFkyBBJ0r333uv0WV1HjhzRokWLLugcFStWlDFGO3bsOPtiN+AeLB82delObdjv2Pnlye5NFBLkH9k4IMDo+d4t9Ld3Cje8OJKVq//8sE6jbqXhBQAA8H1NmjTRRx99pLvuukvNmzdX9+7d1ahRI2VnZ2vHjh2aN2+eKleurPXr1596jpUxRpMnT1Z4eLgk6f7779fMmTM1depUvf7663rssccKnaN69erKzMxUixYt1LNnT2VnZ2vq1Knau3evHnjgAV122WXnVY8v6datm1566SU99dRTatiwoa699lrVrVtXhw8f1vbt2zVnzhx16tRJ06dPP+9zhIWFqV27dpo3b5769++vRo0aKTAwUD179lTLli1d+GmcI2D5qKNZOXrtZ8ffRrSpXVHdW5z7jYne1KJGpPq3i9Vniwr/FuHblXvUr20tXVr/3NuEAgAAeNqAAQN08cUX67XXXtNvv/2mn3/+WeXLl1dMTIxuuummUw/Uvfvuu5WYmKjXX3/dobveuHHjtHTpUj311FPq3LlzoY5+ISEh+vXXX/Wvf/1LkydP1sGDB1WvXj0NHTpUDz300HnX42uefPJJdezYUaNGjdL8+fM1bdo0RUZGqkaNGho8eLBuu+22Cz7HhAkTNGTIEE2fPl2TJk2StVY1a9b0SMAyRV2e9FXx8fE2ISHB22W43aiZm/T6L44B63//10Ftakd5oaILk3o0S11fm6PkI1mFxhtUCdNPj3RWcKB/XJEDAKA0WrdunZo2bertMkq0OnXqSJISExO9WgeK//1ujFlqrY0/fZyfan1QUkamxszZ4jDevXk1vwxX0omGF90dG15sPnBYH/++zQsVAQAAAK5HwPJBb83cqCNZhbueBAUYPemkI58/ualNTbWKdWx48davm7QvjYYXAAAA8H8ELB+z+cBhTfpjp8N4/3axqhtd3gsVuU5AgNFzvVoo4LRHLxzJytV/nDzrCwAAAPA3BCwfYq3VsGmrlZtX+L648NAgPdytoZeqcq0WNSI1oL1ji/nvVu7Rgs3OnwkBAABQ0iUmJnL/VQlBwPIhXy/f7fShwvd3qa9KYaFeqMg9HruqsSqVD3EYH/btGmXl5HmhIgAAAMA1CFg+IuVIlp7/wXGbXM2KZXV3p7peqMh9IssFO72fjIYXAAAA8HcELB/x4k/rHFqYS9JzvVuoTHCgFypyr5ta11RrZw0vZm7S3rRjXqgIAAAAuHAELB+weOshfZGwy2H8+pbVdUXjKl6oyP0CAoxGOml4cTQrV/9xciUPAAAA8AcELC/LzMnVv77+02E8PDRIw65v5oWKPKdFjUjd7qThxfer9up3Gl4AAADADxGwvGzsnK3aknTEYfyJHk1UJaKMFyryrEevLqLhxbTVNLwAAACA3yFgedG2g0f09m+bHcbjalVQ/7axXqjI8yLLBmuok4YXW5KO6CMaXgAAAMDPELC8xFqrp7/50+EqTWCA0Ys3XKSA029OKsFubF1TbWpXdBgfRcMLAAAA+BkClpdMW7FHv292fObVPZ3rqmn1CC9U5D35DS+aO2144ax1PQAAAOCrCFhekHo0S899v9ZhvEaFsnqkW0MvVOR9zWMiNbBDHYfxH1bt1fxNNLwAAADel5iYKGOMBg0a5O1S4MMIWF7wyowNOuTkmVfP926hciFBXqjINwy5qpGiw5w0vPiWhhcAAADn68MPP9R9992ndu3aqVy5cjLG6Omnn/Z2WSUWAcvDjmTm6Muljs+8uu6i6rqiScl85lVx5Te8aOowvjXpiD6cT8MLAACA8/HYY49p7Nix2rRpk2JiYrxdTolHwPKw3zcfdLgaExYapGF/K9nPvCquG1rVULyThhdvz9qkPak0vAAAADhXkydPVmJiopKTk7ly5QEELA/7bcMBh7HrLqquqqXgmVfFkd/wooXThhf/oeEFAADwEevXr1fv3r0VFRWl8uXLq1OnTvr555+drp00aZKuuOIKVaxYUWXKlFHTpk31/PPPKzMzs8hjDxo0SLVq1VJoaKiqVq2q2267TRs2bHBYO2jQIBljtHXrVr399ttq2bKlypYtqy5dupxa0717d9WuXbvYn2348OEyxmj27NkaP368WrVqpbJly6pKlSq66667tG/fPqfv27RpkwYOHKgaNWooJCREMTExGjhwoDZt2uSwNiMjQ88995xatGihiIgIhYeHq379+urbt6+WLl1a7Fp9Uem94ccLrLWatd4xYJX2rYGnaxYToYEd6uiTBYmFxn/4c6/6bUpS54aVvVMYAAAopM7QH7xdwjlLfOm6Cz7Gtm3b1KFDB7Vo0UL33Xef9u7dqylTpqhHjx76/PPP1bdv31Nr7777bn300UeqWbOmbrjhBlWoUEGLFi3SM888o5kzZ+qXX35RUNBfP5JPnz5dN9xwg7Kzs/W3v/1NDRo00K5du/TVV1/phx9+0G+//abWrVs71PTII49o3rx5uu6663TttdcqMDDwgj/nG2+8oZ9//ll9+/ZV9+7dNX/+fH388ceaPXu2Fi9erMqV//qZbMmSJbryyiuVkZGhnj17qlmzZlq/fr0mTpyoadOmaebMmYqPj5eU/zNx9+7dtWDBAnXo0EH33HOPgoKCtHPnTs2ePVudO3dWmzZtLrh+byFgedCaPenan174NxXBgUadGkZ7qSLfNeSqRvp+1V4dPFz47+vZaWv00z86KzTowv/RAAAAOB9z587VP//5T73yyiunxh588EF16NBB999/v3r06KGIiAh98skn+uijj9SnTx9NnDhRZcuWPbV++PDhGjFihEaPHq1HHnlEkpSSkqJbb71V5cqV09y5c9Ws2V+3kKxZs0bt2rXTPffco2XLljnUtGzZMi1fvlx169Z12ef86aeftHjxYrVq1erU2JAhQ/Tmm29q6NCh+vDDDyXlB6aBAwcqPT1dn332mfr3739q/ZQpU9SvXz8NGDBAa9euVUBAgFavXq0FCxaod+/e+vrrrwudMy8vT2lpaS77DN7AFkEPcnb1qn29SgoLJeeeLrJssJ7q0cRhfOtBGl4AAADvioyM1LBhwwqNxcfHq3///kpNTT0VGt566y0FBQXpo48+KhSuJOmZZ55RpUqVNHHixFNjn376qVJTUzVixIhC4UqSmjdvrnvvvVfLly/X2rWOj/t54oknXBquJOn2228vFK6k/GAYGRmpzz///NQWxwULFmj9+vXq0KFDoXAlSX379lWnTp20YcMGzZ8/v9Dc6X8nkhQQEKCKFR3vx/cn/GTvQU63BzZme2BRbmhdQ5OX7NCSxJRC42/P3KzecTUUU8Hxf5QAAADu1rp1a4WHhzuMd+nSRePHj9fy5ct18803a+XKlYqOjtabb77p9DihoaFat+6ve8wXLlwoSVq5cqWGDx/usH7jxo2SpHXr1jkEsLZt257vxynS5Zdf7jAWGRmpuLg4zZkzR+vWrVNcXNypK2pdu3Z1epyuXbtq/vz5Wr58uS677DI1a9ZMcXFxmjRpkrZv365evXqpU6dOio+PV0iI4yN7/A0By0MOHs7Uyl2pDuPdmhKwimJMfsOL69+er9w8e2r8WHaunv9hrd7t7797cwEAgP+qWrWq0/Fq1apJktLS0pSSkiJrrZKSkjRixIhiHffQoUOSpA8++OCM6w4fPlzkuV2pOJ+z4H+rV6/udP3J8dTU/J+FAwMDNWvWLI0cOVJTp07Vk08+KUkKDw/XHXfcoRdffFFhYWGu+yAeRsDykNkbkmRt4bF6lcurdqXy3inITzStHqGBHWrr498TC43/+Oc+zd2YpMsa0fACAABvcUXDCH+0f/9+p+Mnu+tFRkYqMjJSktSqVSun90w5c/I9K1euVMuWLc+pJmPM2Redo+J8zoL/Laq74N69ewutk6SKFSvqjTfe0BtvvKHNmzdrzpw5GjNmjN555x2lpqZqwoQJLvscnsY9WB7ym5PtgV3ZHlgsQ65qpOiwUIfx4d+uUWZOrhcqAgAApdmyZcuUkZHhMD579mxJ+aEqLCxMzZs315o1a5ScnFys47Zv316SNG/ePJfVeiHmzJnjMJaWlqYVK1acajcv6dR9Wic//+lOjjvrfihJDRo00N133605c+YoLCxM06ZNu/DivYiA5QHZuXmauzHJYbwr2wOLJaJMsP51rfOGF+Pm0fACAAB4VlpamkaOHFloLCEhQRMnTlRkZKT69OkjSXr00UeVlZWlu+6669T2uIJSUlIKXd268847VaFCBY0YMUJ//PGHw/q8vLwiQ4w7TJgwQcuXLy80Nnz4cKWlpenWW29VaGj+L8A7duyoxo0ba/78+Zo6dWqh9VOnTtXcuXPVqFEjderUSVJ+m/s1a9Y4nC8lJUWZmZlOm1/4E7YIesCSxGRlZOYUGgsPDdIldaK8VJH/6dOqhib94djw4p1Zm9W7VQ3VoOEFAADwkMsuu0zjxo3T4sWL1bFjx1PPwcrLy9OYMWMUEREhSbrrrru0dOlSvfvuu6pfv76uueYaxcbGKjk5Wdu2bdPcuXN155136v3335ckVapUSVOnTlWfPn3Uvn17devWTc2bN1dAQIB27NihhQsX6tChQzp+/Pg51Ttu3LhTHfw2b94sSfruu++0a9cuSVKTJk00dOhQh/f16NFDHTt21C233KLq1atr/vz5mj9/vurUqaOXXnrp1DpjjMaPH6+rrrpKffv2Va9evdSkSRNt2LBB33zzjcLDw/Xpp58qICD/2s7KlSvVp08ftWnTRi1atFBMTIySkpI0bdo0ZWdnn7ony18RsDzA2fbAyxpVVnAgFxCL64wNL75fq/cG0PACAAB4Rt26dfX+++9r6NChev/995WZmanWrVtr2LBhuuaaawqtHT16tHr06KH3339fv/76q1JTUxUVFaXY2Fg9/vjjGjBgQKH13bp106pVq/Tqq69qxowZmjdvnkJCQhQTE6OuXbvqxhtvPOd658+fr/HjxxcaW7VqlVatWiUpv1ugs4A1ZMgQ9enTR2+++aamTJmisLAwDRo0SC+88IKqVCm8E6tdu3ZasmSJnn/+ef3666/67rvvFB0drVtvvVXPPPOMGjdufGptfHy8nnrqKc2ZM0fTp09XSkqKKleurDZt2ujhhx9Wjx49zvkz+hJjT++84OPi4+NtQkKCt8s4J11fm62tSW174jgAACAASURBVEcKjb1688W6qU1NL1Xkv0Z+t1Yf/e64LXD8XW11OQ0vAABwi3Xr1p263wYl38mHIP/222/q0qWLt8vxuOJ+vxtjllpr408f5xKKmyUePOIQroyRujQmDJyPf1zVUJXDaXgBAAAA30TAcjNnDxe+uGYFp13xcHZFNbzYRsMLAAAA+AAClpv9tsFJe/YmdA+8EL3jaqitkwYhb8/apN2px7xQEQAAAJCPgOVGhzNztHir43MPCFgXxhijkb2bKzCg8AP1jmfn6bnv1nqpKgAAgJJh+PDhstaWyvuvXIGA5UbzNx1UVm5eobGqEaFqHhPhpYpKjibVIjTo0joO49PX7NNsJ1cNAQAAAE8gYLmRs/bsVzSuImOMk9U4V/+4koYXAAAA8C0ELDfJy7Oaxf1XbhVeJlj/vtaxhWbioaP6YO5WL1QEAEDJ5W+P9gHOhyu+zwlYbrJmT7qSMjILjYUEBqhjg2gvVVQy9YqLUdu6jg0v3vlts3alHPVCRQAAlDyBgYHKzs72dhmA22VnZyswMPCCjkHAchNn7dnb16+k8qFBXqim5DLG6LleLZw3vPiehhcAALhCeHi40tPTvV0G4Hbp6ekKDw+/oGMQsNxk1vr9DmNdebiwWzSuFq47nTS8mLFmv9M2+QAA4NxERUUpJSVFBw8eVFZWFtsFUaJYa5WVlaWDBw8qJSVFUVGOu6POBZdT3CApI1Mrd6U5jHdtUtUL1ZQOj1zZUN+u3KMDp23LHP7tGnX4RyWVCb6wS70AAJRmoaGhio2NVXJyshITE5WbSzMplCyBgYEKDw9XbGysQkMdm6idCwKWGyzYctBhrEGVMMVWKueFakqH8DLB+vd1TfXI5BWFxrefaHjxULeGXqoMAICSITQ0VNWrV1f16tW9XQrg09gi6AaJBx2bK3SiuYXb9bw4Ru2KaHixM5mGFwAAAHA/ApYb7E51/GG+bnR5L1RSuhhj9Fxvx4YXmTl5emDiMh3PZjsDAAAA3IuA5Qa7U485jMVUKOuFSkqfRlXDdVfHOg7jf+5O0xNTV3FTLgAAANyKgOUGe1KPO4zVIGB5zCNXNlL1yDIO49+u3KP35/AAYgAAALgPAcvF8vKs0ytYNSoSsDwlLDRI7/ZvrZAgx2/vl2es18x1ji30AQAAAFcgYLnYwSOZysrJKzQWFhqkiDI0bPSkVrEV9WKfixzGrZUembxCm/ZneKEqAAAAlHQELBcranugMcbJarjTjW1qavBl9RzGD2fm6J5PE5R6NMsLVQEAAKAkI2C52O4Utgf6kie7N9HljSo7jG8/dFR//3yZcnLznLwLAAAAOD9uDVjGmO7GmA3GmM3GmKFnWHeTMcYaY+LdWY8nOGvRHlPBseECPCMwwGjUra1Ur7Jjm/zfNx/S8z+s80JVAAAAKKncFrCMMYGSRkvqIamZpFuNMc2crAuX9LCkxe6qxZOcbxEs54VKcFJk2WCNGxivcCf3wX2yIFGT/9jhhaoAAABQErnzClZbSZuttVuttVmSJkvq5WTdc5JeluSYTPzQLrYI+qR6lcP0zm2tFeDkVrhnpq3WksRkzxcFAACAEsedAauGpJ0FXu86MXaKMaaVpFrW2u/PdCBjzGBjTIIxJiEpKcn1lbqQ0xbtbBH0CZc3qqx/XdvUYTw71+r+CUud/t8OAAAAOBfuDFjO2ubZU5PGBEh6Q9JjZzuQtXastTbeWhtfubJjwwJfsjvF8R4stgj6jrs71dWNrWs6jB86kqV7xyfoaFaOF6oCAABASeHOgLVLUq0Cr2tK2lPgdbikFpJmG2MSJbWX9K0/N7rIOJ6t9OOFf0APDjSqEh7qpYpwOmOM/tOnhVrFVnCYW7s3XY9/uUrWWifvBAAAAM7OnQFriaSGxpi6xpgQSf0kfXty0lqbZq2NttbWsdbWkbRIUk9rbYIba3IrZw0uqkWWUYCzG3/gNWWCAzVmQBtVi3DcuvnDn3v1zqzNXqgKAAAAJYHbApa1NkfSg5JmSFon6Qtr7RpjzEhjTE93ndebnLVor1GBBhe+qEpEGY0d2EahQY7/E3jtl42avnqfF6oCAACAv3Prc7CstT9aaxtZa+tba/9zYmyYtfZbJ2u7+PPVK0naTYt2v9KyZgW9fFNLp3OPfrFC6/ele7giAAAA+Du3BqzSZrezFu10EPRpveJq6IEu9R3Gj2bl6p7xCUo+kuWFqgAAAOCvCFgu5LRFO8/A8nn/vLqxrmxaxWF8V8ox/d9nS5Wdm+eFqgAAAOCPCFgutMfpM7DYIujrAgKM3ugbp4ZVwhzmFm9L1ojv1nihKgAAAPgjApYLOdsiGMMWQb8QXiZY4+6IV2TZYIe5zxbt0IRF271QFQAAAPwNActFsnLytD/DsclFDF0E/UbtSuX1bv/WCnTSVn/Et2u0cMshL1QFAAAAf0LAcpH96cd1+vNpo8NCVSY40DsF4bx0bBCtZ65r6jCek2f1wMSl2pns2IofAAAAOImA5SK76CBYYtxxaR31u6SWw3jK0Wzd+2mCjmTmeKEqAAAA+AMClovQQbDkMMZoZK8WuqRORYe59fsy9OgXK5SXZ528EwAAAKUdActFnHcQJGD5q5CgAL03oI1iIh2vQs5Ys19vztzkhaoAAADg6whYLuK8gyABy59Fh4XqgzviVdbJfXSjZm7SD6v2eqEqAAAA+DIClos43SJIwPJ7zWMi9erNFzude+zLFVq9O83DFQEAAMCXEbBcxOkWQe7BKhGua1ldD3dr6DB+PDtPgz9NUFJGpheqAgAAgC8iYLmAtZYrWCXcP7o11DXNqzqM70k7rv/7bKmycvK8UBUAAAB8DQHLBQ4ezlLmaT9glw8JVGTZYC9VBFcLCDB6/ZY4NakW7jCXsD1Fz3yzWvb0B6EBAACg1CFguUBR2wONMV6oBu5SPjRIHwyMV1T5EIe5KQk7NX5BoueLAgAAgE8hYLmAs+2BdBAsmWpFldO7/VsrKMAxPD/3wzr9vvmgF6oCAACAryBguYCzFu3cf1Vyta9XScN7NncYz82zemDiMm0/dMQLVQEAAMAXELBcwGmDCzoIlmgD2tfWgPaxDuNpx7J1z/gEZRzP9kJVAAAA8DYClgvQQbB0evZvzdW+XpTD+KYDh/WPySuUm0fTCwAAgNKGgOUCbBEsnYIDA/Ru/zaq6eRq5cz1B/Tazxu8UBUAAAC8iYDlAnvS2CJYWkWVD9G4O+JVLiTQYe7d2Vs0bcVuL1QFAAAAbyFgXaAjmTlKPVr4fpugAKMq4WW8VBE8rUm1CL3RN87p3BNTV2nVrlQPVwQAAABvIWBdIGf3X1WLLKNAJ228UXJd07yaHruqkcN4Zk6eBn+6VAfSj3uhKgAAAHgaAesC0eACJz3YtYGuu6i6w/i+9OMaPGGpjmfneqEqAAAAeBIB6wLR4AInGWP0ys0t1TwmwmFuxc5U/fvr1bKWzoIAAAAlGQHrAvEMLBRULiRIYwfGKzosxGHuf8t26cP527xQFQAAADyFgHWB9rBFEKepUaGs3h/QRsGBjvfhvfDjOs3ZmOSFqgAAAOAJBKwL5GyLYAwBq9SLrxOl53u3cBjPs9KDny/T1qTDXqgKAAAA7kbAukBsEURR+l4Sq0GX1nEYzzieo3vGJyj1aJbniwIAAIBbEbAuQHZunvY7ab/NFkGc9PR1TdWpQbTD+NaDR3TbB4uVfISQBQAAUJIQsC7AvrTjyjutKVyl8iEqExzonYLgc4ICA/TOba1Uu1I5h7m1e9N169hFSsrI9EJlAAAAcAcC1gVgeyCKo0K5EI0bGK+w0CCHuQ37M9Rv7EIeRAwAAFBCELAuAM/AQnE1rBquDwbGq1yI49XNLUlH1HfsIu1Nc/x+AgAAgH8hYF0AZy3a6SCIonSoX0nj72qr8k5C1raDR3TLmIXamXzUC5UBAADAVQhYF8DpFkECFs7gkjpRmnBPO4U72S64M/mY+o1dpB2HCFkAAAD+ioB1AbgHC+ejdWxFTby3nSLLBjvM7U49plvGLOQ5WQAAAH6KgHUBuIKF89WyZgV9fm87VSznGLL2pR9X37GLtPlAhhcqAwAAwIUgYJ0na63Te7AIWCiu5jGRmjy4g6LDQhzmkjIy1XfMIq3fl+6FygAAAHC+CFjn6dCRLB3Pzis0Vi4kUBWcXJEAitK4WrgmD+6gKuGhDnOHjmTp1rGLtHp3mhcqAwAAwPkgYJ2nojoIGmO8UA38WYMqYZpyXwdVjyzjMJdyNFu3fbBIK3emeqEyAAAAnCsC1nniGVhwpbrR5TVlcAen30Ppx3M0YNxiLd2e4oXKAAAAcC4IWOeJDoJwtdhK5TTlvvaKjSrnMJeRmaOBHy7WH9uSvVAZAAAAiouAdZ7oIAh3qFkxP2TViy7vMHckK1d3fPSHFmw56IXKAAAAUBwErPPEFkG4S/XIspo8uL0aVAlzmDuWnas7P16iuRuTvFAZAAAAzoaAdZ7YIgh3qhJRRpMHt1eTauEOc5k5ebrn0wT9tv6AFyoDAADAmRCwzlNRXQQBV4kOC9Xn97ZXs+oRDnNZOXkaPCFBP6/Z54XKAAAAUBQC1nk4np2rw5k5hcYCA4yqOnmWEXAhosqHaNK97XVxzUiHuexcqwcmLtOPf+71QmUAAABwJsjbBfijMsGBWv9cDx3IOK49qce0K+WY0o5lKyiQvArXiywXrAn3tNOgj/7Qsh2Fn4eVk2f10KTlys7NU6+4Gl6qEAAAACcZa623azgn8fHxNiEhwdtlAB53ODNHd328RH8kOrZqDzDSyzddrJva1PRCZQAAAKWPMWaptTb+9HEuuQB+Iiw0SJ/cdYk61KvkMJdnpcenrtTkP3Z4oTIAAACcRMAC/Ei5kCB9NOgSdW4Y7TBnrTT0qz81YWGix+sCAABAPgIW4GfKhgTqg4Hx6tqkitP5Z6at0Yfzt3m4KgAAAEgELMAvlQkO1PsD2ujqZlWdzj/3/VqNmbPFw1UBAACAgAX4qZCgAI3u31rXXVTd6fyLP63X2zM3ebgqAACA0o2ABfix4MAAvdUvTr3iYpzOv/bLRr3+8wb5W7dQAAAAf0XAAvxcUGCAXr8lrsgW7aNmbdbLMwhZAAAAnuDWgGWM6W6M2WCM2WyMGepk/n5jzJ/GmBXGmPnGmGburAcoqQIDjF6+saVubVvL6fx7s7fo+R/WEbIAAADczG0ByxgTKGm0pB6Smkm61UmA+txae5G1Nk7Sy5Jed1c9QEkXEGD0n94XaWCH2k7nP5y/Tc9+u0Z5eYQsAAAAd3HnFay2kjZba7daa7MkTZbUq+ACa216gZflJfGTH3ABAgKMRvRsrrs71XU6/+nC7fr3N6sJWQAAAG4S5MZj15C0s8DrXZLanb7IGPN3SY9KCpHU1dmBjDGDJQ2WpNjYWJcXCpQkxhg9fV1TBQcG6H0nrdon/bFD2bl5+u+NLRUYYLxQIQAAQMnlzitYzn5yc/i1ubV2tLW2vqQnJT3t7EDW2rHW2nhrbXzlypVdXCZQ8hhj9GT3xnq4W0On81OX7tKjX6xQTm6ehysDAAAo2dwZsHZJKnjHfU1Je86wfrKk3m6sByhVjDF69KpGeuyqRk7np63Yo0emrFA2IQsAAMBl3BmwlkhqaIypa4wJkdRP0rcFFxhjCv56/TpJPBUVcLGHujXU0B5NnM79sGqvHvx8mbJyCFkAAACu4LaAZa3NkfSgpBmS1kn6wlq7xhgz0hjT88SyB40xa4wxK5R/H9Yd7qoHKM3uv7y+nrne+VMQZqzZr/s/W6rj2bkergoAAKDkMf72XJz4+HibkJDg7TIAvzRhYaKembbG6dxljSpr7O1tVCY40LNFAQAA+CFjzFJrbfzp42590DAA33J7hzp66YaLZJy0oJm7MUlP/m8VDyMGAAC4AAQsoJTp1zZWr9x0sZx1aJ+2Yo/ene3Y2h0AAADFQ8ACSqGb2tTUG33jnD4H65UZGzR99V4vVAUAAOD/CFhAKdUrrob+e2NLp3NDpqzU6t1pHq4IAADA/xGwgFLspjY1dd/l9RzGj2Xn6t5PE3Qg47gXqgIAAPBfBCyglHvimia6smkVh/G9acc1+FPatwMAAJwLAhZQygUGGL3Zr5WaVAt3mFuxM1VD6SwIAABQbAQsAAoLDdK4O+JVqXyIw9w3dBYEAAAoNgIWAElSzYrlNOb2NgoJdPxnIb+z4D4vVAUAAOBfCFgATomvE6UXbrjI6dyQKSu0Zg+dBQEAAM4k6EyTxphHzzRvrX3dteUA8Lab2tTUpgMZGjNna6HxY9m5und8gr55sKOqhJfxUnUAAAC+7WxXsMLP8gWgBCqqs+CetOO6bwKdBQEAAIpi/K07WHx8vE1ISPB2GUCJdzgzRze9t0Dr92U4zPWOi9EbfeNkjPFCZQAAAN5njFlqrY0/ffxsWwRHnWneWvvwhRYGwDeFhQbpg4Hx6j36dx06klVo7psVe9Swarj+fkUDL1UHAADgm84YsCQt9UgVAHxSrahyev/2Nur/wWJl5eYVmntlxgY1qBKma5pX81J1AAAAvoctggDOaurSXfrnlysdxsuFBOrL+zuoeUykF6oCAADwnqK2CBarTbsxprIx5lVjzI/GmFknv1xfJgBfdFObmrrvsnoO40ez8jsLHsg47oWqAAAAfE9xn4M1UdI6SXUljZCUKGmJm2oC4IOe6N5E3ZrQWRAAAOBMihuwKllrP5SUba2dY629S1J7N9YFwMcEBhi9dWsrNa7q+ISG5TtS9dRXf8rfthwDAAC4WnEDVvaJ/+41xlxnjGklqaabagLgo8JCgzTujnhVKh/iMPf18t16b84WL1QFAADgO4obsJ43xkRKekzSPyWNkzTEbVUB8FknOwsGBzo+A+vl6Rs0Y80+L1QFAADgG4oVsKy131tr06y1q621V1hr21hrv3V3cQB80yV1ovRCn4uczg2ZskJr96R7uCIAAADfUNwuguONMRUKvK5ojPnIfWUB8HU3x9cqsrPgPeOXKCkj0wtVAQAAeFdxtwi2tNamnnxhrU2R1Mo9JQHwF2fuLJhAZ0EAAFDqFDdgBRhjKp58YYyJkhTknpIA+IszdRZctiNV/6KzIAAAKGWKG7Bek7TAGPOcMWakpAWSXnZfWQD8xcnOglFOOgt+RWdBAABQyhS3ycWnkm6UtF9SkqQbrLUT3FkYAP9RK6qcxhTRWfCVGRv0M50FAQBAKVHcK1iSFCXpiLX2bUlJxpi6bqoJgB8qqrOgtdI/6CwIAABKieJ2EXxW0pOSnjoxFCzpM3cVBcA/3RxfS4OL6Cx476cJdBYEAAAlXnGvYPWR1FPSEUmy1u6R5HhXO4BS78kiOgvuTj1GZ0EAAFDiFTdgZdn8VmBWkowx5d1XEgB/Fhhg9Ga/ODoLAgCAUqm4AesLY8wYSRWMMfdK+lXSOPeVBcCfhZcJPmNnwffnbPVCVQAAAO5X3C6Cr0qaKul/khpLGmatHeXOwgD4tzN1Fnx5xno6CwIAgBKp2F0ErbW/WGsft9b+U9IsY0x/N9YFoAS4pE6U/kNnQQAAUIqcMWAZYyKMMU8ZY94xxlxt8j0oaaukWzxTIgB/dgudBQEAQClytitYE5S/JfBPSfdI+lnSzZJ6WWt7ubk2ACXEmToL3v/ZUmXm0FkQAACUDGcLWPWstYOstWMk3SopXtL11toV7i8NQElxsrNgo6phDnNLt6foKToLAgCAEuJsASv75B+stbmStllrM9xbEoCSKLxMsD684xLnnQWX0VkQAACUDGcLWBcbY9JPfGVIannyz8YY7k4HcE5qRZXT+wOK7iz4y9r9XqgKAADAdc4YsKy1gdbaiBNf4dbaoAJ/jvBUkQBKjrZ1i+4s+Mjk5Vq3l9/dAAAA/1XsNu0A4Cq3xNfSvZ3rOowfzcrVPeMTdPAwnQUBAIB/ImAB8IqhPZqqaxGdBe+bQGdBAADgnwhYALwiMMDoLToLAgCAEoaABcBrztZZcMxcOgsCAAD/QsAC4FVn6iz43+nr9eqMDcrJzfNCZQAAAOeOgAXA69rWjdJ/ejvvLPjOb5t16weLtDftmBcqAwAAODcELAA+4ZZLnHcWlKQliSnq8dY8zVzHc7IAAIBvI2AB8BlDezRV9+bVnM6lHs3W3eMT9Pz3a5WVw5ZBAADgmwhYAHxGYIDR6P6t9ehVjRTgeEuWJGnc/G26+f0F2pl81LPFAQAAFAMBC4BPCQwwerhbQ026t72qRoQ6XbNyV5quHTVPP/6518PVAQAAnBkBC4BPalevkn58uLO6NK7sdD7jeI4emLhMT3/zp45n81BiAADgGwhYAHxWpbBQfXTHJfrXtU0UVMSewc8W7VCfdxdoS9JhD1cHAADgiIAFwKcFBBgNvqy+vry/g2pUKOt0zbq96frb2/P11bJdHq4OAACgMAIWAL/QKraifny4c5FdBo9m5erRL1bqn1+u1NGsHA9XBwAAkI+ABcBvRJYL1nsDWmtkr+YKCXT+z9fUpbvU853ftX5fuoerAwAAIGAB8DPGGA3sUEdfPXCp6lQq53TN5gOH1eud3zXpjx2y1nq4QgAAUJq5NWAZY7obYzYYYzYbY4Y6mX/UGLPWGLPKGDPTGFPbnfUAKDla1IjU9w93Vq+4GKfzmTl5euqrP/Xw5BXKOJ7t4eoAAEBp5baAZYwJlDRaUg9JzSTdaoxpdtqy5ZLirbUtJU2V9LK76gFQ8oSFBunNvnF6+caWKhPs/J+z71bu0fVvz9efu9I8XB0AACiN3HkFq62kzdbardbaLEmTJfUquMBa+5u19uiJl4sk1XRjPQBKIGOMbrmklr59sJMaVglzumb7oaO64b3f9fHv29gyCAAA3MqdAauGpJ0FXu86MVaUuyX95GzCGDPYGJNgjElISkpyYYkASopGVcP17YOd1O+SWk7ns3OtRny3VoMnLFXq0SwPVwcAAEoLdwYsZ08FdfqrY2PMAEnxkl5xNm+tHWutjbfWxleuXNmFJQIoScqGBOqlG1vqrX5xKh8S6HTNL2v367pR87V0e4qHqwMAAKWBOwPWLkkFf5VcU9Ke0xcZY66U9G9JPa21mW6sB0Ap0Suuhr5/uLOax0Q4nd+deky3jFmo92ZvUV4eWwYBAIDruDNgLZHU0BhT1xgTIqmfpG8LLjDGtJI0Rvnh6oAbawFQytSNLq+vHrhUgy6t43Q+N8/qv9PXa9AnS3TwML/bAQAAruG2gGWtzZH0oKQZktZJ+sJau8YYM9IY0/PEslckhUn60hizwhjzbRGHA4BzFhoUqOE9m+v9Aa0VUSbI6Zq5G5N07VvztHDLIQ9XBwAASiLjbx214uPjbUJCgrfLAOBndiYf1UOTlmvFzlSn8wFGeqhrQz3craECA5zdQgoAAPAXY8xSa2386eNufdAwAPiKWlHl9OX9HXTfZfWczudZ6a2Zm9R/3CLtTz/u4eoAAEBJQcACUGoEBwboqWub6uM7L1FU+RCnaxZtTVaPt+Zp9gZuCwUAAOeOgAWg1LmicRX9+HBnta0b5XQ++UiWBn28RC/9tF7ZuXkerg4AAPgzAhaAUqlaZBl9fk87PdytoUwRt1y9P2eL+o5ZqF0pRz1bHAAA8FsELAClVlBggB69qpEm3t1OlcNDna5ZtiNV1741TzPW7PNwdQAAwB8RsACUepc2iNaPD3dW54bRTufTj+fovglLNfzbNcrMyfVwdQAAwJ8QsABAUuXwUI2/s62e6N64yDbtnyxIVM+3f9es9fvlb4+4AAAAnkHAAoATAgKMHujSQFMGt1dMZBmnazbsz9BdnyToljELtSQx2cMVAgAAX0fAAoDTxNeJ0o+PdNaVTasWuWZJYopufn+h7v5kidbtTfdgdQAAwJcRsADAiQrlQvTBwDYadn0zBQcW0WZQ0sz1B3TtqHkaMmWFdhyi2yAAAKUdAQsAimCM0V2d6uqr/+uouFoVilxnrfT18t3q9vpsPTtttZIyMj1YJQAA8CXG327Ujo+PtwkJCd4uA0ApY63VL2v365UZG7TpwOEzri0XEqi7O9XVvZfVU0SZYA9VCAAAPMkYs9RaG+8wTsACgOLLzbP6evluvfHLRu1OPXbGtRXKBevvXRro9g61VSY40EMVAgAATyBgAYALZebkauKiHRr922YdOpJ1xrXVI8vokW4NdVObmgoKZGc2AAAlAQELANzgcGaOPpy3TWPnbtGRrDM/hLhe5fL659WN1aNFNRlTdOMMAADg+whYAOBGhw5n6t3ZWzRh4XZl5eadcW3LmpF64pom6tQw2kPVAQAAVyNgAYAH7Eo5qrd+3aT/LdulvLP889qxQSU9cU0TXXyGDoUAAMA3EbAAwIM27c/Qqz9v0Iw1+8+6tkeLanrs6sZqUCXMA5UBAABXIGABgBcs35Gi/05fr0Vbk8+4LsBIN7eppUeubKiYCmU9VB0AADhfBCwA8BJrreZtOqiXZ6zX6t3pZ1wbEhSgge1r64ErGiiqfIiHKgQAAOeKgAUAXpaXZ/Xj6r167eeN2nbwyBnXhoUGafBl9XR3p7oqHxrkoQoBAEBxEbAAwEdk5+Zp6tJdevPXjdqfnnnGtdFhIXrwiga6tV2sQoN4WDEAAL6CgAUAPuZ4dq7GL0jUu7O3KO1Y9hnX1qxYVo9e1Ui94mooMIBnaAEA4G0ELADwUWnHsjV27hZ9ND9Rx7LP/LDixlXD9c9rGuvKplV4WDEAAF5EwAIAH3cg/bjenrVZk/7YoZyzPESrdWwFPdm9idrVq+Sh6gAAQEEELADwE9sPHdHrv2zUtBV7zrq2S+PK+ufVjdWiRqQHKgMAACcRsADAz6zZk6ZXZ2zQbxuSzrq2c8No/V+X+upQrxJbBwEA8AACFgD4qT+2Jevl6euVsD3lrGsvrlVB/3d5fV3drKoCaIYBAIDbELAAwI9ZazVr/QG9X9UZbAAAG5BJREFUPH2DNuzPOOv6epXL6/7L66t3XA2FBAV4oEIAAEoXAhYAlAD/396dR8dV3mkef35VKu2bZVte5H0Ldmww2DjGBPAYkk4CAwnLwQ4EQnAgJpxJJ51Jp6cnPek+pyf0me7ppBtiMHsSsjAOpE0myyQBjAFjbDDYBkOQvEqysWzta6mq3vmjrmxZKlmSXYuq9P2co1P3vu9bt35H555bevTeJRxx2vR2jf7l//1Z1Q0dg46fWJyrtZfN1Jpl03hgMQAAcUTAAoAMEgxF9Ms3q/XQ5iodONE+6PiSvIBuXzFDX1wxQ2UF2UmoEACAzEbAAoAMFI44/W7PUa3fXKk9Nc2Djs8N+LT64mlae9lMTRmTn4QKAQDITAQsAMhgzjm9XHlc61+s0qtVJwYdn+UzXbt4sr5yxWzNm1CUhAoBAMgsBCwAGCXePtyo9S9W6ffvHtVQDvFXzZ+gdStna8n0MYkvDgCADEHAAoBRpvJYqza8VKVnd9aoOzz4sX7ZzDKtWzlbK+eN51laAAAMgoAFAKPUkaYOPbplv376+iG1B8ODjj9vYpHWrZytqxdNUpafW7wDABALAQsARrnG9qB+tPWgHn9lvxrauwcdP7UsT3ddPls3LZmi3IA/CRUCAJA+CFgAAElSezCkp7cf1sNb9qumcfBnaY0rzNYdl87UrcunqyQvkIQKAQAY+QhYAIDTdIcjeu7tWj24uUp//rB10PGFOVm6Zfk03XnpTJUX5yahQgAARi4CFgAgpkjE6fn3jumHL1bqzUONg47P9vt0w5IpuvvyWZoxriAJFQIAMPIQsAAAZ+Sc0/YDDVr/YqVeeL9u0PE+kz69aJLWXTFbCytKklAhAAAjBwELADBk79Y266GXqvTc27WKDOFr4rK547Ru5WxdMmsst3gHAIwKBCwAwLAdOtGuh7fs09M7DqsrFBl0/AVTS7Xuiln65IKJ8vkIWgCAzEXAAgCctbqWLj3x6n79aOtBtXSGBh0/e3yB7l01R//5/Mk8SwsAkJEIWACAc9bS2a2fbjukR17er7qWrkHHzxibr3tWztHnLqpQgKAFAMggBCwAQNx0dof17M4aPbS5SgdOtA86vqI0T+tWztZNS6coJ4uHFgMA0h8BCwAQd+GI0+/2HNX6zZXaU9M86PiJxbm6+4pZWrNsmnIDBC0AQPoiYAEAEsY5p5crj+v+5yu1bX/9oOPHFeborstn6paPTVdBTlYSKgQAIL4IWACApHh9f73+/fkPtOWD44OOHZMf0J0fn6nbVsxQcW4gCdUBABAfBCwAQFLtPNSg+5+v1J/eOzbo2OLcLH3x0pn60qUzVJqfnYTqAAA4NwQsAEBK7Klp0v3PV+p37xwddGxhTpa+cMl0rf34TI0tzElCdQAAnB0CFgAgpd4/2qL7X6jUr3fVarCvnryAX7d8bJruunyWyotzk1MgAADDQMACAIwIVXWt+uELVfrVWzUKR878HZSd5dPqi6fqK1fM1uTSvCRVCADA4AhYAIAR5dCJdq3fXKmNb1SrO3zm76KA33Tjkim6Z+UcTS3LT1KFAAAMjIAFABiRaho79NDmKv18+2EFQ5EzjvX7TJ9dXKGv/qfZmjW+MEkVAgDQHwELADCiHWvu1EMv7dNT2w6qs/vMQctn0jXnT9a9q+Zo3oSiJFUIAMApAwUsX4I/9FNm9r6ZVZrZt2P0X25mb5pZyMxuTGQtAICRrbw4V9+5ZoFe/utVWrdytgqy/QOOjThp09u1+uS/vqR1P3lD79Q2JbFSAAAGlrAZLDPzS/qzpE9Iqpa0XdIa59y7vcbMkFQs6ZuSNjnnNg62XWawAGB0aGwP6rFXDujxV/arpTM06Pir5pfr3lVztXhqaRKqAwCMdqmYwVomqdI5t885F5T0c0nX9R7gnDvgnNsl6cznggAARp3S/Gx94xPz9Mq3V+mbn5ynMfmBM47/495j+uwDr+gLj27T9gP1SaoSAIDTJTJgVUg63Gu92msbNjO7y8x2mNmOurq6uBQHAEgPxbkB3btqrl7+61X6m0+fp3GF2Wccv+WD47rpwa1avWGrXq08rnS71hgAkN4SGbAsRttZfcs55zY455Y655aOHz/+HMsCAKSjgpws3X3FbG351ir93TULNKE454zjX9tXr88/sk03PrhVL7x/TJFBnrkFAEA8ZCVw29WSpvZanyKpNoGfBwAYBfKy/frSx2fqluXT9H92VGv9i1WqaewYcPwbBxt0x+PbVZiTpQWTi3V+RYkWTSnRoooSzRhbIJ8v1v8DAQA4O4kMWNslzTWzmZJqJK2W9PkEfh4AYBTJyfLr1uXTdfPFU/XsmzV64MVKHTzRPuD41q6QXt9fr9f3n7o+q6gndE0p0cKKEp0/pVTTy/IJXQCAs5bQ52CZ2WckfV+SX9Jjzrl/NLN/kLTDObfJzC6W9KykMZI6JR11zn30TNvkLoIAgFhC4Yie21Wr+5+vVFVd21lvpygnSwu9Wa6FFSU6v6JE08fmy4zQBQA4hQcNAwBGhXDE6bd7juj+5yv13tGWuGyzKDdLiyqipxX2nF44rYzQBQCjGQELADCqRCJOf9z7oX782kHtPNSo1q7Bn6U1HMW5Wb1muUq1qKJEU8vyCF0AMEoQsAAAo1Yk4nTgRJt21zRpd3WTdtc0aU9Nk9qC4bh+TkleQIsqeq7nis50TRlD6AKATETAAgCgl0jEaf+JtpOBa3d1k96pjX/oKs0PnDq90AtfhC4ASH8ELAAABhGOOO0/3uoFrmbtrmnUO7XNao9z6BqTH9BF08Zo7WWzdMnssXHdNgAgOQhYAACchXDEaV+dF7pOznQ1q6M7PqHrv/7FR3TPytnMaAFAmhkoYCXyOVgAAKQ9v880d0KR5k4o0vUXTZEUDV1Vda2nTi+siZ5e2NkdGfb2/9fv39fRpk5999qPys/ztwAg7RGwAAAYJr/PNG9CkeZNKNINS6KhKxSOqKqu50YajV7oalZXaPDQ9ePXDqqupUvfX71YuQF/ossHACQQpwgCAJAgoXBElXWt2lUdvWvhruom7T0ycOhaNqNMD9+2VCX5gSRXCgAYLq7BAgBgBOgOR3Tfb9/Toy/vj9k/t7xQT35pmSaX5iW5MgDAcAwUsHypKAYAgNEq4PfpO9cs0N9+Zn7M/g+Oter6H76q94+2JLkyAEA8ELAAAEiBL18+Sz9YvVgBf/8bWxxt7tSND76q1/adSEFlAIBzQcACACBFrltcoSfuWKbCnP73nGrpDOm2R1/Xb3YfSUFlAICzRcACACCFLp0zTr+4e7nGF+X06wuGI/rqT9/UE6/Evl4LADDyELAAAEixj04u0TPrVmjWuIJ+fc5J333uXd332/eUbjemAoDRiIAFAMAIMLUsXxvXrdCF00pj9j+4uUp/9fTb6g4P/2HGAIDkIWABADBClBVk66drl+vK88pj9j+zs0ZfemK7WrtCSa4MADBUBCwAAEaQvGy/HvrCEq2+eGrM/i0fHNeaDa+prqUryZUBAIaCgAUAwAiT5ffpe9cv0teunBuzf3dNk25Y/6r2H29LcmUAgMEQsAAAGIHMTF//xDx97/pF8vV/VJYO1bfrhvWv6q3DjckvDgAwIAIWAAAj2Jpl07ThC0uVG+j/lV3fFtSaDa/phfePpaAyAEAsBCwAAEa4qxZM0FNrl6s0P9Cvr6M7rLVP7tDTOw6noDIAQF8ELAAA0sCS6WO08SsrVFGa168vHHH61sZd+vc/fcCzsgAgxQhYAACkiTnlhXrmnhWaP6k4Zv+//OHP+s5/7FE4QsgCgFQhYAEAkEYmFOfq6buX69I5Y2P2/+S1Q7rnqTfU2R1OcmUAAImABQBA2inKDejxLy7TtRdMjtn/+3c+1K2PbFNjezDJlQEACFgAAKSh7Cyfvn/zYn35spkx+3ccbNCND25VTWNHkisDgNGNgAUAQJry+Ux/e/UC/fer58fsrzzWqut/+IreO9qc5MoAYPQiYAEAkObWXjZL/7bmQgX8/Z9I/GFzl25av1Vbq06koDIAGH0IWAAAZIBrL5isJ+9YpsKcrH59LV0h3f7Y6/q/u46koDIAGF0IWAAAZIgVc8bp6bsvUXlRTr++YDiie3/2ph5/ZX8KKgOA0YOABQBABlkwuVi/XLdCs8YX9OtzTvr7597V9367VxGelQUACUHAAgAgw0wty9cvv7JCF00rjdn/0OZ9+sbTbykYiiS5MgDIfAQsAAAy0JiCbD21drmumj8hZv+v3qrVnU9uV2tXKMmVAUBmI2ABAJCh8rL9evDWi7Rm2bSY/Vs+OK7VG7bqWEtnkisDgMxFwAIAIINl+X36n59bqK9fNS9m/56aZt2w/lXtq2tNcmUAkJkIWAAAZDgz09eumqv7rl8kX/9HZelwfYdufHCrdh5qSH5xAJBhCFgAAIwSq5dN08O3LVVuoP/Xf31bUJ9/eJse3Fyl7Qfq1ca1WQBwVsy59LpN69KlS92OHTtSXQYAAGnrzUMNuvOJ7Wpo7x5wjJk0e3yhzq8o0cKKEp0/pUQLJhcrP7v/g4wBYDQyszecc0v7tROwAAAYfarqWnX7Y6+ruqFjyO/xmTSnvFALK0q0qCd0TSpRXrY/gZUCwMhEwAIAAKc51typLz6+Xe8eaT7rbfhMmltedHKWa2FFiRZMKiZ0Ach4BCwAANBPS2e3vv6Lt/XHvR/GbZt+n2muN9PVO3TlBghdADIHAQsAAAzo7cON2rrvhHZXN2l3TZMO1bfHdfs9oWtRr9A1n9AFII0RsAAAwJA1tge1p6ZZu2oatacmGroO1w/9eq2hyPKZ5k4o0qKKYi2aUqpFFSU6b2IRoQtAWiBgAQCAc9LQFtSe2ibtqm7Snproa01j/EPXvAlFWlRRokXenQunjMnTuIIc+WI9xAsAUoSABQAA4q6+LajdNdHA1XN6YbxDlyRl+32aWJKrSSW5qijN06TSXE0qyTu5PLk0T8W5gbh/LgAMZKCAxcMsAADAWSsryNYV88brinnjT7adaO06Gbp6ZrtqmzrP6XOC4YgO1bef8dqwwpwsTSqJhq3JXgCbXJqnyV7bxJJcTj8EkHDMYAEAgIQ77oWunlmuPTVNOnKOoetsjC3I1uTSvNhBrDRX5UW58nMqIoAh4BRBAAAwotS1dJ2c5dpd06TdNY36sLkrpTX5faaJxdFTESd5oWuyF8B6QtmY/IDMCGHAaEfAAgAAI96x5k4vbEVnuQ7Vt6u2sVOtXaFUl3aSz6Qsv08Bn0Vf/aYsn09ZflPA71OWz+T3ect+U8DrO/WeU8t+n/d+bxuB08b1eX9PW69t97wnJ8uvghy/inKzVJCTpcKcLBVkZ3FjECCBuAYLAACMeOXFubqyOFdXzp9wWntzZ7eONHaqtrFDtU0dqm3siK43dai2sVNHmzoVDEeSUmPEScFQREFJUjgpn3m2CrL90cCVGw1dhTnRAFbkvfZtL8zxqzAnQFgDzgEBCwAAjHjFuQEVTwzoIxOLYvZHIk7H27p0pLFTR5o6VNPYqSMnw1i07VhLl9LsxJ1z1hYMqy0Y1rGWcz/1cqhhrSDbr8LcgHIDPplMZpJJ0Vczbzn66vNJpuiAnnafqc/7+i9Hs96pdt/JMdFX9W7r1e6c1B2OKBRxCoUj6g47hSIRhcLuVLvXFwo7dfft894TjpzqC4Uj6j7tPb3HRd/X3aev97YDflNZQbZK87NVlp+t0oKAyvKzNaYgW2Pys1VWEDjZV5IXIOimAQIWAABIez6fqbwoepOKC6aWxhwTDEX0YXOnjjTFmgmLtjV1dCe58vQRz7CG01XVtQ1pnM+kkrzAyfDVE8DGnAxkAa/NC2wF0VA2Em/cEgpH1NEdVkd3WJ3B6HKntx5t8167e/UFw7pyfrkunDYm1eWfEQELAACMCtlZPk0ty9fUsvwBx7R1hXSk16xXz0xY71DW2Z2cUxGBviJOamjvVkN7t6ShhTLzQllZfrZK8wOnha9oSAucNls2Jj9bgSxfv4DTEewVgIJhdYb6tkXUGToVjE6Ni5zW1rMcipzddPK4wmwCFgAAQLooyMnSnPIizSmPfSqicz2nkMU+Rezk6WODnIbWHY4o3Gc7A73nZNsAp6GFIhF1dofV1hVWa1co+tMZUkf3yL4+DMnhnNTY3q3G9syYne1Ig39wELAAAACGyMwU8JsCfilPI/uhxaFwJHpaX0/o6gpFlztPX285rT2s1q7u08JaW1dI7UHCGkaGzjT4xwEBCwAAIANl+X0qyfOpJC9wztsKR5zagtEQ1j+U9QpvXkBr7QwpGI7IuegMipOTc9FT3OQtO0VnBCO9liUp4ly/95223Od9ck6u3/uiY/q+z7zfS6DXbe9P3ma/zy30+96C/+T4WLfQ926fH+sW+qduyX/6Nnv6OrvDamgPqqGt23sNqr49qMb2btW3BdXY7q23datlBD2uIFVGfcAys09J+oEkv6RHnHP39enPkfQjSUsknZB0s3PuQCJrAgAAwPD4fRa9k2PuuYc1nL1gKKLG9qAa+oSvhrZoW/Q1qPpeyy2dIzOU+UzKC/iVl+1XbsCvvECv12y/8gK+k2253ri8gF9Lpo/s66+kBAYsM/NLekDSJyRVS9puZpucc+/2GnanpAbn3BwzWy3pnyTdnKiaAAAAgHSVneVTeXGuyotzh/ye7nBEje2nZsca2oOqb+u93u21eYGtLaiIkxdqoiEnL+BXjvfaOxTlBnwx2nrWff2CU+8wFfCbzEbe3Q3jIZEzWMskVTrn9kmSmf1c0nWSeges6yR911veKOl+MzPnRttTKgAAAID4C/h9Gl+Uo/FFOakuZdTwJXDbFZIO91qv9tpijnHOhSQ1SRrbd0NmdpeZ7TCzHXV1dQkqFwAAAADOTSIDVqw5v74zU0MZI+fcBufcUufc0vHjx8elOAAAAACIt0QGrGpJU3utT5FUO9AYM8uSVCKpPoE1AQAAAEDCJDJgbZc018xmmlm2pNWSNvUZs0nS7d7yjZKe5/orAAAAAOkqYTe5cM6FzOxeSb9X9Dbtjznn3jGzf5C0wzm3SdKjkn5sZpWKzlytTlQ9AAAAAJBoCX0OlnPuN5J+06ft73otd0q6KZE1AAAAAECyJPIUQQAAAAAYVQhYAAAAABAnBCwAAAAAiBMCFgAAAADECQELAAAAAOKEgAUAAAAAcULAAgAAAIA4MedcqmsYFjOrk3QwyR87TtLxJH8mMhf7E+KJ/QnxxP6EeGJ/QjyNxP1punNufN/GtAtYqWBmO5xzS1NdBzID+xPiif0J8cT+hHhif0I8pdP+xCmCAAAAABAnBCwAAAAAiBMC1tBsSHUByCjsT4gn9ifEE/sT4on9CfGUNvsT12ABAAAAQJwwgwUAAAAAcULAAgAAAIA4IWANwsw+ZWbvm1mlmX071fUgvZjZVDN7wcz2mtk7ZvY1r73MzP5gZh94r2NSXSvSg5n5zWynmf3aW59pZtu8fekXZpad6hqRPsys1Mw2mtl73nHqEo5POBtm9nXve26Pmf3MzHI5PmE4zOwxMztmZnt6tcU8HlnUv3l/n+8ys4tSV3l/BKwzMDO/pAckfVrSAklrzGxBaqtCmglJ+ivn3HxJyyV91duHvi3pT865uZL+5K0DQ/E1SXt7rf+TpH/19qUGSXempCqkqx9I+p1z7jxJFyi6b3F8wrCYWYWk/yJpqXNuoSS/pNXi+ITheULSp/q0DXQ8+rSkud7PXZLWJ6nGISFgndkySZXOuX3OuaCkn0u6LsU1IY0454445970llsU/eOlQtH96Elv2JOSPpuaCpFOzGyKpKslPeKtm6RVkjZ6Q9iXMGRmVizpckmPSpJzLuicaxTHJ5ydLEl5ZpYlKV/SEXF8wjA4516SVN+neaDj0XWSfuSiXpNUamaTklPp4AhYZ1Yh6XCv9WqvDRg2M5sh6UJJ2yRNcM4dkaIhTFJ56ipDGvm+pG9JinjrYyU1OudC3jrHKAzHLEl1kh73Tjt9xMwKxPEJw+Scq5H0z5IOKRqsmiS9IY5POHcDHY9G9N/oBKwzsxht3Ncew2ZmhZJ+KekvnXPNqa4H6cfMrpF0zDn3Ru/mGEM5RmGosiRdJGm9c+5CSW3idECcBe+6mOskzZQ0WVKBoqdw9cXxCfEyor//CFhnVi1paq/1KZJqU1QL0pSZBRQNV085557xmj/smcr2Xo+lqj6kjUslXWtmBxQ9XXmVojNapd4pORLHKAxPtaRq59w2b32jooGL4xOG6ypJ+51zdc65bknPSFohjk84dwMdj0b03+gErDPbLmmudxecbEUv2NyU4pqQRrxrZB6VtNc59797dW2SdLu3fLuk/0h2bUgvzrm/cc5Ncc7NUPRY9Lxz7hZJL0i60RvGvoQhc84dlXTYzD7iNV0p6V1xfMLwHZK03Mzyve+9nn2J4xPO1UDHo02SbvPuJrhcUlPPqYQjgTk3YmbTRiQz+4yi/yX2S3rMOfePKS4JacTMPi5pi6TdOnXdzH9T9DqspyVNU/SL6SbnXN8LO4GYzGylpG86564xs1mKzmiVSdop6VbnXFcq60P6MLPFit40JVvSPkl3KPrPV45PGBYz+3tJNyt699ydktYqek0MxycMiZn9TNJKSeMkfSjpf0j6lWIcj7wgf7+idx1sl3SHc25HKuqOhYAFAAAAAHHCKYIAAAAAECcELAAAAACIEwIWAAAAAMQJAQsAAAAA4oSABQAAAABxQsACAAAAgDghYAEAAABAnBCwAABpx8zGmtlb3s9RM6vptZ4dh+3fbWbOzOb3attrZjPOddsAgMyWleoCAAAYLufcCUmLJcnMviup1Tn3z3H8iPMlvSXpakl7zSxH0gRJB+P4GQCADMQMFgAg45jZN8xsj/fzl17bDDN7z8yeNLNdZrbRzPIH2MQiSfcpGrAk6aOS9jrnXBLKBwCkMQIWACCjmNkSSXdI+pik5ZK+bGYXet0fkbTBOXe+pGZJ9wywmQWSNkkqN7MSRQPX7oQWDgDICAQsAECm+bikZ51zbc65VknPSLrM6zvsnHvFW/6JN/Y0ZjZV0gnnXIekP0j6C0VPGdyV8MoBAGmPgAUAyDR2hr6+p/g5M/tqrxtkTFY0TPXMVv1G0dMEmcECAAwJAQsAkGlekvRZM8s3swJJn5O0xeubZmaXeMtrJL3snHvAObfY+6nV6WFqs6KzX71DFwAAAyJgAQAyinPuTUlPSHpd0jZJjzjndnrdeyXdbma7JJVJWh9jEycDlnOuy1sOOucaE1w6ACADGDdEAgCMBt4zrH7tnFuY4lIAABmMGSwAAAAAiBNmsAAAAAAgTpjBAgAAAIA4IWABAAAAQJwQsAAAAAAgTghYAAAAABAnBCwAAAAAiBMCFgAAAADECQELAAAAAOLk/wPP9L/IqqDUXgAAAABJRU5ErkJggg==\n", + "text/plain": [ + "<Figure size 864x432 with 1 Axes>" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "plt.figure(figsize=(12, 6))\n", + "ax = sns.lineplot(x='N', y='Rec', hue='experiment', legend='brief', data=result_df)\n", + "plt.title('Top-N Recall')\n", + "for l in ax.lines:\n", + " plt.setp(l, linewidth=5)\n", + "plt.ylabel('Recall')\n", + "plt.xlabel(r'Top-$N$')\n", + "plt.legend(prop={'size': 20})\n", + "plt.tight_layout()\n", + "\n", + "fig_out = os.path.join(experiment_out_dir, 'topN_recall.png')\n", + "plt.savefig(fig_out, dpi=300)" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "<Figure size 864x432 with 1 Axes>" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "plt.figure(figsize=(12, 6))\n", + "ax = sns.lineplot(x='N', y='F1', hue='experiment', legend='brief', data=result_df)\n", + "plt.title('Top-N F1')\n", + "for l in ax.lines:\n", + " plt.setp(l, linewidth=5)\n", + "plt.ylabel(r'$F_{1}\\;score$')\n", + "plt.xlabel(r'Top-$N$')\n", + "plt.legend(prop={'size': 20})\n", + "plt.tight_layout()\n", + "\n", + "fig_out = os.path.join(experiment_out_dir, 'topN_f1.png')\n", + "plt.savefig(fig_out, dpi=300)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/Synthetic data creation scripts/vimms_data_generation/ee.py b/Synthetic data creation scripts/vimms_data_generation/ee.py new file mode 100644 index 00000000..6c4b2eb0 --- /dev/null +++ b/Synthetic data creation scripts/vimms_data_generation/ee.py @@ -0,0 +1,4 @@ +from collections import defaultdict +lines = open('./keep.txt', 'r') + + diff --git a/Synthetic data creation scripts/vimms_data_generation/intermediate b/Synthetic data creation scripts/vimms_data_generation/intermediate new file mode 100644 index 00000000..df0ceb80 --- /dev/null +++ b/Synthetic data creation scripts/vimms_data_generation/intermediate @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +import pickle +import numpy as np +import matplotlib.pyplot as plt +# what do we need to do? +# 1. get all of the rt's first + +PRECISION = 10**4 + +def roughly_equal( a, b, tol=0.001 ): + return abs( a - b ) <= tol + +class FloatSet: + + def __init__( self ): + self.values = [] + self.length = 0 + self.idx = 0 + + def add( self, nval ): + self.values.append( nval ) + + def __iter__( self ): + self.values.sort( ) + temp = [ self.values[0] ] + for vv in self.values[1:]: + if not roughly_equal( vv, temp[-1] ): + temp.append( vv ) + self.values = temp + self.length = len( temp ) + return self + + def __next__( self ): + if self.idx < self.length: + result = self.values[ self.idx ] + self.idx += 1 + return result + else: + raise StopIteration + +class RtMapper: + + def __init__( self, fset ): + self.rt_to_idx_ = list( ) + self.idx_to_rt_ = dict( ) + + self.rts_ = list( fset ) + + for idx, rt in enumerate( self.rts_ ): + self.rt_to_idx_.append((rt, idx)) + self.idx_to_rt_[ idx ] = rt + + self.length_ = len( self.rts_ ) + + def rt_to_idx(self, nrt ): + assert isinstance(nrt, float) + + for (rt, idx) in self.rt_to_idx_: + if roughly_equal(rt, nrt): + return idx + else: + raise TypeError('no match') + + def rts( self ): + return self.rts_ + + def idx_to_rt( self, idx ): + return self.idx_to_rt_[ idx ] + + def length( self ): + return self.length_ + +def zero_pad( data, rtmapper ): + new_res = dict( ) + for chem, vals in data.items(): + # first, get the unique mzs + mzset = FloatSet( ) + for (rt, mz, it) in vals: + mzset.add( mz ) + + for mzval in mzset: + values = list(filter(lambda row: roughly_equal( row[1], mzval ), vals )) + new_rts, new_mzs, new_its = np.array(rtmapper.rts()), np.zeros(rtmapper.length()), np.zeros(rtmapper.length()) + for (rt, mz, it) in vals: + idx = rtmapper.rt_to_idx( rt ) + new_mzs[ idx ] = mz + new_its[ idx ] = it + new_res[(chem, mzval)] = (new_rts, new_mzs, new_its ) + return new_res + +def rt_analysis( data ): + fset = FloatSet( ) + for values in data.values(): + for (rt, mz, it) in values: + fset.add( rt ) + + return RtMapper( fset ) + +def main( ): + data = pickle.load(open('peak_recorder.pickle', 'rb')) + rtmapper = rt_analysis( data ) + padded = zero_pad( data, rtmapper ) + print( padded.keys() ) + +if __name__ == '__main__': + main( ) diff --git a/Synthetic data creation scripts/vimms_data_generation/make.py b/Synthetic data creation scripts/vimms_data_generation/make.py new file mode 100644 index 00000000..61df6370 --- /dev/null +++ b/Synthetic data creation scripts/vimms_data_generation/make.py @@ -0,0 +1,762 @@ +#!/usr/bin/env python3 +# coding: utf-8 import json import sys, os import shutil import pandas as pd +import json +import pickle +import numpy as np +import pandas as pd +from glob import glob +from pathlib import Path +from itertools import product +from vimms.Chemicals import ChemicalCreator +from collections import namedtuple, defaultdict +from vimms.MassSpec import IndependentMassSpectrometer, Scan +from vimms.Controller import SimpleMs1Controller +from vimms.Common import * +from vimms.MzmlWriter import MzmlWriter +import vimms +from scipy.interpolate import interp1d +import argparse + + +# TODO need to document what each of the replicates does and doesn't have +# TODO need to set it up so that we're using the same multipliers for the +# significant compounds +from copy import deepcopy +import matplotlib.pyplot as plt +DATA = json.load(open("multipliers.json", "r")) +GOOD_MULTS = DATA["good"]*10 +BAD_MULTS = DATA["bad"]*10 + +def cv(vals): + return np.std(vals) / np.mean(vals) + +def random_bool( ): + return bool( np.random.randint(0, 2) ) + +def get_max_mz( chem ): + return max( [pr[0] for pr in chem.isotopes] ) + +class Params: + rt_lower = 0 + rt_upper = 2000 + mz_lower = 0 + mz_upper = 2005 + pct_missing = [0, 10, 20] + noise_level = [0, 0.01, 0.1] + + +def safe_mkdir(dirname): + if not os.path.exists(dirname): + os.mkdir(dirname) + + +def remove_x_pct(df: pd.DataFrame, x: int): + """Function that randomly removes x pct of entries from the supplied list of vals. Makes a copy of the inputted vals""" + assert x >= 0 and x <= 100, f"x must be a percentage on the range 0 < x < 100" + # print(float(chems.size)*(1.-float(x)/100.)) + num_cols, num_rows = len(df.columns), len(df[df.columns[0]]) + n_chems = num_cols * num_rows + n_remove = int(n_chems * x / 100.0 + 0.5) + rm_per_col = np.zeros(num_cols, np.int32) + for idx in range(n_remove): + rm_per_col[idx % num_cols] += 1 + + np.random.shuffle(rm_per_col) + row_idxs = np.arange(0, num_rows, 1) + + result = pd.DataFrame() + for c_idx, col in enumerate(df.columns): + + new_col = list(map(lambda x: deepcopy(x), df[col])) + row_idxs = np.sort(row_idxs) + np.random.shuffle(row_idxs) + + col_remove = rm_per_col[c_idx] + + for cr_idx in range(col_remove): + new_col[row_idxs[cr_idx]] = None + + result[col] = new_col + + return result + + + + +def setup_dirs(params: Params, basedir="mvapack-data"): + # layout of the directories are: + # basedir / pct_missing / noise / group + # maybe should return the directories too? + for edir in glob(f"{basedir}/*"): + shutil.rmtree(edir) + + for pct in params.pct_missing: + for noise in params.noise_level: + dirname = "/".join([basedir, f"{pct}_missing", f"{noise:.2f}_noise"]) + if not os.path.isdir(dirname): + Path(dirname).mkdir(parents=True, exist_ok=True) + + +def load_dbs(): + base_dir = os.path.abspath("example_data") + ps = load_obj(Path(base_dir, "peak_sampler_mz_rt_int_19_beers_fullscan.p")) + hmdb = load_obj(Path(base_dir, "hmdb_compounds.p")) + out_dir = Path(base_dir, "results", "MS1_single") + # the list of ROI sources created in the previous notebook '01. Download Data.ipynb' + ROI_Sources = [str(Path(base_dir, "DsDA", "DsDA_Beer", "beer_t10_simulator_files"))] + + return (ROI_Sources, ps, hmdb) + + +def make_dataset(ROI_Sources, ps, hmdb, n_chems): + params = Params() + mz_range = [(params.mz_lower, params.mz_upper)] + rt_range = [(params.rt_lower, params.rt_upper)] + min_ms1_intensity = 1.75e5 # TODO make this into a global + + # m/z and RT range of chemicals + + # the number of chemicals in the sample + # n_chems = 2000 + + # maximum MS level (we do not generate fragmentation peaks when this value is 1) + ms_level = 1 + + chems = ChemicalCreator(ps, ROI_Sources, hmdb) + reps = chems.sample(mz_range, rt_range, min_ms1_intensity, int(n_chems), ms_level) + # out_dir = 'mvapack-data' + # out_dir = 'demo' + # save_obj(dataset, Path(out_dir, 'dataset.p')) + + + bad = set(list(open('bad_chems', 'r').read().splitlines())) + reps = list(filter(lambda s: str(s.formula) not in bad and get_max_mz( s ) < 1800, reps )) + for r in reps: + if r.rt <= params.rt_lower: + r.rt = params.rt_lower + 20 + elif r.rt >= params.rt_upper: + r.rt = params.rt_upper - 20 + return reps[0:n_chems] + + +def merge_chems(cdict, chems): + + for cd_key, chem in zip(cdict.keys(), chems): + cdict[cd_key].append(chem) + + +def get_fc(is_sig): + + if is_sig: + return np.random.random() * 2 + 2 + else: + return np.random.random() * 1 + + +def get_mults(is_sig): + if is_sig: + return GOOD_MULTS.pop(), GOOD_MULTS.pop() + else: + return BAD_MULTS.pop(), BAD_MULTS.pop() + + +def get_t_offset(is_sig): + if is_sig: + return (np.random.random() * 10) - 5 + else: + return (np.random.random() * 60) - 30 + + +def get_t_base(is_sig): + if is_sig: + return (np.random.random() * 30) - 15 + else: + return (np.random.random() * 60) - 30 + + +def get_chemical_lists(chems, NUM_REPS=10): + + SIGNIFICANT = int(len(chems) * 0.40) + keys = list(map(lambda idx: f"rep_{idx}", range(NUM_REPS))) + values = list(map(lambda idx: deepcopy([]), range(NUM_REPS))) + clean_dict = lambda: deepcopy(dict(zip(keys, values))) + group1, group2 = clean_dict(), clean_dict() + np.random.shuffle(chems) + g1_sig, g1_isig, g2_sig, g2_isig = None, None, None, None + + for idx, chem in enumerate(chems): + # first figure out the fold change.... between 2 and 6? + is_sig = idx < SIGNIFICANT + if idx == (SIGNIFICANT - 1): + g1_sig = pd.DataFrame(group1) + g2_sig = pd.DataFrame(group2) + group1, group2 = clean_dict(), clean_dict() + + fc = get_fc(is_sig) + greater, lesser = ( + list(map(lambda idx: deepcopy(chem), range(NUM_REPS))), + list(map(lambda idx: deepcopy(chem), range(NUM_REPS))), + ) + mults1, mults2 = get_mults(is_sig) + + greater_base, lesser_base = get_t_base(is_sig), get_t_base(is_sig) + + for ii, r_key in enumerate(keys): + greater[ii].max_intensity *= mults1[ii] + greater[ii].max_intensity *= fc + greater[ii].rt += greater_base + get_t_offset(is_sig) + lesser[ii].max_intensity *= mults2[ii] + lesser[ii].rt += lesser_base + get_t_offset(is_sig) + # for some reason we can actually get negative peaks... that's a problem + greater[ii].rt = max(greater[ii].rt, 10) + lesser[ii].rt = max(lesser[ii].rt, 10) + + g1_greater = bool(np.random.randint(0, 2)) + + merge_chems(group1 if g1_greater else group2, greater) + merge_chems(group2 if g1_greater else group1, lesser) + + return g1_sig, pd.DataFrame(group1), g2_sig, pd.DataFrame(group2), keys + + +def write_summary(g1, g2, dirname, peak_values): + + unique_formulas = set() + summary_info = dict() + for idx, c in enumerate(g1.columns): + summary_info[f"G0-R{idx}"] = deepcopy(g1[c]) + + for idx, c in enumerate(g2.columns): + summary_info[f"G1-R{idx}"] = deepcopy(g2[c]) + + for k, v in summary_info.items(): + pvs = peak_values[k] + for chem in v: + print(str(chem.formula) in pvs) + break + print(peak_values.keys()) + + for clist in summary_info.values(): + _ = list( + map( + lambda c: unique_formulas.add(str(c.formula)), + list(filter(lambda chem: chem is not None, clist)), + ) + ) + summary = dict(chemical=deepcopy(list(unique_formulas))) + + for rname, rchems in summary_info.items(): + rchem_list = [] + rep_mapper = dict() + for rc in rchems: + if rc is None: + continue + rep_mapper[str(rc.formula)] = deepcopy(rc) + + for uf in unique_formulas: + rchem_list.append(rep_mapper.get(uf, None)) + + summary[rname] = rchem_list + + summary = pd.DataFrame(summary) + summary.set_index("chemical", inplace=True) + summary = summary.transpose() + mapper = dict() + for col in summary.columns: + sum_col = summary[col] + for c in sum_col: + print(c) + # print( sum_col[0] ) + # print( sum_col[0].__dict__ ) + exit(0) + times = np.array( + list(map(lambda chem: chem.rt if chem is not None else None, sum_col)) + ) + mzs = np.array( + list( + map( + lambda chem: chem.isotopes[0][0] if chem is not None else None, + sum_col, + ) + ) + ) + times = times[times != None] + mzs = mzs[mzs != None] + avg_mz, avg_rt = np.mean(mzs), np.mean(times) + mapper[col] = f"{avg_rt:.2f}_{avg_mz:.4f}" + + summary.rename(mapper=mapper, inplace=True, axis=1) + for col in summary.columns: + summary[col] = summary.apply( + lambda row: row[col].max_intensity if row[col] else 0, axis=1 + ) + + summary.to_csv(f"{dirname}/summary.csv") + summary.to_pickle(f"{dirname}/summary.pickle") + print(f"{dirname}/summary.csv") + + +def increase_resolution(controller): + new_scans = [] + old_len = len(controller.scans[1]) + + for idx, scan in enumerate(controller.scans[1]): + # loop through each scan + new_mz, new_it = [], [] + interp_mz = [] + num_points = len(scan.mzs) + + # need to check that there are points + if num_points: + new_mz.append(scan.mzs[0] - 0.1) + new_it.append(0) + + for pt_idx, (m, i) in enumerate(zip(scan.mzs, scan.intensities)): + interp_mz.append(np.linspace(m - 0.021, m + 0.021, 10)) + + new_mz.extend([m - 0.02, m, m + 0.02]) + new_it.extend([0, i, 0]) + + if pt_idx > 0 and pt_idx < num_points - 1: + interp_mz.append(np.arange(m + 0.25, scan.mzs[pt_idx + 1] - 0.25, 0.2)) + + if num_points: + new_mz.append(scan.mzs[-1] + 0.1) + new_it.append(0) + + if not len(scan.mzs): + new_scans.append(deepcopy( scan )) + continue + + interp_mz = np.concatenate(interp_mz) + interp_mz = sorted(interp_mz) + + interp_func = interp1d(new_mz, new_it, kind="linear") + new_x = np.linspace(np.min(new_mz), np.max(new_mz), 1000) + interp_it = interp_func(interp_mz) + interp_it[interp_it < 0] = 0 + + new_scan = deepcopy(scan) + new_scan.mzs = interp_mz + new_scan.intensities = interp_it + new_scans.append(new_scan) + + controller.scans[1] = new_scans + + assert len(controller.scans[1]) == old_len + + return controller + + +def valid_mzs(mzs): + for idx in range(1, len(mzs)): + assert mzs[idx] >= mzs[idx - 1], f"{mzs[idx-1]}, {mzs[idx]}" + + +def add_noise(controller, noise): + if noise == 0: + return controller + + def max_it( scan ): + if len(scan.intensities): + return np.max( scan.intensities ) + else: + return 0 + + old_len = len(controller.scans[1]) + + max_it = np.max( + max(controller.scans[1], key=max_it ).intensities + ) + max_noise = float(max_it) * (float(noise) / 100.0) + new_scans = [] + + for idx, scan in enumerate(controller.scans[1]): + valid_mzs(scan.mzs) + scan_noise = list( + map(lambda it: np.random.rand() * max_noise, range(len(scan.mzs))) + ) + new_its = [] + for ct, nz in zip(scan.intensities, scan_noise): + if ct > 0: + new_its.append(ct) + else: + new_its.append(nz) + + new_scan = deepcopy(scan) + assert len(scan.mzs) == len(scan.intensities) + new_scan.intensities = new_its + new_scans.append(new_scan) + + controller.scans[1] = new_scans + assert len(controller.scans[1]) == old_len + + return controller + + +def make_chem_dict( + group1_sig_orig, group1_isig_orig, group2_sig_orig, group2_isig_orig, rep_keys +): + # shuffle up and deal! + group1_sig_orig = group1_sig_orig.sample(frac=1).reset_index(drop=True) + group2_sig_orig = group2_sig_orig.sample(frac=1).reset_index(drop=True) + group1_isig_orig = group1_isig_orig.sample(frac=1).reset_index(drop=True) + group2_isig_orig = group2_isig_orig.sample(frac=1).reset_index(drop=True) + + chem_dict = dict() + for g_idx, (grp_sig, grp_isig) in enumerate( + [(group1_sig_orig, group1_isig_orig), (group2_sig_orig, group2_isig_orig)] + ): + for idx, r_key in enumerate(rep_keys): + # what do we care about now? need to locate + # the part where noise is actually added + chems_sig, chems_isig = ( + deepcopy(grp_sig[r_key]).to_list(), + deepcopy(grp_isig[r_key]).to_list(), + ) + chems_sig.extend(chems_isig) + chem_dict[f"G{g_idx}-R{idx}"] = deepcopy(chems_sig) + return chem_dict + + +def num_zeros(df): + ct = 0 + + for c in df.columns: + ct += sum(list(map(lambda ch: ch.max_intensity == 0, df[c]))) + + return ct + + +def adjust_groups(group1_sig, group2_sig, peak_values): + # print( peak_values.keys() ) + # print( group1_sig ) + + def col_to_key(colname): + return str(int(colname.split("_")[-1]) + 1) + + for g1_col in group1_sig.columns: + key_name = f"G0-R{col_to_key(g1_col)}" + c_dict = peak_values[key_name] + print(c_dict) + exit(0) + # print( [t.formula for t in group1_sig[ g1_col ]] ) + for t_chem in group1_sig[g1_col]: + print(t_chem.formula in c_dict) + # if t_chem.formula in c_dict: + # print( t_chem.formula ) + # print( c_dict[t_chem.formula] ) + exit(0) + print(key_name) + assert key_name in peak_values + # need to convert the g1_col to the repname + print(g1_col) + + for g2_col in group2_sig.columns: + key_name = f"G1-R{col_to_key(g2_col)}" + print(key_name) + assert key_name in peak_values # need to convert the g1_col to the repname + print(g1_col) + + exit(0) + +def make_master_replicates( chem_dict, POSITIVE, ps, params, sig ): + replicates = dict() + for rep_name, chems in chem_dict.items(): + print( rep_name ) + filt_chems = list(filter(lambda c: c is not None, chems)) + filt_chems = sorted( filt_chems, key=lambda c: str(c.formula)) + + mass_spec = IndependentMassSpectrometer( + POSITIVE, filt_chems, ps, sig, None, True + ) + controller = SimpleMs1Controller(mass_spec, params.mz_upper) + controller.run(params.rt_lower, params.rt_upper, False) + replicates[ rep_name ] = controller + + return replicates + + +def validate_cd( chem_dict ): + counter = defaultdict( int ) + num_reps = len(chem_dict.keys()) + for rep_chems in chem_dict.values(): + for c in rep_chems: + counter[ str(c.formula) ] += 1 + for cname, v in counter.items(): + assert v == num_reps, cname + +def validate_reps( replicates ): + print( replicates.keys() ) + counter = defaultdict( int ) + num_reps = len(replicates.keys()) + + for rep_chems in replicates.values(): + for c in rep_chems.mass_spec.chemicals: + counter[ str(c.formula) ] += 1 + for cname, v in counter.items(): + assert v == num_reps, cname + +def roughly_equal( a, b, tol=0.001 ): + return abs( a - b ) <= tol + +class RtMapper: + + def __init__( self, fset ): + self.rt_to_idx_ = list( ) + self.idx_to_rt_ = dict( ) + + self.rts_ = list( fset ) + + for idx, rt in enumerate( self.rts_ ): + self.rt_to_idx_.append((rt, idx)) + self.idx_to_rt_[ idx ] = rt + + self.length_ = len( self.rts_ ) + + def rt_to_idx(self, nrt ): + assert isinstance(nrt, float) + + for (rt, idx) in self.rt_to_idx_: + if roughly_equal(rt, nrt): + return idx + else: + raise TypeError('no match') + + def rts( self ): + return self.rts_ + + def idx_to_rt( self, idx ): + return self.idx_to_rt_[ idx ] + + def length( self ): + return self.length_ + + +def get_all_rts( controller ): + rts = [ sc.rt for sc in controller.scans[1] ] + rts = sorted( rts ) + return RtMapper( rts ) + +def determine_significant( eics ): + eicnames = list(set(list(map(lambda e: e.name, eics)))) + np.random.shuffle( eicnames ) + mapper = dict( ) + for idx, name in enumerate( eicnames ): + mapper[ name ] = idx < 800 + return mapper + + +def get_multiplier_mapper( sigmapper ): + result = dict( ) + for chem, is_sig in sigmapper.items( ): + mults1, mults2 = get_mults(is_sig) + mults1 = np.array( mults1 ) + mults2 = np.array( mults2 ) + fc = get_fc( is_sig ) + if random_bool( ): + result[ chem ] = np.concatenate(( + mults1*fc, mults2 + )) + else: + result[ chem ] = np.concatenate(( + mults2*fc, mults1 + )) + return result + + +def get_removal_mapper( sigmapper, pct_missing ): + num_sig = sum(sigmapper.values()) + num_to_remove = int(pct_missing/100*num_sig) + chemnames = list(sigmapper.keys()) + np.random.shuffle( chemnames ) + + result = dict( ) + for idx, cname in enumerate( chemnames ): + result[ cname ] = idx < num_to_remove + + return result + +EIC = namedtuple('EIC', 'name mzs rts its') + +def export_summary( summary, target_dir ): + all_chems = set() + for rep, chems in summary.items(): + for chemname in chems.keys(): + all_chems.add( chemname ) + repnames = summary.keys() + data = dict( ) + data['rep'] = repnames + for ac in all_chems: + col_vals = [] + for r in repnames: + if ac in summary[r]: + col_vals.append( summary[r][ac] ) + else: + col_vals.append( None ) + data[ ac ] = col_vals + + df = pd.DataFrame( data ) + mapper = dict( ) + for col in df.columns: + if col == 'rep': + continue + + rts, mzs, its = [], [], [] + for row in df[col]: + if row is None: + its.append( 0 ) + continue + (rt, mz, ct ) = row + rts.append( rt ) + mzs.append( mz ) + its.append( ct ) + rts = np.array( rts ) + mzs = np.array( mzs ) + df[col] = its + mapper[ col ] = f"{np.mean(rts):.2f}_{np.mean(mzs):.4f}" + df.rename( mapper, axis=1, inplace=True) + df.to_csv( f"{target_dir}/summary.csv", index=False ) + +def make_plot( controller ): + scan = controller.scans[1][100] + + plt.plot(scan.mzs, scan.intensities, c='k') + plt.ylabel('intensity (A.U.)') + plt.xlabel('mz (daltons)') + plt.show() + +def create_condition( controller, pct_missing, noise, eics, mapper, sigmapper, basedir='mvapack-data' ): + target_dir = f"{basedir}/{int(pct_missing)}_missing/{noise:.2f}_noise" + mult_mapper = get_multiplier_mapper( sigmapper ) + pickle.dump(mult_mapper, open('mult_mapper.pickle', 'wb')) + exit( 0 ) + assert os.path.isdir( target_dir ), target_dir + allchemnames = list(map(lambda pr: pr.name, eics )) + summary = dict( ) + for r_idx in range( 20 ): + print( r_idx ) + features = dict( ) + should_remove = get_removal_mapper( sigmapper, pct_missing ) + group = "G0" if r_idx < 10 else "G1" + rep = f"R{r_idx%10}" + full_name = group + '-' + rep + '.mzML' + local_eics = [deepcopy(ee) for ee in eics ] + + finalized_eics = [] + for le in local_eics: + rts, its, name = le.rts, le.its, le.name + mult = mult_mapper[ name ][ r_idx ] + its *= mult + + if should_remove[ name ]: + continue + + finalized_eics.append( + EIC(name=name, mzs=le.mzs, rts=rts, its=its) + ) + controller.scans = eics_to_scans( finalized_eics ) + #controller.update_scans( finalized_eics, mapper ) + #make_plot( controller ) + #controller = increase_resolution( controller ) + #controller = add_noise( controller, noise ) + #make_plot( controller ) + controller.write_mzML( target_dir, target_dir + '/' + full_name ) + + for acn in allchemnames: + if should_remove[acn]: + continue + filtered = list(filter( lambda pr: pr.name == acn, finalized_eics )) + max_mz, max_it, max_rt = 0, 0, 0 + for feic in filtered: + mzs, rts, its = feic.mzs, feic.rts, feic.its + + max_idx = np.argmax( its ) + if its[ max_idx ] > max_it: + max_it = its[ max_idx ] + max_mz = mzs[ max_idx ] + max_rt = rts[ max_idx ] + + features[ acn ] = ( max_rt, max_mz, max_it ) + + summary[ full_name ] = features + + export_summary( summary, target_dir ) + +def setup_params( ): + parser = argparse.ArgumentParser( ) + #parser.add_argument('--mode', required=True, type=str ) + parser.add_argument('--pct_missing', required=False, type=int, default=0 ) + parser.add_argument('--noise', required=False, type=float, default=0 ) + parser.add_argument('--idx', required=False, type=int, default=0 ) + + return parser.parse_args( ) + +def eics_to_scans( eics ): + eics = sorted( eics, key=lambda e: e.mzs[0] ) + scans = [] + avg_dur = np.mean(np.array([abs(t1 - t2) for t1, t2 in zip( eics[0].rts[0:-1], eics[0].rts[1:])])) + rt_len = len( eics[0].rts ) + mzs = list(map(lambda idx: [],range(rt_len))) + its = list(map(lambda idx: [],range(rt_len))) + + for idx in range( rt_len ): + for e in eics: + mzs[idx].append( e.mzs[idx] ) + its[idx].append( e.its[idx] ) + + for idx,rt in enumerate( eics[0].rts ): + if idx != rt_len-1: + duration = eics[0].rts[idx+1]-rt + else: + duration = avg_dur + assert duration > 0 + scans.append(Scan( + idx, np.array(mzs[idx]), np.array(its[idx]), 1, rt, duration + )) + + return {1: scans} + +def main( params ): + + np.random.seed( 100 ) + params = Params() + #params.rt_upper = 120 # TODO this is just for debugging + #dgs = setup_dirs(params) + (ROI_Sources, ps, hmdb) = load_dbs() + # can probably make the smallest peaks a bit smaller here + if False: + set_log_level_debug() + (ROI_Sources, ps, hmdb) = load_dbs() + orig_chems = make_dataset(ROI_Sources, ps, hmdb, 10000) + pickle.dump(orig_chems, open('chems.pickle', 'wb')) + else: + orig_chems = pickle.load(open('chems.pickle', 'rb')) + #print(len(orig_chems)) + orig_chems = list(filter(lambda oc: oc.rt > 60, orig_chems )) + sig = False + mass_spec = IndependentMassSpectrometer( + POSITIVE, orig_chems, ps, sig, None, True + ) + controller = SimpleMs1Controller(mass_spec, params.mz_upper) + #controller.run(params.rt_lower, params.rt_upper, False) + ### ok so at this point we have all of the data points. + ### Basically want to take the peak_recorder and then + ### return a list of tuples (chem name (mzs, rts, its )). + mapper = get_all_rts( controller ) + ##eics = controller.create_eics( mapper ) + ##pickle.dump(eics, open('eics.pickle', 'wb')) + ##exit( 0 ) + eics = pickle.load(open('eics-ideal.pickle', 'rb')) + controller.scans = eics_to_scans( eics ) + #controller.write_mzML('idk', 'test.mzML') + #print( scans ) + sigmapper = determine_significant( eics ) + + for pct_missing in params.pct_missing: + for noise in params.noise_level: + create_condition( controller, pct_missing, 0.0025, eics, mapper, sigmapper ) + + +if __name__ == "__main__": + main( setup_params() ) diff --git a/Synthetic data creation scripts/vimms_data_generation/mk.py b/Synthetic data creation scripts/vimms_data_generation/mk.py new file mode 100644 index 00000000..19550a93 --- /dev/null +++ b/Synthetic data creation scripts/vimms_data_generation/mk.py @@ -0,0 +1,542 @@ +#!/usr/bin/env python3 +# coding: utf-8 +import json +import sys, os +import shutil +import pandas as pd +import matplotlib.pyplot as plt +from collections import namedtuple, defaultdict + +sys.path.append("..") +import pickle +import numpy as np +from glob import glob +from pathlib import Path +from itertools import product +from vimms.Chemicals import ChemicalCreator +from vimms.MassSpec import IndependentMassSpectrometer +from vimms.Controller import SimpleMs1Controller +from vimms.Common import * +import vimms +from scipy.interpolate import interp1d + +# TODO need to document what each of the replicates does and doesn't have +# TODO need to set it up so that we're using the same multipliers for the +# significant compounds +from copy import deepcopy +import matplotlib.pyplot as plt + +DATA = json.load(open("multipliers.json", "r")) + +GOOD_MULTS = DATA["good"] +BAD_MULTS = DATA["bad"] + + +class Params: + rt_lower = 0 + rt_upper = 1440 + mz_lower = 0 + mz_upper = 2000 + pct_missing = [0, 10, 20] + noise_level = [0, 0.01, 0.1] + + +def safe_mkdir(dirname): + if not os.path.exists(dirname): + os.mkdir(dirname) + + +def remove_x_pct(df: pd.DataFrame, x: int): + """Function that randomly removes x pct of entries from the supplied list of vals. Makes a copy of the inputted vals""" + assert x >= 0 and x <= 100, f"x must be a percentage on the range 0 < x < 100" + # print(float(chems.size)*(1.-float(x)/100.)) + num_cols, num_rows = len(df.columns), len(df[df.columns[0]]) + n_chems = num_cols * num_rows + n_remove = int(n_chems * x / 100.0 + 0.5) + rm_per_col = np.zeros(num_cols, np.int32) + for idx in range(n_remove): + rm_per_col[idx % num_cols] += 1 + + np.random.shuffle(rm_per_col) + row_idxs = np.arange(0, num_rows, 1) + + result = pd.DataFrame() + for c_idx, col in enumerate(df.columns): + + new_col = list(map(lambda x: deepcopy(x), df[col])) + row_idxs = np.sort(row_idxs) + np.random.shuffle(row_idxs) + + col_remove = rm_per_col[c_idx] + + for cr_idx in range(col_remove): + new_col[row_idxs[cr_idx]] = None + + result[col] = new_col + + return result + + +def cv(vals): + return np.std(vals) / np.mean(vals) + + +def setup_dirs(params: Params, basedir="mvapack-data"): + # layout of the directories are: + # basedir / pct_missing / noise / group + # maybe should return the directories too? + for edir in glob(f"{basedir}/*"): + shutil.rmtree(edir) + + for pct in params.pct_missing: + for noise in params.noise_level: + dirname = "/".join([basedir, f"{pct}_missing", f"{noise:.2f}_noise"]) + if not os.path.isdir(dirname): + Path(dirname).mkdir(parents=True, exist_ok=True) + + +def load_dbs(): + base_dir = os.path.abspath("example_data") + ps = load_obj(Path(base_dir, "peak_sampler_mz_rt_int_19_beers_fullscan.p")) + hmdb = load_obj(Path(base_dir, "hmdb_compounds.p")) + out_dir = Path(base_dir, "results", "MS1_single") + # the list of ROI sources created in the previous notebook '01. Download Data.ipynb' + ROI_Sources = [str(Path(base_dir, "DsDA", "DsDA_Beer", "beer_t10_simulator_files"))] + + return (ROI_Sources, ps, hmdb) + + +def make_datasets(ROI_Sources, ps, hmdb, g1_size=10, g2_size=10): + min_ms1_intensity = 1.75e5 # TODO make this into a global + + # m/z and RT range of chemicals + + # the number of chemicals in the sample + # n_chems = 2000 + n_chems = 2000 + + # maximum MS level (we do not generate fragmentation peaks when this value is 1) + ms_level = 1 + + chems = ChemicalCreator(ps, ROI_Sources, hmdb) + dataset = chems.sample(mz_range, rt_range, min_ms1_intensity, n_chems, ms_level) + # out_dir = 'mvapack-data' + # out_dir = 'demo' + # save_obj(dataset, Path(out_dir, 'dataset.p')) + + group1_reps, group2_reps = [], [] + + for g1 in range(g1_size): + group1_reps.append( + chems.sample(mz_range, rt_range, min_ms1_intensity, n_chems, ms_level) + ) + # this is the part where we change the intensities + for g2 in range(g2_size): + group2_reps.append( + chems.sample(mz_range, rt_range, min_ms1_intensity, n_chems, ms_level) + ) + + return group1_reps, group2_reps + + +def merge_chems(cdict, chems): + + for cd_key, chem in zip(cdict.keys(), chems): + cdict[cd_key].append(chem) + + +def get_fc(is_sig): + + if is_sig: + return np.random.random() * 2 + 2 + else: + return np.random.random() * 1 + + +def get_mults(is_sig): + if is_sig: + return GOOD_MULTS.pop(), GOOD_MULTS.pop() + else: + return BAD_MULTS.pop(), BAD_MULTS.pop() + + +def get_t_offset(is_sig): + if is_sig: + return (np.random.random() * 10) - 5 + else: + return (np.random.random() * 60) - 30 + + +def get_t_base(is_sig): + if is_sig: + return (np.random.random() * 30) - 15 + else: + return (np.random.random() * 60) - 30 + + +def get_chemical_lists(chems, NUM_REPS=10): + + SIGNIFICANT = int(len(chems) * 0.40) + keys = list(map(lambda idx: f"rep_{idx}", range(NUM_REPS))) + values = list(map(lambda idx: deepcopy([]), range(NUM_REPS))) + clean_dict = lambda: deepcopy(dict(zip(keys, values))) + group1, group2 = clean_dict(), clean_dict() + np.random.shuffle(chems) + g1_sig, g1_isig, g2_sig, g2_isig = None, None, None, None + + for idx, chem in enumerate(chems): + # first figure out the fold change.... between 2 and 6? + is_sig = idx < SIGNIFICANT + if idx == (SIGNIFICANT - 1): + g1_sig = pd.DataFrame(group1) + g2_sig = pd.DataFrame(group2) + group1, group2 = clean_dict(), clean_dict() + + fc = get_fc(is_sig) + greater, lesser = ( + list(map(lambda idx: deepcopy(chem), range(NUM_REPS))), + list(map(lambda idx: deepcopy(chem), range(NUM_REPS))), + ) + mults1, mults2 = get_mults(is_sig) + + greater_base, lesser_base = get_t_base(is_sig), get_t_base(is_sig) + + for ii, r_key in enumerate(keys): + greater[ii].max_intensity *= mults1[ii] + greater[ii].max_intensity *= fc + greater[ii].rt += greater_base + get_t_offset(is_sig) + lesser[ii].max_intensity *= mults2[ii] + lesser[ii].rt += lesser_base + get_t_offset(is_sig) + # for some reason we can actually get negative peaks... that's a problem + greater[ii].rt = max(greater[ii].rt, 10) + lesser[ii].rt = max(lesser[ii].rt, 10) + + g1_greater = bool(np.random.randint(0, 2)) + + merge_chems(group1 if g1_greater else group2, greater) + merge_chems(group2 if g1_greater else group1, lesser) + + return g1_sig, pd.DataFrame(group1), g2_sig, pd.DataFrame(group2), keys + + +def write_summary(g1, g2, dirname, peak_values): + + unique_formulas = set() + summary_info = dict() + for idx, c in enumerate(g1.columns): + summary_info[f"G0-R{idx}"] = deepcopy(g1[c]) + + for idx, c in enumerate(g2.columns): + summary_info[f"G1-R{idx}"] = deepcopy(g2[c]) + + for k, v in summary_info.items(): + pvs = peak_values[k] + for chem in v: + print(str(chem.formula) in pvs) + break + print(peak_values.keys()) + exit(0) + + for clist in summary_info.values(): + _ = list( + map( + lambda c: unique_formulas.add(str(c.formula)), + list(filter(lambda chem: chem is not None, clist)), + ) + ) + summary = dict(chemical=deepcopy(list(unique_formulas))) + + for rname, rchems in summary_info.items(): + rchem_list = [] + rep_mapper = dict() + for rc in rchems: + if rc is None: + continue + rep_mapper[str(rc.formula)] = deepcopy(rc) + + for uf in unique_formulas: + rchem_list.append(rep_mapper.get(uf, None)) + + summary[rname] = rchem_list + + summary = pd.DataFrame(summary) + summary.set_index("chemical", inplace=True) + summary = summary.transpose() + mapper = dict() + for col in summary.columns: + sum_col = summary[col] + for c in sum_col: + print(c) + # print( sum_col[0] ) + # print( sum_col[0].__dict__ ) + exit(0) + times = np.array( + list(map(lambda chem: chem.rt if chem is not None else None, sum_col)) + ) + mzs = np.array( + list( + map( + lambda chem: chem.isotopes[0][0] if chem is not None else None, + sum_col, + ) + ) + ) + times = times[times != None] + mzs = mzs[mzs != None] + avg_mz, avg_rt = np.mean(mzs), np.mean(times) + mapper[col] = f"{avg_rt:.2f}_{avg_mz:.4f}" + + summary.rename(mapper=mapper, inplace=True, axis=1) + for col in summary.columns: + summary[col] = summary.apply( + lambda row: row[col].max_intensity if row[col] else 0, axis=1 + ) + + summary.to_csv(f"{dirname}/summary.csv") + summary.to_pickle(f"{dirname}/summary.pickle") + print(f"{dirname}/summary.csv") + + +def increase_resolution(controller): + new_scans = [] + old_len = len(controller.scans[1]) + + for idx, scan in enumerate(controller.scans[1]): + # loop through each scan + new_mz, new_it = [], [] + interp_mz = [] + num_points = len(scan.mzs) + + # need to check that there are points + if num_points: + new_mz.append(scan.mzs[0] - 0.1) + new_it.append(0) + + for pt_idx, (m, i) in enumerate(zip(scan.mzs, scan.intensities)): + interp_mz.append(np.linspace(m - 0.021, m + 0.021, 10)) + + new_mz.extend([m - 0.02, m, m + 0.02]) + new_it.extend([0, i, 0]) + + if pt_idx > 0 and pt_idx < num_points - 1: + interp_mz.append(np.arange(m + 0.25, scan.mzs[pt_idx + 1] - 0.25, 0.2)) + + if num_points: + new_mz.append(scan.mzs[-1] + 0.1) + new_it.append(0) + + if not len(scan.mzs): + new_scans.append(deepcopy( scan )) + continue + + interp_mz = np.concatenate(interp_mz) + interp_mz = sorted(interp_mz) + + interp_func = interp1d(new_mz, new_it, kind="linear") + new_x = np.linspace(np.min(new_mz), np.max(new_mz), 1000) + interp_it = interp_func(interp_mz) + interp_it[interp_it < 0] = 0 + + new_scan = deepcopy(scan) + new_scan.mzs = interp_mz + new_scan.intensities = interp_it + new_scans.append(new_scan) + + controller.scans[1] = new_scans + + assert len(controller.scans[1]) == old_len + + return controller + + +def valid_mzs(mzs): + for idx in range(1, len(mzs)): + assert mzs[idx] >= mzs[idx - 1], f"{mzs[idx-1]}, {mzs[idx]}" + + +def add_noise(controller, noise): + # if noise == 0: + # return controller + + def max_it( scan ): + if len(scan.intensities): + return np.max( scan.intensities ) + else: + return 0 + old_len = len(controller.scans[1]) + + max_it = np.max( + max(controller.scans[1], key=max_it ).intensities + ) + max_noise = float(max_it) * (float(noise) / 100.0) + new_scans = [] + + for idx, scan in enumerate(controller.scans[1]): + valid_mzs(scan.mzs) + scan_noise = list( + map(lambda it: np.random.rand() * max_noise, range(len(scan.mzs))) + ) + new_its = [] + for ct, nz in zip(scan.intensities, scan_noise): + if ct > 0: + new_its.append(ct) + else: + new_its.append(nz) + + new_scan = deepcopy(scan) + assert len(scan.mzs) == len(scan.intensities) + new_scan.intensities = new_its + new_scans.append(new_scan) + + controller.scans[1] = new_scans + assert len(controller.scans[1]) == old_len + + return controller + + +def make_chem_dict( + group1_sig_orig, group1_isig_orig, group2_sig_orig, group2_isig_orig, rep_keys +): + # shuffle up and deal! + group1_sig_orig = group1_sig_orig.sample(frac=1).reset_index(drop=True) + group2_sig_orig = group2_sig_orig.sample(frac=1).reset_index(drop=True) + group1_isig_orig = group1_isig_orig.sample(frac=1).reset_index(drop=True) + group2_isig_orig = group2_isig_orig.sample(frac=1).reset_index(drop=True) + + chem_dict = dict() + for g_idx, (grp_sig, grp_isig) in enumerate( + [(group1_sig_orig, group1_isig_orig), (group2_sig_orig, group2_isig_orig)] + ): + for idx, r_key in enumerate(rep_keys): + # what do we care about now? need to locate + # the part where noise is actually added + chems_sig, chems_isig = ( + deepcopy(grp_sig[r_key]).to_list(), + deepcopy(grp_isig[r_key]).to_list(), + ) + chems_sig.extend(chems_isig) + chem_dict[f"G{g_idx}-R{idx}"] = deepcopy(chems_sig) + return chem_dict + + +def num_zeros(df): + ct = 0 + + for c in df.columns: + ct += sum(list(map(lambda ch: ch.max_intensity == 0, df[c]))) + + return ct + + +def adjust_groups(group1_sig, group2_sig, peak_values): + # print( peak_values.keys() ) + # print( group1_sig ) + + def col_to_key(colname): + return str(int(colname.split("_")[-1]) + 1) + + for g1_col in group1_sig.columns: + key_name = f"G0-R{col_to_key(g1_col)}" + c_dict = peak_values[key_name] + print(c_dict) + exit(0) + # print( [t.formula for t in group1_sig[ g1_col ]] ) + for t_chem in group1_sig[g1_col]: + print(t_chem.formula in c_dict) + # if t_chem.formula in c_dict: + # print( t_chem.formula ) + # print( c_dict[t_chem.formula] ) + exit(0) + print(key_name) + assert key_name in peak_values + # need to convert the g1_col to the repname + print(g1_col) + + for g2_col in group2_sig.columns: + key_name = f"G1-R{col_to_key(g2_col)}" + print(key_name) + assert key_name in peak_values # need to convert the g1_col to the repname + print(g1_col) + + exit(0) + + +def main(): + + np.random.seed(100) + params = Params() + #params.rt_upper = 120 # TODO this is just for debugging + dgs = setup_dirs(params) + (ROI_Sources, ps, hmdb) = load_dbs() + if False: + (ROI_Sources, ps, hmdb) = load_dbs() + + set_log_level_debug() + # can probably make the smallest peaks a bit smaller here + (g1, g2) = make_datasets(ROI_Sources, ps, hmdb) + pickle.dump((g1, g2), open("data.p", "wb")) + + else: + (g1, g2) = pickle.load(open("data.p", "rb")) + # ok for these metabolites to be selected they need to vary between groups and not vary much inside of their own + ( + group1_sig_orig, + group1_isig_orig, + group2_sig_orig, + group2_isig_orig, + rep_keys, + ) = get_chemical_lists(g1[0]) + + # this is the part where the different replicates need to be made + for pct_missing in params.pct_missing: + group1_sig = remove_x_pct(group1_sig_orig, pct_missing) + group2_sig = remove_x_pct(group2_sig_orig, pct_missing) + chem_dict = make_chem_dict( + deepcopy(group1_sig), + group1_isig_orig, + deepcopy(group2_sig), + group2_isig_orig, + rep_keys, + ) + ct = defaultdict( int ) + for noise in params.noise_level: + dirname = f"mvapack-data/{pct_missing}_missing/{noise:.2f}_noise/" + safe_mkdir(dirname) + assert os.path.isdir(dirname), f"The directory {dirname} does not exist" + + peak_values = dict() + + for rep_name, chems in chem_dict.items(): + print( rep_name ) + filt_chems = list(filter(lambda c: c is not None, chems)) + for fc in filt_chems: + ct[ str(fc.formula) ] += 1 + continue + print(len(filt_chems)) + mass_spec = IndependentMassSpectrometer( + POSITIVE, filt_chems, ps, None, True + ) + controller = SimpleMs1Controller(mass_spec, params.mz_upper) + controller.run(params.rt_lower, params.rt_upper, False) + + peak_values[rep_name] = deepcopy(controller.peak_recorder()) + controller = increase_resolution(controller) + controller = add_noise(controller, noise) + mzml_filename = Path(dirname, f"{rep_name}.mzML") + controller.write_mzML( dirname , mzml_filename) + + plt.hist( ct.values() ) + plt.show() + for k,v in ct.items(): + print(k, v) + #pickle.dump( peak_values, open('peak_values.pickle', 'wb')) + exit( 0 ) + print(peak_values) + # group1_sig, group2_sig = adjust_groups( group1_sig, group2_sig, peak_values ) + # exit( 0 ) + write_summary(group1_sig, group2_sig, dirname, peak_values) + exit(0) + + +if __name__ == "__main__": + main() diff --git a/Synthetic data creation scripts/vimms_data_generation/multiple_samples_example.ipynb b/Synthetic data creation scripts/vimms_data_generation/multiple_samples_example.ipynb new file mode 100644 index 00000000..bfac0ccd --- /dev/null +++ b/Synthetic data creation scripts/vimms_data_generation/multiple_samples_example.ipynb @@ -0,0 +1,865 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 176, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 177, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n" + ] + } + ], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 178, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import sys\n", + "import scipy.stats\n", + "import pylab as plt\n", + "from IPython import display\n", + "import pylab as plt\n", + "import glob\n", + "from collections import defaultdict" + ] + }, + { + "cell_type": "code", + "execution_count": 179, + "metadata": {}, + "outputs": [], + "source": [ + "sys.path.append('..')" + ] + }, + { + "cell_type": "code", + "execution_count": 180, + "metadata": {}, + "outputs": [], + "source": [ + "from vimms.Chemicals import *\n", + "from vimms.Chromatograms import *\n", + "from vimms.MassSpec import *\n", + "from vimms.Controller import *\n", + "from vimms.Common import *\n", + "from vimms.DataGenerator import *\n", + "from vimms.DsDA import *" + ] + }, + { + "cell_type": "code", + "execution_count": 181, + "metadata": {}, + "outputs": [], + "source": [ + " set_log_level_warning()\n", + "# set_log_level_info()\n", + "# set_log_level_debug()" + ] + }, + { + "cell_type": "code", + "execution_count": 182, + "metadata": {}, + "outputs": [], + "source": [ + "# base_dir = '..\\\\data'\n", + "# base_dir = 'C:\\\\Users\\\\joewa\\\\University of Glasgow\\\\Vinny Davies - CLDS Metabolomics Project\\\\Trained Models'\n", + "base_dir = '/Users/simon/University of Glasgow/Vinny Davies - CLDS Metabolomics Project/Trained Models'" + ] + }, + { + "cell_type": "code", + "execution_count": 183, + "metadata": {}, + "outputs": [], + "source": [ + "ps = load_obj(os.path.join(base_dir, 'peak_sampler_mz_rt_int_19_beers_fullscan.p'))" + ] + }, + { + "cell_type": "code", + "execution_count": 184, + "metadata": {}, + "outputs": [], + "source": [ + "hmdb = load_obj(os.path.join(base_dir, 'hmdb_compounds.p'))" + ] + }, + { + "cell_type": "code", + "execution_count": 185, + "metadata": {}, + "outputs": [], + "source": [ + "out_dir = '/Users/simon/vimms_data'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Create Initial Chemical" + ] + }, + { + "cell_type": "code", + "execution_count": 186, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /Users/simon/vimms_data/BaseDataset/dataset.p\n" + ] + } + ], + "source": [ + "ROI_Sources = [\"/Users/simon/vimms_data/beer_t10_simulator_files\"]\n", + "min_ms1_intensity = 1.75E5\n", + "rt_range = [(400, 500)]\n", + "mz_range = [(200, 400)]\n", + "n_peaks = 50\n", + "roi_rt_range = [20, 40]\n", + "chems = ChemicalCreator(ps, ROI_Sources, hmdb)\n", + "dataset = chems.sample(mz_range, rt_range, min_ms1_intensity, n_peaks, 1, use_database=True, \n", + " fixed_mz=False, roi_rt_range=roi_rt_range)\n", + "save_obj(dataset, os.path.join(out_dir, 'BaseDataset/dataset.p'))" + ] + }, + { + "cell_type": "code", + "execution_count": 187, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "23.639999999999986\n", + "30.91300000000001\n", + "34.07000000000005\n", + "33.34400000000005\n", + "27.58499999999998\n", + "30.91192\n", + "24.62999999999988\n", + "36.65499999999997\n", + "36.42000000000007\n", + "30.340000000000146\n", + "23.268\n", + "21.972999999999956\n", + "36.011000000000024\n", + "20.067999999999984\n", + "25.733999999999924\n", + "20.24000000000001\n", + "24.460000000000036\n", + "23.871999999999957\n", + "24.733999999999924\n", + "25.93599999999998\n", + "32.3900000000001\n", + "22.5\n", + "29.205000000000013\n", + "22.774999999999977\n", + "28.649\n", + "30.10300000000001\n", + "31.930000000000064\n", + "29.180999999999926\n", + "34.6412\n", + "26.58100000000001\n", + "27.55400000000003\n", + "29.246999999999957\n", + "23.357999999999947\n", + "21.681999999999988\n", + "21.232000000000085\n", + "20.4762\n", + "37.3599999999999\n", + "24.817999999999984\n", + "23.531000000000006\n", + "31.873000000000047\n", + "24.039000000000044\n", + "27.186000000000035\n", + "28.537999999999897\n", + "36.95600000000002\n", + "38.460000000000036\n", + "20.876000000000005\n", + "38.54899999999998\n", + "37.150000000000006\n", + "26.47999999999999\n", + "20.381999999999948\n" + ] + } + ], + "source": [ + "for chem in dataset:\n", + " print(np.abs(chem.chromatogram.min_rt - chem.chromatogram.max_rt))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Create Multiple Samples" + ] + }, + { + "cell_type": "code", + "execution_count": 198, + "metadata": {}, + "outputs": [], + "source": [ + "n_samples = [50,50] # number of files per class\n", + "classes = [\"class%d\" % i for i in range(len(n_samples))] # creates default list of classes\n", + "intensity_noise_sd = [1000] # noise on max intensity" + ] + }, + { + "cell_type": "code", + "execution_count": 199, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['class0', 'class1']" + ] + }, + "execution_count": 199, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "classes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Add intensity changes between different classes" + ] + }, + { + "cell_type": "code", + "execution_count": 200, + "metadata": {}, + "outputs": [], + "source": [ + "change_probabilities = [0 for i in range(len(n_samples))] # probability of intensity changes between different classes\n", + "change_differences_means = [0 for i in range(len(n_samples))] # mean of those intensity changes\n", + "change_differences_sds = [0 for i in range(len(n_samples))] # SD of those intensity changes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Add experimental variables (examples in comments)" + ] + }, + { + "cell_type": "code", + "execution_count": 201, + "metadata": {}, + "outputs": [], + "source": [ + "experimental_classes = None # [[\"male\",\"female\"],[\"Positive\",\"Negative\",\"Unknown\"]]\n", + "experimental_probabilitities = None # [[0.5,0.5],[0.33,0.33,0.34]]\n", + "experimental_sds = None # [[250],[250]]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Dropout chemicals from in different classes" + ] + }, + { + "cell_type": "code", + "execution_count": 202, + "metadata": {}, + "outputs": [], + "source": [ + "dropout_probability = 0.2\n", + "dropout_probabilities = [dropout_probability for i in range(len(n_samples))]\n", + "# dropout_probabilities = None\n", + "# dropout_numbers = 2 # number of chemicals dropped out in each class\n", + "dropout_numbers = None" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Set save location" + ] + }, + { + "cell_type": "code", + "execution_count": 203, + "metadata": {}, + "outputs": [], + "source": [ + "save_location = os.path.join(out_dir, 'ChemicalFiles')" + ] + }, + { + "cell_type": "code", + "execution_count": 204, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "26\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_0.p\n", + "21\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_1.p\n", + "28\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_2.p\n", + "21\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_3.p\n", + "25\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_4.p\n", + "25\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_5.p\n", + "23\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_6.p\n", + "30\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_7.p\n", + "24\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_8.p\n", + "26\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_9.p\n", + "21\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_10.p\n", + "22\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_11.p\n", + "27\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_12.p\n", + "20\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_13.p\n", + "26\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_14.p\n", + "18\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_15.p\n", + "26\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_16.p\n", + "24\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_17.p\n", + "25\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_18.p\n", + "22\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_19.p\n", + "27\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_20.p\n", + "28\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_21.p\n", + "25\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_22.p\n", + "22\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_23.p\n", + "24\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_24.p\n", + "25\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_25.p\n", + "24\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_26.p\n", + "28\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_27.p\n", + "23\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_28.p\n", + "22\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_29.p\n", + "24\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_30.p\n", + "25\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_31.p\n", + "22\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_32.p\n", + "23\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_33.p\n", + "24\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_34.p\n", + "25\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_35.p\n", + "23\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_36.p\n", + "27\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_37.p\n", + "25\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_38.p\n", + "22\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_39.p\n", + "27\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_40.p\n", + "27\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_41.p\n", + "27\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_42.p\n", + "26\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_43.p\n", + "24\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_44.p\n", + "25\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_45.p\n", + "28\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_46.p\n", + "26\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_47.p\n", + "21\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_48.p\n", + "25\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_49.p\n", + "28\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_50.p\n", + "20\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_51.p\n", + "27\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_52.p\n", + "23\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_53.p\n", + "29\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_54.p\n", + "24\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_55.p\n", + "32\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_56.p\n", + "26\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_57.p\n", + "29\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_58.p\n", + "27\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_59.p\n", + "29\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_60.p\n", + "30\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_61.p\n", + "25\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_62.p\n", + "31\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_63.p\n", + "24\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_64.p\n", + "25\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_65.p\n", + "27\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_66.p\n", + "28\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_67.p\n", + "22\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_68.p\n", + "28\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_69.p\n", + "26\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_70.p\n", + "26\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_71.p\n", + "24\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_72.p\n", + "26\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_73.p\n", + "21\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_74.p\n", + "30\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_75.p\n", + "25\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_76.p\n", + "26\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_77.p\n", + "29\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_78.p\n", + "25\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_79.p\n", + "26\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_80.p\n", + "28\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_81.p\n", + "25\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_82.p\n", + "27\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_83.p\n", + "32\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_84.p\n", + "23\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_85.p\n", + "26\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_86.p\n", + "26\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_87.p\n", + "28\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_88.p\n", + "22\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_89.p\n", + "31\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_90.p\n", + "28\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_91.p\n", + "26\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_92.p\n", + "22\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_93.p\n", + "31\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_94.p\n", + "29\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_95.p\n", + "28\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_96.p\n", + "29\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_97.p\n", + "27\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_98.p\n", + "30\n", + "Saving <class 'list'> to /Users/simon/vimms_data/ChemicalFiles/sample_99.p\n" + ] + } + ], + "source": [ + "multiple_samples = MultiSampleCreator(dataset, n_samples, classes, intensity_noise_sd, \n", + " change_probabilities, change_differences_means, change_differences_sds, dropout_probabilities, dropout_numbers,\n", + " experimental_classes, experimental_probabilitities, experimental_sds, save_location=save_location)" + ] + }, + { + "cell_type": "code", + "execution_count": 205, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'C11H11N3O2S', 'C13H18O2', 'C18H27NO3', 'C14H11Cl2NO4', 'C5H6Cl6N2O3', 'C4H7Cl2O4P', 'C18H37NO3', 'C10H12ClN3O3S', 'C15H10O7'}\n" + ] + }, + { + "data": { + "text/plain": [ + "100" + ] + }, + "execution_count": 205, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Check the number of identical formulas in two objects of the same class\n", + "formulas0 = set([str(a.formula) for a in multiple_samples.samples[0]])\n", + "formulas1 = set([str(a.formula) for a in multiple_samples.samples[1]])\n", + "print(formulas0-formulas1)\n", + "\n", + "total_samples = np.sum(multiple_samples.n_samples)\n", + "total_samples" + ] + }, + { + "cell_type": "code", + "execution_count": 206, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving <class 'list'> to /Users/simon/vimms_data/MissingChemicals/missing_chemicals.p\n" + ] + }, + { + "data": { + "text/plain": [ + "[[KnownChemical - 'C15H16Cl3N3O2' rt=409.99 max_intensity=1725954.34,\n", + " KnownChemical - 'C16H28O' rt=431.85 max_intensity=1059507.71,\n", + " KnownChemical - 'C19H18ClN5' rt=423.53 max_intensity=329447.21,\n", + " KnownChemical - 'C17H23NO2' rt=415.68 max_intensity=251532.13,\n", + " KnownChemical - 'C11H13N3O3S' rt=421.16 max_intensity=1558628.97,\n", + " KnownChemical - 'C11H28N4' rt=416.56 max_intensity=2005289.30,\n", + " KnownChemical - 'C13H9NO2S' rt=401.56 max_intensity=239654.60,\n", + " KnownChemical - 'C13H9NO2S' rt=428.33 max_intensity=13229609.72,\n", + " KnownChemical - 'C10H16N2O4' rt=414.90 max_intensity=49733322.17,\n", + " KnownChemical - 'C8H11NO5S' rt=412.33 max_intensity=342886.18,\n", + " KnownChemical - 'C13H11N3O5S2' rt=450.38 max_intensity=267161.99,\n", + " KnownChemical - 'C7H9NO4S' rt=428.11 max_intensity=246202.61,\n", + " KnownChemical - 'C21H29NO' rt=425.12 max_intensity=245703.10,\n", + " KnownChemical - 'C8H12O9' rt=459.26 max_intensity=1531653.32,\n", + " KnownChemical - 'C16H34' rt=427.40 max_intensity=76844746.48],\n", + " [KnownChemical - 'C15H16Cl3N3O2' rt=409.99 max_intensity=1725954.34,\n", + " KnownChemical - 'C11H11N3O2S' rt=424.50 max_intensity=246727.63,\n", + " KnownChemical - 'C10H12ClN3O3S' rt=476.40 max_intensity=1936392.25,\n", + " KnownChemical - 'C9H9Cl2N3O' rt=422.93 max_intensity=547923.51,\n", + " KnownChemical - 'C14H30O3' rt=445.79 max_intensity=1703073.85,\n", + " KnownChemical - 'C5H6Cl6N2O3' rt=425.01 max_intensity=1033219.33,\n", + " KnownChemical - 'C16H26' rt=461.36 max_intensity=6975922.33,\n", + " KnownChemical - 'C13H9NO2S' rt=428.33 max_intensity=13229609.72,\n", + " KnownChemical - 'C13H11N3O5S2' rt=450.38 max_intensity=267161.99,\n", + " KnownChemical - 'C21H29NO' rt=425.12 max_intensity=245703.10,\n", + " KnownChemical - 'C16H26' rt=427.32 max_intensity=674172.61]]" + ] + }, + "execution_count": 206, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "save_obj(multiple_samples.missing_chemicals, os.path.join(out_dir, 'MissingChemicals','missing_chemicals.p'))\n", + "multiple_samples.missing_chemicals" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Run MS1 controller and save out .mzML files" + ] + }, + { + "cell_type": "code", + "execution_count": 207, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "101.0766999999995it [00:00, 585.06it/s] \n", + "101.0076000000006it [00:00, 756.03it/s] \n", + "100.65469999999988it [00:00, 661.05it/s] \n", + "100.08256000000006it [00:00, 764.81it/s] \n", + "100.82959000000005it [00:00, 658.77it/s] \n", + "100.91009999999983it [00:00, 664.08it/s] \n", + "100.93500000000017it [00:00, 662.31it/s] \n", + "101.21058000000016it [00:00, 586.26it/s] \n", + "100.69709999999975it [00:00, 725.90it/s] \n", + "101.0682000000005it [00:00, 662.61it/s] \n", + "100.94901999999905it [00:00, 725.71it/s] \n", + "100.52189999999928it [00:00, 752.98it/s] \n", + "101.13919999999996it [00:00, 660.83it/s] \n", + "100.76020000000017it [00:00, 772.24it/s] \n", + "100.29280000000011it [00:00, 671.33it/s] \n", + "100.12770000000052it [00:00, 850.93it/s] \n", + "100.89423999999963it [00:00, 659.05it/s] \n", + "100.00509999999986it [00:00, 663.76it/s] \n", + "100.21280000000041it [00:00, 647.97it/s] \n", + "100.29063000000008it [00:00, 735.42it/s] \n", + "101.06017999999972it [00:00, 634.71it/s] \n", + "100.28120000000047it [00:00, 634.15it/s] \n", + "100.2417999999991it [00:00, 699.44it/s] \n", + "100.6541000000002it [00:00, 749.57it/s] \n", + "100.80729999999954it [00:00, 730.74it/s] \n", + "100.61380000000008it [00:00, 605.76it/s] \n", + "100.43969999999973it [00:00, 671.23it/s] \n", + "100.09509999999972it [00:00, 528.67it/s] \n", + "100.90539999999976it [00:00, 704.68it/s] \n", + "101.12946999999963it [00:00, 744.37it/s] \n", + "100.98500000000013it [00:00, 727.62it/s] \n", + "100.83409999999947it [00:00, 680.47it/s] \n", + "100.81709999999998it [00:00, 744.30it/s] \n", + "100.31147999999962it [00:00, 711.94it/s] \n", + "100.18090000000007it [00:00, 710.33it/s] \n", + "100.83951000000047it [00:00, 673.48it/s] \n", + "100.25170999999955it [00:00, 711.93it/s] \n", + "100.84059999999982it [00:00, 596.15it/s] \n", + "101.03260000000074it [00:00, 730.29it/s] \n", + "100.18609999999956it [00:00, 740.38it/s] \n", + "101.0018999999997it [00:00, 680.59it/s] \n", + "100.86064999999934it [00:00, 661.15it/s] \n", + "100.57273000000066it [00:00, 637.58it/s] \n", + "100.27547999999979it [00:00, 665.58it/s] \n", + "100.98659999999938it [00:00, 684.00it/s] \n", + "100.82990000000001it [00:00, 692.23it/s] \n", + "101.16020000000037it [00:00, 653.11it/s] \n", + "101.03670000000022it [00:00, 689.48it/s] \n", + "100.5354999999995it [00:00, 744.45it/s] \n", + "101.03590000000048it [00:00, 678.34it/s] \n", + "100.88130000000035it [00:00, 668.63it/s] \n", + "101.37419999999992it [00:00, 769.19it/s] \n", + "100.01150000000007it [00:00, 625.90it/s] \n", + "100.1823000000013it [00:00, 685.04it/s] \n", + "100.93100000000044it [00:00, 635.23it/s] \n", + "100.32763000000011it [00:00, 564.93it/s] \n", + "100.86940000000044it [00:00, 604.78it/s] \n", + "101.10580000000095it [00:00, 669.84it/s] \n", + "100.90390000000008it [00:00, 633.06it/s] \n", + "101.33621000000164it [00:00, 671.74it/s] \n", + "100.46230000000043it [00:00, 648.29it/s] \n", + "100.00379999999984it [00:00, 629.58it/s] \n", + "100.70320000000038it [00:00, 712.30it/s] \n", + "100.32900000000166it [00:00, 595.87it/s] \n", + "100.39048999999966it [00:00, 709.57it/s] \n", + "100.27794999999986it [00:00, 683.80it/s] \n", + "100.46116300000017it [00:00, 661.67it/s] \n", + "101.46010000000024it [00:00, 640.70it/s] \n", + "100.08272000000034it [00:00, 757.71it/s] \n", + "100.0920299999998it [00:00, 511.43it/s] \n", + "100.18530000000004it [00:00, 423.14it/s] \n", + "100.42677000000026it [00:00, 545.44it/s] \n", + "101.02339999999987it [00:00, 575.67it/s] \n", + "100.53609999999924it [00:00, 552.46it/s] \n", + "101.23380000000026it [00:00, 671.33it/s] \n", + "100.07399999999944it [00:00, 572.10it/s] \n", + "100.51989999999978it [00:00, 658.79it/s] \n", + "100.1208600000001it [00:00, 426.95it/s] \n", + "100.58970000000045it [00:00, 584.11it/s] \n", + "100.3201899999994it [00:00, 614.86it/s] \n", + "100.82249999999982it [00:00, 544.32it/s] \n", + "100.97380000000055it [00:00, 512.93it/s] \n", + "100.72855099999964it [00:00, 593.04it/s] \n", + "100.54971000000108it [00:00, 562.24it/s] \n", + "101.33460000000008it [00:00, 470.71it/s] \n", + "100.08457999999865it [00:00, 562.77it/s] \n", + "101.13069999999931it [00:00, 485.30it/s] \n", + "100.06760000000037it [00:00, 435.65it/s] \n", + "100.25849999999991it [00:00, 629.32it/s] \n", + "100.3793079999997it [00:00, 721.45it/s] \n", + "100.44389999999964it [00:00, 566.47it/s] \n", + "100.19509999999968it [00:00, 623.98it/s] \n", + "100.83597999999984it [00:00, 683.44it/s] \n", + "101.16990999999996it [00:00, 726.61it/s] \n", + "100.33540000000096it [00:00, 571.94it/s] \n", + "100.70130000000023it [00:00, 538.94it/s] \n", + "100.94689999999974it [00:00, 548.05it/s] \n", + "100.83749999999907it [00:00, 610.77it/s] \n", + "101.07420000000008it [00:00, 630.68it/s] \n", + "101.04830000000038it [00:00, 614.66it/s] \n" + ] + } + ], + "source": [ + "min_rt = rt_range[0][0]\n", + "max_rt = rt_range[0][1]\n", + "controllers = defaultdict(list)\n", + "controller_to_mzml = {}\n", + "\n", + "mzml_dir = os.path.join(out_dir, 'mzmlFiles')\n", + "num_classes = len(n_samples)\n", + "sample_idx = 0\n", + "for j in range(num_classes):\n", + " num_samples = n_samples[j]\n", + " for i in range(num_samples):\n", + " fname = os.path.join(save_location, 'sample_%d.p' % sample_idx) \n", + " sample = load_obj(fname)\n", + " sample_idx += 1\n", + " \n", + " mass_spec = IndependentMassSpectrometer(POSITIVE, sample, density=ps.density_estimator)\n", + " mzml_filename = os.path.join(mzml_dir,'sample_id_0_number_%d' % i + '_class_%d.mzML' % j)\n", + " controller = SimpleMs1Controller(mass_spec)\n", + " controller.run(min_rt,max_rt)\n", + " controller.write_mzML('my_analysis', mzml_filename)\n", + " \n", + " controllers[j].append(controller)\n", + " controller_to_mzml[controller] = (j, mzml_filename, )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Print out the missing peaks" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_chem_to_peaks(controller):\n", + " chem_to_peaks = defaultdict(list)\n", + " frag_events = controller.mass_spec.fragmentation_events\n", + " for frag_event in frag_events:\n", + " chem = frag_event.chem\n", + " peaks = frag_event.peaks\n", + " chem_to_peaks[chem].extend(peaks)\n", + " return chem_to_peaks" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for controller, (current_class, mzml_filename) in controller_to_mzml.items():\n", + " controller_peaks = get_chem_to_peaks(controller)\n", + " basename = os.path.basename(mzml_filename)\n", + " front, back = os.path.splitext(mzml_filename)\n", + " outfile = front + '.csv'\n", + "\n", + " missing_peaks = [] \n", + " for other_class in range(num_classes):\n", + " if current_class == other_class:\n", + " continue\n", + "\n", + " # get the peaks that are present in current_class but missing in other_class\n", + " missing_chems = multiple_samples.missing_chemicals[other_class]\n", + " for chem in missing_chems:\n", + " peaks = controller_peaks[chem]\n", + " for peak in peaks:\n", + " row = (chem.formula.formula_string, current_class, other_class, peak.mz, peak.rt, peak.intensity)\n", + " missing_peaks.append(row)\n", + " \n", + " # convert to dataframe\n", + " columns = ['formula', 'present_in', 'missing_in', 'mz', 'RT', 'intensity']\n", + " missing_df = pd.DataFrame(missing_peaks, columns=columns)\n", + " missing_df.to_csv(os.path.join(out_dir, 'MissingChemicals', os.path.basename(outfile)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/Synthetic data creation scripts/vimms_data_generation/prepare-eics b/Synthetic data creation scripts/vimms_data_generation/prepare-eics new file mode 100644 index 00000000..276d7cdf --- /dev/null +++ b/Synthetic data creation scripts/vimms_data_generation/prepare-eics @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +import os +import pickle +import numpy as np +import matplotlib.pyplot as plt +from collections import defaultdict, namedtuple +from scipy.optimize import curve_fit +from scipy.signal import find_peaks + +EIC = namedtuple('EIC', 'name mzs rts its') + +import numpy as np +from scipy import sparse +from scipy.sparse.linalg import spsolve + +def baseline_als(y, lam, p, niter=10): + L = len(y) + D = sparse.diags([1,-2,1],[0,-1,-2], shape=(L,L-2)) + w = np.ones(L) + for i in range(niter): + W = sparse.spdiags(w, 0, L, L) + Z = W + lam * D.dot(D.transpose()) + z = spsolve(Z, w*y) + w = p * (y > z) + (1-p) * (y < z) + return z + +def get_lines( fname ): + if not os.path.exists( fname ): + return [] + fh = open( fname, 'r') + lines = fh.read().splitlines() + fh.close() + return lines + +def get_name( eic ): + return f"{eic.name},{eic.mzs[0]:.4f}" + + +def select_compounds( rawnames ): + holder = defaultdict( list ) + for rn in rawnames: + name,_ = rn.split(',') + holder[ name ].append( rn ) + + kept = list() + for clist in holder.values(): + if len(clist ) > 1: + kept.extend( clist ) + + return set(kept) + +def has_baseline( eic ): + its, rts, mzs = eic.its, eic.rts, eic.mzs + mask = its > 0 + its, rts, mzs = its[ mask ], rts[ mask ], mzs[ mask ] + max_idx = np.argmax( its ) + left_slope, right_slope = [], [] + for offset in range( 10 ): # maybe don't hardcode this? + left, right = offset, len( its )-1-offset + + if left + 1 < max_idx: + left_slope.append(abs( + (its[left+1]-its[left])/ + (rts[left+1]-rts[left]) + )) + + if right - 1 > max_idx: + right_slope.append(abs( + (its[right]-its[right-1])/ + (rts[right]-rts[right-1]) + )) + + return np.mean(np.array( left_slope )), np.mean(np.array( right_slope )) + + +def gauss( x, a, b, c, d ): + return a*np.exp(-((x-b)**2)/c) + d + +def idealize_eic( eic ): + its, rts, mzs = eic.its, eic.rts, eic.mzs + highest = np.max( its ) + mask = (its > 0) + rts = rts[mask] + its = its[mask] + mzs = mzs[mask] + max_val = np.max( its ) + max_it = np.argmax( its ) + a = its[ max_it ] + b = rts[ max_it ] + + params = curve_fit( gauss, rts, its, p0=(a,b,1,np.min(its)) )[0] + + if params[-1] > 0.01*max_val: + old = params[-1] + params[-1] = 0.01*max_val + params[0] += abs(old-params[-1]) + + if params[-1] < 0: + params[-1] = 0.01*max_val + + return EIC( name=eic.name, + its=gauss( eic.rts, *params ), + mzs=eic.mzs, rts=eic.rts) + +def main( ): + + kept_eics = select_compounds(get_lines( 'keep.txt' )) + eics = pickle.load(open('eics.pickle', 'rb')) + filt = list(filter(lambda eic: get_name( eic ) in kept_eics, eics)) + filt = sorted( filt, key=lambda ee: ee.name ) + # ok so we need to identify possible issues with this stuff. + # basically the main problem is there isn't any baseline + # for a lot of these + idealized = [] + for f in filt: + try: + idealized.append( idealize_eic( f )) + except: + pass + pickle.dump(idealized, open('eics-ideal.pickle', 'wb')) +if __name__ == '__main__': + main( ) diff --git a/Synthetic data creation scripts/vimms_data_generation/validate b/Synthetic data creation scripts/vimms_data_generation/validate new file mode 100644 index 00000000..506a0988 --- /dev/null +++ b/Synthetic data creation scripts/vimms_data_generation/validate @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + + +import pickle +import numpy as np + +def cv( vals ): + return np.std( vals )/np.mean( vals ) + + + +mm = pickle.load(open('mult_mapper.pickle','rb')) + +good = 0 + +for vv in mm.values(): + g1, g2 = vv[0:10], vv[10:] + good += (cv(g1)<0.20)and(cv(g2)<0.20) + + +print( good ) -- GitLab