/// <summary> /// Calculate isotope ion labels for precursor. /// </summary> /// <param name="relativeIntensityThreshold">Relative intensity threshold (fraction of most abundant isotope)</param> /// <param name="labelModifications">The heavy/light labels.</param> /// <returns>A list of precursor labeled ions.</returns> public List <LabeledIonViewModel> GetIsotopePrecursorLabels(double relativeIntensityThreshold = 0.1, IEnumerable <SearchModification> labelModifications = null) { var ions = new List <LabeledIonViewModel>(); if (Sequence.Count == 0 || LcMsRun == null) { return(ions); } var sequence = Sequence; if (labelModifications != null) { sequence = IonUtils.GetHeavySequence(sequence, labelModifications.ToArray()); } #pragma warning disable 0618 var precursorIonType = new IonType("Precursor", Composition.H2O, Charge, false); #pragma warning restore 0618 var composition = sequence.Aggregate(Composition.Zero, (current, aa) => current + aa.Composition); var relativeIntensities = composition.GetIsotopomerEnvelope(); var indices = new List <int> { -1 }; for (var i = 0; i < relativeIntensities.Envelope.Length; i++) { if (relativeIntensities.Envelope[i] >= relativeIntensityThreshold || i == 0) { indices.Add(i); } } ions.AddRange(indices.Select(index => new LabeledIonViewModel(composition, precursorIonType, false, LcMsRun, null, false, index))); return(ions); }
public void TestDisplaySpectra(string rawFile, string idFile) { // init var idFileReader = IdFileReaderFactory.CreateReader(idFile); var ids = idFileReader.Read(); var lcms = PbfLcMsRun.GetLcMsRun(rawFile); var idList = ids.ToList(); foreach (var id in idList) { id.LcMs = lcms; id.RawFileName = Path.GetFileNameWithoutExtension(rawFile); } idList.Sort(new PrSm.PrSmScoreComparer()); var prsm = idList[0]; // init XicPlotViewModel var dialogService = new TestableMainDialogService(); var spectrumViewModel = new SpectrumViewModel(dialogService, lcms); // init test ions var baseIonTypes = new List <BaseIonType> { BaseIonType.B, BaseIonType.Y }; var neutralLosses = new List <NeutralLoss> { NeutralLoss.NoLoss }; const int charge = 1; const int minCharge = 1, maxCharge = 2; var ionTypeFactory = new IonTypeFactory(maxCharge); var ionTypes = IonUtils.GetIonTypes(ionTypeFactory, baseIonTypes, neutralLosses, minCharge, maxCharge); var ions = IonUtils.GetFragmentIonLabels(prsm.Sequence, charge, ionTypes); var ionVms = ions.Select(label => new LabeledIonViewModel(label.Composition, label.IonType, label.IsFragmentIon, lcms, label.PrecursorIon, label.IsChargeState, label.Index)).ToList(); }
/// <summary> /// Calculate fragment ion labels. /// </summary> /// <param name="labelModifications">The heavy/light labels.</param> /// <returns>A list of fragment labeled ions.</returns> private ReactiveList <LabeledIonViewModel> GenerateFragmentLabels(ReactiveList <SearchModification> labelModifications = null) { var fragmentLabelList = new ReactiveList <LabeledIonViewModel> { ChangeTrackingEnabled = true }; if (this.SelectedPrSm.Sequence.Count < 1) { return(fragmentLabelList); } var sequence = this.SelectedPrSm.Sequence; if (labelModifications != null) { sequence = IonUtils.GetHeavySequence(sequence, labelModifications.ToArray()); } var precursorIon = IonUtils.GetPrecursorIon(sequence, this.SelectedPrSm.Charge); lock (this.cacheLock) { var prefixCompositions = this.prefixCompositionCache.Get(sequence); var suffixCompositions = this.suffixCompositionCache.Get(sequence); foreach (var ionType in this.IonTypes) { var ionFragments = new List <LabeledIonViewModel>(); for (int i = 0; i < prefixCompositions.Length; i++) { var composition = ionType.IsPrefixIon ? prefixCompositions[i] : suffixCompositions[i]; LabeledIonViewModel label = this.fragmentCache.Get(new Tuple <Composition, IonType>(composition, ionType)); label.Index = ionType.IsPrefixIon ? i + 1 : (sequence.Count - (i + 1)); if (label.PrecursorIon == null) { label.PrecursorIon = precursorIon; } ionFragments.Add(label); } if (!ionType.IsPrefixIon) { ionFragments.Reverse(); } fragmentLabelList.AddRange(ionFragments); } } return(fragmentLabelList); }
/// <summary> /// Gets selected ion types based on the selected BaseIonTypes and NeutralLosses. /// </summary> /// <returns>Array of IonTypes.</returns> private IonType[] GetIonTypes() { var charge = Math.Min(this.fragmentationSequence.Charge - 1, 100); charge = Math.Max(charge, 1); return(IonUtils.GetIonTypes( IcParameters.Instance.IonTypeFactory, this.BaseIonTypes.Where(bit => bit.IsSelected).Select(bit => bit.BaseIonType).ToList(), this.NeutralLosses.Where(nl => nl.IsSelected).Select(nl => nl.NeutralLoss).ToList(), 1, charge).ToArray()); }
/// <summary> /// Update ion types based on selected base ion types, selected neutral losses, and charge range. /// </summary> private void UpdateIonTypes() { // set ion types var selectedBase = this.SelectedBaseIonTypes.Cast <BaseIonType>().ToList(); var selectedLosses = this.SelectedNeutralLosses.Cast <NeutralLoss>().ToList(); this.IonTypes = new ReactiveList <IonType>( IonUtils.GetIonTypes( IcParameters.Instance.IonTypeFactory, selectedBase, selectedLosses, this.MinCharge, this.MaxCharge)); }
/// <summary> /// Calculate fragment ion labels. /// </summary> /// <param name="ionTypes">List of IonTypes.</param> /// <param name="labelModifications">The heavy/light labels.</param> /// <returns>A list of fragment labeled ions.</returns> public List <LabeledIonViewModel> GetFragmentLabels(IList <IonType> ionTypes, SearchModification[] labelModifications = null) { var fragmentLabelList = new List <LabeledIonViewModel> { Capacity = this.Sequence.Count * ionTypes.Count * this.Charge }; if (this.Sequence.Count < 1 || this.LcMsRun == null) { return(fragmentLabelList); } var sequence = labelModifications == null ? this.Sequence : IonUtils.GetHeavySequence(this.Sequence, labelModifications); var precursorIon = IonUtils.GetPrecursorIon(sequence, this.Charge); lock (this.cacheLock) { foreach (var ionType in ionTypes) { var ionFragments = new List <LabeledIonViewModel>(); for (int i = 1; i < this.Sequence.Count; i++) { var startIndex = ionType.IsPrefixIon ? 0 : i; var length = ionType.IsPrefixIon ? i : sequence.Count - i; var fragment = new Sequence(this.Sequence.GetRange(startIndex, length)); var ions = ionType.GetPossibleIons(fragment); foreach (var ion in ions) { var labeledIonViewModel = this.fragmentCache.Get(new Tuple <Composition, IonType>(ion.Composition, ionType)); labeledIonViewModel.Index = length; labeledIonViewModel.PrecursorIon = precursorIon; ionFragments.Add(labeledIonViewModel); } if (!ionType.IsPrefixIon) { ionFragments.Reverse(); } } fragmentLabelList.AddRange(ionFragments); } } return(fragmentLabelList); }
/// <summary> /// Score a <see cref="PrSm" /> based on its sequence and MS/MS spectrum. /// </summary> /// <param name="prsm">The <see cref="PrSm" /> to score.</param> private void UpdatePrSmScore(PrSm prsm) { if (this.ScorerFactory == null) { return; } var ms2Spectrum = prsm.Ms2Spectrum; if (ms2Spectrum == null) { return; } var scorer = this.ScorerFactory.GetScorer(prsm.Ms2Spectrum); prsm.Score = IonUtils.ScoreSequence(scorer, prsm.Sequence); }
/// <summary> /// Calculate neighboring charge state ion labels for precursor. /// </summary> /// <param name="labelModifications">The heavy/light labels.</param> /// <returns>A list of neighboring charge state labeled ions.</returns> private ReactiveList <LabeledIonViewModel> GenerateChargePrecursorLabels(ReactiveList <SearchModification> labelModifications = null) { var ions = new ReactiveList <LabeledIonViewModel> { ChangeTrackingEnabled = true }; var numChargeStates = IonUtils.GetNumNeighboringChargeStates(this.SelectedPrSm.Charge); if (this.SelectedPrSm.Sequence.Count == 0) { return(ions); } var sequence = this.SelectedPrSm.Sequence; if (labelModifications != null) { sequence = IonUtils.GetHeavySequence(sequence, labelModifications.ToArray()); } var composition = sequence.Aggregate(Composition.Zero, (current, aa) => current + aa.Composition); var minCharge = Math.Max(1, this.SelectedPrSm.Charge - numChargeStates); var maxCharge = this.SelectedPrSm.Charge + numChargeStates; for (int i = minCharge; i <= maxCharge; i++) { var index = i - minCharge; if (index == 0) { index = this.SelectedPrSm.Charge - minCharge; } if (i == this.SelectedPrSm.Charge) { index = 0; // guarantee that actual charge is index 0 } #pragma warning disable 0618 var precursorIonType = new IonType("Precursor", Composition.H2O, i, false); #pragma warning restore 0618 ions.Add(new LabeledIonViewModel(composition, precursorIonType, false, this.lcms, null, true, index)); } return(ions); }
/// <summary> /// Calculate neighboring charge state ion labels for precursor. /// </summary> /// <param name="labelModifications">The heavy/light labels.</param> /// <returns>A list of neighboring charge state labeled ions.</returns> public List <LabeledIonViewModel> GetChargePrecursorLabels(IEnumerable <SearchModification> labelModifications = null) { var ions = new List <LabeledIonViewModel>(); var numChargeStates = IonUtils.GetNumNeighboringChargeStates(Charge); if (Sequence.Count == 0 || LcMsRun == null) { return(ions); } var sequence = Sequence; if (labelModifications != null) { sequence = IonUtils.GetHeavySequence(sequence, labelModifications.ToArray()); } var composition = sequence.Aggregate(Composition.Zero, (current, aa) => current + aa.Composition); var minCharge = Math.Max(1, Charge - numChargeStates); var maxCharge = Charge + numChargeStates; for (var i = minCharge; i <= maxCharge; i++) { var index = i - minCharge; if (index == 0) { index = Charge - minCharge; } if (i == Charge) { index = 0; // guarantee that actual charge is index 0 } #pragma warning disable 0618 var precursorIonType = new IonType("Precursor", Composition.H2O, i, false); #pragma warning restore 0618 ions.Add(new LabeledIonViewModel(composition, precursorIonType, false, LcMsRun, null, true, index)); } return(ions); }
/// <summary> /// Calculate isotope ion labels for precursor. /// </summary> /// <param name="labelModifications">The heavy/light labels.</param> /// <returns>A list of precursor labeled ions.</returns> private ReactiveList <LabeledIonViewModel> GenerateIsotopePrecursorLabels(ReactiveList <SearchModification> labelModifications = null) { var ions = new ReactiveList <LabeledIonViewModel> { ChangeTrackingEnabled = true }; if (this.SelectedPrSm.Sequence.Count == 0) { return(ions); } var sequence = this.SelectedPrSm.Sequence; if (labelModifications != null) { sequence = IonUtils.GetHeavySequence(sequence, labelModifications.ToArray()); } #pragma warning disable 0618 var precursorIonType = new IonType("Precursor", Composition.H2O, this.SelectedPrSm.Charge, false); #pragma warning restore 0618 var composition = sequence.Aggregate(Composition.Zero, (current, aa) => current + aa.Composition); var relativeIntensities = composition.GetIsotopomerEnvelope(); var indices = new List <int> { -1 }; for (int i = 0; i < relativeIntensities.Envelope.Length; i++) { if (relativeIntensities.Envelope[i] >= IcParameters.Instance.PrecursorRelativeIntensityThreshold || i == 0) { indices.Add(i); } } ions.AddRange(indices.Select(index => new LabeledIonViewModel(composition, precursorIonType, false, this.lcms, null, false, index))); return(ions); }
/// <summary> /// Get XICs for the ion. /// </summary> /// <param name="pointsToSmooth">Smoothing window width. </param> /// <param name="o">Object required for the cache.</param> /// <returns>The XICs for this ion</returns> private IList <XicDataPoint> GetXic(int pointsToSmooth, object o) { if (this.xic == null) { this.xic = this.GetXic(); } var x = this.xic; IonType ionType = null; if (this.IsFragmentIon) { ionType = this.IonType; } // smooth if (pointsToSmooth > 2) { var smoother = new SavitzkyGolaySmoother(pointsToSmooth, 2); x = IonUtils.SmoothXic(smoother, x); } return(x.Where((t, i) => i <= 1 || i >= x.Count - 1 || !this.xic[i - 1].Intensity.Equals(t.Intensity) || !this.xic[i + 1].Intensity.Equals(t.Intensity)) .Select( t => new XicDataPoint( this.Lcms.GetElutionTime(t.ScanNum), t.ScanNum, t.Intensity, this.Index, this.Label) { IonType = ionType }).ToList()); }
public void TestIsotopePeakAlignment(string rawFilePath, string idFilePath) { var idFileReader = IdFileReaderFactory.CreateReader(idFilePath); var lcms = PbfLcMsRun.GetLcMsRun(rawFilePath); var ids = idFileReader.Read(); var idList = ids.ToList(); var rawFileName = Path.GetFileNameWithoutExtension(rawFilePath); foreach (var id in idList) { id.LcMs = lcms; id.RawFileName = rawFileName; } var prsms = idList.Where(prsm => prsm.Sequence.Count > 0); const double relIntThres = 0.1; var tolerance = new Tolerance(10, ToleranceUnit.Ppm); var toleranceValue = tolerance.GetValue(); const int maxCharge = 15; var ionTypeFactory = new IonTypeFactory(maxCharge); var ionTypes = ionTypeFactory.GetAllKnownIonTypes().ToArray(); var psmsValidated = 0; var ionsValidated = 0; var validationErrors = new List <string>(); foreach (var prsm in prsms) { foreach (var ionType in ionTypes) { var composition = prsm.Sequence.Aggregate(Composition.Zero, (current, aa) => current + aa.Composition); var ion = ionType.GetIon(composition); var observedPeaks = prsm.Ms2Spectrum.GetAllIsotopePeaks(ion, tolerance, relIntThres); if (observedPeaks == null) { continue; } var errors = IonUtils.GetIsotopePpmError(observedPeaks, ion, relIntThres); foreach (var error in errors) { if (error == null) { continue; } if (error > toleranceValue) { validationErrors.Add(string.Format("In scan {0}, PSM {1} has error {2:F1} for ion at {3} m/z", prsm.Scan, prsm.SequenceText, error, ion.GetIsotopeMz(0))); } ionsValidated++; } } psmsValidated++; } Console.WriteLine("Validated {0:N0} ions for {1:N0} PSMs", ionsValidated, psmsValidated); if (validationErrors.Count <= 0) { return; } var validationMsg = string.Format("{0} ions had errors greater than {1} ppm", validationErrors.Count, tolerance); Console.WriteLine(validationMsg); foreach (var item in validationErrors.Take(10)) { Console.WriteLine(item); } Assert.Fail(validationMsg); }
/// <summary> /// Calculate fragment ion labels. /// </summary> /// <param name="ionTypes">List of IonTypes.</param> /// <param name="labelModifications">The heavy/light labels.</param> /// <returns>A list of fragment labeled ions.</returns> public List <LabeledIonViewModel> GetFragmentLabels(IList <IonType> ionTypes, SearchModification[] labelModifications = null) { var fragmentLabelList = new List <LabeledIonViewModel> { Capacity = Sequence.Count * ionTypes.Count * Charge }; if (Sequence.Count < 1 || LcMsRun == null) { return(fragmentLabelList); } var sequence = labelModifications == null ? Sequence : IonUtils.GetHeavySequence(Sequence, labelModifications); var precursorIon = IonUtils.GetPrecursorIon(sequence, Charge); var tasks = new List <Task <object> >(); // Define a delegate that prints and returns the system tick count Func <object, List <LabeledIonViewModel> > action = (object type) => { IonType iType = (IonType)type; var ionFragments = new List <LabeledIonViewModel>(); for (var i = 1; i < Sequence.Count; i++) { var startIndex = iType.IsPrefixIon ? 0 : i; var length = iType.IsPrefixIon ? i : sequence.Count - i; var fragment = new Sequence(Sequence.GetRange(startIndex, length)); var ions = iType.GetPossibleIons(fragment); foreach (var ion in ions) { lock (cacheLock) { var labeledIonViewModel = fragmentCache.Get(new Tuple <Composition, IonType>(ion.Composition, iType)); labeledIonViewModel.Index = length; labeledIonViewModel.PrecursorIon = precursorIon; ionFragments.Add(labeledIonViewModel); } } if (!iType.IsPrefixIon) { ionFragments.Reverse(); } } return(ionFragments); }; foreach (var ionType in ionTypes) { tasks.Add(Task <object> .Factory.StartNew(action, ionType)); } Task.WaitAll(tasks.ToArray()); foreach (Task <object> task in tasks) { List <LabeledIonViewModel> list = (List <LabeledIonViewModel>)task.Result; fragmentLabelList.AddRange(list); } return(fragmentLabelList); }
/// <summary> /// Get the peaks for the ion. /// </summary> /// <param name="spectrum">The spectrum to get the peaks from</param> /// <param name="o">Object required for cache</param> /// <returns>The peaks for the ion.</returns> private IList <PeakDataPoint> GetPeakDataPoints(Tuple <Spectrum, bool> spectrum, object o) { var tolerance = this.IsFragmentIon ? IcParameters.Instance.ProductIonTolerancePpm : IcParameters.Instance.PrecursorTolerancePpm; var noPeaks = new List <PeakDataPoint> { new PeakDataPoint(double.NaN, double.NaN, double.NaN, double.NaN, this.Label) { TheoMonoisotopicMass = this.Ion.Composition.Mass, IonType = this.IonType, Index = this.Index } }; var peakDataPoints = new List <PeakDataPoint>(); IonType ionType = null; if (this.IsFragmentIon) { ionType = this.IonType; } var deconvoluted = spectrum.Item2; Ion ion; if (deconvoluted) { if (this.IonType.Charge > 1) { return(peakDataPoints); // Deconvoluted spectrum means decharged (only charge 1 ions shown) } if (!this.IsFragmentIon) { ion = new Ion(this.Composition, 1); } else { var ionTypeFactory = IcParameters.Instance.DeconvolutedIonTypeFactory; var ionTypeName = this.IonType.Name.Insert(1, @"'"); ion = ionTypeFactory.GetIonType(ionTypeName).GetIon(this.Composition); } } else { ion = this.Ion; } var labeledIonPeaks = IonUtils.GetIonPeaks(ion, spectrum.Item1, tolerance, deconvoluted); if (labeledIonPeaks.Item1 == null) { return(noPeaks); } var peaks = labeledIonPeaks.Item1; var correlation = labeledIonPeaks.Item2; if (correlation < IcParameters.Instance.IonCorrelationThreshold) { return(noPeaks); } var errors = IonUtils.GetIsotopePpmError(peaks, ion, 0.1, deconvoluted); peakDataPoints = new List <PeakDataPoint> { Capacity = errors.Length }; for (int i = 0; i < errors.Length; i++) { if (errors[i] != null) { peakDataPoints.Add(new PeakDataPoint(peaks[i].Mz, peaks[i].Intensity, errors[i].Value, correlation, this.Label) { MonoisotopicMass = (peaks[i].Mz * this.Ion.Charge) - InformedProteomics.Backend.Data.Biology.Constants.Proton * this.Ion.Charge, TheoMonoisotopicMass = this.Ion.Composition.Mass, Index = this.Index, IonType = ionType, }); } } peakDataPoints = peakDataPoints.OrderByDescending(x => x.Y).ToList(); return(peakDataPoints); }
/// <summary> /// Implementation of the CreatePRSM command. /// Creates a new protein-spectrum match for the selected sequence, /// charge, and scan number. /// </summary> private void CreatePrSmImplementation() { var sequenceReader = new SequenceReader(); Sequence sequence = null; try { sequence = sequenceReader.Read(this.SequenceText); if (sequence == null) { throw new FormatException("Invalid Sequence."); } } catch (FormatException e) { this.dialogService.MessageBox(e.Message); return; } finally { if (sequence == null) { sequence = new Sequence(new List <AminoAcid>()); } } if (sequence.Count > 0 && this.SelectedCharge == 0) { this.dialogService.MessageBox("Invalid Charge state."); return; } if (sequence.Count == 0 && this.SelectedScan < 0) { this.dialogService.MessageBox("Invalid scan number."); return; } ILcMsRun lcms = this.SelectedDataSetViewModel.LcMs; double score = -1.0; if (lcms != null && this.SelectedScan > 0 && this.ScorerFactory != null && sequence.Count > 0) { var spectrum = lcms.GetSpectrum(this.SelectedScan) as ProductSpectrum; if (spectrum != null) { var scorer = this.ScorerFactory.GetScorer(spectrum); score = IonUtils.ScoreSequence(scorer, sequence); } } string rawFileName = this.SelectedDataSetViewModel.Title; var prsm = new PrSm { Heavy = false, RawFileName = rawFileName, ProteinName = string.Empty, ProteinDesc = string.Empty, Scan = Math.Min(Math.Max(this.SelectedScan, 0), lcms.MaxLcScan), LcMs = lcms, Charge = this.SelectedCharge, Sequence = sequence, SequenceText = this.SequenceText, Score = score, }; this.SelectedPrSm = prsm; }
/// <summary> /// Parse the fragment sequence into the format required for display. /// </summary> private void ParseFragmentationSequence() { if (this.SequenceFragments.Count > 0) { this.SequenceFragments.Clear(); } if (this.FragmentationSequence == null) { // Invalid fragmentation sequence: Just show nothing. this.SequenceFragments.Clear(); return; } if (this.SelectedSpectrum == null) { // Invalid spectrum: clear all ions while retaining the sequence displayed. this.ClearSequenceFragments(); } var labeledIonViewModels = this.FragmentationSequence.LabeledIonViewModels.Where(l => l.IsFragmentIon); var sequence = this.FragmentationSequence.FragmentationSequence.Sequence; var sequenceFragments = new FragmentViewModel[sequence.Count]; // sorted by sequence index for (int i = 0; i < sequence.Count; i++) { sequenceFragments[i] = new FragmentViewModel(sequence[i], i, this.dialogService); } var allPeakDataPoints = new List <PeakDataPoint>(); foreach (var labeledIonViewModel in labeledIonViewModels) { var index = labeledIonViewModel.IonType.IsPrefixIon ? labeledIonViewModel.Index - 1 : sequence.Count - labeledIonViewModel.Index; var peakDataPoints = labeledIonViewModel.GetPeaks(this.SelectedSpectrum, false); if (peakDataPoints.Count == 1 && peakDataPoints[0].X.Equals(double.NaN)) { // Not observed, nothing to add. continue; } allPeakDataPoints.Add(peakDataPoints[0]); var sequenceFragment = sequenceFragments[index]; var fragmentIon = labeledIonViewModel.IonType.IsPrefixIon ? sequenceFragment.PrefixIon : sequenceFragment.SuffixIon; var ionToAdd = labeledIonViewModel; if (fragmentIon != null) // Add fragment prefix or suffix ion to the sequence. { // This a fragment ion has already been marked for this cleavage, select the one that is higher intensity. var label = fragmentIon.LabeledIonViewModel; var newPeakDataPoints = label.GetPeaks(this.SelectedSpectrum, false); var currentMaxAbundance = peakDataPoints.Max(pd => pd.Y); var newMaxAbundance = newPeakDataPoints.Count == 0 ? 0 : newPeakDataPoints.Max(pd => pd.Y); ionToAdd = newMaxAbundance > currentMaxAbundance ? label : labeledIonViewModel; } // Determine the color of the ion. Brush brush = null; if (this.IonColorDictionary != null) { var oxyColor = this.IonColorDictionary.GetColor(ionToAdd.IonType.BaseIonType, 1); brush = oxyColor.ToBrush(); } // Finally, add the ion. if (labeledIonViewModel.IonType.IsPrefixIon) { sequenceFragment.PrefixIon = new FragmentIonViewModel { LabeledIonViewModel = ionToAdd, Color = brush }; } else { sequenceFragment.SuffixIon = new FragmentIonViewModel { LabeledIonViewModel = ionToAdd, Color = brush }; } } foreach (var sequenceFragment in sequenceFragments) { this.SequenceFragments.Add(sequenceFragment); } this.SequenceCoverage = Math.Round(IonUtils.CalculateSequenceCoverage( allPeakDataPoints, this.FragmentationSequence.FragmentationSequence.Sequence.Count), 3); }