offtargeteffect's picture
Add liability/QC, cluster & tree, and experiment tracking
bdd3f19 verified
Raw
History Blame Contribute Delete
5.53 kB
"""
Sequence-liability motif scanning for mRNA constructs.
Scans the functional regions of an mRNA for short sequence motifs that are
known to compromise expression, stability, or processing:
- **uORF start (upstream AUG)** in the 5'UTR β€” can initiate an upstream
open reading frame and reduce translation of the main CDS.
- **Premature polyadenylation signal** (AAUAAA / AUUAAA) inside the CDS β€”
can cause truncated transcripts.
- **AU-rich element (ARE, AUUUA)** in the 3'UTR β€” recruits decay machinery
and shortens mRNA half-life.
- **Cryptic 5' splice-donor consensus** (GT[AG]AGT) anywhere β€” risk of
aberrant splicing when transcribed from a DNA template.
All scanning is done on DNA alphabet (T, not U); inputs are normalised.
Severities are plain strings ("critical" / "warning" / "info") so this module
stays free of any dependency on the liability aggregator.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import List, Optional
# Severity labels (kept as bare strings to avoid import cycles)
CRITICAL = "critical"
WARNING = "warning"
INFO = "info"
# Cap per-motif hits so a pathological sequence can't produce thousands of rows
_MAX_HITS_PER_MOTIF = 50
@dataclass
class MotifHit:
"""A single sequence-liability motif occurrence."""
name: str # machine name, e.g. "uorf"
label: str # human label, e.g. "Upstream AUG (uORF)"
region: str # "5'UTR" | "CDS" | "3'UTR" | "full"
start: int # 0-based start within the scanned region
end: int # exclusive end within the region
match: str # the matched subsequence (DNA alphabet)
severity: str # CRITICAL | WARNING | INFO
description: str # why it matters
recommendation: str # what to do about it
def __repr__(self) -> str:
return f"MotifHit({self.name} {self.region}@{self.start} {self.match!r})"
def _norm(seq: Optional[str]) -> str:
return (seq or "").upper().replace("U", "T")
def _find_all(pattern: str, seq: str) -> List[re.Match]:
"""Find non-overlapping regex matches, capped."""
return list(re.finditer(pattern, seq))[:_MAX_HITS_PER_MOTIF]
def scan_motifs(
five_prime_utr: Optional[str] = None,
cds: Optional[str] = None,
three_prime_utr: Optional[str] = None,
full_seq: Optional[str] = None,
) -> List[MotifHit]:
"""
Scan an mRNA's regions for liability motifs.
Pass the individual components when available. ``full_seq`` is used for
region-agnostic scans (splice donor) and as a fallback when components
are not provided (e.g. a monolithic ``full_mrna`` record).
"""
hits: List[MotifHit] = []
utr5 = _norm(five_prime_utr)
cds_s = _norm(cds)
utr3 = _norm(three_prime_utr)
full = _norm(full_seq) or (utr5 + cds_s + utr3)
# ── uORF: any ATG in the 5'UTR ────────────────────────────────────────────
for m in _find_all("ATG", utr5):
hits.append(MotifHit(
name="uorf",
label="Upstream AUG (uORF)",
region="5'UTR",
start=m.start(), end=m.end(), match=m.group(),
severity=WARNING,
description="An AUG in the 5'UTR can start an upstream ORF and "
"reduce ribosome loading on the main CDS.",
recommendation="Remove or silently mutate upstream AUGs in the 5'UTR.",
))
# ── Premature polyadenylation signal inside the CDS ───────────────────────
for m in _find_all("AATAAA|ATTAAA", cds_s):
hits.append(MotifHit(
name="premature_polya",
label="Premature polyA signal",
region="CDS",
start=m.start(), end=m.end(), match=m.group(),
severity=CRITICAL,
description="A canonical polyadenylation signal inside the CDS can "
"cause premature cleavage and a truncated protein.",
recommendation="Codon-optimise to remove the internal AAUAAA/AUUAAA signal.",
))
# ── AU-rich element (ARE) in the 3'UTR ────────────────────────────────────
for m in _find_all("ATTTA", utr3):
hits.append(MotifHit(
name="are",
label="AU-rich element (ARE)",
region="3'UTR",
start=m.start(), end=m.end(), match=m.group(),
severity=WARNING,
description="AUUUA pentamers recruit mRNA-decay machinery and shorten "
"transcript half-life.",
recommendation="Remove ARE motifs from the 3'UTR to improve stability.",
))
# ── Cryptic 5' splice-donor consensus (region-agnostic) ───────────────────
for m in _find_all("GT[AG]AGT", full):
hits.append(MotifHit(
name="splice_donor",
label="Cryptic splice donor",
region="full",
start=m.start(), end=m.end(), match=m.group(),
severity=INFO,
description="Matches the 5' splice-donor consensus; may cause aberrant "
"splicing if transcribed from a DNA template in cells.",
recommendation="Consider disrupting the GU[A/G]AGU consensus if splicing is a concern.",
))
return hits