pen-stack 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pen_stack/__init__.py +2 -0
- pen_stack/_resources.py +34 -0
- pen_stack/active/__init__.py +20 -0
- pen_stack/active/acquire.py +165 -0
- pen_stack/active/brains.py +74 -0
- pen_stack/active/campaign.py +109 -0
- pen_stack/active/design.py +66 -0
- pen_stack/active/validate.py +104 -0
- pen_stack/adapt/__init__.py +14 -0
- pen_stack/adapt/finetune.py +33 -0
- pen_stack/adapt/ingest.py +86 -0
- pen_stack/adapt/pipeline.py +101 -0
- pen_stack/adapt/recalibrate.py +58 -0
- pen_stack/adapt/report.py +130 -0
- pen_stack/agent/__init__.py +1 -0
- pen_stack/agent/cite.py +175 -0
- pen_stack/agent/co_scientist.py +262 -0
- pen_stack/agent/epistemic.py +102 -0
- pen_stack/agent/guardrails.py +67 -0
- pen_stack/agent/mcp_server.py +215 -0
- pen_stack/agent/orchestrator.py +112 -0
- pen_stack/agent/orchestrator_live.py +56 -0
- pen_stack/agent/pen_agent.py +242 -0
- pen_stack/agent/scope.py +60 -0
- pen_stack/agent/tools.py +130 -0
- pen_stack/api/__init__.py +12 -0
- pen_stack/api/manifest.py +160 -0
- pen_stack/atlas/__init__.py +1 -0
- pen_stack/atlas/atlas.parquet +0 -0
- pen_stack/atlas/build_wtkb.py +80 -0
- pen_stack/atlas/crosslink.py +179 -0
- pen_stack/atlas/expand.py +190 -0
- pen_stack/atlas/guide_design.py +178 -0
- pen_stack/atlas/schema.py +59 -0
- pen_stack/atlas/scorecard.py +134 -0
- pen_stack/atlas/scorecard_v3.parquet +0 -0
- pen_stack/atlas/universe.py +75 -0
- pen_stack/atlas/universe_v3.parquet +0 -0
- pen_stack/atlas/variant_propose.py +155 -0
- pen_stack/atlas/writer_efficiency.py +184 -0
- pen_stack/atlas/writer_predict.py +229 -0
- pen_stack/atlas/writer_recommend.py +170 -0
- pen_stack/atlas/writer_verify.py +167 -0
- pen_stack/atlas/wtkb.parquet +0 -0
- pen_stack/bridge/__init__.py +1 -0
- pen_stack/bridge/activity.py +52 -0
- pen_stack/bridge/cli.py +65 -0
- pen_stack/bridge/fold_qc.py +53 -0
- pen_stack/bridge/guide_qc.py +87 -0
- pen_stack/bridge/ingest.py +139 -0
- pen_stack/bridge/offtarget.py +191 -0
- pen_stack/bridge/offtarget_energetics.py +105 -0
- pen_stack/bridge/ortholog_screen.py +73 -0
- pen_stack/bridge/pipeline.py +83 -0
- pen_stack/build/__init__.py +16 -0
- pen_stack/build/cloudlab.py +74 -0
- pen_stack/build/ingest.py +47 -0
- pen_stack/build/protocol.py +82 -0
- pen_stack/build/simlab.py +30 -0
- pen_stack/cli.py +126 -0
- pen_stack/data/__init__.py +1 -0
- pen_stack/data/encode.py +84 -0
- pen_stack/data/genome.py +71 -0
- pen_stack/data/ingest_chromatin.py +119 -0
- pen_stack/data/ingest_integration.py +112 -0
- pen_stack/data/ingest_safety_annot.py +201 -0
- pen_stack/data/ingest_trip.py +76 -0
- pen_stack/design/__init__.py +14 -0
- pen_stack/design/capsid_generate.py +62 -0
- pen_stack/design/generate.py +70 -0
- pen_stack/design/pareto.py +70 -0
- pen_stack/design/space.py +137 -0
- pen_stack/design/writer_variants.py +121 -0
- pen_stack/env/__init__.py +1 -0
- pen_stack/env/genome_writing_env.py +248 -0
- pen_stack/env/policies.py +94 -0
- pen_stack/graph/__init__.py +21 -0
- pen_stack/graph/build.py +133 -0
- pen_stack/graph/cell_types.py +58 -0
- pen_stack/graph/ingest.py +132 -0
- pen_stack/graph/query.py +148 -0
- pen_stack/graph/schema.py +100 -0
- pen_stack/loop/__init__.py +15 -0
- pen_stack/loop/continual.py +61 -0
- pen_stack/loop/cycle.py +84 -0
- pen_stack/loop/drift.py +41 -0
- pen_stack/mech/__init__.py +1 -0
- pen_stack/mech/classify_atlas.py +71 -0
- pen_stack/mech/pfam_whitelist.yaml +247 -0
- pen_stack/mech/whitelist.py +66 -0
- pen_stack/monitor/__init__.py +1 -0
- pen_stack/monitor/europepmc.py +32 -0
- pen_stack/monitor/run.py +57 -0
- pen_stack/monitor/triage.py +63 -0
- pen_stack/oracles/__init__.py +65 -0
- pen_stack/oracles/affinity.py +116 -0
- pen_stack/oracles/cache.py +53 -0
- pen_stack/oracles/energetics.py +33 -0
- pen_stack/oracles/genome.py +167 -0
- pen_stack/oracles/protein_design.py +136 -0
- pen_stack/oracles/reliability.py +64 -0
- pen_stack/oracles/rna.py +28 -0
- pen_stack/oracles/schema.py +77 -0
- pen_stack/oracles/status.py +123 -0
- pen_stack/oracles/structure.py +42 -0
- pen_stack/oracles/structure_run.py +76 -0
- pen_stack/oracles/vcell.py +74 -0
- pen_stack/planner/__init__.py +1 -0
- pen_stack/planner/ada_risk.py +64 -0
- pen_stack/planner/antipeg_oracle.py +75 -0
- pen_stack/planner/capsid_epitope_oracle.py +135 -0
- pen_stack/planner/cargo.py +56 -0
- pen_stack/planner/cargo_polish.py +146 -0
- pen_stack/planner/chromosome.py +106 -0
- pen_stack/planner/delivery.py +55 -0
- pen_stack/planner/delivery_constraints.py +110 -0
- pen_stack/planner/delivery_immune.py +61 -0
- pen_stack/planner/delivery_immunology.py +222 -0
- pen_stack/planner/delivery_predict.py +196 -0
- pen_stack/planner/delivery_vehicles.py +37 -0
- pen_stack/planner/genotoxicity_oracle.py +112 -0
- pen_stack/planner/immune_mhc2.py +154 -0
- pen_stack/planner/immune_profile.py +292 -0
- pen_stack/planner/innate_sensing.py +135 -0
- pen_stack/planner/multiplex.py +110 -0
- pen_stack/planner/optimize.py +278 -0
- pen_stack/planner/pipeline.py +87 -0
- pen_stack/planner/report.py +26 -0
- pen_stack/planner/router.py +57 -0
- pen_stack/planner/seroprevalence_oracle.py +92 -0
- pen_stack/planner/target_site.py +118 -0
- pen_stack/rag/__init__.py +1 -0
- pen_stack/rag/corpus.py +133 -0
- pen_stack/rag/embed.py +98 -0
- pen_stack/rag/ground.py +131 -0
- pen_stack/rag/index.py +53 -0
- pen_stack/rag/llm.py +215 -0
- pen_stack/rag/qa.py +105 -0
- pen_stack/rag/retrieve.py +48 -0
- pen_stack/rules/__init__.py +9 -0
- pen_stack/rules/evaluators.py +318 -0
- pen_stack/rules/loader.py +31 -0
- pen_stack/rules/schema.py +99 -0
- pen_stack/rules/solver.py +43 -0
- pen_stack/rules/spec.py +78 -0
- pen_stack/safety/__init__.py +21 -0
- pen_stack/safety/audit.py +90 -0
- pen_stack/safety/gate.py +58 -0
- pen_stack/safety/pfam_scan.py +157 -0
- pen_stack/safety/policy.py +69 -0
- pen_stack/safety/redteam.py +71 -0
- pen_stack/safety/registry.py +255 -0
- pen_stack/safety/screen.py +59 -0
- pen_stack/safety/standards.py +141 -0
- pen_stack/score/__init__.py +1 -0
- pen_stack/score/recalibrate.py +77 -0
- pen_stack/score/therapeutic.py +85 -0
- pen_stack/server/__init__.py +1 -0
- pen_stack/server/api.py +647 -0
- pen_stack/spec/__init__.py +18 -0
- pen_stack/spec/clarify.py +42 -0
- pen_stack/spec/extract.py +406 -0
- pen_stack/spec/resolvers/__init__.py +17 -0
- pen_stack/spec/resolvers/cell.py +51 -0
- pen_stack/spec/resolvers/chem.py +32 -0
- pen_stack/spec/resolvers/feature.py +36 -0
- pen_stack/spec/resolvers/gene.py +43 -0
- pen_stack/spec/resolvers/locus.py +29 -0
- pen_stack/spec/resolvers/phenotype.py +37 -0
- pen_stack/spec/satisfy.py +114 -0
- pen_stack/spec/service.py +33 -0
- pen_stack/spec/writespec.py +252 -0
- pen_stack/twin/__init__.py +14 -0
- pen_stack/twin/calibrate.py +61 -0
- pen_stack/twin/data/__init__.py +12 -0
- pen_stack/twin/data/position_effect.py +245 -0
- pen_stack/twin/mechanistic.py +147 -0
- pen_stack/twin/outcome.py +132 -0
- pen_stack/twin/position_effect.py +454 -0
- pen_stack/ui/__init__.py +1 -0
- pen_stack/ui/app.py +713 -0
- pen_stack/validate/__init__.py +1 -0
- pen_stack/validate/adapt_demo.py +69 -0
- pen_stack/validate/agent_eval.py +117 -0
- pen_stack/validate/bench_adversarial_tasks.py +118 -0
- pen_stack/validate/bench_coscientist_tasks.py +60 -0
- pen_stack/validate/bench_graph_tasks.py +64 -0
- pen_stack/validate/bench_rule_tasks.py +84 -0
- pen_stack/validate/bench_trust_tasks.py +92 -0
- pen_stack/validate/bench_writetype_tasks.py +101 -0
- pen_stack/validate/blind_gsh_discovery.py +261 -0
- pen_stack/validate/cargo_directionality.py +57 -0
- pen_stack/validate/closed_loop.py +63 -0
- pen_stack/validate/durability_baselines.py +185 -0
- pen_stack/validate/experiment_design.py +65 -0
- pen_stack/validate/expr_controls.py +39 -0
- pen_stack/validate/forward_hypotheses.py +104 -0
- pen_stack/validate/generative_design.py +62 -0
- pen_stack/validate/guide_qc_demo.py +69 -0
- pen_stack/validate/heldout_celltype_expr.py +32 -0
- pen_stack/validate/immune_calibration.py +133 -0
- pen_stack/validate/intent_specification.py +82 -0
- pen_stack/validate/known_biology_expr.py +38 -0
- pen_stack/validate/offtarget_energetics_eval.py +144 -0
- pen_stack/validate/out_of_scope_refusal.py +82 -0
- pen_stack/validate/outcome_calibration.py +194 -0
- pen_stack/validate/outcome_prediction.py +76 -0
- pen_stack/validate/paper3_benchmark.py +165 -0
- pen_stack/validate/paper4_real_validation.py +144 -0
- pen_stack/validate/paper4_validation.py +82 -0
- pen_stack/validate/protocol_safety.py +62 -0
- pen_stack/validate/safety_screening.py +72 -0
- pen_stack/validate/selective_prediction.py +104 -0
- pen_stack/validate/seq_vs_measured.py +134 -0
- pen_stack/validate/target_site_controls.py +65 -0
- pen_stack/validate/uncertainty_eval.py +244 -0
- pen_stack/validate/ungrounded_baseline.py +234 -0
- pen_stack/validate/within_locus_ranking.py +84 -0
- pen_stack/validate/writer_recovery.py +91 -0
- pen_stack/verify/__init__.py +5 -0
- pen_stack/verify/proof.py +206 -0
- pen_stack/verify/schema.py +53 -0
- pen_stack/verify/service.py +191 -0
- pen_stack/web/__init__.py +18 -0
- pen_stack/web/guide.py +110 -0
- pen_stack/web/llm.py +393 -0
- pen_stack/web/llm_provider.py +119 -0
- pen_stack/web/router.py +119 -0
- pen_stack/web/server.py +96 -0
- pen_stack/web/tools.py +197 -0
- pen_stack/wgenome/__init__.py +1 -0
- pen_stack/wgenome/chromatin_seq.py +83 -0
- pen_stack/wgenome/durability.py +108 -0
- pen_stack/wgenome/export_tracks.py +52 -0
- pen_stack/wgenome/features.py +82 -0
- pen_stack/wgenome/genotoxic_blocklist.py +88 -0
- pen_stack/wgenome/gsh_baseline.py +154 -0
- pen_stack/wgenome/mesh_features.py +61 -0
- pen_stack/wgenome/offtarget_assay.py +80 -0
- pen_stack/wgenome/offtarget_bridge.py +47 -0
- pen_stack/wgenome/offtarget_cast.py +97 -0
- pen_stack/wgenome/offtarget_data.py +148 -0
- pen_stack/wgenome/offtarget_enumerate.py +274 -0
- pen_stack/wgenome/offtarget_integrase.py +155 -0
- pen_stack/wgenome/offtarget_nuclease.py +123 -0
- pen_stack/wgenome/offtarget_paste.py +41 -0
- pen_stack/wgenome/offtarget_predict.py +282 -0
- pen_stack/wgenome/ood.py +135 -0
- pen_stack/wgenome/providers.py +278 -0
- pen_stack/wgenome/safety.py +69 -0
- pen_stack/wgenome/structure3d.py +212 -0
- pen_stack/wgenome/uncertainty.py +250 -0
- pen_stack/wgenome/writability.py +72 -0
- pen_stack-0.1.0.dist-info/METADATA +401 -0
- pen_stack-0.1.0.dist-info/RECORD +259 -0
- pen_stack-0.1.0.dist-info/WHEEL +5 -0
- pen_stack-0.1.0.dist-info/entry_points.txt +3 -0
- pen_stack-0.1.0.dist-info/licenses/LICENSE +21 -0
- pen_stack-0.1.0.dist-info/top_level.txt +1 -0
pen_stack/server/api.py
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
"""PEN-STACK REST API - atlas + cross-link endpoints over FastAPI.
|
|
2
|
+
|
|
3
|
+
Extends the base atlas with the Writer Atlas and the writer<->locus cross-link. Every quantitative
|
|
4
|
+
result is computed by the validated library functions (never guessed); the ``/ask`` route defers numeric
|
|
5
|
+
claims to those tools. Heavy data is loaded lazily so the app boots without the base atlas.
|
|
6
|
+
|
|
7
|
+
Run: ``uvicorn pen_stack.server.api:app --host 0.0.0.0 --port 8000`` (needs the ``server`` extra).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import pandas as pd
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from fastapi import FastAPI, HTTPException, Query, Request
|
|
18
|
+
except ImportError as e: # pragma: no cover - server extra optional
|
|
19
|
+
raise ImportError("FastAPI not installed: pip install 'pen-stack[server]'") from e
|
|
20
|
+
|
|
21
|
+
from pen_stack import __version__
|
|
22
|
+
|
|
23
|
+
_ATLAS = Path(__file__).resolve().parents[1] / "atlas" / "atlas.parquet"
|
|
24
|
+
|
|
25
|
+
app = FastAPI(title="PEN-STACK API", version=__version__,
|
|
26
|
+
description="Open infrastructure for genome writing: Writer Atlas + Writable Genome cross-link.")
|
|
27
|
+
|
|
28
|
+
_DISCLAIMER = ("Decision-support only - predictions are calibrated risk/durability estimates, not "
|
|
29
|
+
"clinical directives. Tier-2/3 reachability is candidate and requires experimental validation.")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _resolve_ct(request: Request, ct: str, cell_type: str | None, allowed_params: set[str]) -> str:
|
|
33
|
+
"""Resolve the cell type from `ct` OR its `cell_type` alias, and REJECT any unrecognized query parameter.
|
|
34
|
+
|
|
35
|
+
Without this, a misnamed cell-type argument (e.g. `?cell_type=hspc` when the endpoint reads `ct`) is silently
|
|
36
|
+
ignored by FastAPI and `ct` falls back to its "k562" default, so the caller gets confidently-wrong K562 data
|
|
37
|
+
labelled coverage=full. We instead accept both names and 422 on any unknown parameter, so the fallback can
|
|
38
|
+
never happen silently. The resolved cell type is validated against the known universe.
|
|
39
|
+
"""
|
|
40
|
+
extra = set(request.query_params) - allowed_params
|
|
41
|
+
if extra:
|
|
42
|
+
raise HTTPException(422, f"unknown query parameter(s) {sorted(extra)}; for the cell type use 'ct' or its "
|
|
43
|
+
f"alias 'cell_type'. Valid parameters: {sorted(allowed_params)}.")
|
|
44
|
+
resolved = (cell_type or ct or "k562").lower()
|
|
45
|
+
known = {c["id"] for c in _CELLTYPES}
|
|
46
|
+
if resolved not in known:
|
|
47
|
+
raise HTTPException(422, f"unknown cell type '{resolved}'; valid cell types: {sorted(known)} "
|
|
48
|
+
f"(only k562/hepg2/hspc have a measured atlas; the others are a data-gated roadmap).")
|
|
49
|
+
return resolved
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _atlas_df() -> pd.DataFrame:
|
|
53
|
+
if not _ATLAS.exists():
|
|
54
|
+
raise HTTPException(503, "atlas.parquet not built")
|
|
55
|
+
return pd.read_parquet(_ATLAS)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _records(df: pd.DataFrame) -> list[dict]:
|
|
59
|
+
"""JSON-safe records from a DataFrame: NaN/inf -> null and numpy scalars -> native (pandas `to_json`
|
|
60
|
+
handles both). Raw `to_dict('records')` leaks non-finite floats, which the JSON encoder rejects (500)."""
|
|
61
|
+
return json.loads(df.to_json(orient="records"))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.get("/health")
|
|
65
|
+
def health():
|
|
66
|
+
"""Liveness + a DATA-PRESENCE check. `atlas_present` is the writer atlas (bundled); `writability_atlas`
|
|
67
|
+
reports the per-cell-type writable-genome atlases that Site Finder / the cell-type dropdown depend on and
|
|
68
|
+
that are provided at runtime (mounted), NOT bundled. Reporting only the writer atlas here once masked a
|
|
69
|
+
deploy that dropped the writability-atlas mount: health stayed green while Site Finder was 100% broken and
|
|
70
|
+
every cell type read 'no atlas'. Surfacing measured_count makes that failure visible (0 = misconfigured)."""
|
|
71
|
+
from pen_stack.atlas.crosslink import writability_path
|
|
72
|
+
measured = []
|
|
73
|
+
for ct in _CELLTYPES:
|
|
74
|
+
try:
|
|
75
|
+
writability_path(ct["id"])
|
|
76
|
+
measured.append(ct["id"])
|
|
77
|
+
except Exception: # noqa: BLE001 - absent atlas is the very thing we are reporting
|
|
78
|
+
pass
|
|
79
|
+
return {"status": "ok", "version": __version__, "atlas_present": _ATLAS.exists(),
|
|
80
|
+
"writability_atlas": {"measured_count": len(measured), "measured": measured,
|
|
81
|
+
"ok": len(measured) > 0}}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.get("/atlas/coverage")
|
|
85
|
+
def atlas_coverage():
|
|
86
|
+
df = _atlas_df()
|
|
87
|
+
cov = (df.groupby("family")
|
|
88
|
+
.agg(n=("representative_system", "size"),
|
|
89
|
+
measured=("confidence", lambda s: int((s == "measured").sum())),
|
|
90
|
+
reachability_tier=("reachability_tier", "first"),
|
|
91
|
+
mechanism=("mechanism_bucket", "first"))
|
|
92
|
+
.reset_index())
|
|
93
|
+
return {"families": int(df["family"].nunique()), "systems": int(len(df)),
|
|
94
|
+
"coverage": cov.to_dict("records"), "disclaimer": _DISCLAIMER}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@app.get("/atlas")
|
|
98
|
+
def atlas(family: str | None = None, limit: int = Query(50, ge=1, le=500)):
|
|
99
|
+
df = _atlas_df()
|
|
100
|
+
if family:
|
|
101
|
+
df = df[df["family"] == family]
|
|
102
|
+
cols = [c for c in ["representative_system", "family", "confidence", "mechanism_bucket",
|
|
103
|
+
"deliv_class", "readiness", "cargo_capacity_bp", "reachability_tier",
|
|
104
|
+
"human_cell_activity"] if c in df.columns]
|
|
105
|
+
return {"n": int(len(df)), "rows": _records(df[cols].head(limit)), "disclaimer": _DISCLAIMER}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.get("/crosslink/writers")
|
|
109
|
+
def crosslink_writers(request: Request, chrom: str, bin: int, ct: str = "k562", cell_type: str | None = None):
|
|
110
|
+
from pen_stack.atlas import crosslink as cl
|
|
111
|
+
ct = _resolve_ct(request, ct, cell_type, {"chrom", "bin", "ct", "cell_type"})
|
|
112
|
+
try:
|
|
113
|
+
w = cl.writers_for_locus(chrom, bin, ct)
|
|
114
|
+
except FileNotFoundError as e:
|
|
115
|
+
raise HTTPException(503, str(e)) from e
|
|
116
|
+
if w.empty:
|
|
117
|
+
return {"locus": f"{chrom}:bin{bin}", "writers": [], "disclaimer": _DISCLAIMER}
|
|
118
|
+
fams = w.groupby("family").size().to_dict()
|
|
119
|
+
return {"locus": f"{chrom}:bin{bin}", "ct": ct,
|
|
120
|
+
"locus_writability": float(w["locus_writability"].iloc[0]),
|
|
121
|
+
"families": {k: int(v) for k, v in fams.items()},
|
|
122
|
+
"n_systems": int(len(w)), "disclaimer": _DISCLAIMER}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@app.get("/crosslink/loci")
|
|
126
|
+
def crosslink_loci(request: Request, family: str, ct: str = "k562", cell_type: str | None = None,
|
|
127
|
+
top: int = Query(20, ge=1, le=200)):
|
|
128
|
+
from pen_stack.atlas import crosslink as cl
|
|
129
|
+
ct = _resolve_ct(request, ct, cell_type, {"family", "ct", "cell_type", "top"})
|
|
130
|
+
try:
|
|
131
|
+
loci = cl.loci_for_writer(family, ct, top=top)
|
|
132
|
+
except FileNotFoundError as e:
|
|
133
|
+
raise HTTPException(503, str(e)) from e
|
|
134
|
+
return {"family": family, "ct": ct, "loci": _records(loci), "disclaimer": _DISCLAIMER}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@app.get("/gene/location")
|
|
138
|
+
def gene_location(gene: str):
|
|
139
|
+
"""The canonical chromosome of a gene (or safe-harbour locus nickname), for the UI's gene/chromosome
|
|
140
|
+
concordance check. found=False when the gene is not in the coordinate table (no fabrication)."""
|
|
141
|
+
from pen_stack.planner.chromosome import canonical_chromosome
|
|
142
|
+
from pen_stack.planner.optimize import gene_region, resolve_gene
|
|
143
|
+
try:
|
|
144
|
+
reg = gene_region(gene)
|
|
145
|
+
except Exception: # noqa: BLE001 - coords table absent -> cannot resolve
|
|
146
|
+
reg = None
|
|
147
|
+
resolved = resolve_gene(gene)
|
|
148
|
+
if reg is None:
|
|
149
|
+
return {"gene": gene, "resolved": resolved, "found": False, "chrom": None}
|
|
150
|
+
return {"gene": gene, "resolved": resolved, "found": True, "chrom": canonical_chromosome(reg[0]),
|
|
151
|
+
"start": int(reg[1]), "end": int(reg[2])}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@app.get("/writable")
|
|
155
|
+
def writable(request: Request, gene: str, ct: str = "k562", cell_type: str | None = None,
|
|
156
|
+
top: int = Query(20, ge=1, le=200)):
|
|
157
|
+
from pen_stack.atlas.crosslink import loci_for_gene
|
|
158
|
+
ct = _resolve_ct(request, ct, cell_type, {"gene", "ct", "cell_type", "top"})
|
|
159
|
+
try:
|
|
160
|
+
g = loci_for_gene(gene, ct)
|
|
161
|
+
except FileNotFoundError as e:
|
|
162
|
+
raise HTTPException(503, str(e)) from e
|
|
163
|
+
cov = next((c["coverage"] for c in _CELLTYPES if c["id"] == ct), "unknown")
|
|
164
|
+
# writability = 0.5*safety + 0.5*p_durable (an additive, decomposable mean -- NOT a product, and there is no
|
|
165
|
+
# separate accessibility axis; chromatin enters as input features to the safety + durability models).
|
|
166
|
+
meta = {"gene": gene, "ct": ct, "coverage": cov, "writability_formula": "0.5*safety + 0.5*p_durable",
|
|
167
|
+
"coverage_note": ("partial chromatin panel for this cell type: durability degrades gracefully over the "
|
|
168
|
+
"missing tracks (still measured, not extrapolated)") if cov == "partial" else None,
|
|
169
|
+
"disclaimer": _DISCLAIMER}
|
|
170
|
+
if g.empty:
|
|
171
|
+
return {**meta, "loci": []}
|
|
172
|
+
cols = ["chrom", "bin", "safety", "p_durable", "writability"]
|
|
173
|
+
return {**meta, "loci": _records(g[cols].head(top))}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# the cell types the Site Finder offers, with their actual coverage. A cell type returns writable loci only when
|
|
177
|
+
# its measured writability atlas (atlas_<ct>.parquet) is actually present; the rest are a data-gated roadmap,
|
|
178
|
+
# never a silently-failing dropdown option.
|
|
179
|
+
_CELLTYPES = [
|
|
180
|
+
{"id": "k562", "label": "K562", "description": "chronic myelogenous leukemia line",
|
|
181
|
+
"coverage": "full", "tracks": "ATAC + histones + TRIP durability + safety"},
|
|
182
|
+
{"id": "hepg2", "label": "HepG2", "description": "hepatocellular carcinoma line",
|
|
183
|
+
"coverage": "full", "tracks": "ATAC + histones + safety (partial TRIP)"},
|
|
184
|
+
{"id": "hspc", "label": "HSPC", "description": "hematopoietic stem and progenitor cells",
|
|
185
|
+
"coverage": "partial", "tracks": "ATAC + expression + genotoxicity; partial histone panel (graceful degradation)"},
|
|
186
|
+
{"id": "h1_hesc", "label": "H1 hESC", "description": "H1 human embryonic stem cells", "coverage": "none", "tracks": ""},
|
|
187
|
+
{"id": "ipsc", "label": "iPSC", "description": "induced pluripotent stem cells", "coverage": "none", "tracks": ""},
|
|
188
|
+
{"id": "cd8_t", "label": "CD8 T", "description": "cytotoxic T lymphocytes", "coverage": "none", "tracks": ""},
|
|
189
|
+
{"id": "pbmc", "label": "PBMC", "description": "peripheral blood mononuclear cells", "coverage": "none", "tracks": ""},
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@app.get("/celltypes", tags=["site finder"])
|
|
194
|
+
def celltypes_endpoint():
|
|
195
|
+
"""Per cell type: whether a MEASURED writability atlas exists (so Site Finder returns real loci) and its
|
|
196
|
+
coverage. Cell types without an atlas are a documented, data-gated roadmap, never a silently-failing option."""
|
|
197
|
+
from pen_stack.atlas.crosslink import writability_path
|
|
198
|
+
out = []
|
|
199
|
+
for ct in _CELLTYPES:
|
|
200
|
+
try:
|
|
201
|
+
writability_path(ct["id"])
|
|
202
|
+
measured = True
|
|
203
|
+
except Exception: # noqa: BLE001
|
|
204
|
+
measured = False
|
|
205
|
+
cov = ct["coverage"] if measured else "none"
|
|
206
|
+
out.append({**ct, "coverage": cov, "measured": measured,
|
|
207
|
+
"note": ct["tracks"] if measured else "no writability atlas built yet (data-gated roadmap)"})
|
|
208
|
+
return {"cell_types": out, "measured_count": sum(c["measured"] for c in out),
|
|
209
|
+
"disclaimer": "Only cell types with a measured writability atlas return loci; the rest are a "
|
|
210
|
+
"data-gated roadmap, never a fabricated or silently-empty result."}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@app.get("/recommend", tags=["writer atlas"])
|
|
214
|
+
def recommend_endpoint(write_type: str = "insertion", cargo_bp: int = Query(2000, ge=1, le=300000), cell_type: str = "K562",
|
|
215
|
+
target_seq: str | None = None, donor_seq: str | None = None,
|
|
216
|
+
top_k: int = Query(8, ge=1, le=30)):
|
|
217
|
+
"""Rank writer families for a write request. KB readiness is the GROUNDED primary ranking;
|
|
218
|
+
each family also carries a CANDIDATE learned efficiency with a trained split-conformal interval, and a
|
|
219
|
+
dependency-free guide / att design when target/donor sequences are supplied. No efficiency is ever
|
|
220
|
+
fabricated for a family the curated dataset never saw (KB-only for those)."""
|
|
221
|
+
from pen_stack.atlas.writer_recommend import WRITE_TYPES, recommend_writers
|
|
222
|
+
if write_type not in WRITE_TYPES:
|
|
223
|
+
raise HTTPException(422, f"unknown write_type {write_type!r}; expected one of {sorted(WRITE_TYPES)}")
|
|
224
|
+
req = {"write_type": write_type, "cargo_bp": cargo_bp, "cell_type": cell_type,
|
|
225
|
+
"target_seq": (target_seq or "").strip() or None, "donor_seq": (donor_seq or "").strip() or None}
|
|
226
|
+
return recommend_writers(req, top_k=top_k)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@app.get("/guide_design", tags=["writer atlas"])
|
|
230
|
+
def guide_design_endpoint(writer_family: str, target_seq: str | None = None,
|
|
231
|
+
donor_seq: str | None = None, integrase: str = "Bxb1"):
|
|
232
|
+
"""Design the targeting component a writer family needs from the published reprogramming rules: the
|
|
233
|
+
IS110/IS621 bridge-RNA loops for a (target, donor) pair, or a prime-editing pegRNA that writes a serine-
|
|
234
|
+
integrase attB at the target. Returned sequences are DESIGN CANDIDATES requiring empirical validation -- no
|
|
235
|
+
activity is claimed, and a documented att is written verbatim or not at all (the site is never fabricated)."""
|
|
236
|
+
from pen_stack.atlas.guide_design import design_guide_for_writer
|
|
237
|
+
return design_guide_for_writer(writer_family, (target_seq or "").strip() or None,
|
|
238
|
+
(donor_seq or "").strip() or None, integrase=integrase)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@app.get("/writer/efficiency", tags=["writer atlas"])
|
|
242
|
+
def writer_efficiency_endpoint():
|
|
243
|
+
"""The curated Writer-Efficiency dataset (real measured integration efficiencies, one row per condition
|
|
244
|
+
with a DOI + verbatim quote) and the held-out Writer-Efficiency-Bench result (validation). The
|
|
245
|
+
pre-registered outcome: the learned predictor beats the KB family-mean baseline on held-out LOCUS (CI excludes
|
|
246
|
+
0) but NOT on held-out FAMILY at this N, so the KB ranking is retained as primary and the predictor ships as a
|
|
247
|
+
candidate advisory."""
|
|
248
|
+
from pen_stack.atlas import writer_efficiency as we
|
|
249
|
+
df = we.human_cell()
|
|
250
|
+
cols = [c for c in ["system", "family", "variant", "cargo_bp", "locus", "cell_type",
|
|
251
|
+
"efficiency_pct", "specificity_pct", "doi", "quote"] if c in df.columns]
|
|
252
|
+
bench = None
|
|
253
|
+
p = _ATLAS.parents[2] / "benchmarks" / "writer_efficiency" / "result.json"
|
|
254
|
+
if p.exists():
|
|
255
|
+
bench = json.loads(p.read_text(encoding="utf-8"))
|
|
256
|
+
return {"dataset_summary": we.provenance_summary(), "records": _records(df[cols]), "benchmark": bench,
|
|
257
|
+
"note": "Measured, DOI-backed integration efficiencies. The bench is the contribution; the "
|
|
258
|
+
"learned predictor is a candidate advisory, not the authoritative ranking."}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@app.get("/writer/variants", tags=["writer atlas"])
|
|
262
|
+
def writer_variants_endpoint(integrase: str | None = None, system: str | None = None):
|
|
263
|
+
"""Variant critique: retrospective recovery of known serine-integrase hyperactive mutants over a frozen
|
|
264
|
+
DOI'd panel (NOT a blind sequence-only predictor), plus the deferral of the blind protein-LM recovery
|
|
265
|
+
(no per-variant fitness endpoint exists, so it abstains rather than fabricate a positive). `system` is accepted
|
|
266
|
+
as an alias for `integrase`; omit both to get the full panel."""
|
|
267
|
+
from pen_stack.design import writer_variants as wv
|
|
268
|
+
target = integrase or system # accept either name; the panel is keyed by serine-integrase
|
|
269
|
+
return {"hyperactive_recovery": wv.hyperactive_recovery(target),
|
|
270
|
+
"blind_lm_recovery": wv.lm_recovery(),
|
|
271
|
+
"panel": wv.hyperactive_panel(target), # honour the integrase filter (was: always the full panel)
|
|
272
|
+
"note": "Retrospective catalogue recovery is real and DOI-backed; the blind LM predictor is deferred "
|
|
273
|
+
"(reported as a known limitation, never a manufactured positive)."}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@app.get("/writer/immune", tags=["writer atlas"])
|
|
277
|
+
def writer_immune_endpoint():
|
|
278
|
+
"""The writer enzyme's immunogenicity as an antigen (writer-as-antigen, surfaced in the Writer Atlas):
|
|
279
|
+
per genome-writer family, the real NetMHCIIpan-4.0 MHC-II/CD4 epitope load + the ADA-risk axis
|
|
280
|
+
(MHC-II density x foreignness, self-tolerance filtered against the human proteome). Read from the committed
|
|
281
|
+
cache - NOT recomputed. Population-level proxy, never a patient-specific magnitude (a known-unknown). The Cas9
|
|
282
|
+
nuclease (an editor, not a large-cargo writer) and the human self control are excluded."""
|
|
283
|
+
from pen_stack.planner.immune_profile import writer_immunogenicity_table
|
|
284
|
+
return {"writers": writer_immunogenicity_table(),
|
|
285
|
+
"method": "NetMHCIIpan-4.0 MHC-II epitope load + ADA risk (MHC-II density x foreignness, self-tolerance "
|
|
286
|
+
"filtered against the human proteome). Population-level proxy from the committed cache; the "
|
|
287
|
+
"realized CD4 response / ADA titer is a known-unknown.",
|
|
288
|
+
"scale": "0-1, higher = lower risk (1 = least presentable / least ADA-driving)",
|
|
289
|
+
"no_fabrication": True}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@app.get("/bridge/design")
|
|
293
|
+
def bridge_design(target: str, donor: str, scaffold: str = "ISCro4_enhanced",
|
|
294
|
+
ct: str | None = None, scan: bool = False):
|
|
295
|
+
"""Bridge-recombinase design + off-target/QC. scan=false by default (genome scan is heavy)."""
|
|
296
|
+
from pen_stack.bridge.pipeline import design_and_assess
|
|
297
|
+
res = design_and_assess(target, donor, scaffold, ct=ct, scan=scan)
|
|
298
|
+
off = res["offtargets"]
|
|
299
|
+
if off.get("scanned") and "table" in off:
|
|
300
|
+
t = off["table"]
|
|
301
|
+
off = {"scanned": True, "n_candidates": off["n_candidates"], "n_exact": off["n_exact"],
|
|
302
|
+
"top": t.head(20).to_dict("records")}
|
|
303
|
+
return {"brna": {k: v for k, v in res["brna"].items() if k != "bridge_sequence"} |
|
|
304
|
+
({"bridge_sequence_len": len(res["brna"]["bridge_sequence"])} if res["brna"].get("available") else {}),
|
|
305
|
+
"qc": res["qc"], "offtargets": off, "disclaimer": res["disclaimer"]}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@app.get("/ask")
|
|
309
|
+
def ask(q: str):
|
|
310
|
+
"""Grounded, cited Q&A. Numeric claims are resolved by tool calls, never guessed."""
|
|
311
|
+
from pen_stack.rag.qa import answer
|
|
312
|
+
return answer(q)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@app.get("/plan")
|
|
316
|
+
def plan(request: Request, gene: str, intent: str,
|
|
317
|
+
cargo_bp: int = Query(2000, ge=1, le=300000, description="donor payload in bp (1..300000)"),
|
|
318
|
+
ct: str = "k562", cell_type: str | None = None, k: int = Query(5, ge=1, le=20)):
|
|
319
|
+
"""Write Planner: goal + edit_intent -> ranked, traceable plans. cargo_bp is bounded to a real
|
|
320
|
+
payload range (1..300 kb); out-of-range values are a 422, never a silently-'valid' plan."""
|
|
321
|
+
from pen_stack.planner.optimize import EditIntent
|
|
322
|
+
from pen_stack.planner.pipeline import plan_write
|
|
323
|
+
ct = _resolve_ct(request, ct, cell_type, {"gene", "intent", "cargo_bp", "ct", "cell_type", "k"})
|
|
324
|
+
try:
|
|
325
|
+
intent_e = EditIntent(intent)
|
|
326
|
+
except ValueError as e:
|
|
327
|
+
raise HTTPException(422, f"unknown edit_intent: {intent}") from e
|
|
328
|
+
try:
|
|
329
|
+
plans = plan_write(gene, intent_e, cargo_bp, ct, k=k)
|
|
330
|
+
except FileNotFoundError as e:
|
|
331
|
+
raise HTTPException(503, str(e)) from e
|
|
332
|
+
cov = next((c["coverage"] for c in _CELLTYPES if c["id"] == ct), "unknown")
|
|
333
|
+
return {"gene": gene, "intent": intent, "ct": ct, "coverage": cov, "n": len(plans), "plans": plans,
|
|
334
|
+
"disclaimer": _DISCLAIMER}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@app.post("/verify")
|
|
338
|
+
def verify_endpoint(design: dict):
|
|
339
|
+
"""Verification service: submit a proposed genomic write, get back a structured Verdict,
|
|
340
|
+
legality + named rejections + calibrated confidence + epistemic status + scope flags. Legality and
|
|
341
|
+
confidence are distinct axes. A `question` key (optional) is checked against the known-unknowns registry."""
|
|
342
|
+
from pen_stack.verify import verify
|
|
343
|
+
return verify(design).model_dump()
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@app.post("/verify/proof")
|
|
347
|
+
def verify_proof_endpoint(design: dict):
|
|
348
|
+
"""Verification service: the repair-oriented proof object. Returns the three axes
|
|
349
|
+
(legality, confidence, biosecurity) reported separately, each with a status, the rule or signature that
|
|
350
|
+
fired, evidence, and a repair hint; the collapsed verdict is None. An agent repairs a failed design from
|
|
351
|
+
the legality axis's repair hint. A `question` key (optional) is checked against the known-unknowns."""
|
|
352
|
+
from pen_stack.verify.proof import verify_proof
|
|
353
|
+
return verify_proof(design).model_dump()
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@app.post("/graph/query")
|
|
357
|
+
def graph_query_endpoint(q: dict):
|
|
358
|
+
"""World-model graph: multi-hop query. Body: {locus, cargo_form?}. Returns writers that
|
|
359
|
+
reach the locus AND are deliverable by a cargo-form-compatible vehicle, each with its provenanced path."""
|
|
360
|
+
from pen_stack.graph import writers_reaching_and_deliverable
|
|
361
|
+
return writers_reaching_and_deliverable(q.get("locus"), cargo_form=q.get("cargo_form"))
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# ======================================================================================
|
|
365
|
+
# The AI Integration Surface: the self-describing contract + the engine tool routes.
|
|
366
|
+
# An external agent fetches /capabilities + /scope and ROUTES on them, then calls the tools.
|
|
367
|
+
# ======================================================================================
|
|
368
|
+
@app.get("/capabilities", tags=["AI surface"])
|
|
369
|
+
def capabilities_endpoint():
|
|
370
|
+
"""Machine-readable: WHAT PEN-STACK can do (tools, inputs, outputs, stability). Route on this, not prose."""
|
|
371
|
+
from pen_stack.api.manifest import capability_manifest
|
|
372
|
+
return capability_manifest()
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
@app.get("/scope", tags=["AI surface"])
|
|
376
|
+
def scope_endpoint():
|
|
377
|
+
"""Machine-readable: WHAT PEN-STACK REFUSES to answer (known-unknowns + oracle scope cards). The contract
|
|
378
|
+
that makes depending on PEN-STACK safe: outputs outside scope are out_of_scope/extrapolating, never asserted."""
|
|
379
|
+
from pen_stack.api.manifest import scope_manifest
|
|
380
|
+
return scope_manifest()
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@app.get("/oracles", tags=["live oracles"])
|
|
384
|
+
def oracles_endpoint(probe: bool = False):
|
|
385
|
+
"""Per-foundation-model EXECUTION + LATENCY CLASS + live status (the 'tell the user the cost up front'
|
|
386
|
+
surface). `?probe=true` pings the local GPU model servers. Live oracles answer in seconds, ~2 min; held cloud
|
|
387
|
+
jobs (AF3/Boltz/Chai/Protenix) run separately and never block; deferred outcomes are never fabricated."""
|
|
388
|
+
from pen_stack.oracles.status import oracle_status, summary
|
|
389
|
+
return {"summary": summary(), "oracles": oracle_status(probe=bool(probe))}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@app.post("/safety", tags=["AI surface"])
|
|
393
|
+
def safety_endpoint(design: dict):
|
|
394
|
+
"""Guardian: biosecurity / dual-use screen -> SafetyVerdict (clear/flag/escalate/refuse) + reason.
|
|
395
|
+
Additive: also carries ``standards``, the SafetyVerdict expressed in the community-standard
|
|
396
|
+
vocabulary (Common Mechanism ScreenStatus, SecureDNA outcome) for this specific decision, an in-design
|
|
397
|
+
concordance, not a certification (see /safety/concordance for the labelled-probe-set benchmark)."""
|
|
398
|
+
from pen_stack.safety import safety_gate
|
|
399
|
+
from pen_stack.safety.standards import align_to_common_mechanism
|
|
400
|
+
sv = safety_gate(design, actor=str(design.get("actor", "api")))
|
|
401
|
+
return {**sv.model_dump(), "standards": align_to_common_mechanism(sv)}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@app.get("/safety/concordance", tags=["AI surface"])
|
|
405
|
+
def safety_concordance_endpoint():
|
|
406
|
+
"""Standards concordance: run the Guardian over the committed labelled probe set
|
|
407
|
+
(configs/safety/probes.yaml) and report, verbatim, its concordance with the Common Mechanism's expected
|
|
408
|
+
ScreenStatus (Pass for benign, Warning/Flag for a hazard). A concordance, not a certification."""
|
|
409
|
+
from pen_stack.safety.standards import concordance_report
|
|
410
|
+
return concordance_report()
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@app.post("/immune", tags=["AI surface"])
|
|
414
|
+
def immune_endpoint(design: dict):
|
|
415
|
+
"""Immune-risk profile: per-axis screen (never collapsed; collapsed_score is None)."""
|
|
416
|
+
from pen_stack.planner.immune_profile import immune_profile
|
|
417
|
+
return immune_profile(design)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@app.post("/offtarget", tags=["off-target"])
|
|
421
|
+
def offtarget_endpoint(req: dict):
|
|
422
|
+
"""Cross-family off-target NOMINATION (NOT a clearance). Body:
|
|
423
|
+
{writer_family, guide?, enzyme?, max_mismatch?, candidate_sites?, sequence?, accessibility?, assay?}.
|
|
424
|
+
|
|
425
|
+
(finder): for a nuclease guide with NO ``candidate_sites``, enumerates the genome-wide off-target set
|
|
426
|
+
over GRCh38 (Cas-OFFinder, replayed from the committed cache) and ranks it by the real CRISOT-Score +
|
|
427
|
+
mismatch-calibrated risk + chromatin annotation - the default. Supplying ``candidate_sites`` keeps the
|
|
428
|
+
score-my-candidates path. Abstains (never fabricates) for a novel guide with no VM scan."""
|
|
429
|
+
from pen_stack.wgenome.offtarget_predict import nominate_offtargets
|
|
430
|
+
return nominate_offtargets(
|
|
431
|
+
req.get("writer_family", ""), guide=req.get("guide"), candidate_sites=req.get("candidate_sites"),
|
|
432
|
+
sequence=req.get("sequence"), accessibility=req.get("accessibility"),
|
|
433
|
+
target_core=req.get("target_core"), assay=req.get("assay", "guideseq"),
|
|
434
|
+
enzyme=req.get("enzyme") or req.get("system"), max_mismatch=int(req.get("max_mismatch", 5)))
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@app.get("/offtarget/assay", tags=["off-target"])
|
|
438
|
+
def offtarget_assay_endpoint(writer_family: str):
|
|
439
|
+
"""Validation-assay recommendation for a writer family (the assay that would confirm a nomination)."""
|
|
440
|
+
from pen_stack.wgenome.offtarget_assay import recommend_assay
|
|
441
|
+
return recommend_assay(writer_family)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@app.get("/offtarget/enumerated", tags=["off-target"])
|
|
445
|
+
def offtarget_enumerated_endpoint():
|
|
446
|
+
"""The guides whose genome-wide off-target enumeration is CACHED (so the finder works here without a VM
|
|
447
|
+
scan). A novel guide abstains (its scan runs on the VM). Enumerated coordinates are public-genome facts."""
|
|
448
|
+
from pen_stack.wgenome.offtarget_data import CANONICAL_GUIDES
|
|
449
|
+
from pen_stack.wgenome.offtarget_enumerate import enumerated_guides
|
|
450
|
+
cached = enumerated_guides()
|
|
451
|
+
seq2name = {v: k for k, v in CANONICAL_GUIDES.items()}
|
|
452
|
+
return {"enzyme": "SpCas9",
|
|
453
|
+
"guides": [{"guide": g, "name": seq2name.get(g, g), "enzyme": e} for e, g in cached],
|
|
454
|
+
"note": ("genome-wide enumeration for these guides is replayed from the committed GRCh38 Cas-OFFinder "
|
|
455
|
+
"cache; a novel guide requires an on-VM scan (the finder abstains rather than fabricate).")}
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
@app.get("/campaign", tags=["closed-loop"])
|
|
459
|
+
def campaign_endpoint():
|
|
460
|
+
"""The validation-campaign engine. Returns the expression-validation campaign: the next batch of
|
|
461
|
+
(cassette x locus x cell type) measurements ordered by expected information gain, the calibrate_axis gate it
|
|
462
|
+
targets, and the active-vs-random result (reported verbatim). Cloud-lab-executable; Level 3, human in control;
|
|
463
|
+
the experiments are candidates, the wet run is the standing bottleneck."""
|
|
464
|
+
from pen_stack.active.campaign import design_campaign
|
|
465
|
+
return design_campaign()
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@app.post("/cloudlab", tags=["closed-loop"])
|
|
469
|
+
def cloudlab_endpoint(req: dict):
|
|
470
|
+
"""Safety-gated cloud-lab submission. Body: {design, experiment?, provider?, actor?}. The
|
|
471
|
+
biosecurity gate runs BEFORE submission; a flagged design returns a structured refusal (blocked=True) and NO
|
|
472
|
+
protocol is emitted. A cleared design returns a mock / dry-run job receipt (a real run needs a partner).
|
|
473
|
+
|
|
474
|
+
An unknown provider is a 422 (not a 500): `submit()` raises CloudLabError for a provider outside the
|
|
475
|
+
recognised set, which submit_gated does NOT catch (it only wraps the safety refusal), so validate it here.
|
|
476
|
+
A null/empty provider falls back to the only wired provider, `mock`."""
|
|
477
|
+
from pen_stack.build.cloudlab import CloudLabError, submit_gated
|
|
478
|
+
provider = req.get("provider") or "mock" # null / "" -> the wired mock/dry-run path
|
|
479
|
+
try:
|
|
480
|
+
return submit_gated(req.get("design", {}), req.get("experiment", {}),
|
|
481
|
+
provider=provider, actor=str(req.get("actor", "api")))
|
|
482
|
+
except CloudLabError as e:
|
|
483
|
+
raise HTTPException(422, str(e)) from e
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
@app.get("/brains", tags=["closed-loop"])
|
|
487
|
+
def brains_endpoint():
|
|
488
|
+
"""Benchmark the EIG/VOI experiment designer against the public SDL optimizers (BayBE / Atlas),
|
|
489
|
+
reported verbatim with both cited (a win is not required; the result is falsifiable)."""
|
|
490
|
+
from pen_stack.active.brains import benchmark
|
|
491
|
+
return benchmark()
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
@app.post("/writespec", tags=["writespec"])
|
|
495
|
+
def writespec_endpoint(req: dict):
|
|
496
|
+
"""Parse a plain-language genome-writing request into a typed, ontology-backed WriteSpec.
|
|
497
|
+
Body: {prose, overrides?, check_feasibility?}. Returns the typed spec (with per-field provenance), the
|
|
498
|
+
assumptions behind every inferred field, clarifying questions for anything underspecified, the unresolved
|
|
499
|
+
terms (kept null, never invented), the downstream design adapter, and the feasibility verdict. A WriteSpec is
|
|
500
|
+
a REQUEST, not a claim; the extractor never fabricates intent."""
|
|
501
|
+
from pen_stack.spec.service import parse_request
|
|
502
|
+
return parse_request(req.get("prose", ""), overrides=req.get("overrides"),
|
|
503
|
+
check_feasibility=bool(req.get("check_feasibility", True)))
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
@app.post("/oracle/affinity", tags=["oracle"])
|
|
507
|
+
def oracle_affinity_endpoint(req: dict):
|
|
508
|
+
"""Protein-ligand binding-affinity (Boltz-2 head) under the oracle contract. Body:
|
|
509
|
+
{protein_seq, ligand_smiles, pair_type?, ligand_name?}. Returns a CANDIDATE affinity (binder probability +
|
|
510
|
+
predicted value) with native uncertainty, cache-or-abstain; protein-protein/protein-DNA pair types are
|
|
511
|
+
flagged extrapolating (the head is protein-ligand only). Never runs the long job on the request path."""
|
|
512
|
+
from pen_stack.oracles.affinity import predict_affinity
|
|
513
|
+
r = predict_affinity(req.get("protein_seq", ""), req.get("ligand_smiles", ""),
|
|
514
|
+
pair_type=req.get("pair_type", "ligand"), ligand_name=req.get("ligand_name"))
|
|
515
|
+
return r.model_dump()
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
@app.post("/delivery", tags=["delivery"])
|
|
519
|
+
def delivery_endpoint(req: dict):
|
|
520
|
+
"""Cross-modality delivery recommender. Body: {cargo_form, cargo_bp?, target_tissue?,
|
|
521
|
+
safety_weight?, in_vivo?}. Returns ranked vehicles + a grounded serotype->tissue tropism prior (approved
|
|
522
|
+
therapies; known-unknown for novel capsids) + the learned capsid-fitness bench. Never fabricates tropism."""
|
|
523
|
+
from pen_stack.planner.delivery_predict import recommend_delivery_plus
|
|
524
|
+
return recommend_delivery_plus(req.get("cargo_form", ""), req.get("cargo_bp"), req.get("target_tissue"),
|
|
525
|
+
safety_weight=float(req.get("safety_weight", 0.5)), in_vivo=req.get("in_vivo"),
|
|
526
|
+
serotype=req.get("serotype"))
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
@app.post("/capsid_fitness", tags=["delivery"])
|
|
530
|
+
def capsid_fitness_endpoint(req: dict):
|
|
531
|
+
"""Learned AAV capsid packaging-fitness for a VP1 sequence (FLIP-AAV-trained). Body: {vp1_sequence}.
|
|
532
|
+
A CANDIDATE for the measured packaging axis, not an in-vivo tropism claim; abstains if the model is absent."""
|
|
533
|
+
from pen_stack.planner.delivery_predict import capsid_fitness
|
|
534
|
+
return capsid_fitness(req.get("vp1_sequence", ""), req.get("vector", "AAV"))
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
@app.post("/capsid/generate", tags=["delivery"])
|
|
538
|
+
def capsid_generate_endpoint(req: dict):
|
|
539
|
+
"""Verify-gated generative AAV capsid candidates: propose VP1 555-595 variants of a WT, score each
|
|
540
|
+
with the learned FLIP-AAV fitness model, and keep only survivors with fitness >= WT (verifier-as-discriminator).
|
|
541
|
+
Body: {wt_vp1, n?, max_mut?, top?}. Candidates are labelled -- packaging fitness ONLY; assembly, in-vivo tropism
|
|
542
|
+
and immunogenicity are NOT claimed (immunogenicity is deferred to the immune profile, run before
|
|
543
|
+
synthesis). Abstains without the model; never fabricates a capsid."""
|
|
544
|
+
from pen_stack.design.capsid_generate import generate_capsid_candidates
|
|
545
|
+
n = max(1, min(int(req.get("n", 200)), 500))
|
|
546
|
+
max_mut = max(1, min(int(req.get("max_mut", 4)), 8))
|
|
547
|
+
top = max(1, min(int(req.get("top", 20)), 50))
|
|
548
|
+
return generate_capsid_candidates(req.get("wt_vp1", ""), n=n, max_mut=max_mut, top=top)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
@app.get("/delivery/tropism", tags=["delivery"])
|
|
552
|
+
def delivery_tropism_endpoint(serotype: str | None = None, target_tissue: str | None = None):
|
|
553
|
+
"""Grounded AAV serotype<->tissue tropism priors (approved therapies). Pass a `serotype` for the tissue(s)
|
|
554
|
+
it reaches (with the approved product + DOI), or a `target_tissue` for the approved serotypes that reach it; a
|
|
555
|
+
known-unknown when there is no approved precedent. Never fabricates a tissue."""
|
|
556
|
+
from pen_stack.planner.delivery_predict import serotype_tropism, serotypes_for_tissue
|
|
557
|
+
if serotype:
|
|
558
|
+
return serotype_tropism(serotype)
|
|
559
|
+
if target_tissue:
|
|
560
|
+
return serotypes_for_tissue(target_tissue)
|
|
561
|
+
raise HTTPException(422, "provide a serotype (-> tissue) or a target_tissue (-> serotypes)")
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
@app.post("/generate", tags=["AI surface"])
|
|
565
|
+
def generate_endpoint(req: dict):
|
|
566
|
+
"""Generative designer: verifier-as-discriminator. Body: {goal?, candidates?, keep?}. Hazardous/illegal
|
|
567
|
+
candidates are discarded; survivors are calibrated + immune-profiled candidates (never asserted to work).
|
|
568
|
+
|
|
569
|
+
The GOAL'S cargo function is screened by the Guardian FIRST, before the vehicle x cargo sweep. A
|
|
570
|
+
hazardous goal (e.g. a furin-cleavage tropism-enhancement or a dominant-negative tumor-suppressor ablation) is
|
|
571
|
+
REFUSED up front and the response carries the explicit safety verdict, so an empty result is correctly
|
|
572
|
+
attributed to a biosecurity refusal (not a silent 'no candidates'). The per-candidate Guardian still runs in
|
|
573
|
+
verify() as defence in depth."""
|
|
574
|
+
from pen_stack.design import generate_designs
|
|
575
|
+
goal = req.get("goal")
|
|
576
|
+
# Guardian pre-screen on the goal's declared cargo function (the artifact, not any free-text justification).
|
|
577
|
+
if isinstance(goal, dict) and any(goal.get(f) for f in ("cargo_function", "cargo_seq")):
|
|
578
|
+
from pen_stack.safety.gate import safety_gate
|
|
579
|
+
screen = {f: goal[f] for f in ("cargo_function", "cargo_seq", "gene", "delivery_vehicle", "in_vivo",
|
|
580
|
+
"delivery_tropism", "replication_competent") if goal.get(f) is not None}
|
|
581
|
+
gv = safety_gate(screen, actor=str(req.get("actor", "api")))
|
|
582
|
+
if gv.decision in ("refuse", "escalate"):
|
|
583
|
+
return {"survivors": [], "refused": True,
|
|
584
|
+
"safety": {"decision": gv.decision, "reason": gv.reason,
|
|
585
|
+
"hits": [{"detail": h.detail, "severity": h.severity, "kind": h.kind}
|
|
586
|
+
for h in gv.hits]},
|
|
587
|
+
"disclaimer": _DISCLAIMER}
|
|
588
|
+
return {"survivors": generate_designs(goal, candidates=req.get("candidates"),
|
|
589
|
+
keep=int(req.get("keep", 25)), actor=str(req.get("actor", "api"))),
|
|
590
|
+
"refused": False, "disclaimer": _DISCLAIMER}
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
@app.post("/predict", tags=["AI surface"])
|
|
594
|
+
def predict_endpoint(req: dict):
|
|
595
|
+
"""Digital twin: calibrated, OOD-gated, phenotype-bounded outcome. Body: {design, cell_state}."""
|
|
596
|
+
from pen_stack.twin import predict_outcome
|
|
597
|
+
return predict_outcome(req["design"], req.get("cell_state", "k562"))
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
@app.get("/twin/promoters", tags=["AI surface"])
|
|
601
|
+
def promoters_endpoint():
|
|
602
|
+
"""The selectable promoter palette (constitutive + tissue-specific, literature strength/context/DOI).
|
|
603
|
+
The Twin's relative-expression estimate reacts to the chosen promoter (design.promoter)."""
|
|
604
|
+
from pen_stack.twin.mechanistic import promoter_palette
|
|
605
|
+
return {"promoters": promoter_palette()}
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
@app.post("/suggest", tags=["AI surface"])
|
|
609
|
+
def suggest_endpoint(req: dict):
|
|
610
|
+
"""Experiment designer: a diverse, informative next-experiment batch. Body: {candidates, cell_state, k?}."""
|
|
611
|
+
from pen_stack.active import select_batch
|
|
612
|
+
return {"batch": select_batch(req["candidates"], req.get("cell_state", "k562"), {},
|
|
613
|
+
k=int(req.get("k", 8))), "disclaimer": _DISCLAIMER}
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
@app.post("/session", tags=["AI surface"])
|
|
617
|
+
def session_endpoint(req: dict):
|
|
618
|
+
"""Co-scientist: drive the full loop. Body: {goal, cell_state, candidates?}. Returns strategies +
|
|
619
|
+
predicted outcomes + per-axis immune profiles + suggested experiments + citations + scope ledger + safety."""
|
|
620
|
+
from pen_stack.agent.co_scientist import co_scientist_session
|
|
621
|
+
return co_scientist_session(req["goal"], req.get("cell_state", "k562"), candidates=req.get("candidates"))
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
# ======================================================================================
|
|
625
|
+
# The Genome-Writing Challenge (read-only surface for the web Challenge page).
|
|
626
|
+
# Public tasks (NO labels) + the PEN-STACK reference submission that anchors the leaderboard. Submissions
|
|
627
|
+
# are Python `predict_fn`s scored offline (`benchmarks/genome_writing_challenge/run.py`), never accepted
|
|
628
|
+
# over HTTP, so these routes only EXPOSE the held-out round and the anchor score.
|
|
629
|
+
# ======================================================================================
|
|
630
|
+
@app.get("/challenge/tasks", tags=["challenge"])
|
|
631
|
+
def challenge_tasks(round_id: str = "2026R1"):
|
|
632
|
+
"""The public inputs of the current held-out round (family + design + instructions; NEVER the label)."""
|
|
633
|
+
from benchmarks.genome_writing_challenge.harness import _round_tasks
|
|
634
|
+
tasks = _round_tasks(round_id)
|
|
635
|
+
return {"round": round_id, "n_tasks": len(tasks),
|
|
636
|
+
"tasks": [{"id": t.id, "family": t.family, "public_input": t.public_input} for t in tasks]}
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
@app.get("/challenge/leaderboard", tags=["challenge"])
|
|
640
|
+
def challenge_leaderboard(round_id: str = "2026R1"):
|
|
641
|
+
"""The leaderboard anchored by the PEN-STACK reference submission (deterministic, non-circular labels,
|
|
642
|
+
no-fabrication audited). External submissions are scored offline and appended here after a round."""
|
|
643
|
+
from benchmarks.genome_writing_challenge.harness import evaluate, reference_submission
|
|
644
|
+
ref = evaluate(reference_submission(), round_id)
|
|
645
|
+
return {"round": round_id, "leaderboard": [ref],
|
|
646
|
+
"rules": {"no_circular_labels": ref["no_circular_labels"],
|
|
647
|
+
"no_fabrication_audited": True, "labels": "validated PEN-STACK verifier / oracles"}}
|