diff --git a/.github/workflows/build-push-notebooks.yaml b/.github/workflows/build-push-notebooks.yaml index 337677c55..cc99d33e3 100644 --- a/.github/workflows/build-push-notebooks.yaml +++ b/.github/workflows/build-push-notebooks.yaml @@ -1,9 +1,9 @@ name: Building latest jupyter Notebooks and push to master+ipynb on: - schedule: + #schedule: # Trigger 5:30 UTC - - cron: '30 5 * * *' + # - cron: '30 5 * * *' push: branches: [ master, merge-workflows ] workflow_dispatch: diff --git a/.github/workflows/code-blocks-linting.yaml b/.github/workflows/code-blocks-linting.yaml index afcb568f8..c206952b8 100644 --- a/.github/workflows/code-blocks-linting.yaml +++ b/.github/workflows/code-blocks-linting.yaml @@ -16,7 +16,7 @@ jobs: - name: Get changed files id: changed-files - uses: tj-actions/changed-files@v35 + uses: tj-actions/changed-files@v41 with: dir_names_max_depth: 0 files: | diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index 892695dd8..bb98480d9 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -19,7 +19,7 @@ jobs: - name: Get changed files id: changed-files - uses: tj-actions/changed-files@v35 + uses: tj-actions/changed-files@v41 with: dir_names_max_depth: 0 files: | diff --git a/README.md b/README.md index 9c6a1299c..0467c9d07 100644 --- a/README.md +++ b/README.md @@ -34,4 +34,4 @@ Are created by CI and stored in master+ipynb to not clutter the master branch. Binder integration ============= -Binder uses the Jupyter Notebooks in master+ipynb. The conda environment is described in environment.yml, the post-build event installs the nightly pyopenms wheel. Currently, only environment.yml is used by binder. Note: You can test a branch "jpfeuffer-patch-6" using https://mybinder.org/v2/gh/OpenMS/pyopenms-docs/jpfeuffer-patch-6 +Binder uses the Jupyter Notebooks in master+ipynb. The conda environment is described in environment.yml, the post-build event installs the nightly pyopenms wheel. Currently, only environment.yml is used by binder. Note: You can test a branch "jpfeuffer-patch-6" using https://notebooks.gesis.org/binder/v2/gh/OpenMS/pyopenms-docs/jpfeuffer-patch-6 diff --git a/docs/requirements.txt b/docs/requirements.txt index 098304e68..6af827451 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ # Defining the exact version will make sure things don't break sphinx==6.1.0 pydata_sphinx_theme -readthedocs-sphinx-search==0.3.1 +readthedocs-sphinx-search==0.3.2 sphinx-copybutton==0.5.1 sphinx-hoverxref sphinx-remove-toctrees diff --git a/docs/source/_templates/navbar-run-binder.html b/docs/source/_templates/navbar-run-binder.html index 1aed67eea..55c4ea96e 100644 --- a/docs/source/_templates/navbar-run-binder.html +++ b/docs/source/_templates/navbar-run-binder.html @@ -1,6 +1,6 @@ \ No newline at end of file diff --git a/docs/source/community/build_from_source.rst b/docs/source/community/build_from_source.rst index d4cfa62f3..0da77310c 100644 --- a/docs/source/community/build_from_source.rst +++ b/docs/source/community/build_from_source.rst @@ -35,22 +35,33 @@ Depending on your systems setup, it may make sense to do this inside a virtual e virtualenv pyopenms_venv source pyopenms_venv/bin/activate - -Next, configure OpenMS with pyOpenMS: execute ``cmake`` as usual, but with -parameters ``DPYOPENMS=ON``. Also, if using virtualenv or using a specific -Python version, add ``-DPYTHON_EXECUTABLE:FILEPATH=/path/to/python`` to ensure +Next, we will configure the CMake-based OpenMS build system +to enable the pyOpenMS target with the configuration option ``-DPYOPENMS=ON``. +If your are using virtualenv or a specific Python version, +add ``-DPYTHON_EXECUTABLE:FILEPATH=/path/to/python`` to ensure that the correct Python executable is used. Compiling pyOpenMS can use a lot of memory and take some time, however you can reduce the memory consumption by breaking up the compilation into multiple units and compiling in parallel, for example ``-DPY_NUM_THREADS=2 -DPY_NUM_MODULES=4`` will build 4 modules with 2 -threads. You can then configure pyOpenMS: +threads. You can now configure pyOpenMS (inside your build folder) with: .. code-block:: bash cmake -DPYOPENMS=ON - make pyopenms + + +Remember, that you can pass the other options as described above to the first +command by adding ``-DOPTION=VALUE`` statements if you need them. + +Now build pyOpenMS (now there should be pyOpenMS specific build targets). +If you are still inside your build folder, you can use "." as the build +folder parameter. + +.. code-block:: bash + + cmake --build $YOURBUILDFOLDER --target pyopenms --config Release + -Build pyOpenMS (now there should be pyOpenMS specific build targets). Afterwards, test that all went well by running the tests: .. code-block:: bash diff --git a/docs/source/user_guide/adduct_detection.rst b/docs/source/user_guide/adduct_detection.rst new file mode 100644 index 000000000..6c47c8266 --- /dev/null +++ b/docs/source/user_guide/adduct_detection.rst @@ -0,0 +1,86 @@ +Adduct Detection +================ + +In mass spectrometry it is crucial to ionize analytes prior to detection, because they are accelerated and manipulated in electric fields, allowing their separation based on mass-to-charge ratio. +This happens by addition of protons in positive mode or loss of protons in negative mode. Other ions present in the buffer solution can ionize the analyte as well, e.g. sodium, potassium or formic acid. +Depending on the size and chemical compsition, multiple adducts can bind leading to multiple charges on the analyte. In metabolomics with smaller analytes the number of charges is typically low with one or two, whereas in proteomics the number of charges is much higher. +Furthermore, analytes can loose functional groups during ionization, e.g. a neutral water loss. +Since the ionization happens after liquid chromatography, different adducts for an analyte have similar retention times. + +.. image:: img/adduct_detection.png + +In pyOpenMS, :py:class:`~.MetaboliteFeatureDeconvolution` takes a :term:`feature map` as input adding adduct information as additional meta values. Features belonging to an adduct group will be stored in a :term:`consensus map`. The most important parameters are explained in the comments. + +| **Input file generation:** +| The input :term:`feature map` can be obtained using a `feature finder algorithm `_. + +| **Suggested follow up step:** +| The resulting feature map can be exported to a pandas DataFrame with adduct information from the *dc_charge_adducts* feature meta values. +| Multiple feature maps can be `combined using the feature linking algorithms `_. Each consensus feature will get a new meta value *best ion* based on the most common annotated adduct within the consensus feature group. + +.. code-block:: python + + from urllib.request import urlretrieve + import pyopenms as poms + + # get example data file with metabolmics feature map + gh = "https://raw.githubusercontent.com/OpenMS/pyopenms-docs/master" + urlretrieve(gh + "/src/data/MetaboliteFeatureDeconvolution_input.featureXML", "example.featureXML") + + # open example input feature map + feature_map = poms.FeatureMap() + poms.FeatureXMLFile().load("example.featureXML", feature_map) + + # initialize MetaboliteFeatureDeconvolution + mfd = poms.MetaboliteFeatureDeconvolution() + + # get default parameters + params = mfd.getDefaults() + # update/explain most important parameters + + # adducts to expect: elements, charge and probability separated by colon + # the total probability of all charged adducts needs to be 1 + # e.g. positive mode: + # proton dduct "H:+:0.6", sodium adduct "Na:+:0.4" and neutral water loss "H-2O-1:0:0.2" + # e.g. negative mode: + # with neutral formic acid adduct: "H-1:-:1", "CH2O2:0:0.5" + # multiples don't need to be specified separately: + # e.g. [M+H2]2+ and double water loss will be detected as well! + # optionally, retention time shifts caused by adducts can be added + # e.g. a formic acid adduct causes 3 seconds earlier elution "CH2O2:0:0.5:-3" + params.setValue("potential_adducts", ["H:+:0.6", "Na:+:0.4", "H-2O-1:0:0.2"]) + + # expected charge range + # e.g. for positive mode metabolomics: + # minimum of 1, maximum of 3, maximum charge span for a single feature 3 + # for negative mode: + # charge_min = -3, charge_max = -1 + params.setValue("charge_min", 1, "Minimal possible charge") + params.setValue("charge_max", 3, "Maximal possible charge") + params.setValue("charge_span_max", 3) + + # maximum RT difference between any two features for grouping + # maximum RT difference between between two co-features, after adduct shifts have been accounted for + # (if you do not have any adduct shifts, this value should be equal to "retention_max_diff") + params.setValue("retention_max_diff", 3.0) + params.setValue("retention_max_diff_local", 3.0) + + # set updated paramters object + mfd.setParameters(params) + + # result feature map: will store features with adduct information + feature_map_MFD = poms.FeatureMap() + # result consensus map: will store grouped features belonging to a charge group + groups = poms.ConsensusMap() + # result consensus map: will store paired features connected by an edge + edges = poms.ConsensusMap() + + # compute adducts + mfd.compute(feature_map, feature_map_MFD, groups, edges) + + # export feature map as pandas DataFrame and append adduct information + df = feature_map_MFD.get_df(export_peptide_identifications=False) + df["adduct"] = [f.getMetaValue("dc_charge_adducts") for f in feature_map_MFD] + + # display data + print(df.head()) \ No newline at end of file diff --git a/docs/source/user_guide/charge_isotope_deconvolution.rst b/docs/source/user_guide/charge_isotope_deconvolution.rst index ee4a73bcb..9bf7838c3 100644 --- a/docs/source/user_guide/charge_isotope_deconvolution.rst +++ b/docs/source/user_guide/charge_isotope_deconvolution.rst @@ -4,7 +4,7 @@ Charge and Isotope Deconvolution A single mass spectrum contains measurements of one or more analytes and the m/z values recorded for these analytes. Most analytes produce multiple signals in the mass spectrometer, due to the natural abundance of carbon :math:`13` (naturally -occurring at ca. :math:`1%` frequency) and the large amount of carbon atoms in most +occurring at ca. :math:`1\%` frequency) and the large amount of carbon atoms in most organic molecules, most analytes produce a so-called isotopic pattern with a monoisotopic peak (all carbon are :chem:`^{12}C`) and a first isotopic peak (exactly one carbon atom is a :chem:`^{13}C`), a second isotopic peak (exactly two atoms are :chem:`^{13}C`) etc. diff --git a/docs/source/user_guide/chemistry.rst b/docs/source/user_guide/chemistry.rst index 810058997..e839e8fd0 100644 --- a/docs/source/user_guide/chemistry.rst +++ b/docs/source/user_guide/chemistry.rst @@ -137,7 +137,7 @@ The isotope distribution of oxygen and sulfur can be displayed with the followin from matplotlib import pyplot as plt - # very simple overlappping correction of annotations + # very simple overlapping correction of annotations def adjustText(x1, y1, x2, y2): if y1 > y2: plt.annotate( diff --git a/docs/source/user_guide/export_files_GNPS.rst b/docs/source/user_guide/export_files_GNPS.rst index 3548e98d0..100e6f212 100644 --- a/docs/source/user_guide/export_files_GNPS.rst +++ b/docs/source/user_guide/export_files_GNPS.rst @@ -5,7 +5,7 @@ With pyOpenMS you can automatically generate all files needed for GNPS Feature-B Ion Identity Molecular Networking (IIMN). Pre-requisites are your input :term:`mzML` files and a :py:class:`~.ConsensusMap`, generated by an -`untargeted metabolomics pre-processing workflow `_. +`untargeted metabolomics pre-processing workflow `_. Ensure that :term:`MS2` data has been mapped to the :py:class:`~.FeatureMap` objects with :py:class:`~.IDMapper`. For IIMN adduct detection must have been performed on the :py:class:`~.FeatureMap` objects during pre-processing with :py:class:`~.MetaboliteFeatureDeconvolution`. diff --git a/docs/source/user_guide/feature_detection.rst b/docs/source/user_guide/feature_detection.rst index f2ce84a24..87208d9b6 100644 --- a/docs/source/user_guide/feature_detection.rst +++ b/docs/source/user_guide/feature_detection.rst @@ -13,7 +13,6 @@ FeatureFinders are available in pyOpenMS: - :py:class:`~.FeatureFinderMultiplexAlgorithm` (e.g., :term:`SILAC`, Dimethyl labeling, (and label-free), identification free feature detection of peptides) - :py:class:`~.FeatureFinderAlgorithmPicked` (Label-free, identification free feature detection of peptides) - :py:class:`~.FeatureFinderIdentificationAlgorithm` (Label-free identification-guided feature detection of peptides) - - :py:class:`~.FeatureFinderAlgorithmIsotopeWavelet` (old instruments) - :py:class:`~.FeatureFindingMetabo` (Label-free, identification free feature detection of metabolites) - :py:class:`~.FeatureFinderAlgorithmMetaboIdent` (Label-free, identification guided feature detection of metabolites) diff --git a/docs/source/user_guide/fragment_spectrum_generation.rst b/docs/source/user_guide/fragment_spectrum_generation.rst index 333e6cb42..4da932d3b 100644 --- a/docs/source/user_guide/fragment_spectrum_generation.rst +++ b/docs/source/user_guide/fragment_spectrum_generation.rst @@ -52,7 +52,6 @@ which you could plot with :py:meth:`pyopenms.plotting.plot_spectrum`, automatica import matplotlib.pyplot as plt from pyopenms.plotting import plot_spectrum - import matplotlib.pyplot as plt plot_spectrum(spec1) plt.show() @@ -122,10 +121,6 @@ which you can again visualize with: .. code-block:: python :linenos: - import matplotlib.pyplot as plt - from pyopenms.plotting import plot_spectrum - import matplotlib.pyplot as plt - plot_spectrum(spec2, annotate_ions=False) plt.show() diff --git a/docs/source/user_guide/img/DFPIANGER_theo.png b/docs/source/user_guide/img/DFPIANGER_theo.png index 0a6af4908..a733a0089 100644 Binary files a/docs/source/user_guide/img/DFPIANGER_theo.png and b/docs/source/user_guide/img/DFPIANGER_theo.png differ diff --git a/docs/source/user_guide/img/DFPIANGER_theo_full.png b/docs/source/user_guide/img/DFPIANGER_theo_full.png index a102ea614..6658c5232 100644 Binary files a/docs/source/user_guide/img/DFPIANGER_theo_full.png and b/docs/source/user_guide/img/DFPIANGER_theo_full.png differ diff --git a/docs/source/user_guide/img/DFPIANGER_theo_full_mirror.png b/docs/source/user_guide/img/DFPIANGER_theo_full_mirror.png deleted file mode 100644 index e681f3f0e..000000000 Binary files a/docs/source/user_guide/img/DFPIANGER_theo_full_mirror.png and /dev/null differ diff --git a/docs/source/user_guide/img/adduct_detection.png b/docs/source/user_guide/img/adduct_detection.png new file mode 100644 index 000000000..3e24b3026 Binary files /dev/null and b/docs/source/user_guide/img/adduct_detection.png differ diff --git a/docs/source/user_guide/img/after_normalization.png b/docs/source/user_guide/img/after_normalization.png new file mode 100644 index 000000000..f27b5c093 Binary files /dev/null and b/docs/source/user_guide/img/after_normalization.png differ diff --git a/docs/source/user_guide/img/after_normalization_TIC.png b/docs/source/user_guide/img/after_normalization_TIC.png new file mode 100644 index 000000000..2ce5a3941 Binary files /dev/null and b/docs/source/user_guide/img/after_normalization_TIC.png differ diff --git a/docs/source/user_guide/img/before_normalization.png b/docs/source/user_guide/img/before_normalization.png new file mode 100644 index 000000000..e2aa07b0f Binary files /dev/null and b/docs/source/user_guide/img/before_normalization.png differ diff --git a/docs/source/user_guide/img/nlargest.png b/docs/source/user_guide/img/nlargest.png new file mode 100644 index 000000000..3d5e77f9f Binary files /dev/null and b/docs/source/user_guide/img/nlargest.png differ diff --git a/docs/source/user_guide/img/spec_alignment_1.png b/docs/source/user_guide/img/spec_alignment_1.png index df4cb564f..79e43a561 100644 Binary files a/docs/source/user_guide/img/spec_alignment_1.png and b/docs/source/user_guide/img/spec_alignment_1.png differ diff --git a/docs/source/user_guide/img/spec_alignment_2.png b/docs/source/user_guide/img/spec_alignment_2.png index ea768668c..9d1763b7b 100644 Binary files a/docs/source/user_guide/img/spec_alignment_2.png and b/docs/source/user_guide/img/spec_alignment_2.png differ diff --git a/docs/source/user_guide/img/spec_averaging.png b/docs/source/user_guide/img/spec_averaging.png new file mode 100644 index 000000000..f17613d0b Binary files /dev/null and b/docs/source/user_guide/img/spec_averaging.png differ diff --git a/docs/source/user_guide/img/spec_merging_1.png b/docs/source/user_guide/img/spec_merging_1.png new file mode 100644 index 000000000..aa449cace Binary files /dev/null and b/docs/source/user_guide/img/spec_merging_1.png differ diff --git a/docs/source/user_guide/img/spec_merging_2.png b/docs/source/user_guide/img/spec_merging_2.png new file mode 100644 index 000000000..0d3ef7064 Binary files /dev/null and b/docs/source/user_guide/img/spec_merging_2.png differ diff --git a/docs/source/user_guide/img/spec_merging_3.png b/docs/source/user_guide/img/spec_merging_3.png new file mode 100644 index 000000000..5551586b3 Binary files /dev/null and b/docs/source/user_guide/img/spec_merging_3.png differ diff --git a/docs/source/user_guide/img/threshold_mower.png b/docs/source/user_guide/img/threshold_mower.png new file mode 100644 index 000000000..bd04ec81f Binary files /dev/null and b/docs/source/user_guide/img/threshold_mower.png differ diff --git a/docs/source/user_guide/img/window_mower.png b/docs/source/user_guide/img/window_mower.png new file mode 100644 index 000000000..9d008e168 Binary files /dev/null and b/docs/source/user_guide/img/window_mower.png differ diff --git a/docs/source/user_guide/index.rst b/docs/source/user_guide/index.rst index e39fb9d27..cab40a9ae 100644 --- a/docs/source/user_guide/index.rst +++ b/docs/source/user_guide/index.rst @@ -43,9 +43,11 @@ headings and structure. smoothing centroiding spectrum_normalization + spectrum_merging charge_isotope_deconvolution feature_detection map_alignment + adduct_detection feature_linking peptide_search chromatographic_analysis diff --git a/docs/source/user_guide/interactive_plots.rst b/docs/source/user_guide/interactive_plots.rst index 7da133f8f..428981568 100644 --- a/docs/source/user_guide/interactive_plots.rst +++ b/docs/source/user_guide/interactive_plots.rst @@ -94,7 +94,7 @@ Result: With this you can also easily create whole dashboards like the one -hosted `here `_ on a Binder instance. +hosted `here `_ on a Binder instance. If you are reading/executing this on Binder already, execute the next cell to get a link to your current instance. .. code-block:: python diff --git a/docs/source/user_guide/ms_data.rst b/docs/source/user_guide/ms_data.rst index 9c365d100..2daa887e3 100644 --- a/docs/source/user_guide/ms_data.rst +++ b/docs/source/user_guide/ms_data.rst @@ -188,7 +188,6 @@ We can also visualize our mass spectrum from before using the :py:func:`~.plot_s import matplotlib.pyplot as plt from pyopenms.plotting import plot_spectrum - import matplotlib.pyplot as plt plot_spectrum(spectrum) plt.show() @@ -639,8 +638,8 @@ But first, we will load some test data: oms.MzMLFile().load("test.mzML", inp) -Filtering Mass Spectra by :term`MS` Level -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Filtering Mass Spectra by MS Level +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We will filter the data from ``test.mzML`` file by only retaining mass spectra that are not :term:`MS1` spectra @@ -707,4 +706,111 @@ Similarly we could only retain peaks above a certain intensity or keep only the top N peaks in each mass spectrum. For more advanced filtering tasks pyOpenMS provides special algorithm classes. -We will take a closer look at some of them in the algorithm section. +We will take a closer look at some of them in the next section. + + +Filtering Mass Spectra with TOPP Tools +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We can also use predefined TOPP tools to filter our data. First we need to load in the data: + +.. code-block:: python + :linenos: + + import matplotlib.pyplot as plt + from pyopenms.plotting import plot_spectrum, mirror_plot_spectrum + + gh = "https://raw.githubusercontent.com/OpenMS/pyopenms-docs/master" + urlretrieve( + gh + "/src/data/YIC(Carbamidomethyl)DNQDTISSK.mzML", "observed.mzML" + ) + + exp = oms.MSExperiment() + # Load mzML file and obtain spectrum for peptide YIC(Carbamidomethyl)DNQDTISSK + oms.MzMLFile().load("observed.mzML", exp) + + # Get first spectrum + spectra = exp.getSpectra() + observed_spectrum = spectra[0] + +The :py:class:`~.WindowMower` tool can be used to remove peaks in a sliding or jumping window. The window size, +number of highest peaks to keep and move type can be set with a :py:class:`~.Param` object + +.. code-block:: python + :linenos: + + from copy import deepcopy + + window_mower_filter = oms.WindowMower() + + # Copy the original spectrum + mowed_spectrum = deepcopy(observed_spectrum) + + # Set parameters + params = oms.Param() + # Defines the m/z range of the sliding window + params.setValue("windowsize", 100.0, "") + # Defines the number of highest peaks to keep in the sliding window + params.setValue("peakcount", 1, "") + # Defines the type of window movement: jump (window size steps) or slide (one peak steps) + params.setValue("movetype", "jump", "") + + # Apply window mowing + window_mower_filter.setParameters(params) + window_mower_filter.filterPeakSpectrum(mowed_spectrum) + + # Visualize the resulting data together with the original spectrum + mirror_plot_spectrum(observed_spectrum, mowed_spectrum) + plt.show() + +.. image:: img/window_mower.png + + +Noise can be easily removed with :py:class:`~.ThresholdMower` by setting a threshold value for the intensity of peaks +and cutting off everything below. + +.. code-block:: python + :linenos: + + # Copy spectrum + threshold_mower_spectrum = deepcopy(observed_spectrum) + + threshold_mower_filter = oms.ThresholdMower() + + # Set parameters + params = oms.Param() + params.setValue("threshold", 20.0, "") + + # Apply threshold mowing + threshold_mower_filter.setParameters(params) + threshold_mower_filter.filterPeakSpectrum(threshold_mower_spectrum) + + mirror_plot_spectrum(observed_spectrum, threshold_mower_spectrum) + plt.show() + +.. image:: img/threshold_mower.png + + +We can also use e.g. :py:class:`~.NLargest` to keep only the N highest peaks in a spectrum. + +.. code-block:: python + :linenos: + + # Copy spectrum + nlargest_spectrum = deepcopy(observed_spectrum) + + nlargest_filter = oms.NLargest() + + # Set parameters + params = oms.Param() + params.setValue("n", 4, "") + + # Apply N-Largest filter + nlargest_filter.setParameters(params) + nlargest_filter.filterPeakSpectrum(nlargest_spectrum) + + mirror_plot_spectrum(observed_spectrum, nlargest_spectrum) + plt.show() + # Two peaks are overlapping, so only three peaks are really visible in the plot + +.. image:: img/nlargest.png diff --git a/docs/source/user_guide/parameter_handling.rst b/docs/source/user_guide/parameter_handling.rst index 97f7e495e..fb0963790 100644 --- a/docs/source/user_guide/parameter_handling.rst +++ b/docs/source/user_guide/parameter_handling.rst @@ -52,13 +52,13 @@ The param object can be copy and merge in to other param object as .. code-block:: python :linenos: - # print the key and values pairs stored in a Param object + # print the key and value pairs stored in a Param object def printParamKeyAndValues(p): if p.size(): for i in p.keys(): print("Key:", i, "Value:", p[i]) else: - print("no data availabe") + print("no data available") new_p = oms.Param() @@ -76,7 +76,6 @@ The param object can be copy and merge in to other param object as print(" print the key and values pairs stored in a Param object p ") printParamKeyAndValues(p) - In param object the keys values can be remove by key_name or prefix as .. code-block:: python @@ -100,3 +99,30 @@ In param object the keys values can be remove by key_name or prefix as print("Keys and values after deleting all entries.") printParamKeyAndValues(new_p) # All keys of new_p deleted + +For the algorithms that inherit :py:class:`~.DefaultParamHandler`, the users can list all parameters along with their descriptions by using, for instance, the following simple function. + +.. code-block:: python + :linenos: + + # print all parameters + def printParams(p): + if p.size(): + for i in p.keys(): + print( + "Param:", i, "Value:", p[i], "Description:", p.getDescription(i) + ) + else: + print("no data available") + + # print all parameters in GaussFilter class + gf = oms.GaussFilter() + printParams(gf.getParameters()) + +.. code-block:: output + + Param: b'gaussian_width' Value: 0.2 Description: Use a gaussian filter width which has approximately the same width as your mass peaks (FWHM in m/z). + Param: b'ppm_tolerance' Value: 10.0 Description: Gaussian width, depending on the m/z position. + The higher the value, the wider the peak and therefore the wider the gaussian. + Param: b'use_ppm_tolerance' Value: false Description: If true, instead of the gaussian_width value, the ppm_tolerance is used. The gaussian is calculated in each step anew, so this is much slower. + Param: b'write_log_messages' Value: false Description: true: Warn if no signal was found by the Gauss filter algorithm. diff --git a/docs/source/user_guide/quantitative_data.rst b/docs/source/user_guide/quantitative_data.rst index 9fc425577..05c6ec273 100644 --- a/docs/source/user_guide/quantitative_data.rst +++ b/docs/source/user_guide/quantitative_data.rst @@ -5,8 +5,7 @@ features ************************** In OpenMS, information about quantitative data is stored in a so-called -:py:class:`~.Feature` which we have previously discussed `here -`_. Each +:py:class:`~.Feature`. Each :py:class:`~.Feature` represents a region in RT and m/z space use for quantitative analysis. diff --git a/docs/source/user_guide/query_msexperiment_massql.rst b/docs/source/user_guide/query_msexperiment_massql.rst index e72825b39..c43b007f7 100644 --- a/docs/source/user_guide/query_msexperiment_massql.rst +++ b/docs/source/user_guide/query_msexperiment_massql.rst @@ -1,5 +1,5 @@ -Query :py:class:`~.MSExperiment` with MassQL -============================================ +Query MSExperiment with MassQL +============================== MassQL is a powerful, SQL-like query language for mass spectrometry data. For further information visit the `MassQL documentation diff --git a/docs/source/user_guide/spectrum_alignment.rst b/docs/source/user_guide/spectrum_alignment.rst index 3ac8adf54..1969074c4 100644 --- a/docs/source/user_guide/spectrum_alignment.rst +++ b/docs/source/user_guide/spectrum_alignment.rst @@ -49,7 +49,6 @@ Now we can plot the observed and theoretical mass spectrum as a mirror plot: import matplotlib.pyplot as plt from pyopenms.plotting import mirror_plot_spectrum - import matplotlib.pyplot as plt mirror_plot_spectrum( observed_spectrum, @@ -123,15 +122,12 @@ The mirror plot can also be used to visualize the aligned mass spectrum: .. code-block:: python :linenos: - import matplotlib.pyplot as plt - from pyopenms.plotting import mirror_plot_spectrum - import matplotlib.pyplot as plt - + match_peaks_observed, match_peaks_theoretical = list(zip(*alignment)) mirror_plot_spectrum( observed_spectrum, theo_spectrum, - alignment=alignment, - spectrum_bottom_kws={"annotate_ions": False}, + spectrum_top_kws={"matched_peaks": match_peaks_theoretical}, + spectrum_bottom_kws={"annotate_ions": False, "matched_peaks": match_peaks_observed} ) plt.show() diff --git a/docs/source/user_guide/spectrum_merging.rst b/docs/source/user_guide/spectrum_merging.rst new file mode 100644 index 000000000..c82176d7d --- /dev/null +++ b/docs/source/user_guide/spectrum_merging.rst @@ -0,0 +1,348 @@ +Spectra Merge Algorithm +************************* + +OpenMS provides spectra merging and averaging algorithms in :py:class:`~.SpectraMerger` class. Spectra merging is to merge multiple related spectra into a single one - thus, often we end up with a reduced number of spectra. +For instance, MS1 spectra within a pre-defined retention time window or MS2 spectra from the same precursor ion. On the other hand, spectra averaging averages neighbouring spectra for each spectrum. +Thus, the number of spectra remains the same after spectra averaging. Both merging and averaging attempt to increase the quality of spectrum by increasing its signal to noise ratio. + +Spectra merging and averaging are implemented in SpectraMerger in pyOpenMS, which provides two merging (block wise and precursor method - see below) and two averaging methods (gaussian and tophat - see below). +For merging, we can use + +- mergeSpectraBlockWise +- mergeSpectraPrecursors + +and for averaging, we use + +- average + +with two different options: gaussian and tophat methods. :py:class:`~.SpectraMerger` inherits :py:class:`~.DefaultParamHandler`. Thus you could list the full set of parameters as described in `Parameter handling `_. + + +Loading the Raw Data +-------------------- + +Let's take a look at the different algorithms in the examples below. First we download a test MS/MS dataset. + +.. code-block:: python + :linenos: + + # retrieve data + from urllib.request import urlretrieve + import pyopenms as oms + import matplotlib.pyplot as plt + + gh = "https://raw.githubusercontent.com/OpenMS/pyopenms-docs/master" + urlretrieve(gh + "/src/data/small.mzML", "test.mzML") + + # load MS data and store as MSExperiment object + exp = oms.MSExperiment() + oms.MzMLFile().load("test.mzML", exp) + + + +Block wise spectra merging +-------------------------- +Our first example merges MS1 spectra block wise. + +.. code-block:: python + :linenos: + + spectra = exp.getSpectra() + + # Collecting only MS1 spectra + spectra_ms1 = [s for s in spectra if s.getMSLevel() == 1] + print(f"Number of MS1 spectra before merge are {len(spectra_ms1)}") + + # merges blocks of MS1 + merger = oms.SpectraMerger() + merger.mergeSpectraBlockWise(exp) + + # Get spectra from the updated (merged) experiment + spectraMerged = exp.getSpectra() + spectraMerged_ms1 = [s for s in spectraMerged if s.getMSLevel() == 1] + print(f"Number of MS1 spectra after merge are {len(spectraMerged_ms1)}") + # store merged spectra in the disk + oms.MzMLFile().store("blockwiseMerged.mzML", exp) + + # Setting up subplots for five original spectra and merged spectrum + fig, axs = plt.subplots(6) + fig.set_figheight(8) + plt.subplots_adjust(hspace=1) + + for i in range(0, 6): + s = spectra_ms1[i] if i < 5 else spectraMerged_ms1[0] + axs[i].plot(s.get_peaks()[0], s.get_peaks()[1], linewidth=0.2) + axs[i].set_yscale("log") + axs[i].set_ylim(1e3, 1e7) + axs[i].set_xlim(360, 1000) + axs[i].title.set_text( + "Input MS1 spectrum " + str(i + 1) if i < 5 else "Merged MS1 spectrum" + ) + plt.show() + + +.. code-block:: output + + Number of MS1 spectra before merge are 183 + Number of MS1 spectra after merge are 37 + Cluster sizes: + size 3: 1x + size 5: 36x + Number of merged peaks: 87177/360394 (24.19 %) of blocked spectra + + +.. image:: img/spec_merging_1.png + :align: center + :alt: Blockwise merging (of 5 MS1 scans) + +Above example clearly demonstrates the benefit of spectra merging. The upper rows show the input spectra and the bottom the merged one. The merged spectrum (bottom) has far more signal peaks of higher intensities than the input spectra. + +By default, the method ``mergeSpectraBlockWise`` of :py:class:`~.SpectraMerger` merges 5 consecutive MS1 spectra into a block. +The block size could be adjusted by using ``block_method:rt_block_size`` parameter as follow: + +.. code-block:: python + :linenos: + + + # again load MS data and store as MSExperiment object + exp = oms.MSExperiment() + oms.MzMLFile().load("test.mzML", exp) + + # again load MS data and store as MSExperiment object + exp = oms.MSExperiment() + oms.MzMLFile().load("test.mzML", exp) + + # adjust block size to 10 spectra and merge + merger = oms.SpectraMerger() + param = merger.getParameters() + param.setValue("block_method:rt_block_size", 10) + merger.setParameters(param) + merger.mergeSpectraBlockWise(exp) + + spectraMerged = exp.getSpectra() + spectraMerged_ms1_10scans = [s for s in spectraMerged if s.getMSLevel() == 1] + + # store merged spectra in the disk + oms.MzMLFile().store("blockwiseMerged_10scans.mzML", exp) + + fig, axs = plt.subplots(2) + fig.set_figheight(4) + plt.subplots_adjust(hspace=1) + + for i in range(0, 2): + s = spectraMerged_ms1_10scans[0] if i == 0 else spectraMerged_ms1[0] + axs[i].plot(s.get_peaks()[0], s.get_peaks()[1], linewidth=0.2) + axs[i].set_yscale("log") + axs[i].set_ylim(1e3, 1e7) + axs[i].set_xlim(360, 1000) + axs[i].title.set_text( + "Merged MS1 spectrum with 10 scans" + if i == 0 + else "Merged MS1 spectrum with 5 scans" + ) + plt.show() + + +.. code-block:: output + + Number of MS1 spectra after merge are 19 + Cluster sizes: + size 3: 1x + size 10: 18x + Number of merged peaks: 117793/360394 (32.68 %) of blocked spectra + 72 spectra and 1 chromatograms stored. + + +.. image:: img/spec_merging_2.png + :align: center + :alt: Blockwise merging 10 scans vs. 5 scans + +As shown in the above figure, clearer signal peaks are obtained with 10 MS1 scans being merged than 5 MS1 scans. Note that the y-axis is in log scale. But if too many scans are merged, +spectra containing too different sets of molecules would be merged, yielding a poor quality spectrum. The users may want to try a few different parameters to produce spectra of optimal quality. + +MS2 spectra merging with precursor method +----------------------------------------- +Next we perform MS2 spectra merging with precursor method by using the ``mergeSpectraPrecursors`` method. With this method, the MS2 spectra from the same precursor m/z (subject to tolerance) are merged. + +.. code-block:: python + :linenos: + + + # load MS data and store as MSExperiment object + exp = oms.MSExperiment() + oms.MzMLFile().load("test.mzML", exp) + + spectra = exp.getSpectra() + + # spectra with ms_level = 2 + spectra_ms2 = [s for s in spectra if s.getMSLevel() == 2] + print(f"Number of MS2 spectra before merge are {len(spectra_ms2)}") + + # merge spectra with similar precursors + merger = oms.SpectraMerger() + merger.mergeSpectraPrecursors(exp) + + spectraMerged = exp.getSpectra() + spectraMerged_ms2 = [s for s in spectraMerged if s.getMSLevel() == 2] + print(f"Number of MS2 spectra after merge are {len(spectraMerged_ms2)}") + + +.. code-block:: output + + Number of MS2 spectra before merge are 53 + Number of MS2 spectra after merge are 53 + Cluster sizes: + Number of merged peaks: 0/0 (nan %) of blocked spectra + +In the above example, no MS2 spectra have been merged because no MS2 spectra had the same precursor m/z values (subject to tolerance) within retention time window. +By default, the retention time window size is 5.0 seconds and the precursor m/z tolerance is 1e-4Th. If you opens the test.mzML file, you can see a few MS2 spectra (e.g., scan numbers 2077 and 2099) +have quite close precursor m/z values (both have precursor m/z of 432.902Th), but they are apart from each other by about 10 seconds. We adjust both m/z tolerance and retention time so such MS2 spectra are merged together with ``precursor_method:mz_tolerance`` and ``precursor_method:rt_tolerance`` parameters. + +.. code-block:: python + :linenos: + + # adjust mz and rt tolerances for MS2 spectra grouping for merging + param = merger.getParameters() + param.setValue("precursor_method:rt_tolerance", 10.0) + param.setValue("precursor_method:mz_tolerance", 1e-3) + merger.setParameters(param) + merger.mergeSpectraPrecursors(exp) + + # now rerun precursor method merging of MS2 spectra + spectraMerged = exp.getSpectra() + spectraMerged_ms2 = [s for s in spectraMerged if s.getMSLevel() == 2] + print(f"Number of MS2 spectra after merge are {len(spectraMerged_ms2)}") + + # store modified data + oms.MzMLFile().store("precursorMethodMerged.mzML", exp) + +.. code-block:: output + + Number of MS2 spectra after merge are 45 + Cluster sizes: + size 2: 8x + Number of merged peaks: 488/2262 (21.57 %) of blocked spectra + +To check which MS2 spectra are merged together, one can print out the native IDs of the spectra. +The native ID of each merged spectrum contains all native IDs of the spectra being merged (comma separated) - this also holds for block wise merging method. + +.. code-block:: python + :linenos: + + # check which input MS2 spectra were merged + merged_spectra = dict() + for index, s in enumerate(spectraMerged_ms2): + native_IDs = s.getNativeID().split(",") + if len(native_IDs) > 1: # spectrum is merged + print(native_IDs) + merged_specs = [] + for native_ID in native_IDs: + for s2 in spectra_ms2: # original spectra + if native_ID == s2.getNativeID(): + merged_specs.append(s2) + break + merged_spectra[index] = merged_specs + +.. code-block:: output + + ['controllerType=0 controllerNumber=1 scan=1986', 'controllerType=0 controllerNumber=1 scan=2010'] + ['controllerType=0 controllerNumber=1 scan=1991', 'controllerType=0 controllerNumber=1 scan=2015'] + ['controllerType=0 controllerNumber=1 scan=1992', 'controllerType=0 controllerNumber=1 scan=2014'] + ['controllerType=0 controllerNumber=1 scan=2026', 'controllerType=0 controllerNumber=1 scan=2050'] + ['controllerType=0 controllerNumber=1 scan=2037', 'controllerType=0 controllerNumber=1 scan=2059'] + ['controllerType=0 controllerNumber=1 scan=2062', 'controllerType=0 controllerNumber=1 scan=2088'] + ['controllerType=0 controllerNumber=1 scan=2077', 'controllerType=0 controllerNumber=1 scan=2099'] + ['controllerType=0 controllerNumber=1 scan=2084', 'controllerType=0 controllerNumber=1 scan=2107'] + +We can confirm that scans 2077 and 2099 have been merged. In addition, we had a few more pairs of MS2 spectra that were merged. We also plot the input and merged spectra below. + +.. code-block:: python + :linenos: + + # plot the merged and merging MS2 spectra + + fig, axs = plt.subplots(3, min(4, len(merged_spectra))) + fig.set_figheight(7) + fig.set_figwidth(14) + plt.subplots_adjust(hspace=1) + + for index, item in enumerate(merged_spectra.items()): + if index == 4: # show 4 examples + break + specs = item[1] + for i in range(0, 3): + s = specs[i] if i < 2 else spectraMerged_ms2[item[0]] + axs[i, index].bar(s.get_peaks()[0], s.get_peaks()[1], width=1) + axs[i, index].set_yscale("log") + axs[i, index].set_ylim(1e3, 1e5) + axs[i, index].set_xlim(0, 1200) + axs[i, index].title.set_text( + "Input MS2 spectrum" if i < 2 else "Merged MS2 spectrum" + ) + plt.show() + + +.. image:: img/spec_merging_3.png + :align: center + :alt: Precursor method merging + +Four examples of MS2 spectra before and after merging are provided above. Each column shows an example. The upper rows show the input spectra and the bottom the merged one. The input MS2 spectra selected by the precursor method show quite similar peak distributions, indicating they are indeed from the same molecule ions. +Moreover, as in the above block wise merging, we can check that a merged MS2 spectrum has more peaks than input spectra, possibly containing more complete fragmentation ion masses. + +Spectra averaging : gaussian and top hat methods +------------------------------------------------ + +:py:class:`~.SpectraMerger` presents a method ``average`` to average peak intensities over neighbouring spectra for a given spectrum. +As mentioned above, apart from spectra merging, the number of spectra after averaging does not change since it is carried out for each individual input spectrum. +The two averaging methods (``gaussian`` or ``tophat``) determine how neighbouring spectra are collected and how weights for the averaging are determined. +The ``gaussian`` method performs weighted average over the neighbouring spectra with weights having the shape of gaussian shape (i.e., sharply decreasing from the center). +On the other hand, the ``tophat`` method, as the name implies, performs a simple averaging over the neighbouring spectra. Below we perform ``gaussian`` averaging method. + + +.. code-block:: python + :linenos: + + # load MS data and store as MSExperiment object + exp = oms.MSExperiment() + oms.MzMLFile().load("test.mzML", exp) + spectra = exp.getSpectra() + + # number of MS1 spectra before averaging + spectra_ms1 = [s for s in spectra if s.getMSLevel() == 1] + print(f"Number of MS1 spectra before averaging are {len(spectra_ms1)}") + + # average spectra with gaussian + merger = oms.SpectraMerger() + merger.average(exp, "gaussian") + spectraAveraged = exp.getSpectra() + + # number of MS1 spectra after averaging + spectraAveraged_ms1 = [s for s in spectraAveraged if s.getMSLevel() == 1] + print(f"Number of MS1 spectra after averaging are {len(spectraAveraged_ms1)}") + + fig, axs = plt.subplots(2) + fig.set_figheight(4) + plt.subplots_adjust(hspace=1) + + for i in range(0, 2): + s = spectra_ms1[0] if i == 0 else spectraAveraged_ms1[0] + axs[i].plot(s.get_peaks()[0], s.get_peaks()[1], linewidth=.2) + axs[i].set_yscale("log") + axs[i].set_ylim(5e2, 1e6) + axs[i].set_xlim(360, 600) + axs[i].title.set_text("Before averaging" if i == 0 else "After averaging") + plt.show() + + # store modified data + oms.MzMLFile().store("averagedData.mzML", exp) + +.. code-block:: output + + Number of MS1 spectra before averaging are 183 + Number of MS1 spectra after averaging are 183 + +.. image:: img/spec_averaging.png + :align: center + :alt: Averging + +After averaging has been applied, the the number of spectra does not change as we mentioned above. But the above plots show that the base line intensity has decreased significantly after averaging. The signal peaks are better separated in the averaged spectrum than in the original spectrum as well. \ No newline at end of file diff --git a/docs/source/user_guide/spectrum_normalization.rst b/docs/source/user_guide/spectrum_normalization.rst index b37cdeafa..35d0aacb8 100644 --- a/docs/source/user_guide/spectrum_normalization.rst +++ b/docs/source/user_guide/spectrum_normalization.rst @@ -1,9 +1,12 @@ Spectrum Normalization ====================== -Another very basic mass spectrum processing step is normalization by base peak intensity (the maximum intensity of a mass spectrum). +Normalization by base peak intensity is a fundamental processing step in mass spectrometry. This method scales the peak intensities in a spectrum such that the highest peak reaches a maximum value, typically set to one. This approach facilitates the comparison of different spectra by standardizing the intensity scale. -Let's first load the raw data. +Loading the Raw Data +-------------------- + +To begin, we need to load the mass spectrometry data. The following Python code demonstrates how to load a spectrum from an mzML file using the pyOpenMS library. .. code-block:: python :linenos: @@ -13,20 +16,22 @@ Let's first load the raw data. import matplotlib.pyplot as plt gh = "https://raw.githubusercontent.com/OpenMS/pyopenms-docs/master" - urlretrieve( - gh + "/src/data/peakpicker_tutorial_1_baseline_filtered.mzML", - "tutorial.mzML", - ) + urlretrieve(gh + "/src/data/peakpicker_tutorial_1_baseline_filtered.mzML", "tutorial.mzML") + exp = oms.MSExperiment() oms.MzMLFile().load("tutorial.mzML", exp) - plt.bar( - exp.getSpectrum(0).get_peaks()[0], - exp.getSpectrum(0).get_peaks()[1], - snap=False, - ) + plt.bar(exp.getSpectrum(0).get_peaks()[0], exp.getSpectrum(0).get_peaks()[1], snap=False) + plt.show() + +.. image:: img/before_normalization.png + :align: center + :alt: Raw spectrum before normalization -Now we apply the normalization. +Normalization Procedure +----------------------- + +After loading the data, the next step is to apply normalization. .. code-block:: python :linenos: @@ -37,13 +42,27 @@ Now we apply the normalization. normalizer.setParameters(param) normalizer.filterPeakMap(exp) - plt.bar( - exp.getSpectrum(0).get_peaks()[0], - exp.getSpectrum(0).get_peaks()[1], - snap=False, - ) + plt.bar(exp.getSpectrum(0).get_peaks()[0], exp.getSpectrum(0).get_peaks()[1], snap=False) + plt.show() + +.. image:: img/after_normalization.png + :align: center + :alt: Spectrum after normalization + +TIC Normalization +----------------- +Another approach to normalization is using the Total Ion Count (TIC). This method adjusts the intensities so that their total sum equals 1.0 in each mass spectrum. + +.. code-block:: python + :linenos: + + param.setValue("method", "to_TIC") + normalizer.setParameters(param) + normalizer.filterPeakMap(exp) + plt.bar(exp.getSpectrum(0).get_peaks()[0], exp.getSpectrum(0).get_peaks()[1], snap=False) + plt.show() -Another way of normalizing is by TIC (total ion count) of the mass spectrum, which scales intensities -so they add up to :math:`1.0` in each mass spectrum. -Try it out for yourself by setting: ``param.setValue("method", "to_TIC")``. +.. image:: img/after_normalization_TIC.png + :align: center + :alt: Spectrum after TIC normalization diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..fc5000fcc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,16 @@ +matplotlib +plotly +pandas +adjustText +scikit-learn +tabulate +requests +bokeh +datashader +holoviews +pyviz_comms +jupyter-server-proxy +massql +sphinx-hoverxref + +pyopenms diff --git a/src/data/MetaboliteFeatureDeconvolution_input.featureXML b/src/data/MetaboliteFeatureDeconvolution_input.featureXML new file mode 100644 index 000000000..630019c13 --- /dev/null +++ b/src/data/MetaboliteFeatureDeconvolution_input.featureXML @@ -0,0 +1,1266 @@ + + + + + + 544.846792703000006 + 209.153597773757895 + 1.684743e06 + 0.0 + 0.0 + 3.645548e-03 + 1 + + + + + + + + + + + + 499.604203856000027 + 279.093382674517045 + 1.407178e07 + 0.0 + 0.0 + 0.031899 + 1 + + + + + + + + + + + + 499.604203856000027 + 301.075332313651245 + 1.974032e06 + 0.0 + 0.0 + 4.390591e-03 + 1 + + + + + + + + + + + + 397.14206475200001 + 403.232951123850455 + 2.310259e06 + 0.0 + 0.0 + 5.177198e-03 + 1 + + + + + + + + + + + + 434.750724432000027 + 579.332351009664308 + 5.806919e05 + 0.0 + 0.0 + 1.434447e-03 + 1 + + + + + + + + + + + + 439.568155423999997 + 581.347893153353311 + 4.070358e06 + 0.0 + 0.0 + 9.988968e-03 + 1 + + + + + + + + + + + + 434.750724432000027 + 597.342943821151152 + 1.003383e06 + 0.0 + 0.0 + 2.402893e-03 + 1 + + + + + + + + + + + + 439.568155423999997 + 599.358466257417604 + 1.171156e07 + 0.0 + 0.0 + 0.02888 + 1 + + + + + + + + + + + + 397.14206475200001 + 615.353502582470469 + 1.191484e06 + 0.0 + 0.0 + 2.872852e-03 + 1 + + + + + + + + + + + + 434.750724432000027 + 615.353512356112219 + 9.867863e05 + 0.0 + 0.0 + 2.389552e-03 + 1 + + + + + + + + + + + + 439.568155423999997 + 617.368995627995332 + 1.376789e07 + 0.0 + 0.0 + 0.034601 + 1 + + + + + + + + + + + + 429.829101728000012 + 617.369144640029731 + 3.742081e05 + 0.0 + 0.0 + 8.885223e-04 + 1 + + + + + + + + + + + + 462.660851216000026 + 633.364079192483587 + 2.584255e05 + 0.0 + 0.0 + 6.056734e-04 + 1 + + + + + + + + + + + + 397.14206475200001 + 633.364218641984849 + 3.485948e06 + 0.0 + 0.0 + 8.770891e-03 + 1 + + + + + + + + + + + + 376.649976767999988 + 633.364234476017259 + 7.782676e05 + 0.0 + 0.0 + 1.927407e-03 + 1 + + + + + + + + + + + + 439.568155423999997 + 633.364238938177778 + 4.749519e05 + 0.0 + 0.0 + 1.157045e-03 + 1 + + + + + + + + + + + + 455.265223552999998 + 633.364258810205115 + 3.248659e05 + 0.0 + 0.0 + 7.788208e-04 + 1 + + + + + + + + + + + + 439.568155423999997 + 635.37966361303404 + 2.784853e07 + 0.0 + 0.0 + 0.070096 + 1 + + + + + + + + + + + + 429.829101728000012 + 635.379823148663377 + 6.975729e05 + 0.0 + 0.0 + 1.735244e-03 + 1 + + + + + + + + + + + + 359.658565456000019 + 651.374670097813237 + 8.525544e05 + 0.0 + 0.0 + 2.029491e-03 + 1 + + + + + + + + + + + + 411.68795742399999 + 651.374781126079824 + 1.213572e06 + 0.0 + 0.0 + 2.916779e-03 + 1 + + + + + + + + + + + + 337.849123328000019 + 651.374785169796951 + 6.574663e05 + 0.0 + 0.0 + 1.586039e-03 + 1 + + + + + + + + + + + + 353.556051872000012 + 651.374806862547871 + 5.205967e05 + 0.0 + 0.0 + 1.279064e-03 + 1 + + + + + + + + + + + + 376.649976767999988 + 651.374854338615705 + 1.367585e06 + 0.0 + 0.0 + 3.285501e-03 + 1 + + + + + + + + + + + + 427.356123120000007 + 651.374873707206802 + 1.035238e06 + 0.0 + 0.0 + 2.440474e-03 + 1 + + + + + + + + + + + + 397.14206475200001 + 651.374876257395044 + 9.109984e06 + 0.0 + 0.0 + 0.023374 + 1 + + + + + + + + + + + + 502.019781599999988 + 651.374958418438041 + 7.590384e05 + 0.0 + 0.0 + 1.875277e-03 + 1 + + + + + + + + + + + + 439.568155423999997 + 651.374960833301998 + 1.39325e06 + 0.0 + 0.0 + 3.234763e-03 + 1 + + + + + + + + + + + + 439.568155423999997 + 653.390196415072296 + 6.315822e07 + 0.0 + 0.0 + 0.165761 + 1 + + + + + + + + + + + + 432.293544575999988 + 653.3904077447379 + 1.463147e06 + 0.0 + 0.0 + 3.649919e-03 + 1 + + + + + + + + + + + + 439.568155423999997 + 667.369350437546814 + 3.68284e05 + 0.0 + 0.0 + 8.421e-04 + 1 + + + + + + + + + + + + 439.568155423999997 + 669.385307450081996 + 1.204008e06 + 0.0 + 0.0 + 3.039011e-03 + 1 + + + + + + + + + + + + 359.658565456000019 + 669.385366969100119 + 1.174278e06 + 0.0 + 0.0 + 2.762107e-03 + 1 + + + + + + + + + + + + 337.849123328000019 + 669.385401036245526 + 6.165724e05 + 0.0 + 0.0 + 1.49068e-03 + 1 + + + + + + + + + + + + 376.649976767999988 + 669.385474920675961 + 1.543885e06 + 0.0 + 0.0 + 3.617731e-03 + 1 + + + + + + + + + + + + 386.431577376000007 + 669.385502321270565 + 8.606681e05 + 0.0 + 0.0 + 2.043069e-03 + 1 + + + + + + + + + + + + 397.14206475200001 + 669.385502856176117 + 9.934746e06 + 0.0 + 0.0 + 0.024842 + 1 + + + + + + + + + + + + 427.356123120000007 + 669.385521631100232 + 1.580409e06 + 0.0 + 0.0 + 3.890034e-03 + 1 + + + + + + + + + + + + 494.671117951999975 + 669.38557879145776 + 1.327421e06 + 0.0 + 0.0 + 3.14276e-03 + 1 + + + + + + + + + + + + 502.019781599999988 + 669.385601852597233 + 1.581108e06 + 0.0 + 0.0 + 4.000937e-03 + 1 + + + + + + + + + + + + 440.696651311999972 + 672.404288182604773 + 1.830204e06 + 0.0 + 0.0 + 4.308654e-03 + 1 + + + + + + + + + + + + 428.573603552000009 + 679.367381214101215 + 4.341041e05 + 0.0 + 0.0 + 1.042521e-03 + 1 + + + + + + + + + + + + 440.696651311999972 + 685.380172490758923 + 3.617573e05 + 0.0 + 0.0 + 8.648254e-04 + 1 + + + + + + + + + + + + 480.106445775999987 + 685.38044858694218 + 4.389381e05 + 0.0 + 0.0 + 1.079429e-03 + 1 + + + + + + + + + + + + 353.556051872000012 + 687.395798265181497 + 7.517256e05 + 0.0 + 0.0 + 1.785739e-03 + 1 + + + + + + + + + + + + 439.568155423999997 + 687.395858594819742 + 4.23967e05 + 0.0 + 0.0 + 9.991996e-04 + 1 + + + + + + + + + + + + 417.696000911999988 + 687.395924015666878 + 9.49622e05 + 0.0 + 0.0 + 2.268281e-03 + 1 + + + + + + + + + + + + 386.431577376000007 + 687.395975094794267 + 5.151064e05 + 0.0 + 0.0 + 1.311748e-03 + 1 + + + + + + + + + + + + 376.649976767999988 + 687.395983344667457 + 1.458801e06 + 0.0 + 0.0 + 3.419722e-03 + 1 + + + + + + + + + + + + 397.14206475200001 + 687.395987122123984 + 1.403924e07 + 0.0 + 0.0 + 0.035377 + 1 + + + + + + + + + + + + 427.356123120000007 + 687.396044404506029 + 3.544868e06 + 0.0 + 0.0 + 8.61421e-03 + 1 + + + + + + + + + + + + 434.750724432000027 + 691.367317059545485 + 1.730371e06 + 0.0 + 0.0 + 4.046787e-03 + 1 + + + + + + + + + + + + 493.464502767999988 + 691.367429111042725 + 6.695803e05 + 0.0 + 0.0 + 1.565285e-03 + 1 + + + + + + + + + + + + 502.019781599999988 + 691.367453096310101 + 1.064413e06 + 0.0 + 0.0 + 2.524781e-03 + 1 + + + + + + + + + + + + 439.568155423999997 + 693.382761852628391 + 2.788267e07 + 0.0 + 0.0 + 0.071067 + 1 + + + + + + + + + + + + 429.829101728000012 + 693.382901722563474 + 1.289598e06 + 0.0 + 0.0 + 3.05512e-03 + 1 + + + + + + + + + + + + 439.568155423999997 + 707.362290614614381 + 3.512118e05 + 0.0 + 0.0 + 8.772107e-04 + 1 + + + + + + + + + + + + 439.568155423999997 + 709.377355136013421 + 6.381269e05 + 0.0 + 0.0 + 1.463539e-03 + 1 + + + + + + + + + + + + 417.696000911999988 + 709.377900191378444 + 5.251747e05 + 0.0 + 0.0 + 1.265407e-03 + 1 + + + + + + + + + + + + 397.14206475200001 + 709.37796698367265 + 1.055213e07 + 0.0 + 0.0 + 0.026512 + 1 + + + + + + + + + + + + 427.356123120000007 + 709.378015513492187 + 2.039157e06 + 0.0 + 0.0 + 5.148567e-03 + 1 + + + + + + + + + + + + 385.269469952000009 + 709.378045816671943 + 3.977196e05 + 0.0 + 0.0 + 1.074924e-03 + 1 + + + + + + + + + + + + 439.568155423999997 + 724.312559164657046 + 4.684971e05 + 0.0 + 0.0 + 1.113408e-03 + 1 + + + + + + + + + + + + 353.556051872000012 + 727.388595891864156 + 7.717165e05 + 0.0 + 0.0 + 1.89558e-03 + 1 + + + + + + + + + + + + 376.649976767999988 + 727.388621234975403 + 1.021633e06 + 0.0 + 0.0 + 2.366106e-03 + 1 + + + + + + + + + + + + 331.912858576000019 + 737.396463224546096 + 1.163941e06 + 0.0 + 0.0 + 2.785855e-03 + 1 + + + + + + + + + + + + 374.193755807000002 + 741.404400266495486 + 6.235882e05 + 0.0 + 0.0 + 1.47135e-03 + 1 + + + + + + + + + + + + 325.885799312000017 + 753.391255096317991 + 1.296664e06 + 0.0 + 0.0 + 3.140358e-03 + 1 + + + + + + + + + + + + 439.568155423999997 + 755.353375889169797 + 4.157225e05 + 0.0 + 0.0 + 1.062132e-03 + 1 + + + + + + + + + + + + 440.696651311999972 + 771.325455521739173 + 5.200204e05 + 0.0 + 0.0 + 1.297486e-03 + 1 + + + + + + + + + + + +