private static string RecalculateApproximateStitchedDirections(CigarDirection cigarDirections, CigarAlignment cigarData, CigarAlignment newCigarData)
        {
            var cigarBaseDirectionMap = cigarDirections.Expand().ToArray();

            var cigarBaseAlleleMap    = cigarData.Expand();
            var newCigarBaseAlleleMap = newCigarData.Expand();

            var sequencedBaseDirectionMap = new DirectionType[cigarData.GetReadSpan()];

            var directions = new List <DirectionOp>();

            var sequencedBaseIndex = 0;

            var cigarBaseIndex    = 0;
            var newCigarBaseIndex = 0;

            while (true)
            {
                if (cigarBaseIndex >= cigarBaseAlleleMap.Count || newCigarBaseIndex >= newCigarBaseAlleleMap.Count)
                {
                    // If new is longer than old, fill out the rest with the last direction of the old cigar
                    if (newCigarBaseIndex < newCigarBaseAlleleMap.Count)
                    {
                        directions.Add(new DirectionOp(cigarBaseDirectionMap[cigarBaseIndex - 1], newCigarBaseAlleleMap.Count - newCigarBaseIndex));
                    }

                    break;
                }

                while (!cigarBaseAlleleMap[cigarBaseIndex].IsReadSpan())
                {
                    // Skip these
                    cigarBaseIndex++;

                    // TODO is it ever possible to go off the end here?
                }

                while (!newCigarBaseAlleleMap[newCigarBaseIndex].IsReadSpan())
                {
                    directions.Add(new DirectionOp(cigarBaseDirectionMap[cigarBaseIndex], 1)); // TODO perhaps something more nuanced here? unclear what the best solution is. For now, just be consistent: take the last one that we were on at this point in the old cigar
                    newCigarBaseIndex++;

                    // TODO is it ever possible to go off the end here?
                }

                sequencedBaseDirectionMap[sequencedBaseIndex] = cigarBaseDirectionMap[cigarBaseIndex];
                directions.Add(new DirectionOp(cigarBaseDirectionMap[cigarBaseIndex], 1));
                sequencedBaseIndex++;

                cigarBaseIndex++;
                newCigarBaseIndex++;
            }

            var compressedDirections = DirectionHelper.CompressDirections(directions);

            return(new CigarDirection(compressedDirections).ToString());
        }
예제 #2
0
파일: Read.cs 프로젝트: snashraf/Pisces
        private static void ValidateCigar(CigarAlignment cigarData, int readLength)
        {
            if (cigarData.Count == 1 && (cigarData[0].Type == 'I' || cigarData[0].Type == 'D'))
            {
                throw new Exception(string.Format("Invalid cigar '{0}': indel must have anchor", cigarData));
            }

            if (cigarData.Count > 0 && cigarData.GetReadSpan() != readLength)
            {
                throw new Exception(string.Format("Invalid cigar '{0}': does not match length {1} of read", cigarData,
                                                  readLength));
            }
        }
예제 #3
0
        private static void ValidateCigar(CigarAlignment cigarData, int readLength, string readName = "")
        {
            if (cigarData.Count == 1 && (cigarData[0].Type == 'I' || cigarData[0].Type == 'D'))
            {
                //tjd: change this to a warning to be more gentle to BWA-mem results
                //throw new InvalidDataException(string.Format("Invalid cigar '{0}': indel must have anchor", cigarData));
                Logger.WriteWarningToLog("Anomalous alignment {0}. '{1}': indel without anchor", readName, cigarData);
            }

            if (cigarData.Count > 0 && cigarData.GetReadSpan() != readLength)
            {
                throw new InvalidDataException(string.Format("Check alignment {0}. Invalid cigar '{1}': does not match length {2} of read", readName, cigarData,
                                                             readLength));
            }
        }
예제 #4
0
        public static DirectionType[] CreateSequencedBaseDirectionMap(DirectionType[] cigarBaseDirectionMap, CigarAlignment cigarData)
        {
            var cigarBaseAlleleMap        = cigarData.Expand();
            var sequencedBaseDirectionMap = new DirectionType[cigarData.GetReadSpan()];

            int sequencedBaseIndex = 0;

            for (int cigarBaseIndex = 0; cigarBaseIndex < cigarBaseDirectionMap.Length; cigarBaseIndex++)
            {
                var cigarOp = cigarBaseAlleleMap[cigarBaseIndex];

                if (cigarOp.IsReadSpan()) //choices: (MIDNSHP)
                {
                    sequencedBaseDirectionMap[sequencedBaseIndex] = cigarBaseDirectionMap[cigarBaseIndex];
                    sequencedBaseIndex++;
                }
            }
            return(sequencedBaseDirectionMap);
        }
예제 #5
0
        private static BamAlignment BuildRead(AbstractAlignment alignment,
                                              byte qualityForAll, Tuple <int, int> MNVdata)
        {
            int MNVPosition = MNVdata.Item1;
            int MNVLength   = MNVdata.Item2;

            try
            {
                var ca         = new CigarAlignment(alignment.Cigar);
                int readLength = (int)ca.GetReadSpan();


                string readSequence = new string('A', readLength); //originalAlignment.Sequence;

                if (MNVLength > 0)
                {
                    readSequence  = new string('A', MNVPosition - 1);
                    readSequence += new string('G', MNVLength);
                    readSequence += new string('A', readLength - readSequence.Length);
                }


                var varTagUtils = new TagUtils();
                varTagUtils.AddStringTag("XD", alignment.Directions);

                var varRead = new BamAlignment()
                {
                    RefID      = 1,
                    Position   = alignment.Position - 1,
                    CigarData  = ca,
                    Bases      = readSequence,
                    TagData    = varTagUtils.ToBytes(),
                    Qualities  = Enumerable.Repeat(qualityForAll, readLength).ToArray(),
                    MapQuality = 50
                };
                return(varRead);
            }
            catch
            {
                return(null);
            }
        }
        /// <summary>
        /// Returns a very basic read based on the abstract alignment. We don't yet
        /// </summary>
        /// <returns></returns>
        public Read ToRead()
        {
            var        cigar         = new CigarAlignment(Cigar);
            const byte qualityForAll = 30;

            var readLength = (int)cigar.GetReadSpan();

            var alignment = new BamAlignment
            {
                CigarData = cigar,
                Position  = Position - 1,
                RefID     = 1,
                Bases     = Directions.EndsWith("F") ? new string('A', readLength) : new string('T', readLength),
                Qualities = Enumerable.Repeat(qualityForAll, readLength).ToArray()
            };

            alignment.MapQuality = 30;
            var read = new Read("chr1", alignment);
            var di   = new DirectionInfo(Directions);

            read.SequencedBaseDirectionMap = di.ToDirectionMap();
            return(read);
        }
예제 #7
0
        public StitchingInfo GetStitchedCigar(CigarAlignment cigar1, int pos1, CigarAlignment cigar2, int pos2, bool reverseFirst, bool pairIsOutie)
        {
            var positions = GetStitchedSites(cigar1, cigar2, pos2, pos1);

            var success = true;

            var stitchingInfo = ReconcileSites(positions, reverseFirst, out success, pairIsOutie ? (int)cigar2.GetPrefixClip() : (int)cigar1.GetPrefixClip(), pairIsOutie ? (int)(cigar1.GetReadSpan() - (int)cigar1.GetSuffixClip()) : (int)(cigar2.GetReadSpan() - (int)cigar2.GetSuffixClip()), pairIsOutie);

            return(success ? stitchingInfo : null);
        }
예제 #8
0
        // generate consensus read based on stitched cigar and previously determined overlap boundaries
        // todo try different consensus approaches
        protected Read GenerateConsensus(Read read1, Read read2, CigarAlignment stitchedCigar, OverlapBoundary overlapBoundary)
        {
            var totalStitchedLength = (int)stitchedCigar.GetReadSpan();

            // init consensus
            var stitchedBasesSb   = new StringBuilder();
            var stitchedQualities = new byte[totalStitchedLength];
            var directionMap      = new DirectionType[totalStitchedLength];

            // take everything from read1 for positions before overlap
            stitchedBasesSb.Append(read1.Sequence.Substring(0, overlapBoundary.Read1.StartIndex));
            Array.Copy(read1.Qualities, stitchedQualities, overlapBoundary.Read1.StartIndex);

            for (var i = 0; i < overlapBoundary.Read1.StartIndex; i++)
            {
                directionMap[i] = read1.DirectionMap[i];
            }

            // determine consensus base + qscore in the overlap region
            for (int overlapIdx = 0; overlapIdx < overlapBoundary.OverlapLength; overlapIdx++)
            {
                var read1Index = overlapBoundary.Read1.StartIndex + overlapIdx;
                var read2Index = overlapBoundary.Read2.StartIndex + overlapIdx;

                var base1 = read1.Sequence[read1Index];
                var base2 = read2.Sequence[read2Index];
                var q1    = read1.Qualities[read1Index];
                var q2    = read2.Qualities[read2Index];

                directionMap[read1Index] = DirectionType.Stitched;

                if (base1 == base2)
                {
                    stitchedBasesSb.Append(base1);
                    stitchedQualities[read1Index] = Math.Max(q1, q2);
                }
                else
                {
                    if (q1 >= _minBaseCallQuality && q2 >= _minBaseCallQuality)
                    {
                        // we have two high-quality disagreeing bases
                        stitchedBasesSb.Append('N');
                        stitchedQualities[read1Index] = 0;
                    }
                    else
                    {
                        // take the higher quality base
                        stitchedBasesSb.Append(q1 < q2 ? base2 : base1);
                        stitchedQualities[read1Index] = Math.Max(q1, q2);
                    }
                }
            }

            // take everything from read2 for positions after overlap
            stitchedBasesSb.Append(read2.Sequence.Substring(overlapBoundary.Read2.EndIndex + 1));
            Array.Copy(read2.Qualities, overlapBoundary.Read2.EndIndex + 1,
                       stitchedQualities, overlapBoundary.Read1.EndIndex + 1,
                       read2.Sequence.Length - overlapBoundary.Read2.EndIndex - 1);

            for (var i = overlapBoundary.Read1.EndIndex + 1; i < directionMap.Length; i++)
            {
                directionMap[i] = read2.DirectionMap[overlapBoundary.Read2.EndIndex + 1 + i - (overlapBoundary.Read1.EndIndex + 1)];
            }

            var mergedRead = new Read(read1.Chromosome, new BamAlignment()
            {
                Bases     = stitchedBasesSb.ToString(),
                Position  = read1.Position - 1,
                Qualities = stitchedQualities,
                CigarData = stitchedCigar
            }, true)
            {
                DirectionMap  = directionMap,
                StitchedCigar = stitchedCigar
            };

            return(mergedRead);
        }
예제 #9
0
        private Read GetTestRead(string cigarString, int prefixNs = 0, int suffixNs = 0)
        {
            var cigarData = new CigarAlignment(cigarString);

            return(new Read("chr1", new BamAlignment
            {
                Position = 99,  // zero index for bam alignment
                CigarData = cigarData,
                Bases = string.Join(string.Empty, Enumerable.Repeat("N", prefixNs).Concat(Enumerable.Repeat("A", (int)cigarData.GetReadSpan() - prefixNs - suffixNs)).Concat(Enumerable.Repeat("N", suffixNs)))
            }));
        }
예제 #10
0
        public static ReadPair GetPair(string cigar1, string cigar2, uint mapq1 = 30, uint mapq2 = 30, PairStatus pairStatus = PairStatus.Paired, bool singleReadOnly = false, int nm = 0, int read2Offset = 0, int?nm2 = null, string name = null, string basesRaw = "AAAGTTTTCCCCCCCCCCCCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", int read1Position = 99, string read1Bases = null, string read2Bases = null)
        {
            int nmRead2 = nm2 ?? nm;

            var tagUtils = new TagUtils();

            if (nm >= 0)
            {
                tagUtils.AddIntTag("NM", nm);
            }

            var cigarAln1 = new CigarAlignment(cigar1);

            var qualities1 = new List <byte>();

            for (int i = 0; i < cigarAln1.GetReadSpan(); i++)
            {
                qualities1.Add(30);
            }

            //var basesRaw = "AAAGTTTTCCCCCCCCCCCC";
            var alignment = new BamAlignment
            {
                Name       = name ?? "hi:1:2:3:4:5:6",
                RefID      = 1,
                Position   = read1Position,
                Bases      = read1Bases ?? basesRaw.Substring(0, (int)cigarAln1.GetReadSpan()),
                CigarData  = cigarAln1,
                Qualities  = qualities1.ToArray(),
                MapQuality = mapq1
            };

            alignment.TagData = tagUtils.ToBytes();
            if (!singleReadOnly)
            {
                alignment.SetIsProperPair(true);
                alignment.MateRefID = 1;
            }
            var pair = new ReadPair(alignment);

            if (!singleReadOnly)
            {
                alignment.SetIsMateUnmapped(false);
                var tagUtils2 = new TagUtils();
                if (nmRead2 >= 0)
                {
                    tagUtils2.AddIntTag("NM", nmRead2);
                }

                var cigarAln2 = new CigarAlignment(cigar2);

                var qualities2 = new List <byte>();
                for (int i = 0; i < cigarAln2.GetReadSpan(); i++)
                {
                    qualities2.Add(30);
                }

                var alignment2 = new BamAlignment
                {
                    Name       = "hi:1:2:3:4:5:6",
                    RefID      = 1,
                    Position   = read1Position + read2Offset,
                    Bases      = read2Bases ?? basesRaw.Substring(read2Offset, (int)cigarAln2.GetReadSpan()),
                    CigarData  = cigarAln2,
                    Qualities  = qualities2.ToArray(),
                    MapQuality = mapq2
                };

                alignment2.MateRefID = pair.Read1.RefID;
                alignment2.SetIsProperPair(true);
                alignment2.SetIsSecondMate(true);
                alignment2.SetIsReverseStrand(true);
                alignment2.TagData = tagUtils2.ToBytes();
                pair.AddAlignment(alignment2);
            }


            pair.PairStatus = pairStatus;
            return(pair);
        }
예제 #11
0
        private BaseStitcher.OverlapBoundary ExecuteOverlapTest(int read1Position, string read1Cigar, int read2Position, string read2Cigar)
        {
            var read1CigarAlignment = new CigarAlignment(read1Cigar);
            var read1 = TestHelper.CreateRead("chr1",
                                              string.Join(string.Empty, Enumerable.Repeat("A", (int)read1CigarAlignment.GetReadSpan())), read1Position,
                                              read1CigarAlignment);

            var read2CigarAlignment = new CigarAlignment(read2Cigar);
            var read2 = TestHelper.CreateRead("chr1",
                                              string.Join(string.Empty, Enumerable.Repeat("A", (int)read2CigarAlignment.GetReadSpan())), read2Position,
                                              read2CigarAlignment);

            var stitcher = new BasicStitcher(10);

            return(stitcher.GetOverlapBoundary(read1, read2));
        }
예제 #12
0
        public static CigarAlignment SoftclipCigar(CigarAlignment rawCigar, MatchType[] mismatchMap, uint originalSoftclipPrefix,
                                                   uint originalSoftclipSuffix, bool rescueEdgeMatches = true, bool maskNsOnly = false, int prefixNs       = 0, int suffixNs = 0,
                                                   bool softclipEvenIfMatch = false, bool softclipRepresentsMess = true, float allowOneSoftclipMismatchPer = 12)
        {
            // If realignment creates a bunch of mismatches at beginning where it was once softclipped,
            // can we softclip them?
            // Which bases should be softclipped?
            // - Things that were softclipped before and are mismatches? Or are Ms?
            // - Things that were softclipped before and are Ns
            // Softclips in new alignment can be shorter than before, but not longer
            // Softclips should be terminal
            // This is rooted in an assumption that the original softclips are terminal

            if (originalSoftclipPrefix == 0 && originalSoftclipSuffix == 0)
            {
                return(rawCigar);
            }

            var expandedCigar = rawCigar.Expand();
            var changed       = false;

            // Start at end of potential prefix softclip region and work backwards. This way we can rescue things that were matches previously sandwiched in softclips and now freed up by realignment.
            var mismatchMapIndex = (int)originalSoftclipPrefix;
            var startedSoftclip  = false;

            var maxSoftclipPrefixLength = Math.Min(expandedCigar.FindIndex(x => x.Type != 'M' && x.Type != 'S') + 1, originalSoftclipPrefix);
            var maxSoftclipSuffixLength = Math.Min(expandedCigar.Count - expandedCigar.FindLastIndex(x => x.Type != 'M' && x.Type != 'S'), originalSoftclipSuffix);

            var minMismatchesToSoftclipPrefix = originalSoftclipPrefix / allowOneSoftclipMismatchPer;

            var minMismatchesToSoftclipSuffix = originalSoftclipSuffix / allowOneSoftclipMismatchPer;

            var numMismatchesInOrigPrefixClip = 0;
            var tmpMismatchMapIndex           = mismatchMapIndex;

            for (var i = 0; i < maxSoftclipPrefixLength; i++)
            {
                tmpMismatchMapIndex--;
                var foundMismatch = (mismatchMap[tmpMismatchMapIndex] == MatchType.Mismatch || mismatchMap[tmpMismatchMapIndex] == MatchType.NMismatch);

                if (foundMismatch)
                {
                    numMismatchesInOrigPrefixClip++;
                }
            }

            var prefixTooMessyToRescue = numMismatchesInOrigPrefixClip > minMismatchesToSoftclipPrefix;

            var previousOp         = 'N';
            var previousPreviousOp = 'N';

            for (var i = 0; i < maxSoftclipPrefixLength; i++)
            {
                var index = (int)maxSoftclipPrefixLength - 1 - i;

                mismatchMapIndex--;

                var opAtIndex = expandedCigar[index].Type;
                if (opAtIndex != 'M')
                {
                    previousOp = opAtIndex;
                    continue;
                }

                bool shouldSoftclip;

                if (maskNsOnly)
                {
                    shouldSoftclip = index < prefixNs;
                }
                else
                {
                    shouldSoftclip = softclipEvenIfMatch || !rescueEdgeMatches || startedSoftclip || prefixTooMessyToRescue;
                    // Rescue edge matches if we haven't seen any mismatches yet
                    if (!shouldSoftclip)
                    {
                        var foundMismatch = (mismatchMap[mismatchMapIndex] == MatchType.Mismatch || mismatchMap[mismatchMapIndex] == MatchType.NMismatch);
                        if (foundMismatch)
                        {
                            shouldSoftclip = true;
                        }
                    }

                    // Don't resoftclip if we are <1 base from the end.
                    if (previousOp == 'D' || previousOp == 'I' || (softclipRepresentsMess && (previousPreviousOp == 'D' || previousPreviousOp == 'I')))
                    {
                        // Always provide an anchor
                        shouldSoftclip = false;
                    }
                }

                if (shouldSoftclip)
                {
                    changed              = true;
                    startedSoftclip      = true;
                    expandedCigar[index] = new CigarOp('S', 1);
                }

                previousPreviousOp = previousOp;
                previousOp         = opAtIndex;
            }

            // Start at beginning of potential suffix softclip region and work forwards
            startedSoftclip  = false;
            mismatchMapIndex = mismatchMap.Length - (int)maxSoftclipSuffixLength - 1;

            var numMismatchesInOrigSuffixClip = 0;

            tmpMismatchMapIndex = mismatchMapIndex;
            for (var i = 0; i < maxSoftclipSuffixLength; i++)
            {
                tmpMismatchMapIndex++;
                var foundMismatch = (mismatchMap[tmpMismatchMapIndex] == MatchType.Mismatch || mismatchMap[tmpMismatchMapIndex] == MatchType.NMismatch);
                if (foundMismatch)
                {
                    numMismatchesInOrigSuffixClip++;
                }
            }

            var suffixTooMessyToRescue = numMismatchesInOrigSuffixClip > minMismatchesToSoftclipSuffix;

            previousOp = 'N';
            for (var i = 0; i < maxSoftclipSuffixLength; i++)
            {
                var index = expandedCigar.Count() - ((int)maxSoftclipSuffixLength - i);
                mismatchMapIndex++;

                var opAtIndex = expandedCigar[index].Type;

                if (opAtIndex != 'M')
                {
                    previousOp = opAtIndex;
                    continue;
                }
                bool shouldSoftclip;
                if (maskNsOnly)
                {
                    shouldSoftclip = suffixNs > 0 && mismatchMapIndex >= rawCigar.GetReadSpan() - suffixNs;
                }
                else
                {
                    shouldSoftclip = !rescueEdgeMatches || startedSoftclip || suffixTooMessyToRescue;

                    // Rescue edge matches if we haven't seen any mismatches yet
                    if (!shouldSoftclip)
                    {
                        var foundMismatch = (mismatchMap[mismatchMapIndex] == MatchType.Mismatch || mismatchMap[mismatchMapIndex] == MatchType.NMismatch);
                        if (foundMismatch)
                        {
                            shouldSoftclip = true;
                        }
                    }
                    if (previousOp == 'D' || previousOp == 'I')
                    {
                        // Always provide an anchor
                        shouldSoftclip = false;
                    }
                }
                if (shouldSoftclip)
                {
                    changed              = true;
                    startedSoftclip      = true;
                    expandedCigar[index] = new CigarOp('S', 1);
                }

                previousOp = opAtIndex;
            }

            // We can only anchor a read on an M, so if we've softclipped everything away we're in trouble! Add back one.
            if (!expandedCigar.Any(o => o.Type == 'M'))
            {
                var hasAnyNonSoftclipPos = expandedCigar.Any(o => o.Type != 'S');
                var firstNonSoftclipPos  = hasAnyNonSoftclipPos
                    ? expandedCigar.FindIndex(o => o.Type != 'S')
                    : (expandedCigar.Count);
                // Set the last position of softclip to M.
                expandedCigar[firstNonSoftclipPos - 1] = new CigarOp('M', expandedCigar[firstNonSoftclipPos - 1].Length);
            }

            if (!changed)
            {
                return(rawCigar);
            }

            // Re-compile back into a revised cigar.
            var revisedCigar = new CigarAlignment();

            foreach (var cigarOp in expandedCigar)
            {
                revisedCigar.Add(cigarOp);
            }
            revisedCigar.Compress();

            return(revisedCigar);
        }
예제 #13
0
 public static uint GetReadSpanBetweenClippedEnds(this CigarAlignment cigar)
 {
     return(cigar.GetReadSpan() - cigar.GetPrefixClip() - cigar.GetSuffixClip());
 }
예제 #14
0
        public static CigarAlignment SoftclipCigar(CigarAlignment rawCigar, MatchType[] mismatchMap, uint originalSoftclipPrefix,
                                                   uint originalSoftclipSuffix, bool rescueEdgeMatches = true, bool maskNsOnly = false, int prefixNs = 0, int suffixNs = 0)
        {
            // If realignment creates a bunch of mismatches at beginning where it was once softclipped,
            // can we softclip them?
            // Which bases should be softclipped?
            // - Things that were softclipped before and are mismatches? Or are Ms?
            // - Things that were softclipped before and are Ns
            // Softclips in new alignment can be shorter than before, but not longer
            // Softclips should be terminal
            // This is rooted in an assumption that the original softclips are terminal

            if (originalSoftclipPrefix == 0 && originalSoftclipSuffix == 0)
            {
                return(rawCigar);
            }

            var expandedCigar = rawCigar.Expand();

            // Start at end of potential prefix softclip region and work backwards. This way we can rescue things that were matches previously sandwiched in softclips and now freed up by realignment.
            var mismatchMapIndex = (int)originalSoftclipPrefix;
            var startedSoftclip  = false;

            var maxSoftclipPrefixLength = Math.Min(expandedCigar.FindIndex(x => x.Type != 'M') + 1, originalSoftclipPrefix);
            var maxSoftclipSuffixLength = Math.Min(expandedCigar.Count - expandedCigar.FindLastIndex(x => x.Type != 'M'), originalSoftclipSuffix);

            for (var i = 0; i < maxSoftclipPrefixLength; i++)
            {
                var index = (int)maxSoftclipPrefixLength - 1 - i;

                mismatchMapIndex--;

                if (expandedCigar[index].Type != 'M')
                {
                    continue;
                }

                bool shouldSoftclip;

                if (maskNsOnly)
                {
                    shouldSoftclip = index < prefixNs;
                }
                else
                {
                    shouldSoftclip = !rescueEdgeMatches || startedSoftclip || mismatchMap[mismatchMapIndex] != MatchType.Match;
                }

                if (shouldSoftclip)
                {
                    startedSoftclip      = true;
                    expandedCigar[index] = new CigarOp('S', 1);
                }
            }

            // Start at beginning of potential suffix softclip region and work forwards
            startedSoftclip  = false;
            mismatchMapIndex = mismatchMap.Length - (int)maxSoftclipSuffixLength - 1;
            for (var i = 0; i < maxSoftclipSuffixLength; i++)
            {
                var index = expandedCigar.Count() - ((int)maxSoftclipSuffixLength - i);
                mismatchMapIndex++;

                if (expandedCigar[index].Type != 'M')
                {
                    continue;
                }
                bool shouldSoftclip;
                if (maskNsOnly)
                {
                    shouldSoftclip = suffixNs > 0 && mismatchMapIndex >= rawCigar.GetReadSpan() - suffixNs;
                }
                else
                {
                    shouldSoftclip = !rescueEdgeMatches || startedSoftclip || mismatchMap[mismatchMapIndex] != MatchType.Match;
                }
                if (shouldSoftclip)
                {
                    startedSoftclip      = true;
                    expandedCigar[index] = new CigarOp('S', 1);
                }
            }

            // We can only anchor a read on an M, so if we've softclipped everything away we're in trouble! Add back one.
            if (!expandedCigar.Any(o => o.Type == 'M'))
            {
                var hasAnyNonSoftclipPos = expandedCigar.Any(o => o.Type != 'S');
                var firstNonSoftclipPos  = hasAnyNonSoftclipPos
                    ? expandedCigar.FindIndex(o => o.Type != 'S')
                    : (expandedCigar.Count);
                // Set the last position of softclip to M.
                expandedCigar[firstNonSoftclipPos - 1] = new CigarOp('M', expandedCigar[firstNonSoftclipPos - 1].Length);
            }

            // Re-compile back into a revised cigar.
            var revisedCigar = new CigarAlignment();

            foreach (var cigarOp in expandedCigar)
            {
                revisedCigar.Add(cigarOp);
            }
            revisedCigar.Compress();

            return(revisedCigar);
        }