Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement SVConcordance tool #7977

Merged
merged 4 commits into from
Feb 9, 2023
Merged

Implement SVConcordance tool #7977

merged 4 commits into from
Feb 9, 2023

Conversation

mwalker174
Copy link
Contributor

Analogous to GenotypeConcordance, but for SVs.

@codecov
Copy link

codecov bot commented Aug 5, 2022

Codecov Report

Merging #7977 (27cf6ef) into master (d1a4f94) will decrease coverage by 0.006%.
The diff coverage is 92.234%.

Additional details and impacted files
@@               Coverage Diff               @@
##              master     #7977       +/-   ##
===============================================
- Coverage     86.658%   86.652%   -0.006%     
- Complexity     39024     39154      +130     
===============================================
  Files           2337      2346        +9     
  Lines         182961    183608      +647     
  Branches       20079     20147       +68     
===============================================
+ Hits          158550    159100      +550     
- Misses         17365     17447       +82     
- Partials        7046      7061       +15     
Impacted Files Coverage Δ
...roadinstitute/hellbender/tools/sv/SVLocatable.java 25.000% <ø> (ø)
...titute/hellbender/utils/GenotypeUtilsUnitTest.java 94.624% <ø> (ø)
...ellbender/tools/sv/cluster/CanonicalSVLinkage.java 83.146% <62.963%> (-6.043%) ⬇️
...ender/utils/variant/GATKSVVariantContextUtils.java 93.939% <66.667%> (-2.835%) ⬇️
...stitute/hellbender/tools/sv/SVCallRecordUtils.java 79.167% <73.810%> (-4.518%) ⬇️
...der/tools/walkers/sv/SVClusterIntegrationTest.java 98.974% <77.778%> (-0.522%) ⬇️
...oadinstitute/hellbender/tools/sv/SVCallRecord.java 84.348% <79.167%> (+2.144%) ⬆️
...r/tools/sv/concordance/SVConcordanceAnnotator.java 83.465% <83.465%> (ø)
...roadinstitute/hellbender/tools/sv/SVTestUtils.java 90.274% <84.286%> (-6.426%) ⬇️
...lbender/tools/sv/cluster/CanonicalSVCollapser.java 80.635% <86.508%> (-6.836%) ⬇️
... and 30 more

Copy link
Contributor

@TedBrookings TedBrookings left a comment

Choose a reason for hiding this comment

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

This was a challenging review for me: there's lots of detailed logic (if this kind of SV with this property, then do X) that is difficult for me to check. I did try to follow it all though, and I didn't notice anything wrong. In a few places, I indicated that it would be ideal to add comments explaining the code logic. I also found a few places where the code could be altered to achieve better efficiency / conform more to THE JAVA WAY (as I understand it). I did not see anything that absolutely needs to be changed, so I am approving.

Comment on lines 56 to 59
/**
* Get allele frequencies (AF). Avoid calling this repeatedly on the same object, as computations are performed
* on the fly.
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems like if you wanted to prevent these frequencies from being recalculated, you could make them an assigned class member and make the routine to compute them a private function called by the constructor (and then have a public getter).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have no idea why I used a factory here… I've changed this to a simple class like you suggested.

@@ -215,7 +222,9 @@ private static Pair<Boolean, Boolean> inferStrands(final StructuralVariantType t
return Pair.of(Boolean.FALSE, Boolean.TRUE);
} else {
if (type.equals(StructuralVariantType.INV)) {
Utils.validateArg(inputStrandA != null && inputStrandB != null, "Cannot create variant of type " + type + " with null strands");
if (inputStrandA == null && inputStrandB == null) {
return Pair.of(Boolean.TRUE, Boolean.TRUE);
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe add a comment explaining why it's okay to set the strands to True if they are input as null?

Copy link
Contributor Author

@mwalker174 mwalker174 Dec 19, 2022

Choose a reason for hiding this comment

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

Thanks for catching this. I did some digging and I actually found our cleaned vcfs don't have STRANDS for INV types and I think this is fine. We do keep them there in intermediate vcfs since manta reports them that way, but we only retain ones with all 4 breakends (see manta manual about that). I've modified the check to allow null.

* Asserts presence of {@link GATKSVVCFConstants#EXPECTED_COPY_NUMBER_FORMAT} and
* {@link GATKSVVCFConstants#COPY_NUMBER_FORMAT} attributes.
*/
public static void assertHasCopyStateFields(final Genotype g) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like the word should be written out. In complicated SV code every little bit helps.

Suggested change
public static void assertHasCopyStateFields(final Genotype g) {
public static void assertHasCopyStateFields(final Genotype genotype) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

return record;
}
final ArrayList<Genotype> newGenotypes = new ArrayList<>(record.getGenotypes().size());
for (final Genotype g : record.getGenotypes()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same deal, g -> genotype

@@ -312,7 +314,7 @@ protected List<Allele> collapseSampleGenotypeAlleles(final Collection<Genotype>
final List<Allele> alleles;
if (altAlleles.contains(Allele.SV_SIMPLE_DEL) || altAlleles.contains(Allele.SV_SIMPLE_DUP)) {
// For CNVs, collapse genotype using copy number
alleles = getCNVGenotypeAllelesFromCopyNumber(altAlleles, refAllele, expectedCopyNumber, copyNumber);
alleles = getCNVGenotypeAllelesFromCopyNumber(altAlleles, refAllele, expectedCopyNumber, copyNumber, true);
Copy link
Contributor

Choose a reason for hiding this comment

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

Since sometimes you assume biallelic DUPs and sometimes you don't, it might be nice to include a brief comment explaining the choice in each location.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Biallelic dups are now assumed everywhere for simple DUP types

for (final String sample : samples) {
final Genotype genotypeA = genotypesA.get(sample);
final Genotype genotypeB = genotypesB.get(sample);
final int cnA = genotypeA == null ?
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe add a quick comment here explaining why genotypeB having ECN == CN counts as a match if genotypeA is null? I'm guessing it's because null Genotype means that not CNV was found for that sample, but ECN=CN means that it was explicitly called REF in the other?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes if a sample doesn't exist in one of the records, this assumes it's copy-neutral. I've added a comment to clarify.

/**
* Efficiently clusters a set of evaluation ("eval") SVs with their closest truth SVs.
*
* "Closest" is defined in the {@link #getClosestItem} method an selects first on total breakpoint distance
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* "Closest" is defined in the {@link #getClosestItem} method an selects first on total breakpoint distance
* "Closest" is defined in the {@link #getClosestItem} method and selects first on total breakpoint distance

Comment on lines 117 to 121
final List<Long> finalizedRefItems = truthIdToItemMap.entrySet().stream()
.filter(e -> linkage.getMaxClusterableStartingPosition(e.getValue()) < lastItemStart)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
finalizedRefItems.forEach(truthIdToItemMap::remove);
Copy link
Contributor

Choose a reason for hiding this comment

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

You can avoid creating the list and doing a second iteration. Changes to values() are backed by the set, and removeIf does exactly what you want.
https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#values--
https://docs.oracle.com/javase/8/docs/api/java/util/Collection.html#removeIf-java.util.function.Predicate-

Suggested change
final List<Long> finalizedRefItems = truthIdToItemMap.entrySet().stream()
.filter(e -> linkage.getMaxClusterableStartingPosition(e.getValue()) < lastItemStart)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
finalizedRefItems.forEach(truthIdToItemMap::remove);
truthIdToItemMap.values()
.removeIf(cluster -> linkage.getMaxClusterableStartingPosition(cluster) < lastItemStart);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice! Never knew that values() could be modified like that

Comment on lines 160 to 182
final List<Map.Entry<Long, SVCallRecord>> linkedItems = candidates.stream()
.filter(other -> linkage.areClusterable(evalRecord, other.getValue()))
.collect(Collectors.toList());
final int[] distances = linkedItems.stream()
.mapToInt(e -> totalDistance(evalRecord, e.getValue()))
.toArray();
if (distances.length == 0) {
return null;
}
final int minDistance = MathUtils.arrayMin(distances);
int numMin = 0;
int minDistIndex = 0;
for (int i = 0; i < distances.length; i++) {
if (distances[i] == minDistance) {
numMin++;
minDistIndex = i;
}
}
if (numMin == 1) {
return linkedItems.get(minDistIndex);
} else {
return getClosestItemWithTiebreakers(evalRecord, linkedItems);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if performance / etc is worth the time of rewriting, but you can construct a Comparator that breaks ties by chaining with .thenComparing(). You could then have a single stream terminated with min() acting on that Comparator. This way you could have arbitrary comparisons without passing around intermediate lists that get winnowed down. If the comparator was constructed from a function (with input evalRecord), it could be used in update() below as well, decreasing the potential for bugs if the two code paths get out of sync.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep that's a much better way to do it. That cut down the code a lot

} else if (g.isMixed()) {
return GenotypeConcordanceStates.CallState.IS_MIXED;
} else {
throw new IllegalArgumentException("Could not determine truth state for genotype: " + g);
Copy link
Contributor

Choose a reason for hiding this comment

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

Presumably a typo. Although I wonder if you could combine getTruthState and getEvalState by passing a boolean allowNull

Suggested change
throw new IllegalArgumentException("Could not determine truth state for genotype: " + g);
throw new IllegalArgumentException("Could not determine eval state for genotype: " + g);

@mwalker174 mwalker174 merged commit d3b2c5c into master Feb 9, 2023
@mwalker174 mwalker174 deleted the mw_sv_concordance branch February 9, 2023 03:15
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