VIDRAFT Claude Opus 5 commited on
Commit Β·
b2e7518
0
Parent(s):
Open Discovery Challenge #1 Malaria - submission intake and leaderboard
Browse filesScoring runs on a separate GPU worker, never in this service: docking takes minutes,
the tunnel cuts at ~125s, and an in-process boltz call already wedged an API once.
Submissions queue here and results are posted back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- .gitignore +2 -0
- Dockerfile +12 -0
- README.md +18 -0
- app.py +218 -0
- data/anchor_scores.json +382 -0
- gates.py +138 -0
- index.html +167 -0
- requirements.txt +4 -0
.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
data/ledger.jsonl
|
| 2 |
+
__pycache__/
|
Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HF Spaces runs the container as a non-root user; write only under paths we own.
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
RUN useradd -m -u 1000 user
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
COPY . .
|
| 8 |
+
RUN mkdir -p /app/data && chown -R user:user /app
|
| 9 |
+
USER user
|
| 10 |
+
ENV ODC_LEDGER=/app/data/ledger.jsonl
|
| 11 |
+
EXPOSE 7860
|
| 12 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Open Discovery Challenge
|
| 3 |
+
emoji: π§¬
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Open Discovery Challenge #1 β Malaria
|
| 12 |
+
|
| 13 |
+
AIκ° μ μν μ λΆμ ν보λ₯Ό 곡κ°λ κΈ°μ€μΌλ‘ μλ μ±μ νλ κ°λ°©ν μ μ½ λ°κ΅΄ λν.
|
| 14 |
+
|
| 15 |
+
- **νμ ** PfDHODH (*Plasmodium falciparum* dihydroorotate dehydrogenase)
|
| 16 |
+
- **μνμ ** human DHODH β κΈ°μμΆ© ν¨μλ§ λ§κ³ μ¬λ κ²μ νΌν΄μΌ ν¨
|
| 17 |
+
|
| 18 |
+
μ μλ **κ³μ° κΈ°λ° ν보 νκ°**μ΄λ©°, μ€μ ν¨λ₯Β·μμ μ±μ΄λ μΉμΈμ½κ³Όμ μ°μ΄μ λ»νμ§ μμ΅λλ€.
|
app.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""Open Discovery Challenge - submission intake and leaderboard.
|
| 3 |
+
|
| 4 |
+
Deliberately thin. Nothing here computes a score.
|
| 5 |
+
|
| 6 |
+
Scoring needs docking, docking needs a GPU, and a docking call takes minutes: the tunnel
|
| 7 |
+
in front of this service cuts a request at ~125 s, and boltz spawns grandchildren that
|
| 8 |
+
hold the stdout pipe open so a subprocess call never returns and wedges the whole worker.
|
| 9 |
+
That combination already took the PharmaOS API down once. So submissions land in a ledger
|
| 10 |
+
here, a worker on the GPU box picks them up, and results come back the same way. One
|
| 11 |
+
entrant can never block the service.
|
| 12 |
+
|
| 13 |
+
Endpoints
|
| 14 |
+
GET / leaderboard page
|
| 15 |
+
POST /api/submit accept a structure, run the gates, queue it
|
| 16 |
+
GET /api/leaderboard ranked table, structures masked
|
| 17 |
+
GET /api/queue how much work is outstanding
|
| 18 |
+
GET /api/pending worker: claim unscored entries
|
| 19 |
+
POST /api/result worker: post a score back
|
| 20 |
+
"""
|
| 21 |
+
import hashlib
|
| 22 |
+
import json
|
| 23 |
+
import os
|
| 24 |
+
import time
|
| 25 |
+
import uuid
|
| 26 |
+
|
| 27 |
+
from fastapi import FastAPI, HTTPException, Request
|
| 28 |
+
from fastapi.responses import FileResponse, JSONResponse
|
| 29 |
+
from pydantic import BaseModel
|
| 30 |
+
|
| 31 |
+
import gates
|
| 32 |
+
|
| 33 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 34 |
+
LEDGER = os.environ.get("ODC_LEDGER", os.path.join(HERE, "data", "ledger.jsonl"))
|
| 35 |
+
ANCHORS = os.path.join(HERE, "data", "anchor_scores.json")
|
| 36 |
+
# Only the worker may post scores. Without this anyone could write their own.
|
| 37 |
+
WORKER_KEY = os.environ.get("ODC_WORKER_KEY", "")
|
| 38 |
+
# Masked identifiers must not be reversible from the public table.
|
| 39 |
+
SALT = os.environ.get("ODC_SALT", "odc-season1")
|
| 40 |
+
|
| 41 |
+
SEASON = {"name": "Open Discovery Challenge", "number": 1, "topic": "Malaria",
|
| 42 |
+
"target": "PfDHODH", "counter_target": "human DHODH",
|
| 43 |
+
"weights": {"μ½ν¨": 30, "κ²°ν©": 20, "μ νμ±": 20, "ADMET": 15, "μ κ·μ±": 10, "ν©μ±": 5}}
|
| 44 |
+
|
| 45 |
+
app = FastAPI(title="Open Discovery Challenge")
|
| 46 |
+
os.makedirs(os.path.dirname(LEDGER), exist_ok=True)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class Submission(BaseModel):
|
| 50 |
+
structure: str # SMILES or InChI
|
| 51 |
+
display_name: str # name, affiliation or handle - entrant's choice
|
| 52 |
+
model_name: str = "" # which model proposed it; free text, blank allowed
|
| 53 |
+
rationale: str = "" # optional design note
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _read():
|
| 57 |
+
if not os.path.exists(LEDGER):
|
| 58 |
+
return []
|
| 59 |
+
out = []
|
| 60 |
+
with open(LEDGER, encoding="utf-8") as f:
|
| 61 |
+
for line in f:
|
| 62 |
+
line = line.strip()
|
| 63 |
+
if line:
|
| 64 |
+
out.append(json.loads(line))
|
| 65 |
+
return out
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _append(rec):
|
| 69 |
+
with open(LEDGER, "a", encoding="utf-8") as f:
|
| 70 |
+
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _latest():
|
| 74 |
+
"""Last write per submission id wins, so a worker can post a score by appending."""
|
| 75 |
+
by_id = {}
|
| 76 |
+
for r in _read():
|
| 77 |
+
by_id[r["id"]] = {**by_id.get(r["id"], {}), **r}
|
| 78 |
+
return list(by_id.values())
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def public_id(inchikey):
|
| 82 |
+
"""A stable public handle that cannot be walked back to the structure.
|
| 83 |
+
|
| 84 |
+
The skeleton block of an InChIKey is a hash of connectivity, so it is safe to show
|
| 85 |
+
and still lets anyone check two entries are the same compound. The full key and the
|
| 86 |
+
SMILES stay in the ledger."""
|
| 87 |
+
h = hashlib.sha256((SALT + inchikey).encode()).hexdigest()[:6].upper()
|
| 88 |
+
return "ODC-%s" % h
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def mask(rec):
|
| 92 |
+
"""What the public table is allowed to see: enough to verify and compare, never
|
| 93 |
+
enough to reconstruct. Entrants keep their chemistry until they choose otherwise."""
|
| 94 |
+
return {
|
| 95 |
+
"candidate_id": rec.get("candidate_id"),
|
| 96 |
+
"skeleton": (rec.get("inchikey") or "")[:14],
|
| 97 |
+
"display_name": rec.get("display_name"),
|
| 98 |
+
"model_name": rec.get("model_name"),
|
| 99 |
+
"mw_band": rec.get("mw_band"),
|
| 100 |
+
"status": rec.get("status"),
|
| 101 |
+
"total": rec.get("total"),
|
| 102 |
+
"axes": rec.get("axes_points"),
|
| 103 |
+
"tier": rec.get("tier", 1),
|
| 104 |
+
"relegate_reason": rec.get("relegate_reason") or [],
|
| 105 |
+
"submitted_at": rec.get("submitted_at"),
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def band(x, step=50):
|
| 110 |
+
if x is None:
|
| 111 |
+
return None
|
| 112 |
+
lo = int(x // step) * step
|
| 113 |
+
return "%d-%d" % (lo, lo + step)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
@app.get("/")
|
| 117 |
+
def index():
|
| 118 |
+
return FileResponse(os.path.join(HERE, "index.html"))
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
@app.get("/api/season")
|
| 122 |
+
def season():
|
| 123 |
+
return SEASON
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
@app.post("/api/submit")
|
| 127 |
+
def submit(s: Submission):
|
| 128 |
+
if not s.display_name.strip():
|
| 129 |
+
raise HTTPException(400, "νμ IDλ₯Ό μ
λ ₯νμΈμ")
|
| 130 |
+
# Season 1's target is not a covalent-mechanism enzyme, so that rule is off here.
|
| 131 |
+
v = gates.check(s.structure, covalent_rule=False)
|
| 132 |
+
if not v["admitted"]:
|
| 133 |
+
return JSONResponse({"accepted": False, "reasons": v["reject"]}, status_code=422)
|
| 134 |
+
|
| 135 |
+
existing = {r.get("inchikey") for r in _latest()}
|
| 136 |
+
if v["inchikey"] in existing:
|
| 137 |
+
return JSONResponse(
|
| 138 |
+
{"accepted": False,
|
| 139 |
+
"reasons": ["μ΄λ―Έ μ μΆλ ꡬ쑰μ
λλ€ (%s)" % public_id(v["inchikey"])]},
|
| 140 |
+
status_code=409)
|
| 141 |
+
|
| 142 |
+
rec = {
|
| 143 |
+
"id": uuid.uuid4().hex,
|
| 144 |
+
"candidate_id": public_id(v["inchikey"]),
|
| 145 |
+
"smiles": v["smiles"], "inchikey": v["inchikey"],
|
| 146 |
+
"mw": v["mw"], "mw_band": band(v["mw"]),
|
| 147 |
+
"display_name": s.display_name.strip()[:60],
|
| 148 |
+
"model_name": s.model_name.strip()[:80],
|
| 149 |
+
"rationale": s.rationale.strip()[:2000],
|
| 150 |
+
"status": "queued", "submitted_at": int(time.time()),
|
| 151 |
+
}
|
| 152 |
+
_append(rec)
|
| 153 |
+
ahead = sum(1 for r in _latest() if r.get("status") == "queued")
|
| 154 |
+
return {"accepted": True, "candidate_id": rec["candidate_id"],
|
| 155 |
+
"queue_position": ahead,
|
| 156 |
+
"note": "μ±μ μ GPU μμ
μΌλ‘ μ²λ¦¬λλ©° μλ£κΉμ§ λͺ λΆ κ±Έλ¦½λλ€."}
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
@app.get("/api/leaderboard")
|
| 160 |
+
def leaderboard():
|
| 161 |
+
rows = [mask(r) for r in _latest() if r.get("status") == "scored"]
|
| 162 |
+
anchors = []
|
| 163 |
+
if os.path.exists(ANCHORS):
|
| 164 |
+
for a in json.load(open(ANCHORS, encoding="utf-8")):
|
| 165 |
+
if not a.get("admitted"):
|
| 166 |
+
continue
|
| 167 |
+
anchors.append({"candidate_id": a["label"], "is_anchor": True,
|
| 168 |
+
"display_name": "κΈ°μ€λ¬Όμ§", "model_name": "",
|
| 169 |
+
"total": a["total"], "tier": a.get("tier", 1),
|
| 170 |
+
"axes": {k: v["points"] for k, v in a["axes"].items()},
|
| 171 |
+
"note": a.get("note", "")})
|
| 172 |
+
merged = rows + anchors
|
| 173 |
+
merged.sort(key=lambda e: (e.get("tier", 1), -(e.get("total") or -1)))
|
| 174 |
+
n = 0
|
| 175 |
+
for e in merged:
|
| 176 |
+
if e.get("is_anchor") or e.get("tier", 1) != 1:
|
| 177 |
+
e["rank"] = None # anchors mark the ladder; they do not climb it
|
| 178 |
+
else:
|
| 179 |
+
n += 1
|
| 180 |
+
e["rank"] = n
|
| 181 |
+
return {"season": SEASON, "entries": merged,
|
| 182 |
+
"counts": {"scored": len(rows), "anchors": len(anchors)}}
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
@app.get("/api/queue")
|
| 186 |
+
def queue():
|
| 187 |
+
rs = _latest()
|
| 188 |
+
return {"queued": sum(1 for r in rs if r.get("status") == "queued"),
|
| 189 |
+
"scoring": sum(1 for r in rs if r.get("status") == "scoring"),
|
| 190 |
+
"scored": sum(1 for r in rs if r.get("status") == "scored")}
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def _auth(request: Request):
|
| 194 |
+
if not WORKER_KEY or request.headers.get("X-Worker-Key") != WORKER_KEY:
|
| 195 |
+
raise HTTPException(401, "worker key required")
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
@app.get("/api/pending")
|
| 199 |
+
def pending(request: Request, limit: int = 5):
|
| 200 |
+
_auth(request)
|
| 201 |
+
out = [r for r in _latest() if r.get("status") == "queued"][:limit]
|
| 202 |
+
for r in out:
|
| 203 |
+
_append({"id": r["id"], "status": "scoring"})
|
| 204 |
+
return {"items": [{"id": r["id"], "smiles": r["smiles"],
|
| 205 |
+
"candidate_id": r["candidate_id"]} for r in out]}
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
@app.post("/api/result")
|
| 209 |
+
async def result(request: Request):
|
| 210 |
+
_auth(request)
|
| 211 |
+
body = await request.json()
|
| 212 |
+
if "id" not in body:
|
| 213 |
+
raise HTTPException(400, "id required")
|
| 214 |
+
_append({"id": body["id"], "status": "scored",
|
| 215 |
+
"total": body.get("total"), "axes_points": body.get("axes_points"),
|
| 216 |
+
"tier": body.get("tier", 1), "relegate_reason": body.get("relegate_reason"),
|
| 217 |
+
"scored_at": int(time.time()), "run": body.get("run")})
|
| 218 |
+
return {"ok": True}
|
data/anchor_scores.json
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"admitted": true,
|
| 4 |
+
"gate": {
|
| 5 |
+
"admitted": true,
|
| 6 |
+
"reject": [],
|
| 7 |
+
"relegate": [],
|
| 8 |
+
"notes": [],
|
| 9 |
+
"smiles": "Cc1cc(Nc2ccc(S(F)(F)(F)(F)F)cc2)n2nc(C(C)(F)F)nc2n1",
|
| 10 |
+
"inchikey": "OIZSVTOIBNSVOS-UHFFFAOYSA-N",
|
| 11 |
+
"mw": 415.34,
|
| 12 |
+
"heavy_atoms": 27
|
| 13 |
+
},
|
| 14 |
+
"relegated": false,
|
| 15 |
+
"relegate_reason": [],
|
| 16 |
+
"axes": {
|
| 17 |
+
"activity": {
|
| 18 |
+
"points": 14.45,
|
| 19 |
+
"detail": {
|
| 20 |
+
"pAct_q10": 5.467,
|
| 21 |
+
"fraction": 0.482
|
| 22 |
+
}
|
| 23 |
+
},
|
| 24 |
+
"binding": {
|
| 25 |
+
"points": 15.12,
|
| 26 |
+
"detail": {
|
| 27 |
+
"pIC50": 7.27,
|
| 28 |
+
"LE": 0.369,
|
| 29 |
+
"potency_term": 0.756,
|
| 30 |
+
"efficiency_cap": 1.0
|
| 31 |
+
}
|
| 32 |
+
},
|
| 33 |
+
"selectivity": {
|
| 34 |
+
"points": 12.59,
|
| 35 |
+
"detail": {
|
| 36 |
+
"fold": 46.3,
|
| 37 |
+
"selectivity_term": 0.833,
|
| 38 |
+
"target_engagement": 0.756
|
| 39 |
+
}
|
| 40 |
+
},
|
| 41 |
+
"admet": {
|
| 42 |
+
"points": 5.89,
|
| 43 |
+
"detail": {
|
| 44 |
+
"geometric_mean": 0.519,
|
| 45 |
+
"per_endpoint": {
|
| 46 |
+
"solubility-aqsoldb": 0.405,
|
| 47 |
+
"caco2-wang": 0.763,
|
| 48 |
+
"hia-hou": 1.0,
|
| 49 |
+
"bioavailability-ma": 1.0,
|
| 50 |
+
"pgp-broccatelli": 1.0,
|
| 51 |
+
"cyp2c9-veith": 0.349,
|
| 52 |
+
"cyp3a4-substrate-carbonmangels": 0.638,
|
| 53 |
+
"ld50-zhu": 0.66,
|
| 54 |
+
"herg": 0.116,
|
| 55 |
+
"dili": 0.05
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
},
|
| 59 |
+
"novelty": {
|
| 60 |
+
"points": 0.0,
|
| 61 |
+
"detail": {
|
| 62 |
+
"max_tanimoto": 1.0,
|
| 63 |
+
"novelty": 0.0
|
| 64 |
+
}
|
| 65 |
+
},
|
| 66 |
+
"synthesis": {
|
| 67 |
+
"points": 2.84,
|
| 68 |
+
"detail": {
|
| 69 |
+
"sa_score": 3.37
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
},
|
| 73 |
+
"total": 50.88,
|
| 74 |
+
"tier": 1,
|
| 75 |
+
"label": "DSM265",
|
| 76 |
+
"is_anchor": true,
|
| 77 |
+
"note": "μμν보 (μμ± μ΅μ»€)",
|
| 78 |
+
"rank": null
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"admitted": true,
|
| 82 |
+
"gate": {
|
| 83 |
+
"admitted": true,
|
| 84 |
+
"reject": [],
|
| 85 |
+
"relegate": [],
|
| 86 |
+
"notes": [],
|
| 87 |
+
"smiles": "Cc1c(-c2ccc(-c3ccccc3F)cc2)nc2ccc(F)cc2c1C(=O)O",
|
| 88 |
+
"inchikey": "PHEZJEYUWHETKO-UHFFFAOYSA-N",
|
| 89 |
+
"mw": 375.37,
|
| 90 |
+
"heavy_atoms": 28
|
| 91 |
+
},
|
| 92 |
+
"relegated": false,
|
| 93 |
+
"relegate_reason": [],
|
| 94 |
+
"axes": {
|
| 95 |
+
"activity": {
|
| 96 |
+
"points": 2.58,
|
| 97 |
+
"detail": {
|
| 98 |
+
"pAct_q10": 4.755,
|
| 99 |
+
"fraction": 0.086
|
| 100 |
+
}
|
| 101 |
+
},
|
| 102 |
+
"binding": {
|
| 103 |
+
"points": 0.0,
|
| 104 |
+
"detail": {
|
| 105 |
+
"reason": "λνΉ μμ"
|
| 106 |
+
}
|
| 107 |
+
},
|
| 108 |
+
"selectivity": {
|
| 109 |
+
"points": 0.0,
|
| 110 |
+
"detail": {
|
| 111 |
+
"reason": "μ μΈ‘μ μμ"
|
| 112 |
+
}
|
| 113 |
+
},
|
| 114 |
+
"admet": {
|
| 115 |
+
"points": 0.47,
|
| 116 |
+
"detail": {
|
| 117 |
+
"geometric_mean": 0.363,
|
| 118 |
+
"per_endpoint": {
|
| 119 |
+
"solubility-aqsoldb": 0.05,
|
| 120 |
+
"caco2-wang": 0.714,
|
| 121 |
+
"hia-hou": 1.0,
|
| 122 |
+
"bioavailability-ma": 1.0,
|
| 123 |
+
"pgp-broccatelli": 0.766,
|
| 124 |
+
"cyp2c9-veith": 0.696,
|
| 125 |
+
"cyp3a4-substrate-carbonmangels": 0.88,
|
| 126 |
+
"ld50-zhu": 0.271,
|
| 127 |
+
"herg": 0.115,
|
| 128 |
+
"dili": 0.05
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
},
|
| 132 |
+
"novelty": {
|
| 133 |
+
"points": 0.56,
|
| 134 |
+
"detail": {
|
| 135 |
+
"max_tanimoto": 0.348,
|
| 136 |
+
"novelty": 0.652
|
| 137 |
+
}
|
| 138 |
+
},
|
| 139 |
+
"synthesis": {
|
| 140 |
+
"points": 0.43,
|
| 141 |
+
"detail": {
|
| 142 |
+
"sa_score": 2.06
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
},
|
| 146 |
+
"total": 4.04,
|
| 147 |
+
"tier": 1,
|
| 148 |
+
"label": "Brequinar",
|
| 149 |
+
"is_anchor": false,
|
| 150 |
+
"note": "μ¬λ DHODH μ΅μ μ (μ νμ± λ°λ‘)",
|
| 151 |
+
"rank": 1
|
| 152 |
+
},
|
| 153 |
+
{
|
| 154 |
+
"admitted": true,
|
| 155 |
+
"gate": {
|
| 156 |
+
"admitted": true,
|
| 157 |
+
"reject": [],
|
| 158 |
+
"relegate": [],
|
| 159 |
+
"notes": [],
|
| 160 |
+
"smiles": "C/C(O)=C(\\C#N)C(=O)Nc1ccc(C(F)(F)F)cc1",
|
| 161 |
+
"inchikey": "UTNUDOFZCWSZMS-YFHOEESVSA-N",
|
| 162 |
+
"mw": 270.21,
|
| 163 |
+
"heavy_atoms": 19
|
| 164 |
+
},
|
| 165 |
+
"relegated": false,
|
| 166 |
+
"relegate_reason": [],
|
| 167 |
+
"axes": {
|
| 168 |
+
"activity": {
|
| 169 |
+
"points": 1.66,
|
| 170 |
+
"detail": {
|
| 171 |
+
"pAct_q10": 4.362,
|
| 172 |
+
"fraction": 0.055
|
| 173 |
+
}
|
| 174 |
+
},
|
| 175 |
+
"binding": {
|
| 176 |
+
"points": 0.0,
|
| 177 |
+
"detail": {
|
| 178 |
+
"reason": "λνΉ μμ"
|
| 179 |
+
}
|
| 180 |
+
},
|
| 181 |
+
"selectivity": {
|
| 182 |
+
"points": 0.0,
|
| 183 |
+
"detail": {
|
| 184 |
+
"reason": "μ μΈ‘μ μμ"
|
| 185 |
+
}
|
| 186 |
+
},
|
| 187 |
+
"admet": {
|
| 188 |
+
"points": 0.51,
|
| 189 |
+
"detail": {
|
| 190 |
+
"geometric_mean": 0.614,
|
| 191 |
+
"per_endpoint": {
|
| 192 |
+
"solubility-aqsoldb": 0.575,
|
| 193 |
+
"caco2-wang": 0.851,
|
| 194 |
+
"hia-hou": 1.0,
|
| 195 |
+
"bioavailability-ma": 1.0,
|
| 196 |
+
"pgp-broccatelli": 1.0,
|
| 197 |
+
"cyp2c9-veith": 0.568,
|
| 198 |
+
"cyp3a4-substrate-carbonmangels": 0.21,
|
| 199 |
+
"ld50-zhu": 0.459,
|
| 200 |
+
"herg": 1.0,
|
| 201 |
+
"dili": 0.11
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
},
|
| 205 |
+
"novelty": {
|
| 206 |
+
"points": 0.35,
|
| 207 |
+
"detail": {
|
| 208 |
+
"max_tanimoto": 0.357,
|
| 209 |
+
"novelty": 0.643
|
| 210 |
+
}
|
| 211 |
+
},
|
| 212 |
+
"synthesis": {
|
| 213 |
+
"points": 0.28,
|
| 214 |
+
"detail": {
|
| 215 |
+
"sa_score": 2.27
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
},
|
| 219 |
+
"total": 2.79,
|
| 220 |
+
"tier": 1,
|
| 221 |
+
"label": "Teriflunomide",
|
| 222 |
+
"is_anchor": false,
|
| 223 |
+
"note": "μ¬λ DHODH μ΅μ μ (μ νμ± λ°λ‘)",
|
| 224 |
+
"rank": 2
|
| 225 |
+
},
|
| 226 |
+
{
|
| 227 |
+
"admitted": true,
|
| 228 |
+
"gate": {
|
| 229 |
+
"admitted": true,
|
| 230 |
+
"reject": [],
|
| 231 |
+
"relegate": [],
|
| 232 |
+
"notes": [],
|
| 233 |
+
"smiles": "CC(C)Cc1ccc(C(C)C(=O)O)cc1",
|
| 234 |
+
"inchikey": "HEFNNWSXXWATRW-UHFFFAOYSA-N",
|
| 235 |
+
"mw": 206.28,
|
| 236 |
+
"heavy_atoms": 15
|
| 237 |
+
},
|
| 238 |
+
"relegated": false,
|
| 239 |
+
"relegate_reason": [],
|
| 240 |
+
"axes": {
|
| 241 |
+
"activity": {
|
| 242 |
+
"points": 1.04,
|
| 243 |
+
"detail": {
|
| 244 |
+
"pAct_q10": 4.174,
|
| 245 |
+
"fraction": 0.035
|
| 246 |
+
}
|
| 247 |
+
},
|
| 248 |
+
"binding": {
|
| 249 |
+
"points": 0.06,
|
| 250 |
+
"detail": {
|
| 251 |
+
"pIC50": 3.57,
|
| 252 |
+
"LE": 0.326,
|
| 253 |
+
"potency_term": 0.003,
|
| 254 |
+
"efficiency_cap": 1.0
|
| 255 |
+
}
|
| 256 |
+
},
|
| 257 |
+
"selectivity": {
|
| 258 |
+
"points": 0.0,
|
| 259 |
+
"detail": {
|
| 260 |
+
"fold": 0.3,
|
| 261 |
+
"selectivity_term": 0.0,
|
| 262 |
+
"target_engagement": 0.0
|
| 263 |
+
}
|
| 264 |
+
},
|
| 265 |
+
"admet": {
|
| 266 |
+
"points": 0.38,
|
| 267 |
+
"detail": {
|
| 268 |
+
"geometric_mean": 0.74,
|
| 269 |
+
"per_endpoint": {
|
| 270 |
+
"solubility-aqsoldb": 0.677,
|
| 271 |
+
"caco2-wang": 1.0,
|
| 272 |
+
"hia-hou": 1.0,
|
| 273 |
+
"bioavailability-ma": 1.0,
|
| 274 |
+
"pgp-broccatelli": 1.0,
|
| 275 |
+
"cyp2c9-veith": 1.0,
|
| 276 |
+
"cyp3a4-substrate-carbonmangels": 1.0,
|
| 277 |
+
"ld50-zhu": 0.212,
|
| 278 |
+
"herg": 1.0,
|
| 279 |
+
"dili": 0.528
|
| 280 |
+
}
|
| 281 |
+
}
|
| 282 |
+
},
|
| 283 |
+
"novelty": {
|
| 284 |
+
"points": 0.21,
|
| 285 |
+
"detail": {
|
| 286 |
+
"max_tanimoto": 0.386,
|
| 287 |
+
"novelty": 0.614
|
| 288 |
+
}
|
| 289 |
+
},
|
| 290 |
+
"synthesis": {
|
| 291 |
+
"points": 0.17,
|
| 292 |
+
"detail": {
|
| 293 |
+
"sa_score": 2.19
|
| 294 |
+
}
|
| 295 |
+
}
|
| 296 |
+
},
|
| 297 |
+
"total": 1.86,
|
| 298 |
+
"tier": 1,
|
| 299 |
+
"label": "Ibuprofen",
|
| 300 |
+
"is_anchor": true,
|
| 301 |
+
"note": "μμ±λμ‘°",
|
| 302 |
+
"rank": null
|
| 303 |
+
},
|
| 304 |
+
{
|
| 305 |
+
"admitted": true,
|
| 306 |
+
"gate": {
|
| 307 |
+
"admitted": true,
|
| 308 |
+
"reject": [],
|
| 309 |
+
"relegate": [],
|
| 310 |
+
"notes": [],
|
| 311 |
+
"smiles": "Cn1c(=O)c2c(ncn2C)n(C)c1=O",
|
| 312 |
+
"inchikey": "RYYVLZVUVIJVGH-UHFFFAOYSA-N",
|
| 313 |
+
"mw": 194.19,
|
| 314 |
+
"heavy_atoms": 14
|
| 315 |
+
},
|
| 316 |
+
"relegated": false,
|
| 317 |
+
"relegate_reason": [],
|
| 318 |
+
"axes": {
|
| 319 |
+
"activity": {
|
| 320 |
+
"points": 1.1,
|
| 321 |
+
"detail": {
|
| 322 |
+
"pAct_q10": 4.197,
|
| 323 |
+
"fraction": 0.037
|
| 324 |
+
}
|
| 325 |
+
},
|
| 326 |
+
"binding": {
|
| 327 |
+
"points": 0.06,
|
| 328 |
+
"detail": {
|
| 329 |
+
"pIC50": 3.62,
|
| 330 |
+
"LE": 0.354,
|
| 331 |
+
"potency_term": 0.003,
|
| 332 |
+
"efficiency_cap": 1.0
|
| 333 |
+
}
|
| 334 |
+
},
|
| 335 |
+
"selectivity": {
|
| 336 |
+
"points": 0.0,
|
| 337 |
+
"detail": {
|
| 338 |
+
"fold": 1.3,
|
| 339 |
+
"selectivity_term": 0.058,
|
| 340 |
+
"target_engagement": 0.0
|
| 341 |
+
}
|
| 342 |
+
},
|
| 343 |
+
"admet": {
|
| 344 |
+
"points": 0.27,
|
| 345 |
+
"detail": {
|
| 346 |
+
"geometric_mean": 0.496,
|
| 347 |
+
"per_endpoint": {
|
| 348 |
+
"solubility-aqsoldb": 1.0,
|
| 349 |
+
"caco2-wang": 1.0,
|
| 350 |
+
"hia-hou": 1.0,
|
| 351 |
+
"bioavailability-ma": 1.0,
|
| 352 |
+
"pgp-broccatelli": 1.0,
|
| 353 |
+
"cyp2c9-veith": 1.0,
|
| 354 |
+
"cyp3a4-substrate-carbonmangels": 0.05,
|
| 355 |
+
"ld50-zhu": 0.096,
|
| 356 |
+
"herg": 0.1,
|
| 357 |
+
"dili": 0.58
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
},
|
| 361 |
+
"novelty": {
|
| 362 |
+
"points": 0.23,
|
| 363 |
+
"detail": {
|
| 364 |
+
"max_tanimoto": 0.367,
|
| 365 |
+
"novelty": 0.633
|
| 366 |
+
}
|
| 367 |
+
},
|
| 368 |
+
"synthesis": {
|
| 369 |
+
"points": 0.18,
|
| 370 |
+
"detail": {
|
| 371 |
+
"sa_score": 2.3
|
| 372 |
+
}
|
| 373 |
+
}
|
| 374 |
+
},
|
| 375 |
+
"total": 1.84,
|
| 376 |
+
"tier": 1,
|
| 377 |
+
"label": "Caffeine",
|
| 378 |
+
"is_anchor": true,
|
| 379 |
+
"note": "μμ±λμ‘°",
|
| 380 |
+
"rank": null
|
| 381 |
+
}
|
| 382 |
+
]
|
gates.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""Season 1 admission gates.
|
| 3 |
+
|
| 4 |
+
Gates decide eligibility, never quality. Anything that reflects how good a molecule is
|
| 5 |
+
belongs in the continuous score; a gate only answers "is this a valid Season 1 entry".
|
| 6 |
+
Failing a gate does not put a submission at the bottom of the table - it means the entry
|
| 7 |
+
is returned, because a covalent warhead in a non-covalent season is not a bad candidate,
|
| 8 |
+
it is the wrong kind of candidate.
|
| 9 |
+
|
| 10 |
+
The one exception is ADMET, and it is deliberate: a candidate that fails a toxicity gate
|
| 11 |
+
is still scored in full and still shown, but sorts below every clean entry no matter how
|
| 12 |
+
high that score is. A molecule that binds beautifully and is mutagenic is not a lead.
|
| 13 |
+
|
| 14 |
+
Every threshold here was checked against a panel of approved drugs first. A gate that
|
| 15 |
+
rejects an approved drug is a broken gate, and two of the obvious candidates - hERG and
|
| 16 |
+
DILI - were thrown out for exactly that reason: our own ADMET model scores ensitrelvir
|
| 17 |
+
at hERG 0.978 and DILI 0.995, and caffeine at hERG 0.885. See data/anchors.json.
|
| 18 |
+
"""
|
| 19 |
+
from rdkit import Chem, RDLogger
|
| 20 |
+
from rdkit.Chem import Descriptors
|
| 21 |
+
from rdkit.Chem.FilterCatalog import FilterCatalog, FilterCatalogParams
|
| 22 |
+
|
| 23 |
+
RDLogger.DisableLog("rdApp.*")
|
| 24 |
+
|
| 25 |
+
_p = FilterCatalogParams()
|
| 26 |
+
_p.AddCatalog(FilterCatalogParams.FilterCatalogs.PAINS)
|
| 27 |
+
_PAINS = FilterCatalog(_p)
|
| 28 |
+
|
| 29 |
+
MW_MAX = 550.0 # ensitrelvir is 531.9; a 500 cap would disqualify the reference drug
|
| 30 |
+
HEAVY_MAX = 45
|
| 31 |
+
AMES_MAX = 0.7 # panel range was 0.024-0.302, so this leaves real headroom
|
| 32 |
+
LOGS_MIN = -7.0 # panel low was -5.42
|
| 33 |
+
|
| 34 |
+
# Electrophiles that form a covalent bond with the catalytic residue. Season 1 excludes
|
| 35 |
+
# them: they are a different medicinal-chemistry problem, and a docking-based score
|
| 36 |
+
# rewards raw reactivity in ways that are easy to game.
|
| 37 |
+
WARHEADS = [
|
| 38 |
+
# A nitrile on its own is not a warhead. Teriflunomide is an approved, non-covalent
|
| 39 |
+
# DHODH inhibitor and a bare [CX2]#[NX1] pattern rejected it - the same mistake as
|
| 40 |
+
# gating on hERG, caught the same way, by running an approved drug through first.
|
| 41 |
+
# What actually reacts with a catalytic cysteine is the peptidyl nitrile: the nitrile
|
| 42 |
+
# carbon sitting on an sp3 centre that carries the amide nitrogen, as in nirmatrelvir.
|
| 43 |
+
("peptidyl nitrile", "[NX3][CX4][CX2]#[NX1]"),
|
| 44 |
+
# Michael acceptors must be terminal to react. The general C=C-C(=O)N pattern also
|
| 45 |
+
# matches teriflunomide, whose alkene is an enol stabilised by an adjacent nitrile
|
| 46 |
+
# and hydroxyl - an approved drug, and not an electrophile.
|
| 47 |
+
("acrylamide", "[CH2X3]=[CHX3][CX3](=O)[NX3]"),
|
| 48 |
+
("vinyl sulfone", "[CX3]=[CX3][SX4](=O)(=O)"),
|
| 49 |
+
("aldehyde", "[CX3H1](=O)[#6]"),
|
| 50 |
+
("epoxide", "[OX2r3]1[#6r3][#6r3]1"),
|
| 51 |
+
("aziridine", "[NX3r3]1[#6r3][#6r3]1"),
|
| 52 |
+
("haloacetamide", "[NX3][CX3](=O)[CH2][F,Cl,Br,I]"),
|
| 53 |
+
("boronic acid", "[BX3]([OX2H1])[OX2H1]"),
|
| 54 |
+
("alpha-keto amide", "[CX3](=O)[CX3](=O)[NX3]"),
|
| 55 |
+
]
|
| 56 |
+
_WARHEADS = [(n, Chem.MolFromSmarts(s)) for n, s in WARHEADS]
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def canonical(text):
|
| 60 |
+
"""Accept SMILES or InChI. A molecular formula cannot be accepted - too many isomers."""
|
| 61 |
+
t = (text or "").strip()
|
| 62 |
+
if not t:
|
| 63 |
+
return None, "λΉ μ
λ ₯"
|
| 64 |
+
m = Chem.MolFromInchi(t) if t.upper().startswith("INCHI=") else Chem.MolFromSmiles(t)
|
| 65 |
+
if m is None:
|
| 66 |
+
return None, "ꡬ쑰λ₯Ό ν΄μν μ μμ΅λλ€ (SMILES λλ InChIλ‘ μ μΆνμΈμ; λΆμμμ μ΄μ±μ§μ²΄κ° λ§μ νκ° λΆκ°)"
|
| 67 |
+
return m, None
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def check(text, ames=None, logs=None, known_inchikeys=(), covalent_rule=True):
|
| 71 |
+
"""Return a verdict dict. `ames`/`logs` come from the ADMET engine; omit them and
|
| 72 |
+
those two gates are simply reported as not evaluated rather than silently passed.
|
| 73 |
+
|
| 74 |
+
`covalent_rule` is target-scoped on purpose. Excluding covalent binders makes sense
|
| 75 |
+
for Mpro, where warheads react with a catalytic cysteine and a docking score can be
|
| 76 |
+
gamed by raw reactivity. PfDHODH is not inhibited that way, so for Season 1 the rule
|
| 77 |
+
buys nothing and only creates false rejections - two approved drugs were wrongly
|
| 78 |
+
turned away here before the patterns were tightened."""
|
| 79 |
+
m, err = canonical(text)
|
| 80 |
+
if m is None:
|
| 81 |
+
return {"admitted": False, "reject": [err], "relegate": []}
|
| 82 |
+
|
| 83 |
+
smi = Chem.MolToSmiles(m)
|
| 84 |
+
key = Chem.MolToInchiKey(m)
|
| 85 |
+
mw = Descriptors.MolWt(m)
|
| 86 |
+
heavy = m.GetNumHeavyAtoms()
|
| 87 |
+
|
| 88 |
+
reject, relegate, notes = [], [], []
|
| 89 |
+
|
| 90 |
+
for name, patt in (_WARHEADS if covalent_rule else []):
|
| 91 |
+
if patt is not None and m.HasSubstructMatch(patt):
|
| 92 |
+
reject.append("곡μ κ²°ν© warhead κ²μΆ (%s) β Season 1μ λΉκ³΅μ νΈλμ
λλ€" % name)
|
| 93 |
+
break
|
| 94 |
+
|
| 95 |
+
if mw > MW_MAX:
|
| 96 |
+
reject.append("λΆμλ %.1f > %.0f" % (mw, MW_MAX))
|
| 97 |
+
if heavy > HEAVY_MAX:
|
| 98 |
+
reject.append("λ¬΄κ±°μ΄ μμ %d > %d" % (heavy, HEAVY_MAX))
|
| 99 |
+
|
| 100 |
+
hits = _PAINS.GetMatches(m)
|
| 101 |
+
if hits:
|
| 102 |
+
reject.append("PAINS ꡬ쑰 (%s)" % ", ".join(h.GetDescription() for h in hits)[:80])
|
| 103 |
+
|
| 104 |
+
if key in set(known_inchikeys):
|
| 105 |
+
reject.append("κΈ°μ‘΄ μ μΆ λλ κΈ°μ€λ¬Όμ§κ³Ό λμΌν ꡬ쑰μ
λλ€")
|
| 106 |
+
|
| 107 |
+
# ADMET does not reject. It relegates: full score, shown, but below every clean entry.
|
| 108 |
+
if ames is None:
|
| 109 |
+
notes.append("Ames λ―Ένκ°")
|
| 110 |
+
elif ames > AMES_MAX:
|
| 111 |
+
relegate.append("λ³μ΄μμ±(Ames) %.3f > %.2f" % (ames, AMES_MAX))
|
| 112 |
+
if logs is None:
|
| 113 |
+
notes.append("μ©ν΄λ λ―Ένκ°")
|
| 114 |
+
elif logs < LOGS_MIN:
|
| 115 |
+
relegate.append("κ·Ήλ¨μ λΆμ©μ± (logS %.2f < %.1f)" % (logs, LOGS_MIN))
|
| 116 |
+
|
| 117 |
+
return {"admitted": not reject, "reject": reject, "relegate": relegate,
|
| 118 |
+
"notes": notes, "smiles": smi, "inchikey": key,
|
| 119 |
+
"mw": round(mw, 2), "heavy_atoms": heavy}
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
if __name__ == "__main__":
|
| 123 |
+
import json, os, sys
|
| 124 |
+
# the Windows console defaults to cp949 here and dies on the em-dash in a reject reason
|
| 125 |
+
try:
|
| 126 |
+
sys.stdout.reconfigure(encoding="utf-8")
|
| 127 |
+
except Exception:
|
| 128 |
+
pass
|
| 129 |
+
A = json.load(open(os.path.join("data", "anchors.json"), encoding="utf-8"))
|
| 130 |
+
print("%-30s %-9s %-7s %-6s %s" % ("compound", "admitted", "MW", "heavy", "reason"))
|
| 131 |
+
print("-" * 100)
|
| 132 |
+
for a in A["anchors"]:
|
| 133 |
+
ad = a.get("measured_admet", {})
|
| 134 |
+
v = check(a["smiles"], ames=ad.get("ames"), logs=ad.get("solubility-aqsoldb"))
|
| 135 |
+
why = "; ".join(v["reject"] + ["[κ°λ±] " + r for r in v["relegate"]]) or "-"
|
| 136 |
+
print("%-30s %-9s %-7s %-6s %s"
|
| 137 |
+
% (a["label"][:29], "PASS" if v["admitted"] else "REJECT",
|
| 138 |
+
v.get("mw"), v.get("heavy_atoms"), why[:60]))
|
index.html
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<meta charset="utf-8">
|
| 3 |
+
<title>Open Discovery Challenge #1 Malaria</title>
|
| 4 |
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 5 |
+
<style>
|
| 6 |
+
:root{
|
| 7 |
+
--bg:#0e1116; --panel:#161b23; --line:#252c38; --ink:#e6edf5; --muted:#8b98ab;
|
| 8 |
+
--accent:#2fe7d6; --anchor:#f0b429; --bad:#e5534b; --ok:#3fb950;
|
| 9 |
+
}
|
| 10 |
+
*{box-sizing:border-box}
|
| 11 |
+
body{margin:0;background:var(--bg);color:var(--ink);
|
| 12 |
+
font:15px/1.6 -apple-system,"Segoe UI",Roboto,"Malgun Gothic",sans-serif}
|
| 13 |
+
.wrap{max-width:1120px;margin:0 auto;padding:28px 18px 80px}
|
| 14 |
+
header{border-bottom:1px solid var(--line);padding-bottom:18px;margin-bottom:26px}
|
| 15 |
+
h1{margin:0 0 4px;font-size:26px;letter-spacing:-.2px}
|
| 16 |
+
h1 small{color:var(--accent);font-size:13px;letter-spacing:2px;display:block;
|
| 17 |
+
text-transform:uppercase;margin-bottom:6px;font-weight:600}
|
| 18 |
+
.sub{color:var(--muted);font-size:13.5px;margin:0}
|
| 19 |
+
.grid{display:grid;grid-template-columns:1fr 340px;gap:22px;align-items:start}
|
| 20 |
+
@media(max-width:900px){.grid{grid-template-columns:1fr}}
|
| 21 |
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px 18px}
|
| 22 |
+
.card h2{margin:0 0 12px;font-size:14px;letter-spacing:1.2px;text-transform:uppercase;color:var(--muted)}
|
| 23 |
+
table{width:100%;border-collapse:collapse;font-size:13.5px}
|
| 24 |
+
th{text-align:right;color:var(--muted);font-weight:600;font-size:11px;
|
| 25 |
+
letter-spacing:.6px;text-transform:uppercase;padding:0 8px 8px;border-bottom:1px solid var(--line)}
|
| 26 |
+
th:nth-child(-n+3){text-align:left}
|
| 27 |
+
td{padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.04);text-align:right}
|
| 28 |
+
td:nth-child(-n+3){text-align:left}
|
| 29 |
+
tr.anchor{background:rgba(240,180,41,.07)}
|
| 30 |
+
tr.anchor td{color:#f4d089}
|
| 31 |
+
tr.tier2{opacity:.65}
|
| 32 |
+
.rk{width:38px;font-variant-numeric:tabular-nums;color:var(--muted)}
|
| 33 |
+
.cid{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12.5px}
|
| 34 |
+
.tot{font-weight:700;font-size:15px;font-variant-numeric:tabular-nums}
|
| 35 |
+
.who{color:var(--muted);font-size:12.5px}
|
| 36 |
+
.badge{display:inline-block;font-size:10px;letter-spacing:.6px;padding:2px 6px;
|
| 37 |
+
border-radius:5px;background:rgba(240,180,41,.16);color:var(--anchor);margin-left:6px}
|
| 38 |
+
.badge.rel{background:rgba(229,83,75,.15);color:var(--bad)}
|
| 39 |
+
label{display:block;font-size:12px;color:var(--muted);margin:12px 0 4px;letter-spacing:.4px}
|
| 40 |
+
input,textarea{width:100%;background:#0d1117;border:1px solid var(--line);border-radius:8px;
|
| 41 |
+
color:var(--ink);padding:9px 11px;font:inherit;font-size:13.5px}
|
| 42 |
+
textarea{resize:vertical;min-height:56px}
|
| 43 |
+
button{width:100%;margin-top:16px;background:var(--accent);color:#06231f;border:0;
|
| 44 |
+
border-radius:8px;padding:11px;font:inherit;font-weight:700;cursor:pointer}
|
| 45 |
+
button:disabled{opacity:.5;cursor:default}
|
| 46 |
+
.msg{margin-top:12px;font-size:12.5px;padding:10px 12px;border-radius:8px;display:none}
|
| 47 |
+
.msg.ok{display:block;background:rgba(63,185,80,.12);color:var(--ok)}
|
| 48 |
+
.msg.err{display:block;background:rgba(229,83,75,.12);color:var(--bad)}
|
| 49 |
+
.note{color:var(--muted);font-size:11.5px;line-height:1.65;margin-top:14px;
|
| 50 |
+
border-top:1px solid var(--line);padding-top:12px}
|
| 51 |
+
.wts{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
|
| 52 |
+
.wts span{font-size:11px;color:var(--muted);background:#0d1117;border:1px solid var(--line);
|
| 53 |
+
border-radius:20px;padding:3px 10px}
|
| 54 |
+
</style>
|
| 55 |
+
|
| 56 |
+
<div class="wrap">
|
| 57 |
+
<header>
|
| 58 |
+
<h1><small>Open Discovery Challenge</small>#1 Malaria</h1>
|
| 59 |
+
<p class="sub">
|
| 60 |
+
AIκ° μ μν μ λΆμ ν보λ₯Ό 곡κ°λ κΈ°μ€μΌλ‘ μλ μ±μ νλ κ°λ°©ν μ μ½ λ°κ΅΄ λν.
|
| 61 |
+
νμ μ <b>PfDHODH</b>(λ§λΌλ¦¬μ μμΆ© ν¨μ), μνμ μ <b>μ¬λ DHODH</b>μ
λλ€.
|
| 62 |
+
</p>
|
| 63 |
+
<div class="wts" id="wts"></div>
|
| 64 |
+
</header>
|
| 65 |
+
|
| 66 |
+
<div class="grid">
|
| 67 |
+
<div class="card">
|
| 68 |
+
<h2>μμν <span id="counts" style="float:right;font-size:11px"></span></h2>
|
| 69 |
+
<table>
|
| 70 |
+
<thead><tr>
|
| 71 |
+
<th class="rk">#</th><th>ν보</th><th>μ μΆμ / λͺ¨λΈ</th>
|
| 72 |
+
<th>μ½ν¨</th><th>κ²°ν©</th><th>μ νμ±</th><th>ADMET</th><th>μ κ·</th><th>ν©μ±</th><th>μ΄μ </th>
|
| 73 |
+
</tr></thead>
|
| 74 |
+
<tbody id="rows"><tr><td colspan="10" class="who">λΆλ¬μ€λ μ€β¦</td></tr></tbody>
|
| 75 |
+
</table>
|
| 76 |
+
<div class="note">
|
| 77 |
+
κΈ°μ€λ¬Όμ§(λ
Έλ ν)μ μ μλ₯Ό λ°λ <b>λ±μλ₯Ό κ°μ§ μμ΅λλ€</b>. μ²λλ₯Ό λμΌλ‘ νμΈνμλΌκ³ λ£μ κ²μ
λλ€.<br>
|
| 78 |
+
ꡬ쑰λ μ 체 곡κ°νμ§ μμ΅λλ€. 골격 ν΄μμ λΆμλ ꡬκ°λ§ νμλλ©°, μλ³Έμ λΉκ³΅κ°λ‘ 보κ΄λ©λλ€.<br>
|
| 79 |
+
μ μλ <b>κ³μ° κΈ°λ° ν보 νκ°</b>μ΄λ©° μ€μ ν¨λ₯Β·μμ μ±μ΄λ μΉμΈμ½κ³Όμ μ°μ΄μ λ»νμ§ μμ΅λλ€.
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
<div class="card">
|
| 84 |
+
<h2>μ μΆ</h2>
|
| 85 |
+
<label>λΆμ ꡬ쑰μ (SMILES λλ InChI)</label>
|
| 86 |
+
<input id="structure" placeholder="μ: CC1=NC2=NC(=NN2C(=C1)N...)">
|
| 87 |
+
<label>νμ ID (μ΄λ¦Β·μμΒ·λλ€μ)</label>
|
| 88 |
+
<input id="display_name" placeholder="κ³΅κ° μμνμ νμλ©λλ€">
|
| 89 |
+
<label>μ¬μ© λͺ¨λΈλͺ
(μ ν)</label>
|
| 90 |
+
<input id="model_name" placeholder="μ: anthropic/claude-sonnet, Qwen/β¦, μ§μ μ€κ³">
|
| 91 |
+
<label>μ€κ³ κ·Όκ±° (μ ν)</label>
|
| 92 |
+
<textarea id="rationale" placeholder="μ μ΄ κ΅¬μ‘°μΈμ§ κ°λ¨ν"></textarea>
|
| 93 |
+
<button id="go">μ μΆ</button>
|
| 94 |
+
<div class="msg" id="msg"></div>
|
| 95 |
+
<div class="note">
|
| 96 |
+
λΆμμ(C20H25N3O4 λ±)μ λ°μ§ μμ΅λλ€ β μ΄μ±μ§μ²΄κ° λ무 λ§μ νκ°κ° λΆκ°λ₯ν©λλ€.<br>
|
| 97 |
+
μ μΆ μ¦μ ꡬ쑰 κ²μ¬λ₯Ό κ±°μΉκ³ , μ±μ μ GPU μμ
λκΈ°μ΄μμ μ²λ¦¬λ©λλ€(μ λΆ μμ).
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
<script>
|
| 104 |
+
const $ = s => document.querySelector(s);
|
| 105 |
+
const AX = ["activity","binding","selectivity","admet","novelty","synthesis"];
|
| 106 |
+
const f1 = v => (v==null||v==="") ? "β" : (+v).toFixed(1);
|
| 107 |
+
|
| 108 |
+
async function load(){
|
| 109 |
+
const d = await (await fetch("api/leaderboard")).json();
|
| 110 |
+
$("#wts").innerHTML = Object.entries(d.season.weights)
|
| 111 |
+
.map(([k,v]) => `<span>${k} ${v}</span>`).join("");
|
| 112 |
+
$("#counts").textContent = `μ μΆ ${d.counts.scored} Β· κΈ°μ€λ¬Όμ§ ${d.counts.anchors}`;
|
| 113 |
+
|
| 114 |
+
const tb = $("#rows");
|
| 115 |
+
if(!d.entries.length){ tb.innerHTML = '<tr><td colspan="10" class="who">μμ§ μ±μ λ μ μΆμ΄ μμ΅λλ€.</td></tr>'; return; }
|
| 116 |
+
tb.innerHTML = d.entries.map(e => {
|
| 117 |
+
const a = e.axes || {};
|
| 118 |
+
const cls = [e.is_anchor ? "anchor" : "", e.tier===2 ? "tier2" : ""].join(" ").trim();
|
| 119 |
+
const who = e.is_anchor ? (e.note||"κΈ°μ€λ¬Όμ§")
|
| 120 |
+
: `${e.display_name||"β"}${e.model_name?` <span class="who">Β· ${e.model_name}</span>`:""}`;
|
| 121 |
+
const id = e.is_anchor ? e.candidate_id
|
| 122 |
+
: `<span class="cid">${e.candidate_id}</span>` +
|
| 123 |
+
(e.skeleton?` <span class="who">${e.skeleton}</span>`:"");
|
| 124 |
+
const flag = e.is_anchor ? '<span class="badge">κΈ°μ€</span>'
|
| 125 |
+
: (e.tier===2 ? '<span class="badge rel">κ°λ±</span>' : "");
|
| 126 |
+
return `<tr class="${cls}">
|
| 127 |
+
<td class="rk">${e.rank ?? "β"}</td>
|
| 128 |
+
<td>${id}${flag}</td><td>${who}</td>
|
| 129 |
+
${AX.map(k=>`<td>${f1(a[k])}</td>`).join("")}
|
| 130 |
+
<td class="tot">${f1(e.total)}</td></tr>`;
|
| 131 |
+
}).join("");
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
$("#go").onclick = async () => {
|
| 135 |
+
const btn = $("#go"), msg = $("#msg");
|
| 136 |
+
const body = {
|
| 137 |
+
structure: $("#structure").value.trim(),
|
| 138 |
+
display_name: $("#display_name").value.trim(),
|
| 139 |
+
model_name: $("#model_name").value.trim(),
|
| 140 |
+
rationale: $("#rationale").value.trim(),
|
| 141 |
+
};
|
| 142 |
+
if(!body.structure || !body.display_name){
|
| 143 |
+
msg.className = "msg err"; msg.textContent = "ꡬ쑰μ νμ IDλ νμμ
λλ€."; return;
|
| 144 |
+
}
|
| 145 |
+
btn.disabled = true; msg.className = "msg"; msg.textContent = "";
|
| 146 |
+
try{
|
| 147 |
+
const r = await fetch("api/submit", {method:"POST",
|
| 148 |
+
headers:{"Content-Type":"application/json"}, body: JSON.stringify(body)});
|
| 149 |
+
const d = await r.json();
|
| 150 |
+
if(d.accepted){
|
| 151 |
+
msg.className = "msg ok";
|
| 152 |
+
msg.textContent = `μ μλμμ΅λλ€ Β· ${d.candidate_id} Β· λκΈ° ${d.queue_position}건. ${d.note}`;
|
| 153 |
+
$("#structure").value = "";
|
| 154 |
+
load();
|
| 155 |
+
}else{
|
| 156 |
+
msg.className = "msg err";
|
| 157 |
+
msg.textContent = (d.reasons||["μ μΆμ΄ κ±°λΆλμμ΅λλ€"]).join(" / ");
|
| 158 |
+
}
|
| 159 |
+
}catch(e){
|
| 160 |
+
msg.className = "msg err"; msg.textContent = "μμ² μ€ν¨: " + e;
|
| 161 |
+
}
|
| 162 |
+
btn.disabled = false;
|
| 163 |
+
};
|
| 164 |
+
|
| 165 |
+
load();
|
| 166 |
+
setInterval(load, 20000);
|
| 167 |
+
</script>
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.6
|
| 2 |
+
uvicorn[standard]==0.34.0
|
| 3 |
+
pydantic==2.10.4
|
| 4 |
+
rdkit==2024.9.4
|