Spaces:
Running
Running
| """Base-editing design β a first-class CRISPR mode (Phase 3, M1). | |
| Where the knockout pipeline asks "where will Cas9 cut and how cleanly?", | |
| base editing asks "which single-base change does this guide install, and | |
| what does it do to the protein?". Most competing tools only *flag* that a | |
| guide is base-editable; here we predict the actual edit(s), warn about | |
| bystanders, score editability, and β when a reading frame is known β | |
| report the codon β amino-acid consequence, including premature stops for | |
| DSB-free knockout (CRISPR-STOP / iSTOP). | |
| Two editor families: | |
| * CBE (cytosine base editors): deaminate CβT on the protospacer | |
| (non-target) strand within an editor-specific activity window. | |
| * ABE (adenine base editors): deaminate AβG in a similar window. | |
| Coordinate convention (matches dee/core/crispr.py and _scan_base_editor): | |
| Spacer positions are 1-indexed from the 5' / PAM-DISTAL end. For SpCas9 | |
| the 20-nt protospacer sits 5'β[1..20]β3' with the NGG PAM at 21-23. The | |
| canonical editing window is reported in these coordinates. | |
| Honesty: the windows + per-position activity profiles below are the | |
| published literature consensus, NOT a trained model. They rank-order | |
| editable sites well and match each editor's reported window, but exact | |
| per-base efficiencies vary by sequence context and cell type. The UI says | |
| so. | |
| References: | |
| * Komor AC et al. (2016). Programmable editing of a target base in | |
| genomic DNA without double-stranded DNA cleavage. Nature 533, 420. | |
| [BE3, CBE window 4-8] | |
| * Koblan LW et al. (2018). Improving cytidine and adenine base editors | |
| ... Nat Biotechnol 36, 843. [BE4max] | |
| * Thuronyi BW et al. (2019). Continuous evolution of base editors with | |
| expanded target compatibility. Nat Biotechnol 37, 1070. [evoCDA β wide] | |
| * Gaudelli NM et al. (2017). Programmable base editing of Aβ’T to Gβ’C ... | |
| Nature 551, 464. [ABE7.10, window 4-7] | |
| * Richter MF et al. (2020). Phage-assisted evolution of an adenine base | |
| editor with improved Cas domain compatibility and activity. Nat | |
| Biotechnol 38, 883. [ABE8e β wide, processive] | |
| * Kuscu C et al. (2017). CRISPR-STOP: gene silencing via base editing | |
| -induced nonsense mutations. Nat Methods 14, 710. | |
| * Billon P et al. (2017). CRISPR-mediated base editing enables efficient | |
| disruption of eukaryotic genes through induction of STOP codons. Mol | |
| Cell 67, 1068. [iSTOP] | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass, field | |
| from typing import Dict, List, Optional, Tuple | |
| # βββ Standard genetic code (DNA codons β 1-letter AA; '*' = stop) βββββ | |
| CODON_TABLE: Dict[str, str] = { | |
| "TTT": "F", "TTC": "F", "TTA": "L", "TTG": "L", | |
| "CTT": "L", "CTC": "L", "CTA": "L", "CTG": "L", | |
| "ATT": "I", "ATC": "I", "ATA": "I", "ATG": "M", | |
| "GTT": "V", "GTC": "V", "GTA": "V", "GTG": "V", | |
| "TCT": "S", "TCC": "S", "TCA": "S", "TCG": "S", | |
| "CCT": "P", "CCC": "P", "CCA": "P", "CCG": "P", | |
| "ACT": "T", "ACC": "T", "ACA": "T", "ACG": "T", | |
| "GCT": "A", "GCC": "A", "GCA": "A", "GCG": "A", | |
| "TAT": "Y", "TAC": "Y", "TAA": "*", "TAG": "*", | |
| "CAT": "H", "CAC": "H", "CAA": "Q", "CAG": "Q", | |
| "AAT": "N", "AAC": "N", "AAA": "K", "AAG": "K", | |
| "GAT": "D", "GAC": "D", "GAA": "E", "GAG": "E", | |
| "TGT": "C", "TGC": "C", "TGA": "*", "TGG": "W", | |
| "CGT": "R", "CGC": "R", "CGA": "R", "CGG": "R", | |
| "AGT": "S", "AGC": "S", "AGA": "R", "AGG": "R", | |
| "GGT": "G", "GGC": "G", "GGA": "G", "GGG": "G", | |
| } | |
| class BaseEditor: | |
| id: str | |
| name: str | |
| kind: str # "CBE" | "ABE" | |
| target_base: str # base edited ON THE PROTOSPACER strand | |
| result_base: str # what it becomes | |
| window: Tuple[int, int] # 1-indexed inclusive spacer positions | |
| # Per-position relative deamination activity in [0,1]. Positions | |
| # outside the window are absent (treated as 0 β no editing). | |
| activity: Dict[int, float] | |
| citation: str | |
| note: str = "" | |
| def _profile(window: Tuple[int, int], peak: int, peak_val: float = 1.0, | |
| edge_val: float = 0.35) -> Dict[int, float]: | |
| """Triangular activity profile across `window`, maximal at `peak`.""" | |
| lo, hi = window | |
| prof: Dict[int, float] = {} | |
| span = max(peak - lo, hi - peak, 1) | |
| for p in range(lo, hi + 1): | |
| frac = abs(p - peak) / span | |
| prof[p] = round(peak_val - (peak_val - edge_val) * frac, 3) | |
| return prof | |
| # βββ Curated editor library ββββββββββββββββββββββββββββββββββββββββββ | |
| # Five editors covering the cases real users reach for: a standard + | |
| # an optimized + a wide-window CBE, and a standard + wide ABE. | |
| BASE_EDITORS: Dict[str, BaseEditor] = { | |
| "be4max": BaseEditor( | |
| id="be4max", name="BE4max (CBE)", kind="CBE", | |
| target_base="C", result_base="T", window=(4, 8), | |
| activity=_profile((4, 8), peak=6), | |
| citation="Koblan 2018", note="Optimized cytosine base editor β the common default.", | |
| ), | |
| "be3": BaseEditor( | |
| id="be3", name="BE3 (CBE)", kind="CBE", | |
| target_base="C", result_base="T", window=(4, 8), | |
| activity=_profile((4, 8), peak=6), | |
| citation="Komor 2016", note="Original cytosine base editor.", | |
| ), | |
| "evocda_be4max": BaseEditor( | |
| id="evocda_be4max", name="evoCDA1-BE4max (wide CBE)", kind="CBE", | |
| target_base="C", result_base="T", window=(2, 10), | |
| activity=_profile((2, 10), peak=6, edge_val=0.45), | |
| citation="Thuronyi 2019", | |
| note="Wide-window CBE β more reach, more bystander risk.", | |
| ), | |
| "abe8e": BaseEditor( | |
| id="abe8e", name="ABE8e (ABE)", kind="ABE", | |
| target_base="A", result_base="G", window=(3, 9), | |
| activity=_profile((3, 9), peak=6, edge_val=0.45), | |
| citation="Richter 2020", | |
| note="Highly processive adenine base editor β wide window.", | |
| ), | |
| "abe7.10": BaseEditor( | |
| id="abe7.10", name="ABE7.10 (ABE)", kind="ABE", | |
| target_base="A", result_base="G", window=(4, 7), | |
| activity=_profile((4, 7), peak=6), | |
| citation="Gaudelli 2017", note="Standard adenine base editor β narrow window.", | |
| ), | |
| } | |
| DEFAULT_BASE_EDITOR = "be4max" | |
| def list_base_editors(kind: str = "") -> List[BaseEditor]: | |
| out = list(BASE_EDITORS.values()) | |
| if kind: | |
| out = [e for e in out if e.kind.upper() == kind.upper()] | |
| return out | |
| def get_base_editor(editor_id: str) -> Optional[BaseEditor]: | |
| return BASE_EDITORS.get((editor_id or "").lower()) | |
| # βββ Per-guide edit prediction (sequence-only) βββββββββββββββββββββββ | |
| class BaseEditPrediction: | |
| editor_id: str | |
| # Each edit: (spacer_position_1indexed, from_base, to_base, activity) | |
| edits: List[Tuple[int, str, str, float]] = field(default_factory=list) | |
| editability: float = 0.0 # strongest in-window target activity [0,1] | |
| has_bystander: bool = False # >1 target base in the window | |
| n_targets: int = 0 | |
| summary: str = "" # short label for the table | |
| def predict_base_edits(spacer: str, editor_id: str) -> BaseEditPrediction: | |
| """Which bases this editor would change in the guide's window. | |
| Sequence-only: needs no genome context. Reports every target base in | |
| the activity window, the strongest as the 'editability', and whether | |
| bystander edits are present. | |
| """ | |
| ed = get_base_editor(editor_id) | |
| if ed is None or not spacer: | |
| return BaseEditPrediction(editor_id=editor_id or "") | |
| spacer = spacer.upper() | |
| lo, hi = ed.window | |
| edits: List[Tuple[int, str, str, float]] = [] | |
| for pos in range(lo, hi + 1): | |
| idx = pos - 1 | |
| if 0 <= idx < len(spacer) and spacer[idx] == ed.target_base: | |
| edits.append((pos, ed.target_base, ed.result_base, | |
| ed.activity.get(pos, 0.0))) | |
| editability = max((a for _, _, _, a in edits), default=0.0) | |
| n = len(edits) | |
| if n == 0: | |
| summary = f"no {ed.target_base} in window" | |
| elif n == 1: | |
| p, fb, tb, _ = edits[0] | |
| summary = f"{fb}{p}β{tb}" | |
| else: | |
| summary = ", ".join(f"{fb}{p}β{tb}" for p, fb, tb, _ in edits) + " (bystander)" | |
| return BaseEditPrediction( | |
| editor_id=ed.id, edits=edits, editability=round(editability, 3), | |
| has_bystander=n > 1, n_targets=n, summary=summary, | |
| ) | |
| # βββ Codon-level consequence (frame-aware) βββββββββββββββββββββββββββ | |
| class CodonConsequence: | |
| cds_index: int | |
| codon_before: str | |
| codon_after: str | |
| aa_before: str | |
| aa_after: str | |
| kind: str # "silent" | "missense" | "nonsense" | "stop_loss" | |
| creates_stop: bool | |
| label: str # e.g. "Qβ* (stop gained)" or "A123A (silent)" | |
| def codon_consequence(cds: str, cds_index: int, ref_base: str, | |
| alt_base: str) -> Optional[CodonConsequence]: | |
| """Translate the effect of changing coding-strand base `cds_index` | |
| from ref_baseβalt_base. | |
| Returns None (no guess) if the index is out of range, the codon is | |
| incomplete, or the CDS base doesn't match ref_base (alignment slipped). | |
| `cds` must be the coding strand 5'β3'; cds_index 0-based. | |
| """ | |
| if cds_index < 0 or cds_index >= len(cds): | |
| return None | |
| if cds[cds_index] != ref_base: | |
| return None # alignment mismatch β refuse to fabricate a consequence | |
| codon_start = (cds_index // 3) * 3 | |
| if codon_start + 3 > len(cds): | |
| return None | |
| within = cds_index - codon_start | |
| codon_before = cds[codon_start:codon_start + 3] | |
| codon_after = codon_before[:within] + alt_base + codon_before[within + 1:] | |
| aa_before = CODON_TABLE.get(codon_before, "?") | |
| aa_after = CODON_TABLE.get(codon_after, "?") | |
| creates_stop = aa_after == "*" and aa_before != "*" | |
| if aa_before == aa_after: | |
| kind = "silent" | |
| elif creates_stop: | |
| kind = "nonsense" | |
| elif aa_before == "*" and aa_after != "*": | |
| kind = "stop_loss" | |
| else: | |
| kind = "missense" | |
| aa_num = codon_start // 3 + 1 # 1-based residue number | |
| if kind == "silent": | |
| label = f"{aa_before}{aa_num}= (silent)" | |
| elif kind == "nonsense": | |
| label = f"{aa_before}{aa_num}* (stop gained)" | |
| elif kind == "stop_loss": | |
| label = f"*{aa_num}{aa_after} (stop lost)" | |
| else: | |
| label = f"{aa_before}{aa_num}{aa_after}" | |
| return CodonConsequence( | |
| cds_index=cds_index, codon_before=codon_before, codon_after=codon_after, | |
| aa_before=aa_before, aa_after=aa_after, kind=kind, | |
| creates_stop=creates_stop, label=label, | |
| ) | |