Skip to content

Commit d60ee3d

Browse files
committed
Add max_assembly_sequences circuit breaker to iterative_overlap_assembly
When the number of variant sequences after substring collapse exceeds max_assembly_sequences (default 1000), skip the O(n^2)-per-round greedy merge and return sequences sorted by read support. This prevents silent hangs on high-coverage loci. Three new tests: breaker triggers above threshold, disabled with None, normal merge below threshold. Fixes #151
1 parent ee04e76 commit d60ee3d

3 files changed

Lines changed: 87 additions & 9 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.12"
13+
__version__ = "1.4.13"
1414

1515

1616
from .allele_read import AlleleRead

isovar/assembly.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,23 +130,34 @@ def collapse_substrings(variant_sequences):
130130
]
131131

132132

133+
DEFAULT_MAX_ASSEMBLY_SEQUENCES = 1000
134+
135+
133136
def iterative_overlap_assembly(
134137
variant_sequences,
135-
min_overlap_size=MIN_VARIANT_SEQUENCE_ASSEMBLY_OVERLAP_SIZE):
138+
min_overlap_size=MIN_VARIANT_SEQUENCE_ASSEMBLY_OVERLAP_SIZE,
139+
max_assembly_sequences=DEFAULT_MAX_ASSEMBLY_SEQUENCES):
136140
"""
137141
Assembles longer sequences from reads centered on a variant by
138-
between merging all pairs of overlapping sequences and collapsing
142+
merging all pairs of overlapping sequences and collapsing
139143
shorter sequences onto every longer sequence which contains them.
140144
145+
Parameters
146+
----------
147+
variant_sequences : list of VariantSequence
148+
149+
min_overlap_size : int
150+
151+
max_assembly_sequences : int or None
152+
If the number of input sequences exceeds this threshold after
153+
substring collapse, skip the O(n^2)-per-round greedy merge and
154+
return sequences sorted by read support. Set to None to disable.
155+
141156
Returns a list of variant sequences, sorted by decreasing read support.
142157
"""
143158
if len(variant_sequences) <= 1:
144-
# if we don't have at least two sequences to start with then
145-
# skip the whole mess below
146159
return variant_sequences
147160

148-
# reduce the number of inputs to the merge algorithm by first collapsing
149-
# shorter sequences onto the longer sequences which contain them
150161
n_before_collapse = len(variant_sequences)
151162
variant_sequences = collapse_substrings(variant_sequences)
152163
n_after_collapse = len(variant_sequences)
@@ -155,7 +166,16 @@ def iterative_overlap_assembly(
155166
n_before_collapse,
156167
n_after_collapse)
157168

158-
merged_variant_sequences = greedy_merge(variant_sequences, min_overlap_size)
169+
if (max_assembly_sequences is not None
170+
and n_after_collapse > max_assembly_sequences):
171+
logger.warning(
172+
"Too many variant sequences (%d > %d) for greedy assembly; "
173+
"skipping merge and returning sequences sorted by read support",
174+
n_after_collapse,
175+
max_assembly_sequences)
176+
else:
177+
variant_sequences = greedy_merge(variant_sequences, min_overlap_size)
178+
159179
return list(sorted(
160-
merged_variant_sequences,
180+
variant_sequences,
161181
key=lambda seq: -len(seq.reads)))

tests/test_assembly.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,3 +422,61 @@ def test_iterative_assembly_deterministic_across_input_orders():
422422
result_sequences = sorted(r.sequence for r in result)
423423
assert result_sequences == reference_sequences, \
424424
"Trial %d: different assembly from shuffled input" % trial
425+
426+
427+
def test_assembly_circuit_breaker_skips_merge():
428+
"""
429+
When the number of sequences after collapse exceeds
430+
max_assembly_sequences, the greedy merge is skipped and sequences
431+
are returned sorted by read support.
432+
"""
433+
# Use distinct prefixes so collapse_substrings can't fold them
434+
prefixes = ["GAT", "CTA", "AGC", "TCA", "GCG"]
435+
variant_sequences = [
436+
VariantSequence(
437+
prefix=p, alt="X", suffix="TTT",
438+
reads={"%s_%d" % (p, k) for k in range(i + 1)})
439+
for i, p in enumerate(prefixes)
440+
]
441+
442+
result = iterative_overlap_assembly(
443+
variant_sequences,
444+
min_overlap_size=1,
445+
max_assembly_sequences=3)
446+
447+
eq_(len(result), 5, "All 5 sequences should be returned (no merge)")
448+
read_counts = [len(r.reads) for r in result]
449+
assert read_counts == sorted(read_counts, reverse=True), \
450+
"Results should be sorted by decreasing read count, got %s" % read_counts
451+
452+
453+
def test_assembly_circuit_breaker_disabled_with_none():
454+
"""max_assembly_sequences=None disables the circuit breaker."""
455+
variant_sequences = [
456+
VariantSequence(prefix="A" * 5, alt="CC", suffix="T" * 5, reads={"r1"}),
457+
VariantSequence(prefix="A" * 5, alt="CC", suffix="T" * 5 + "G", reads={"r2"}),
458+
]
459+
result = iterative_overlap_assembly(
460+
variant_sequences,
461+
min_overlap_size=1,
462+
max_assembly_sequences=None)
463+
# Should merge normally despite any threshold
464+
eq_(len(result), 1)
465+
466+
467+
def test_assembly_circuit_breaker_not_triggered_under_threshold():
468+
"""
469+
When the sequence count is under the threshold, greedy merge proceeds
470+
normally (same as no circuit breaker).
471+
"""
472+
variant_sequences = [
473+
VariantSequence(prefix="AAAA", alt="CC", suffix="TTTT", reads={"r1"}),
474+
VariantSequence(prefix="AA", alt="CC", suffix="TTTTGG", reads={"r2"}),
475+
]
476+
result_with_breaker = iterative_overlap_assembly(
477+
variant_sequences, min_overlap_size=1, max_assembly_sequences=100)
478+
result_without_breaker = iterative_overlap_assembly(
479+
variant_sequences, min_overlap_size=1, max_assembly_sequences=None)
480+
eq_(len(result_with_breaker), len(result_without_breaker))
481+
for r1, r2 in zip(result_with_breaker, result_without_breaker):
482+
eq_(r1.sequence, r2.sequence)

0 commit comments

Comments
 (0)