1 /* 2 This file is part of BioD. 3 Copyright (C) 2012-2017 Artem Tarasov <lomereiter@gmail.com> 4 5 Permission is hereby granted, free of charge, to any person obtaining a 6 copy of this software and associated documentation files (the "Software"), 7 to deal in the Software without restriction, including without limitation 8 the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 and/or sell copies of the Software, and to permit persons to whom the 10 Software is furnished to do so, subject to the following conditions: 11 12 The above copyright notice and this permission notice shall be included in 13 all copies or substantial portions of the Software. 14 15 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 DEALINGS IN THE SOFTWARE. 22 23 */ 24 module bio.bam.bai.indexing; 25 26 import bio.bam.reader; 27 import bio.bam.readrange; 28 import bio.bam.constants; 29 30 import bio.bam.bai.bin; 31 import bio.core.bgzf.chunk; 32 33 import undead.stream; 34 import std.array; 35 import std.algorithm; 36 import std.system; 37 import std.exception; 38 import core.stdc.string; 39 40 // Suppose we have an alignment which covers bases on a reference, 41 // starting from one position and ending at another position. 42 // In order to build linear index, we need to find to which windows 43 // the two positions correspond. 44 // 45 // 46 // (K = 16384) 47 // 48 // [0, K)[K, 2K)[2K, 3K)... <- windows 49 // [.......) <- alignment 50 // 51 private size_t toLinearIndexOffset(int position) { 52 return position < 0 ? 0 : position / BAI_LINEAR_INDEX_WINDOW_SIZE; 53 } 54 55 /// 56 struct IndexBuilder { 57 private { 58 // array of linear offsets for the current reference entry 59 VirtualOffset[] _linear_index; 60 // (maximum index in _linear_index where data was written) + 1 61 size_t _linear_index_write_length; 62 63 static struct PreviousRead { 64 int ref_id = -1; 65 int position; 66 int end_position; 67 int basesCovered() { return end_position - position; } 68 Bin bin; 69 bool is_unmapped; 70 71 private char[256] _name_buf; // by spec., name length is <= 255 chars 72 string name; 73 74 VirtualOffset start_virtual_offset; 75 VirtualOffset end_virtual_offset; 76 } 77 78 PreviousRead _prev_read; 79 80 Stream _stream; 81 int _n_refs; 82 83 ulong _no_coord = 0; 84 // metadata for each reference 85 ulong _beg_vo = -1UL; 86 ulong _end_vo = 0; 87 ulong _unmapped = 0; 88 ulong _mapped = 0; 89 90 bool _first_read = true; 91 92 // map: bin ID -> array of chunks 93 Chunk[][uint] _chunks; 94 95 VirtualOffset _current_chunk_beg; // start of current chunk 96 97 // no metadata for empty references 98 void writeEmptyReference() { 99 _stream.write(cast(int)0); // n_bins 100 _stream.write(cast(int)0); // n_intv 101 } 102 103 void updateLastReadInfo(ref BamReadBlock read) { 104 with (_prev_read) { 105 ref_id = read.ref_id; 106 position = read.position; 107 end_position = position + read.basesCovered(); 108 bin = read.bin; 109 is_unmapped = read.is_unmapped; 110 _name_buf[0 .. read.name.length] = read.name[]; 111 name = cast(string)_name_buf[0 .. read.name.length]; 112 start_virtual_offset = read.start_virtual_offset; 113 end_virtual_offset = read.end_virtual_offset; 114 } 115 } 116 117 void updateMetadata(ref BamReadBlock read) { 118 if (read.ref_id == -1) { 119 ++_no_coord; 120 } else { 121 if (read.is_unmapped) { 122 ++_unmapped; 123 } else { 124 ++_mapped; 125 } 126 127 if (_beg_vo == -1UL) 128 _beg_vo = cast(ulong)read.start_virtual_offset; 129 _end_vo = cast(ulong)read.end_virtual_offset; 130 } 131 } 132 133 void updateLinearIndex() { 134 assert(_prev_read.ref_id >= 0); 135 136 size_t beg, end; 137 138 if (_prev_read.is_unmapped) { 139 end = beg = toLinearIndexOffset(_prev_read.position); 140 } else { 141 beg = toLinearIndexOffset(_prev_read.position); 142 end = toLinearIndexOffset(_prev_read.position + _prev_read.basesCovered() - 1); 143 } 144 145 debug { 146 import std.stdio; 147 if (end >= _linear_index.length) { 148 writeln("beg: ", beg); 149 writeln("end: ", end); 150 writeln("pos: ", _prev_read.position); 151 writeln("bases: ", _prev_read.basesCovered()); 152 } 153 } 154 155 foreach (i; beg .. end + 1) 156 if (_linear_index[i] == 0UL) 157 _linear_index[i] = _prev_read.start_virtual_offset; 158 159 if (end + 1 > _linear_index_write_length) 160 _linear_index_write_length = end + 1; 161 } 162 163 void dumpCurrentLinearIndex() { 164 _stream.write(cast(int)_linear_index_write_length); 165 166 // 167 // There might be untouched places in linear index 168 // with virtual offset equal to zero. 169 // However, it's not a good idea to leave those zeros, 170 // since we can start lookup from the last non-zero virtual offset 171 // encountered before the untouched window. 172 // 173 VirtualOffset last_voffset = 0; 174 175 foreach (voffset; _linear_index[0 .. _linear_index_write_length]) { 176 if (voffset == 0) 177 voffset = last_voffset; 178 else 179 last_voffset = voffset; 180 _stream.write(cast(ulong)voffset); 181 } 182 } 183 184 void dumpCurrentReference() { 185 // +1 because we output dummy bin, too 186 _stream.write(cast(int)(_chunks.length + 1)); 187 188 foreach (bin_id, bin_chunks; _chunks) { 189 if (bin_chunks.length > 0) { 190 _stream.write(cast(uint)bin_id); 191 _stream.write(cast(int)bin_chunks.length); 192 foreach (chunk; bin_chunks) { 193 _stream.write(cast(ulong)chunk.beg); 194 _stream.write(cast(ulong)chunk.end); 195 } 196 } 197 } 198 _stream.write(cast(uint)37450); 199 _stream.write(cast(int)2); 200 _stream.write(cast(ulong)_beg_vo); 201 _stream.write(cast(ulong)_end_vo); 202 _stream.write(cast(ulong)_mapped); 203 _stream.write(cast(ulong)_unmapped); 204 205 dumpCurrentLinearIndex(); 206 207 // reset data 208 memset(_linear_index.ptr, 0, _linear_index.length * ulong.sizeof); 209 _linear_index_write_length = 0; 210 _chunks = null; 211 _current_chunk_beg = _prev_read.end_virtual_offset; 212 213 _beg_vo = _end_vo = cast(ulong)_current_chunk_beg; 214 _unmapped = 0; 215 _mapped = 0; 216 } 217 218 // adds chunk to the current bin (which is determined from _prev_read) 219 void updateChunks() { 220 auto current_chunk_end = _prev_read.end_virtual_offset; 221 222 auto bin_id = _prev_read.bin.id; 223 224 if (bin_id !in _chunks) 225 _chunks[bin_id] = []; 226 auto cs = _chunks[bin_id]; 227 228 bool canMergeWithPreviousChunk() { 229 assert(cs.length > 0); 230 auto last_chunk = cs[$ - 1]; 231 232 if (last_chunk.end.coffset == _current_chunk_beg.coffset) 233 return true; 234 235 return false; 236 } 237 238 if (cs.length == 0 || !canMergeWithPreviousChunk()) { 239 auto new_chunk = Chunk(_current_chunk_beg, current_chunk_end); 240 _chunks[_prev_read.bin.id] ~= new_chunk; 241 } else { 242 _chunks[_prev_read.bin.id][$ - 1].end = current_chunk_end; 243 } 244 245 _current_chunk_beg = current_chunk_end; 246 } 247 248 void checkThatBinIsCorrect(ref BamReadBlock read) { 249 if (!check_bins) 250 return; 251 auto expected = reg2bin(read.position, 252 read.position + read.basesCovered()); 253 enforce(read.bin.id == expected, 254 "Bin in read with name '" ~ read.name ~ 255 "' is set incorrectly (" ~ to!string(read.bin.id) ~ 256 " instead of expected " ~ to!string(expected) ~ ")"); 257 } 258 259 void checkThatInputIsSorted(ref BamReadBlock read) { 260 if (_first_read) return; 261 if (read.ref_id == -1) return; // unmapped 262 if (_prev_read.ref_id < read.ref_id) return; 263 264 enforce(read.ref_id == _prev_read.ref_id && 265 read.position >= _prev_read.position, 266 "BAM file is not coordinate-sorted: " ~ 267 "read '" ~ read.name ~ "' (" ~ read.ref_id.to!string ~ ":" ~ read.position.to!string ~ ")" ~ 268 " must be after read '" ~ _prev_read.name ~ "' (" ~ _prev_read.ref_id.to!string ~ ":" ~ _prev_read.position.to!string ~ ")" ~ 269 "' (at virtual offsets " ~ 270 to!string(_prev_read.start_virtual_offset) ~ ", " ~ read.start_virtual_offset.to!string ~ ")"); 271 } 272 } 273 274 /// 275 this(Stream output_stream, int number_of_references) { 276 _stream = new EndianStream(output_stream, Endian.littleEndian); 277 _n_refs = number_of_references; 278 279 size_t size = BAI_MAX_BIN_ID - BAI_MAX_NONLEAF_BIN_ID + 1; 280 _linear_index = new VirtualOffset[](size); 281 282 _stream.writeString(BAI_MAGIC); // write BAI magic string 283 _stream.write(cast(int)_n_refs); // and number of references 284 } 285 286 /// Check that bins are correct. 287 bool check_bins; 288 289 /// Add a read. The reads must be put in coordinate-sorted order. 290 void put(BamReadBlock read) { 291 checkThatInputIsSorted(read); 292 scope(exit) updateMetadata(read); 293 294 if (read.ref_id < 0) 295 return; 296 297 // start position is unavailable, skip 298 if (read.position < 0) 299 return; 300 301 if (_first_read) { 302 updateLastReadInfo(read); 303 _first_read = false; 304 _current_chunk_beg = read.start_virtual_offset; 305 306 if (read.ref_id > 0) 307 foreach (i; 0 .. read.ref_id) 308 writeEmptyReference(); 309 310 return; 311 } 312 313 checkThatBinIsCorrect(read); 314 315 // new reference, so write data for previous one(s) 316 if (read.ref_id > _prev_read.ref_id) { 317 updateLinearIndex(); 318 updateChunks(); 319 dumpCurrentReference(); 320 321 foreach (i; _prev_read.ref_id + 1 .. read.ref_id) 322 writeEmptyReference(); 323 } 324 325 if (read.ref_id == _prev_read.ref_id) { 326 updateLinearIndex(); 327 328 if (read.bin.id != _prev_read.bin.id) 329 updateChunks(); 330 } 331 332 updateLastReadInfo(read); 333 } 334 335 /// Closes the stream 336 void finish() { 337 if (!_first_read) { // at least one was processed 338 assert(_prev_read.ref_id >= 0); 339 updateLinearIndex(); 340 updateChunks(); 341 dumpCurrentReference(); 342 } 343 344 // _prev_read.ref_id == -1 if all are unmapped 345 foreach (i; _prev_read.ref_id + 1 .. _n_refs) 346 writeEmptyReference(); 347 348 _stream.write(cast(ulong)_no_coord); 349 _stream.close(); 350 } 351 } 352 353 /// Writes BAM index to the $(D stream) 354 /// 355 /// Accepts optional $(D progressBarFunc) 356 void createIndex(BamReader bam, Stream stream, bool check_bins=false, 357 void delegate(lazy float p) progressBarFunc=null) 358 { 359 auto n_refs = cast(int)bam.reference_sequences.length; 360 auto index_builder = IndexBuilder(stream, n_refs); 361 index_builder.check_bins = check_bins; 362 auto reads = bam.readsWithProgress!withOffsets(progressBarFunc); 363 foreach (read; reads) 364 index_builder.put(read); 365 index_builder.finish(); 366 }