/* BEGIN software license
 *
 * MsXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright (C) 2009--2026 Filippo Rusconi
 *
 * http://www.msxpertsuite.org
 *
 * This file is part of the MsXpertSuite project.
 *
 * The MsXpertSuite project is the successor of the massXpert project. This
 * project now includes various independent modules:
 *
 * - massXpert, model polymer chemistries and simulate mass spectrometric data;
 * - mineXpert, a powerful TIC chromatogram/mass spectrum viewer/miner;
 *
 * This program 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.
 *
 * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
 *
 * END software license
 */


/////////////////////// stdlib includes


/////////////////////// Qt includes
#include <QList>
#include <QDebug>


/////////////////////// pappsomspp includes


/////////////////////// Local includes
#include "MsXpS/libXpertMassCore/Processing/AbstractLowMassDeconvolver.hpp"
#include "MsXpS/libXpertMassCore/IsotopicClusterGenerator.hpp"

namespace MsXpS
{
namespace libXpertMassCore
{
namespace Processing
{

/////////////////// SupportingIon /////////////////////////
/////////////////// SupportingIon /////////////////////////
/////////////////// SupportingIon /////////////////////////
/////////////////// SupportingIon /////////////////////////

/*!
\struct MsXpS::libXpertMassCore::Processing::SupportingIon
\inmodule libXpertMassCore
\ingroup XpertMassCoreProcessing
\inheaderfile MsXpS/libXpertMassCore/Processing/AbstractLowMassDeconvolver.hpp

\brief The SupportingIon struct encapsulates an input mass spectrum's
monoisotopic centroid of a cluster that was succesfully matched to a
corresponding calculated cluster.

This struct is used in support of the code that performs low mass deconvolution
of a given input mass spectrum.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::SupportingIon::charge

\brief The charge of the analyte beneath the isotopic cluster.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::SupportingIon::monoisotopicMz

\brief The monoisotopic m/z value of the analyte beneath the isotopic cluster.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::SupportingIon::intensity

\brief The intensity of the monoisotopic m/z of the analyte beneath the
isotopic cluster.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::SupportingIon::matchingIsotopicClusterQSPtr

\brief QSharedPointer to the pappso::Trace object representing the \e
calculated isotopic cluster that matched succesfully an isotopic cluster from
the input mass spectrum.

The calculated isotopic cluster's centroids have their intensities normalized to
the input mass spectrum's cluster's monoisotopic centroid. This calculated
isotopic cluster is used in case a synthetic a mass spectrum recapitulating the
match is requested.
*/


/*!
\fn MsXpS::libXpertMassCore::Processing::SupportingIon::SupportingIon()

\brief Constructs an empty SupportingIon instance.
*/

/*!
\fn MsXpS::libXpertMassCore::Processing::SupportingIon::SupportingIon(int
charge, double monoisotopic_mz, double intensity, QSharedPointer<pappso::Trace>
matching_isotopic_cluster_qsp)

\brief Constructs a SupportingIon instance and initializes it with the
parameters.

\list

\li \a charge: the charge of the analyte beneath the isotopic cluster
\li \a monoisotopic_mz: the monoisotopic m/z value of the analyte beneath the
isotopic cluster
\li \a intensity: the intensity of the cluster's monoisotopic centroid
\li \a matching_isotopic_cluster_qsp: QSharedPointer to the pappso::Trace object
representing the \e calculated isotopic cluster that matched succesfully an
isotopic cluster from the input mass spectrum.

\endlist
*/

/*!
\fn MsXpS::libXpertMassCore::Processing::SupportingIon::SupportingIon(const
SupportingIon &other)

\brief Constructs a SupportingIon instance using \a other as a template.
*/

/*!
\fn MsXpS::libXpertMassCore::Processing::SupportingIon::operator=(const
SupportingIon &other)

\brief Initializes this SupportingIon instance using \a other.

Returns a reference to this instance.
*/


/*!
\fn MsXpS::libXpertMassCore::Processing::SupportingIon::toString() const

\brief Crafts a string documenting this instance and returns it.
*/

/////////////////// IsoMatch /////////////////////////
/////////////////// IsoMatch /////////////////////////
/////////////////// IsoMatch /////////////////////////
/////////////////// IsoMatch /////////////////////////

/*!
\struct MsXpS::libXpertMassCore::Processing::IsoMatch
\inmodule libXpertMassCore
\ingroup XpertMassCoreProcessing
\inheaderfile MsXpS/libXpertMassCore/Processing/AbstractLowMassDeconvolver.hpp

\brief The IsoMatch struct describes a match between a theoretical isotopic
cluster centroid and a centroid of the input centroided mass spectrum
(observed).

This struct is used in support of the code that performs low mass deconvolution
of a given input mass spectrum.

When performing matches between calculated isotopic clusters and input mass
spectrum clusters, this struct documents a successful centroid match (not
necessarily of the monoisotopic centroid).
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::IsoMatch::theoreticalClusterIndex

\brief Index of the matching centroid in the calculated isotopic cluster.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::IsoMatch::observedPeakIndex

\brief Index of the matching centroid in the input (experimental) isotopic
cluster.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::IsoMatch::observedMz

\brief m/z value of the matching centroid in the input mass data.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::IsoMatch::observedIntensity

\brief Intensity value of the matching centroid in the input mass data.
*/

/*!
\fn MsXpS::libXpertMassCore::Processing::IsoMatch::toString()const

\brief Crafts a string documenting this IsoMatch instance and returns it.
*/


/////////////////// BinnedIsotopicCentroid /////////////////////////
/////////////////// BinnedIsotopicCentroid /////////////////////////
/////////////////// BinnedIsotopicCentroid /////////////////////////
/////////////////// BinnedIsotopicCentroid /////////////////////////

/*!
\struct MsXpS::libXpertMassCore::Processing::BinnedIsotopicCentroid
\inmodule libXpertMassCore
\ingroup XpertMassCoreProcessing
\inheaderfile MsXpS/libXpertMassCore/Processing/AbstractLowMassDeconvolver.hpp

\brief This struct describes a calculated isotopic cluster's centroid
for a calculated isotopic cluster that was binned so as to remove
hyperfine isotopic structure centroids that would be of no use during
the matching of centroids with the input mass spectrum's centroids.

Binning of the calculated original isotopic cluster is performed to remove
very close peaks that would serve no purpose in the matches with the
input mass spectrum's isotopic cluster's centroids. This binning is sometimes
called the \e collapsing of the calculated isotopic cluster.
*/


/*!
\variable \
MsXpS::libXpertMassCore::Processing::BinnedIsotopicCentroid::isotopeIndex

\brief The isotopologue index of the centroid in the isotopic cluster.

Note that when working with heavy isotope-labelled analytes, index 0
is not the index of the monoisotopic centroid.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::BinnedIsotopicCentroid::neutralMass
\brief Weighted centroid that may result from binning of multiple
hyperfine isotopic cluster centroids that had very close mass values.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::BinnedIsotopicCentroid::intensity
\brief Intensity of the centroid.
*/

/*!
\fn MsXpS::libXpertMassCore::Processing::BinnedIsotopicCentroid::toString()const

\brief Crafts a string documenting this BinnedIsotopicCentroid instance and
returns it.
*/


/////////////////// FitQuality /////////////////////////
/////////////////// FitQuality /////////////////////////
/////////////////// FitQuality /////////////////////////
/////////////////// FitQuality /////////////////////////


/*!
\struct MsXpS::libXpertMassCore::Processing::FitQuality
\inmodule libXpertMassCore
\ingroup XpertMassCoreProcessing
\inheaderfile MsXpS/libXpertMassCore/Processing/AbstractLowMassDeconvolver.hpp

\brief This struct holds the metric data about the matches between the
calculated isotopic cluster's centroids and those from the input centroided
mass spectrum. This struct is mainly for use in the reporting to the user.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::FitQuality::pearsonCorrelation
\brief Pearson correlation value computed between the different centroids of
the calculated and observed isotopic clusters.

The Pearson correlation checks that the observed and theoretical
isotopic cluster centroids are correlated by their intensities.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::FitQuality::spacingErrorPpm
\brief Mean of the spacing errors between contiguous isotopic cluster centroids
in the input mass spectrum compared to the theoretically expected spacings.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::FitQuality::explainedIntensity
\brief For a given isotopic cluster set of matches, difference between
the sum of the input mass spectral isotopic cluster's centroid intensities and
that obtained for their calculated isotopic cluster counterparts, divided by
that latter sum.

This value compares the sum of the intensities in the calculated cluster with
the sum of the intensities of the matched centroids taking the calculated as the
reference of the expected isotopic cluster profile.
*/

/*!
\fn MsXpS::libXpertMassCore::Processing::FitQuality::FitQuality()
\brief Constructs an empty FitQuality instance.
*/

/*!
\fn MsXpS::libXpertMassCore::Processing::FitQuality::FitQuality(double
pearson_correlation, double spacing_error_ppm, double explained_intensity)
\brief Constructs a FitQuality instance using the parameters to initialize it.
\list
\li \a pearson_correlation: Pearson correlation
\li \a spacing_error_ppm: mean of the spacing errors between contiguous isotopic
cluster centroids
\li \a explained_intensity: comparison of the sum of the centroid intensities in
the calculated and observed isotopic clusters
\endlist
*/


/////////////////// DeconvolutedFeature /////////////////////////
/////////////////// DeconvolutedFeature /////////////////////////
/////////////////// DeconvolutedFeature /////////////////////////
/////////////////// DeconvolutedFeature /////////////////////////

/*!
\struct MsXpS::libXpertMassCore::Processing::DeconvolutedFeature
\inmodule libXpertMassCore
\ingroup XpertMassCoreProcessing
\inheaderfile MsXpS/libXpertMassCore/Processing/AbstractLowMassDeconvolver.hpp

\brief This struct holds the data about an analyte's neutral mass and intensity
as defined by the deconvolution of one or more isotopic clusters representing
different ionization levels of the same analyte.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::DeconvolutedFeature::neutralMass
\brief Neutral mass of the feature.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::DeconvolutedFeature::intensity
\brief This intensity value is a combination of the different intensity
values of the different ions of differing charges (if any) that were
found to correspond to the neutral mass of the analyte characterized
by this deconvoluted feature.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::DeconvolutedFeature::supportingIons
\brief Set of \l SupportingIon instances that were determined as
contributing to the deconvolution of this feature.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::DeconvolutedFeature::fitQuality
\brief \l FitQuality computed for this deconvoluted feature.
*/

/*!
\fn
MsXpS::libXpertMassCore::Processing::DeconvolutedFeature::DeconvolutedFeature()
\brief Constructs an empty DeconvolutedFeature instance.
*/

/*!
\fn
MsXpS::libXpertMassCore::Processing::DeconvolutedFeature::DeconvolutedFeature(const
DeconvolutedFeature &other)
\brief Constructs a DeconvolutedFeature instance using \a other as a template.
*/

/*!
\fn MsXpS::libXpertMassCore::Processing::DeconvolutedFeature::operator =(const
DeconvolutedFeature &other)
\brief Initialized this instance using \a other as a template.
*/

/*!
\fn MsXpS::libXpertMassCore::Processing::DeconvolutedFeature::toString(bool
with_supporting_ions = true) const
\brief Crafts a string representing this instance and returns it.
If \a with_supporting_ions is true, the string also reports the supporting ions
contained in this instance.
*/


/////////////////// ChargeSpecificFeature /////////////////////////
/////////////////// ChargeSpecificFeature /////////////////////////
/////////////////// ChargeSpecificFeature /////////////////////////
/////////////////// ChargeSpecificFeature /////////////////////////

/*!
\struct MsXpS::libXpertMassCore::Processing::ChargeSpecificFeature
\inmodule libXpertMassCore
\ingroup XpertMassCoreProcessing
\inheaderfile MsXpS/libXpertMassCore/Processing/AbstractLowMassDeconvolver.hpp

\brief This struct holds the data describing an input mass spectrum's
centroid with a given tested charge.

When a deconvolution is started, one of the firsts steps is to iterate in the
input centroided mass spectrum and for each centroid that it contains:

\list

\li Convert m/z to neutral mass assuming the ion has a charge in the
user-defined range of possible charges.

\li Fill in the ChargeSpecificFeature and transmit it to the fitting function.

\li Repeat for all the possible charges in the user-defined range.

\endlist
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::ChargeSpecificFeature::charge
\brief The charge being tested for the input centroided mass spectrum's
centroid.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::ChargeSpecificFeature::testedNeutralMass
\brief The neutral mass calculated assuming the original input mass spectrum's
centroid's m/z value was for the member variable \l charge value.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::ChargeSpecificFeature::testedMz
\brief The original input mass spectrum's centroid's m/z value.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::ChargeSpecificFeature::testedIntensity
\brief The original input mass spectrum's centroid's intensity value.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::ChargeSpecificFeature::monoisotopicNeutralMass
\brief The neutral mass for the monoisotopic centroid.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::ChargeSpecificFeature::monoisotopicMz
\brief The m/z value for the monoisotopic centroid.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::ChargeSpecificFeature::monoisotopicIntensity
\brief The intensity value for the monoisotopic centroid.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::ChargeSpecificFeature::matchingIsotopicClusterQSPtr
\brief Shared pointer to the pappso::Trace representing the isotopic cluster
that could be matched to this instance.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::ChargeSpecificFeature::fitQuality
\brief \l FitQuality for this instance.
*/

/*!
\fn MsXpS::libXpertMassCore::Processing::ChargeSpecificFeature::toString(bool
with_fit_quality = false) const
\brief Crafts a string representing this instance and returns it.

If \a with_fit_quality is true, the returned string also documents the \l
FitQuality member.
*/


/////////////////// LowMassDeconvolverParameters /////////////////////////
/////////////////// LowMassDeconvolverParameters /////////////////////////
/////////////////// LowMassDeconvolverParameters /////////////////////////
/////////////////// LowMassDeconvolverParameters /////////////////////////


/*!
\struct MsXpS::libXpertMassCore::Processing::LowMassDeconvolverParameters
\inmodule libXpertMassCore
\ingroup XpertMassCoreProcessing
\inheaderfile MsXpS/libXpertMassCore/Processing/AbstractLowMassDeconvolver.hpp

\brief The LowMassDeconvolverParameters struct encapsulates all the
parameters needed to configure the low mass deconvolving process.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LowMassDeconvolverParameters::minCharge

\brief The minimum charge deconvolved mass peaks may have.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LowMassDeconvolverParameters::maxCharge

\brief The maximum charge deconvolved mass peaks may have.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LowMassDeconvolverParameters::minMass

\brief The minimum mass deconvolved mass peaks may have.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LowMassDeconvolverParameters::maxMass

\brief The maximum mass deconvolved mass peaks may have.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LowMassDeconvolverParameters::ppmMassTolerance

\brief The tolerance in part-per-million of the mass matches.

For a tight tolerance, use 3-5 ppm.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LowMassDeconvolverParameters::minIntensity

\brief The minimum intensity signal must have to be taken into account in the
processing.

Typically this value would be just above the noise signal.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LowMassDeconvolverParameters::minIsotopicClusterMatches

\brief The minimum number of matches between the calculated isotopic cluster
centroids and the input (experimental, or observed) mass data centroids.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LowMassDeconvolverParameters::maxIsotopicClusterMatches

\brief The maximum number of matches between the calculated isotopic cluster
centroids and the input (experimental, or observed) mass data centroids.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LowMassDeconvolverParameters::clusterShapeCentroidCount

\brief The number of centroids in the isotopic cluster that must be used
to compute the centroid intensity ratio between the calculated and
input isotopic cluster centroids.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LowMassDeconvolverParameters::averagine

\brief The \l Averagine to be used for the calculation of the isotopic
clusters based on the input mass data neutral masses.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LowMassDeconvolverParameters::fullHeavyIsotopeLabelling

\brief The flag that indicates that the mass spectral data were obtained for
heavy isotope labelling (which changes the way mass spectral data are looked
into because the isotopic cluster shapes are inverted.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LowMassDeconvolverParameters::processingDetailsLogFilePath

\brief The path to the file in which all the processing details are stored for
forensic analysis of the whole deconvolution process.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LowMassDeconvolverParameters::totalClusterIntensityTolerance

\brief This float value (by default set to 0.5) is used at the end of the
process described below:

When a set of matches has occurred between a given theoretical cluster
and the corresponding matching input centroided mass spectrum centroids,
there might be a discrepancy between the sum of the intensities of the
theoretical cluster matched centroids and the intensities of the corresponding
input matched centroids. Of course the intensities are normalized to the
ratio between the intensity of the monoisotopic (highest probability)
theoretical cluster centroid and that of the matched input centroid. The
absolute difference between the summed observed intensities and the summed
theoretical intensity divided by the summed theoretical intensities reflect
the proportion of observed or theoretical signal that deviates from the
other. If that proportion is beyond this parameter, then there is too big
a mismatch between the theoretical and the observed clusters intensities.
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LowMassDeconvolverParameters::clusterShapeTolerance

\brief This float value (by default set to 0.7) is used at the end of the
process described below:

When comparing the relative intensity of the observed vs theoretical isotopic
cluster centroids, two-by-tow (isotopologues 0, then isotopologues 1), the
following value is the tolerance that is accepted for a deviation in the
observed centroid intensity with respect to the theoretical intensty. For
example 0.6 would mean: "tolerate that the intensity difference between
observed and theoretical isotopologues at position 0 of the cluster be up to
(0.6 * theoretical intensity): if (abs(obs_intensity - theo_intensity) >
0.6 * theoretical intensity -> REJECT.

*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::LowMassDeconvolverParameters::minPearsonCorrelationScore

\brief This float value (by default set to 0.3) is the minimum value to be
obtained when computing the Pearson correlation between the centroids
of the calculated isotopic cluster and the centroids from the matching input
mass data. The Pearson correlation involves the comparison of the intensities of
the different isotopic cluster's centroids in the observed centroided isotopic
cluster and in the calculated isotopic cluster.
*/

/*!
\brief Constructs an empty LowMassDeconvolverParameters instance.
*/
LowMassDeconvolverParameters::LowMassDeconvolverParameters()
{
}

/*!
\brief Constructs a LowMassDeconvolverParameters instance with all the
parameters.

For the description of the parameters, check their description.

\list

\li \a min_charge: minimum charge of the deconvolved peak
\li \a max_charge: maximum charge of the deconvolved peak
\li \a min_mass: minimum mass of the deconvolved peak
\li \a max_mass: maximum mass of the deconvolved peak
\li \a ppm_mass_tolerance: tolerance (in parts-per-million) for the mass matches
\li \a min_intensity: minimum intensity of the deconvolved mass peak
\li \a min_isotopic_cluster_matches: minimum number of matches to be obtained
\li \a max_isotopic_cluster_matches: maximum number of matches to be obtained

\li \a cluster_shape_centroid_count: the number of cluster centroids used to
compute the relative intensity ratios between the calculated isotopic cluster
centroids and their input mass data counterpart

\li \a averagine: \l Averagine describing the average composition of the
analytes

\li \a full_heavy_isotope_labelling: indicates if the deconvolution process
is going to be performed on mass spectral data obtained on analytes fully
labelled with stable heavy isotopes [13C] and/or [15N] such that the isotopic
cluster profiles are inverted.

\li \a processing_details_log_file_path: path to the file in which the
processing details are logged

\li \a total_cluster_intensity_tolerance: threshold below which matches between
theoretical data and input mass spectral data are rejected

\li \a relative_cluster_shape_tolerance: tolerance on the relative intensity of
the centroids in the calculated isotopic cluster with respect to the
experimental input cluster above which matches are rejected

\li \a min_pearson_correlation_score: threshold below which matches are
rejected. The Pearson correlation is calculate for the centroids of the
calculated isotopic cluster and of the input mass spectral isotopic cluster.

\endlist

\a averagine must contain a valid formula, as it is validated
against the isotopic data. The mass of the averagine formula (neutral formula)
is calculated and stored in the object.

Any failure with these assumptions triggers a qFatal() call.

 */
LowMassDeconvolverParameters::LowMassDeconvolverParameters(
  int min_charge,
  int max_charge,
  double min_mass,
  double max_mass,
  double ppm_mass_tolerance,
  double min_intensity,
  int min_isotopic_cluster_matches,
  int max_isotopic_cluster_matches,
  int cluster_shape_centroid_count,
  const Averagine &averagine,
  bool full_heavy_isotope_labelling,
  const QString &processing_details_log_file_path,
  double total_cluster_intensity_tolerance,
  double relative_cluster_shape_tolerance,
  double min_pearson_correlation_score)
  : minCharge(min_charge),
    maxCharge(max_charge),
    minMass(min_mass),
    maxMass(max_mass),
    ppmMassTolerance(ppm_mass_tolerance),
    minIntensity(min_intensity),
    minIsotopicClusterMatches(min_isotopic_cluster_matches),
    maxIsotopicClusterMatches(max_isotopic_cluster_matches),
    clusterShapeCentroidCount(cluster_shape_centroid_count),
    averagine(averagine),
    fullHeavyIsotopeLabelling(full_heavy_isotope_labelling),
    processingDetailsLogFilePath(processing_details_log_file_path),
    totalClusterIntensityTolerance(total_cluster_intensity_tolerance),
    clusterShapeTolerance(relative_cluster_shape_tolerance),
    minPearsonCorrelationScore(min_pearson_correlation_score)
{
  // qDebug() << "In the all-args constructor.";

  if(!averagine.isValid())
    qFatal() << "The Averagine instance is not valid.";

  // qDebug() << "Finished constructing the parameters:" << toString();
}

/*!
\brief Constructs an instance using \a other as a template.
*/
LowMassDeconvolverParameters::LowMassDeconvolverParameters(
  const LowMassDeconvolverParameters &other)
{
  // qDebug() << "In the copy constructor, other:" << other.toString();

  minCharge                 = other.minCharge;
  maxCharge                 = other.maxCharge;
  minMass                   = other.minMass;
  maxMass                   = other.maxMass;
  ppmMassTolerance          = other.ppmMassTolerance;
  minIntensity              = other.minIntensity;
  minIsotopicClusterMatches = other.minIsotopicClusterMatches;
  maxIsotopicClusterMatches = other.maxIsotopicClusterMatches;
  clusterShapeCentroidCount = other.clusterShapeCentroidCount;

  // qDebug() << "Other averagine:" << other.averagine.toString();

  averagine.initialize(other.averagine);
  if(!averagine.isValid())
    qFatal() << "The Averagine instance is not valid.";

  fullHeavyIsotopeLabelling      = other.fullHeavyIsotopeLabelling;
  processingDetailsLogFilePath   = other.processingDetailsLogFilePath;
  totalClusterIntensityTolerance = other.totalClusterIntensityTolerance;
  clusterShapeTolerance          = other.clusterShapeTolerance;
  minPearsonCorrelationScore     = other.minPearsonCorrelationScore;

  // qDebug() << "The Averagine constructed instance:" << toString();
}

/*!
\brief Initializes this instance using \a other as a template.

Returns a reference to this instance.
*/
LowMassDeconvolverParameters &
LowMassDeconvolverParameters::operator=(
  const LowMassDeconvolverParameters &other)
{
  // qDebug() << "In the operator=(), other:" << other.toString();

  if(&other == this)
    return *this;

  minCharge             = other.minCharge;
  maxCharge             = other.maxCharge;
  minMass               = other.minMass;
  maxMass               = other.maxMass;
  ppmMassTolerance      = other.ppmMassTolerance;
  minIntensity          = other.minIntensity;
  minIsotopicClusterMatches    = other.minIsotopicClusterMatches;
  maxIsotopicClusterMatches    = other.maxIsotopicClusterMatches;
  clusterShapeCentroidCount    = other.clusterShapeCentroidCount;

  // qDebug() << "Other averagine:" << other.averagine.toString();

  averagine.initialize(other.averagine);
  if(!averagine.isValid())
    qFatal() << "The Averagine instance is not valid.";

  fullHeavyIsotopeLabelling      = other.fullHeavyIsotopeLabelling;
  processingDetailsLogFilePath   = other.processingDetailsLogFilePath;
  totalClusterIntensityTolerance = other.totalClusterIntensityTolerance;
  clusterShapeTolerance = other.clusterShapeTolerance;
  minPearsonCorrelationScore   = other.minPearsonCorrelationScore;

  return *this;
}

/*!
\brief Initializes this instance using the parameters

\list

\li \a min_charge: minimum charge of the deconvolved peak
\li \a max_charge: maximum charge of the deconvolved peak
\li \a min_mass: minimum mass of the deconvolved peak
\li \a max_mass: maximum mass of the deconvolved peak
\li \a ppm_mass_tolerance: tolerance (in parts-per-million) for the mass matches
\li \a min_intensity: minimum intensity of the deconvolved mass peak
\li \a min_isotopic_cluster_matches: minimum number of matches to be obtained
\li \a max_isotopic_cluster_matches: maximum number of matches to be obtained

\li \a cluster_shape_centroid_count: the number of cluster centroids used to
compute the relative intensity ratios between the calculated isotopic cluster
centroids and their input mass data counterpart

\li \a the_averagine: \l Averagine describing the average composition of the
analytes

\li \a full_heavy_isotope_labelling: indicates if the deconvolution process
is going to be performed on mass spectral data obtained on analytes fully
labelled with stable heavy isotopes [13C] and/or [15N] such that the isotopic
cluster profiles are inverted.

\li \a processing_details_log_file_path: path to the file in which the
processing details are logged

\li \a total_cluster_intensity_tolerance: threshold below which matches between
theoretical data and input mass spectral data are rejected

\li \a relative_cluster_shape_tolerance: tolerance on the relative intensity of
the centroids in the calculated isotopic cluster with respect to the
experimental input cluster above which matches are rejected

\li \a min_pearson_correlation_score: threshold below which matches are
rejected. The Pearson correlation is calculate for the centroids of the
calculated isotopic cluster and of the input mass spectral isotopic cluster.

\endlist

\a the_averagine must contain a valid formula, as it is validated
against the isotopic data. The mass of the averagine formula (neutral formula)
is calculated and stored in the object.


Any failure with these assumptions triggers a qFatal() call.
*/
void
LowMassDeconvolverParameters::initialize(
  int min_charge,
  int max_charge,
  double min_mass,
  double max_mass,
  double ppm_mass_tolerance,
  double min_intensity,
  int min_isotopic_cluster_matches,
  int max_isotopic_cluster_matches,
  int cluster_shape_centroid_count,
  const Averagine &the_averagine,
  bool full_heavy_isotope_labelling,
  const QString &processing_details_log_file_path,
  double total_cluster_intensity_tolerance,
  int relative_cluster_shape_tolerance,
  double min_pearson_correlation_score)
{
  minCharge             = min_charge;
  maxCharge             = max_charge;
  minMass               = min_mass;
  maxMass               = max_mass;
  ppmMassTolerance      = ppm_mass_tolerance;
  minIntensity          = min_intensity;
  minIsotopicClusterMatches    = min_isotopic_cluster_matches;
  maxIsotopicClusterMatches    = max_isotopic_cluster_matches;
  clusterShapeCentroidCount    = cluster_shape_centroid_count;

  averagine.initialize(the_averagine);
  if(!averagine.isValid())
    qFatal() << "The Averagine instance is not valid.";

  fullHeavyIsotopeLabelling      = full_heavy_isotope_labelling;
  processingDetailsLogFilePath   = processing_details_log_file_path;
  totalClusterIntensityTolerance = total_cluster_intensity_tolerance;
  clusterShapeTolerance = relative_cluster_shape_tolerance;
  minPearsonCorrelationScore   = min_pearson_correlation_score;
}

/*!
\brief Crafts a text string describing the parameters and returns it.
*/
QString
LowMassDeconvolverParameters::toString() const
{
  QString text;
  text +=
    QString(
      "\nmin charge: %1 - "
      "max charge: %2 - "
      "min mass: %3 - "
      "max mass: %4 - "
      "mass tolerance (ppm): %5 - "
      "min intensity: %6 - "
      "min isotopic cluster centroid matches: %7 - "
      "max isotopic cluster centroid matches: %8 - "
      "cluster shape centroid count: %9 - "
      "full heavy isotope labelling: %10 - "
      "averagine isotopic data file path: %11 - "
      "averagine formula: '%12' - "
      "averagine mono mass: %13 - "
      "averagine avg mass: %14 - "
      "explained intensity threshold: %15 - "
      "min fit percentage for isotopic cluster shape: %16 - "
      "min score: %17\n")
      .arg(minCharge)
      .arg(maxCharge)
      .arg(minMass)
      .arg(maxMass)
      .arg(ppmMassTolerance, 0, 'f', 5)
      .arg(minIntensity)
      .arg(minIsotopicClusterMatches)
      .arg(maxIsotopicClusterMatches)
      .arg(clusterShapeCentroidCount)
      .arg(fullHeavyIsotopeLabelling ? "yes" : "no")
      .arg(averagine.getIsotopicData()->getFilePath())
      .arg(averagine.getFormulaCstRef().getActionFormula(true /*with_title*/))
      .arg(averagine.getMonoMass(), 0, 'f', 5)
      .arg(averagine.getAvgMass(), 0, 'f', 5)
      .arg(totalClusterIntensityTolerance, 0, 'f', 2)
      .arg(clusterShapeTolerance)
      .arg(minPearsonCorrelationScore);
  return text;
}

/*!
\class MsXpS::libXpertMassCore::Processing::AbstractLowMassDeconvolver
\inmodule libXpertMassCore
\ingroup XpertMassCoreProcessing
\inheaderfile MsXpS/libXpertMassCore/Processing/AbstractLowMassDeconvolver.hpp

\brief The AbstractLowMassDeconvolver class manages the deconvolution of
low mass analytes mass spectra.

By low mass analytes, it is meant that the
isotopic cluster they produce is shaped with the monoisotopic peak most
intense of all.

In the protein chemistry field, small analytes are those that are contained in
the [100 - 2000] dalton range.

The AbstractLowMassDeconvolver class serves as the base class to two classes
specialized in the low mass deconvolution of mass spectra acquired either on
natural isotopic abundance analytes (\l{LowMassDeconvolverNoLabelling}) or on
analytes fully labelled with stable heavy isotopes
(\l{LowMassDeconvolverFullLabelling}; which inverts the shape of the isotopic
cluster while maintaining the monoisotopic isotopologue peak the most intense of
all).
*/

/*!
\variable \
MsXpS::libXpertMassCore::Processing::AbstractLowMassDeconvolver::m_params

\brief The \l LowMassDeconvolverParameters member.
*/


/*!
\fn inline double AbstractLowMassDeconvolver::neutralToMz(double neutral_mass,
int charge) const
\brief Returns the m/z value calculated by ionizing  \a neutral_mass with \a
charge protons.

qFatal() is triggered if \a charge is 0.

\code
return (neutral_mass + charge * PROTON_MASS) / charge;
\endcode
*/

/*!
\fn inline double AbstractLowMassDeconvolver::mzToNeutral(double mz, int charge)
const
\brief Returns the neutral mass of the ion at m/z \a mz with charge \a charge.

qFatal() is triggered if \a charge is 0.

\code
return charge * (mz - PROTON_MASS);
\endcode

The ionization agent is the proton.
*/

/*!
\brief Constructs a LowMassDeconvolver instance using \a parameters.

The \a parameters structure members are used to initialize the isotopic data and
the \l Averagine formula. Any failure to do so triggers qFatal() calls.
*/
AbstractLowMassDeconvolver::AbstractLowMassDeconvolver(
  const LowMassDeconvolverParameters &parameters)
  : m_params(parameters)
{
  // qDebug() << "Got parameters:" << m_params.toString();

  if(!m_params.averagine.isValid())
    qFatal() << "The averagine is not valid.";

  // In this constructor we truncate the file.
  if(!m_params.processingDetailsLogFilePath.isEmpty())
    {
      // The QFile needs to be alive as long as the QTextStream is!
      mp_logFile = new QFile(m_params.processingDetailsLogFilePath);
      if(mp_logFile == nullptr)
        qFatal() << "Failed to create a new log file instance.";

      if(mp_logFile->open(QIODevice::WriteOnly | QIODevice::Truncate |
                          QIODevice::Text))
        {
          mp_logTextStream = new QTextStream(mp_logFile);
          *mp_logTextStream << "This is a processing details log file.\n\n";
          mp_logTextStream->flush();
        }
      else
        qFatal() << "Failed to open the log file for writing.";
    }

  QString processing_details_string =
    QString("LowMassDeconvolve parameters: %1\n\n").arg(m_params.toString());

  // qDebug() << processing_details_string;
  logProcessingDetails(processing_details_string);
}

/*!
\brief Destructs this instance.
*/
AbstractLowMassDeconvolver::~AbstractLowMassDeconvolver()
{
  // First destroy the stream!
  if(mp_logTextStream != nullptr)
    {
      // I think the flush is performed automatically upon destruction of the
      // stream, but anyways.
      mp_logTextStream->flush();
      delete mp_logTextStream;
    }

  // Next destroy the file!
  if(mp_logFile != nullptr)
    delete mp_logFile;
}

/*!
\brief Creates an isotopic cluster for \a formula with its intensity set to \a
intensity and returns it as a pappso::Trace.

The \l IsotopicClusterGenerator used for the calculation is configured using
the isotopic data provided in the m_params (\l LowMassDeconvolverParameters)
 \l Averagine member.

*/
pappso::Trace
AbstractLowMassDeconvolver::isotopicClusterForFormula(const QString &formula,
                                                      double intensity) const
{
  IsotopicClusterGenerator isotopic_cluster_generator(
    std::const_pointer_cast<IsotopicData>(
      m_params.averagine.getIsotopicData()));

  isotopic_cluster_generator.setIsotopicDataType(
    IsotopicDataType::LIBRARY_CONFIG);
  isotopic_cluster_generator.setMaxSummedProbability(0.95);
  isotopic_cluster_generator.setNormalizationIntensity(intensity);
  isotopic_cluster_generator.setSortType(pappso::Enums::SortType::x);
  isotopic_cluster_generator.setSortOrder(pappso::Enums::SortOrder::ascending);

  FormulaChargePair formula_charge_pair(formula, 0);

  isotopic_cluster_generator.setFormulaChargePair(formula_charge_pair);

  std::size_t count = isotopic_cluster_generator.run();

  std::vector<IsotopicClusterChargePair> isotopic_cluster_charge_pairs =
    isotopic_cluster_generator.getIsotopicClusterChargePairs();

  Q_ASSERT(count == isotopic_cluster_charge_pairs.size());

  return *isotopic_cluster_charge_pairs.at(0).first;
}

/*!
\brief Creates an isotopic cluster for \a neutral_mass with the specified \a
intensity and returns it as a pappso::Trace.

Any message that needs to be fed back to the caller is appended to \a message.

First, the neutral mass is used to establish a corresponding Averagine-based
neutral formula. The formula is then used to create the isotopic cluster.

\sa isotopicClusterForFormula.
*/
pappso::Trace
AbstractLowMassDeconvolver::isotopicClusterForNeutralMass(
  double neutral_mass, double intensity, QString &message) const
{
  bool ok;
  QString averagine_based_estimated_formula =
    m_params.averagine.elementalComposition(
      Enums::MassType::MONO, neutral_mass, false /*as_integer_indices*/, ok);

  if(averagine_based_estimated_formula.isEmpty())
    qFatal() << "Programming error. The elemental composition is empty.";

  message = QString("The averagine-based formula for neutral mass %1 is %2 - ")
              .arg(neutral_mass, 0, 'f', 5)
              .arg(averagine_based_estimated_formula);

  Formula averagine_formula(averagine_based_estimated_formula);

  ErrorList error_list;
  ok = averagine_formula.validate(m_params.averagine.getIsotopicData(),
                                  true /*store*/,
                                  true /*reset*/,
                                  &error_list);

  if(!ok)
    qFatal() << "Programming error. The averagine-based elemental composition "
                "formula failed to validate successfully.";

  ok = false;

  double mono_mass_for_formula = 0;
  double avg_mass_for_formula  = 0;

  averagine_formula.accountMasses(ok,
                                  m_params.averagine.getIsotopicData(),
                                  mono_mass_for_formula,
                                  avg_mass_for_formula,
                                  1 /*times*/);

  if(!ok)
    qFatal() << "Programming error. Failed to compute masses for the formula.";

  message += QString("The calculated mono mass for the formula: %1\n\n")
               .arg(mono_mass_for_formula, 0, 'f', 5);

  // qDebug() << message;

  pappso::Trace centroids =
    isotopicClusterForFormula(averagine_based_estimated_formula, intensity);

  if(!centroids.size())
    qFatal() << "Programming error. The returned centroids are empty.";

  return centroids;
}

/*!
\brief Converts the highly detailed (with hyperfine isotopic structure
centroids) \a theoretical_cluster isotopic cluster into a cluster where very
close peaks are merged (which is what is called the collapse). The new isotopic
cluster is returned as a pappso::Trace.

\sa BinnedIsotopicCentroid
*/
pappso::Trace
AbstractLowMassDeconvolver::collapseTheoreticalIsotopicCluster(
  const pappso::Trace &theoretical_cluster) const
{
  // This only works on neutral masses!

  Q_ASSERT(!theoretical_cluster.empty());

  // Get an iterator to the monoisotopic data point (highest intensity).
  pappso::Trace::const_iterator max_y_datapoint_iterator =
    pappso::maxYDataPoint(theoretical_cluster.begin(),
                          theoretical_cluster.end());

  if(max_y_datapoint_iterator == theoretical_cluster.end())
    qFatal() << "Programming error. Failed to get iterator to max Y datapoint "
                "in isotopic cluster.";

  const double mono_mass = max_y_datapoint_iterator->x;

  // isotopeIndex → (sum_mass, sum_intensity)
  std::map<int, std::pair<double, double>> bins;

  for(const pappso::DataPoint &dp : theoretical_cluster)
    {
      const double mass      = dp.x;
      const double intensity = dp.y;

      if(intensity <= 0.0)
        continue;

      const int isotope_index = static_cast<int>(
        std::llround((mass - mono_mass) / ISOTOPIC_NEUTRON_MASS));

      auto &bin   = bins[isotope_index];
      bin.first  += mass * intensity; // weighted mass sum
      bin.second += intensity;        // intensity sum
    }

  std::vector<BinnedIsotopicCentroid> result;
  result.reserve(bins.size());

  for(const auto &[iso_idx, sums] : bins)
    {
      const double weighted_mass = sums.first;
      const double total_int     = sums.second;

      if(total_int <= 0.0)
        continue;

      result.push_back(
        BinnedIsotopicCentroid{iso_idx, weighted_mass / total_int, total_int});
    }

  // Ensure strict ordering by isotope index
  std::sort(result.begin(), result.end(), [](const auto &a, const auto &b) {
    return a.isotopeIndex < b.isotopeIndex;
  });

  pappso::Trace collapsed_isotopic_cluster;

  for(const BinnedIsotopicCentroid &centroid : result)
    collapsed_isotopic_cluster.append(
      pappso::DataPoint(centroid.neutralMass, centroid.intensity));

  return collapsed_isotopic_cluster;
}

/*!
\brief Iterates in the \a input_centroids centroided mass spectrum
and filters all the data points that have their m/z or intensity value
outside of configured ranges (as defined in m_params).

Returns a new filtered pappso::Trace.
*/
pappso::Trace
AbstractLowMassDeconvolver::selectCandidatePeaks(
  const pappso::Trace &input_centroids) const
{
  std::vector<pappso::DataPoint> candidates;
  candidates.reserve(input_centroids.size());

  for(const pappso::DataPoint &dp : input_centroids)
    {
      // Skip peaks outside mass bounds
      if(dp.x < m_params.minMass || dp.x > m_params.maxMass)
        continue;

      // Skip peaks below intensity threshold
      if(dp.y < m_params.minIntensity)
        continue;

      candidates.push_back(dp);
    }

  return candidates;
}

/*!
\brief Iterates in the each ChargeSpecificFeature in \a
charge_specific_features and checks if multiple items in it correspond to the
same neutral analyte.

The notion here is that a given analyte might be found as ions of different
charges and the input centroids might thus have matched theoretical isotopic
clusters generated from different ChargeSpecificFeature instances of the same
analyte. Each time a ChargeSpecificFeature instance is iterated into, it
triggers either the allocation of a new DeconvolutedFeature instance if no
previous instance was found for the corresponding neutral mass, or the
update of an existing instance if an instance already existed with the same
neutral mass.

The created DeconvolutedFeature instances (neutral mass along with supporting
ions) are returned in a container.
*/
QList<DeconvolutedFeature>
AbstractLowMassDeconvolver::groupByNeutralMass(
  const std::vector<ChargeSpecificFeature> &charge_specific_features) const
{
  QString processing_details_string =
    "\n\nNow grouping ions by neutral mass.\n\n";

  QList<DeconvolutedFeature> deconvoluted_features;

  processing_details_string +=
    QString("Found %1 charge-specific features (ions).\n")
      .arg(charge_specific_features.size());

  for(const ChargeSpecificFeature &charge_specific_feature :
      charge_specific_features)
    {
      // Sanity check, there must be the matching isotopic cluster.
      if(charge_specific_feature.matchingIsotopicClusterQSPtr == nullptr)
        qFatal() << "Programming error. The pointer cannot be nullptr.";

      bool merged = false;

      // qDebug().noquote() << qSetRealNumberPrecision(5)
      //                    << "Now iterating into charge specific feature:"
      //                    << charge_specific_feature.toString();

      for(DeconvolutedFeature &iter_feature : deconvoluted_features)
        {
          // qDebug().noquote()
          //   << "At least one deconvoluted feature existed already, "
          //      "iterating into one:"
          //   << iter_feature.toString(true /*with_supporting_ions*/);

          double reference_mass = iter_feature.neutralMass;

          // delta_mass = | observed − reference |
          double delta_mass = std::abs(
            charge_specific_feature.monoisotopicNeutralMass - reference_mass);

          double delta_ppm = deltaToPpm(delta_mass, reference_mass);

          // qDebug().noquote()
          //   << qSetRealNumberPrecision(5) << "Delta mass:" << delta_mass
          //   << "reference mass:" << reference_mass
          //   << " - delta_ppm:" << delta_ppm
          //   << " - params tolerance (ppm):" << m_params.ppmMassTolerance
          //   << " - Merging only if delta_ppm is less than "
          //      "params tolerance (ppm)";

          if(delta_ppm <= m_params.ppmMassTolerance)
            {
              // Merge into existing feature.
              // qDebug() << "Merge new charge-specific feature into an
              // existing
              // "
              //             "deconvoluted feature.";

              SupportingIon ion;
              ion.charge         = charge_specific_feature.charge;
              ion.monoisotopicMz = charge_specific_feature.monoisotopicMz;
              ion.intensity = charge_specific_feature.monoisotopicIntensity;

              // Store a pointer to the matching theoretical isotopic cluster
              // so that the user of this class will be able to craft
              // a synthetic mass spectrum matching the observed mass spectrum
              // signal, which will help when double-checking the deconvolution
              // of border case isotopic cluster shapes.
              ion.matchingIsotopicClusterQSPtr =
                charge_specific_feature.matchingIsotopicClusterQSPtr;

              iter_feature.supportingIons.push_back(ion);

              // Add the intensity to the sum of intensities of all the
              // supporting ions for a given neutral mass.
              iter_feature.intensity +=
                charge_specific_feature.monoisotopicIntensity;

              // Aggregate FitQuality params.
              iter_feature.fitQuality.pearsonCorrelation =
                std::max(iter_feature.fitQuality.pearsonCorrelation,
                         charge_specific_feature.fitQuality.pearsonCorrelation);

              iter_feature.fitQuality.spacingErrorPpm =
                (iter_feature.fitQuality.spacingErrorPpm *
                   (iter_feature.intensity -
                    charge_specific_feature.monoisotopicIntensity) +
                 charge_specific_feature.fitQuality.spacingErrorPpm *
                   charge_specific_feature.monoisotopicIntensity) /
                iter_feature.intensity;

              iter_feature.fitQuality.explainedIntensity =
                (iter_feature.fitQuality.explainedIntensity *
                   (iter_feature.intensity -
                    charge_specific_feature.monoisotopicIntensity) +
                 charge_specific_feature.fitQuality.explainedIntensity *
                   charge_specific_feature.monoisotopicIntensity) /
                iter_feature.intensity;

              merged = true;
              break;
            }
        }

      if(!merged)
        {
          // Start new feature from scratch.

          processing_details_string +=
            QString(
              "Creating final new deconvoluted feature with neutral mass: "
              "%1.\n")
              .arg(charge_specific_feature.testedNeutralMass, 0, 'f', 5);

          DeconvolutedFeature feature;
          feature.neutralMass = charge_specific_feature.monoisotopicNeutralMass;
          feature.intensity   = charge_specific_feature.monoisotopicIntensity;
          feature.fitQuality  = charge_specific_feature.fitQuality;

          SupportingIon ion;
          ion.charge         = charge_specific_feature.charge;
          ion.monoisotopicMz = charge_specific_feature.monoisotopicMz;
          ion.intensity      = charge_specific_feature.monoisotopicIntensity;

          // Store a pointer to the matching theoretical isotopic cluster
          // so that the user of this class will be able to craft
          // a synthetic mass spectrum matching the observed mass spectrum
          // signal, which will help when double-checking the deconvolution
          // of border case isotopic cluster shapes.
          ion.matchingIsotopicClusterQSPtr =
            charge_specific_feature.matchingIsotopicClusterQSPtr;

          feature.supportingIons.push_back(ion);

          deconvoluted_features.append(std::move(feature));
        }
    }

  if(deconvoluted_features.size())
    {
      processing_details_string += QString("\n%1 deconvoluted feature(s):\n\n")
                                     .arg(deconvoluted_features.size());

      for(const DeconvolutedFeature &deconvoluted_feature :
          deconvoluted_features)
        {
          processing_details_string += QString("----->\n%1\n")
                                         .arg(deconvoluted_feature.toString(
                                           true /*with_supporting_ions*/));
          processing_details_string += "<-----\n\n";
        }
    }

  // qDebug().noquote() << processing_details_string;
  logProcessingDetails(processing_details_string);

  return deconvoluted_features;
}

/*!
\brief Converts the input \a neutral_isotopic_cluster into an isotopic cluster
of  \a charge and returns it as a new pappso::Trace.

\sa neutralToMz.
*/
pappso::Trace
AbstractLowMassDeconvolver::convertFromNeutralToChargedIsotopicCluster(
  const pappso::Trace &neutral_isotopic_cluster, int charge) const
{
  pappso::Trace new_charged_isotopic_cluster;

  for(const pappso::DataPoint &dp : neutral_isotopic_cluster)
    new_charged_isotopic_cluster.append(
      pappso::DataPoint(neutralToMz(dp.x, charge), dp.y));

  return new_charged_isotopic_cluster;
}

} // namespace Processing
} // namespace libXpertMassCore
} // namespace MsXpS
