]> git.donarmstrong.com Git - bamtools.git/blob - src/api/internal/BamToolsIndex_p.cpp
05d43275e1c307372481eb559004a741ff81fa88
[bamtools.git] / src / api / internal / BamToolsIndex_p.cpp
1 // ***************************************************************************
2 // BamToolsIndex.cpp (c) 2010 Derek Barnett
3 // Marth Lab, Department of Biology, Boston College
4 // All rights reserved.
5 // ---------------------------------------------------------------------------
6 // Last modified: 27 April 2011 (DB)
7 // ---------------------------------------------------------------------------
8 // Provides index operations for the BamTools index format (".bti")
9 // ***************************************************************************
10
11 #include <api/BamAlignment.h>
12 #include <api/internal/BamReader_p.h>
13 #include <api/internal/BamToolsIndex_p.h>
14 #include <api/internal/BgzfStream_p.h>
15 using namespace BamTools;
16 using namespace BamTools::Internal;
17
18 #include <cstdio>
19 #include <cstdlib>
20 #include <cstring>
21 #include <algorithm>
22 #include <iostream>
23 #include <iterator>
24 #include <map>
25 using namespace std;
26
27 // static BamToolsIndex constants
28 const int BamToolsIndex::DEFAULT_BLOCK_LENGTH = 1000;
29 const string BamToolsIndex::BTI_EXTENSION     = ".bti";
30 const char* const BamToolsIndex::BTI_MAGIC    = "BTI\1";
31 const int BamToolsIndex::SIZEOF_BLOCK         = sizeof(int32_t)*2 + sizeof(int64_t);
32
33 // ctor
34 BamToolsIndex::BamToolsIndex(Internal::BamReaderPrivate* reader)
35     : BamIndex(reader)
36     , m_indexStream(0)
37     , m_cacheMode(BamIndex::LimitedIndexCaching)
38     , m_blockSize(BamToolsIndex::DEFAULT_BLOCK_LENGTH)
39     , m_inputVersion(0)
40     , m_outputVersion(BTI_1_2) // latest version - used for writing new index files
41 {
42     m_isBigEndian = BamTools::SystemIsBigEndian();
43 }
44
45 // dtor
46 BamToolsIndex::~BamToolsIndex(void) {
47     CloseFile();
48 }
49
50 bool BamToolsIndex::CheckMagicNumber(void) {
51
52     // check 'magic number' to see if file is BTI index
53     char magic[4];
54     size_t elementsRead = fread(magic, sizeof(char), 4, m_indexStream);
55     if ( elementsRead != 4 ) {
56         cerr << "BamToolsIndex ERROR: could not read format 'magic' number" << endl;
57         return false;
58     }
59
60     if ( strncmp(magic, BamToolsIndex::BTI_MAGIC, 4) != 0 ) {
61         cerr << "BamToolsIndex ERROR: invalid format" << endl;
62         return false;
63     }
64
65     // otherwise ok
66     return true;
67 }
68
69 // check index file version, return true if OK
70 bool BamToolsIndex::CheckVersion(void) {
71
72     // read version from file
73     size_t elementsRead = fread(&m_inputVersion, sizeof(m_inputVersion), 1, m_indexStream);
74     if ( elementsRead != 1 ) return false;
75     if ( m_isBigEndian ) SwapEndian_32(m_inputVersion);
76
77     // if version is negative, or zero
78     if ( m_inputVersion <= 0 ) {
79         cerr << "BamToolsIndex ERROR: could not load index file: invalid version."
80              << endl;
81         return false;
82     }
83
84     // if version is newer than can be supported by this version of bamtools
85     else if ( m_inputVersion > m_outputVersion ) {
86         cerr << "BamToolsIndex ERROR: could not load index file. This version of BamTools does not recognize new index file version"
87              << endl
88              << "Please update BamTools to a more recent version to support this index file."
89              << endl;
90         return false;
91     }
92
93     // ------------------------------------------------------------------
94     // check for deprecated, unsupported versions
95     // (typically whose format did not accomodate a particular bug fix)
96
97     else if ( (Version)m_inputVersion == BamToolsIndex::BTI_1_0 ) {
98         cerr << "BamToolsIndex ERROR: could not load index file. This version of the index contains a bug related to accessing data near reference ends."
99              << endl << endl
100              << "Please run 'bamtools index -bti -in yourData.bam' to generate an up-to-date, fixed BTI file."
101              << endl << endl;
102         return false;
103     }
104
105     else if ( (Version)m_inputVersion == BamToolsIndex::BTI_1_1 ) {
106         cerr << "BamToolsIndex ERROR: could not load index file. This version of the index contains a bug related to handling empty references."
107              << endl << endl
108              << "Please run 'bamtools index -bti -in yourData.bam' to generate an up-to-date, fixed BTI file."
109              << endl << endl;
110         return false;
111     }
112
113     // otherwise ok
114     else return true;
115 }
116
117 void BamToolsIndex::ClearReferenceEntry(BtiReferenceEntry& refEntry) {
118     refEntry.ID = -1;
119     refEntry.Blocks.clear();
120 }
121
122 void BamToolsIndex::CloseFile(void) {
123     if ( IsFileOpen() )
124         fclose(m_indexStream);
125     m_indexFileSummary.clear();
126 }
127
128 // builds index from associated BAM file & writes out to index file
129 bool BamToolsIndex::Create(void) {
130
131     // return false if BamReader is invalid or not open
132     if ( m_reader == 0 || !m_reader->IsOpen() ) {
133         cerr << "BamToolsIndex ERROR: BamReader is not open"
134              << ", aborting index creation" << endl;
135         return false;
136     }
137
138     // rewind BamReader
139     if ( !m_reader->Rewind() ) {
140         cerr << "BamToolsIndex ERROR: could not rewind BamReader to create index"
141              << ", aborting index creation" << endl;
142         return false;
143     }
144
145     // open new index file (read & write)
146     string indexFilename = m_reader->Filename() + Extension();
147     if ( !OpenFile(indexFilename, "w+b") ) {
148         cerr << "BamToolsIndex ERROR: could not open ouput index file " << indexFilename
149              << ", aborting index creation" << endl;
150         return false;
151     }
152
153     // initialize BtiFileSummary with number of references
154     const int& numReferences = m_reader->GetReferenceCount();
155     InitializeFileSummary(numReferences);
156
157     // initialize output file
158     bool createdOk = true;
159     createdOk &= WriteHeader();
160
161     // index building markers
162     int32_t currentBlockCount      = 0;
163     int64_t currentAlignmentOffset = m_reader->Tell();
164     int32_t blockRefId             = -1;
165     int32_t blockMaxEndPosition    = -1;
166     int64_t blockStartOffset       = currentAlignmentOffset;
167     int32_t blockStartPosition     = -1;
168
169     // plow through alignments, storing index entries
170     BamAlignment al;
171     BtiReferenceEntry refEntry;
172     while ( m_reader->LoadNextAlignment(al) ) {
173
174         // if moved to new reference
175         if ( al.RefID != blockRefId ) {
176
177             // if first pass, check:
178             if ( currentBlockCount == 0 ) {
179
180                 // write any empty references up to (but not including) al.RefID
181                 for ( int i = 0; i < al.RefID; ++i ) {
182                     BtiReferenceEntry emptyEntry(i);
183                     createdOk &= WriteReferenceEntry(emptyEntry);
184                 }
185             }
186
187             // not first pass:
188             else {
189
190                 // store previous BTI block data in reference entry
191                 BtiBlock block(blockMaxEndPosition, blockStartOffset, blockStartPosition);
192                 refEntry.Blocks.push_back(block);
193
194                 // write reference entry, then clear
195                 createdOk &= WriteReferenceEntry(refEntry);
196                 ClearReferenceEntry(refEntry);
197
198                 // write any empty references between (but not including) the last blockRefID and current al.RefID
199                 for ( int i = blockRefId+1; i < al.RefID; ++i ) {
200                     BtiReferenceEntry emptyEntry(i);
201                     createdOk &= WriteReferenceEntry(emptyEntry);
202                 }
203
204                 // reset block count
205                 currentBlockCount = 0;
206             }
207
208             // set ID for new reference entry
209             refEntry.ID = al.RefID;
210         }
211
212         // if beginning of block, update counters
213         if ( currentBlockCount == 0 ) {
214             blockRefId          = al.RefID;
215             blockStartOffset    = currentAlignmentOffset;
216             blockStartPosition  = al.Position;
217             blockMaxEndPosition = al.GetEndPosition();
218         }
219
220         // increment block counter
221         ++currentBlockCount;
222
223         // check end position
224         int32_t alignmentEndPosition = al.GetEndPosition();
225         if ( alignmentEndPosition > blockMaxEndPosition )
226             blockMaxEndPosition = alignmentEndPosition;
227
228         // if block is full, get offset for next block, reset currentBlockCount
229         if ( currentBlockCount == m_blockSize ) {
230
231             // store previous block data in reference entry
232             BtiBlock block(blockMaxEndPosition, blockStartOffset, blockStartPosition);
233             refEntry.Blocks.push_back(block);
234
235             // update markers
236             blockStartOffset  = m_reader->Tell();
237             currentBlockCount = 0;
238         }
239
240         // not the best name, but for the next iteration, this value will be the offset of the *current* alignment
241         // necessary because we won't know if this next alignment is on a new reference until we actually read it
242         currentAlignmentOffset = m_reader->Tell();
243     }
244
245     // after finishing alignments, if any data was read, check:
246     if ( blockRefId >= 0 ) {
247
248         // store last BTI block data in reference entry
249         BtiBlock block(blockMaxEndPosition, blockStartOffset, blockStartPosition);
250         refEntry.Blocks.push_back(block);
251
252         // write last reference entry, then clear
253         createdOk &= WriteReferenceEntry(refEntry);
254         ClearReferenceEntry(refEntry);
255
256         // then write any empty references remaining at end of file
257         for ( int i = blockRefId+1; i < numReferences; ++i ) {
258             BtiReferenceEntry emptyEntry(i);
259             createdOk &= WriteReferenceEntry(emptyEntry);
260         }
261     }
262
263     // rewind reader & return result
264     createdOk &= m_reader->Rewind();
265
266     // return result
267     return createdOk;
268 }
269
270 // returns format's file extension
271 const std::string BamToolsIndex::Extension(void) {
272     return BamToolsIndex::BTI_EXTENSION;
273 }
274
275 bool BamToolsIndex::GetOffset(const BamRegion& region, int64_t& offset, bool* hasAlignmentsInRegion) {
276
277     // return false ref ID is not a valid index in file summary data
278     if ( region.LeftRefID < 0 || region.LeftRefID >= (int)m_indexFileSummary.size() )
279         return false;
280
281     // retrieve reference index data for left bound reference
282     BtiReferenceEntry refEntry(region.LeftRefID);
283     if ( !ReadReferenceEntry(refEntry) ) {
284         cerr << "BamToolsIndex ERROR: could not retrieve index data from BTI file" << endl;
285         return false;
286     }
287
288     // binary search for an overlapping block (may not be first one though)
289     bool found = false;
290     typedef BtiBlockVector::const_iterator BtiBlockConstIterator;
291     BtiBlockConstIterator blockFirst = refEntry.Blocks.begin();
292     BtiBlockConstIterator blockIter  = blockFirst;
293     BtiBlockConstIterator blockLast  = refEntry.Blocks.end();
294     iterator_traits<BtiBlockConstIterator>::difference_type count = distance(blockFirst, blockLast);
295     iterator_traits<BtiBlockConstIterator>::difference_type step;
296     while ( count > 0 ) {
297         blockIter = blockFirst;
298         step = count/2;
299         advance(blockIter, step);
300
301         const BtiBlock& block = (*blockIter);
302         if ( block.StartPosition <= region.RightPosition ) {
303             if ( block.MaxEndPosition >= region.LeftPosition ) {
304                 offset = block.StartOffset;
305                 break;
306             }
307             blockFirst = ++blockIter;
308             count -= step+1;
309         }
310         else count = step;
311     }
312
313     // if we didn't search "off the end" of the blocks
314     if ( blockIter != blockLast ) {
315
316         // "walk back" until we've gone too far
317         while ( blockIter != blockFirst ) {
318             const BtiBlock& currentBlock = (*blockIter);
319
320             --blockIter;
321             const BtiBlock& previousBlock = (*blockIter);
322             if ( previousBlock.MaxEndPosition < region.LeftPosition ) {
323                 offset = currentBlock.StartOffset;
324                 found = true;
325                 break;
326             }
327         }
328
329         // if we walked all the way to first block, just return that and let the reader's
330         // region overlap parsing do the rest
331         if ( blockIter == blockFirst ) {
332             const BtiBlock& block = (*blockIter);
333             offset = block.StartOffset;
334             found = true;
335         }
336     }
337
338
339     // sets to false if blocks container is empty, or if no matching block could be found
340     *hasAlignmentsInRegion = found;
341
342     // return success
343     return true;
344 }
345
346 // returns whether reference has alignments or no
347 bool BamToolsIndex::HasAlignments(const int& referenceID) const {
348     if ( referenceID < 0 || referenceID >= (int)m_indexFileSummary.size() )
349         return false;
350     const BtiReferenceSummary& refSummary = m_indexFileSummary.at(referenceID);
351     return ( refSummary.NumBlocks > 0 );
352 }
353
354 void BamToolsIndex::InitializeFileSummary(const int& numReferences) {
355     m_indexFileSummary.clear();
356     for ( int i = 0; i < numReferences; ++i )
357         m_indexFileSummary.push_back( BtiReferenceSummary() );
358 }
359
360 bool BamToolsIndex::IsFileOpen(void) const {
361     return ( m_indexStream != 0 );
362 }
363
364 // attempts to use index data to jump to @region, returns success/fail
365 // a "successful" jump indicates no error, but not whether this region has data
366 //   * thus, the method sets a flag to indicate whether there are alignments
367 //     available after the jump position
368 bool BamToolsIndex::Jump(const BamTools::BamRegion& region, bool* hasAlignmentsInRegion) {
369
370     // clear flag
371     *hasAlignmentsInRegion = false;
372
373     // skip if invalid reader or not open
374     if ( m_reader == 0 || !m_reader->IsOpen() )
375         return false;
376
377     // make sure left-bound position is valid
378     const RefVector& references = m_reader->GetReferenceData();
379     if ( region.LeftPosition > references.at(region.LeftRefID).RefLength )
380         return false;
381
382     // calculate nearest offset to jump to
383     int64_t offset;
384     if ( !GetOffset(region, offset, hasAlignmentsInRegion) ) {
385         cerr << "BamToolsIndex ERROR: could not jump"
386              << ", unable to calculate offset for specified region" << endl;
387         return false;
388     }
389
390     // return success/failure of seek
391     return m_reader->Seek(offset);
392 }
393
394 // loads existing data from file into memory
395 bool BamToolsIndex::Load(const std::string& filename) {
396
397     // attempt open index file (read-only)
398     if ( !OpenFile(filename, "rb") ) {
399         cerr << "BamToolsIndex ERROR: could not open input index file " << filename
400              << ", aborting index load" << endl;
401         return false;
402     }
403
404     // attempt to load & validate BTI header data
405     if ( !LoadHeader() ) {
406         cerr << "BamToolsIndex ERROR: could load header from index file " << filename
407              << ", aborting index load" << endl;
408         CloseFile();
409         return false;
410     }
411
412     // attempt to load index file summary
413     if ( !LoadFileSummary() ) {
414         cerr << "BamToolsIndex ERROR: could not generate a summary of index file " << filename
415              << ", aborting index load" << endl;
416         CloseFile();
417         return false;
418     }
419
420     // if we get here, index summary is loaded OK
421     return true;
422 }
423
424 bool BamToolsIndex::LoadFileSummary(void) {
425
426     // load number of reference sequences
427     int numReferences;
428     if ( !LoadNumReferences(numReferences) )
429         return false;
430
431     // initialize file summary data
432     InitializeFileSummary(numReferences);
433
434     // iterate over reference entries
435     bool loadedOk = true;
436     BtiFileSummary::iterator summaryIter = m_indexFileSummary.begin();
437     BtiFileSummary::iterator summaryEnd  = m_indexFileSummary.end();
438     for ( ; summaryIter != summaryEnd; ++summaryIter )
439         loadedOk &= LoadReferenceSummary(*summaryIter);
440
441     // return result
442     return loadedOk;
443 }
444
445 bool BamToolsIndex::LoadHeader(void) {
446
447     // if invalid format 'magic number'
448     if ( !CheckMagicNumber() )
449         return false;
450
451     // if invalid BTI version
452     if ( !CheckVersion() )
453         return false;
454
455     // use file's BTI block size to set member variable
456     size_t elementsRead = fread(&m_blockSize, sizeof(m_blockSize), 1, m_indexStream);
457     if ( m_isBigEndian ) SwapEndian_32(m_blockSize);
458     return ( elementsRead == 1 );
459 }
460
461 bool BamToolsIndex::LoadNumBlocks(int& numBlocks) {
462     size_t elementsRead = 0;
463     elementsRead += fread(&numBlocks, sizeof(numBlocks), 1, m_indexStream);
464     if ( m_isBigEndian ) SwapEndian_32(numBlocks);
465     return ( elementsRead == 1 );
466 }
467
468 bool BamToolsIndex::LoadNumReferences(int& numReferences) {
469     size_t elementsRead = 0;
470     elementsRead += fread(&numReferences, sizeof(numReferences), 1, m_indexStream);
471     if ( m_isBigEndian ) SwapEndian_32(numReferences);
472     return ( elementsRead == 1 );
473 }
474
475 bool BamToolsIndex::LoadReferenceSummary(BtiReferenceSummary& refSummary) {
476
477     // load number of blocks
478     int numBlocks;
479     if ( !LoadNumBlocks(numBlocks) )
480         return false;
481
482     // store block summary data for this reference
483     refSummary.NumBlocks = numBlocks;
484     refSummary.FirstBlockFilePosition = Tell();
485
486     // skip blocks in index file (and return status)
487     return SkipBlocks(numBlocks);
488 }
489
490 bool BamToolsIndex::OpenFile(const std::string& filename, const char* mode) {
491
492     // make sure any previous index file is closed
493     CloseFile();
494
495     // attempt to open file
496     m_indexStream = fopen(filename.c_str(), mode);
497     return IsFileOpen();
498 }
499
500 bool BamToolsIndex::ReadBlock(BtiBlock& block) {
501
502     // read in block data members
503     size_t elementsRead = 0;
504     elementsRead += fread(&block.MaxEndPosition, sizeof(block.MaxEndPosition), 1, m_indexStream);
505     elementsRead += fread(&block.StartOffset,    sizeof(block.StartOffset),    1, m_indexStream);
506     elementsRead += fread(&block.StartPosition,  sizeof(block.StartPosition),  1, m_indexStream);
507
508     // swap endian-ness if necessary
509     if ( m_isBigEndian ) {
510         SwapEndian_32(block.MaxEndPosition);
511         SwapEndian_64(block.StartOffset);
512         SwapEndian_32(block.StartPosition);
513     }
514
515     // return success/failure
516     return ( elementsRead == 3 );
517 }
518
519 bool BamToolsIndex::ReadBlocks(const BtiReferenceSummary& refSummary, BtiBlockVector& blocks) {
520
521     // prep blocks container
522     blocks.clear();
523     blocks.reserve(refSummary.NumBlocks);
524
525     // skip to first block entry
526     if ( !Seek( refSummary.FirstBlockFilePosition, SEEK_SET ) ) {
527         cerr << "BamToolsIndex ERROR: could not seek to position "
528              << refSummary.FirstBlockFilePosition << endl;
529         return false;
530     }
531
532     // read & store block entries
533     bool readOk = true;
534     BtiBlock block;
535     for ( int i = 0; i < refSummary.NumBlocks; ++i ) {
536         readOk &= ReadBlock(block);
537         blocks.push_back(block);
538     }
539     return readOk;
540 }
541
542 bool BamToolsIndex::ReadReferenceEntry(BtiReferenceEntry& refEntry) {
543
544     // return false if refId not valid index in file summary structure
545     if ( refEntry.ID < 0 || refEntry.ID >= (int)m_indexFileSummary.size() )
546         return false;
547
548     // use index summary to assist reading the reference's BTI blocks
549     const BtiReferenceSummary& refSummary = m_indexFileSummary.at(refEntry.ID);
550     return ReadBlocks(refSummary, refEntry.Blocks);
551 }
552
553 bool BamToolsIndex::Seek(const int64_t& position, const int& origin) {
554     return ( fseek64(m_indexStream, position, origin) == 0 );
555 }
556
557 // change the index caching behavior
558 void BamToolsIndex::SetCacheMode(const BamIndex::IndexCacheMode& mode) {
559     m_cacheMode = mode;
560     // do nothing else here ? cache mode will be ignored from now on, most likely
561 }
562
563 bool BamToolsIndex::SkipBlocks(const int& numBlocks) {
564     return Seek( numBlocks*BamToolsIndex::SIZEOF_BLOCK, SEEK_CUR );
565 }
566
567 int64_t BamToolsIndex::Tell(void) const {
568     return ftell64(m_indexStream);
569 }
570
571 bool BamToolsIndex::WriteBlock(const BtiBlock& block) {
572
573     // copy entry data
574     int32_t maxEndPosition = block.MaxEndPosition;
575     int64_t startOffset    = block.StartOffset;
576     int32_t startPosition  = block.StartPosition;
577
578     // swap endian-ness if necessary
579     if ( m_isBigEndian ) {
580         SwapEndian_32(maxEndPosition);
581         SwapEndian_64(startOffset);
582         SwapEndian_32(startPosition);
583     }
584
585     // write the reference index entry
586     size_t elementsWritten = 0;
587     elementsWritten += fwrite(&maxEndPosition, sizeof(maxEndPosition), 1, m_indexStream);
588     elementsWritten += fwrite(&startOffset,    sizeof(startOffset),    1, m_indexStream);
589     elementsWritten += fwrite(&startPosition,  sizeof(startPosition),  1, m_indexStream);
590     return ( elementsWritten == 3 );
591 }
592
593 bool BamToolsIndex::WriteBlocks(const BtiBlockVector& blocks) {
594     bool writtenOk = true;
595     BtiBlockVector::const_iterator blockIter = blocks.begin();
596     BtiBlockVector::const_iterator blockEnd  = blocks.end();
597     for ( ; blockIter != blockEnd; ++blockIter )
598         writtenOk &= WriteBlock(*blockIter);
599     return writtenOk;
600 }
601
602 bool BamToolsIndex::WriteHeader(void) {
603
604     size_t elementsWritten = 0;
605
606     // write BTI index format 'magic number'
607     elementsWritten += fwrite(BamToolsIndex::BTI_MAGIC, 1, 4, m_indexStream);
608
609     // write BTI index format version
610     int32_t currentVersion = (int32_t)m_outputVersion;
611     if ( m_isBigEndian ) SwapEndian_32(currentVersion);
612     elementsWritten += fwrite(&currentVersion, sizeof(currentVersion), 1, m_indexStream);
613
614     // write block size
615     int32_t blockSize = m_blockSize;
616     if ( m_isBigEndian ) SwapEndian_32(blockSize);
617     elementsWritten += fwrite(&blockSize, sizeof(blockSize), 1, m_indexStream);
618
619     // write number of references
620     int32_t numReferences = m_indexFileSummary.size();
621     if ( m_isBigEndian ) SwapEndian_32(numReferences);
622     elementsWritten += fwrite(&numReferences, sizeof(numReferences), 1, m_indexStream);
623
624     // return success/failure of write
625     return ( elementsWritten == 7 );
626 }
627
628 bool BamToolsIndex::WriteReferenceEntry(const BtiReferenceEntry& refEntry) {
629
630     size_t elementsWritten = 0;
631
632     // write number of blocks this reference
633     uint32_t numBlocks = refEntry.Blocks.size();
634     if ( m_isBigEndian ) SwapEndian_32(numBlocks);
635     elementsWritten += fwrite(&numBlocks, sizeof(numBlocks), 1, m_indexStream);
636
637     // write actual block entries
638     const bool blocksOk = WriteBlocks(refEntry.Blocks);
639
640     // return success/fail
641     return ( elementsWritten == 1) && blocksOk;
642 }