Spaces:
Running
Running
File size: 7,762 Bytes
dd2d535 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | """Base-editing design (dee/core/base_editor.py).
Pins the editor library, sequence-only edit prediction (incl. bystander
detection), and β most importantly β the frame-aware codon consequence,
which is what tells a customer "this guide installs a premature stop".
A wrong AA call here would be as damaging as a wrong off-target, so the
codon math is tested against hand-verified examples and is required to
return None rather than guess when the alignment doesn't check out.
"""
import pytest
from dee.core import base_editor as BE
# βββββββββββββββββββββββββββ editor library βββββββββββββββββββββββββββ
def test_library_shape():
be4 = BE.get_base_editor("be4max")
assert be4.kind == "CBE" and be4.target_base == "C" and be4.result_base == "T"
assert be4.window == (4, 8)
abe = BE.get_base_editor("abe7.10")
assert abe.kind == "ABE" and abe.target_base == "A" and abe.result_base == "G"
assert BE.get_base_editor("nope") is None
def test_list_filters_by_kind():
assert {e.id for e in BE.list_base_editors("CBE")} == {"be4max", "be3", "evocda_be4max"}
assert {e.id for e in BE.list_base_editors("ABE")} == {"abe8e", "abe7.10"}
def test_activity_profiles_bounded_and_peaked():
for ed in BE.list_base_editors():
lo, hi = ed.window
assert set(ed.activity) == set(range(lo, hi + 1))
assert all(0.0 <= v <= 1.0 for v in ed.activity.values())
assert max(ed.activity.values()) == pytest.approx(1.0)
# βββββββββββββββββββββββββββ edit prediction βββββββββββββββββββββββββββ
def test_single_cbe_edit_in_window():
spacer = "AAAC" + "A" * 16 # C at position 4 only
p = BE.predict_base_edits(spacer, "be4max")
assert p.n_targets == 1
assert p.edits[0][:3] == (4, "C", "T")
assert p.editability == pytest.approx(0.35) # window edge (pos 4)
assert p.has_bystander is False
assert p.summary == "C4βT"
def test_bystander_flagged_and_editability_is_strongest():
spacer = "AAACA" + "C" + "A" * 14 # C at positions 4 and 6
p = BE.predict_base_edits(spacer, "be4max")
assert p.n_targets == 2
assert p.has_bystander is True
assert p.editability == pytest.approx(1.0) # pos 6 is the window peak
assert "bystander" in p.summary
def test_no_target_in_window():
p = BE.predict_base_edits("A" * 20, "be4max")
assert p.n_targets == 0 and p.editability == 0.0
assert "no C" in p.summary
def test_abe_targets_adenine():
spacer = "GGGA" + "G" * 16 # A at position 4
p = BE.predict_base_edits(spacer, "abe7.10")
assert p.n_targets == 1
assert p.edits[0][:3] == (4, "A", "G")
def test_unknown_editor_returns_empty():
p = BE.predict_base_edits("ACGT" * 5, "not_an_editor")
assert p.n_targets == 0 and p.edits == []
# βββββββββββββββββββββββββββ codon consequence βββββββββββββββββββββββββββ
def test_istop_cga_to_tga():
# CGA (Arg) --CβT at codon position 1--> TGA (stop). The CRISPR-STOP move.
c = BE.codon_consequence("CGA" + "GGG", 0, "C", "T")
assert c.creates_stop is True
assert c.kind == "nonsense"
assert (c.aa_before, c.aa_after) == ("R", "*")
assert c.label == "R1* (stop gained)"
@pytest.mark.parametrize("codon,aa", [("CAA", "Q"), ("CAG", "Q"), ("CGA", "R")])
def test_all_cbe_istop_codons(codon, aa):
# The three codons CBE can turn into a stop by a single CβT.
c = BE.codon_consequence(codon + "TTT", 0, "C", "T")
assert c.creates_stop and c.aa_before == aa and c.aa_after == "*"
def test_silent_edit():
# GCT (Ala) --TβC at position 3--> GCC (still Ala).
c = BE.codon_consequence("GCT" + "AAA", 2, "T", "C")
assert c.kind == "silent" and c.aa_before == c.aa_after == "A"
assert "silent" in c.label
def test_missense_edit():
# GCT (Ala) --GβA at position 1--> ACT (Thr).
c = BE.codon_consequence("GCT" + "AAA", 0, "G", "A")
assert c.kind == "missense" and (c.aa_before, c.aa_after) == ("A", "T")
def test_stop_loss_edit():
# TGA (stop) --AβG at position 3--> TGG (Trp).
c = BE.codon_consequence("TGA" + "GCT", 2, "A", "G")
assert c.kind == "stop_loss" and c.creates_stop is False
assert (c.aa_before, c.aa_after) == ("*", "W")
def test_residue_numbering_uses_codon_index():
# Second codon β residue 2.
c = BE.codon_consequence("GGG" + "CGA", 3, "C", "T")
assert c.label == "R2* (stop gained)"
def test_refuses_to_guess_on_mismatch_or_out_of_range():
# ref_base disagrees with the CDS β None (no fabricated consequence).
assert BE.codon_consequence("GCT", 0, "A", "T") is None
# index past the end β None.
assert BE.codon_consequence("GCT", 99, "T", "C") is None
# incomplete trailing codon β None.
assert BE.codon_consequence("GC", 0, "G", "A") is None
# βββββββββββββββ integration: find_guides(mode="base_edit") βββββββββββββββ
# Locks the full path β edit prediction + coordinate mapping (both strands)
# + frame auto-detection + codon translation β against a fixed in-frame CDS.
from dee.core import crispr as C # noqa: E402
_CDS = "".join([
"ATG", "GCT", "CGA", "GAT", "CAG", "GGC", "CGT", "CAA", "GCC", "TGG",
"CGC", "GAA", "CGG", "CTG", "CGA", "GCA", "CAG", "GAT", "CGT", "TAA",
]) # 60 nt, starts ATG, ends TAA, no internal stop β auto-detected frame
def test_base_edit_mode_populates_fields_and_ranks_by_editability():
guides = C.find_guides(_CDS, enzyme="cas9", max_results=200,
mode="base_edit", base_editor="be4max")
assert guides
for g in guides:
assert g.be_editor == "be4max"
assert 0.0 <= g.be_editability <= 1.0
eds = [g.be_editability for g in guides]
assert eds == sorted(eds, reverse=True) # editability-first ranking
def test_base_edit_detects_istop_on_forward_strand():
guides = C.find_guides(_CDS, enzyme="cas9", max_results=200,
mode="base_edit", base_editor="be4max")
stops = {g.be_aa_change for g in guides if g.be_creates_stop}
# CAG (Q) β TAG (*) via a CβT in the window, at residues 5 and 8.
assert "Q5* (stop gained)" in stops
assert "Q8* (stop gained)" in stops
for g in guides:
if g.be_creates_stop:
assert g.be_aa_change.endswith("* (stop gained)")
def test_base_edit_reverse_strand_aa_mapping():
# A '-' strand guide editing the coding-strand GβA: validates the
# reverse-strand coordinate + complement transform end-to-end.
guides = C.find_guides(_CDS, enzyme="cas9", max_results=200,
mode="base_edit", base_editor="be4max")
changes = {g.be_aa_change for g in guides if g.strand == "-"}
assert "E12K" in changes
def test_base_edit_flags_bystander():
guides = C.find_guides(_CDS, enzyme="cas9", max_results=200,
mode="base_edit", base_editor="be4max")
assert any(g.be_has_bystander for g in guides)
def test_no_aa_change_without_frame():
# A random (non-CDS) sequence β edits predicted, but no AA call.
import random
rng = random.Random(11)
seq = "".join(rng.choice("ACGT") for _ in range(400))
guides = C.find_guides(seq, enzyme="cas9", max_results=50,
mode="base_edit", base_editor="be4max")
assert guides
assert all(g.be_aa_change == "" for g in guides) # frame unknown β no guess
|