Skip to content

vg surject : --diploid-map and other code changes - #5026

Draft
Sagorikanag wants to merge 17 commits into
vgteam:masterfrom
Sagorikanag:surject-follow-up-fixes
Draft

vg surject : --diploid-map and other code changes#5026
Sagorikanag wants to merge 17 commits into
vgteam:masterfrom
Sagorikanag:surject-follow-up-fixes

Conversation

@Sagorikanag

Copy link
Copy Markdown

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

  • Add vg surject --diploid-map.
  • Compare alternative graph placements jointly.
  • Select haplotype-local and global primary surjections.
  • Add hp, hq, GlobalQ, and aq annotations.
  • Score paired surjections using alignment and fragment-length evidence.
  • Learn the fragment-length distribution before enabling parallel processing.
  • Handle paired reads with no compatible path/strand placement.
  • Prune mapper-declared tail-region anchors.
  • Classify overlapping alternatives as secondary and disjoint alternatives as
    supplementary.
  • Rank paths by their best alignment.
  • Realign anchors containing substitutions.
  • Validate anchor sliding on target paths.
  • Preserve tail annotations through GAF.
  • Add implementation and evaluation documentation.

  • Dependencies

This PR depends on two open libvgio PRs:

@adamnovak adamnovak left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +43 to +45
<< " -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +915 to 918
}
}
}
} else if (input_format == "GAMP") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +858 to +878
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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/surjector.cpp
Comment on lines +342 to +377
// 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;
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if there needs to be more to FragmentLengthDistribution so that this doesn't look so much like

vg/src/minimizer_mapper.hpp

Lines 538 to 549 in 3a37aa3

bool fragment_distr_is_finalized () {return fragment_length_distr.is_finalized();}
void finalize_fragment_length_distr() {
if (!fragment_length_distr.is_finalized()) {
fragment_length_distr.force_parameters(fragment_length_distr.mean(), fragment_length_distr.std_dev());
}
}
void force_fragment_length_distr(double mean, double stdev) {
fragment_length_distr.force_parameters(mean, stdev);
}
double get_fragment_length_mean() const { return fragment_length_distr.mean(); }
double get_fragment_length_stdev() const {return fragment_length_distr.std_dev(); }
size_t get_fragment_length_sample_size() const { return fragment_length_distr.curr_sample_size(); }

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)?

vg/src/mapper.hpp

Lines 144 to 168 in 3a37aa3

/**
* A class trait/mixin which defines a mapper's paired end distribution support.
*
* Doesn't actually define the paired-end mapping methods.
*/
class PairedEndMapper {
public:
/// Set parameters for estimating fragment length distribution.
/// TODO: setting alignment threads after this could mess up the internal memory for how many threads to reset to
void set_fragment_length_distr_params(size_t maximum_sample_size = 1000, size_t reestimation_frequency = 1000,
double robust_estimation_fraction = 0.95);
/// Returns true if fragment length distribution has been fixed
bool has_fixed_fragment_length_distr();
/// Use the given fragment length distribution parameters instead of
/// estimating them.
void force_fragment_length_distr(double mean, double stddev);
protected:
/// Holds the actual fragment length distribution and estimation information
FragmentLengthDistribution fragment_length_distr;
};

Comment thread src/surjector.cpp
return llabs(static_cast<int64_t>(lengths.first));
}

// Select the highest-scoring candidate, shuffling ties deterministically.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These comments maybe want to be in the header?

Comment thread src/surjector.cpp
Comment on lines +550 to +551
bool can_train = winning_placement.source_first.mapping_quality() == 60
&& winning_placement.source_second.mapping_quality() == 60

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we accept input from other mappers with MAPQs higher than 60?

Comment thread src/surjector.cpp
}

fragment_scoring_available = fragment_length_distribution->is_finalized();
vector<placement_t> work;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/surjector.cpp
Comment on lines +645 to +661
// 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));
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants