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
|
@@ -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"}
|
pen_stack/oracles/rna.py
ADDED
|
@@ -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")
|