-
Notifications
You must be signed in to change notification settings - Fork 596
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
Conversation
Codecov Report
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
|
There was a problem hiding this 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.
/** | ||
* Get allele frequencies (AF). Avoid calling this repeatedly on the same object, as computations are performed | ||
* on the fly. | ||
*/ |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
public static void assertHasCopyStateFields(final Genotype g) { | |
public static void assertHasCopyStateFields(final Genotype genotype) { |
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ? |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* "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 |
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); |
There was a problem hiding this comment.
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-
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); |
There was a problem hiding this comment.
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
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); | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
throw new IllegalArgumentException("Could not determine truth state for genotype: " + g); | |
throw new IllegalArgumentException("Could not determine eval state for genotype: " + g); |
c0241d1
to
c6626ce
Compare
eaacae4
to
69ec8ad
Compare
Analogous to GenotypeConcordance, but for SVs.