vg surject : --diploid-map and other code changes - #5026
Conversation
Make sure tail annotations come through GAF for new surject features
Updated the surjection MAPQ explanation and removed outdated details about target-path selection and calibration experiments.
Clarify the preset mismatch in diploid Illumina input and suggest a controlled follow-up for rerunning the diploid candidate.
adamnovak
left a comment
There was a problem hiding this comment.
I've looked at the overall design here and the backing abstractions, and made some comments about them. I found some things about how this is written out that I would want to change (I don't think we want to merge the big Markdown reports, for example, and some things might want renaming).
I haven't really dug much into the core algorithms; I think they really need to be constrained by tests. If they give the right answers for weird edge cases I will be more confident in the core algorithms.
There was a problem hiding this comment.
This report and diploid-map-surjection.md are both very thorough, and could help people understand this PR, but I don't think they make a lot of sense to carry in the vg repository, at least without organizing things so it's clear that these are reports about a particular project that was done at a particular time.
Some of the material in these would be useful on the vg wiki, because it covers how vg suject's diploid mode now works, and what the new tags mean.
But a lot of this is about how the changeset in particular was tested, and what particular changes to surjection are part of this project, and there are a lot of particular accuracy statistics and references to particular experiments you or I did at some point. That is all of limited utility to future readers of the vg repository, who will only care about how everything works at that future point, and would be better placed in a manuscript about your project.
| << " -d, --diploid-map NAME like --into-ref, but also turns on --multimap and" << endl | ||
| << " emits hp/hq/aq tags for haplotype-aware" << endl | ||
| << " diploid surjection" << endl |
There was a problem hiding this comment.
--into-ref is described as just surject into this reference assembly, so we don't save a lot of space by referring to it, whereas it makes the user need to find another option later in the list to understand this one.
It's not quite clear from this if "haplotype-aware diploid surjection" is the name of what this turns on, or some downstream process that consumes the tags, or some other thing that can be flagged on separately and, if turned on, will emit those tags when this flag is used.
| total_reads_surjected += 2; | ||
| }; | ||
|
|
||
| auto process_fragment = [&](PairedSurjector::fragment_group_t& placements) { |
There was a problem hiding this comment.
This then gets passed to e.g. vg::io::gam_paired_grouped_for_each_parallel_after_wait, which doesn't know about/depend on PairedSurjector::fragment_group_t and always wants to give you a vector of pairs of Alignments. That makes it seem like there might be some confusion around exactly what piece of code owns the idea of a fragment_group_t, in case we wanted to someday change it to a real struct or something.
I would describe the things that need to attach to libvgio in the same terms that libvgio does, and if that's too complicated and we need some abstractions for the types, the definitions should live in libvgio if we can get away with it.
| } | ||
| } | ||
| } | ||
| } else if (input_format == "GAMP") { |
There was a problem hiding this comment.
What if someone provides --diploid-map with GAMP input? It doesn't look like we document that it won't work, or like we produce an error when asked to do that, but it also doesn't look like it actually activates diploid mode. So a user can think that they've run diploid surjection when they haven't.
| all_surjected.reserve(placements.size() * 2); | ||
|
|
||
| for (auto& placement : placements) { | ||
| if (validate) { | ||
| ensure_alignment_is_for_graph(logger, placement, *xgidx); | ||
| } | ||
| set_metadata(placement); | ||
|
|
||
| // Diploid mode retains surjections to all overlapping haplotype paths. | ||
| vector<Alignment> surjected = surjector.surject(placement, paths, subpath_global, spliced); | ||
|
|
||
| // Compute hp/hq within this graph placement. | ||
| surjector.annotate_hap_tags(placement, surjected); | ||
|
|
||
| all_surjected.insert(all_surjected.end(), | ||
| std::make_move_iterator(surjected.begin()), | ||
| std::make_move_iterator(surjected.end())); | ||
| } | ||
|
|
||
| // Compute GlobalQ and select one primary across all graph placements. | ||
| surjector.annotate_global_mapq_and_primary(placements.front(), all_surjected); |
There was a problem hiding this comment.
I think most of this (maybe except for the ensure_alignment_is_for_graph() and/or set_metadata() calls) might want to be a method on Surjector (maybe an overload or variant of surject()?) because you need all these pieces to accomplish the operation of taking a collection of graph placements and producing the corresponding collection of linear placements. This is a piece we might want to be able to write a unit test for.
| // Processing can parallelize once fragment-length learning finishes or is abandoned. | ||
| bool PairedSurjector::ready_for_parallel() const { | ||
| return fragment_length_distribution->is_finalized() || learning_abandoned; | ||
| } | ||
|
|
||
| double PairedSurjector::fragment_length_mean() const { | ||
| return fragment_length_distribution->mean(); | ||
| } | ||
|
|
||
| double PairedSurjector::fragment_length_stddev() const { | ||
| return fragment_length_distribution->std_dev(); | ||
| } | ||
|
|
||
| size_t PairedSurjector::fragment_length_sample_size() const { | ||
| return fragment_length_distribution->curr_sample_size(); | ||
| } | ||
|
|
||
| // Finalize the current fragment-length estimate if usable; otherwise disable fragment scoring. | ||
| void PairedSurjector::finalize_learning() { | ||
| if (ready_for_parallel()) { | ||
| fragment_scoring_available = fragment_length_distribution->is_finalized(); | ||
| return; | ||
| } | ||
| if (fragment_length_distribution->curr_sample_size() >= minimum_samples_for_estimate | ||
| && isfinite(fragment_length_distribution->mean()) | ||
| && isfinite(fragment_length_distribution->std_dev()) | ||
| && fragment_length_distribution->std_dev() > 0.0) { | ||
| fragment_length_distribution->force_parameters( | ||
| fragment_length_distribution->mean(), fragment_length_distribution->std_dev()); | ||
| fragment_scoring_available = true; | ||
| } | ||
| else { | ||
| learning_abandoned = true; | ||
| fragment_scoring_available = false; | ||
| } | ||
| } |
There was a problem hiding this comment.
I'm wondering if there needs to be more to FragmentLengthDistribution so that this doesn't look so much like
Lines 538 to 549 in 3a37aa3
Why do we need to both have the FragmentLengthDistribution around and also wrap and expose all these methods? Does the abandonment feature need to be pushed down into FragmentLengthDistribution?
Maybe we should be inheriting from PairedEndMapper (which might need to be renamed to encompass other kinds of paired-end read processing)?
Lines 144 to 168 in 3a37aa3
| return llabs(static_cast<int64_t>(lengths.first)); | ||
| } | ||
|
|
||
| // Select the highest-scoring candidate, shuffling ties deterministically. |
There was a problem hiding this comment.
These comments maybe want to be in the header?
| bool can_train = winning_placement.source_first.mapping_quality() == 60 | ||
| && winning_placement.source_second.mapping_quality() == 60 |
There was a problem hiding this comment.
Should we accept input from other mappers with MAPQs higher than 60?
| } | ||
|
|
||
| fragment_scoring_available = fragment_length_distribution->is_finalized(); | ||
| vector<placement_t> work; |
There was a problem hiding this comment.
If this is some kind of scratch that we use through the whole function, it might make sense to explain how we make sure that it has the values we need it to have at any given point, or what it's supposed to look like after every pass through it.
| // Attach supplementary alignments to the best available mate on the same path. | ||
| for (auto& aln : placement.first) { | ||
| if (is_supplementary(aln) && !has_annotation(aln, "mate_info")) { | ||
| const Alignment* mate = nullptr; | ||
| if (aln.refpos_size()) { | ||
| auto found = best_per_path.find(aln.refpos(0).name()); | ||
| if (found != best_per_path.end()) { | ||
| mate = &placement.second[placement.candidates[found->second].second]; | ||
| } | ||
| } | ||
| set_annotation(aln, "mate_info", | ||
| mate | ||
| ? mate_info(mate->refpos(0).name(), mate->refpos(0).offset(), | ||
| mate->refpos(0).is_reverse(), false) | ||
| : mate_info("", -1, false, false)); | ||
| } | ||
| } |
There was a problem hiding this comment.
I don't really understand this. I think it was like this before, but don't we need to figure out which alignment of this read the supplementary alignment supplements, and then figure out which alignment of the pair partner is actually paired with it?
Otherwise we might have a pair of primary non-supplementary alignments at the start of a chromosome, and a pair of secondary non-supplementary alignments at the end, and then all the supplementary alignments saying they belong to the pair at the start of the chromosome even if they themselves appear at the end, right?
Purpose
This draft combines the complete diploid map surjection work into one branch for review.
This has not been rebased over #5025 yet. The paired-surjection implementation overlaps with that PR, and will be changed after reviewing by @adamnovak.
Main changes
vg surject --diploid-map.hp,hq,GlobalQ, andaqannotations.supplementary.
Dependencies
This PR depends on two open
libvgioPRs: