UNPKG

@gmod/cram

Version:

read CRAM files with pure Javascript

371 lines 13.3 kB
import Constants from "./constants.js"; function decodeReadSequence(cramRecord, refRegion) { // if it has no length, it has no sequence if (!cramRecord.lengthOnRef && !cramRecord.readLength) { return null; } if (cramRecord.isUnknownBases()) { return null; } // remember: all coordinates are 1-based closed const regionSeqOffset = cramRecord.alignmentStart - refRegion.start; if (!cramRecord.readFeatures) { return refRegion.seq .slice(regionSeqOffset, regionSeqOffset + (cramRecord.lengthOnRef || 0)) .toUpperCase(); } let bases = ''; let regionPos = regionSeqOffset; let currentReadFeature = 0; while (bases.length < cramRecord.readLength) { if (currentReadFeature < cramRecord.readFeatures.length) { const feature = cramRecord.readFeatures[currentReadFeature]; if (feature.code === 'Q' || feature.code === 'q') { currentReadFeature += 1; } else if (feature.pos === bases.length + 1) { // process the read feature currentReadFeature += 1; if (feature.code === 'b') { // specify a base pair for some reason const added = feature.data; bases += added; regionPos += added.length; } else if (feature.code === 'B') { // base pair and associated quality // TODO: do we need to set the quality in the qual scores? bases += feature.data[0]; regionPos += 1; } else if (feature.code === 'X') { // base substitution bases += feature.sub; regionPos += 1; } else if (feature.code === 'I') { // insertion bases += feature.data; } else if (feature.code === 'D') { // deletion regionPos += feature.data; } else if (feature.code === 'i') { // insert single base bases += feature.data; } else if (feature.code === 'N') { // reference skip. delete some bases // do nothing // seqBases.splice(feature.pos - 1, feature.data) regionPos += feature.data; } else if (feature.code === 'S') { // soft clipped bases that should be present in the read seq // seqBases.splice(feature.pos - 1, 0, ...feature.data.split('')) bases += feature.data; } else if (feature.code === 'P') { // padding, do nothing } else if (feature.code === 'H') { // hard clip, do nothing } } else if (currentReadFeature < cramRecord.readFeatures.length) { // put down a chunk of sequence up to the next read feature const chunk = refRegion.seq.slice(regionPos, regionPos + feature.pos - bases.length - 1); bases += chunk; regionPos += chunk.length; } } else { // put down a chunk of reference up to the full read length const chunk = refRegion.seq.slice(regionPos, regionPos + cramRecord.readLength - bases.length); bases += chunk; regionPos += chunk.length; } } return bases.toUpperCase(); } const baseNumbers = { a: 0, A: 0, c: 1, C: 1, g: 2, G: 2, t: 3, T: 3, n: 4, N: 4, }; function decodeBaseSubstitution(cramRecord, refRegion, compressionScheme, readFeature) { // decode base substitution code using the substitution matrix const refCoord = readFeature.refPos - refRegion.start; const refBase = refRegion.seq.charAt(refCoord); if (refBase) { readFeature.ref = refBase; } let baseNumber = baseNumbers[refBase]; if (baseNumber === undefined) { baseNumber = 4; } const substitutionScheme = compressionScheme.substitutionMatrix[baseNumber]; const base = substitutionScheme[readFeature.data]; if (base) { readFeature.sub = base; } } export const BamFlags = [ [0x1, 'Paired'], [0x2, 'ProperlyPaired'], [0x4, 'SegmentUnmapped'], [0x8, 'MateUnmapped'], [0x10, 'ReverseComplemented'], // the mate is mapped to the reverse strand [0x20, 'MateReverseComplemented'], // this is read1 [0x40, 'Read1'], // this is read2 [0x80, 'Read2'], // not primary alignment [0x100, 'Secondary'], // QC failure [0x200, 'FailedQc'], // optical or PCR duplicate [0x400, 'Duplicate'], // supplementary alignment [0x800, 'Supplementary'], ]; export const CramFlags = [ [0x1, 'PreservingQualityScores'], [0x2, 'Detached'], [0x4, 'WithMateDownstream'], [0x8, 'DecodeSequenceAsStar'], ]; export const MateFlags = [ [0x1, 'OnNegativeStrand'], [0x2, 'Unmapped'], ]; function makeFlagsHelper(x) { const r = {}; for (const [code, name] of x) { r[`is${name}`] = (flags) => !!(flags & code); r[`set${name}`] = (flags) => flags | code; } return r; } export const BamFlagsDecoder = makeFlagsHelper(BamFlags); export const CramFlagsDecoder = makeFlagsHelper(CramFlags); export const MateFlagsDecoder = makeFlagsHelper(MateFlags); /** * Class of each CRAM record returned by this API. */ export default class CramRecord { constructor({ flags, cramFlags, readLength, mappingQuality, lengthOnRef, qualityScores, mateRecordNumber, readBases, readFeatures, mateToUse, readGroupId, readName, sequenceId, uniqueId, templateSize, alignmentStart, tags, }) { this.flags = flags; this.cramFlags = cramFlags; this.readLength = readLength; this.mappingQuality = mappingQuality; this.lengthOnRef = lengthOnRef; this.qualityScores = qualityScores; if (readBases) { this.readBases = readBases; } this.readGroupId = readGroupId; this.readName = readName; this.sequenceId = sequenceId; this.uniqueId = uniqueId; this.templateSize = templateSize; this.alignmentStart = alignmentStart; this.tags = tags; // backwards compatibility if (readFeatures) { this.readFeatures = readFeatures; } if (mateToUse) { this.mate = { flags: mateToUse.mateFlags, readName: mateToUse.mateReadName, sequenceId: mateToUse.mateSequenceId, alignmentStart: mateToUse.mateAlignmentStart, }; } if (mateRecordNumber) { this.mateRecordNumber = mateRecordNumber; } } /** * @returns {boolean} true if the read is paired, regardless of whether both segments are mapped */ isPaired() { return !!(this.flags & Constants.BAM_FPAIRED); } /** @returns {boolean} true if the read is paired, and both segments are mapped */ isProperlyPaired() { return !!(this.flags & Constants.BAM_FPROPER_PAIR); } /** @returns {boolean} true if the read itself is unmapped; conflictive with isProperlyPaired */ isSegmentUnmapped() { return !!(this.flags & Constants.BAM_FUNMAP); } /** @returns {boolean} true if the read itself is unmapped; conflictive with isProperlyPaired */ isMateUnmapped() { return !!(this.flags & Constants.BAM_FMUNMAP); } /** @returns {boolean} true if the read is mapped to the reverse strand */ isReverseComplemented() { return !!(this.flags & Constants.BAM_FREVERSE); } /** @returns {boolean} true if the mate is mapped to the reverse strand */ isMateReverseComplemented() { return !!(this.flags & Constants.BAM_FMREVERSE); } /** @returns {boolean} true if this is read number 1 in a pair */ isRead1() { return !!(this.flags & Constants.BAM_FREAD1); } /** @returns {boolean} true if this is read number 2 in a pair */ isRead2() { return !!(this.flags & Constants.BAM_FREAD2); } /** @returns {boolean} true if this is a secondary alignment */ isSecondary() { return !!(this.flags & Constants.BAM_FSECONDARY); } /** @returns {boolean} true if this read has failed QC checks */ isFailedQc() { return !!(this.flags & Constants.BAM_FQCFAIL); } /** @returns {boolean} true if the read is an optical or PCR duplicate */ isDuplicate() { return !!(this.flags & Constants.BAM_FDUP); } /** @returns {boolean} true if this is a supplementary alignment */ isSupplementary() { return !!(this.flags & Constants.BAM_FSUPPLEMENTARY); } /** * @returns {boolean} true if the read is detached */ isDetached() { return !!(this.cramFlags & Constants.CRAM_FLAG_DETACHED); } /** @returns {boolean} true if the read has a mate in this same CRAM segment */ hasMateDownStream() { return !!(this.cramFlags & Constants.CRAM_FLAG_MATE_DOWNSTREAM); } /** @returns {boolean} true if the read contains qual scores */ isPreservingQualityScores() { return !!(this.cramFlags & Constants.CRAM_FLAG_PRESERVE_QUAL_SCORES); } /** @returns {boolean} true if the read has no sequence bases */ isUnknownBases() { return !!(this.cramFlags & Constants.CRAM_FLAG_NO_SEQ); } /** * Get the original sequence of this read. * @returns {String} sequence basepairs */ getReadBases() { if (!this.readBases && this._refRegion) { const decoded = decodeReadSequence(this, this._refRegion); if (decoded) { this.readBases = decoded; } } return this.readBases; } /** * Get the pair orientation of a paired read. Adapted from igv.js * @returns {String} of paired orientatin */ getPairOrientation() { if (!this.isSegmentUnmapped() && this.isPaired() && !this.isMateUnmapped() && this.mate && this.sequenceId === this.mate.sequenceId) { const s1 = this.isReverseComplemented() ? 'R' : 'F'; const s2 = this.isMateReverseComplemented() ? 'R' : 'F'; let o1 = ' '; let o2 = ' '; if (this.isRead1()) { o1 = '1'; o2 = '2'; } else if (this.isRead2()) { o1 = '2'; o2 = '1'; } const tmp = []; let isize = this.templateLength || this.templateSize; if (isize === undefined) { throw new Error('One of templateSize and templateLength must be set'); } if (this.alignmentStart > this.mate.alignmentStart && isize > 0) { isize = -isize; } if (isize > 0) { tmp[0] = s1; tmp[1] = o1; tmp[2] = s2; tmp[3] = o2; } else { tmp[2] = s1; tmp[3] = o1; tmp[0] = s2; tmp[1] = o2; } return tmp.join(''); } return null; } /** * Annotates this feature with the given reference sequence basepair * information. This will add a `sub` and a `ref` item to base * substitution read features given the actual substituted and reference * base pairs, and will make the `getReadSequence()` method work. * * @param {object} refRegion * @param {number} refRegion.start * @param {number} refRegion.end * @param {string} refRegion.seq * @param {CramContainerCompressionScheme} compressionScheme * @returns {undefined} nothing */ addReferenceSequence(refRegion, compressionScheme) { if (this.readFeatures) { // use the reference bases to decode the bases substituted in each base // substitution this.readFeatures.forEach(readFeature => { if (readFeature.code === 'X') { decodeBaseSubstitution(this, refRegion, compressionScheme, readFeature); } }); } // if this region completely covers this read, // keep a reference to it if (!this.readBases && refRegion.start <= this.alignmentStart && refRegion.end >= this.alignmentStart + (this.lengthOnRef || this.readLength) - 1) { this._refRegion = refRegion; } } toJSON() { const data = {}; Object.keys(this).forEach(k => { if (k.startsWith('_')) { return; } data[k] = this[k]; }); data.readBases = this.getReadBases(); return data; } } //# sourceMappingURL=record.js.map