# -*- coding: utf-8 -*-
#
#     Copyright (C) 2018 Kevin De Bruycker and Tim Krappitz
#
#     This file is part of pyMacroMS.
#     pyMacroMS allows for high performance quantification of complex high resolution polymer mass spectra.
#
#     pyMacroMS is free software: you can redistribute it and/or modify
#     it under the terms of the GNU General Public License as published by
#     the Free Software Foundation, either version 3 of the License, or
#     (at your option) any later version.
#
#     pyMacroMS is distributed in the hope that it will be useful,
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#     GNU General Public License for more details.
#
#     You should have received a copy of the GNU General Public License
#     along with pyMacroMS.  If not, see <https://www.gnu.org/licenses/>.
#

from __future__ import absolute_import
from collections import Counter
import itertools
import pymacroms
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
from operator import itemgetter, attrgetter
import re
from sklearn import linear_model
import sys
import progressbar

class Spectrum:

    def __init__(self, filename, mzRange, minRelAbundance = 0.01, ppmDev = 5, resolution = None, mergePeaksWithinResolution = False, filterByMonomer = None):
        """

        :type filename: str
        :type mzRange: list
        :type minRelAbundance: float
        :type filterByMonomer: Counter or str
        :type ppmDev: float
        :type resolution: float
        :type mergePeaksWithinResolution: bool
        """

        self.ppmDev = ppmDev
        self.resolution = resolution
        self.filename = re.sub("\A(.*)\.([^.]*)\Z", "\\1", filename)
        self.extension = re.sub("\A(.*)\.([^.]*)\Z", "\\2", filename)

        print("Loading spectrum...")
        self.spectrumCSV = pd.read_csv(filename, "\t", names=["mz", "abundance"])
        self.peakData = self.spectrumCSV.copy(deep=True)
        self.peakData.columns = ["mz", "relAbundance"]
        # Save the raw unfilteres spectrum for later reference
        # max_val = pd.max(spectrum["abundance"])
        # spectrum["relAbundance"] = spectrum.abundance/spectrum.loc[(spectrum["mz"] >= mzRange[0]) & (spectrum["mz"] <= mzRange[1])].abundance.max()
        # spectrum["outsideROI"] = ~((spectrum.mz >= mzRange[0]) & (spectrum.mz <= mzRange[1])) # condition is true when inside the range, ~ negates the condition
        # outsideROI is not necessary because the filter for repeating units will ignore the mass peaks outside the ROI and set those rows for deletion anyway
        # calculate the RelAbundance based on the abundances in the ROI only
        self.peakData.relAbundance = self.peakData.relAbundance / self.peakData.loc[(self.peakData.mz >= mzRange[0]) & (self.peakData.mz <= mzRange[1])].relAbundance.max()  # devides the abundance by the maximum of the abundances of which the mass is not outside the region of interest
        self.rawSpectrum = pd.DataFrame(self.peakData.loc[(self.peakData.mz >= mzRange[0]) & (self.peakData.mz <= mzRange[1])])
        self.rawSpectrum.reset_index(drop=True, inplace=True)
        print("--> Done\n")

        print("Filtering spectrum...")
        if filterByMonomer is not None:
            if type(filterByMonomer) == str:
                filterByMonomer = pymacroms.getMonoIsotopicMass(pymacroms.database.monomers[filterByMonomer])
            else:
                filterByMonomer = pymacroms.getMonoIsotopicMass(filterByMonomer)
            # filter the spectrum for peaks that only have a corresponding peak at + and - 1 repeating unit (the first monomer is used for this assessment)
            noPlusMinOneRU = []
            for mz in self.peakData.mz:
                if mz >= mzRange[0] and mz <= mzRange[1]:
                    noPlusMinOneRU.append(not self.hasPlusMinOneRU(mz, filterByMonomer))
                else:
                    noPlusMinOneRU.append(True)
            self.peakData["noPlusMinOneRU"] = noPlusMinOneRU
            print("--> Peaks outside mass range removed")
            print("--> Peaks without a +/- 1 repeating unit counterpart removed")
        else:
            self.peakData["noPlusMinOneRU"] = ~((self.peakData.mz >= mzRange[0]) & (self.peakData.mz <= mzRange[1]))  # condition is true when inside the range, ~ negates the condition
            print("--> Peaks outside mass range removed")
        self.peakData.drop(self.peakData[self.peakData["noPlusMinOneRU"]].index, inplace=True)
        self.peakData.drop(columns=["noPlusMinOneRU"], inplace=True)
        self.peakData.reset_index(drop=True, inplace=True)
        if mergePeaksWithinResolution:
            self.peakData = pymacroms.combineIsotopes(self.peakData, resolution)
            self.peakData.relAbundance = self.peakData.relAbundance / self.peakData.relAbundance.max() # recalculate relative abundance
            print("--> Merged peaks that are not separated with a resolution of " + str(self.resolution))
        # tag the entries that have a too low RelAbund to be deleted
        # self.peakData.loc[(self.peakData.relAbundance < minRelAbundance)].relAbundance = None # Doesn't work, only works when setting the whole 'matched' row to None, but mz values still needed for later matching of +- 1 RU
        self.peakData["tooLowRelAbund"] = self.peakData.relAbundance < minRelAbundance
        # self.peakData["toBeDeleted"] = self.peakData.tooLowRelAbund | self.peakData.noPlusMinOneRU
        self.peakData.drop(self.peakData[self.peakData["tooLowRelAbund"]].index, inplace=True)
        self.peakData.drop(columns=["tooLowRelAbund"], inplace=True)
        self.peakData.reset_index(drop=True, inplace=True)
        print("--> Peaks with too low relative abundance removed")
        print("--> Done\n")

        # spectrum.to_csv("TN60P_filtered.csv", index=False)
        # print("Filtered spectrum saved")

        # for row in spectrum["toBeDeleted"]:
        #     if row:
        #         spectrum.drop(spectrum["toBeDeleted"], 0)

    def hasPlusMinOneRU(self, mz, RUMass):
        mzRangeMinOne = [mz - mz * self.ppmDev * 1e-6 - RUMass - (mz - mz * self.ppmDev * 1e-6 - RUMass) * self.ppmDev * 1e-6,
                         mz + mz * self.ppmDev * 1e-6 - RUMass + (mz + mz * self.ppmDev * 1e-6 - RUMass) * self.ppmDev * 1e-6]
        # The 'true' mass of an experimental peak should be within a certain ppm range of that experimental peak,
        # but the experimental peak can again be ppmDev apart from that range -RUMass
        mzRangePlusOne = [mz - mz * self.ppmDev * 1e-6 + RUMass - (mz - mz * self.ppmDev * 1e-6 + RUMass) * self.ppmDev * 1e-6,
                          mz + mz * self.ppmDev * 1e-6 + RUMass + (mz + mz * self.ppmDev * 1e-6 + RUMass) * self.ppmDev * 1e-6]
        if len(self.peakData.loc[(self.peakData.mz >= mzRangeMinOne[0]) & (self.peakData.mz <= mzRangeMinOne[1])]) > 0 and len(
                self.peakData.loc[(self.peakData.mz >= mzRangePlusOne[0]) & (self.peakData.mz <= mzRangePlusOne[1])]) > 0:
            return True
        else:
            return False

    def matchPeaks(self, polymerSimulation):
        self.polymerSimulation = polymerSimulation

        # Caution! for fitting, the normalised intensity is used rather than the relative intensity for the representation of the isotopic patterns!
        # This is fixed by now since everything should be transferred to norm abund internally

        print("Matching experimental spectrum to peaks of the simulated polymer...")
        print("(accuracy = " + str(self.ppmDev) + " ppm, resolution = " + str(self.resolution) + ")")
        if self.resolution is not None:
            print("--> Combining isotopologues within resolution limits for all %i macromolecules" % len(self.polymerSimulation.macromolecules))
            sys.stdout.flush()
            for macromolecule in progressbar.progressbar(self.polymerSimulation.macromolecules, 0, len(self.polymerSimulation.macromolecules)):
                macromolecule["moleculeData"].resolution = self.resolution
                macromolecule["moleculeData"].isotopicDist_resolution = pymacroms.combineIsotopes(macromolecule["moleculeData"].isotopicDist, self.resolution)
                macromolecule["moleculeData"].massMostAbundant = sorted(macromolecule["moleculeData"].isotopicDist_resolution, key=itemgetter(1), reverse=True)[0][0]
        print("--> Total amount of experimental peaks: " + str(len(self.peakData)))
        theoPeakContributions = []  # for every experimental peak, this variable contains the theoretical composition:
        # [[indices of contributing macromolecules], [corresponding normalised abundances]]
        # identifiedMacromolecules = []
        matchedIsotopes = dict()
        peakMatched = []
        print("--> Matching...")
        sys.stdout.flush()
        for mz, relAbundance in progressbar.progressbar(self.peakData.itertuples(index=False, name=None), 0, len(self.peakData)):
            moleculeList = self.polymerSimulation.getMoleculeList(mz, self.ppmDev, self.resolution)
            if moleculeList is not None:
                macromolecules = []
                normAbundances = []
                for macromoleculeIndex, repUnitsCombination, isotopesNearMass in moleculeList:
                    macromolecules.append(macromoleculeIndex)
                    normAbundances.append(sum(list(zip(*isotopesNearMass))[1])) # the normAbundance, i.e. contribution of 1 macromolecule to the experimental peak is the sum of the normalised abundances within the accuracy of ppmDev
                    if macromoleculeIndex in matchedIsotopes:
                        matchedIsotopes[macromoleculeIndex] += list((mz, isotopeMass, deviation) for isotopeMass, normAbundance, deviation in isotopesNearMass)
                    else:
                        matchedIsotopes[macromoleculeIndex] = list((mz, isotopeMass, deviation) for isotopeMass, normAbundance, deviation in isotopesNearMass)
                theoPeakContributions.append([macromolecules, normAbundances])
                # identifiedMacromolecules += macromolecules
                # matchedCalcMass.append(np.average(masses, weights=massWeightingFactors))
                peakMatched.append(True)
            else:
                # matchedCalcMass.append(None)
                peakMatched.append(False)
        self.peakData["peakMatched"] = peakMatched
        self.theoPeakContributions = theoPeakContributions
        # self.identifiedMacromolecules = np.unique(identifiedMacromolecules)
        # self.identifiedMacromolecules = sorted(list(matchedIsotopes.keys()))
        self.matchedIsotopes = matchedIsotopes
        print("--> %i peaks successfully matched" % len(self.peakData[self.peakData.peakMatched]))
        print("--> These peaks are (potentially) originating from %i macromolecules" % len(self.matchedIsotopes))
        print("--> %i experimental m/z values could not be matched to a calculated m/z" % len(self.peakData[~self.peakData.peakMatched]))
        print("--> Done\n")

    def plotMatchPeaksOverview(self, mzRange: list = None, showMatched: bool = True, showNotMatched: bool = True):
        if showMatched:
            mz_axis = []
            relAbund_axis = []
            for mz, relAbund in self.peakData[["mz", "relAbundance"]].loc[self.peakData.peakMatched].itertuples(index=False, name=None):
                mz_axis += list([mz - 1e-10, mz, mz + 1e-10])
                relAbund_axis += list([-0.1, relAbund, -0.1])
            plt.plot(mz_axis, relAbund_axis, color="green", label="Matched to theoretical m/z")
        if showNotMatched:
            mz_axis = []
            relAbund_axis = []
            for mz, relAbund in self.peakData[["mz", "relAbundance"]].loc[~self.peakData.peakMatched].itertuples(index=False, name=None):
                mz_axis += list([mz - 1e-10, mz, mz + 1e-10])
                relAbund_axis += list([-0.1, relAbund, -0.1])
            plt.plot(mz_axis, relAbund_axis, color="red", label="Not matched to theoretical m/z")
        plt.legend()
        plt.title("Filtered experimental spectrum")
        plt.xlabel("m/z")
        plt.ylabel("Rel. abundance")
        if mzRange is not None:
            plt.xlim(mz for mz in mzRange)
        plt.ylim(0, 1.05)
        # plt.savefig("output.pdf", dpi=300, format='pdf')
        plt.show()
        # plt.close()

    def calcComposition(self, minimumAmount: float = 0, minimumIsotopeFraction: float = 0.1):
        self.compositionMinimumAmount = minimumAmount # save for future reference
        print("Calculating the composition by fitting the experimental and theoretical intensities...")
        # target = pd.DataFrame(np.zeros(len(self.theoPeakComposition)), columns=["relAbundance"])
        target = pd.DataFrame(self.peakData.loc[self.peakData.peakMatched].relAbundance, columns=["relAbundance"])
        # target.reset_index(drop=True, inplace=True)
        df = pd.DataFrame(np.zeros((len(self.theoPeakContributions), len(self.matchedIsotopes))), columns=sorted(list(self.matchedIsotopes.keys())))
        for index, [macromolecules, normAbundances] in enumerate(self.theoPeakContributions): #index, [indices of contributing macromolecules], [corresponding normalised abundances]]
            # target.iloc[index].relAbundance = relAbundance
            for macromolecule, normAbundance in zip(macromolecules, normAbundances):
                df.loc[index, macromolecule] = normAbundance
        df.index = target.index
        dfColumnsSum = pd.DataFrame(df.sum(), columns=["dfSum"])
        # this sum represents the fraction of the (normAbundance of the) isotopic pattern that is matched in the spectrum:
        # if all the peaks of the theoretical pattern are matched, this sum should be 1;
        # if only 1 minor peak of the isotopic pattern is found in the experimental spectrum, the sum will be low --> probably mismatch
        df.drop(columns=dfColumnsSum.loc[dfColumnsSum.dfSum < minimumIsotopeFraction].index, inplace=True)
        regression = linear_model.LinearRegression(fit_intercept=False)
        regression.fit(df, target)
        regressionR2 = regression.score(df, target)
        print("--> Composition found with an R^2 of %.5f" % round(regressionR2, 5))
        regResult = pd.DataFrame(np.transpose(regression.coef_), index=df.columns, columns=["amounts"])
        # self.peakData["fittedRelAbundance"] = pd.DataFrame(np.dot(df, regResult), index=target.index, columns=["calcRelAbundance"]).calcRelAbundance # Don't do this, use the dedicated method getFittedIsotopicDist instead: this will exclude isotopes that were not matched while they will be included when calculating the whole spectrum
        self.peakData["diffRelAbundance"] = pd.DataFrame(np.dot(df, regResult), index=target.index, columns=["calcRelAbundance"]).calcRelAbundance - target.relAbundance
        regResult.amounts = regResult.amounts / regResult.amounts.max() # normalisation
        while regResult.amounts.min() < minimumAmount:
            print("--> One or more macromolecules have a contribution\n    that is either negative or below the threshold of %.2f!" % round(minimumAmount, 2))
            print("--> Recalculating...")
            # remove values that are too low and repeat linear regression
            df.drop(columns=regResult.loc[regResult.amounts < minimumAmount].index, inplace=True)
            regression.fit(df, target)
            regressionR2 = regression.score(df, target)
            print("--> Composition found with an R^2 of %.5f" % round(regressionR2, 5))
            regResult = pd.DataFrame(np.transpose(regression.coef_), index=df.columns, columns=["amounts"])
            self.peakData["diffRelAbundance"] = pd.DataFrame(np.dot(df, regResult), index=target.index, columns=["calcRelAbundance"]).calcRelAbundance - target.relAbundance
            regResult.amounts = regResult.amounts / regResult.amounts.max() # normalisation
        if len(regResult) == 0:
            self.composition = None
            self.regressionR2 = None
        else:
            self.composition = regResult
            self.regressionR2 = regressionR2
        print("--> Done\n")

    def printComposition(self):
        if not hasattr(self, "composition"):
            sys.exit("Calculate the composition first using Spectrum.calcComposition")
        if self.composition is not None:
            report = []
            for macromolecule, amount in self.composition.itertuples(name=None):
                report.append([list(self.polymerSimulation.macromolecules[macromolecule]["repUnitsCombination"]),
                               self.polymerSimulation.endgroupPairs[self.polymerSimulation.macromolecules[macromolecule]["endgroupPairIndex"]],
                               self.polymerSimulation.macromolecules[macromolecule]["moleculeData"].formula_ion,
                               amount,
                               self.polymerSimulation.macromolecules[macromolecule]["moleculeData"].massMostAbundant,
                               self.matchedIsotopes[macromolecule]])
            report.sort(key=itemgetter(4))
            print("R^2 = %.5f" % round(self.regressionR2, 5))
            print("Monomer combination\tEnd-groups\tFormula (ion)\tAmount\tMost abundant mass\tMatched isotopes")
            print(str(self.polymerSimulation.monomers) + "\t\t\t\t\t(m/z (exp), m/z (calc), dev (ppm))")
            for repUnitsCombination, endgroupPair, formula_ion, amount, massMostAbundant, matchedIsotopes in report:
                print(str(repUnitsCombination) + "\t" + endgroupPair + "\t" + formula_ion + "\t" + "%.2f" % round(amount, 2) + "\t" + str(massMostAbundant) + "\t" + str(matchedIsotopes))
        else:
            print("No matching molecules found...")

    def saveComposition(self):
        if not hasattr(self, "composition"):
            sys.exit("Calculate the composition first using Spectrum.calcComposition")
        if self.composition is not None:
            report = []
            for macromolecule, amount in self.composition.itertuples(name=None):
                report.append([list(self.polymerSimulation.macromolecules[macromolecule]["repUnitsCombination"]),
                               self.polymerSimulation.endgroupPairs[self.polymerSimulation.macromolecules[macromolecule]["endgroupPairIndex"]],
                               self.polymerSimulation.macromolecules[macromolecule]["moleculeData"].formula_ion,
                               amount,
                               self.polymerSimulation.macromolecules[macromolecule]["moleculeData"].massMostAbundant,
                               self.matchedIsotopes[macromolecule]])
            report.sort(key=itemgetter(4))
            export = pd.DataFrame(list((repUnitsCombination, endgroupPair, formula_ion, amount, massMostAbundant, list(matchedIsotopes)) for repUnitsCombination, endgroupPair, formula_ion, amount, massMostAbundant, matchedIsotopes in report), columns=["Monomer combination", "End-groups", "Formula (ion)", "Amount", "Most abundant mass", "Matched isotopes (m/z (exp), m/z (calc), dev (ppm))"])
        else:
            export = pd.DataFrame(columns=["No matching molecules found..."])
        export.to_csv(self.filename + "_composition." + self.extension, index=False)

    def getFittedIsotopicDist(self, resolution: float = None):
        if not hasattr(self, "composition"):
            sys.exit("Calculate the composition first using Spectrum.calcComposition")
        if self.composition is not None:
            fittedIsotopicDist = pd.DataFrame(columns=["mz", "normAbundance"])
            for macromolecule, amount in self.composition.itertuples(name=None):
                for mz, normAbund in self.polymerSimulation.macromolecules[macromolecule]["moleculeData"].isotopicDist:
                    fittedIsotopicDist = fittedIsotopicDist.append(pd.DataFrame([[mz, normAbund * amount]], columns=["mz", "normAbundance"]), ignore_index=True)
            fittedIsotopicDist.normAbundance = fittedIsotopicDist.normAbundance / fittedIsotopicDist.normAbundance.sum() # normalise
            # fittedIsotopicDist["relAbundance"] = fittedIsotopicDist.normAbundance / fittedIsotopicDist.normAbundance.max()
            fittedIsotopicDist.sort_values("mz", inplace=True)
            fittedIsotopicDist.reset_index(drop=True, inplace=True)
            if resolution is not None:
                return pymacroms.combineIsotopes(fittedIsotopicDist, resolution)
            else:
                return fittedIsotopicDist
        else:
            return None

    def printFittedSpectrum(self, resolution: float = None):
        fittedIsotopicDist = self.getFittedIsotopicDist(resolution)
        if fittedIsotopicDist is not None:
            print("Fitted spectrum")
            print("Mass\tRel. Abundance")
            for mz, relAbund in pymacroms.toRelativeAbundance(list(fittedIsotopicDist.itertuples(index=False, name=None))):
                print(str(mz) + "\t" + str(relAbund))
        else:
            print("No matching molecules found...")

    def plotFittedSpectrum(self, resolution: float = None, color = "black", mzRange: list = None):
        fittedIsotopicDist = self.getFittedIsotopicDist(resolution)
        if fittedIsotopicDist is not None:
            mz_axis = []
            relAbund_axis = []
            for mz, relAbund in pymacroms.toRelativeAbundance(list(fittedIsotopicDist.itertuples(index=False, name=None))):
                mz_axis += list([mz - 1e-10, mz, mz + 1e-10])
                relAbund_axis += list([-0.1, relAbund, -0.1])
            plt.plot(mz_axis, relAbund_axis, color=color)
            plt.title("Fitted spectrum")
            plt.xlabel("m/z")
            plt.ylabel("Rel. abundance")
            if mzRange is not None:
                plt.xlim(mz for mz in mzRange)
            plt.ylim(0, 1.05)
            plt.show()
        else:
            print("No matching molecules found...")

    def plotComparisonSpectrum(self, resolution: float = None, color = "black", mzRange: list = None):
        fittedIsotopicDist = self.getFittedIsotopicDist(resolution)
        if fittedIsotopicDist is not None:
            rawSpectrum = plt.subplot(211)
            mz_axis = []
            relAbund_axis = []
            for mz, relAbund in self.rawSpectrum.itertuples(index=False, name=None):
                mz_axis += list([mz - 1e-10, mz, mz + 1e-10])
                relAbund_axis += list([-0.1, relAbund, -0.1])
            rawSpectrum.plot(mz_axis, relAbund_axis, color=color, label="Raw data")
            plt.legend()
            plt.ylabel("Rel. abundance")
            if mzRange is not None:
                plt.xlim(mz for mz in mzRange)
            plt.ylim(0, 1.05)
            fittedSpectrum = plt.subplot(212, sharex=rawSpectrum, sharey=rawSpectrum)
            mz_axis = []
            relAbund_axis = []
            for mz, relAbund in pymacroms.toRelativeAbundance(list(fittedIsotopicDist.itertuples(index=False, name=None))):
                mz_axis += list([mz - 1e-10, mz, mz + 1e-10])
                relAbund_axis += list([-0.1, relAbund, -0.1])
            fittedSpectrum.plot(mz_axis, relAbund_axis, color=color, label="Fitted spectrum")
            # plt.title(r"Isotopic distribution ({}{} adducts) of a (co)polymer of".format(self.amountAdductIons,
            #                                                                              re.sub("([0-9]+)", "$_{\\1}$",
            #                                                                                     re.sub("([+-]+)",
            #                                                                                            "$^{\\1}$",
            #                                                                                            self.adductIon))) + "\n" +
            #           "{} with end-group(s): {}".format(re.sub("([0-9]+)", "$_{\\1}$", str(self.monomers)),
            #                                             re.sub("([0-9]+)", "$_{\\1}$", str(self.endgroupPairs))))
            plt.legend()
            plt.xlabel("m/z")
            plt.ylabel("Rel. abundance")
            if mzRange is not None:
                plt.xlim(mz for mz in mzRange)
            plt.ylim(0, 1.05)
            # plt.savefig("output.pdf", dpi=300, format='pdf')
            plt.show()
            # plt.close()
        else:
            print("No matching molecules found...")

    def plotDiffIsotopicDist(self, mzRange: list = None, showExperimental: bool = True, showNonMatched: bool = False, showFitted: bool = False, resolution: float = None):
        if not hasattr(self, "composition"):
            sys.exit("Calculate the composition first using Spectrum.calcComposition")
        if self.composition is not None:
            filteredSpectrum = plt.subplot(211)
            if showExperimental:
                mz_axis = []
                relAbund_axis = []
                for mz, relAbund in self.peakData[["mz", "relAbundance"]].loc[self.peakData.peakMatched].itertuples(
                        index=False, name=None):
                    mz_axis += list([mz - 1e-10, mz, mz + 1e-10])
                    relAbund_axis += list([-0.1, relAbund, -0.1])
                filteredSpectrum.plot(mz_axis, relAbund_axis, color="green", label="(Filtered) matched experimental")
            if showNonMatched:
                mz_axis = []
                relAbund_axis = []
                for mz, relAbund in self.peakData[["mz", "relAbundance"]].loc[~self.peakData.peakMatched].itertuples(
                        index=False, name=None):
                    mz_axis += list([mz - 1e-10, mz, mz + 1e-10])
                    relAbund_axis += list([-0.1, relAbund, -0.1])
                filteredSpectrum.plot(mz_axis, relAbund_axis, color="red", label="Not matched to theoretical m/z")
            if showFitted:
                fittedIsotopicDist = self.getFittedIsotopicDist(resolution)
                if fittedIsotopicDist is not None:
                    mz_axis = []
                    relAbund_axis = []
                    for mz, relAbund in pymacroms.toRelativeAbundance(list(fittedIsotopicDist.itertuples(index=False, name=None))):
                        mz_axis += list([mz - 1e-10, mz, mz + 1e-10])
                        relAbund_axis += list([-0.1, relAbund, -0.1])
                    filteredSpectrum.plot(mz_axis, relAbund_axis, color="blue", label="(Fitted) calculated")
            plt.legend()
            plt.title("Filtered experimental spectrum")
            # plt.xlabel("m/z")
            plt.ylabel("Rel. abundance")
            if mzRange is not None:
                plt.xlim(mz for mz in mzRange)
            plt.ylim(0, 1.05)

            diffFittedSpectrum = plt.subplot(212, sharex=filteredSpectrum)
            mz_axis = []
            diffRelAbund_axis = []
            for mz, diffRelAbund in self.peakData[["mz", "diffRelAbundance"]].loc[self.peakData.peakMatched].itertuples(index=False, name=None):
                mz_axis += list([mz - 1e-10, mz, mz + 1e-10])
                diffRelAbund_axis += list([0, diffRelAbund, 0])
            diffFittedSpectrum.plot(mz_axis, diffRelAbund_axis, color="black", label="Difference fitted-experimental")
            plt.legend()
            plt.xlabel("m/z")
            plt.ylabel("Rel. abundance")
            if mzRange is not None:
                plt.xlim(mz for mz in mzRange)
            # plt.ylim(-0.55, 0.55)
            # plt.savefig("output.pdf", dpi=300, format='pdf')
            plt.show()
            # plt.close()






