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.
Files changed (259) hide show
  1. pen_stack/__init__.py +2 -0
  2. pen_stack/_resources.py +34 -0
  3. pen_stack/active/__init__.py +20 -0
  4. pen_stack/active/acquire.py +165 -0
  5. pen_stack/active/brains.py +74 -0
  6. pen_stack/active/campaign.py +109 -0
  7. pen_stack/active/design.py +66 -0
  8. pen_stack/active/validate.py +104 -0
  9. pen_stack/adapt/__init__.py +14 -0
  10. pen_stack/adapt/finetune.py +33 -0
  11. pen_stack/adapt/ingest.py +86 -0
  12. pen_stack/adapt/pipeline.py +101 -0
  13. pen_stack/adapt/recalibrate.py +58 -0
  14. pen_stack/adapt/report.py +130 -0
  15. pen_stack/agent/__init__.py +1 -0
  16. pen_stack/agent/cite.py +175 -0
  17. pen_stack/agent/co_scientist.py +262 -0
  18. pen_stack/agent/epistemic.py +102 -0
  19. pen_stack/agent/guardrails.py +67 -0
  20. pen_stack/agent/mcp_server.py +215 -0
  21. pen_stack/agent/orchestrator.py +112 -0
  22. pen_stack/agent/orchestrator_live.py +56 -0
  23. pen_stack/agent/pen_agent.py +242 -0
  24. pen_stack/agent/scope.py +60 -0
  25. pen_stack/agent/tools.py +130 -0
  26. pen_stack/api/__init__.py +12 -0
  27. pen_stack/api/manifest.py +160 -0
  28. pen_stack/atlas/__init__.py +1 -0
  29. pen_stack/atlas/atlas.parquet +0 -0
  30. pen_stack/atlas/build_wtkb.py +80 -0
  31. pen_stack/atlas/crosslink.py +179 -0
  32. pen_stack/atlas/expand.py +190 -0
  33. pen_stack/atlas/guide_design.py +178 -0
  34. pen_stack/atlas/schema.py +59 -0
  35. pen_stack/atlas/scorecard.py +134 -0
  36. pen_stack/atlas/scorecard_v3.parquet +0 -0
  37. pen_stack/atlas/universe.py +75 -0
  38. pen_stack/atlas/universe_v3.parquet +0 -0
  39. pen_stack/atlas/variant_propose.py +155 -0
  40. pen_stack/atlas/writer_efficiency.py +184 -0
  41. pen_stack/atlas/writer_predict.py +229 -0
  42. pen_stack/atlas/writer_recommend.py +170 -0
  43. pen_stack/atlas/writer_verify.py +167 -0
  44. pen_stack/atlas/wtkb.parquet +0 -0
  45. pen_stack/bridge/__init__.py +1 -0
  46. pen_stack/bridge/activity.py +52 -0
  47. pen_stack/bridge/cli.py +65 -0
  48. pen_stack/bridge/fold_qc.py +53 -0
  49. pen_stack/bridge/guide_qc.py +87 -0
  50. pen_stack/bridge/ingest.py +139 -0
  51. pen_stack/bridge/offtarget.py +191 -0
  52. pen_stack/bridge/offtarget_energetics.py +105 -0
  53. pen_stack/bridge/ortholog_screen.py +73 -0
  54. pen_stack/bridge/pipeline.py +83 -0
  55. pen_stack/build/__init__.py +16 -0
  56. pen_stack/build/cloudlab.py +74 -0
  57. pen_stack/build/ingest.py +47 -0
  58. pen_stack/build/protocol.py +82 -0
  59. pen_stack/build/simlab.py +30 -0
  60. pen_stack/cli.py +126 -0
  61. pen_stack/data/__init__.py +1 -0
  62. pen_stack/data/encode.py +84 -0
  63. pen_stack/data/genome.py +71 -0
  64. pen_stack/data/ingest_chromatin.py +119 -0
  65. pen_stack/data/ingest_integration.py +112 -0
  66. pen_stack/data/ingest_safety_annot.py +201 -0
  67. pen_stack/data/ingest_trip.py +76 -0
  68. pen_stack/design/__init__.py +14 -0
  69. pen_stack/design/capsid_generate.py +62 -0
  70. pen_stack/design/generate.py +70 -0
  71. pen_stack/design/pareto.py +70 -0
  72. pen_stack/design/space.py +137 -0
  73. pen_stack/design/writer_variants.py +121 -0
  74. pen_stack/env/__init__.py +1 -0
  75. pen_stack/env/genome_writing_env.py +248 -0
  76. pen_stack/env/policies.py +94 -0
  77. pen_stack/graph/__init__.py +21 -0
  78. pen_stack/graph/build.py +133 -0
  79. pen_stack/graph/cell_types.py +58 -0
  80. pen_stack/graph/ingest.py +132 -0
  81. pen_stack/graph/query.py +148 -0
  82. pen_stack/graph/schema.py +100 -0
  83. pen_stack/loop/__init__.py +15 -0
  84. pen_stack/loop/continual.py +61 -0
  85. pen_stack/loop/cycle.py +84 -0
  86. pen_stack/loop/drift.py +41 -0
  87. pen_stack/mech/__init__.py +1 -0
  88. pen_stack/mech/classify_atlas.py +71 -0
  89. pen_stack/mech/pfam_whitelist.yaml +247 -0
  90. pen_stack/mech/whitelist.py +66 -0
  91. pen_stack/monitor/__init__.py +1 -0
  92. pen_stack/monitor/europepmc.py +32 -0
  93. pen_stack/monitor/run.py +57 -0
  94. pen_stack/monitor/triage.py +63 -0
  95. pen_stack/oracles/__init__.py +65 -0
  96. pen_stack/oracles/affinity.py +116 -0
  97. pen_stack/oracles/cache.py +53 -0
  98. pen_stack/oracles/energetics.py +33 -0
  99. pen_stack/oracles/genome.py +167 -0
  100. pen_stack/oracles/protein_design.py +136 -0
  101. pen_stack/oracles/reliability.py +64 -0
  102. pen_stack/oracles/rna.py +28 -0
  103. pen_stack/oracles/schema.py +77 -0
  104. pen_stack/oracles/status.py +123 -0
  105. pen_stack/oracles/structure.py +42 -0
  106. pen_stack/oracles/structure_run.py +76 -0
  107. pen_stack/oracles/vcell.py +74 -0
  108. pen_stack/planner/__init__.py +1 -0
  109. pen_stack/planner/ada_risk.py +64 -0
  110. pen_stack/planner/antipeg_oracle.py +75 -0
  111. pen_stack/planner/capsid_epitope_oracle.py +135 -0
  112. pen_stack/planner/cargo.py +56 -0
  113. pen_stack/planner/cargo_polish.py +146 -0
  114. pen_stack/planner/chromosome.py +106 -0
  115. pen_stack/planner/delivery.py +55 -0
  116. pen_stack/planner/delivery_constraints.py +110 -0
  117. pen_stack/planner/delivery_immune.py +61 -0
  118. pen_stack/planner/delivery_immunology.py +222 -0
  119. pen_stack/planner/delivery_predict.py +196 -0
  120. pen_stack/planner/delivery_vehicles.py +37 -0
  121. pen_stack/planner/genotoxicity_oracle.py +112 -0
  122. pen_stack/planner/immune_mhc2.py +154 -0
  123. pen_stack/planner/immune_profile.py +292 -0
  124. pen_stack/planner/innate_sensing.py +135 -0
  125. pen_stack/planner/multiplex.py +110 -0
  126. pen_stack/planner/optimize.py +278 -0
  127. pen_stack/planner/pipeline.py +87 -0
  128. pen_stack/planner/report.py +26 -0
  129. pen_stack/planner/router.py +57 -0
  130. pen_stack/planner/seroprevalence_oracle.py +92 -0
  131. pen_stack/planner/target_site.py +118 -0
  132. pen_stack/rag/__init__.py +1 -0
  133. pen_stack/rag/corpus.py +133 -0
  134. pen_stack/rag/embed.py +98 -0
  135. pen_stack/rag/ground.py +131 -0
  136. pen_stack/rag/index.py +53 -0
  137. pen_stack/rag/llm.py +215 -0
  138. pen_stack/rag/qa.py +105 -0
  139. pen_stack/rag/retrieve.py +48 -0
  140. pen_stack/rules/__init__.py +9 -0
  141. pen_stack/rules/evaluators.py +318 -0
  142. pen_stack/rules/loader.py +31 -0
  143. pen_stack/rules/schema.py +99 -0
  144. pen_stack/rules/solver.py +43 -0
  145. pen_stack/rules/spec.py +78 -0
  146. pen_stack/safety/__init__.py +21 -0
  147. pen_stack/safety/audit.py +90 -0
  148. pen_stack/safety/gate.py +58 -0
  149. pen_stack/safety/pfam_scan.py +157 -0
  150. pen_stack/safety/policy.py +69 -0
  151. pen_stack/safety/redteam.py +71 -0
  152. pen_stack/safety/registry.py +255 -0
  153. pen_stack/safety/screen.py +59 -0
  154. pen_stack/safety/standards.py +141 -0
  155. pen_stack/score/__init__.py +1 -0
  156. pen_stack/score/recalibrate.py +77 -0
  157. pen_stack/score/therapeutic.py +85 -0
  158. pen_stack/server/__init__.py +1 -0
  159. pen_stack/server/api.py +647 -0
  160. pen_stack/spec/__init__.py +18 -0
  161. pen_stack/spec/clarify.py +42 -0
  162. pen_stack/spec/extract.py +406 -0
  163. pen_stack/spec/resolvers/__init__.py +17 -0
  164. pen_stack/spec/resolvers/cell.py +51 -0
  165. pen_stack/spec/resolvers/chem.py +32 -0
  166. pen_stack/spec/resolvers/feature.py +36 -0
  167. pen_stack/spec/resolvers/gene.py +43 -0
  168. pen_stack/spec/resolvers/locus.py +29 -0
  169. pen_stack/spec/resolvers/phenotype.py +37 -0
  170. pen_stack/spec/satisfy.py +114 -0
  171. pen_stack/spec/service.py +33 -0
  172. pen_stack/spec/writespec.py +252 -0
  173. pen_stack/twin/__init__.py +14 -0
  174. pen_stack/twin/calibrate.py +61 -0
  175. pen_stack/twin/data/__init__.py +12 -0
  176. pen_stack/twin/data/position_effect.py +245 -0
  177. pen_stack/twin/mechanistic.py +147 -0
  178. pen_stack/twin/outcome.py +132 -0
  179. pen_stack/twin/position_effect.py +454 -0
  180. pen_stack/ui/__init__.py +1 -0
  181. pen_stack/ui/app.py +713 -0
  182. pen_stack/validate/__init__.py +1 -0
  183. pen_stack/validate/adapt_demo.py +69 -0
  184. pen_stack/validate/agent_eval.py +117 -0
  185. pen_stack/validate/bench_adversarial_tasks.py +118 -0
  186. pen_stack/validate/bench_coscientist_tasks.py +60 -0
  187. pen_stack/validate/bench_graph_tasks.py +64 -0
  188. pen_stack/validate/bench_rule_tasks.py +84 -0
  189. pen_stack/validate/bench_trust_tasks.py +92 -0
  190. pen_stack/validate/bench_writetype_tasks.py +101 -0
  191. pen_stack/validate/blind_gsh_discovery.py +261 -0
  192. pen_stack/validate/cargo_directionality.py +57 -0
  193. pen_stack/validate/closed_loop.py +63 -0
  194. pen_stack/validate/durability_baselines.py +185 -0
  195. pen_stack/validate/experiment_design.py +65 -0
  196. pen_stack/validate/expr_controls.py +39 -0
  197. pen_stack/validate/forward_hypotheses.py +104 -0
  198. pen_stack/validate/generative_design.py +62 -0
  199. pen_stack/validate/guide_qc_demo.py +69 -0
  200. pen_stack/validate/heldout_celltype_expr.py +32 -0
  201. pen_stack/validate/immune_calibration.py +133 -0
  202. pen_stack/validate/intent_specification.py +82 -0
  203. pen_stack/validate/known_biology_expr.py +38 -0
  204. pen_stack/validate/offtarget_energetics_eval.py +144 -0
  205. pen_stack/validate/out_of_scope_refusal.py +82 -0
  206. pen_stack/validate/outcome_calibration.py +194 -0
  207. pen_stack/validate/outcome_prediction.py +76 -0
  208. pen_stack/validate/paper3_benchmark.py +165 -0
  209. pen_stack/validate/paper4_real_validation.py +144 -0
  210. pen_stack/validate/paper4_validation.py +82 -0
  211. pen_stack/validate/protocol_safety.py +62 -0
  212. pen_stack/validate/safety_screening.py +72 -0
  213. pen_stack/validate/selective_prediction.py +104 -0
  214. pen_stack/validate/seq_vs_measured.py +134 -0
  215. pen_stack/validate/target_site_controls.py +65 -0
  216. pen_stack/validate/uncertainty_eval.py +244 -0
  217. pen_stack/validate/ungrounded_baseline.py +234 -0
  218. pen_stack/validate/within_locus_ranking.py +84 -0
  219. pen_stack/validate/writer_recovery.py +91 -0
  220. pen_stack/verify/__init__.py +5 -0
  221. pen_stack/verify/proof.py +206 -0
  222. pen_stack/verify/schema.py +53 -0
  223. pen_stack/verify/service.py +191 -0
  224. pen_stack/web/__init__.py +18 -0
  225. pen_stack/web/guide.py +110 -0
  226. pen_stack/web/llm.py +393 -0
  227. pen_stack/web/llm_provider.py +119 -0
  228. pen_stack/web/router.py +119 -0
  229. pen_stack/web/server.py +96 -0
  230. pen_stack/web/tools.py +197 -0
  231. pen_stack/wgenome/__init__.py +1 -0
  232. pen_stack/wgenome/chromatin_seq.py +83 -0
  233. pen_stack/wgenome/durability.py +108 -0
  234. pen_stack/wgenome/export_tracks.py +52 -0
  235. pen_stack/wgenome/features.py +82 -0
  236. pen_stack/wgenome/genotoxic_blocklist.py +88 -0
  237. pen_stack/wgenome/gsh_baseline.py +154 -0
  238. pen_stack/wgenome/mesh_features.py +61 -0
  239. pen_stack/wgenome/offtarget_assay.py +80 -0
  240. pen_stack/wgenome/offtarget_bridge.py +47 -0
  241. pen_stack/wgenome/offtarget_cast.py +97 -0
  242. pen_stack/wgenome/offtarget_data.py +148 -0
  243. pen_stack/wgenome/offtarget_enumerate.py +274 -0
  244. pen_stack/wgenome/offtarget_integrase.py +155 -0
  245. pen_stack/wgenome/offtarget_nuclease.py +123 -0
  246. pen_stack/wgenome/offtarget_paste.py +41 -0
  247. pen_stack/wgenome/offtarget_predict.py +282 -0
  248. pen_stack/wgenome/ood.py +135 -0
  249. pen_stack/wgenome/providers.py +278 -0
  250. pen_stack/wgenome/safety.py +69 -0
  251. pen_stack/wgenome/structure3d.py +212 -0
  252. pen_stack/wgenome/uncertainty.py +250 -0
  253. pen_stack/wgenome/writability.py +72 -0
  254. pen_stack-0.1.0.dist-info/METADATA +401 -0
  255. pen_stack-0.1.0.dist-info/RECORD +259 -0
  256. pen_stack-0.1.0.dist-info/WHEEL +5 -0
  257. pen_stack-0.1.0.dist-info/entry_points.txt +3 -0
  258. pen_stack-0.1.0.dist-info/licenses/LICENSE +21 -0
  259. pen_stack-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,77 @@
1
+ """The oracle contract, one uniform result type for every foundation model.
2
+
3
+ PEN-STACK is the composition + verification layer *on top of* the biomolecular foundation models
4
+ (AlphaGenome, Evo2, AlphaFold3, Boltz-2, Chai-1, Protenix, ESM3, RFdiffusion, ProteinMPNN, ViennaRNA, ...).
5
+ Every oracle, however different internally, answers through ONE contract:
6
+
7
+ predict(...) -> OracleResult{value, provenance(model+version), native_uncertainty, scope}
8
+
9
+ Three invariants are encoded in the type itself:
10
+ 1. **A generative output is a candidate, never a claim**, `candidate=True` outputs cannot enter a claim
11
+ path without passing writer-verification (`as_claim()` raises; a guard test asserts it).
12
+ 2. **One contract for every oracle**, provenance (model+version) and the oracle's *native* uncertainty are
13
+ always carried, never discarded; every call is cache-keyed on inputs+model+version.
14
+ 3. **Scope is explicit**, each result carries the id of its oracle's scope card and an `extrapolating`
15
+ flag; the field's evidence that these models do not generalize to unseen loci is *labelled*, not hidden.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from typing import Any, Literal
20
+
21
+ from pydantic import BaseModel, ConfigDict, Field
22
+
23
+ OutputKind = Literal["claim", "candidate", "baseline"]
24
+
25
+
26
+ class Provenance(BaseModel):
27
+ """Immutable (frozen): once an oracle stamps a result with its model, version, and source, that record
28
+ cannot be edited after the fact. Frozen here as well as on OracleResult, since a frozen OracleResult with
29
+ a mutable Provenance would still let a caller rewrite `model`/`version`/`source` on the nested object."""
30
+ model_config = ConfigDict(frozen=True)
31
+
32
+ model: str # e.g. "boltz-2", "alphagenome", "evo2"
33
+ version: str # pinned model/version string
34
+ source: str = "adapter" # adapter | cache | hosted_api | local_gpu
35
+ cache_key: str | None = None
36
+ extra: dict[str, Any] = Field(default_factory=dict)
37
+
38
+
39
+ class OracleResult(BaseModel):
40
+ """The single result type returned by every oracle adapter.
41
+
42
+ Frozen (immutable) so the scope, provenance, and availability an oracle reports cannot be edited after the
43
+ fact: once an adapter returns a result, no downstream caller (or agent step) can flip `available`,
44
+ `in_scope`, `output_kind`, or `extrapolating` to launder a candidate into a claim or hide extrapolation.
45
+ `as_claim()` therefore *filters* (returns self or raises), it never mutates. Build a changed copy with
46
+ `model_copy(update=...)` if a transform genuinely needs one; that yields a new, separately-provenanced
47
+ result rather than silently rewriting an existing one."""
48
+ model_config = ConfigDict(frozen=True)
49
+
50
+ oracle: str # family: genome | structure | protein_design | rna | energetics
51
+ value: Any | None # the prediction (kind depends on the oracle)
52
+ provenance: Provenance
53
+ native_uncertainty: float | None = None # the oracle's OWN uncertainty (e.g. 1 - pLDDT), surfaced not hidden
54
+ scope_card: str | None = None # id of the scope card stating what this oracle is valid for
55
+ in_scope: bool = True # input falls within the scope card
56
+ extrapolating: bool = False # OOD relative to the oracle's training/validity envelope
57
+ output_kind: OutputKind = "claim" # claim | candidate (generative) | baseline (comparator)
58
+ available: bool = True # backend present + ran (False -> deferred, value may be cached/None)
59
+ cached: bool = False
60
+ note: str | None = None
61
+
62
+ @property
63
+ def is_candidate(self) -> bool:
64
+ return self.output_kind == "candidate"
65
+
66
+ def as_claim(self) -> "OracleResult":
67
+ """Return self for use in a claim path, or RAISE if this is a generative candidate (Principle 1).
68
+
69
+ A generated sequence/structure/backbone (Evo2/ESM3/RFdiffusion/ProteinMPNN) is a *proposal*; it must be
70
+ scored against measured data through writer-verification before any claim. This guard makes the
71
+ pen-assemble lesson (0 validatable de-novo writers) un-bypassable in code."""
72
+ if self.is_candidate:
73
+ raise ValueError(
74
+ f"oracle '{self.provenance.model}' returned a CANDIDATE ({self.oracle}); it cannot enter a "
75
+ "claim path without writer-verification scoring (Principle 1). Call "
76
+ "pen_stack.atlas.writer_verify on it first.")
77
+ return self
@@ -0,0 +1,123 @@
1
+ """Live-oracle status + latency surface (PEN-STACK).
2
+
3
+ Reports, for every foundation-model oracle, HOW it executes, its per-query LATENCY CLASS, and whether it is
4
+ currently LIVE (or why not), so the assistant can tell the user the cost up front ("this runs on the GPU,
5
+ ~1 min" / "this is a long cloud job, run it separately") and never silently blocks. The static map lives in
6
+ `configs/oracles/execution.yaml`; this module adds the runtime liveness check (key present? server up?).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from functools import lru_cache
12
+
13
+ from pen_stack._resources import resource
14
+
15
+
16
+ @lru_cache(maxsize=1)
17
+ def execution_map() -> dict:
18
+ import yaml
19
+ return yaml.safe_load(resource("configs/oracles/execution.yaml").read_text(encoding="utf-8"))["oracles"]
20
+
21
+
22
+ def _net_on() -> bool:
23
+ return os.getenv("PEN_STACK_ORACLE_NET") == "1"
24
+
25
+
26
+ def _alphagenome_key() -> bool:
27
+ if os.getenv("ALPHAGENOME_API_KEY"):
28
+ return True
29
+ try:
30
+ return resource("configs/alphagenome_api_key.txt").exists()
31
+ except Exception: # noqa: BLE001
32
+ return False
33
+
34
+
35
+ def _nvidia_key() -> bool:
36
+ if os.getenv("NVIDIA_API_KEY"):
37
+ return True
38
+ try:
39
+ return resource("configs/nvidia_api_key.txt").exists()
40
+ except Exception: # noqa: BLE001
41
+ return False
42
+
43
+
44
+ _SERVER_ENV = {"proteinmpnn": ("PEN_STACK_PROTEINMPNN_URL", "http://localhost:9011"),
45
+ "esm3": ("PEN_STACK_ESM3_URL", "http://localhost:9012"),
46
+ "rfdiffusion": ("PEN_STACK_RFDIFFUSION_URL", "http://localhost:9013")}
47
+
48
+
49
+ def _server_up(model: str) -> bool:
50
+ env, default = _SERVER_ENV[model]
51
+ url = os.getenv(env, default).rstrip("/")
52
+ try:
53
+ import requests
54
+ return requests.get(f"{url}/health", timeout=2).status_code == 200
55
+ except Exception: # noqa: BLE001
56
+ return False
57
+
58
+
59
+ def _is_live(model: str, card: dict, probe: bool) -> tuple[bool, str]:
60
+ ex = card.get("execution")
61
+ if ex == "in_process":
62
+ return True, "runs in-process"
63
+ if card.get("live") is False or ex in {"deferred", "cloud_a100"}:
64
+ return False, card.get("note", "not active")
65
+ if ex == "hosted_api":
66
+ if not _net_on():
67
+ return False, "set PEN_STACK_ORACLE_NET=1 to enable live calls"
68
+ if model == "alphagenome":
69
+ if not _alphagenome_key():
70
+ return False, "add configs/alphagenome_api_key.txt"
71
+ try:
72
+ from pen_stack.wgenome.providers import package_available
73
+ if not package_available():
74
+ return False, "pip install alphagenome"
75
+ except Exception: # noqa: BLE001
76
+ return False, "alphagenome package unavailable"
77
+ if model == "evo2" and not _nvidia_key():
78
+ return False, "add configs/nvidia_api_key.txt"
79
+ return True, "hosted API ready"
80
+ if ex == "local_gpu":
81
+ if not _net_on():
82
+ return False, "set PEN_STACK_ORACLE_NET=1"
83
+ if probe:
84
+ return (_server_up(model), "server up" if _server_up(model) else "model server not running (start it on demand)")
85
+ return True, "enabled (start the model server on demand)"
86
+ return False, "unknown execution"
87
+
88
+
89
+ def oracle_status(probe: bool = False) -> dict:
90
+ """Per-oracle execution + latency_class + live status + published reliability. `probe=True` pings the local
91
+ model servers (adds a short network check); default is config-level only. Reliability is the wrapped model's
92
+ PUBLISHED benchmark accuracy, reported verbatim with citation, never a claim about this stack's accuracy."""
93
+ try:
94
+ from pen_stack.oracles.reliability import all_reliability
95
+ rel = all_reliability()
96
+ except Exception: # noqa: BLE001
97
+ rel = {}
98
+ out = {}
99
+ for model, card in execution_map().items():
100
+ live, why = _is_live(model, card, probe)
101
+ out[model] = {"execution": card.get("execution"), "latency_class": card.get("latency_class"),
102
+ "live": live, "status": why, "note": card.get("note", ""),
103
+ "server": card.get("server"), "reliability": rel.get(model)}
104
+ return out
105
+
106
+
107
+ def summary() -> dict:
108
+ """Compact roll-up for the capability manifest + the chat meta facts."""
109
+ st = oracle_status(probe=False)
110
+ live = sorted(m for m, s in st.items() if s["live"])
111
+ held = sorted(m for m, s in st.items() if s["execution"] == "cloud_a100")
112
+ deferred = sorted(m for m, s in st.items() if s["execution"] == "deferred")
113
+ out = {"live": live, "held_cloud": held, "deferred": deferred,
114
+ "latency_classes": {m: s["latency_class"] for m, s in st.items()},
115
+ "note": ("Live oracles answer in seconds, ~2 min; held cloud jobs (AF3/Boltz/Chai/Protenix) are run "
116
+ "separately and never block; deferred outcomes are known-unknowns, never fabricated.")}
117
+ try:
118
+ from pen_stack.oracles.reliability import disagreement_widens_monotonically, disclaimer
119
+ out["reliability_note"] = disclaimer()
120
+ out["disagreement_to_interval"] = disagreement_widens_monotonically()
121
+ except Exception: # noqa: BLE001
122
+ pass
123
+ return out
@@ -0,0 +1,42 @@
1
+ """Structure oracles, AlphaFold3 / Boltz-2 / Chai-1 / Protenix, with cross-oracle consistency.
2
+
3
+ Each predictor returns structure confidence (pLDDT-like) under the oracle contract, with the model's native
4
+ uncertainty surfaced (1 − pLDDT). Where redundant oracles are available, `consistency()` combines them: their
5
+ agreement is a confidence signal and **their disagreement widens the reported interval** (Principle 3).
6
+ Heavy backends run on-demand (hosted/local GPU) and are cached; absent → deferred / cache replay.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from pen_stack.oracles import build_result, cache_get, consensus
11
+ from pen_stack.oracles.schema import OracleResult
12
+
13
+ _STRUCT_MODELS = ["alphafold3", "boltz-2", "chai-1", "protenix"]
14
+ _BACKEND = {"alphafold3": "alphafold3", "boltz-2": "boltz", "chai-1": "chai_lab", "protenix": "protenix"}
15
+
16
+
17
+ def predict_structure(sequence: str, model: str = "boltz-2") -> OracleResult:
18
+ """Predict a structure's confidence with one model (pLDDT-like). Deferred / cache-replayed if absent."""
19
+ if model not in _STRUCT_MODELS:
20
+ raise ValueError(f"unknown structure model {model!r}; choose from {_STRUCT_MODELS}")
21
+ inputs = {"seq_len": len(sequence), "seq": sequence.upper(), "model": model}
22
+ r = build_result("structure", model, inputs=inputs, available=False,
23
+ note=f"{model} backend not installed (on-demand hosted/local GPU)")
24
+ hit = cache_get(r.provenance.cache_key)
25
+ if hit is not None:
26
+ return build_result("structure", model, inputs=inputs, value=hit.get("value"),
27
+ native_uncertainty=hit.get("native_uncertainty"), available=True, cached=True,
28
+ source="cache", note="replayed from committed oracle cache")
29
+ try:
30
+ __import__(_BACKEND[model])
31
+ except Exception: # noqa: BLE001
32
+ return r
33
+ return build_result("structure", model, inputs=inputs, available=False,
34
+ note=f"{model} present; wire the predictor for the live pLDDT/PAE value")
35
+
36
+
37
+ def consistency(sequence: str, models: list[str] | None = None) -> OracleResult:
38
+ """Cross-oracle self-consistency over the available structure predictors; divergence widens the interval."""
39
+ models = models or _STRUCT_MODELS
40
+ results = [predict_structure(sequence, m) for m in models]
41
+ out = consensus(results, oracle="structure")
42
+ return out.model_copy(update={"note": (out.note or "") + f" | members tried: {models}"})
@@ -0,0 +1,76 @@
1
+ """Off-request structure-oracle runner over named genome-writing complexes.
2
+
3
+ The structure oracles (AlphaFold3 / Boltz-2 / Chai-1 / Protenix) are held: a full complex prediction is a long
4
+ GPU / cloud batch, so it runs OFF the request path and the result is cached; the request path only ever replays
5
+ the cache or abstains (:func:`pen_stack.oracles.structure.consistency`). This module names the writer-substrate /
6
+ att-site complexes the substrate cares about, exposes a cache-or-abstain :func:`get` over the cross-oracle
7
+ consistency, and an :func:`ingest_boltz_confidence` hook that folds a completed off-request Boltz run's confidence
8
+ (pLDDT / pTM) into the cache so the request path can replay it.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from pathlib import Path
14
+
15
+ from pen_stack.oracles import build_result, cache_put
16
+ from pen_stack.oracles.schema import OracleResult
17
+ from pen_stack.oracles.structure import consistency, predict_structure
18
+
19
+ # Named complexes relevant to genome writing. Sequences are supplied at call time; these entries describe what a
20
+ # held off-request run would target. `kind` records why a structure oracle is or is not the right tool.
21
+ _COMPLEXES = {
22
+ "ert2_4oht": {
23
+ "description": "ERT2 ligand-binding domain (human ESR1, UniProt P03372 res 305-554) with "
24
+ "4-hydroxytamoxifen, the inducible-writer chemical switch",
25
+ "kind": "protein_ligand"},
26
+ "bxb1_attb": {
27
+ "description": "Bxb1 serine integrase engaging its attB DNA target",
28
+ "kind": "protein_dna"},
29
+ "prime_editor_target": {
30
+ "description": "Prime editor (Cas9 nickase-RT fusion) engaging its primed genomic target",
31
+ "kind": "protein_dna"},
32
+ }
33
+
34
+
35
+ def complexes() -> dict:
36
+ """The registry of named writer-substrate / att-site complexes a held structure run would target."""
37
+ return dict(_COMPLEXES)
38
+
39
+
40
+ def get(protein_seq: str, models: list[str] | None = None) -> OracleResult:
41
+ """Cross-oracle structure consistency for a complex's protein, cache-or-abstain (never runs the long job).
42
+
43
+ Replays committed structure-oracle caches and combines them; divergence widens the interval. When no backend
44
+ has been run off-request and cached, the consensus is unavailable (a deferred result), never fabricated.
45
+ """
46
+ return consistency(protein_seq, models)
47
+
48
+
49
+ def single(protein_seq: str, model: str = "boltz-2") -> OracleResult:
50
+ """One structure oracle's confidence for a sequence (cache-replayed if present, else deferred)."""
51
+ return predict_structure(protein_seq, model)
52
+
53
+
54
+ def ingest_boltz_confidence(protein_seq: str, out_dir: str | Path, model: str = "boltz-2") -> str:
55
+ """Fold a completed OFF-REQUEST Boltz run's confidence into the structure cache, keyed as ``predict_structure``.
56
+
57
+ Reads Boltz's ``confidence_*.json`` (``complex_plddt`` on a 0-1 scale) and caches a pLDDT-like value with
58
+ native uncertainty ``1 - pLDDT``, so :func:`predict_structure` (and thus :func:`get`) replays it. Returns the
59
+ cache key. Called after a separate GPU batch completes; never on the request path.
60
+ """
61
+ out = Path(out_dir)
62
+ cj = sorted(p for p in out.rglob("confidence*.json"))
63
+ if not cj:
64
+ raise FileNotFoundError(f"no confidence*.json under {out}")
65
+ d = json.loads(cj[0].read_text(encoding="utf-8"))
66
+ plddt = d.get("complex_plddt")
67
+ if plddt is None:
68
+ raise KeyError(f"complex_plddt absent in {cj[0].name}")
69
+ plddt = float(plddt)
70
+ if plddt > 1.0: # some writers report 0-100; normalise to 0-1
71
+ plddt = plddt / 100.0
72
+ inputs = {"seq_len": len(protein_seq), "seq": protein_seq.upper(), "model": model}
73
+ key = build_result("structure", model, inputs=inputs, available=False).provenance.cache_key
74
+ cache_put(key, {"value": round(plddt, 4), "native_uncertainty": round(1.0 - plddt, 4),
75
+ "ptm": d.get("ptm"), "iptm": d.get("iptm"), "source_file": cj[0].name})
76
+ return key
@@ -0,0 +1,74 @@
1
+ """Virtual-cell outcome oracles, Arc STATE / scGPT under the OracleResult contract.
2
+
3
+ A perturbation-response prediction is a CANDIDATE (a hypothesis), never a claim. It is OOD-gated: a cell context
4
+ or perturbation outside the model's documented validity envelope sets `extrapolating=True` / `in_scope=False`,
5
+ because the field's own evidence (Arc's Virtual Cell Challenge) is that perturbation models do not yet
6
+ consistently beat naive baselines and do not generalize to unseen contexts. Heavy backends run on-demand and are
7
+ cached; absent → deferred / cache replay (the value may be None, never fabricated).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import os
12
+
13
+ from pen_stack.oracles import build_result, cache_get, cache_put
14
+ from pen_stack.oracles.schema import OracleResult
15
+
16
+ # documented in-distribution envelope (representative trained human cell contexts + perturbation kinds).
17
+ _IN_DISTRIBUTION_CONTEXTS = {
18
+ "k562", "hepg2", "jurkat", "hek293", "hek293t", "h1_hesc", "h1", "a549", "thp1", "raw264",
19
+ "hela", "mcf7", "ipsc", "pbmc", "cd4_t", "cd8_t", "hspc",
20
+ }
21
+ _VALID_PERTURBATION_KINDS = {"genetic", "crispr", "knockout", "knockdown", "overexpression",
22
+ "chemical", "drug", "cytokine"}
23
+
24
+
25
+ def _normalize(s) -> str:
26
+ return str(s or "").strip().lower().replace("-", "").replace(" ", "")
27
+
28
+
29
+ def in_distribution(cell_state: str, perturbation: dict) -> bool:
30
+ """True iff the cell context is a documented trained context AND the perturbation kind is supported."""
31
+ ctx_ok = _normalize(cell_state) in {_normalize(c) for c in _IN_DISTRIBUTION_CONTEXTS}
32
+ kind = _normalize(perturbation.get("kind") or perturbation.get("type"))
33
+ kind_ok = (not kind) or kind in {_normalize(k) for k in _VALID_PERTURBATION_KINDS}
34
+ return bool(ctx_ok and kind_ok)
35
+
36
+
37
+ def predict_response(cell_state: str, perturbation: dict, *, model: str = "state",
38
+ live: bool = False) -> OracleResult:
39
+ """Virtual-cell outcome oracle (Arc STATE / scGPT). OOD-gated: a context outside the scope card ->
40
+ `extrapolating`, never a confident claim. Cached/deferred when the backend is absent (replay is CI default)."""
41
+ extrap = not in_distribution(cell_state, perturbation)
42
+ inputs = {"cell_state": cell_state, "perturbation": perturbation, "live": bool(live)}
43
+ key_obj = build_result("vcell", model, inputs=inputs)
44
+ hit = cache_get(key_obj.provenance.cache_key)
45
+ if hit is not None:
46
+ return build_result("vcell", model, inputs=inputs, value=hit.get("value"),
47
+ native_uncertainty=hit.get("native_uncertainty"), available=True, cached=True,
48
+ source="cache", extrapolating=extrap, in_scope=not extrap,
49
+ note="replayed from committed oracle cache")
50
+ # Live hook (uniform with the other oracles): a State *Transition* server can be stood up to predict a
51
+ # perturbation response; if PEN_STACK_VCELL_URL is set + up, use it. No such server ships by default,
52
+ # Arc STATE's SE-600M only EMBEDS cells (needs an scRNA AnnData), while a real perturbation OUTCOME needs
53
+ # the ST model + a reference cell population. So this path normally defers, and we DO NOT fabricate a number.
54
+ url = os.getenv("PEN_STACK_VCELL_URL")
55
+ if os.getenv("PEN_STACK_ORACLE_NET") == "1" and url:
56
+ try:
57
+ import requests
58
+ resp = requests.post(f"{url.rstrip('/')}/predict",
59
+ json={"cell_state": cell_state, "perturbation": perturbation},
60
+ timeout=float(os.getenv("PEN_STACK_MODEL_TIMEOUT", "300"))).json()
61
+ cache_put(key_obj.provenance.cache_key, {"value": resp})
62
+ return build_result("vcell", model, inputs=inputs, value=resp, available=True, source="local_gpu",
63
+ extrapolating=extrap, in_scope=not extrap,
64
+ note="Arc STATE transition server prediction (OOD-gated). Still a candidate hypothesis.")
65
+ except Exception: # noqa: BLE001 - server absent/down → defer
66
+ pass
67
+ # default: deferred (value None), the contract + OOD gate are real; the OUTCOME is a known-unknown
68
+ return build_result("vcell", model, inputs=inputs, value=None, available=False,
69
+ extrapolating=extrap, in_scope=not extrap,
70
+ note=(f"{model} perturbation-OUTCOME deferred; OOD={extrap}. The Arc STATE model is "
71
+ "installable (`pip install arc-state`; SE-600M embeds cells), but a trustworthy "
72
+ "perturbation response needs the State-Transition model + a reference scRNA "
73
+ "population, and even SOTA does not yet consistently beat naive baselines (Arc VCC), "
74
+ "so the magnitude stays a known-unknown rather than a fabricated number."))
@@ -0,0 +1 @@
1
+ """pen_stack.planner - see the PEN-STACK program doc."""
@@ -0,0 +1,64 @@
1
+ """ADA-risk.
2
+
3
+ A protein's MHC-II epitope load (presentation potential) is necessary but not sufficient for anti-drug antibodies
4
+ (ADA): **self** proteins carry MHC-II epitopes too, yet are tolerated (central tolerance deletes self-reactive T
5
+ cells). What drives ADA is **non-self** (foreign) presentable epitopes. So:
6
+
7
+ ADA-risk = MHC-II epitope density (real, NetMHCIIpan-4.0) x foreignness
8
+
9
+ where **foreignness is the protein ORIGIN** (self vs bacterial/viral/phage), the authoritative, definitive
10
+ self/non-self signal (central tolerance), not a heuristic. The **real human-proteome 9-mer self-match** (computed
11
+ on the VM against the full UniProt human reference proteome) is reported as a cross-check. NO heuristic fallback:
12
+ when the MHC-II density is not cached (NetMHCIIpan not run) OR the origin is unknown, ADA-risk **abstains** (an
13
+ known-unknown) rather than guessing.
14
+
15
+ a population-level proxy, never a patient-specific ADA titer (a known-unknown). The calibration
16
+ against a public ADA-incidence set runs through the EXISTING calibrate_axis gate and stays at public-data power.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from pen_stack.planner.immune_mhc2 import _real_cache, mhc2_epitope_load, writer_sequences
21
+
22
+ ADA_DOIS = ["10.1038/s41467-021-25414-9"] # Cas9 MHC-II CD4 immunogenicity (Simhadri 2021)
23
+ _FOREIGN = {"self": 0.0, "foreign": 1.0}
24
+
25
+
26
+ def real_self_match(name: str) -> dict | None:
27
+ """The REAL human-proteome 9-mer self-match for a bundled antigen (cross-check; computed on the VM against the
28
+ full UniProt human reference proteome). Foreign proteins ~0, human ~1. None if not in the cache."""
29
+ rec = (_real_cache().get("self_match") or {}).get(name)
30
+ if rec is None:
31
+ return None
32
+ return {"human_9mer_match_fraction": rec.get("fraction"), "n_9mers": rec.get("n"),
33
+ "reference": (_real_cache().get("self_match_meta") or {}).get("reference",
34
+ "UniProt human reference proteome (full)")}
35
+
36
+
37
+ def ada_risk(seq: str, origin: str | None = None, name: str | None = None) -> dict:
38
+ """ADA-risk = real MHC-II epitope density x foreignness(origin). Abstains (no proxy/heuristic) when the MHC-II
39
+ density is uncached or the origin is unknown. Higher ada_risk_score = MORE ADA risk; ada_immune_score = 1 - it."""
40
+ el = mhc2_epitope_load(seq, name)
41
+ density = el.get("epitope_density")
42
+ foreign = _FOREIGN.get(str(origin)) if origin is not None else None
43
+ sm = real_self_match(name) if name else None
44
+ base = {"epitope_density": density, "foreignness": foreign, "origin": origin,
45
+ "self_match_human_proteome": sm, "dois": ADA_DOIS,
46
+ "filter": "central tolerance: foreign (non-self) MHC-II epitopes drive ADA; self tolerated. Foreignness "
47
+ "= protein origin (authoritative); MHC-II density = NetMHCIIpan-4.0; real human-proteome "
48
+ "self-match reported as cross-check (no heuristic).",
49
+ "status": "population-level; patient ADA titer / magnitude is a known-unknown"}
50
+ if density is None or foreign is None:
51
+ return {**base, "ada_risk_score": None, "ada_immune_score": None, "backend": "abstain",
52
+ "note": ("abstains (no proxy): " + ("NetMHCIIpan-4.0 not run for this sequence" if density is None
53
+ else "protein origin unknown, foreignness is not guessed"))}
54
+ risk = round(density * foreign, 4)
55
+ return {**base, "ada_risk_score": risk, "ada_immune_score": round(1.0 - risk, 4), "backend": "real"}
56
+
57
+
58
+ def ada_risk_named(name: str) -> dict:
59
+ """ADA-risk for a bundled writer/control protein by name (uses its declared origin + the real MHC-II cache)."""
60
+ rec = writer_sequences().get(name)
61
+ if not rec:
62
+ return {"available": False, "note": f"no bundled sequence {name!r}"}
63
+ return {"available": True, "name": name, "family": rec.get("family"),
64
+ **ada_risk(rec["seq"], rec.get("origin"), name=name)}
@@ -0,0 +1,75 @@
1
+ """Anti-PEG immunity axis for PEGylated LNP.
2
+
3
+ Covers pre-existing / induced anti-PEG antibodies, which
4
+ gate RE-DOSING of PEGylated LNP. Anti-PEG is a property of the PEGylation chemistry, not of any encoded
5
+ sequence, so, exactly like the anti-vector seroprevalence oracle, it is grounded in published serosurvey
6
+ DATA (configs/antipeg.yaml, a population prevalence range with DOIs):
7
+
8
+ preexisting_antipeg_score = 1 - midpoint(anti_PEG_prevalence_pct) / 100 # 1 = fewest excluded
9
+
10
+ Answers through the OracleResult contract (output_kind="baseline"). ABSTAINS for non-PEGylated vehicles.
11
+ SCOPE: a POPULATION prevalence (a range, region/age/assay-dependent), NOT a given patient's anti-PEG titer
12
+ (a clinical test, patient-specific -> a known-unknown); induced anti-PEG after dose 1 is a separate, larger
13
+ dynamic (noted, not modelled).
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from functools import lru_cache
18
+
19
+ import yaml
20
+
21
+ from pen_stack._resources import resource
22
+ from pen_stack.oracles.schema import OracleResult, Provenance
23
+
24
+ _SCOPE_CARD = "antipeg"
25
+
26
+
27
+ @lru_cache(maxsize=1)
28
+ def _table() -> dict:
29
+ return yaml.safe_load(resource("configs/antipeg.yaml").read_text(encoding="utf-8"))
30
+
31
+
32
+ def _all_dois() -> list[str]:
33
+ return list((_table().get("prevalence") or {}).get("dois", []) or [])
34
+
35
+
36
+ def _prov(**extra) -> Provenance:
37
+ return Provenance(model="anti_peg_seroprevalence", version=str(_table().get("version", "1.0")),
38
+ source="cache", extra=extra)
39
+
40
+
41
+ def antipeg_oracle(vehicle_name: str, pegylated: bool | None = None) -> OracleResult:
42
+ """Anti-PEG pre-existing immunity for a vehicle, as an OracleResult.
43
+
44
+ - PEGylated vehicle -> preexisting_antipeg_score = 1 - midpoint(prevalence)/100 (gates RE-DOSING).
45
+ - non-PEGylated vehicle -> ABSTAINS (available=False, value=None): the anti-PEG axis is not applicable.
46
+ `pegylated` overrides the config's vehicle list when given. Never fabricates a patient titer."""
47
+ t = _table()
48
+ is_peg = pegylated if pegylated is not None else (vehicle_name in (t.get("pegylated_vehicles") or []))
49
+ if not is_peg:
50
+ return OracleResult(oracle="genome", value=None, provenance=_prov(), scope_card=_SCOPE_CARD,
51
+ in_scope=False, available=False, output_kind="baseline",
52
+ note=f"non-PEGylated vehicle {vehicle_name!r}: anti-PEG axis not applicable (abstains).")
53
+
54
+ rec = t["prevalence"]
55
+ lo, hi = rec["anti_peg_prevalence_pct"]
56
+ mid = (lo + hi) / 2.0
57
+ score = max(0.0, min(1.0, 1.0 - mid / 100.0))
58
+ return OracleResult(
59
+ oracle="genome",
60
+ value={"preexisting_antipeg_score": round(score, 3), "anti_peg_prevalence_pct": [lo, hi],
61
+ "midpoint_pct": mid, "gates": "re-dosing", "dois": rec.get("dois", [])},
62
+ provenance=_prov(dois=rec.get("dois", [])), native_uncertainty=round((hi - lo) / 200.0, 4),
63
+ scope_card=_SCOPE_CARD, in_scope=True, extrapolating=False, output_kind="baseline", available=True,
64
+ note=(f"population anti-PEG prevalence {lo}-{hi}% -> preexisting_antipeg_score={score:.3f}; gates "
65
+ "RE-DOSING of PEGylated LNP. A POPULATION range (region/age/assay-dependent) - NOT a patient's "
66
+ "anti-PEG titer (a known-unknown); induced anti-PEG after dose 1 is a separate, larger dynamic "
67
+ "(not modelled)."))
68
+
69
+
70
+ def computed_antipeg_score(vehicle_name: str, pegylated: bool | None = None) -> tuple[float | None, OracleResult]:
71
+ """Convenience: (anti-PEG score or None, full OracleResult). None when the oracle abstains (non-PEG).
72
+ Never fabricates."""
73
+ r = antipeg_oracle(vehicle_name, pegylated)
74
+ val = (r.value or {}).get("preexisting_antipeg_score") if (r.available and r.value) else None
75
+ return val, r