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,77 @@
|
|
|
1
|
+
"""The oracle contract, one uniform result type for every foundation model.
|
|
2
|
+
|
|
3
|
+
PEN-STACK is the composition + verification layer *on top of* the biomolecular foundation models
|
|
4
|
+
(AlphaGenome, Evo2, AlphaFold3, Boltz-2, Chai-1, Protenix, ESM3, RFdiffusion, ProteinMPNN, ViennaRNA, ...).
|
|
5
|
+
Every oracle, however different internally, answers through ONE contract:
|
|
6
|
+
|
|
7
|
+
predict(...) -> OracleResult{value, provenance(model+version), native_uncertainty, scope}
|
|
8
|
+
|
|
9
|
+
Three invariants are encoded in the type itself:
|
|
10
|
+
1. **A generative output is a candidate, never a claim**, `candidate=True` outputs cannot enter a claim
|
|
11
|
+
path without passing writer-verification (`as_claim()` raises; a guard test asserts it).
|
|
12
|
+
2. **One contract for every oracle**, provenance (model+version) and the oracle's *native* uncertainty are
|
|
13
|
+
always carried, never discarded; every call is cache-keyed on inputs+model+version.
|
|
14
|
+
3. **Scope is explicit**, each result carries the id of its oracle's scope card and an `extrapolating`
|
|
15
|
+
flag; the field's evidence that these models do not generalize to unseen loci is *labelled*, not hidden.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any, Literal
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
22
|
+
|
|
23
|
+
OutputKind = Literal["claim", "candidate", "baseline"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Provenance(BaseModel):
|
|
27
|
+
"""Immutable (frozen): once an oracle stamps a result with its model, version, and source, that record
|
|
28
|
+
cannot be edited after the fact. Frozen here as well as on OracleResult, since a frozen OracleResult with
|
|
29
|
+
a mutable Provenance would still let a caller rewrite `model`/`version`/`source` on the nested object."""
|
|
30
|
+
model_config = ConfigDict(frozen=True)
|
|
31
|
+
|
|
32
|
+
model: str # e.g. "boltz-2", "alphagenome", "evo2"
|
|
33
|
+
version: str # pinned model/version string
|
|
34
|
+
source: str = "adapter" # adapter | cache | hosted_api | local_gpu
|
|
35
|
+
cache_key: str | None = None
|
|
36
|
+
extra: dict[str, Any] = Field(default_factory=dict)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class OracleResult(BaseModel):
|
|
40
|
+
"""The single result type returned by every oracle adapter.
|
|
41
|
+
|
|
42
|
+
Frozen (immutable) so the scope, provenance, and availability an oracle reports cannot be edited after the
|
|
43
|
+
fact: once an adapter returns a result, no downstream caller (or agent step) can flip `available`,
|
|
44
|
+
`in_scope`, `output_kind`, or `extrapolating` to launder a candidate into a claim or hide extrapolation.
|
|
45
|
+
`as_claim()` therefore *filters* (returns self or raises), it never mutates. Build a changed copy with
|
|
46
|
+
`model_copy(update=...)` if a transform genuinely needs one; that yields a new, separately-provenanced
|
|
47
|
+
result rather than silently rewriting an existing one."""
|
|
48
|
+
model_config = ConfigDict(frozen=True)
|
|
49
|
+
|
|
50
|
+
oracle: str # family: genome | structure | protein_design | rna | energetics
|
|
51
|
+
value: Any | None # the prediction (kind depends on the oracle)
|
|
52
|
+
provenance: Provenance
|
|
53
|
+
native_uncertainty: float | None = None # the oracle's OWN uncertainty (e.g. 1 - pLDDT), surfaced not hidden
|
|
54
|
+
scope_card: str | None = None # id of the scope card stating what this oracle is valid for
|
|
55
|
+
in_scope: bool = True # input falls within the scope card
|
|
56
|
+
extrapolating: bool = False # OOD relative to the oracle's training/validity envelope
|
|
57
|
+
output_kind: OutputKind = "claim" # claim | candidate (generative) | baseline (comparator)
|
|
58
|
+
available: bool = True # backend present + ran (False -> deferred, value may be cached/None)
|
|
59
|
+
cached: bool = False
|
|
60
|
+
note: str | None = None
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def is_candidate(self) -> bool:
|
|
64
|
+
return self.output_kind == "candidate"
|
|
65
|
+
|
|
66
|
+
def as_claim(self) -> "OracleResult":
|
|
67
|
+
"""Return self for use in a claim path, or RAISE if this is a generative candidate (Principle 1).
|
|
68
|
+
|
|
69
|
+
A generated sequence/structure/backbone (Evo2/ESM3/RFdiffusion/ProteinMPNN) is a *proposal*; it must be
|
|
70
|
+
scored against measured data through writer-verification before any claim. This guard makes the
|
|
71
|
+
pen-assemble lesson (0 validatable de-novo writers) un-bypassable in code."""
|
|
72
|
+
if self.is_candidate:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"oracle '{self.provenance.model}' returned a CANDIDATE ({self.oracle}); it cannot enter a "
|
|
75
|
+
"claim path without writer-verification scoring (Principle 1). Call "
|
|
76
|
+
"pen_stack.atlas.writer_verify on it first.")
|
|
77
|
+
return self
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Live-oracle status + latency surface (PEN-STACK).
|
|
2
|
+
|
|
3
|
+
Reports, for every foundation-model oracle, HOW it executes, its per-query LATENCY CLASS, and whether it is
|
|
4
|
+
currently LIVE (or why not), so the assistant can tell the user the cost up front ("this runs on the GPU,
|
|
5
|
+
~1 min" / "this is a long cloud job, run it separately") and never silently blocks. The static map lives in
|
|
6
|
+
`configs/oracles/execution.yaml`; this module adds the runtime liveness check (key present? server up?).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
|
|
13
|
+
from pen_stack._resources import resource
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@lru_cache(maxsize=1)
|
|
17
|
+
def execution_map() -> dict:
|
|
18
|
+
import yaml
|
|
19
|
+
return yaml.safe_load(resource("configs/oracles/execution.yaml").read_text(encoding="utf-8"))["oracles"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _net_on() -> bool:
|
|
23
|
+
return os.getenv("PEN_STACK_ORACLE_NET") == "1"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _alphagenome_key() -> bool:
|
|
27
|
+
if os.getenv("ALPHAGENOME_API_KEY"):
|
|
28
|
+
return True
|
|
29
|
+
try:
|
|
30
|
+
return resource("configs/alphagenome_api_key.txt").exists()
|
|
31
|
+
except Exception: # noqa: BLE001
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _nvidia_key() -> bool:
|
|
36
|
+
if os.getenv("NVIDIA_API_KEY"):
|
|
37
|
+
return True
|
|
38
|
+
try:
|
|
39
|
+
return resource("configs/nvidia_api_key.txt").exists()
|
|
40
|
+
except Exception: # noqa: BLE001
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_SERVER_ENV = {"proteinmpnn": ("PEN_STACK_PROTEINMPNN_URL", "http://localhost:9011"),
|
|
45
|
+
"esm3": ("PEN_STACK_ESM3_URL", "http://localhost:9012"),
|
|
46
|
+
"rfdiffusion": ("PEN_STACK_RFDIFFUSION_URL", "http://localhost:9013")}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _server_up(model: str) -> bool:
|
|
50
|
+
env, default = _SERVER_ENV[model]
|
|
51
|
+
url = os.getenv(env, default).rstrip("/")
|
|
52
|
+
try:
|
|
53
|
+
import requests
|
|
54
|
+
return requests.get(f"{url}/health", timeout=2).status_code == 200
|
|
55
|
+
except Exception: # noqa: BLE001
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _is_live(model: str, card: dict, probe: bool) -> tuple[bool, str]:
|
|
60
|
+
ex = card.get("execution")
|
|
61
|
+
if ex == "in_process":
|
|
62
|
+
return True, "runs in-process"
|
|
63
|
+
if card.get("live") is False or ex in {"deferred", "cloud_a100"}:
|
|
64
|
+
return False, card.get("note", "not active")
|
|
65
|
+
if ex == "hosted_api":
|
|
66
|
+
if not _net_on():
|
|
67
|
+
return False, "set PEN_STACK_ORACLE_NET=1 to enable live calls"
|
|
68
|
+
if model == "alphagenome":
|
|
69
|
+
if not _alphagenome_key():
|
|
70
|
+
return False, "add configs/alphagenome_api_key.txt"
|
|
71
|
+
try:
|
|
72
|
+
from pen_stack.wgenome.providers import package_available
|
|
73
|
+
if not package_available():
|
|
74
|
+
return False, "pip install alphagenome"
|
|
75
|
+
except Exception: # noqa: BLE001
|
|
76
|
+
return False, "alphagenome package unavailable"
|
|
77
|
+
if model == "evo2" and not _nvidia_key():
|
|
78
|
+
return False, "add configs/nvidia_api_key.txt"
|
|
79
|
+
return True, "hosted API ready"
|
|
80
|
+
if ex == "local_gpu":
|
|
81
|
+
if not _net_on():
|
|
82
|
+
return False, "set PEN_STACK_ORACLE_NET=1"
|
|
83
|
+
if probe:
|
|
84
|
+
return (_server_up(model), "server up" if _server_up(model) else "model server not running (start it on demand)")
|
|
85
|
+
return True, "enabled (start the model server on demand)"
|
|
86
|
+
return False, "unknown execution"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def oracle_status(probe: bool = False) -> dict:
|
|
90
|
+
"""Per-oracle execution + latency_class + live status + published reliability. `probe=True` pings the local
|
|
91
|
+
model servers (adds a short network check); default is config-level only. Reliability is the wrapped model's
|
|
92
|
+
PUBLISHED benchmark accuracy, reported verbatim with citation, never a claim about this stack's accuracy."""
|
|
93
|
+
try:
|
|
94
|
+
from pen_stack.oracles.reliability import all_reliability
|
|
95
|
+
rel = all_reliability()
|
|
96
|
+
except Exception: # noqa: BLE001
|
|
97
|
+
rel = {}
|
|
98
|
+
out = {}
|
|
99
|
+
for model, card in execution_map().items():
|
|
100
|
+
live, why = _is_live(model, card, probe)
|
|
101
|
+
out[model] = {"execution": card.get("execution"), "latency_class": card.get("latency_class"),
|
|
102
|
+
"live": live, "status": why, "note": card.get("note", ""),
|
|
103
|
+
"server": card.get("server"), "reliability": rel.get(model)}
|
|
104
|
+
return out
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def summary() -> dict:
|
|
108
|
+
"""Compact roll-up for the capability manifest + the chat meta facts."""
|
|
109
|
+
st = oracle_status(probe=False)
|
|
110
|
+
live = sorted(m for m, s in st.items() if s["live"])
|
|
111
|
+
held = sorted(m for m, s in st.items() if s["execution"] == "cloud_a100")
|
|
112
|
+
deferred = sorted(m for m, s in st.items() if s["execution"] == "deferred")
|
|
113
|
+
out = {"live": live, "held_cloud": held, "deferred": deferred,
|
|
114
|
+
"latency_classes": {m: s["latency_class"] for m, s in st.items()},
|
|
115
|
+
"note": ("Live oracles answer in seconds, ~2 min; held cloud jobs (AF3/Boltz/Chai/Protenix) are run "
|
|
116
|
+
"separately and never block; deferred outcomes are known-unknowns, never fabricated.")}
|
|
117
|
+
try:
|
|
118
|
+
from pen_stack.oracles.reliability import disagreement_widens_monotonically, disclaimer
|
|
119
|
+
out["reliability_note"] = disclaimer()
|
|
120
|
+
out["disagreement_to_interval"] = disagreement_widens_monotonically()
|
|
121
|
+
except Exception: # noqa: BLE001
|
|
122
|
+
pass
|
|
123
|
+
return out
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Structure oracles, AlphaFold3 / Boltz-2 / Chai-1 / Protenix, with cross-oracle consistency.
|
|
2
|
+
|
|
3
|
+
Each predictor returns structure confidence (pLDDT-like) under the oracle contract, with the model's native
|
|
4
|
+
uncertainty surfaced (1 − pLDDT). Where redundant oracles are available, `consistency()` combines them: their
|
|
5
|
+
agreement is a confidence signal and **their disagreement widens the reported interval** (Principle 3).
|
|
6
|
+
Heavy backends run on-demand (hosted/local GPU) and are cached; absent → deferred / cache replay.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pen_stack.oracles import build_result, cache_get, consensus
|
|
11
|
+
from pen_stack.oracles.schema import OracleResult
|
|
12
|
+
|
|
13
|
+
_STRUCT_MODELS = ["alphafold3", "boltz-2", "chai-1", "protenix"]
|
|
14
|
+
_BACKEND = {"alphafold3": "alphafold3", "boltz-2": "boltz", "chai-1": "chai_lab", "protenix": "protenix"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def predict_structure(sequence: str, model: str = "boltz-2") -> OracleResult:
|
|
18
|
+
"""Predict a structure's confidence with one model (pLDDT-like). Deferred / cache-replayed if absent."""
|
|
19
|
+
if model not in _STRUCT_MODELS:
|
|
20
|
+
raise ValueError(f"unknown structure model {model!r}; choose from {_STRUCT_MODELS}")
|
|
21
|
+
inputs = {"seq_len": len(sequence), "seq": sequence.upper(), "model": model}
|
|
22
|
+
r = build_result("structure", model, inputs=inputs, available=False,
|
|
23
|
+
note=f"{model} backend not installed (on-demand hosted/local GPU)")
|
|
24
|
+
hit = cache_get(r.provenance.cache_key)
|
|
25
|
+
if hit is not None:
|
|
26
|
+
return build_result("structure", model, inputs=inputs, value=hit.get("value"),
|
|
27
|
+
native_uncertainty=hit.get("native_uncertainty"), available=True, cached=True,
|
|
28
|
+
source="cache", note="replayed from committed oracle cache")
|
|
29
|
+
try:
|
|
30
|
+
__import__(_BACKEND[model])
|
|
31
|
+
except Exception: # noqa: BLE001
|
|
32
|
+
return r
|
|
33
|
+
return build_result("structure", model, inputs=inputs, available=False,
|
|
34
|
+
note=f"{model} present; wire the predictor for the live pLDDT/PAE value")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def consistency(sequence: str, models: list[str] | None = None) -> OracleResult:
|
|
38
|
+
"""Cross-oracle self-consistency over the available structure predictors; divergence widens the interval."""
|
|
39
|
+
models = models or _STRUCT_MODELS
|
|
40
|
+
results = [predict_structure(sequence, m) for m in models]
|
|
41
|
+
out = consensus(results, oracle="structure")
|
|
42
|
+
return out.model_copy(update={"note": (out.note or "") + f" | members tried: {models}"})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Off-request structure-oracle runner over named genome-writing complexes.
|
|
2
|
+
|
|
3
|
+
The structure oracles (AlphaFold3 / Boltz-2 / Chai-1 / Protenix) are held: a full complex prediction is a long
|
|
4
|
+
GPU / cloud batch, so it runs OFF the request path and the result is cached; the request path only ever replays
|
|
5
|
+
the cache or abstains (:func:`pen_stack.oracles.structure.consistency`). This module names the writer-substrate /
|
|
6
|
+
att-site complexes the substrate cares about, exposes a cache-or-abstain :func:`get` over the cross-oracle
|
|
7
|
+
consistency, and an :func:`ingest_boltz_confidence` hook that folds a completed off-request Boltz run's confidence
|
|
8
|
+
(pLDDT / pTM) into the cache so the request path can replay it.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from pen_stack.oracles import build_result, cache_put
|
|
16
|
+
from pen_stack.oracles.schema import OracleResult
|
|
17
|
+
from pen_stack.oracles.structure import consistency, predict_structure
|
|
18
|
+
|
|
19
|
+
# Named complexes relevant to genome writing. Sequences are supplied at call time; these entries describe what a
|
|
20
|
+
# held off-request run would target. `kind` records why a structure oracle is or is not the right tool.
|
|
21
|
+
_COMPLEXES = {
|
|
22
|
+
"ert2_4oht": {
|
|
23
|
+
"description": "ERT2 ligand-binding domain (human ESR1, UniProt P03372 res 305-554) with "
|
|
24
|
+
"4-hydroxytamoxifen, the inducible-writer chemical switch",
|
|
25
|
+
"kind": "protein_ligand"},
|
|
26
|
+
"bxb1_attb": {
|
|
27
|
+
"description": "Bxb1 serine integrase engaging its attB DNA target",
|
|
28
|
+
"kind": "protein_dna"},
|
|
29
|
+
"prime_editor_target": {
|
|
30
|
+
"description": "Prime editor (Cas9 nickase-RT fusion) engaging its primed genomic target",
|
|
31
|
+
"kind": "protein_dna"},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def complexes() -> dict:
|
|
36
|
+
"""The registry of named writer-substrate / att-site complexes a held structure run would target."""
|
|
37
|
+
return dict(_COMPLEXES)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get(protein_seq: str, models: list[str] | None = None) -> OracleResult:
|
|
41
|
+
"""Cross-oracle structure consistency for a complex's protein, cache-or-abstain (never runs the long job).
|
|
42
|
+
|
|
43
|
+
Replays committed structure-oracle caches and combines them; divergence widens the interval. When no backend
|
|
44
|
+
has been run off-request and cached, the consensus is unavailable (a deferred result), never fabricated.
|
|
45
|
+
"""
|
|
46
|
+
return consistency(protein_seq, models)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def single(protein_seq: str, model: str = "boltz-2") -> OracleResult:
|
|
50
|
+
"""One structure oracle's confidence for a sequence (cache-replayed if present, else deferred)."""
|
|
51
|
+
return predict_structure(protein_seq, model)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def ingest_boltz_confidence(protein_seq: str, out_dir: str | Path, model: str = "boltz-2") -> str:
|
|
55
|
+
"""Fold a completed OFF-REQUEST Boltz run's confidence into the structure cache, keyed as ``predict_structure``.
|
|
56
|
+
|
|
57
|
+
Reads Boltz's ``confidence_*.json`` (``complex_plddt`` on a 0-1 scale) and caches a pLDDT-like value with
|
|
58
|
+
native uncertainty ``1 - pLDDT``, so :func:`predict_structure` (and thus :func:`get`) replays it. Returns the
|
|
59
|
+
cache key. Called after a separate GPU batch completes; never on the request path.
|
|
60
|
+
"""
|
|
61
|
+
out = Path(out_dir)
|
|
62
|
+
cj = sorted(p for p in out.rglob("confidence*.json"))
|
|
63
|
+
if not cj:
|
|
64
|
+
raise FileNotFoundError(f"no confidence*.json under {out}")
|
|
65
|
+
d = json.loads(cj[0].read_text(encoding="utf-8"))
|
|
66
|
+
plddt = d.get("complex_plddt")
|
|
67
|
+
if plddt is None:
|
|
68
|
+
raise KeyError(f"complex_plddt absent in {cj[0].name}")
|
|
69
|
+
plddt = float(plddt)
|
|
70
|
+
if plddt > 1.0: # some writers report 0-100; normalise to 0-1
|
|
71
|
+
plddt = plddt / 100.0
|
|
72
|
+
inputs = {"seq_len": len(protein_seq), "seq": protein_seq.upper(), "model": model}
|
|
73
|
+
key = build_result("structure", model, inputs=inputs, available=False).provenance.cache_key
|
|
74
|
+
cache_put(key, {"value": round(plddt, 4), "native_uncertainty": round(1.0 - plddt, 4),
|
|
75
|
+
"ptm": d.get("ptm"), "iptm": d.get("iptm"), "source_file": cj[0].name})
|
|
76
|
+
return key
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Virtual-cell outcome oracles, Arc STATE / scGPT under the OracleResult contract.
|
|
2
|
+
|
|
3
|
+
A perturbation-response prediction is a CANDIDATE (a hypothesis), never a claim. It is OOD-gated: a cell context
|
|
4
|
+
or perturbation outside the model's documented validity envelope sets `extrapolating=True` / `in_scope=False`,
|
|
5
|
+
because the field's own evidence (Arc's Virtual Cell Challenge) is that perturbation models do not yet
|
|
6
|
+
consistently beat naive baselines and do not generalize to unseen contexts. Heavy backends run on-demand and are
|
|
7
|
+
cached; absent → deferred / cache replay (the value may be None, never fabricated).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
from pen_stack.oracles import build_result, cache_get, cache_put
|
|
14
|
+
from pen_stack.oracles.schema import OracleResult
|
|
15
|
+
|
|
16
|
+
# documented in-distribution envelope (representative trained human cell contexts + perturbation kinds).
|
|
17
|
+
_IN_DISTRIBUTION_CONTEXTS = {
|
|
18
|
+
"k562", "hepg2", "jurkat", "hek293", "hek293t", "h1_hesc", "h1", "a549", "thp1", "raw264",
|
|
19
|
+
"hela", "mcf7", "ipsc", "pbmc", "cd4_t", "cd8_t", "hspc",
|
|
20
|
+
}
|
|
21
|
+
_VALID_PERTURBATION_KINDS = {"genetic", "crispr", "knockout", "knockdown", "overexpression",
|
|
22
|
+
"chemical", "drug", "cytokine"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _normalize(s) -> str:
|
|
26
|
+
return str(s or "").strip().lower().replace("-", "").replace(" ", "")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def in_distribution(cell_state: str, perturbation: dict) -> bool:
|
|
30
|
+
"""True iff the cell context is a documented trained context AND the perturbation kind is supported."""
|
|
31
|
+
ctx_ok = _normalize(cell_state) in {_normalize(c) for c in _IN_DISTRIBUTION_CONTEXTS}
|
|
32
|
+
kind = _normalize(perturbation.get("kind") or perturbation.get("type"))
|
|
33
|
+
kind_ok = (not kind) or kind in {_normalize(k) for k in _VALID_PERTURBATION_KINDS}
|
|
34
|
+
return bool(ctx_ok and kind_ok)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def predict_response(cell_state: str, perturbation: dict, *, model: str = "state",
|
|
38
|
+
live: bool = False) -> OracleResult:
|
|
39
|
+
"""Virtual-cell outcome oracle (Arc STATE / scGPT). OOD-gated: a context outside the scope card ->
|
|
40
|
+
`extrapolating`, never a confident claim. Cached/deferred when the backend is absent (replay is CI default)."""
|
|
41
|
+
extrap = not in_distribution(cell_state, perturbation)
|
|
42
|
+
inputs = {"cell_state": cell_state, "perturbation": perturbation, "live": bool(live)}
|
|
43
|
+
key_obj = build_result("vcell", model, inputs=inputs)
|
|
44
|
+
hit = cache_get(key_obj.provenance.cache_key)
|
|
45
|
+
if hit is not None:
|
|
46
|
+
return build_result("vcell", model, inputs=inputs, value=hit.get("value"),
|
|
47
|
+
native_uncertainty=hit.get("native_uncertainty"), available=True, cached=True,
|
|
48
|
+
source="cache", extrapolating=extrap, in_scope=not extrap,
|
|
49
|
+
note="replayed from committed oracle cache")
|
|
50
|
+
# Live hook (uniform with the other oracles): a State *Transition* server can be stood up to predict a
|
|
51
|
+
# perturbation response; if PEN_STACK_VCELL_URL is set + up, use it. No such server ships by default,
|
|
52
|
+
# Arc STATE's SE-600M only EMBEDS cells (needs an scRNA AnnData), while a real perturbation OUTCOME needs
|
|
53
|
+
# the ST model + a reference cell population. So this path normally defers, and we DO NOT fabricate a number.
|
|
54
|
+
url = os.getenv("PEN_STACK_VCELL_URL")
|
|
55
|
+
if os.getenv("PEN_STACK_ORACLE_NET") == "1" and url:
|
|
56
|
+
try:
|
|
57
|
+
import requests
|
|
58
|
+
resp = requests.post(f"{url.rstrip('/')}/predict",
|
|
59
|
+
json={"cell_state": cell_state, "perturbation": perturbation},
|
|
60
|
+
timeout=float(os.getenv("PEN_STACK_MODEL_TIMEOUT", "300"))).json()
|
|
61
|
+
cache_put(key_obj.provenance.cache_key, {"value": resp})
|
|
62
|
+
return build_result("vcell", model, inputs=inputs, value=resp, available=True, source="local_gpu",
|
|
63
|
+
extrapolating=extrap, in_scope=not extrap,
|
|
64
|
+
note="Arc STATE transition server prediction (OOD-gated). Still a candidate hypothesis.")
|
|
65
|
+
except Exception: # noqa: BLE001 - server absent/down → defer
|
|
66
|
+
pass
|
|
67
|
+
# default: deferred (value None), the contract + OOD gate are real; the OUTCOME is a known-unknown
|
|
68
|
+
return build_result("vcell", model, inputs=inputs, value=None, available=False,
|
|
69
|
+
extrapolating=extrap, in_scope=not extrap,
|
|
70
|
+
note=(f"{model} perturbation-OUTCOME deferred; OOD={extrap}. The Arc STATE model is "
|
|
71
|
+
"installable (`pip install arc-state`; SE-600M embeds cells), but a trustworthy "
|
|
72
|
+
"perturbation response needs the State-Transition model + a reference scRNA "
|
|
73
|
+
"population, and even SOTA does not yet consistently beat naive baselines (Arc VCC), "
|
|
74
|
+
"so the magnitude stays a known-unknown rather than a fabricated number."))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""pen_stack.planner - see the PEN-STACK program doc."""
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""ADA-risk.
|
|
2
|
+
|
|
3
|
+
A protein's MHC-II epitope load (presentation potential) is necessary but not sufficient for anti-drug antibodies
|
|
4
|
+
(ADA): **self** proteins carry MHC-II epitopes too, yet are tolerated (central tolerance deletes self-reactive T
|
|
5
|
+
cells). What drives ADA is **non-self** (foreign) presentable epitopes. So:
|
|
6
|
+
|
|
7
|
+
ADA-risk = MHC-II epitope density (real, NetMHCIIpan-4.0) x foreignness
|
|
8
|
+
|
|
9
|
+
where **foreignness is the protein ORIGIN** (self vs bacterial/viral/phage), the authoritative, definitive
|
|
10
|
+
self/non-self signal (central tolerance), not a heuristic. The **real human-proteome 9-mer self-match** (computed
|
|
11
|
+
on the VM against the full UniProt human reference proteome) is reported as a cross-check. NO heuristic fallback:
|
|
12
|
+
when the MHC-II density is not cached (NetMHCIIpan not run) OR the origin is unknown, ADA-risk **abstains** (an
|
|
13
|
+
known-unknown) rather than guessing.
|
|
14
|
+
|
|
15
|
+
a population-level proxy, never a patient-specific ADA titer (a known-unknown). The calibration
|
|
16
|
+
against a public ADA-incidence set runs through the EXISTING calibrate_axis gate and stays at public-data power.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from pen_stack.planner.immune_mhc2 import _real_cache, mhc2_epitope_load, writer_sequences
|
|
21
|
+
|
|
22
|
+
ADA_DOIS = ["10.1038/s41467-021-25414-9"] # Cas9 MHC-II CD4 immunogenicity (Simhadri 2021)
|
|
23
|
+
_FOREIGN = {"self": 0.0, "foreign": 1.0}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def real_self_match(name: str) -> dict | None:
|
|
27
|
+
"""The REAL human-proteome 9-mer self-match for a bundled antigen (cross-check; computed on the VM against the
|
|
28
|
+
full UniProt human reference proteome). Foreign proteins ~0, human ~1. None if not in the cache."""
|
|
29
|
+
rec = (_real_cache().get("self_match") or {}).get(name)
|
|
30
|
+
if rec is None:
|
|
31
|
+
return None
|
|
32
|
+
return {"human_9mer_match_fraction": rec.get("fraction"), "n_9mers": rec.get("n"),
|
|
33
|
+
"reference": (_real_cache().get("self_match_meta") or {}).get("reference",
|
|
34
|
+
"UniProt human reference proteome (full)")}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def ada_risk(seq: str, origin: str | None = None, name: str | None = None) -> dict:
|
|
38
|
+
"""ADA-risk = real MHC-II epitope density x foreignness(origin). Abstains (no proxy/heuristic) when the MHC-II
|
|
39
|
+
density is uncached or the origin is unknown. Higher ada_risk_score = MORE ADA risk; ada_immune_score = 1 - it."""
|
|
40
|
+
el = mhc2_epitope_load(seq, name)
|
|
41
|
+
density = el.get("epitope_density")
|
|
42
|
+
foreign = _FOREIGN.get(str(origin)) if origin is not None else None
|
|
43
|
+
sm = real_self_match(name) if name else None
|
|
44
|
+
base = {"epitope_density": density, "foreignness": foreign, "origin": origin,
|
|
45
|
+
"self_match_human_proteome": sm, "dois": ADA_DOIS,
|
|
46
|
+
"filter": "central tolerance: foreign (non-self) MHC-II epitopes drive ADA; self tolerated. Foreignness "
|
|
47
|
+
"= protein origin (authoritative); MHC-II density = NetMHCIIpan-4.0; real human-proteome "
|
|
48
|
+
"self-match reported as cross-check (no heuristic).",
|
|
49
|
+
"status": "population-level; patient ADA titer / magnitude is a known-unknown"}
|
|
50
|
+
if density is None or foreign is None:
|
|
51
|
+
return {**base, "ada_risk_score": None, "ada_immune_score": None, "backend": "abstain",
|
|
52
|
+
"note": ("abstains (no proxy): " + ("NetMHCIIpan-4.0 not run for this sequence" if density is None
|
|
53
|
+
else "protein origin unknown, foreignness is not guessed"))}
|
|
54
|
+
risk = round(density * foreign, 4)
|
|
55
|
+
return {**base, "ada_risk_score": risk, "ada_immune_score": round(1.0 - risk, 4), "backend": "real"}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def ada_risk_named(name: str) -> dict:
|
|
59
|
+
"""ADA-risk for a bundled writer/control protein by name (uses its declared origin + the real MHC-II cache)."""
|
|
60
|
+
rec = writer_sequences().get(name)
|
|
61
|
+
if not rec:
|
|
62
|
+
return {"available": False, "note": f"no bundled sequence {name!r}"}
|
|
63
|
+
return {"available": True, "name": name, "family": rec.get("family"),
|
|
64
|
+
**ada_risk(rec["seq"], rec.get("origin"), name=name)}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Anti-PEG immunity axis for PEGylated LNP.
|
|
2
|
+
|
|
3
|
+
Covers pre-existing / induced anti-PEG antibodies, which
|
|
4
|
+
gate RE-DOSING of PEGylated LNP. Anti-PEG is a property of the PEGylation chemistry, not of any encoded
|
|
5
|
+
sequence, so, exactly like the anti-vector seroprevalence oracle, it is grounded in published serosurvey
|
|
6
|
+
DATA (configs/antipeg.yaml, a population prevalence range with DOIs):
|
|
7
|
+
|
|
8
|
+
preexisting_antipeg_score = 1 - midpoint(anti_PEG_prevalence_pct) / 100 # 1 = fewest excluded
|
|
9
|
+
|
|
10
|
+
Answers through the OracleResult contract (output_kind="baseline"). ABSTAINS for non-PEGylated vehicles.
|
|
11
|
+
SCOPE: a POPULATION prevalence (a range, region/age/assay-dependent), NOT a given patient's anti-PEG titer
|
|
12
|
+
(a clinical test, patient-specific -> a known-unknown); induced anti-PEG after dose 1 is a separate, larger
|
|
13
|
+
dynamic (noted, not modelled).
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from functools import lru_cache
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
from pen_stack._resources import resource
|
|
22
|
+
from pen_stack.oracles.schema import OracleResult, Provenance
|
|
23
|
+
|
|
24
|
+
_SCOPE_CARD = "antipeg"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@lru_cache(maxsize=1)
|
|
28
|
+
def _table() -> dict:
|
|
29
|
+
return yaml.safe_load(resource("configs/antipeg.yaml").read_text(encoding="utf-8"))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _all_dois() -> list[str]:
|
|
33
|
+
return list((_table().get("prevalence") or {}).get("dois", []) or [])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _prov(**extra) -> Provenance:
|
|
37
|
+
return Provenance(model="anti_peg_seroprevalence", version=str(_table().get("version", "1.0")),
|
|
38
|
+
source="cache", extra=extra)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def antipeg_oracle(vehicle_name: str, pegylated: bool | None = None) -> OracleResult:
|
|
42
|
+
"""Anti-PEG pre-existing immunity for a vehicle, as an OracleResult.
|
|
43
|
+
|
|
44
|
+
- PEGylated vehicle -> preexisting_antipeg_score = 1 - midpoint(prevalence)/100 (gates RE-DOSING).
|
|
45
|
+
- non-PEGylated vehicle -> ABSTAINS (available=False, value=None): the anti-PEG axis is not applicable.
|
|
46
|
+
`pegylated` overrides the config's vehicle list when given. Never fabricates a patient titer."""
|
|
47
|
+
t = _table()
|
|
48
|
+
is_peg = pegylated if pegylated is not None else (vehicle_name in (t.get("pegylated_vehicles") or []))
|
|
49
|
+
if not is_peg:
|
|
50
|
+
return OracleResult(oracle="genome", value=None, provenance=_prov(), scope_card=_SCOPE_CARD,
|
|
51
|
+
in_scope=False, available=False, output_kind="baseline",
|
|
52
|
+
note=f"non-PEGylated vehicle {vehicle_name!r}: anti-PEG axis not applicable (abstains).")
|
|
53
|
+
|
|
54
|
+
rec = t["prevalence"]
|
|
55
|
+
lo, hi = rec["anti_peg_prevalence_pct"]
|
|
56
|
+
mid = (lo + hi) / 2.0
|
|
57
|
+
score = max(0.0, min(1.0, 1.0 - mid / 100.0))
|
|
58
|
+
return OracleResult(
|
|
59
|
+
oracle="genome",
|
|
60
|
+
value={"preexisting_antipeg_score": round(score, 3), "anti_peg_prevalence_pct": [lo, hi],
|
|
61
|
+
"midpoint_pct": mid, "gates": "re-dosing", "dois": rec.get("dois", [])},
|
|
62
|
+
provenance=_prov(dois=rec.get("dois", [])), native_uncertainty=round((hi - lo) / 200.0, 4),
|
|
63
|
+
scope_card=_SCOPE_CARD, in_scope=True, extrapolating=False, output_kind="baseline", available=True,
|
|
64
|
+
note=(f"population anti-PEG prevalence {lo}-{hi}% -> preexisting_antipeg_score={score:.3f}; gates "
|
|
65
|
+
"RE-DOSING of PEGylated LNP. A POPULATION range (region/age/assay-dependent) - NOT a patient's "
|
|
66
|
+
"anti-PEG titer (a known-unknown); induced anti-PEG after dose 1 is a separate, larger dynamic "
|
|
67
|
+
"(not modelled)."))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def computed_antipeg_score(vehicle_name: str, pegylated: bool | None = None) -> tuple[float | None, OracleResult]:
|
|
71
|
+
"""Convenience: (anti-PEG score or None, full OracleResult). None when the oracle abstains (non-PEG).
|
|
72
|
+
Never fabricates."""
|
|
73
|
+
r = antipeg_oracle(vehicle_name, pegylated)
|
|
74
|
+
val = (r.value or {}).get("preexisting_antipeg_score") if (r.available and r.value) else None
|
|
75
|
+
return val, r
|