Skip to content

Commit d6998d9

Browse files
committed
Handle mixed alleles in get_single_allele_from_reads instead of crashing
Previously raised ValueError if reads had different alleles. Now uses the most common allele and filters to only matching reads, with a warning. Return signature changed to (allele, filtered_reads) tuple. This handles edge cases from indel representation ambiguity or subclonal mixtures without crashing. Callers using the old single-return-value signature are updated (variant_sequence_creator.py). Fixes #152
1 parent ba1c9c0 commit d6998d9

4 files changed

Lines changed: 61 additions & 11 deletions

File tree

isovar/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# See the License for the specific language governing permissions and
1111
# limitations under the License.
1212

13-
__version__ = "1.4.13"
13+
__version__ = "1.4.14"
1414

1515

1616
from .allele_read import AlleleRead

isovar/allele_read_helpers.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
AlleleRead objects.
1616
"""
1717

18-
from collections import defaultdict
18+
from collections import Counter, defaultdict
1919

2020
from .common import groupby
2121
from .logging import get_logger
@@ -34,19 +34,38 @@ def group_reads_by_allele(allele_reads):
3434

3535
def get_single_allele_from_reads(allele_reads):
3636
"""
37-
Given a sequence of AlleleRead objects, which are expected to all have
38-
the same allele, return that allele.
37+
Given a sequence of AlleleRead objects which are expected to all have
38+
the same allele, return that allele and the reads supporting it.
39+
40+
If reads carry different alleles (e.g. from indel representation
41+
ambiguity or subclonal mixtures), the most common allele is returned
42+
along with only the reads that match it. A warning is logged about
43+
the discarded minority alleles.
44+
45+
Returns (allele_string, list_of_matching_reads)
3946
"""
4047
allele_reads = list(allele_reads)
4148

4249
if len(allele_reads) == 0:
4350
raise ValueError("Expected non-empty list of AlleleRead objects")
4451

45-
seq = allele_reads[0].allele
46-
if any(read.allele != seq for read in allele_reads):
47-
raise ValueError("Expected all AlleleRead objects to have same allele '%s', got %s" % (
48-
seq, allele_reads))
49-
return seq
52+
allele_counts = Counter(read.allele for read in allele_reads)
53+
most_common_allele = allele_counts.most_common(1)[0][0]
54+
55+
if len(allele_counts) > 1:
56+
logger.warning(
57+
"Expected all reads to have same allele but found %d distinct "
58+
"alleles: %s. Using most common allele '%s' (%d/%d reads).",
59+
len(allele_counts),
60+
dict(allele_counts),
61+
most_common_allele,
62+
allele_counts[most_common_allele],
63+
len(allele_reads))
64+
allele_reads = [
65+
r for r in allele_reads if r.allele == most_common_allele
66+
]
67+
68+
return most_common_allele, allele_reads
5069

5170

5271
def group_unique_sequences(

isovar/variant_sequence_creator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def reads_to_variant_sequences(
8989
if len(variant_reads) == 0:
9090
return []
9191

92-
alt_seq = get_single_allele_from_reads(variant_reads)
92+
alt_seq, variant_reads = get_single_allele_from_reads(variant_reads)
9393

9494
# the number of context nucleotides on either side of the variant
9595
# is half the desired length (minus the number of variant nucleotides)

tests/test_read_helpers.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
# limitations under the License.
1212

1313
from isovar import ReadCollector
14-
from isovar.allele_read_helpers import group_unique_sequences
14+
from isovar.allele_read import AlleleRead
15+
from isovar.allele_read_helpers import group_unique_sequences, get_single_allele_from_reads
1516

1617
from varcode import Variant
1718

19+
from .common import eq_
1820
from .testing_helpers import load_bam
1921

2022

@@ -45,3 +47,32 @@ def test_group_unique_sequences():
4547
# there are some redundant reads, so we expect that the number of
4648
# unique entries should be less than the total read partitions
4749
assert len(variant_reads) > len(groups)
50+
51+
52+
def test_get_single_allele_from_reads_uniform():
53+
reads = [
54+
AlleleRead(prefix="AA", allele="G", suffix="TT", name="r1"),
55+
AlleleRead(prefix="AA", allele="G", suffix="TT", name="r2"),
56+
]
57+
allele, filtered_reads = get_single_allele_from_reads(reads)
58+
eq_(allele, "G")
59+
eq_(len(filtered_reads), 2)
60+
61+
62+
def test_get_single_allele_from_reads_mixed_uses_most_common():
63+
reads = [
64+
AlleleRead(prefix="AA", allele="G", suffix="TT", name="r1"),
65+
AlleleRead(prefix="AA", allele="G", suffix="TT", name="r2"),
66+
AlleleRead(prefix="AA", allele="G", suffix="TT", name="r3"),
67+
AlleleRead(prefix="AA", allele="C", suffix="TT", name="r4"),
68+
]
69+
allele, filtered_reads = get_single_allele_from_reads(reads)
70+
eq_(allele, "G")
71+
eq_(len(filtered_reads), 3)
72+
assert all(r.allele == "G" for r in filtered_reads)
73+
74+
75+
def test_get_single_allele_from_reads_empty_raises():
76+
import pytest
77+
with pytest.raises(ValueError):
78+
get_single_allele_from_reads([])

0 commit comments

Comments
 (0)