From 594447ac821306cee89d2c09cadc0cb78357429c Mon Sep 17 00:00:00 2001 From: Johannes von Kleist <80823398+JohannesvKL@users.noreply.github.com> Date: Mon, 30 Sep 2024 08:24:37 +0200 Subject: [PATCH] Updating SageAdapter with new functionality + compatibility with new Sage versions (#7577) * First Attempt to cluster, data reading already works * Clustering test * Clustering with KDE works now, access to masses in unimod.obo implemented * CLustering functional, defect pushing mod-mass up during KDE still a probem * Clustering (with workaround), mapping, and reading to idXML completed * Output as a .tsv file complete, sorting based on charge implemented * Added some bug checks to help debug streamlit app * Added multiple masses being written to output * Removed print-bloat * Annotation meta values are now saved, FDR filtering implemented * Added annotation feature * Edited annotation notation * Fixed naming of output-table for PTMs * Added wide-window option, improved annotation * Updated variable modification specification to be compatible with sage v0.15.0 * Re-added some filtering, tweaked backend, wide window mode available now * Removed smoothing to speed up code, removed some old print statements * Cleaned up code a bit, preparing for PR * Removed unnecessary functions * Refactored code to be compliant with OpenMS coding conventions * Also updated minor changes in percolator to be form-compliant * Update AUTHORS * Update CHANGELOG * Changed matching back-end, removed white-space changes to other files * Added better handling for combinations of mods + speed-up through binary search * Changed ini, some code * Changed to ModificationsDB * THIRDPARTY submodule updated * Updated submodule to latest master * Refactored code and made mapping less buggy, changed two thresholds * Removed old ini file and fixed whitespac issues * Removed commented out code block * Added isotope check to mapping * Changed description of smoothing option * add doc * todo * Added parameter to check for Sage * Refactored and rewrote core parts, clarified names * Added parameter description to Percolator, updated ini * Updated ini file, updated .idXML test file, fixed weird segfault bug in percolator * Removed unnecessary files * Removed last unecessary table * Update src/openms/source/FORMAT/PercolatorInfile.cpp Co-authored-by: Timo Sachsenberg * Formatting fixes * Fixed build issue on Windows, fixed .idXML test-file * Changed cmake file for tests --------- Co-authored-by: Timo Sachsenberg --- AUTHORS | 1 + CHANGELOG | 5 + THIRDPARTY | 2 +- .../include/OpenMS/FORMAT/PercolatorInfile.h | 39 +- src/openms/source/FORMAT/PercolatorInfile.cpp | 152 ++++- src/tests/topp/THIRDPARTY/SageAdapter_1.ini | 29 +- .../topp/THIRDPARTY/SageAdapter_1_out.idXML | 39 +- .../THIRDPARTY/matched_fragments.sage.tsv | 29 + .../topp/THIRDPARTY/third_party_tests.cmake | 2 +- src/topp/SageAdapter.cpp | 625 +++++++++++++++++- vcpkg | 2 +- 11 files changed, 847 insertions(+), 78 deletions(-) create mode 100644 src/tests/topp/THIRDPARTY/matched_fragments.sage.tsv diff --git a/AUTHORS b/AUTHORS index 6db5bc54822..16fe55b1f56 100644 --- a/AUTHORS +++ b/AUTHORS @@ -53,6 +53,7 @@ the authors tag in the respective file header. - Johan Teleman - Johannes Junker - Johannes Veit + - Johannes von Kleist - Joshua Charkow - Julia Thueringer - Juliane Schmachtenberg diff --git a/CHANGELOG b/CHANGELOG index 162c48888e7..4bf3cce9a1a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -21,12 +21,15 @@ PR - Pull Request (on GitHub), i.e. integration of a new feature or bugfix ---- OpenMS 3.2.0 ---- ------------------------------------------------------------------------------------------ + + What's new: - Changes breaking backwards compatibility: - Rename of parameters for TOPP tool FeatureFinderCentroided (debug -> advanced), and PeakPickerWavelet/TOFCalibration (optimization -> optimization:type) (#7154) - Rename of parameters for TOPP tool IDFilter (score:pep -> score:psm; score:prot -> score:protein; score:protgroup -> score:proteingroup) with 'nan' as new default (#7541) - 3.2.0 KNIME package requires KNIME 5.3 or later - Support for SubsetNeighborSearch (SNS) via DecoyDatabase (#7565) +- SageAdapter received large updates including added functionality for PTM discovery + enabling features such as chimera seach, RT prediction, filtering by q-value, etc. Library: - Extend FileHandler to support load and store operations for our major datastructures (spectra, features, identifications, etc.). Replaced file type specific code with the more generic FileHandler calls to decouple the IO code from other parts of the library. @@ -43,6 +46,7 @@ New Tools: - AssayGeneratorMetaboSirius -- Assay library generation from a SIRIUS project directory (Metabolomics) - SiriusExport -- Metabolite identification using single and tandem mass spectrometry + Fixes: - FileConverter: more robust (#7176) - MSFragger: allow relative path to database (#7155) @@ -57,6 +61,7 @@ Fixes: - TOPPAS: open files in TOPPView (#7213) - pyOpenMS: Log warnings in pure Python code with warnings.warn instead of print (#7418) - more robust parsing of mzIdentML (#7153) +- SageAdapter now works with sage v0.15.0 and beyond - OpenSwath: Fix bug in diaPASEF window determination (#7546) Misc: diff --git a/THIRDPARTY b/THIRDPARTY index d6594eb775e..6c4aff683f5 160000 --- a/THIRDPARTY +++ b/THIRDPARTY @@ -1 +1 @@ -Subproject commit d6594eb775ebca0255f0129d946d7e582b37ac37 +Subproject commit 6c4aff683f5d6cf209a240a4e29fae7be16ce1ad diff --git a/src/openms/include/OpenMS/FORMAT/PercolatorInfile.h b/src/openms/include/OpenMS/FORMAT/PercolatorInfile.h index 2e5da34cba2..ccca14828eb 100644 --- a/src/openms/include/OpenMS/FORMAT/PercolatorInfile.h +++ b/src/openms/include/OpenMS/FORMAT/PercolatorInfile.h @@ -30,19 +30,42 @@ namespace OpenMS int min_charge, int max_charge); - /** @brief load pin file and convert to a vector of PeptideIdentification using the given score column @p score_name and orientation @p higher_score_better. - If a decoy prefix is provided, the decoy status is set from the protein accessions. - Otherwise, it assumes that the pin file already contains the correctly annotated decoy status. - If @p extra_scores is not empty, the scores are added to the PeptideHit as MetaValues. - If a filename column is encountered the set of @p filenames is filled in the order of appearance and PeptideIdentifications annotated with the id_merge_index meta value to link them to the filename (similar to a merged idXML file). - TODO: implement something similar to PepXMLFile().setPreferredFixedModifications(getModifications_(fixed_modifications_names)); - **/ + + /** + * @brief Loads peptide identifications from a Percolator input file. + * + * This function reads a Percolator input file (`pin_file`) and returns a vector of `PeptideIdentification` objects. + * It extracts relevantinformation such as peptide sequences, scores, charges, annotations, and protein accessions, applying + * specified thresholds and handling decoy targets as needed. + * Note: If a filename column is encountered the set of @p filenames is filled in the order of appearance and PeptideIdentifications annotated with the id_merge_index meta value to link them to the filename (similar to a merged idXML file). + * + * @param pin_file he path to the Percolator input file with a `.pin` extension. + * + * @param higher_score_better A boolean flag indicating whether higher scores are considered better (`true`) or lower scores are better (`false`). + * + * @param score_name The name of the primary score to be used for ranking peptide hits. + * + * @param extra_scores A list of additional score names that should be extracted and stored in each `PeptideHit`. + * + * @param filenames Will be populated with the unique raw file names extracted from the input data. + * + * @param decoy_prefix The prefix used to identify decoy protein accessions. Proteins with accessions starting with this prefix are marked as decoys. Otherwise, it assumes that the pin file already contains the correctly annotated decoy status. + * @param threshold A double value representing the threshold for the `spectrum_q` value. Only spectra with `spectrum_q` below this threshold are processed. + Implemented to allow prefiltering of Sage results. + * @param SageAnnotation A boolean value used to determine if the pin file is coming from Sage or not + * @return A `std::vector` of `PeptideIdentification` objects containing the peptide identifications. + + * @throws `Exception::ParseError` if any line in the input file does not have the expected number of columns. + * TODO: implement something similar to PepXMLFile().setPreferredFixedModifications(getModifications_(fixed_modifications_names)); + */ static std::vector load(const String& pin_file, bool higher_score_better, const String& score_name, const StringList& extra_scores, StringList& filenames, - String decoy_prefix = ""); + String decoy_prefix = "", + double threshold = 0.01, + bool SageAnnotation = false); // uses spectrum_reference, if empty uses spectrum_id, if also empty fall back to using index static String getScanIdentifier(const PeptideIdentification& pid, size_t index); diff --git a/src/openms/source/FORMAT/PercolatorInfile.cpp b/src/openms/source/FORMAT/PercolatorInfile.cpp index bf80f8b4fa1..76ba01299ed 100644 --- a/src/openms/source/FORMAT/PercolatorInfile.cpp +++ b/src/openms/source/FORMAT/PercolatorInfile.cpp @@ -3,7 +3,7 @@ // // -------------------------------------------------------------------------- // $Maintainer: Timo Sachsenberg $ -// $Authors: Timo Sachsenberg $ +// $Authors: Timo Sachsenberg, Johannes von Kleist $ // -------------------------------------------------------------------------- #include @@ -62,29 +62,105 @@ namespace OpenMS const String& score_name, const StringList& extra_scores, StringList& filenames, - String decoy_prefix) + String decoy_prefix, + double threshold, + bool SageAnnotation) { CsvFile csv(pin_file, '\t'); - StringList header; - csv.getRow(0, header); + + //Sage Variables, initialized in the following block if SageAnnotation is set + map> anno_mapping; + CsvFile tsv; + CsvFile annos; + unordered_map to_idx_t; + + if (SageAnnotation) // Block for special treatment of sage + { + String tsv_file_path = pin_file.substr(0, pin_file.size()-3); + tsv_file_path = tsv_file_path + "tsv"; + tsv = CsvFile(tsv_file_path,'\t'); + + String temp_diff = "results.sage.pin"; + String anno_file_path = pin_file.substr(0, pin_file.size()-temp_diff.length()); + anno_file_path = anno_file_path + "matched_fragments.sage.tsv"; + annos = CsvFile(anno_file_path, '\t'); + //map PSMID to vec of PeakAnnotation + StringList sage_tsv_header; + tsv.getRow(0, sage_tsv_header); + to_idx_t; // map column name to column index, for full .tsv file + { + int idx_t{}; + for (const auto& h : sage_tsv_header) { to_idx_t[h] = idx_t++; } + } + + // processs annotation file + StringList sage_annotation_header; + annos.getRow(0, sage_annotation_header); + unordered_map to_idx_a; // map column name to column index, for full annotation file file + { + int idx_a{}; + for (const auto& h : sage_annotation_header) { to_idx_a[h] = idx_a++; } + } + // map PSMs -> PeakAnnotation vector + auto num_rows = annos.rowCount(); + + for (size_t i = 1; i < num_rows; ++i) + { + StringList row; + annos.getRow(i, row); + + //Check if mapping already has PSM, if it does add + if (anno_mapping.find(row[to_idx_a.at("psm_id")].toInt()) == anno_mapping.end()) + { + //Make a new vector of annotations + PeptideHit::PeakAnnotation peak_temp; + + peak_temp.annotation = row[to_idx_a.at("fragment_type")] + row[to_idx_a.at("fragment_ordinals")]; + peak_temp.charge = row[to_idx_a.at("fragment_charge")].toInt(); + peak_temp.intensity = row[to_idx_a.at("fragment_intensity")].toDouble(); + peak_temp.mz = row[to_idx_a.at("fragment_mz_experimental")].toDouble(); + + vector temp_anno_vec; + temp_anno_vec.push_back(peak_temp); + anno_mapping[ row[to_idx_a.at("psm_id")].toInt() ] = temp_anno_vec; + } + else + { + //Add values to exisiting vector + PeptideHit::PeakAnnotation peak_temp; + + peak_temp.annotation = row[to_idx_a.at("fragment_type")] + row[to_idx_a.at("fragment_ordinals")]; + peak_temp.charge = row[to_idx_a.at("fragment_charge")].toInt(); + peak_temp.intensity = row[to_idx_a.at("fragment_intensity")].toDouble(); + peak_temp.mz = row[to_idx_a.at("fragment_mz_experimental")].toDouble(); + + anno_mapping[ row[to_idx_a.at("psm_id")].toInt() ].push_back(peak_temp); + } + } + } + + StringList pin_header; + + csv.getRow(0, pin_header); unordered_map to_idx; // map column name to column index { int idx{}; - for (const auto& h : header) { to_idx[h] = idx++; } + for (const auto& h : pin_header) { to_idx[h] = idx++; } } + // determine file name column index in percolator in file int file_name_column_index{-1}; - if (auto it = std::find(header.begin(), header.end(), "FileName"); it != header.end()) + if (auto it = std::find(pin_header.begin(), pin_header.end(), "FileName"); it != pin_header.end()) { - file_name_column_index = it - header.begin(); + file_name_column_index = it - pin_header.begin(); } - - // get column indices of extra scores - std::set found_extra_scores; // additional (non-main) scores that should be stored in the PeptideHit, order important for comparable idXML + + // determine extra scores and store column indices + std::set found_extra_scores; // additional (non-main) scores that should be stored in the PeptideHit, order important for comparable idXML for (const String& s : extra_scores) { - if (auto it = std::find(header.begin(), header.end(), s); it != header.end()) + if (auto it = std::find(pin_header.begin(), pin_header.end(), s); it != pin_header.end()) { found_extra_scores.insert(s); } @@ -93,7 +169,7 @@ namespace OpenMS OPENMS_LOG_WARN << "Extra score: " << s << " not found in Percolator input file." << endl; } } - + // charge columns are not standardized, so we check for the format and create hash to lookup column name to charge mapping std::regex charge_one_hot_pattern("^charge\\d+$"); std::regex sage_one_hot_pattern("^z=\\d+$"); @@ -104,7 +180,7 @@ namespace OpenMS // The reason is that sage searches always for the charge annotated in the spectrum raw file. Only if the annotation is missing it will search // the suggested charge range. bool found_sage_otherz_charge_column{false}; - for (const String& c : header) + for (const String& c : pin_header) { if (std::regex_match(c, charge_one_hot_pattern)) { @@ -136,11 +212,21 @@ namespace OpenMS StringList row; csv.getRow(i, row); - if (row.size() != header.size()) + StringList t_row; + + if (SageAnnotation) + { + tsv.getRow(i, t_row); + // skip if spectrum_q is above threshold + if (t_row[to_idx_t.at("spectrum_q")].toDouble() > threshold ) continue; + } + + if (row.size() != pin_header.size()) { - throw Exception::ParseError(__FILE__, __LINE__, OPENMS_PRETTY_FUNCTION, "Error: line " + String(i) + " of file '" + pin_file + "' does not have the same number of columns as the header!", String(i)); + throw Exception::ParseError(__FILE__, __LINE__, OPENMS_PRETTY_FUNCTION, "Error: line " + String(i) + " of file '" + pin_file + "' does not have the same number of columns as the pin_header!", String(i)); } + if (file_name_column_index >= 0) { raw_file_name = row[file_name_column_index]; @@ -150,7 +236,6 @@ namespace OpenMS map_filename_to_idx[raw_file_name] = filenames.size() - 1; } } - // NOTE: In our pin files that we WRITE, SpecID will be filename + vendor spectrum native ID // However, many search engines (e.g. Sage) choose arbitrary IDs, which is unfortunately allowed // by this loosely defined format. @@ -159,14 +244,12 @@ namespace OpenMS if (auto it = to_idx.find("ion_mobility"); it != to_idx.end()) { const String& sIM = row[it->second]; - const double IM = sIM.toDouble(); - pids.back().setMetaValue(Constants::UserParam::IM, IM); + const double IM = sIM.toDouble(); + if (!pids.empty()) pids.back().setMetaValue(Constants::UserParam::IM, IM); } - // In theory, this should be an integer, but Sage currently cannot extract the number from all vendor spectrum IDs, // so it writes the full ID as string String sScanNr = row[to_idx.at("ScanNr")]; - if (sSpecId != spec_id) { pids.resize(pids.size() + 1); @@ -174,13 +257,12 @@ namespace OpenMS pids.back().setScoreType(score_name); pids.back().setMetaValue(Constants::UserParam::ID_MERGE_INDEX, map_filename_to_idx.at(raw_file_name)); pids.back().setRT(row[to_idx.at("retentiontime")].toDouble() * 60.0); // search engines typically write minutes (e.g., sage) - pids.back().setMetaValue("PinSpecId", sSpecId); + pids.back().setMetaValue("PinSpecId", sSpecId); // Since ScanNr is the closest to help in identifying the spectrum in the file later on, // we use it as spectrum_reference. Since it can be integer only or the complete // vendor ID, you will need a lookup in case of number only later!! - pids.back().setSpectrumReference(sScanNr); + pids.back().setSpectrumReference(sScanNr); } - String sPeptide = row[to_idx.at("Peptide")]; const double score = row[to_idx.at(score_name)].toDouble(); String target_decoy = row[to_idx.at("Label")].toInt() == 1 ? "target" : "decoy"; @@ -253,12 +335,33 @@ namespace OpenMS AASequence aa_seq = AASequence::fromString(sPeptide); PeptideHit ph(score, rank, charge, std::move(aa_seq)); ph.setMetaValue("target_decoy", target_decoy); + for (const auto& name : found_extra_scores) { ph.setMetaValue(name, row[to_idx.at(name)]); } ph.setRank(rank); + // adding own meta values + if (SageAnnotation) + { + ph.setMetaValue("spectrum_q", t_row[to_idx_t.at("spectrum_q")].toDouble()); //TODO: check if column exists / SAGE specific treatment + } + ph.setMetaValue("DeltaMass", ( row[to_idx.at("ExpMass")].toDouble() - row[to_idx.at("CalcMass")].toDouble()) ); + // add annotations + if (SageAnnotation) + { + if (anno_mapping.find(sSpecId.toInt()) != anno_mapping.end()) + { + // copy annotations from mapping to PeptideHit + vector pep_vec; + for (const PeptideHit::PeakAnnotation& pep : anno_mapping[sSpecId.toInt()]) + { + pep_vec.push_back(pep) ; + } + ph.setPeakAnnotations(pep_vec); + } + } // add link to protein (we only know the accession but not start/end, aa_before/after in protein at this point) for (const String& accession : accessions) { @@ -267,6 +370,7 @@ namespace OpenMS pids.back().insertHit(std::move(ph)); } + return pids; } @@ -542,4 +646,4 @@ namespace OpenMS return count; } -} +} \ No newline at end of file diff --git a/src/tests/topp/THIRDPARTY/SageAdapter_1.ini b/src/tests/topp/THIRDPARTY/SageAdapter_1.ini index eeb1b291f80..cb576f38aea 100644 --- a/src/tests/topp/THIRDPARTY/SageAdapter_1.ini +++ b/src/tests/topp/THIRDPARTY/SageAdapter_1.ini @@ -1,14 +1,14 @@ - + - + @@ -19,7 +19,7 @@ - + @@ -31,13 +31,28 @@ + + + + + + + + + + + + + + + + - - + @@ -50,6 +65,10 @@ + + + + diff --git a/src/tests/topp/THIRDPARTY/SageAdapter_1_out.idXML b/src/tests/topp/THIRDPARTY/SageAdapter_1_out.idXML index 999dc827a09..10ed2e21077 100644 --- a/src/tests/topp/THIRDPARTY/SageAdapter_1_out.idXML +++ b/src/tests/topp/THIRDPARTY/SageAdapter_1_out.idXML @@ -1,14 +1,14 @@ - + - + - - + + @@ -36,10 +36,17 @@ + + + + + + + + - @@ -58,29 +65,33 @@ - + - - + + + - + + + - - + + - - + + + - + diff --git a/src/tests/topp/THIRDPARTY/matched_fragments.sage.tsv b/src/tests/topp/THIRDPARTY/matched_fragments.sage.tsv new file mode 100644 index 00000000000..06234e0c23b --- /dev/null +++ b/src/tests/topp/THIRDPARTY/matched_fragments.sage.tsv @@ -0,0 +1,29 @@ +psm_id fragment_type fragment_ordinals fragment_charge fragment_mz_calculated fragment_mz_experimental fragment_intensity +1 b 2 1 242.14992 242.14989 540839.4 +1 b 4 1 485.28305 485.28275 552907.0 +1 b 5 1 582.3358 582.33417 249718.42 +1 b 6 1 653.3729 653.3723 448620.62 +1 b 7 1 724.41003 724.4097 7813033.5 +1 b 7 2 362.70865 362.70834 240484.1 +1 b 8 1 821.46277 821.4622 348964.8 +1 b 8 2 411.23502 411.235 1077902.5 +1 b 9 2 459.76138 459.76132 257510.27 +1 b 10 1 989.5526 989.55237 7521718.0 +1 b 10 2 495.27994 495.27847 315987.97 +1 b 12 1 1143.627 1143.6226 443099.75 +1 y 17 2 843.4759 843.4913 637089.7 +1 y 12 2 602.3459 602.3467 691258.44 +1 y 11 1 1106.6318 1106.6305 1731329.5 +1 y 10 1 1009.57904 1009.57855 1041480.6 +1 y 9 1 938.54193 938.5417 9045039.0 +1 y 9 2 469.7746 469.77466 2463108.8 +1 y 8 1 841.4892 841.4886 4294338.0 +1 y 8 2 421.24823 421.24786 259093.06 +1 y 7 1 784.4677 784.46735 3691260.2 +1 y 7 2 392.7375 392.73746 983722.6 +1 y 6 1 687.415 687.4143 1646364.6 +1 y 5 1 630.3935 630.3935 603377.6 +1 y 4 1 502.3349 502.33484 1054908.6 +1 y 3 1 389.25085 389.251 712993.75 +1 y 2 1 288.2032 288.20273 213723.94 +1 y 1 1 175.11914 175.11903 675764.75 diff --git a/src/tests/topp/THIRDPARTY/third_party_tests.cmake b/src/tests/topp/THIRDPARTY/third_party_tests.cmake index 84d38d08711..a88ebe49ebc 100644 --- a/src/tests/topp/THIRDPARTY/third_party_tests.cmake +++ b/src/tests/topp/THIRDPARTY/third_party_tests.cmake @@ -127,7 +127,7 @@ endif() if (NOT (${SAGE_BINARY} STREQUAL "SAGE_BINARY-NOTFOUND")) ### NOT needs to be added after the binarys have been included add_test("TOPP_SageAdapter_1" ${TOPP_BIN_PATH}/SageAdapter -test -ini ${DATA_DIR_TOPP}/THIRDPARTY/SageAdapter_1.ini -database ${DATA_DIR_TOPP}/THIRDPARTY/SageAdapter_1.fasta -in ${DATA_DIR_TOPP}/THIRDPARTY/SageAdapter_1.mzML -out SageAdapter_1_out.tmp.idXML -sage_executable "${SAGE_BINARY}") - add_test("TOPP_SageAdapter_1_out1" ${DIFF} -in1 SageAdapter_1_out.tmp.idXML -in2 ${DATA_DIR_TOPP}/THIRDPARTY/SageAdapter_1_out.idXML -whitelist "search_engine_version" "IdentificationRun date" "spectra_data" "SearchParameters id=\"SP_0\" db=" "UserParam type=\"stringList\" name=\"SageAdapter:1:in\" value=" "UserParam type=\"string\" name=\"SageAdapter:1:database\" value=" "UserParam type=\"string\" name=\"SageAdapter:1:sage_executable\" value=") + add_test("TOPP_SageAdapter_1_out1" ${DIFF} -in1 SageAdapter_1_out.tmp.idXML -in2 ${DATA_DIR_TOPP}/THIRDPARTY/SageAdapter_1_out.idXML -whitelist "search_engine_version" "IdentificationRun date" "spectra_data" "SearchParameters id="SP_0" db=" "UserParam type="stringList" name="SageAdapter:1:in" value=" "UserParam type="string" name="SageAdapter:1:database" value=" "UserParam type="string" name="SageAdapter:1:sage_executable" value=" "fragment_annotation") set_tests_properties("TOPP_SageAdapter_1_out1" PROPERTIES DEPENDS "TOPP_SageAdapter_1") endif() diff --git a/src/topp/SageAdapter.cpp b/src/topp/SageAdapter.cpp index 549f46533e2..8feccf03b33 100644 --- a/src/topp/SageAdapter.cpp +++ b/src/topp/SageAdapter.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -29,9 +30,18 @@ #include #include +#include +#include +#include +#include +#include +#include + +#include using namespace OpenMS; using namespace std; +using boost::math::normal; //------------------------------------------------------------- //Doxygen docu @@ -72,8 +82,13 @@ because of limitations in OpenMS' data structures and file formats. /// @cond TOPPCLASSES -/* -*/ +#define CHRONOSET + +#ifndef M_PI + #define M_PI 3.14159265358979323846 +#endif + + class TOPPSageAdapter : public SearchEngineBase @@ -90,6 +105,486 @@ class TOPPSageAdapter : { } + // Saves details of PTMs as well, useful for if more than one PTM is mapped to a given mass + struct modification + { + double count = 0; + vector mass; + int numcharges = 0; + }; + + // Define a struct to hold modification data + struct ModData + { + int count; // Modification rate + String name; // Modification name + int numcharges; // Number of charges + vector masses; // Masses associated with the modification + }; + + // Comparator for approximate comparison of double values + struct FuzzyDoubleComparator { + double epsilon; + FuzzyDoubleComparator(double eps = 1e-9) : epsilon(eps) {} + bool operator()(const double& a, const double& b) const + { + return std::fabs(a - b) >= epsilon && a < b; + } + }; + +// delta mass counts to delta masses +//typedef map CountToDeltaMass; + +typedef map DeltaMassHistogram; // maps delta mass to count +typedef map DeltaMasstoCharge; // maps delta mass to count + +// Gaussian function +static double gaussian(double x, double sigma) { + return exp(-(x*x) / (2 * sigma*sigma)) / (sigma * sqrt(2 * M_PI)); +} + +// Smooths the PTM-mass histogram , uses a Kernel Density Estimation on top of the histogram. +// Smooths the PTM-mass histogram using Gaussian Kernel Density Estimation (KDE). +static DeltaMassHistogram smoothDeltaMassHist(const DeltaMassHistogram& hist, double sigma = 0.001) +{ + if (hist.size() < 3) + { + return hist; //Not enough data points for smoothing + } + // Create a smoothed histogram with a fuzzy comparator for floating-point keys + DeltaMassHistogram smoothed_hist(FuzzyDoubleComparator(1e-9)); + + // Extract delta masses and counts into vectors for efficient access + std::vector deltas; + std::vector counts; + deltas.reserve(hist.size()); + counts.reserve(hist.size()); + + for (const auto& [delta, count] : hist) + { + deltas.push_back(delta); + counts.push_back(count); + } + + const size_t n = deltas.size(); + std::vector smoothed_counts(n, 0.0); + + // Perform Gaussian smoothing + for (size_t i = 0; i < n; ++i) + { + double weight_sum = 0.0; + + for (size_t j = 0; j < n; ++j) + { + double mz_diff = deltas[i] - deltas[j]; + + // Ignore points beyond 3 standard deviations + if (std::abs(mz_diff) > 3.0 * sigma) + continue; + + double weight = gaussian(mz_diff, sigma); + smoothed_counts[i] += weight * counts[j]; + weight_sum += weight; + } + + if (weight_sum != 0.0) + { + smoothed_counts[i] /= weight_sum; + } + } + + // Populate the smoothed histogram + for (size_t i = 0; i < n; ++i) + { + smoothed_hist[deltas[i]] = smoothed_counts[i]; + } + + return smoothed_hist; +} + +// Identifies local maxima in the delta mass histogram based on count threshold and SNR. +static DeltaMassHistogram findPeaksInDeltaMassHistogram(const DeltaMassHistogram& hist, double count_threshold = 0.0, double SNR = 2.0) +{ + if (hist.size() < 3) + { + return hist; // Not enough data points to find peaks + } + + DeltaMassHistogram peaks(FuzzyDoubleComparator(1e-9)); + + // Extract counts to compute noise level (median count) + std::vector counts; + counts.reserve(hist.size()); + + for (const auto& [_, count] : hist) + { + counts.push_back(count); + } + + // Calculate median as noise level + std::nth_element(counts.begin(), counts.begin() + counts.size() / 2, counts.end()); + double noise_level = counts[counts.size() / 2]; + + // Convert histogram to vector for indexed access + std::vector> hist_vector(hist.begin(), hist.end()); + + // Check each point except the first and last for local maxima + for (size_t i = 1; i < hist_vector.size() - 1; ++i) + { + double prev_count = hist_vector[i - 1].second; + double curr_count = hist_vector[i].second; + double next_count = hist_vector[i + 1].second; + + // Check if current point is a local maximum + if (curr_count >= prev_count && curr_count >= next_count && + curr_count > count_threshold && + curr_count / noise_level > SNR) + { + peaks[hist_vector[i].first] = curr_count; + } + } + + return peaks; +} + +// Returns the maxima of a histogram from the delta masses of each peptide. +std::pair getDeltaClusterCenter(const std::vector& pips, bool smoothing = false, bool debug = false) +{ + // Constants + constexpr double deltamass_tolerance = 0.0005; + constexpr double delta_mass_zero_treshold = 0.05; + + // Lambda to round values to the specified tolerance + auto roundToTolerance = [deltamass_tolerance](double value) { + return std::round(value / deltamass_tolerance) * deltamass_tolerance; + }; + + // Data structures to store histogram and charge states + DeltaMassHistogram hist(FuzzyDoubleComparator(1e-9)); + DeltaMasstoCharge num_charges_at_mass(FuzzyDoubleComparator(1e-9)); + std::unordered_map> charge_states; + + // Process each peptide identification + for (const auto& id : pips) + { + const auto& hits = id.getHits(); + for (const auto& hit : hits) + { + // Retrieve delta mass and charge + double delta_mass = hit.getMetaValue("DeltaMass"); + int charge = hit.getCharge(); + + // Ignore delta masses close to zero + if (std::abs(delta_mass) <= delta_mass_zero_treshold) + continue; + + // Round delta mass to bin similar values + double rounded_mass = roundToTolerance(delta_mass); + + // Update histogram count + hist[rounded_mass] += 1.0; + + // Update unique charge count + if (charge_states[rounded_mass].insert(charge).second) + { + num_charges_at_mass[rounded_mass] += 1.0; + } + } + } + + // Prepare results + std::pair results; + results = { hist, num_charges_at_mass }; + + // Apply smoothing if requested + if (smoothing) + { + DeltaMassHistogram smoothed_hist = smoothDeltaMassHist(hist, 0.0001); + DeltaMassHistogram hist_maxima = findPeaksInDeltaMassHistogram(smoothed_hist, 0.0, 3.0); + + // Update charge counts for the smoothed maxima + DeltaMasstoCharge num_charges_at_mass_smoothed(FuzzyDoubleComparator(1e-9)); + for (const auto& [mass, _] : hist_maxima) + { + num_charges_at_mass_smoothed[mass] = num_charges_at_mass[mass]; + } + + // Update results with smoothed data + results = { hist_maxima, num_charges_at_mass_smoothed }; + } + + return results; +} + +//Fucntion that maps a selection of masses to certain PTMs and returns a summary of said PTMs. Also adds PTM for each petide without in-peptide localization. +vector mapDifftoMods(DeltaMassHistogram hist, DeltaMasstoCharge charge_hist, vector& pips, double precursor_mass_tolerance_ = 5, bool precursor_mass_tolerance_unit_ppm = true, String outfile = "") +{ + vector> clusters(hist.size(), vector()); + map mass_of_mods(FuzzyDoubleComparator(1e-9)); + vector> mass_of_mods_vec; + + // Load modifications from the database + vector searchmodifications_names; + ModificationsDB* mod_db = ModificationsDB::getInstance(); + mod_db->getAllSearchModifications(searchmodifications_names); + for (const String& m : searchmodifications_names) + { + const ResidueModification* residue = mod_db->getModification(m); + String res_name = residue->getFullName(); + double res_diffmonoMass = residue->getDiffMonoMass(); + if (res_name.find("substitution") == string::npos) + mass_of_mods[res_diffmonoMass] = res_name; + } + + // Generate combinations of modifications + map combo_mods(FuzzyDoubleComparator(1e-9)); + for (auto mit = mass_of_mods.begin(); mit != mass_of_mods.end(); ++mit) + { + for (auto mit2 = mit; mit2 != mass_of_mods.end(); ++mit2) + { + combo_mods[mit->first + mit2->first] = mit->second + "++" + mit2->second; + } + } + + // Variables for mapping + StringList modnames; + map modifications; + map hist_found; + + // Helper function to add or update modifications + auto addOrUpdateModification = [&](const String& mod_name, double mass, double count, int numcharges) + { + if (modifications.find(mod_name) == modifications.end()) + { + modification modi{}; + modi.mass.push_back(mass); + modi.count = count; + modi.numcharges = numcharges; + modifications[mod_name] = modi; + } + else + { + modifications[mod_name].count += count; + modifications[mod_name].numcharges = max(numcharges, modifications[mod_name].numcharges); + } + }; + + // Mapping with tolerances //TODO: fix code again, add back high_it + for (const auto& hist_entry : hist) + { + //Values from the histogram + double current_cluster_mass = hist_entry.first; + double count = hist_entry.second; + + double lowerbound, upperbound; + + const double epsilon = 1e-8; + + if (precursor_mass_tolerance_unit_ppm) // ppm + { + double tolerance = current_cluster_mass * precursor_mass_tolerance_ * 1e-6; + lowerbound = current_cluster_mass - tolerance; + upperbound = current_cluster_mass + tolerance; + } + else // Dalton + { + lowerbound = current_cluster_mass - precursor_mass_tolerance_; + upperbound = current_cluster_mass + precursor_mass_tolerance_; + } + + // Search for modifications within bounds + bool mapping_found = false; + String mod_name; + double mod_mass = 0.0; + + // Search in single modifications using lower_bound + auto it_lower = mass_of_mods.lower_bound(lowerbound - epsilon); + bool found_lower = false; + if (it_lower != mass_of_mods.end() && fabs(it_lower->first - current_cluster_mass) <= precursor_mass_tolerance_) + { + found_lower = true; + } + + // Search in single modifications using upper_bound + auto it_upper = mass_of_mods.upper_bound(upperbound + epsilon); + bool found_upper = false; + if (it_upper != mass_of_mods.begin()) + { + --it_upper; // Move to the largest element <= upperbound + if (fabs(it_upper->first - current_cluster_mass) <= precursor_mass_tolerance_) + { + found_upper = true; + } + } + + // Compare results from lower_bound and upper_bound + if (found_lower && found_upper) + { + if (it_lower->first == it_upper->first && it_lower->second == it_upper->second) + { + // Both methods found the same modification + mod_name = it_lower->second; + mod_mass = it_lower->first; + hist_found[mod_mass] = mod_name; + mapping_found = true; + } + else + { + // Different results from lower_bound and upper_bound + // Choose the closer one + mod_name = it_lower->second + "//" + it_upper->second; + mod_mass = current_cluster_mass; + hist_found[it_lower->first] = it_lower->second; + hist_found[it_upper->first] = it_upper->second; + mapping_found = true; + } + } + else + { + // Check if modification can be explained by known modifications + for (const auto& hit : hist_found) + { + if (fabs(hit.first - current_cluster_mass) < precursor_mass_tolerance_) + { + addOrUpdateModification(hit.second, hit.first, count, charge_hist[current_cluster_mass]); + mapping_found = true; + break; + } // Check if modification can be explained by a +1 Isotope variant of a known modification + else if (fabs((hit.first + 1) - current_cluster_mass) < precursor_mass_tolerance_) + { + String temp_mod_name = hit.second + "+1Da"; + addOrUpdateModification(temp_mod_name, hit.first + 1, count, charge_hist[current_cluster_mass]); + hist_found[hit.first + 1] = temp_mod_name; + mapping_found = true; + break; + } + } + // Search in combination modifications + if (!mapping_found) + { + auto it = combo_mods.lower_bound(current_cluster_mass - epsilon); + if (it != combo_mods.end() && fabs(it->first - current_cluster_mass) <= precursor_mass_tolerance_ / 10) + { + mod_name = it->second; + mod_mass = it->first; + mapping_found = true; + } + } + } + if (fabs(mod_mass) < precursor_mass_tolerance_) continue; //If the closest mod_mass is too close to 0, continue + + if (mapping_found) + { + modnames.push_back(mod_name); + addOrUpdateModification(mod_name, mod_mass, count, charge_hist[current_cluster_mass]); + } + else + { + // Unknown modification + String unknown_mod_name = "Unknown" + std::to_string(std::round(current_cluster_mass)); + addOrUpdateModification(unknown_mod_name, current_cluster_mass, count, charge_hist[current_cluster_mass]); + } + } + + // Collect all modification data into a vector + vector mods_by_count; + + //Fill vetcor + for (const auto& mod_pair : modifications) + { + ModData mod_data; + mod_data.count = std::round(mod_pair.second.count); + mod_data.name = mod_pair.first; + mod_data.numcharges = mod_pair.second.numcharges; + mod_data.masses = mod_pair.second.mass; + + mods_by_count.push_back(mod_data); + } + + // Sort the modifications based on (numcharges + rate) in descending order + sort(mods_by_count.begin(), mods_by_count.end(), + [](const ModData& a, const ModData& b) + { + return (a.numcharges + a.count) > (b.numcharges + b.count); + }); + + // Add the modifications to the output for each peptide + for (auto& id : pips) + { + auto& hits = id.getHits(); + for (auto& h : hits) + { + double deltamass = h.getMetaValue("DeltaMass"); + String PTM = ""; + + // Check if too close to zero + if (fabs(deltamass) < 0.05) + { + h.setMetaValue("PTM", PTM); + continue; + } + + bool found = false; + // Check with error tolerance if already present in histogram + for (const auto& mit : hist_found) + { + if (fabs(deltamass - mit.first) < precursor_mass_tolerance_) + { + PTM = mit.second; + found = true; + break; + } + } + //Otherwise assign unkwown + if (!found) + { + PTM = "Unknown" + String(deltamass); + } + h.setMetaValue("PTM", PTM); + } + } + // Remove 'idxml' from output file name and write the table + String output_tab = outfile.substr(0, outfile.size() - 5) + "_OutputTable.tsv"; + std::ofstream outfile_stream(output_tab); + + // Check if the file was opened successfully + if (!outfile_stream.is_open()) + { + std::cerr << "Error opening file: " << output_tab << std::endl; + // Handle the error appropriately, e.g., return or exit + return pips; // Assuming pips is the default return value + } + + outfile_stream << "Name\tMass\tModified Peptides (incl. charge variants)\tModified Peptides\n"; + + // Iterate over the data and write to the file + for (const auto& mod_data : mods_by_count) + { + outfile_stream << mod_data.name << '\t'; + + // Output mass or masses + if (mod_data.masses.size() < 2) + { + outfile_stream << mod_data.masses.at(0) << '\t'; + } + else + { + outfile_stream << mod_data.masses.at(0) << "/" << mod_data.masses.at(1) << '\t'; + } + + // Output rounded values + outfile_stream << mod_data.numcharges + mod_data.count << '\t' + << mod_data.count << '\n'; + } + + // Close the file + outfile_stream.close(); + + //Return the peptides with the additional PTM column + return pips; +} + + protected: // create a template-based configuration file for sage // variable values correspond to sage parameter that can be configured via TOPP tool parameter. @@ -141,7 +636,7 @@ class TOPPSageAdapter : }, "max_variable_mods": ##max_variable_mods##, "generate_decoys": false, - "decoy_tag": "##decoy_tag##" + "decoy_tag": "##decoy_prefix##" }, "precursor_tol": { "##precursor_tol_unit##": [ @@ -161,14 +656,14 @@ class TOPPSageAdapter : "isotope_errors": [ ##isotope_errors## ], - "deisotope": false, - "chimera": false, - "wide_window": false, - "predict_rt": false, + "deisotope": ##deisotope##, + "chimera": ##chimera##, + "predict_rt": ##predict_rt##, "min_peaks": ##min_peaks##, "max_peaks": ##max_peaks##, "min_matched_peaks": ##min_matched_peaks##, - "report_psms": ##report_psms## + "report_psms": ##report_psms##, + "wide_window": ##wide_window## } )"; @@ -177,7 +672,7 @@ class TOPPSageAdapter : { String origin; if (mod->getTermSpecificity() == ResidueModification::N_TERM) - { + { origin += "^"; } else if (mod->getTermSpecificity() == ResidueModification::C_TERM) @@ -244,7 +739,14 @@ class TOPPSageAdapter : config_file.substitute("##min_peaks##", String(getIntOption_("min_peaks"))); config_file.substitute("##max_peaks##", String(getIntOption_("max_peaks"))); config_file.substitute("##report_psms##", String(getIntOption_("report_psms"))); - config_file.substitute("##decoy_tag##", String(getStringOption_("decoy_prefix"))); + config_file.substitute("##deisotope##", getStringOption_("deisotope")); + config_file.substitute("##chimera##", getStringOption_("chimera")); + config_file.substitute("##predict_rt##", getStringOption_("predict_rt")); + config_file.substitute("##decoy_prefix##", getStringOption_("decoy_prefix")); + config_file.substitute("##wide_window##", getStringOption_("wide_window")); + + + //Look at decoy handling String enzyme = getStringOption_("enzyme"); String enzyme_details; @@ -338,12 +840,12 @@ class TOPPSageAdapter : config_file.substitute("##enzyme_details##", enzyme_details); + auto fixed_mods = getStringList_("fixed_modifications"); set fixed_unique(fixed_mods.begin(), fixed_mods.end()); fixed_mods.assign(fixed_unique.begin(), fixed_unique.end()); ModifiedPeptideGenerator::MapToResidueType fixed_mod_map = ModifiedPeptideGenerator::getModifications(fixed_mods); // std::unordered_map val; String static_mods_details = getModDetailsString(fixed_mod_map); - config_file.substitute("##static_mods##", static_mods_details); auto variable_mods = getStringList_("variable_modifications"); set variable_unique(variable_mods.begin(), variable_mods.end()); @@ -351,7 +853,34 @@ class TOPPSageAdapter : ModifiedPeptideGenerator::MapToResidueType variable_mod_map = ModifiedPeptideGenerator::getModifications(variable_mods); String variable_mods_details = getModDetailsString(variable_mod_map); - config_file.substitute("##variable_mods##", variable_mods_details); + //Treat variables as list for sage v0.15 and beyond + StringList static_mods_details_list; + StringList variable_mods_details_list; + + String static_mods_details_split = static_mods_details; + String variable_mods_details_split = variable_mods_details; + static_mods_details_split.split(",", static_mods_details_list); + variable_mods_details_split.split(",", variable_mods_details_list); + + String temp_String_var; + for (auto& x : variable_mods_details_list) + { + StringList temp_split; + x.split(":", temp_split); + + temp_split.insert(temp_split.begin()+1, ":["); + temp_split.insert(temp_split.end(), "]"); + String temp_split_Str = ""; + + for (auto& y : temp_split) + { + temp_split_Str = temp_split_Str + y; + } + temp_String_var = temp_String_var + "," + temp_split_Str ; + } + String temp_String_var_Fin = temp_String_var.substr(1, temp_String_var.size()-1); + config_file.substitute("##static_mods##", static_mods_details); + config_file.substitute("##variable_mods##", temp_String_var_Fin); return config_file; } @@ -419,6 +948,7 @@ class TOPPSageAdapter : "Can be negative. E.g. '-1,3' for considering '-1/0/1/2/3'", false, true); registerStringOption_("charges", "", charges_if_not_annotated, "Range of precursor charges to consider if not annotated in the file." , false, true); + //Search Enzyme vector all_enzymes; @@ -434,12 +964,22 @@ class TOPPSageAdapter : registerStringList_("variable_modifications", "", ListUtils::create("Oxidation (M)", ','), "Variable modifications, specified using Unimod (www.unimod.org) terms, e.g. 'Carbamidomethyl (C)' or 'Oxidation (M)'", false); setValidStrings_("variable_modifications", all_mods); + //FDR and misc + + registerDoubleOption_("q_value_threshold", "", 1, "The FDR threshhold for filtering peptides", false, false); + registerStringOption_("annotate_matches", "", "true", "If the matches should be annotated (default: false),", false, false); + registerStringOption_("deisotope", "", "false", "Sets deisotope option (true or false), default: false", false, false ); + registerStringOption_("chimera", "", "false", "Sets chimera option (true or false), default: false", false, false ); + registerStringOption_("predict_rt", "", "false", "Sets predict_rt option (true or false), default: false", false, false ); + registerStringOption_("wide_window", "", "false", "Sets wide_window option (true or false), default: false", false, false); + registerStringOption_("smoothing", "", "true", "Should the PTM histogram be smoothed and local maxima be picked. If false, uses raw data, default: false", false, false); + registerIntOption_("threads", "", 1, "Amount of threads available to the program", false, false); + // register peptide indexing parameter (with defaults for this search engine) registerPeptideIndexingParameter_(PeptideIndexing().getParameters()); } - ExitCodes main_(int, const char**) override { //------------------------------------------------------------- @@ -448,6 +988,7 @@ class TOPPSageAdapter : // do this early, to see if Sage is installed String sage_executable = getStringOption_("sage_executable"); + std::cout << sage_executable << " sage executable" << std::endl; String proc_stdout, proc_stderr; TOPPBase::ExitCodes exit_code = runExternalProcess_(sage_executable.toQString(), QStringList() << "--help", proc_stdout, proc_stderr, ""); auto major_minor_patch = getVersionNumber_(proc_stdout); @@ -483,20 +1024,52 @@ class TOPPSageAdapter : debug_config_stream.close(); } + String annotation_check; + QStringList arguments; + + if ( (getStringOption_("annotate_matches").compare("true")) == 0) + { arguments << config_file.toQString() << "-f" << fasta_file.toQString() << "-o" << output_folder.toQString() - << "--write-pin"; + << "--annotate-matches" + << "--write-pin"; + } + else + { + arguments << config_file.toQString() + << "-f" << fasta_file.toQString() + << "-o" << output_folder.toQString() + << "--write-pin"; + } + if (batch >= 1) arguments << "--batch-size" << QString(batch); for (auto s : input_files) arguments << s.toQString(); OPENMS_LOG_INFO << "Sage command line: " << sage_executable << " " << arguments.join(' ').toStdString() << std::endl; + + //std::chrono lines for testing/writing purposes only! + + #ifdef CHRONOSET + std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now(); + // Sage execution with the executable and the arguments StringList + exit_code = runExternalProcess_(sage_executable.toQString(), arguments); + std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now(); + std::cout << "Time difference = " << std::chrono::duration_cast(end - begin).count() << "[s]" << std::endl; + #endif + #ifndef CHRONOSET // Sage execution with the executable and the arguments StringList - exit_code = runExternalProcess_(sage_executable.toQString(), arguments); + exit_code = runExternalProcess_(sage_executable.toQString(), arguments); + #endif + + + + if (exit_code != EXECUTION_OK) { + std::cout << "Sage executable not found" << std::endl; return exit_code; } @@ -509,16 +1082,19 @@ class TOPPSageAdapter : StringList filenames; StringList extra_scores = {"ln(-poisson)", "ln(delta_best)", "ln(delta_next)", "ln(matched_intensity_pct)", "longest_b", "longest_y", - "longest_y_pct", "matched_peaks", "scored_candidates"}; + "longest_y_pct", "matched_peaks", "scored_candidates"}; + double FDR_threshhold = getDoubleOption_("q_value_threshold"); + vector peptide_identifications = PercolatorInfile::load( output_folder + "/results.sage.pin", true, "ln(hyperscore)", extra_scores, filenames, - decoy_prefix); + decoy_prefix, + FDR_threshhold, + true); - // rename SAGE subscores to have prefix "SAGE:" for (auto& id : peptide_identifications) { auto& hits = id.getHits(); @@ -529,12 +1105,17 @@ class TOPPSageAdapter : if (h.metaValueExists(meta)) { h.setMetaValue("SAGE:" + meta, h.getMetaValue(meta)); - h.removeMetaValue(meta); - } + h.removeMetaValue(meta); + } } } } + + String smoothing_string = getStringOption_("smoothing"); + bool smoothing = !(smoothing_string.compare("true")); + const pair resultsClus = getDeltaClusterCenter(peptide_identifications, smoothing, false); + vector mapD = mapDifftoMods(resultsClus.first, resultsClus.second, peptide_identifications, 0.01, false, output_file); //peptide_identifications; // remove hits without charge state assigned or charge outside of default range (fix for downstream bugs). TODO: remove if all charges annotated in sage IDFilter::filterPeptidesByCharge(peptide_identifications, 2, numeric_limits::max()); @@ -625,7 +1206,6 @@ class TOPPSageAdapter : it->second.emplace(nr,nID); } } - } } @@ -653,12 +1233,9 @@ class TOPPSageAdapter : { } } - IdXMLFile().store(output_file, protein_identifications, peptide_identifications); - return EXECUTION_OK; } - }; diff --git a/vcpkg b/vcpkg index 64ca152891d..efdc0912145 160000 --- a/vcpkg +++ b/vcpkg @@ -1 +1 @@ -Subproject commit 64ca152891d6ab135c6c27881e7eb0ac2fa15bba +Subproject commit efdc09121456bc4a5f96a71cbef4a41fb0100bd0