Use multi iterators in the synced reader#2052
Conversation
Add BCF and tabix equivalents to sam_itr_regions(). Signed-off-by: Rob Davies <rmd+git@sanger.ac.uk>
Add a reader_seek_multi() function that creates a multi-region iterator over all intervals left for the current chromosome. Falls back to the old _reader_seek() function when regions are being read from a file, or on various other edge cases. The new function is called by _readers_next_region() in place of _reader_seek(). When records span multiple regions, the multi-region iterator will only return them once. As the synced reader filters out duplicated records, the results from a multi-region iterator should match those from using single-region iterators, without the inefficiency of reading and filtering out the unwanted duplicates. Iterators do not try to span more than one chromosome to keep the implementation simple. As duplicated records will not span regions on different chromosomes, this should not harm efficiency. As the multi-region iterators don't stop at region boundaries like the single ones do, some changes are needed to the code in _reader_fill_buffer() that checks records overlap the current region. As some multi-iterators may have moved on to the next region, it's necessary to track where they are on a per-file basis. Ideally this would be done by adding a variable to the bcf_sr_t struct, but that would result in an ABI change due to the struct being in a public header. Instead the data is stored in an array in the private regions_t struct. The presence of this array is also used to flag that the iterator is in multi mode. The overlap checking code is put in function multi_check_overlap() as it's rather more complicated than the single case. Notably, if BCF_SR_REGIONS_OVERLAP is 2, it's possible for a record to start beyond the current region, but still match a later one. As the multi iterator only yields the record once, it's necessary to search forward to check which region it matches, if any. Fixes a single-iterator bug where records could be returned out of order when BCF_SR_REGIONS_OVERLAP is 2. This could happen if the variant in the record occurred after the end of the first region it overlapped, but also hit a later region. Signed-off-by: Rob Davies <rmd+git@sanger.ac.uk>
When reading a tabix-indexed regions file, and an iterator has not yet been set by calling bcf_sr_regions_seek(), bcf_sr_regions_next() was switching to non-indexed mode by closing and reopening the regions file. Instead, it's possible to get the same result by creating an iterator over the whole file, which should be more efficient. Signed-off-by: Rob Davies <rmd+git@sanger.ac.uk>
Allow the synced BCF reader to use multi-region iterators when using regions from a compressed and tabix-indexed file. The strategy used is to build an in-memory region list for the current chromosome being read from the regions file, allowing the existing code for in-memory regions to then build a multi-iterator. Unfortunately this does mean using more memory, as it needs to store regions for the entire chromosome instead of just the current one. For region lists that reference more than one chromosome it will still be smaller than reading the entire file. As adjacent regions are merged, the worst case for regions on Human would be about 2Gb (chr1 with regions listing alternate bases). One minor complication is that the only way to find out a new chromosome has been started is to read the entry for it from the regions file iterator. As there's no way to rewind, and the entry doesn't belong on the list for the current chromosome, the new location is temporarily stored in bcf_srs_t::aux. The next time a region is fetched, this data if present is returned so that it can be used to start the list for the next chromosome. This change means bcf_sr_regions_t::regs == NULL is no longer a reliable indicator that the regions are being read from a file. It is now also necessary to check bcf_sr_regions_t::is_bin, which will be true when the iterator is in use. Note only the regions data accessed via bcf_srs_t::regions is affected by this change. Regions in bcf_srs_t::targets will continue to work the old way, as they are not used for building iterators. Signed-off-by: Rob Davies <rmd+git@sanger.ac.uk>
|
How does this
Interact with the frameshifts plugin here: It explicitly checks start and end for each region. The comment is a bit vague about tracking as accurately as before, but it sounds like this should now fail. I'll explore more in testing. Edit: it's a different interface and is apparently not interacting with the changes, although this is very unclear initially. |
jkbonfield
left a comment
There was a problem hiding this comment.
Mostly minor issues. Typos and queries. Quite a few of my problems with it stem from the original code. I needed to figure out what was going on in order to understand the changes, and that's far from clear due to the opaqueness and lack of adequate commenting.
Comparing against bcftools/plugins/frameshifts.c, and our conversation earlier:
Initially it was unclear why frameshifts isn't bugged given the change in behaviour of bcf_sr_regions_t::start/end. However I believe it's the difference between bcf_sr_next_line vs bcf_sr_regions_overlap. Although it's unclear how bcf_sr_regions_next also differs here. I think it falls into the same camp as bcf_sr_regions_overlap, which is that it doesn't use a multi-region iterator.
It looks like bcf_sr_next_line operates on file(s), while bcf_sr_regions_next operates on region lists instead (agnostic of associated files). The documentation doesn't adequately explain why it's beneficial when processing a single file to add it to an array of incoming synced-readers and iterate that way instead of using the manual approach. It probably should add a comment however stating the benefits of doing multi-region work when using the file based interfaces. (Plus maybe a comment on bcf_sr_regions_overlap as to why it's not using a file, which is a bit confusing given the naming convention.)
Also "bcf_sr_regions_next" claims to return the next interval, while "bcf_sr_regions_overlap" checks a specific location, presumably (but unstated) updating bcf_sr_regions_t::start/end itself if the queried region does infact overlap. This needs documenting, as bcf_sr_regions_next currently does.
That said, if bcf_sr_regions_next isn't updating start/end correctly in some circumstances, then those also need explicitly mentioning in the documentation too. Or rather I suspect it's setting them correctly, but the regions may not be the same ones given in the input file due to region-merging?
| hts_itr_t *bcf_itr_regarray(const hts_idx_t *idx, bcf_hdr_t *hdr, | ||
| char **regarray, unsigned int regcount); | ||
|
|
||
| /// Crate a multi-region iterator from a hts_reglist_t regions list |
| if (i + 1 < files->nreaders) | ||
| memmove(®->multi_curr[i], ®->multi_curr[i + 1], | ||
| (files->nreaders-i-1)*sizeof(*reg->multi_curr)); | ||
| if (had_itr) |
There was a problem hiding this comment.
Any chance we can fix the function declaration to use something other than int i. Looking at the review I couldn't figure out what was happening, because my instinct was this is a loop variable yet we only have seq for that. I later realised i is passed in as an argument!
Something far more descriptive would be useful, especially given it has no documentation or comment above the function. It doesn't need to be long or overly verbose, just not something most commonly associated with trivial local looping.
There was a problem hiding this comment.
It's now reader_idx and some documentation has been added to the header.
| if (copy_reglist) { | ||
| // The iterator takes ownership of the region list, so take | ||
| // a copy if it's going to be used by multiple iterators. | ||
| reglist = malloc(sizeof(*reglist)); | ||
| if (!reglist) | ||
| goto memfail; | ||
| memcpy(reglist, reglist_in, sizeof(*reglist)); | ||
| reglist->intervals = hts_malloc_p(sizeof(*reglist->intervals), | ||
| reglist_in->count); | ||
| if (!reglist->intervals) | ||
| goto memfail; | ||
| memcpy(reglist->intervals, reglist_in->intervals, | ||
| sizeof(*reglist->intervals) * reglist_in->count); | ||
| } else { | ||
| // Use the one passed in | ||
| reglist = reglist_in; | ||
| } |
There was a problem hiding this comment.
This still leaves reglist->reg as a copy to the old pointer. Does it matter if we don't completely copy reglist_in and have a shared pointer?
I believe not, as it's just a cached copy of regions->seq_names[regions->iseq] anyway, but thought I'd double check.
There was a problem hiding this comment.
No, it shouldn't matter. The pointer needs to be valid when calling tbx_itr_regions() / bcf_itr_regions() but after that it isn't used. Ownership of the string pointed to is also expected to be elsewhere.
| ++idx; | ||
| } while ( idx < reg->nregs | ||
| && ( beg > reg->regs[idx].end | ||
| || reg->regs[idx].start > reg->regs[idx].end) ); |
There was a problem hiding this comment.
How is it possible for reg[idx].start > reg[idx].end? This feels like corrupted data, as each region should always have end >= start.
Is this a typo for something else?
Edit: no - it's documented in region1_t as:
hts_pos_t start, end; // records are marked for skipping have start>end
This probably needs a comment as the code simply looks wrong.
There was a problem hiding this comment.
I checked: this is required as a consequence of regions_merge() which can produce redundant regions. It is cheaper to mark as redundant (by making them invalid) then memmove the structures around.
| int32_t i; | ||
| for (i = reg->creg; i < reg->nregs; i++) { | ||
| if (reg->regs[i].start > reg->regs[i].end) | ||
| continue; // Marked to skip by merge_regions |
There was a problem hiding this comment.
I cannot find a function named merge_regions.
After trial and error I found regions_merge instead. This and the other mention need renaming. It's not relating to changes in this PR, but I was attempting to figure out why we checked start > end and discovered these comments in the process.
There was a problem hiding this comment.
Changed to regions_merge(), along with the other instances mentioned.
| if ( pos <= reg->regs[midx].end ) | ||
| break; | ||
| do { // Advance, also avoiding regions marked to skip by merge_regions() | ||
| ++midx; |
There was a problem hiding this comment.
Fix to regions_merge().
| * returns 0 on success, -1 on failure (also sets files->errnum) | ||
| */ | ||
| static int _reader_fill_buffer(bcf_srs_t *files, bcf_sr_t *reader) | ||
| static int _reader_fill_buffer(bcf_srs_t *files, int reader_idx) |
There was a problem hiding this comment.
If we're modifying internal function declarations, then should we also make them C specification compliant by removing the leading underscores?
|
I still need to do some integration testing. The above was simply a review by reading the code, rather than stress testing it. That's up next. I'll put on my evil-user hat! :-) |
|
If we have a bed file that contains no overlap, eg our VCF is chr20 only and our bed file is for chr1 only, then there's a difference, albeit in warning only. Dev prints up nothing, while this PR prints up: This is incorrect, as chr1 is infact in the header contig lines. |
|
I see vcfstats.c uses I thought I managed to get a difference in output, but it was just me forgetting to grep out the header lines. Nonetheless, it is running considerably faster in this PR, implying that the multi-region iterator is in use. So I wonder in what scenarios the start / end are incorrect and whether I'm just getting lucky on this data set. It may be that the field it's accessing is the underlying bed iterator rather than the region iterator. It's all a bit convoluted! |
|
The following is a problematic BED file as start == end, and start is zero based so it should always be less than end. Nonetheless, a zero-sized region should not invalidate the next line, and our handling of this case is a regression. Although If I remove either of the first two lines, then the 3rd line now reports the entry in both tools. It's marginal though, so maybe this is an off-by-one error lurking in there somewhere. |
|
The above bug is fixed by this: The problem is prev_end was accessing a record which had been skipped due to it being zero length. The general skip strategy (start>end, which it will be for zero length data due to the nature of BED) fixes it. |
Signed-off-by: Rob Davies <rmd+git@sanger.ac.uk>
Improve compliance with C norms around reserved identifiers. Signed-off-by: Rob Davies <rmd+git@sanger.ac.uk>
These have always referenced merge_regions, but the function added at the same time was actually called regions_merge. Signed-off-by: Rob Davies <rmd+git@sanger.ac.uk>
When finding the end point of the previous region, it's necessary to skip over ones marked for skipping by regions_merge(). Signed-off-by: Rob Davies <rmd+git@sanger.ac.uk>
By default, hts_itr_regions() prints a warning if a reference in its regions list doesn't exist in the file being iterated over. This is a bit too strict for the synced reader as individual input files may contain a subset of the full list of references (especially in the case where data has been split by chromosome). To prevent needless warnings, the reference id lookups are now done in reader_seek_multi(), with missing ones being silently dropped. When the reference is missing, an iterator is made over HTS_IDX_NONE (i.e. nothing). This ensures all readers get an iterator, which simplifies book-keeping in the data structures tracking which ones are in use. Signed-off-by: Rob Davies <rmd+git@sanger.ac.uk>
|
The bug fix has been included and the "unknown reference" messages have been suppressed. The accessing of |

Adds
bcf_itr_regions()andtbx_itr_regions()interfaces as BCF and tabix equivalents tosam_itr_regions().Uses the new interfaces to add multi-iterator support to the BCF/VCF synced reader. This makes it considerably faster when index jumping over a large number of regions by reducing the amount of work done looking up index entries. Note that this only affects regions supplied via
bcf_sr_set_regions()which works using iterators over the input files. The similarbcf_sr_set_targets()function works as a filter and does not use iterators.The locations supplied to
bcf_sr_set_regions()can either be an in-memory list, or read from a file (which can itself be indexed withtabix). This change should enable multi-iterators for both of these cases.There are a few changes to behaviour as a result of this change, which unfortunately can't be completely hidden from callers due to the visibility of the synced reader structures in the public headers.
bcf_sr_regions_t::startandbcf_sr_regions_t::endfields don't track the location of the current output records as accurately as before because the iterators no longer stop at the end of each listed region.bcf_sr_regions_t::regsmay be non-NULL when reading regions from a tabix indexed file.Additionally this fixes a bug where setting the
BCF_SR_REGIONS_OVERLAPoption to 2 ("true variant overlap") withbcf_sr_set_opt()could result in records being returned out of order. This could happen if the variant in the record occurred after the end of the first region it overlapped, but also hit a later region.Fixes samtools/bcftools#2557