"""Adapter-dimer / short-insert read-through — the Illumina 10x 3' failure. Both sub-types lead with the TSO (the hallmark), built from the spec's constants: readthrough (insert 1-30): TSO + insert + poly(A) + revcomp(UMI) + revcomp(CB) + revcomp(R1 primer) + revcomp(P5), fit to the read length. At ~90 nt this fits by truncation -> adapter read-through (the AGATCGGAAGAGC stem) and reverse-complemented barcode are visible. pure_dimer (insert 0): TSO + short poly(A) + poly-G no-signal tail to the read length — the classic empty TSO<->poly-dT product on two-color instruments. revcomp(CB)/revcomp(UMI) use *this pair's* barcode/UMI (from R1), never random. Synthesized bases get a spuriously-high quality so a naive quality filter cannot flag them. """ from __future__ import annotations from seqcolyte.dna import homopolymer, revcomp from sim.base import FailureMode, R2Result, ReadCtx, draw_uniform_int, fit_to_length, synth_quality from sim.registry import register _TSO = "oligo_template_switching_oligo_tso" _R1_READINTO = "oligo_r1_readinto_adapter" _P5_RC = "oligo_p5_rc" @register class AdapterDimer(FailureMode): name = "adapter_dimer" platform = "illumina" def build_r2(self, ctx: ReadCtx) -> R2Result: spec, rng, params = ctx.spec, ctx.rng, ctx.params qcfg = params.get("quality", {}) phred = int(qcfg.get("phred", 37)) pad_base = spec.platform_params.get("dark_base") or "G" polya_base = spec.platform_params.get("polyA_base") or "A" tso = spec.oligo_sequence(_TSO) polya_len = draw_uniform_int(rng, params.get("polyA_len", {"min": 5, "max": 20})) parts: list[tuple[str, str]] = [(tso, synth_quality(len(tso), phred))] if ctx.subtype == "pure_dimer": insert_len = 0 parts.append((homopolymer(polya_base, polya_len), synth_quality(polya_len, phred))) recipe = f"TSO|polyA({polya_len})" else: # readthrough insert_len = draw_uniform_int(rng, params.get("readthrough_insert_len", {"min": 0, "max": 30})) insert = ctx.r2.sequence[:insert_len] insert_qual = ( ctx.r2.quality[:insert_len] if qcfg.get("overlay_insert", True) else synth_quality(insert_len, phred) ) r1_readinto = spec.oligo_sequence(_R1_READINTO) p5_rc = spec.oligo_sequence(_P5_RC) parts += [ (insert, insert_qual), (homopolymer(polya_base, polya_len), synth_quality(polya_len, phred)), (revcomp(ctx.umi), synth_quality(len(ctx.umi), phred)), (revcomp(ctx.cb), synth_quality(len(ctx.cb), phred)), (r1_readinto, synth_quality(len(r1_readinto), phred)), (p5_rc, synth_quality(len(p5_rc), phred)), ] recipe = f"TSO|insert({insert_len})|polyA({polya_len})|rc_UMI|rc_CB|rc_R1primer|rc_P5" seq = "".join(s for s, _ in parts) qual = "".join(q for _, q in parts) seq, qual, pad_len, truncated = fit_to_length(seq, qual, ctx.r2_len, pad_base, phred) construct = f"{recipe}|padG({pad_len})" if pad_len else (f"{recipe}|trunc" if truncated else recipe) return R2Result( sequence=seq, quality=qual, construct=construct, fields={"insert_len": insert_len, "polyA_len": polya_len, "pad_len": pad_len, "truncated": truncated}, )