Spaces:
Sleeping
Sleeping
| """ | |
| AdaptiveDNA API β Hugging Face Space backend | |
| FastAPI implementation of all AdaptiveDNA GPT tools. | |
| Deploy steps: | |
| 1. Create a new HF Space (SDK: Docker or Gradio) | |
| 2. Upload this file as app.py | |
| 3. Upload requirements.txt alongside it | |
| 4. The Space URL becomes your OpenAPI server URL in schema.yaml | |
| """ | |
| from __future__ import annotations | |
| import re | |
| from typing import Literal | |
| from fastapi import FastAPI, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel, Field | |
| app = FastAPI( | |
| title="AdaptiveDNA API", | |
| description="CRISPR guide RNA design and DNA sequence analysis for the AdaptiveDNA GPT.", | |
| version="1.0.0", | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # ββ IUPAC ambiguity codes for PAM matching ββββββββββββββββββββββββββββββββββββ | |
| IUPAC: dict[str, str] = { | |
| "N": "[ATGC]", "R": "[AG]", "Y": "[CT]", "S": "[GC]", | |
| "W": "[AT]", "K": "[GT]", "M": "[AC]", "B": "[CGT]", | |
| "D": "[AGT]", "H": "[ACT]", "V": "[ACG]", | |
| } | |
| RESTRICTION_ENZYMES: dict[str, str] = { | |
| "EcoRI": "GAATTC", "BamHI": "GGATCC", "HindIII": "AAGCTT", | |
| "NcoI": "CCATGG", "XhoI": "CTCGAG", "NheI": "GCTAGC", | |
| "SalI": "GTCGAC", "XbaI": "TCTAGA", "SmaI": "CCCGGG", | |
| "KpnI": "GGTACC", "SacI": "GAGCTC", "ApaI": "GGGCCC", | |
| } | |
| NUCLEASE_PRESETS: dict[str, dict] = { | |
| "SpCas9": {"pam": "NGG", "guide_length": 20, "description": "Most common β Streptococcus pyogenes Cas9"}, | |
| "SaCas9": {"pam": "NNGRRT", "guide_length": 21, "description": "Smaller size β Staphylococcus aureus Cas9, AAV-compatible"}, | |
| "Cpf1/Cas12a": {"pam": "TTTV", "guide_length": 24, "description": "5β² PAM, staggered cuts β Cpf1/Cas12a, good for AT-rich regions"}, | |
| "CjCas9": {"pam": "NNNNRYAC", "guide_length": 22, "description": "Ultra-compact β Campylobacter jejuni Cas9"}, | |
| } | |
| CROP_GENE_TARGETS: dict[str, list[dict]] = { | |
| "Rice": [ | |
| {"gene": "OsBADH2", "trait": "Aroma", "description": "Knockout produces 2-AP aromatic compounds"}, | |
| {"gene": "OsSPL14", "trait": "Yield", "description": "Regulation of panicle branching"}, | |
| {"gene": "OsGW5", "trait": "Grain width", "description": "Controls grain width and weight"}, | |
| {"gene": "OsDREB1", "trait": "Drought", "description": "Dehydration-responsive element binding"}, | |
| {"gene": "Xa13", "trait": "Disease resist", "description": "Bacterial blight susceptibility gene"}, | |
| ], | |
| "Maize": [ | |
| {"gene": "ZmLG1", "trait": "Leaf angle", "description": "Liguleless1 β canopy architecture"}, | |
| {"gene": "ZmKW6", "trait": "Kernel weight", "description": "Controls kernel width"}, | |
| {"gene": "ZmWAKL", "trait": "Blight resist", "description": "Wall-associated kinase-like"}, | |
| {"gene": "ZmDREB2A", "trait": "Heat stress", "description": "Drought/heat tolerance transcription factor"}, | |
| ], | |
| "Wheat": [ | |
| {"gene": "TaGW2", "trait": "Grain size", "description": "RING-type E3 ubiquitin ligase"}, | |
| {"gene": "TaDep1", "trait": "Dense ears", "description": "Dense and erect panicle orthologue"}, | |
| {"gene": "TaMLO", "trait": "Powdery mildew", "description": "Mlo orthologue β disease resistance"}, | |
| {"gene": "TaGASR7", "trait": "Grain length", "description": "Gibberellin-regulated"}, | |
| ], | |
| "Tomato": [ | |
| {"gene": "SlCLV3", "trait": "Fruit size", "description": "Clavata3 β meristem identity"}, | |
| {"gene": "SlWUS", "trait": "Fruit number", "description": "Wuschel β locule number"}, | |
| {"gene": "SlPDS", "trait": "Carotenoids", "description": "Phytoene desaturase β lycopene"}, | |
| {"gene": "SlMlo1", "trait": "PM resistance", "description": "Powdery mildew resistance"}, | |
| ], | |
| "Soybean": [ | |
| {"gene": "GmFT2a", "trait": "Flowering", "description": "Florigen β photoperiod adaptation"}, | |
| {"gene": "GmFAD2", "trait": "Fatty acids", "description": "Fatty acid desaturase β oleic acid"}, | |
| {"gene": "GmPDS11", "trait": "Pigmentation", "description": "Phytoene desaturase visual marker"}, | |
| ], | |
| } | |
| # ββ Positional scoring weights (Doench 2016 / Rule Set 2 simplified) βββββββββ | |
| POSITIONAL_WEIGHTS = [ | |
| (1, "G", 0.12), (2, "A", -0.07), (3, "C", 0.05), (4, "T", -0.04), | |
| (12, "C", 0.09), (12, "T", -0.11), (13, "G", 0.08), (16, "G", 0.07), | |
| (17, "A", -0.06), (19, "G", 0.10), (20, "G", 0.08), | |
| ] | |
| # ββ Sequence utilities ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| COMPLEMENT = str.maketrans("ATGCatgcNn", "TACGtacgNn") | |
| def clean_sequence(raw: str) -> str: | |
| return re.sub(r"[^ATGCNatgcn]", "", raw).upper() | |
| def reverse_complement(seq: str) -> str: | |
| return seq.translate(COMPLEMENT)[::-1] | |
| def gc_content(seq: str) -> float: | |
| if not seq: | |
| return 0.0 | |
| gc = sum(1 for b in seq.upper() if b in "GC") | |
| return round((gc / len(seq)) * 1000) / 10 | |
| def melting_temp(seq: str) -> float: | |
| s = seq.upper() | |
| at = sum(1 for b in s if b in "AT") | |
| gc = sum(1 for b in s if b in "GC") | |
| return float(2 * at + 4 * gc) | |
| def pam_to_regex(pam: str) -> re.Pattern: | |
| pattern = "".join(IUPAC.get(c, c) for c in pam.upper()) | |
| return re.compile(pattern) | |
| def score_guide(guide: str) -> float: | |
| g = guide.upper() | |
| score = 0.5 | |
| gc = gc_content(g) / 100 | |
| if 0.4 <= gc <= 0.7: | |
| score += 0.15 | |
| elif gc < 0.3 or gc > 0.8: | |
| score -= 0.25 | |
| if re.search(r"TTTT", g): | |
| score -= 0.20 | |
| if re.search(r"AAAA|CCCC|GGGG", g): | |
| score -= 0.10 | |
| for pos, base, weight in POSITIONAL_WEIGHTS: | |
| if pos - 1 < len(g) and g[pos - 1] == base: | |
| score += weight | |
| seed = g[11:] | |
| seed_gc = gc_content(seed) / 100 | |
| if 0.35 <= seed_gc <= 0.65: | |
| score += 0.08 | |
| if g and g[0] == "G": | |
| score += 0.04 | |
| return max(0.0, min(1.0, score)) | |
| def estimate_off_targets(guide: str) -> int: | |
| seed = guide[-12:].upper() | |
| gc_seed = gc_content(seed) / 100 | |
| risk = 0 | |
| if gc_seed > 0.7: | |
| risk += 3 | |
| elif gc_seed > 0.55: | |
| risk += 1 | |
| if re.search(r"(.{3,})\1", guide): | |
| risk += 2 | |
| if re.search(r"^.{0,3}GGG|GGG.{0,3}$", guide): | |
| risk += 1 | |
| return max(0, risk) | |
| def complexity(score: float, off_targets: int) -> str: | |
| if score >= 0.7 and off_targets == 0: | |
| return "Low" | |
| if score >= 0.5 and off_targets <= 2: | |
| return "Medium" | |
| return "High" | |
| def design_guides(sequence: str, pam: str, guide_len: int, top_n: int) -> list[dict]: | |
| seq = clean_sequence(sequence) | |
| pam_rx = pam_to_regex(pam) | |
| results: list[dict] = [] | |
| for strand, s in [("+", seq), ("-", reverse_complement(seq))]: | |
| for m in pam_rx.finditer(s): | |
| pam_start = m.start() | |
| if pam_start < guide_len: | |
| continue | |
| guide = s[pam_start - guide_len: pam_start] | |
| sc = score_guide(guide) | |
| gc = gc_content(guide) | |
| off = estimate_off_targets(guide) | |
| pos = pam_start - guide_len + 1 if strand == "+" else len(seq) - pam_start + 1 | |
| results.append({ | |
| "sequence": guide, | |
| "pam": m.group(), | |
| "position": pos, | |
| "strand": strand, | |
| "gc_content": gc, | |
| "score": round(sc, 2), | |
| "off_targets": off, | |
| "tm_celsius": melting_temp(guide), | |
| "complexity": complexity(sc, off), | |
| }) | |
| results.sort(key=lambda r: r["score"], reverse=True) | |
| return results[:top_n] | |
| # ββ Request / response models βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class SequenceRequest(BaseModel): | |
| sequence: str = Field(..., description="DNA sequence (ATGCN characters)") | |
| class GuideRNARequest(BaseModel): | |
| sequence: str = Field(..., description="Target DNA sequence (min 23 bp)") | |
| nuclease: Literal["SpCas9", "SaCas9", "Cpf1/Cas12a", "CjCas9"] = Field("SpCas9") | |
| top_n: int = Field(5, ge=1, le=10) | |
| class CropRequest(BaseModel): | |
| crop: Literal["Rice", "Maize", "Wheat", "Tomato", "Soybean"] | |
| # ββ Endpoints βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def analyze_sequence(body: SequenceRequest) -> dict: | |
| seq = clean_sequence(body.sequence) | |
| if not seq: | |
| raise HTTPException(422, detail={"error": "Sequence is empty after cleaning"}) | |
| s = seq.upper() | |
| return { | |
| "length": len(seq), | |
| "gc_content": gc_content(seq), | |
| "at_content": round(((s.count("A") + s.count("T")) / len(seq)) * 1000) / 10, | |
| "tm_celsius": melting_temp(seq), | |
| "a_count": s.count("A"), | |
| "t_count": s.count("T"), | |
| "g_count": s.count("G"), | |
| "c_count": s.count("C"), | |
| "n_count": s.count("N"), | |
| } | |
| def design_guide_rna(body: GuideRNARequest) -> dict: | |
| seq = clean_sequence(body.sequence) | |
| if len(seq) < 23: | |
| raise HTTPException(422, detail={"error": f"Sequence too short ({len(seq)} bp); minimum 23 bp required"}) | |
| preset = NUCLEASE_PRESETS[body.nuclease] | |
| guides = design_guides(seq, preset["pam"], preset["guide_length"], body.top_n) | |
| return {"count": len(guides), "guides": guides} | |
| def find_crop_targets(body: CropRequest) -> dict: | |
| targets = CROP_GENE_TARGETS.get(body.crop) | |
| if not targets: | |
| raise HTTPException(422, detail={"error": f"Unknown crop: {body.crop}"}) | |
| return {"crop": body.crop, "targets": targets} | |
| def calculate_gc(body: SequenceRequest) -> dict: | |
| seq = clean_sequence(body.sequence) | |
| preview = seq[:20] + ("β¦" if len(seq) > 20 else "") | |
| return {"sequence_preview": preview, "gc_percent": gc_content(seq)} | |
| def get_nuclease_presets() -> dict: | |
| presets = [ | |
| {"name": name, "pam": v["pam"], "guide_length": v["guide_length"], "description": v["description"]} | |
| for name, v in NUCLEASE_PRESETS.items() | |
| ] | |
| return {"presets": presets} | |
| def list_crops() -> dict: | |
| return {"crops": list(CROP_GENE_TARGETS.keys())} | |
| def find_restriction_sites(body: SequenceRequest) -> dict: | |
| seq = clean_sequence(body.sequence) | |
| sites = [] | |
| for enzyme, site in RESTRICTION_ENZYMES.items(): | |
| positions = [] | |
| idx = seq.find(site) | |
| while idx != -1: | |
| positions.append(idx + 1) | |
| idx = seq.find(site, idx + 1) | |
| if positions: | |
| sites.append({"enzyme": enzyme, "site": site, "positions": positions}) | |
| return {"sites": sites} | |
| def health() -> dict: | |
| return {"status": "ok", "service": "AdaptiveDNA API"} | |