File size: 12,750 Bytes
9fa9716
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b284c7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9fa9716
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6a3f2e0
 
 
 
9fa9716
 
6a3f2e0
 
 
 
 
 
 
 
 
 
 
 
35fa528
 
 
 
 
 
 
 
 
 
 
 
 
6a3f2e0
 
 
 
 
9fa9716
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b284c7
 
 
 
 
 
 
 
 
 
 
9fa9716
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6a3f2e0
 
9fa9716
 
7b284c7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9fa9716
 
 
 
 
7b284c7
9fa9716
 
7b284c7
 
 
 
 
 
9fa9716
 
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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
"""Provenance for every scored column the CRISPR table shows — the glass-box
disclosure layer.

dee/core/crispr.py has always been scrupulously honest in its own comments
about what each number is (and is not): the on-target score is Doench-INSPIRED
rather than Rule Set 2, the off-target CFD is input-only rather than
genome-wide, the indel model is a heuristic standing in for inDelphi. None of
that reached the user, so a scientist reviewing the tool had to ask for the
composite's weights and flag the missing cell-type caveat — both of which the
code already knew. This module lifts those facts out of the comments and makes
them a first-class, testable, servable artifact (GET /api/crispr/methods) so
the UI can show, per column, exactly how the number was made and where it
stops being trustworthy.

Rule for editing this file: it must describe what dee/core/crispr.py ACTUALLY
does. If the algorithm changes, this changes in the same commit. Never
describe an aspiration here.
"""
from typing import Any, Dict, List

# Keys match the CRISPR result-table column labels the user sees.
METHODS: Dict[str, Dict[str, Any]] = {
    "composite": {
        "label": "Composite",
        "what": "The column the table sorts by — one number balancing "
                "how well a guide should cut against how uniquely it targets.",
        # Kept in sync with dee.core.crispr.COMPOSITE_OFFTARGET_WEIGHT by
        # test_composite_weight_is_disclosed_with_its_real_value, because this
        # entry previously claimed a formula the code did not use.
        "formula": "composite = on_target × (1 − 0.6 × self_off)",
        "basis": "An efficacy estimate scaled down by the specificity "
                 "penalty. The 0.6 is the only tuning constant in the whole "
                 "CRISPR ranking and it is our judgement call, not a "
                 "published value: it means a guide whose spacer is "
                 "duplicated exactly elsewhere in your input keeps 40% of "
                 "its on-target score instead of dropping to zero, because a "
                 "repeat inside a pasted region is often a second legitimate "
                 "copy of your target rather than a disqualifying "
                 "off-target. Every guide carries the weight in its own row "
                 "(composite_offtarget_weight), so the arithmetic is "
                 "checkable per guide.",
        "limits": "The 0.6 is uncalibrated — no measured cleavage dataset was "
                  "used to fit it, and a different weight would reorder guides "
                  "whose self-off scores differ. Sort by On-target and Self-off "
                  "separately if you want to apply your own trade-off. It also "
                  "inherits every limit of its two inputs — in particular the "
                  "specificity term only sees the sequence you pasted.",
        "citations": [],
    },
    "on_target": {
        "label": "On-target",
        "what": "Predicted cleavage efficiency from the spacer sequence alone.",
        "formula": "Doench-2016 per-position nucleotide + dinucleotide weights, "
                   "summed and clamped, mapped to [0, 1].",
        "basis": "Doench-INSPIRED, not Doench Rule Set 2 itself. We implement "
                 "the strongest sequence-only features from the 2016 paper but "
                 "skip its gradient-boosted ML layer. The column is labelled "
                 "“TuringDNA on-target” precisely so we never claim to be RS2.",
        "limits": "Correlates roughly 0.55–0.65 with measured cleavage, versus "
                  "about 0.70 for the full RS2 model — within ~80% of "
                  "state-of-the-art, and sequence-only (it knows nothing about "
                  "chromatin, expression, or your delivery method).",
        "citations": ["Doench et al. 2016, Nat Biotechnol (Rule Set 2)"],
    },
    "self_off": {
        "label": "Self-off",
        "what": "Worst-case similarity between this guide and any OTHER "
                "candidate site, as a specificity warning.",
        "formula": "Maximum CFD score against every other PAM-adjacent site in "
                   "the submitted sequence (0 = unique here, 1 = exact repeat).",
        "basis": "The exact 20×4 mismatch-penalty matrix from Doench 2016 "
                 "Supplementary Table 19 — the industry standard for "
                 "off-target ranking.",
        "limits": "SCOPE: this column searches only the sequence you pasted. "
                  "For a real off-target check, pick an organism — the engine "
                  "then screens your top guides against the genome and fills "
                  "the “Genome off” column (see below).",
        "citations": ["Doench et al. 2016, Nat Biotechnol, Suppl. Table 19 (CFD)"],
    },
    "genome_off": {
        "label": "Genome off",
        "what": "Real off-target search against the organism's genome — the "
                "column that tells you whether a guide cuts somewhere it "
                "shouldn't. Runs when you choose an organism.",
        "formula": "Seed-indexed search (8 nt PAM-proximal seed, ≤1 seed and "
                   "≤4 total mismatches), each candidate CFD-scored; hits "
                   "above CFD 0.05 are kept and ranked.",
        "basis": "Same Doench 2016 CFD matrix used for Self-off, applied "
                 "across the indexed genome rather than just your input. Your "
                 "guide never leaves the engine — only the public reference "
                 "genome is downloaded, and it is cached and reused.",
        "limits": "Two scope limits. (1) COVERAGE: E. coli, S. cerevisiae, "
                  "C. elegans and D. melanogaster are indexed over the "
                  "COMPLETE genome, so an off-target anywhere is found. Human "
                  "(GRCh38) and mouse (GRCm39) are indexed over CODING "
                  "SEQUENCE ONLY — off-targets in introns and intergenic DNA "
                  "are not seen there, and a dedicated whole-genome tool is "
                  "still the right call for that work. The reason is memory, "
                  "not preference: the seed index costs on the order of "
                  "100 bytes per site (measured), and a whole human genome "
                  "carries roughly 390 million PAM sites — tens of gigabytes, "
                  "well beyond the container. (2) DEPTH: only the top-ranked guides are "
                  "screened (the ones you would realistically order), not "
                  "every candidate; the run tells you how many were checked.",
        "citations": [
            "Doench et al. 2016, Nat Biotechnol, Suppl. Table 19 (CFD)",
            "Ensembl release 112 (GRCh38 / GRCm39 CDS); NCBI NC_000913.3 (E. coli K-12)",
        ],
    },
    "ko_score": {
        "label": "KO score",
        "what": "Probability the cut produces a true loss-of-function knockout.",
        "formula": "ko ≈ 0.67 × cut-position bias (linear 1.0 → 0.3 across the input)",
        "basis": "Two empirical factors: a ~67% base rate that Cas9 indels are "
                 "non-multiple-of-3 (true frameshifts), and a position term "
                 "favouring earlier cuts, which truncate more of the protein.",
        "limits": "Approximate: without CDS exon coordinates it cannot tell a "
                  "constitutive exon from a skipped one, nor account for "
                  "nonsense-mediated decay escape.",
        "citations": ["Allen et al. 2018, Nat Biotechnol"],
    },
    "indels": {
        "label": "Top indel · FS % · Dominance",
        "what": "The most likely repair outcomes at this cut: the single most "
                "probable indel, the share of outcomes that frameshift, and how "
                "dominant that top outcome is (40%+ = clean, <25% = messy).",
        "formula": "MMEJ deletions weighted MH_length × exp(−Δ/10), combined "
                   "with a ~32% templated +1 insertion class; FS % = share of "
                   "predicted outcomes whose length is not a multiple of 3.",
        "basis": "Microhomology-mediated end-joining modelled per Bae 2014, plus "
                 "the templated single-base insertion class Cas9 is known to "
                 "produce. These ARE per-guide, sequence-driven predictions — "
                 "not a fixed average.",
        # The repair context these numbers assume, stated as a first-class
        # field so the UI can print it next to FS % rather than leaving the
        # reader to infer it. Everything here is read off the algorithm:
        # crispr._predict_indels models MMEJ deletions + a fixed 32% templated
        # +1 insertion class and has no donor, cell-type or organism argument.
        "assumes": "Template-free end-joining repair (MMEJ + classical NHEJ) "
                   "of a blunt Cas9 double-strand break, in a cell with intact "
                   "repair machinery and no HDR donor supplied. Supply a repair "
                   "template, use a nickase or a base editor, or work in a "
                   "repair-deficient background, and these outcomes do not "
                   "apply.",
        "limits": "Two real limits. (1) HEURISTIC: this is not the inDelphi "
                  "neural network — it reproduces roughly 80% of that model's "
                  "rank-ordering, not its calibrated frequencies. (2) "
                  "CELL-TYPE-AGNOSTIC: its parameters are fixed constants from "
                  "published averages, so the same guide returns the same FS % "
                  "whether you work in mESC, U2OS, HEK293 or primary cells — "
                  "yet the MMEJ/NHEJ repair balance genuinely differs between "
                  "them. Treat these as relative rankings between guides, not "
                  "absolute rates for your cell line.",
        "citations": [
            "Bae et al. 2014, Nat Methods (microhomology / MMEJ)",
            "van Overbeek et al. 2016, Mol Cell; Lemos et al. 2018, PNAS (+1 insertions)",
            "Shen et al. 2018, Nature (inDelphi — the model this approximates)",
        ],
    },
}

# Order the UI should present them in (matches the table's column order).
METHOD_ORDER: List[str] = ["composite", "on_target", "self_off", "genome_off",
                           "ko_score", "indels"]


def genome_scopes() -> Dict[str, Dict[str, Any]]:
    """What the genome off-target search actually covers, per organism.

    Read straight off ``offtarget.GENOME_SOURCES`` rather than restated here:
    the results panel prints this next to the off-target numbers, and a
    hand-copied list is exactly how a panel ends up claiming a whole-genome
    screen for an organism that is indexed over coding sequence only. Adding
    an organism to the registry adds it here with no second edit.
    """
    try:
        from dee.core.offtarget import GENOME_SOURCES
    except Exception:  # noqa: BLE001 — provenance must never break the route
        return {}
    out: Dict[str, Dict[str, Any]] = {}
    for key, src in GENOME_SOURCES.items():
        scope = str(src.get("scope", ""))
        complete = scope == "full genome"
        out[key] = {
            "name": str(src.get("name", key)),
            "scope": scope,
            "complete": complete,
            # One sentence the UI can print verbatim, so the wording lives
            # next to the registry that decides whether it is true.
            "note": (
                "Complete genome indexed — an off-target anywhere is found."
                if complete else
                "CODING SEQUENCE ONLY. Off-targets in introns, intergenic and "
                "regulatory DNA are outside this index and will not appear. "
                "Use a whole-genome tool if your application needs them."
            ),
        }
    return out


def methods_payload() -> Dict[str, Any]:
    """Serializable provenance block for GET /api/crispr/methods."""
    return {
        "order": list(METHOD_ORDER),
        "methods": {k: dict(METHODS[k]) for k in METHOD_ORDER},
        "genome_scopes": genome_scopes(),
        "summary": (
            "Every score here is computed from published, sequence-based "
            "methods — no black box, and exactly one tuning constant (the "
            "0.6 off-target weight in Composite, which is our judgement "
            "call). Two scope limits matter most: for human and mouse the "
            "genome off-target search covers coding sequence only, and indel "
            "predictions assume template-free end-joining repair with fixed "
            "constants — they are not tuned to your cell type."
        ),
    }