UNPKG

@gmod/cram

Version:

read CRAM files with pure Javascript

159 lines (146 loc) 4.66 kB
import { instantiateCodec } from '../codecs/index.ts' import type CramCodec from '../codecs/_base.ts' import type { DataSeriesEncodingKey, DataSeriesEncodingMap, } from '../codecs/dataSeriesTypes.ts' import type { CramEncoding } from '../encoding.ts' import type { CramCompressionHeader } from '../sectionParsers.ts' // the hardcoded data type to be decoded for each core // data field const dataSeriesTypes = { BF: 'int', CF: 'int', RI: 'int', RL: 'int', AP: 'int', RG: 'int', MF: 'int', NS: 'int', NP: 'int', TS: 'int', NF: 'int', TC: 'byte', TN: 'int', FN: 'int', FC: 'byte', FP: 'int', BS: 'byte', IN: 'byteArray', SC: 'byteArray', DL: 'int', BA: 'byte', BB: 'byteArray', RS: 'int', PD: 'int', HC: 'int', MQ: 'int', RN: 'byteArray', QS: 'byte', QQ: 'byteArray', TL: 'int', // TM: 'ignore', // TV: 'ignore', } as const export { dataSeriesTypes } export type DataSeriesTypes = typeof dataSeriesTypes // For each reference base index 0..4 (A,C,G,T,N), the three other bases plus N // (or T for ref=N), in the order they're packed into the 2-bit substitution code const SUBSTITUTIONS = [ ['C', 'G', 'T', 'N'], ['A', 'G', 'T', 'N'], ['A', 'C', 'T', 'N'], ['A', 'C', 'G', 'N'], ['A', 'C', 'G', 'T'], ] as const function parseSubstitutionMatrix(byteArray: number[]) { const matrix: string[][] = new Array(5) for (let i = 0; i < 5; i++) { const row = new Array<string>(4) const byte = byteArray[i]! const subs = SUBSTITUTIONS[i]! for (let j = 0; j < 4; j++) { row[(byte >> (6 - 2 * j)) & 3] = subs[j]! } matrix[i] = row } return matrix } type DataSeriesCache = { [K in DataSeriesEncodingKey]?: CramCodec<DataSeriesTypes[K]> } export default class CramContainerCompressionScheme { public readNamesIncluded: boolean public APdelta: boolean public referenceRequired: boolean public tagIdsDictionary: Record<number, string[]> public substitutionMatrix: string[][] public dataSeriesCodecCache: DataSeriesCache = {} public tagCodecCache: Record<string, CramCodec> = {} public tagEncoding: Record<string, CramEncoding> = {} public dataSeriesEncoding: DataSeriesEncodingMap constructor(content: CramCompressionHeader) { // interpret some of the preservation map tags for convenient use // preservation-map defaults when a key is absent, per the CRAM spec // (matches htslib cram_decode.c): RN=false, AP=true, RR=true. this.readNamesIncluded = content.preservation.RN ?? false this.APdelta = content.preservation.AP ?? true this.referenceRequired = content.preservation.RR ?? true this.tagIdsDictionary = content.preservation.TD this.substitutionMatrix = parseSubstitutionMatrix(content.preservation.SM) this.dataSeriesEncoding = content.dataSeriesEncoding this.tagEncoding = content.tagEncoding } /** * @param {string} tagName three-character tag name * @private */ getCodecForTag(tagName: string): CramCodec { if (!this.tagCodecCache[tagName]) { const encodingData = this.tagEncoding[tagName] if (!encodingData) { throw new Error('Error, no tag encoding') } // all tags are byte array data this.tagCodecCache[tagName] = instantiateCodec(encodingData, 'byteArray') } return this.tagCodecCache[tagName] } /** * * @param {number} tagListId ID of the tag list to fetch from the tag dictionary * @private */ getTagNames(tagListId: number) { return this.tagIdsDictionary[tagListId] } getCodecForDataSeries<TDataSeries extends DataSeriesEncodingKey>( dataSeriesName: TDataSeries, ): CramCodec<DataSeriesTypes[TDataSeries]> | undefined { let r: CramCodec<DataSeriesTypes[TDataSeries]> | undefined = this.dataSeriesCodecCache[dataSeriesName] if (r === undefined) { const encodingData = this.dataSeriesEncoding[dataSeriesName] if (encodingData) { r = instantiateCodec(encodingData, dataSeriesTypes[dataSeriesName]) // TS can't unify the per-key cache value type with the generic // TDataSeries — store via an untyped slot. this.dataSeriesCodecCache[dataSeriesName] = r as DataSeriesCache[TDataSeries] } } return r } // Used implicitly by snapshot tests to keep the codec caches (which contain // class instances and are noisy/non-stable) out of the serialized form. toJSON() { const data: Record<string, unknown> = {} Object.keys(this).forEach(k => { if (k.endsWith('Cache')) { return } data[k] = (this as Record<string, unknown>)[k] }) return data } }