package com.compomics.util.experiment.identification.spectrum_annotation; import com.compomics.util.experiment.identification.spectrum_assumptions.TagAssumption; import com.compomics.util.experiment.identification.spectrum_assumptions.PeptideAssumption; import com.compomics.util.experiment.biology.IonFactory; import com.compomics.util.experiment.biology.Ion; import com.compomics.util.experiment.biology.Ion.IonType; import com.compomics.util.experiment.biology.NeutralLoss; import com.compomics.util.experiment.biology.ions.ElementaryIon; import com.compomics.util.experiment.biology.ions.PeptideFragmentIon; import com.compomics.util.experiment.biology.ions.TagFragmentIon; import com.compomics.util.experiment.identification.SpectrumIdentificationAssumption; import com.compomics.util.experiment.identification.matches.IonMatch; import com.compomics.util.experiment.identification.spectrum_annotation.spectrum_annotators.PeptideSpectrumAnnotator; import com.compomics.util.experiment.identification.spectrum_annotation.spectrum_annotators.TagSpectrumAnnotator; import com.compomics.util.experiment.massspectrometry.Charge; import com.compomics.util.experiment.massspectrometry.MSnSpectrum; import com.compomics.util.experiment.massspectrometry.Peak; import com.compomics.util.experiment.massspectrometry.Spectrum; import com.compomics.util.experiment.massspectrometry.indexes.SpectrumIndex; import com.compomics.util.gui.interfaces.SpectrumAnnotation; import com.compomics.util.gui.spectrum.DefaultSpectrumAnnotation; import com.compomics.util.gui.spectrum.SpectrumPanel; import com.compomics.util.preferences.SequenceMatchingPreferences; import java.io.IOException; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Vector; /** * The spectrum annotator annotates peaks in a spectrum. * * @author Marc Vaudel * @author Harald Barsnes */ public abstract class SpectrumAnnotator { /** * Enum of the possibilities for ties resolution when multiple peaks can be * annotated. */ public enum TiesResolution { /** * The most intense peak is retained. If two peaks have the same * intensity, the one with the most accurate m/z is retained. */ mostIntense, /** * The peak of most accurate m/z is retained. If two peaks have the same * error the most intense is retained. */ mostAccurateMz; } /** * The precursor charge as deduced by the search engine. */ protected int precursorCharge; /** * The theoretic fragment ions. */ protected HashMap<Integer, HashMap<Integer, ArrayList<Ion>>> theoreticalFragmentIons; /** * The Fragment factory which will generate the fragment ions. */ protected IonFactory fragmentFactory = IonFactory.getInstance(); /** * The key of the currently loaded spectrum. */ private String spectrumKey = ""; /** * The intensity limit to use. */ protected double intensityLimit = 0; /** * Index for the spectrum. */ private SpectrumIndex spectrumIndex; /** * The m/z tolerance for peak matching. */ protected double mzTolerance; /** * Boolean indicating whether the tolerance is in ppm (true) or in Dalton * (false). */ protected boolean isPpm; /** * Minimal isotopic correction when matching an ion. */ protected static final boolean subtractIsotope = false; /** * The minimal isotope correction. By default only the monoisotopic peak is * annotated (min=0). */ protected static final Integer minIsotopicCorrection = 0; /** * The maximal isotope correction. By default only the monoisotopic peak is * annotated (max=0). */ protected static final Integer maxIsotopicCorrection = 0; /** * m/z shift applied to all theoretic peaks. */ protected double massShift = 0; /** * N-terminal m/z shift applied to all forward ions. */ protected double massShiftNTerm = 0; /** * C-terminal m/z shift applied to all reverse ions. */ protected double massShiftCTerm = 0; /** * The methods to use to select the best peak when multiple are possible. */ protected TiesResolution tiesResolution; /** * If provided, the annotator will only look for the ions included in the * specific annotation settings. */ protected SpecificAnnotationSettings specificAnnotationSettings = null; /** * The cache to use for the ion match keys. */ protected IonMatchKeysCache ionMatchKeysCache = new IonMatchKeysCache(); /** * Translates the list of ion matches into a vector of annotations which can * be read by the SpectrumPanel. * * @param ionMatches list of ion matches * * @return vector of default spectrum annotations */ public static Vector<SpectrumAnnotation> getSpectrumAnnotation(ArrayList<IonMatch> ionMatches) { Vector<SpectrumAnnotation> currentAnnotations = new Vector(); for (IonMatch ionMatch : ionMatches) { currentAnnotations.add(new DefaultSpectrumAnnotation(ionMatch.peak.mz, ionMatch.getAbsoluteError(minIsotopicCorrection, maxIsotopicCorrection), SpectrumPanel.determineFragmentIonColor(ionMatch.ion, true), ionMatch.getPeakAnnotation())); } return currentAnnotations; } /** * Matches a theoretic ion in the spectrum. Returns an IonMatch containing * the ion and the peak. Null if not found. * * @param theoreticIon the theoretic ion * @param inspectedCharge the expected charge * * @return the IonMatch between the ion and the peak */ protected IonMatch matchInSpectrum(Ion theoreticIon, Integer inspectedCharge) { Double fragmentMz = theoreticIon.getTheoreticMz(inspectedCharge); // Get the peaks matching the desired m/z ArrayList<Peak> matchedPeaks = spectrumIndex.getMatchingPeaks(fragmentMz); if (matchedPeaks.isEmpty()) { return null; } // Select the most accurate or most intense according to the annotation settings IonMatch ionMatch = new IonMatch(null, theoreticIon, inspectedCharge); ionMatch.peak = (matchedPeaks.size() == 1) ? matchedPeaks.get(0) : getBestPeak(matchedPeaks, ionMatch); return ionMatch; } /** * Returns the peak to retain of the matched peaks according to the ties * resolution setting. * * @param matchedPeaks the peaks matched * @param ionMatch an ion match with the ion to be matched * * @return the peak to retain */ protected Peak getBestPeak(ArrayList<Peak> matchedPeaks, IonMatch ionMatch) { Peak bestPeak = null; switch (tiesResolution) { case mostAccurateMz: Double bestPeakError = null; for (Peak peak : matchedPeaks) { if (bestPeak == null) { bestPeak = peak; ionMatch.peak = peak; bestPeakError = Math.abs(ionMatch.getError(isPpm)); } else { ionMatch.peak = peak; double peakError = Math.abs(ionMatch.getError(isPpm)); if (peakError < bestPeakError) { bestPeak = peak; bestPeakError = peakError; } else if (peakError == bestPeakError && peak.intensity > bestPeak.intensity) { bestPeak = peak; } } } return bestPeak; case mostIntense: for (Peak peak : matchedPeaks) { if (bestPeak == null || peak.intensity > bestPeak.intensity) { bestPeak = peak; } else if (peak.intensity == bestPeak.intensity) { ionMatch.peak = bestPeak; bestPeakError = Math.abs(ionMatch.getError(isPpm)); ionMatch.peak = peak; double peakError = Math.abs(ionMatch.getError(isPpm)); if (peakError < bestPeakError) { bestPeak = peak; } } } return bestPeak; default: throw new UnsupportedOperationException("Ties resolution method " + tiesResolution + " not implemented."); } } /** * Sets a new spectrum to annotate. * * @param spectrum the spectrum to inspect * @param intensityLimit the minimal intensity to account for */ protected void setSpectrum(MSnSpectrum spectrum, double intensityLimit) { if (spectrumIndex == null || !spectrumKey.equals(spectrum.getSpectrumKey()) || this.intensityLimit != intensityLimit) { // Save spectrum number and intensity limit spectrumKey = spectrum.getSpectrumKey(); this.intensityLimit = intensityLimit; // See whether the index was previously stored spectrumIndex = new SpectrumIndex(); spectrumIndex = (SpectrumIndex) spectrum.getUrParam(spectrumIndex); // Create new index spectrumIndex = new SpectrumIndex(spectrum.getPeakMap(), intensityLimit, mzTolerance, isPpm); spectrum.addUrParam(spectrumIndex); } } /** * Sets a new m/z tolerance for peak matching. * * @param mzTolerance the new m/z tolerance (in m/z, Th) * @param isPpm a boolean indicating whether the mass tolerance is in ppm or * in Da * @param tiesResolution the method used to resolve ties */ protected void setMassTolerance(double mzTolerance, boolean isPpm, TiesResolution tiesResolution) { if (mzTolerance != this.mzTolerance || tiesResolution != this.tiesResolution) { // Clear previous index spectrumIndex = null; // Save new values this.mzTolerance = mzTolerance; this.isPpm = isPpm; this.tiesResolution = tiesResolution; } } /** * Returns a boolean indicating whether the neutral loss should be accounted * for. * * @param neutralLosses map of expected neutral losses * @param neutralLoss the neutral loss of interest * @param ion the fragment ion of interest * * @return boolean indicating whether the neutral loss should be considered */ public boolean isAccounted(NeutralLossesMap neutralLosses, NeutralLoss neutralLoss, Ion ion) { if (neutralLosses == null || neutralLosses.isEmpty()) { return false; } for (String neutralLossName : neutralLosses.getAccountedNeutralLosses()) { NeutralLoss neutralLossRef = NeutralLoss.getNeutralLoss(neutralLossName); if (neutralLoss.isSameAs(neutralLossRef)) { switch (ion.getType()) { case PEPTIDE_FRAGMENT_ION: PeptideFragmentIon peptideFragmentIon = ((PeptideFragmentIon) ion); switch (ion.getSubType()) { case PeptideFragmentIon.A_ION: case PeptideFragmentIon.B_ION: case PeptideFragmentIon.C_ION: return neutralLosses.getForwardStart(neutralLossName) <= peptideFragmentIon.getNumber(); case PeptideFragmentIon.X_ION: case PeptideFragmentIon.Y_ION: case PeptideFragmentIon.Z_ION: return neutralLosses.getRewindStart(neutralLossName) <= peptideFragmentIon.getNumber(); default: throw new UnsupportedOperationException("Fragment ion type " + ion.getSubTypeAsString() + " not implemented in the spectrum annotator."); } case TAG_FRAGMENT_ION: TagFragmentIon tagFragmentIon = ((TagFragmentIon) ion); switch (ion.getSubType()) { case TagFragmentIon.A_ION: case TagFragmentIon.B_ION: case TagFragmentIon.C_ION: return neutralLosses.getForwardStart(neutralLossName) <= tagFragmentIon.getNumber(); case TagFragmentIon.X_ION: case TagFragmentIon.Y_ION: case TagFragmentIon.Z_ION: return neutralLosses.getRewindStart(neutralLossName) <= tagFragmentIon.getNumber(); default: throw new UnsupportedOperationException("Fragment ion type " + ion.getSubTypeAsString() + " not implemented in the spectrum annotator."); } default: return true; } } } return false; } /** * Returns a boolean indicating whether the neutral losses of the given * fragment ion fit the requirement of the given neutral losses map. * * @param neutralLosses map of expected neutral losses: neutral loss * @param theoreticIon the ion of interest * * @return a boolean indicating whether the neutral losses of the given * fragment ion are fit the requirement of the given neutral losses map */ public boolean lossesValidated(NeutralLossesMap neutralLosses, Ion theoreticIon) { if (theoreticIon.hasNeutralLosses()) { for (NeutralLoss neutralLoss : theoreticIon.getNeutralLosses()) { if (!isAccounted(neutralLosses, neutralLoss, theoreticIon)) { return false; } } } return true; } /** * Returns a boolean indicating whether the given charge can be found on the * given fragment ion. * * @param theoreticIon the ion of interest * @param charge the candidate charge * @param precursorCharge the precursor charge * * @return a boolean indicating whether the given charge can be found on the * given fragment ion */ public boolean chargeValidated(Ion theoreticIon, int charge, int precursorCharge) { if (charge == 1) { return true; } switch (theoreticIon.getType()) { case IMMONIUM_ION: case RELATED_ION: // note: it is possible to implement higher charges but then modify IonMatch.getPeakAnnotation(boolean html) as well to see the charge displayed on the spectrum return false; case REPORTER_ION: // note: it is possible to implement higher charges but then modify IonMatch.getPeakAnnotation(boolean html) as well to see the charge displayed on the spectrum return false; case PEPTIDE_FRAGMENT_ION: PeptideFragmentIon peptideFragmentIon = ((PeptideFragmentIon) theoreticIon); return charge <= peptideFragmentIon.getNumber() && charge < precursorCharge; case TAG_FRAGMENT_ION: TagFragmentIon tagFragmentIon = ((TagFragmentIon) theoreticIon); return charge <= tagFragmentIon.getNumber() && charge < precursorCharge; case PRECURSOR_ION: return charge >= precursorCharge; default: throw new UnsupportedOperationException("Ion type " + theoreticIon.getTypeAsString() + " not implemented in the spectrum annotator."); } } /** * Returns the currently matched ions with the given settings. * * @param spectrum the spectrum of interest * @param annotationSettings the annotation settings * @param specificAnnotationSettings the specific annotation settings * * @return the currently matched ions with the given settings */ public abstract ArrayList<IonMatch> getCurrentAnnotation(MSnSpectrum spectrum, AnnotationSettings annotationSettings, SpecificAnnotationSettings specificAnnotationSettings); /** * Returns the spectrum currently inspected. * * @return the spectrum currently inspected */ public String getCurrentlyLoadedSpectrumKey() { return spectrumKey; } /** * Returns the m/z shift applied to the fragment ions. * * @return the m/z shift applied to the fragment ions */ public double getMassShift() { return massShift; } /** * Returns the N-terminal m/z shift applied to all forward ions. * * @return the N-terminal m/z shift applied to all forward ions */ public double getMassShiftNTerm() { return massShiftNTerm; } /** * Returns the C-terminal m/z shift applied to all reverse ions. * * @return the C-terminal m/z shift applied to all reverse ions */ public double getMassShiftCTerm() { return massShiftNTerm; } /** * Sets an m/z shift on all ions. The previous mass main shift will be * removed. * * @param aMassShift the m/z shift to apply */ public void setMassShift(double aMassShift) { this.massShift = aMassShift; updateMassShifts(); } /** * Sets the m/z shifts. The previous mass shifts will be removed. * * @param aMassShift the m/z shift to apply * @param aMassShiftNTerm the n-terminal mass shift to apply to all forward * ions * @param aMassShiftCTerm the c-terminal mass shift to apply to all reverse * ions */ public void setMassShifts(double aMassShift, double aMassShiftNTerm, double aMassShiftCTerm) { this.massShift = aMassShift; this.massShiftNTerm = aMassShiftNTerm; this.massShiftCTerm = aMassShiftCTerm; updateMassShifts(); } /** * Sets the terminal m/z shifts. * * @param aMassShiftNTerm the n-terminal mass shift to apply to all forward * ions * @param aMassShiftCTerm the c-terminal mass shift to apply to all reverse * ions */ public void setTerminalMassShifts(double aMassShiftNTerm, double aMassShiftCTerm) { this.massShiftNTerm = aMassShiftNTerm; this.massShiftCTerm = aMassShiftCTerm; updateMassShifts(); } /** * Updates the mass shifts. */ protected void updateMassShifts() { if (theoreticalFragmentIons != null) { HashMap<Integer, ArrayList<Ion>> peptideFragmentIons = theoreticalFragmentIons.get(IonType.PEPTIDE_FRAGMENT_ION.index); ArrayList<Ion> ions = peptideFragmentIons.get(PeptideFragmentIon.A_ION); if (ions != null) { for (Ion ion : ions) { ion.setTheoreticMass(ion.getTheoreticMass() + massShift + massShiftNTerm); } } ions = peptideFragmentIons.get(PeptideFragmentIon.B_ION); if (ions != null) { for (Ion ion : ions) { ion.setTheoreticMass(ion.getTheoreticMass() + massShift + massShiftNTerm); } } ions = peptideFragmentIons.get(PeptideFragmentIon.C_ION); if (ions != null) { for (Ion ion : ions) { ion.setTheoreticMass(ion.getTheoreticMass() + massShift + massShiftNTerm); } } ions = peptideFragmentIons.get(PeptideFragmentIon.X_ION); if (ions != null) { for (Ion ion : ions) { ion.setTheoreticMass(ion.getTheoreticMass() + massShift + massShiftCTerm); } } ions = peptideFragmentIons.get(PeptideFragmentIon.Y_ION); if (ions != null) { for (Ion ion : ions) { ion.setTheoreticMass(ion.getTheoreticMass() + massShift + massShiftCTerm); } } ions = peptideFragmentIons.get(PeptideFragmentIon.Z_ION); if (ions != null) { for (Ion ion : ions) { ion.setTheoreticMass(ion.getTheoreticMass() + massShift + massShiftCTerm); } } HashMap<Integer, ArrayList<Ion>> tagFragmentIons = theoreticalFragmentIons.get(IonType.TAG_FRAGMENT_ION.index); ions = tagFragmentIons.get(TagFragmentIon.A_ION); if (ions != null) { for (Ion ion : ions) { ion.setTheoreticMass(ion.getTheoreticMass() + massShift + massShiftNTerm); } } ions = tagFragmentIons.get(TagFragmentIon.B_ION); if (ions != null) { for (Ion ion : ions) { ion.setTheoreticMass(ion.getTheoreticMass() + massShift + massShiftNTerm); } } ions = tagFragmentIons.get(TagFragmentIon.C_ION); if (ions != null) { for (Ion ion : ions) { ion.setTheoreticMass(ion.getTheoreticMass() + massShift + massShiftNTerm); } } ions = tagFragmentIons.get(TagFragmentIon.X_ION); if (ions != null) { for (Ion ion : ions) { ion.setTheoreticMass(ion.getTheoreticMass() + massShift + massShiftCTerm); } } ions = tagFragmentIons.get(TagFragmentIon.Y_ION); if (ions != null) { for (Ion ion : ions) { ion.setTheoreticMass(ion.getTheoreticMass() + massShift + massShiftCTerm); } } ions = tagFragmentIons.get(TagFragmentIon.Z_ION); if (ions != null) { for (Ion ion : ions) { ion.setTheoreticMass(ion.getTheoreticMass() + massShift + massShiftCTerm); } } } } /** * Returns the possible neutral losses expected by default for a given * peptide. /!\ this method will work only if the PTM found in the peptide * are in the PTMFactory. * * @param spectrumIdentificationAssumption the * spectrumIdentificationAssumption of interest * @param sequenceMatchingPreferences the sequence matching settings for * peptide to protein mapping * @param ptmSequenceMatchingPreferences the sequence matching settings for * PTM to peptide mapping * * @return the expected possible neutral losses * * @throws IOException exception thrown whenever an error occurred while * interacting with a file while mapping potential modification sites * @throws InterruptedException exception thrown whenever a threading issue * occurred while mapping potential modification sites * @throws ClassNotFoundException exception thrown whenever an error * occurred while deserializing an object from the ProteinTree * @throws SQLException exception thrown whenever an error occurred while * interacting with the ProteinTree */ public static NeutralLossesMap getDefaultLosses(SpectrumIdentificationAssumption spectrumIdentificationAssumption, SequenceMatchingPreferences sequenceMatchingPreferences, SequenceMatchingPreferences ptmSequenceMatchingPreferences) throws IOException, InterruptedException, ClassNotFoundException, SQLException { if (spectrumIdentificationAssumption instanceof PeptideAssumption) { PeptideAssumption peptideAssumption = (PeptideAssumption) spectrumIdentificationAssumption; return PeptideSpectrumAnnotator.getDefaultLosses(peptideAssumption.getPeptide(), sequenceMatchingPreferences, ptmSequenceMatchingPreferences); } else if (spectrumIdentificationAssumption instanceof TagAssumption) { TagAssumption tagAssumption = (TagAssumption) spectrumIdentificationAssumption; return TagSpectrumAnnotator.getDefaultLosses(tagAssumption.getTag(), ptmSequenceMatchingPreferences); } else { throw new IllegalArgumentException("Default neutral loss map not implemented for SpectrumIdentificationAssumption " + spectrumIdentificationAssumption.getClass() + "."); } } /** * This method matches the potential fragment ions of a given peptide with a * given peak. Note: fragment ions need to be initiated by the * SpectrumAnnotator extending class. * * @param specificAnnotationSettings the specific annotation settings * @param peak The peak to match * @return A list of potential ion matches */ protected ArrayList<IonMatch> matchPeak(SpecificAnnotationSettings specificAnnotationSettings, Peak peak) { ArrayList<IonMatch> result = new ArrayList<IonMatch>(); HashMap<Ion.IonType, HashSet<Integer>> ionTypes = specificAnnotationSettings.getIonTypes(); for (Ion.IonType ionType : ionTypes.keySet()) { HashMap<Integer, ArrayList<Ion>> ionMap = theoreticalFragmentIons.get(ionType.index); if (ionMap != null) { HashSet<Integer> subtypes = ionTypes.get(ionType); for (int subType : subtypes) { ArrayList<Ion> ions = ionMap.get(subType); if (ions != null) { for (Ion ion : ions) { for (int charge : specificAnnotationSettings.getSelectedCharges()) { if (chargeValidated(ion, charge, specificAnnotationSettings.getPrecursorCharge()) && lossesValidated(specificAnnotationSettings.getNeutralLossesMap(), ion)) { IonMatch ionMatch = new IonMatch(peak, ion, charge); if (Math.abs(ionMatch.getError(specificAnnotationSettings.isFragmentIonPpm(), minIsotopicCorrection, maxIsotopicCorrection)) <= specificAnnotationSettings.getFragmentIonAccuracy()) { result.add(ionMatch); } } } } } } } } return result; } /** * Returns the expected ions in a map indexed by the possible charges. * * Note that, except for +1 precursors, fragments ions will be expected to * have a charge strictly smaller than the precursor ion charge. * * Note: fragment ions need to be initiated by the SpectrumAnnotator * extending class. * * @param specificAnnotationSettings the specific annotation settings * * @return an ArrayList of IonMatch containing the ion matches with the * given settings */ protected HashMap<Integer, ArrayList<Ion>> getExpectedIons(SpecificAnnotationSettings specificAnnotationSettings) { HashMap<Integer, ArrayList<Ion>> result = new HashMap<Integer, ArrayList<Ion>>(); HashMap<Ion.IonType, HashSet<Integer>> ionTypes = specificAnnotationSettings.getIonTypes(); for (Ion.IonType ionType : ionTypes.keySet()) { HashMap<Integer, ArrayList<Ion>> ionMap = theoreticalFragmentIons.get(ionType.index); if (ionMap != null) { HashSet<Integer> subtypes = ionTypes.get(ionType); for (int subType : subtypes) { ArrayList<Ion> ions = ionMap.get(subType); if (ions != null) { for (Ion ion : ions) { if (lossesValidated(specificAnnotationSettings.getNeutralLossesMap(), ion)) { for (int charge : specificAnnotationSettings.getSelectedCharges()) { if (chargeValidated(ion, charge, precursorCharge)) { ArrayList<Ion> resultsAtCharge = result.get(charge); if (resultsAtCharge == null) { resultsAtCharge = new ArrayList<Ion>(); result.put(charge, resultsAtCharge); } resultsAtCharge.add(ion); } } } } } } } } return result; } /** * Convenience method to match a reporter ion in a spectrum. The charge is * assumed to be 1. * * @param theoreticIon the theoretic ion to look for * @param charge the charge of the ion * @param spectrum the spectrum * @param massTolerance the mass tolerance to use * * @return a list of all the ion matches * * @throws java.lang.InterruptedException exception thrown if the thread is * interrupted */ public static ArrayList<IonMatch> matchReporterIon(Ion theoreticIon, int charge, Spectrum spectrum, double massTolerance) throws InterruptedException { ArrayList<IonMatch> result = new ArrayList<IonMatch>(1); double targetMass = theoreticIon.getTheoreticMz(charge); for (double mz : spectrum.getOrderedMzValues()) { if (Math.abs(mz - targetMass) <= massTolerance) { result.add(new IonMatch(spectrum.getPeakMap().get(mz), theoreticIon, charge)); } if (mz > targetMass + massTolerance) { break; } } return result; } }