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,116 @@
1
+ """Binding-affinity oracle, the Boltz-2 protein-ligand affinity head under the contract.
2
+
3
+ Boltz-2 (MIT; Passaro et al. 2025, DOI 10.1101/2025.06.14.659707) jointly predicts a complex structure and a
4
+ binding affinity for a protein with a small-molecule ligand: a binder probability (``affinity_probability_binary``)
5
+ and a predicted affinity value (``affinity_pred_value``, a log(IC50) micromolar-scale number, lower = stronger).
6
+ This oracle wraps that head under the L1 ``OracleResult`` contract:
7
+
8
+ * the prediction is a CANDIDATE / hypothesis, never a measured Kd;
9
+ * the model's own outputs supply the native uncertainty (the spread between Boltz-2's two affinity heads, or a
10
+ binder-call entropy proxy);
11
+ * the affinity head is protein-SMALL-MOLECULE only, so protein-protein and protein-DNA requests are flagged
12
+ out-of-scope (``extrapolating``);
13
+ * the backend runs OFF the request path (a GPU batch, ~10-30 min including MSA) and is cached; when no cached run
14
+ is present the request path is cache-or-abstain and never blocks.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ from pathlib import Path
20
+
21
+ from pen_stack.oracles import build_result, cache_get, cache_put
22
+ from pen_stack.oracles.schema import OracleResult
23
+
24
+ _MODEL = "boltz-2-affinity"
25
+
26
+ # Pair types the affinity head COVERS (protein + small-molecule ligand), with a genome-writing-relevant gloss.
27
+ _LIGAND_PAIRS = {
28
+ "inducer_switch": "a small-molecule inducer that switches a genome writer on/off (e.g. 4-OHT / ERT2)",
29
+ "capsid_ligand": "a small molecule binding a delivery capsid (stabiliser or targeting ligand)",
30
+ "effector_drug": "a drug binding a delivered effector protein",
31
+ "ligand": "a generic protein + small-molecule ligand pair",
32
+ }
33
+ # Pair types OUTSIDE the affinity head's domain (it is protein-ligand only): returned extrapolating.
34
+ _OOD_PAIRS = {
35
+ "protein_protein": "protein-protein affinity is not the Boltz-2 affinity head's domain (protein-ligand only)",
36
+ "protein_dna": "protein-DNA affinity is not the Boltz-2 affinity head's domain (protein-ligand only)",
37
+ }
38
+
39
+ _UNITS = ("Boltz-2 affinity_pred_value: log(IC50) on a micromolar scale, lower = stronger binder "
40
+ "(a prediction, not a measured Kd/IC50)")
41
+
42
+
43
+ def _inputs(protein_seq: str, ligand_smiles: str, pair_type: str) -> dict:
44
+ return {"protein_len": len(protein_seq), "protein": protein_seq.upper(),
45
+ "ligand_smiles": ligand_smiles, "pair_type": pair_type}
46
+
47
+
48
+ def predict_affinity(protein_seq: str, ligand_smiles: str, pair_type: str = "ligand",
49
+ ligand_name: str | None = None) -> OracleResult:
50
+ """Predict protein-ligand binding affinity with the Boltz-2 head; cache-replayed if present, else deferred.
51
+
52
+ ``pair_type`` in {inducer_switch, capsid_ligand, effector_drug, ligand} is in-scope; ``protein_protein`` /
53
+ ``protein_dna`` are OUT of the affinity head's domain and returned extrapolating (the head is protein-ligand).
54
+ The long GPU job is never run on the request path: an absent cached run yields a deferred result.
55
+ """
56
+ in_scope = pair_type in _LIGAND_PAIRS
57
+ extrapolating = pair_type in _OOD_PAIRS
58
+ if not in_scope and not extrapolating:
59
+ raise ValueError(f"unknown pair_type {pair_type!r}; choose from "
60
+ f"{sorted(_LIGAND_PAIRS) + sorted(_OOD_PAIRS)}")
61
+ inputs = _inputs(protein_seq, ligand_smiles, pair_type)
62
+ note0 = (_OOD_PAIRS[pair_type] if extrapolating
63
+ else "Boltz-2 affinity head; backend runs off-request on the GPU and is cached")
64
+ r = build_result("affinity", _MODEL, inputs=inputs, available=False,
65
+ in_scope=in_scope, extrapolating=extrapolating, note=note0 + "; cache-or-abstain")
66
+ hit = cache_get(r.provenance.cache_key)
67
+ if hit is not None:
68
+ note = "replayed from committed oracle cache"
69
+ if extrapolating:
70
+ note += "; OUT-OF-SCOPE pair_type for a protein-ligand head: treat as extrapolative"
71
+ if ligand_name:
72
+ note += f"; ligand={ligand_name}"
73
+ return build_result("affinity", _MODEL, inputs=inputs, value=hit.get("value"),
74
+ native_uncertainty=hit.get("native_uncertainty"),
75
+ available=True, cached=True, source="cache",
76
+ in_scope=in_scope, extrapolating=extrapolating, note=note)
77
+ return r # deferred: no cached run, and the long job never runs on the request path
78
+
79
+
80
+ def value_from_boltz_output(out_dir: str | Path) -> dict:
81
+ """Parse a Boltz-2 affinity prediction directory into the cached {value, native_uncertainty} payload.
82
+
83
+ Native uncertainty is taken from the model's own outputs: half the spread between Boltz-2's two affinity
84
+ heads when both are present, else a binder-call entropy proxy (``1 - |2p - 1|``, 0 at p in {0,1}, 1 at 0.5).
85
+ """
86
+ out = Path(out_dir)
87
+ js = sorted(p for p in out.rglob("affinity*.json"))
88
+ if not js:
89
+ raise FileNotFoundError(f"no affinity*.json under {out}")
90
+ d = json.loads(js[0].read_text(encoding="utf-8"))
91
+ val = d.get("affinity_pred_value")
92
+ prob = d.get("affinity_probability_binary")
93
+ v1, v2 = d.get("affinity_pred_value1"), d.get("affinity_pred_value2")
94
+ if v1 is not None and v2 is not None:
95
+ unc = round(abs(float(v1) - float(v2)) / 2.0, 4)
96
+ unc_src = "half the spread between the two Boltz-2 affinity heads"
97
+ elif prob is not None:
98
+ unc = round(1.0 - abs(2.0 * float(prob) - 1.0), 4)
99
+ unc_src = "binder-call entropy proxy (1 - |2p - 1|)"
100
+ else:
101
+ unc, unc_src = None, "unavailable"
102
+ value = {"affinity_pred_value": val, "binder_probability": prob, "units": _UNITS,
103
+ "heads": {"affinity_pred_value1": v1, "affinity_pred_value2": v2},
104
+ "native_uncertainty_source": unc_src, "source_file": js[0].name}
105
+ return {"value": value, "native_uncertainty": unc}
106
+
107
+
108
+ def cache_affinity_run(protein_seq: str, ligand_smiles: str, pair_type: str, out_dir: str | Path) -> str:
109
+ """Ingest an OFF-REQUEST Boltz-2 affinity run into the oracle cache, so the request path can replay it.
110
+
111
+ Returns the cache key. Called after a separate GPU batch completes; never on the request path.
112
+ """
113
+ payload = value_from_boltz_output(out_dir)
114
+ r = build_result("affinity", _MODEL, inputs=_inputs(protein_seq, ligand_smiles, pair_type), available=False)
115
+ cache_put(r.provenance.cache_key, payload)
116
+ return r.provenance.cache_key
@@ -0,0 +1,53 @@
1
+ """Deterministic, version-pinned oracle cache + scope-card loader.
2
+
3
+ Every oracle call is keyed on (oracle family, model, version, canonicalised inputs) so a cached round-trip
4
+ reproduces committed values offline, the substrate's *core stays runnable from cache* even when a heavy
5
+ oracle backend (AF3, Evo2, ESM3) is not installed (the compute policy). Cache entries are plain JSON
6
+ under `oracle_cache/` (committed for offline CI).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ from functools import lru_cache
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ import yaml
17
+
18
+ from pen_stack._resources import project_root
19
+
20
+ _CACHE_DIR = project_root() / "oracle_cache"
21
+
22
+
23
+ def scope_cards_path() -> Path:
24
+ from pen_stack._resources import resource
25
+ return resource("configs/oracles/scope_cards.yaml")
26
+
27
+
28
+ @lru_cache(maxsize=1)
29
+ def load_scope_cards() -> dict:
30
+ return yaml.safe_load(scope_cards_path().read_text(encoding="utf-8"))["oracles"]
31
+
32
+
33
+ def scope_card(model: str) -> dict | None:
34
+ return load_scope_cards().get(model)
35
+
36
+
37
+ def cache_key(oracle: str, model: str, version: str, inputs: dict[str, Any]) -> str:
38
+ """A stable key over the oracle family, the pinned model+version, and canonicalised inputs."""
39
+ payload = json.dumps({"oracle": oracle, "model": model, "version": version, "inputs": inputs},
40
+ sort_keys=True, separators=(",", ":"))
41
+ return hashlib.sha256(payload.encode()).hexdigest()[:24]
42
+
43
+
44
+ def cache_get(key: str) -> dict | None:
45
+ p = _CACHE_DIR / f"{key}.json"
46
+ if p.exists():
47
+ return json.loads(p.read_text(encoding="utf-8"))
48
+ return None
49
+
50
+
51
+ def cache_put(key: str, value: dict) -> None:
52
+ _CACHE_DIR.mkdir(parents=True, exist_ok=True)
53
+ (_CACHE_DIR / f"{key}.json").write_text(json.dumps(value, indent=2, default=str), encoding="utf-8")
@@ -0,0 +1,33 @@
1
+ """Energetics oracle, bridge off-target relative-risk, under the oracle contract.
2
+
3
+ Generalises the MC3 off-target energetics model into the mesh. The HARD GATE is unchanged: the
4
+ energetics model ships only if its held-out AUROC beats BOTH the position-weight model on the same split AND
5
+ the published 0.77 baseline (the eval lives in `validate.offtarget_energetics_eval`); off the VM (Perry data
6
+ absent) the adapter returns a deferred result rather than a fabricated number.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from pen_stack.oracles import build_result
11
+ from pen_stack.oracles.schema import OracleResult
12
+
13
+ GATE_AUROC = 0.77
14
+
15
+
16
+ def gate() -> OracleResult:
17
+ """Run the MC3 held-out discrimination eval and report whether energetics still earns its place."""
18
+ inputs = {"eval": "offtarget_energetics_eval", "gate": GATE_AUROC}
19
+ try:
20
+ from pen_stack.validate.offtarget_energetics_eval import run
21
+ rep = run()
22
+ except Exception as e: # noqa: BLE001 - Perry tables absent off the VM
23
+ return build_result("energetics", "bridge_energetics", inputs=inputs, available=False,
24
+ note=f"off-target energetics eval unavailable: {type(e).__name__}: {e}")
25
+ if not rep.get("available", True):
26
+ return build_result("energetics", "bridge_energetics", inputs=inputs, available=False,
27
+ note=rep.get("note", "Perry off-target tables absent (runs on the VM)"))
28
+ auroc = rep.get("energetics_auroc") or rep.get("model_auroc")
29
+ ships = bool(rep.get("ships", auroc is not None and auroc > GATE_AUROC))
30
+ return build_result("energetics", "bridge_energetics", inputs=inputs, value={"held_out_auroc": auroc,
31
+ "beats_0_77": ships, "report": rep},
32
+ native_uncertainty=None if auroc is None else round(max(0.0, 1.0 - auroc), 3),
33
+ note=f"MC3 gate: ships only if held-out AUROC>{GATE_AUROC} AND > position-weight model")
@@ -0,0 +1,167 @@
1
+ """Genome oracles, AlphaGenome / Evo2 / accessibility baselines, under one contract.
2
+
3
+ AlphaGenome (regulatory tracks + variant effect) is **OOD-gated**: scoring a locus outside the model's
4
+ training distribution sets `extrapolating=True` / `in_scope=False` (the model does not generalize to unseen
5
+ loci, labelled, not hidden). Evo2 supplies a likelihood/zero-shot *claim*-scope scalar and, separately,
6
+ *generated DNA candidates* (output_kind=candidate → cannot enter a claim path). ChromBPNet/Borzoi are kept as
7
+ baselines. Heavy backends run on-demand and are cached; when absent the adapter defers (or replays a
8
+ committed cache entry) rather than fabricating a value.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import re
14
+
15
+ from pen_stack.oracles import build_result, cache_get, cache_put
16
+ from pen_stack.oracles.schema import OracleResult
17
+
18
+ # ---- hosted Evo2-40B (NVIDIA), live path, opt-in via PEN_STACK_ORACLE_NET=1 (CI stays offline by default) ----
19
+ _EVO2_URL = "https://health.api.nvidia.com/v1/biology/arc/evo2-40b/generate"
20
+
21
+
22
+ def _oracle_net_enabled() -> bool:
23
+ """Live oracle network calls are opt-in (the deployed VM sets it; CI/offline leave it unset → deferred)."""
24
+ return os.getenv("PEN_STACK_ORACLE_NET") == "1"
25
+
26
+
27
+ def _nvidia_key() -> str | None:
28
+ key = os.getenv("NVIDIA_API_KEY")
29
+ if key:
30
+ return key.strip()
31
+ from pen_stack._resources import project_root
32
+ f = project_root() / "configs" / "nvidia_api_key.txt"
33
+ return f.read_text(encoding="utf-8").strip() if f.exists() else None
34
+
35
+
36
+ def _sanitize_dna(s: str) -> str:
37
+ return "".join(c for c in (s or "").upper() if c in "ACGTN")
38
+
39
+
40
+ def _call_evo2_generate(seed: str, n: int) -> dict:
41
+ import requests
42
+ key = _nvidia_key()
43
+ if not key:
44
+ raise RuntimeError("no NVIDIA_API_KEY for hosted Evo2")
45
+ body = {"sequence": seed, "num_tokens": int(n), "top_k": 4, "enable_sampled_probs": True}
46
+ r = requests.post(_EVO2_URL, headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
47
+ json=body, timeout=float(os.getenv("PEN_STACK_ORACLE_TIMEOUT", "120")))
48
+ r.raise_for_status()
49
+ return r.json()
50
+
51
+
52
+ def _deferred_or_cached(oracle: str, model: str, inputs: dict, *, extrapolating: bool = False,
53
+ in_scope: bool = True, output_kind: str | None = None,
54
+ backend_note: str = "backend not installed") -> OracleResult:
55
+ r = build_result(oracle, model, inputs=inputs, available=False, extrapolating=extrapolating,
56
+ in_scope=in_scope, output_kind=output_kind, note=backend_note)
57
+ hit = cache_get(r.provenance.cache_key)
58
+ if hit is not None:
59
+ return build_result(oracle, model, inputs=inputs, value=hit.get("value"),
60
+ native_uncertainty=hit.get("native_uncertainty"), available=True, cached=True,
61
+ source="cache", extrapolating=extrapolating, in_scope=in_scope,
62
+ output_kind=output_kind, note="replayed from committed oracle cache")
63
+ return r
64
+
65
+
66
+ _VAR_RE = re.compile(r"(?:(chr[\w]+)[:\s]*)?(\d+)?\s*([ACGT]+)\s*>\s*([ACGT]+)", re.I)
67
+ _LOCUS_RE = re.compile(r"(chr[\w]+)[:\s]*(\d+)?", re.I)
68
+
69
+
70
+ def _parse_variant(variant: str, locus: str):
71
+ """Parse a variant ('chr1:1000 A>T' / 'chr1:1A>T' / 'A>G' + locus 'chr19:1010000') -> (chrom,pos,ref,alt)."""
72
+ m = _VAR_RE.search(variant or "")
73
+ if not m:
74
+ return None
75
+ chrom, pos, ref, alt = m.group(1), m.group(2), m.group(3), m.group(4)
76
+ if not chrom or not pos:
77
+ lm = _LOCUS_RE.search(locus or "")
78
+ if lm:
79
+ chrom = chrom or lm.group(1)
80
+ pos = pos or lm.group(2)
81
+ if not (chrom and pos and ref and alt):
82
+ return None
83
+ return chrom, int(pos), ref.upper(), alt.upper()
84
+
85
+
86
+ def variant_effect(variant: str, locus: str, in_distribution: bool = True) -> OracleResult:
87
+ """AlphaGenome variant-effect prediction, OOD-gated by `in_distribution`.
88
+
89
+ LIVE via the existing `wgenome.AlphaGenomeProvider` (real `score_variant`, REF vs ALT) when
90
+ `PEN_STACK_ORACLE_NET=1` and the alphagenome package + key are present; otherwise deferred / cache-replay
91
+ (value None, OOD gate intact, never fabricated). Reuses the existing provider, no duplicate client."""
92
+ inputs = {"variant": variant, "locus": locus}
93
+ parsed = _parse_variant(variant, locus)
94
+ if _oracle_net_enabled() and parsed:
95
+ from pen_stack.wgenome.providers import AlphaGenomeProvider
96
+ prov = AlphaGenomeProvider()
97
+ if prov.available():
98
+ chrom, pos, ref, alt = parsed
99
+ try:
100
+ rec = prov.score_variant(chrom, pos, ref, alt)
101
+ except Exception as e: # noqa: BLE001 - hosted/network; defer, never fabricate
102
+ return _deferred_or_cached("genome", "alphagenome", inputs, extrapolating=not in_distribution,
103
+ in_scope=in_distribution,
104
+ backend_note=f"AlphaGenome score_variant failed ({type(e).__name__}); deferred")
105
+ if rec.get("available"):
106
+ return build_result(
107
+ "genome", "alphagenome", inputs=inputs,
108
+ value={"effect_max_abs": rec["effect_max_abs"], "effect_mean_abs": rec["effect_mean_abs"],
109
+ "output": rec["output"], "n_scores": rec["n_scores"],
110
+ "chrom": chrom, "position": pos, "ref": ref, "alt": alt},
111
+ available=True, source="hosted_api", extrapolating=not in_distribution, in_scope=in_distribution,
112
+ note=("AlphaGenome score_variant (REF vs ALT, recommended RNA_SEQ scorer): max|effect| over the "
113
+ "predicted regulatory tracks. A regulatory-effect magnitude, not a claim the edit works."))
114
+ # deferred / cache-replay, package/key/flag absent or variant unparseable (OOD gate preserved, no fabrication)
115
+ return _deferred_or_cached("genome", "alphagenome", inputs, extrapolating=not in_distribution,
116
+ in_scope=in_distribution,
117
+ backend_note="AlphaGenome deferred (set PEN_STACK_ORACLE_NET=1 + package/key to score live)")
118
+
119
+
120
+ def sequence_likelihood(seq: str) -> OracleResult:
121
+ """Evo2 zero-shot sequence likelihood, a claim-scope scalar (NOT a generated sequence)."""
122
+ inputs = {"seq_len": len(seq), "seq": seq.upper()}
123
+ try:
124
+ import evo2 # noqa: F401
125
+ except Exception: # noqa: BLE001
126
+ return _deferred_or_cached("genome", "evo2", inputs, output_kind="claim",
127
+ backend_note="Evo2 backend not installed (large; on-demand)")
128
+ return build_result("genome", "evo2", inputs=inputs, output_kind="claim", available=False,
129
+ note="Evo2 present; wire likelihood scoring for the live value")
130
+
131
+
132
+ def generate_dna(prompt: str, n: int = 20) -> OracleResult:
133
+ """Evo2 generative DNA, a CANDIDATE (output_kind=candidate); cannot enter a claim path unverified.
134
+
135
+ LIVE via NVIDIA's hosted Evo2-40B when `PEN_STACK_ORACLE_NET=1` and an NVIDIA key is present: Evo2 conditions
136
+ on a DNA context (the seed) and extends it; the per-token model probability is surfaced as native uncertainty.
137
+ Otherwise deferred / cache-replay (value None, never fabricated). The result is still a CANDIDATE, a generated
138
+ sequence is a proposal, not a claim (`as_claim()` raises)."""
139
+ seed = _sanitize_dna(prompt)
140
+ inputs = {"prompt": prompt, "seed": seed, "n": int(n)}
141
+ if _oracle_net_enabled() and _nvidia_key() and seed:
142
+ key_obj = build_result("genome", "evo2", inputs=inputs, output_kind="candidate")
143
+ try:
144
+ resp = _call_evo2_generate(seed, n)
145
+ except Exception as e: # noqa: BLE001 - hosted/network; fall back to deferred (no fabrication)
146
+ return _deferred_or_cached("genome", "evo2", inputs, output_kind="candidate",
147
+ backend_note=f"hosted Evo2 call failed ({type(e).__name__}); deferred")
148
+ continuation = _sanitize_dna(resp.get("sequence", "")) # hosted endpoint returns the continuation only
149
+ probs = resp.get("sampled_probs") or []
150
+ mean_p = (sum(probs) / len(probs)) if probs else None
151
+ unc = round(1.0 - mean_p, 4) if mean_p is not None else None
152
+ val = {"generated": continuation, "full": seed + continuation, "seed": seed,
153
+ "per_token_prob": probs, "n_tokens": len(continuation), "elapsed_ms": resp.get("elapsed_ms")}
154
+ cache_put(key_obj.provenance.cache_key, {"value": val, "native_uncertainty": unc})
155
+ return build_result("genome", "evo2", inputs=inputs, value=val, native_uncertainty=unc,
156
+ available=True, source="hosted_api", output_kind="candidate",
157
+ note=("Evo2-40B (NVIDIA hosted) extended the DNA seed; per-token model probability "
158
+ "surfaced as uncertainty. CANDIDATE, verify (writer-verification) before any claim."))
159
+ return _deferred_or_cached("genome", "evo2", inputs, output_kind="candidate",
160
+ backend_note="Evo2 generation is a CANDIDATE; verify before any claim")
161
+
162
+
163
+ def accessibility_baseline(locus: str) -> OracleResult:
164
+ """ChromBPNet/Borzoi accessibility/expression, an BASELINE comparator to AlphaGenome."""
165
+ inputs = {"locus": locus}
166
+ return _deferred_or_cached("genome", "chrombpnet_borzoi", inputs, output_kind="baseline",
167
+ backend_note="ChromBPNet/Borzoi baseline; on-demand")
@@ -0,0 +1,136 @@
1
+ """Protein-design oracles, RFdiffusion / ProteinMPNN / ESM3, all CANDIDATES.
2
+
3
+ Every output here is a generative **candidate** (output_kind=candidate): a backbone, a designed sequence, or
4
+ an ESM3 design. By the contract (`OracleResult.as_claim()` raises) and a guard test, none of these can enter a
5
+ claim path without passing writer-verification scoring against measured data, the encoded
6
+ pen-assemble lesson (0 validatable de-novo writers; we score/critique, never assert function). Heavy backends
7
+ run on-demand; absent → deferred candidate.
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
+
17
+ def _oracle_net_enabled() -> bool:
18
+ """Live oracle calls are opt-in (the VM sets it; CI/offline leave it unset → deferred)."""
19
+ return os.getenv("PEN_STACK_ORACLE_NET") == "1"
20
+
21
+
22
+ def _model_server(env_var: str, default: str) -> str:
23
+ return os.getenv(env_var, default).rstrip("/")
24
+
25
+
26
+ def _post(url: str, payload: dict, timeout_env: str = "PEN_STACK_MODEL_TIMEOUT", default_timeout: str = "600"):
27
+ import requests
28
+ r = requests.post(url, json=payload, timeout=float(os.getenv(timeout_env, default_timeout)))
29
+ r.raise_for_status()
30
+ return r.json()
31
+
32
+
33
+ def _candidate(model: str, inputs: dict, backend: str, note: str) -> OracleResult:
34
+ r = build_result("protein_design", model, inputs=inputs, available=False, output_kind="candidate", note=note)
35
+ hit = cache_get(r.provenance.cache_key)
36
+ if hit is not None:
37
+ return build_result("protein_design", model, inputs=inputs, value=hit.get("value"), available=True,
38
+ cached=True, source="cache", output_kind="candidate",
39
+ note="replayed from committed oracle cache (still a CANDIDATE)")
40
+ try:
41
+ __import__(backend)
42
+ except Exception: # noqa: BLE001
43
+ return r
44
+ return build_result("protein_design", model, inputs=inputs, available=False, output_kind="candidate",
45
+ note=f"{model} present; wire the generator (output stays a CANDIDATE)")
46
+
47
+
48
+ def generate_backbone(spec: dict, model: str = "rfdiffusion") -> OracleResult:
49
+ """RFdiffusion / RFdiffusion-AA backbone generation, a CANDIDATE.
50
+
51
+ LIVE via the local RFdiffusion model server (`PEN_STACK_RFDIFFUSION_URL`, default localhost:9013) when
52
+ `PEN_STACK_ORACLE_NET=1`, the spec gives a `length` or `contigs`, and the service is up: RFdiffusion
53
+ diffuses a real backbone PDB (still a CANDIDATE, verify before any claim). Otherwise deferred."""
54
+ inputs = {"spec": spec}
55
+ length = (spec or {}).get("length") if isinstance(spec, dict) else None
56
+ contigs = (spec or {}).get("contigs") if isinstance(spec, dict) else None
57
+ if _oracle_net_enabled() and (length or contigs):
58
+ key_obj = build_result("protein_design", model, inputs=inputs, output_kind="candidate")
59
+ url = _model_server("PEN_STACK_RFDIFFUSION_URL", "http://localhost:9013")
60
+ payload = {k: v for k, v in {"length": length, "contigs": contigs,
61
+ "num_designs": spec.get("num_designs", 1)}.items() if v is not None}
62
+ try:
63
+ resp = _post(f"{url}/generate", payload, default_timeout="1200")
64
+ except Exception: # noqa: BLE001 - service down → defer
65
+ return _candidate(model, inputs, "rfdiffusion",
66
+ "RFdiffusion server unreachable; deferred CANDIDATE (start the model server)")
67
+ designs = resp.get("designs") or []
68
+ val = {"designs": designs, "n": len(designs), "contigs": resp.get("contigs"),
69
+ "n_residues": (designs[0].get("n_residues") if designs else None)}
70
+ cache_put(key_obj.provenance.cache_key, {"value": val})
71
+ return build_result("protein_design", model, inputs=inputs, value=val, available=True,
72
+ source="local_gpu", output_kind="candidate",
73
+ note=("RFdiffusion diffused a backbone (local GPU). CANDIDATE, design a sequence "
74
+ "(ProteinMPNN) + verify fold/activity before any claim."))
75
+ return _candidate(model, inputs, "rfdiffusion",
76
+ "RFdiffusion backbone is a CANDIDATE; verify before any claim")
77
+
78
+
79
+ def design_sequence(backbone: dict, model: str = "proteinmpnn") -> OracleResult:
80
+ """ProteinMPNN / LigandMPNN sequence design for a fixed backbone, a CANDIDATE.
81
+
82
+ LIVE via the local ProteinMPNN model server (`PEN_STACK_PROTEINMPNN_URL`, default localhost:9011) when
83
+ `PEN_STACK_ORACLE_NET=1`, the backbone carries a `pdb`, and the service is up; the designed sequences are a
84
+ real ProteinMPNN output (still a CANDIDATE, `as_claim()` raises). Otherwise deferred / cache-replay."""
85
+ inputs = {"backbone": backbone}
86
+ pdb = (backbone or {}).get("pdb") if isinstance(backbone, dict) else None
87
+ if _oracle_net_enabled() and isinstance(pdb, str) and "ATOM" in pdb:
88
+ key_obj = build_result("protein_design", model, inputs=inputs, output_kind="candidate")
89
+ url = _model_server("PEN_STACK_PROTEINMPNN_URL", "http://localhost:9011")
90
+ try:
91
+ resp = _post(f"{url}/design", {"pdb": pdb, "chains": backbone.get("chains"),
92
+ "num_seqs": int(backbone.get("num_seqs", 4))})
93
+ except Exception: # noqa: BLE001 - service down/unreachable → defer (no fabrication)
94
+ return _candidate(model, inputs, "proteinmpnn",
95
+ "ProteinMPNN server unreachable; deferred CANDIDATE (start the model server to design)")
96
+ designs = resp.get("designs") or []
97
+ best = min((d.get("global_score") for d in designs if d.get("global_score") is not None), default=None)
98
+ val = {"designs": designs, "n": len(designs), "best_global_score": best}
99
+ cache_put(key_obj.provenance.cache_key, {"value": val})
100
+ return build_result("protein_design", model, inputs=inputs, value=val, available=True,
101
+ source="local_gpu", output_kind="candidate",
102
+ note=("ProteinMPNN designed sequences for the backbone (local GPU). CANDIDATE, score "
103
+ "against measured data (writer-verification) before any claim."))
104
+ return _candidate(model, inputs, "proteinmpnn",
105
+ "ProteinMPNN sequence is a CANDIDATE; score against measured data before any claim")
106
+
107
+
108
+ def esm3_design(prompt: dict, model: str = "esm3") -> OracleResult:
109
+ """ESM3 generative protein design / representation, a CANDIDATE.
110
+
111
+ LIVE via the local ESM3-open model server (`PEN_STACK_ESM3_URL`, default localhost:9012) when
112
+ `PEN_STACK_ORACLE_NET=1`, the prompt gives a masked `sequence` or a `length`, and the service is up: ESM3
113
+ generates a real protein (still a CANDIDATE, verify fold/activity before any claim). Otherwise deferred."""
114
+ inputs = {"prompt": prompt}
115
+ seq = (prompt or {}).get("sequence") if isinstance(prompt, dict) else None
116
+ length = (prompt or {}).get("length") if isinstance(prompt, dict) else None
117
+ if _oracle_net_enabled() and (seq or length):
118
+ key_obj = build_result("protein_design", model, inputs=inputs, output_kind="candidate")
119
+ url = _model_server("PEN_STACK_ESM3_URL", "http://localhost:9012")
120
+ payload = {k: v for k, v in {"sequence": seq, "length": length,
121
+ "num_steps": prompt.get("num_steps"),
122
+ "temperature": prompt.get("temperature")}.items() if v is not None}
123
+ try:
124
+ resp = _post(f"{url}/generate", payload)
125
+ except Exception: # noqa: BLE001 - service down → defer
126
+ return _candidate(model, inputs, "esm",
127
+ "ESM3 server unreachable; deferred CANDIDATE (start the model server to design)")
128
+ val = {"sequence": resp.get("sequence"), "length": resp.get("length"),
129
+ "num_steps": resp.get("num_steps"), "model": resp.get("backend")}
130
+ cache_put(key_obj.provenance.cache_key, {"value": val})
131
+ return build_result("protein_design", model, inputs=inputs, value=val, available=True,
132
+ source="local_gpu", output_kind="candidate",
133
+ note=("ESM3-open generated a protein sequence (local GPU). CANDIDATE, verify "
134
+ "fold/activity (writer-verification) before any claim."))
135
+ return _candidate(model, inputs, "esm",
136
+ "ESM3 design is a CANDIDATE; verify fold/activity before any claim")
@@ -0,0 +1,64 @@
1
+ """Per-oracle reliability registry + the disagreement-to-interval monotonicity check.
2
+
3
+ Reliability here is the wrapped model's PUBLISHED accuracy on PUBLIC benchmarks, reported VERBATIM with citation
4
+ (``configs/oracles/reliability.yaml``). It is NOT recomputed here and NOT a claim about this stack's own accuracy.
5
+ Each oracle output remains a candidate carrying its own native uncertainty; reliability is surfaced precisely so a
6
+ confident-looking value is not over-trusted. Where a verbatim number was not independently verified the registry
7
+ records ``null`` plus the cited benchmark (the pointer), never a guess.
8
+
9
+ The module also exposes :func:`disagreement_widens_monotonically`, which confirms the cross-oracle consensus
10
+ mechanism (:func:`pen_stack.oracles.consensus`, native uncertainty + half the cross-oracle spread) widens the
11
+ reported interval MONOTONICALLY as the spread grows. That is the acceptance check for the
12
+ disagreement-to-uncertainty rule.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from functools import lru_cache
17
+
18
+ from pen_stack._resources import resource
19
+
20
+
21
+ @lru_cache(maxsize=1)
22
+ def _doc() -> dict:
23
+ import yaml
24
+ return yaml.safe_load(resource("configs/oracles/reliability.yaml").read_text(encoding="utf-8"))
25
+
26
+
27
+ def reliability(oracle: str) -> list | None:
28
+ """The published-reliability records for one oracle (or None if the oracle is not in the registry)."""
29
+ return _doc()["oracles"].get(oracle)
30
+
31
+
32
+ def all_reliability() -> dict:
33
+ """The full per-oracle reliability registry."""
34
+ return _doc()["oracles"]
35
+
36
+
37
+ def disclaimer() -> str:
38
+ """The standing disclaimer: published numbers, reported verbatim, not a claim about this stack."""
39
+ return _doc()["disclaimer"].strip()
40
+
41
+
42
+ def _num_result(value: float, unc: float):
43
+ from pen_stack.oracles import build_result
44
+ return build_result("structure", "boltz-2", value=value, native_uncertainty=unc, available=True)
45
+
46
+
47
+ def disagreement_widens_monotonically(spreads: list[float] | None = None) -> dict:
48
+ """Confirm :func:`consensus` widens the reported interval monotonically with the cross-oracle spread.
49
+
50
+ For each spread ``s`` two numeric oracles are placed symmetrically about a common centre, each with the same
51
+ small native uncertainty; the consensus native uncertainty must be non-decreasing in ``s``. Returns the
52
+ measured sequence and the monotonicity verdict (the gate reports the mechanism as working or broken).
53
+ """
54
+ from pen_stack.oracles import consensus
55
+ spreads = spreads if spreads is not None else [0.0, 0.05, 0.1, 0.2, 0.4]
56
+ centre, member_unc = 0.5, 0.05
57
+ uncs: list[float] = []
58
+ for s in spreads:
59
+ members = [_num_result(centre - s / 2, member_unc), _num_result(centre + s / 2, member_unc)]
60
+ uncs.append(consensus(members, oracle="structure").native_uncertainty)
61
+ monotone = all(uncs[i + 1] >= uncs[i] - 1e-9 for i in range(len(uncs) - 1))
62
+ return {"spreads": spreads, "native_uncertainty": uncs, "monotone_nondecreasing": monotone,
63
+ "rule": "native_uncertainty = max(member native uncertainty) + 0.5 * (max - min) over the "
64
+ "available numeric oracles"}
@@ -0,0 +1,28 @@
1
+ """RNA oracle, ViennaRNA secondary-structure fold, under the oracle contract.
2
+
3
+ A thin contract wrapper over the EXISTING `bridge.fold_qc` (the ViennaRNA QC that already feeds the
4
+ hard fold-legality rule). ViennaRNA is real and runs in the VM image; when absent the adapter returns a
5
+ deferred `OracleResult` (available=False) rather than a fabricated structure. Fold legality remains a HARD
6
+ rule input (it does not become a soft signal here).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from pen_stack.oracles import build_result
11
+ from pen_stack.oracles.schema import OracleResult
12
+
13
+
14
+ def fold(scaffold_seq: str) -> OracleResult:
15
+ """MFE fold of a bridge-RNA scaffold via ViennaRNA, wrapped in the oracle contract."""
16
+ from pen_stack.bridge.fold_qc import fold as _fold
17
+ r = _fold(scaffold_seq)
18
+ inputs = {"scaffold_len": len(scaffold_seq), "scaffold_seq": scaffold_seq.upper()}
19
+ if not r.get("available"):
20
+ return build_result("rna", "viennarna", inputs=inputs, available=False,
21
+ note=r.get("note", "ViennaRNA not installed (runs in the VM image)"))
22
+ # native uncertainty proxy: a less-negative MFE per nt = a weaker, less-certain fold
23
+ mfe, n = float(r["mfe"]), max(1, int(r["length"]))
24
+ unc = max(0.0, min(1.0, 1.0 + (mfe / n) / 0.5)) # ~0 for strong folds, ->1 as MFE/nt -> 0
25
+ return build_result("rna", "viennarna", inputs=inputs,
26
+ value={"structure": r["structure"], "mfe": r["mfe"], "length": r["length"]},
27
+ native_uncertainty=round(unc, 3),
28
+ note="ViennaRNA MFE fold; a hard legality input for bridge-RNA QC")