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,121 @@
|
|
|
1
|
+
"""Writer-variant critique, serine-integrase hyperactive mutants.
|
|
2
|
+
|
|
3
|
+
Extends the writer-verification (`atlas.writer_verify`, which scores ISCro4 bridge-recombinase variants
|
|
4
|
+
against the Perry DMS) to the **serine integrases**, using the directed-evolution hyperactive-mutant tables of
|
|
5
|
+
Hew, Gupta, Sato et al. (Nucleic Acids Res 2024, 52(14):e64, 10.1093/nar/gkae534) and the foundational PhiC31
|
|
6
|
+
mutants of Keravala et al. (Mol Ther 2009, 10.1038/mt.2008.241).
|
|
7
|
+
|
|
8
|
+
Scope spine (carried forward): a variant score is a CANDIDATE plausibility, never a measured-activity claim.
|
|
9
|
+
Two distinct things are kept separate, and the SECOND is reported as the scientifically-correct caveat:
|
|
10
|
+
* **Retrospective recovery**, the measured hyperactive mutants (fold > 1) outrank wild-type. This is a
|
|
11
|
+
catalogue criterion over a FROZEN, DOI'd panel of real fold-improvements (deterministic, CI-safe), it
|
|
12
|
+
recovers the known hyperactive mutations, but it is NOT a blind sequence-only predictor (it uses the measured
|
|
13
|
+
folds), exactly as `writer_verify.blind_recovery` is labelled.
|
|
14
|
+
* **LM recovery vs a conservation baseline**, can a protein LM (ESM3/Evo2) or conservation RANK the hyperactive
|
|
15
|
+
mutants above WT *blind*? This is the genuinely hard, falsifiable claim. We attempt it via the oracle when the
|
|
16
|
+
model server is available and **report the result verbatim**, including the expected NEGATIVE: protein LMs
|
|
17
|
+
score *naturalness*, while hyperactive engineered mutants are **gain-of-function** and need not be LM-favoured.
|
|
18
|
+
Deferred (no fabrication) when the server is absent.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
|
|
24
|
+
# FROZEN, DOI'd panel of REAL serine-integrase variants + measured fold-improvement over wild-type.
|
|
25
|
+
# fold = measured integration-efficiency fold over WT (>1 = hyperactive); WT anchor = 1.0.
|
|
26
|
+
# Bxb1 combination "c22" = I87L + H95Y + V122M + A369P + E434G (Hew NAR 2024); 11.2-fold in K562 (2.7% -> 30.3%).
|
|
27
|
+
_HYPERACTIVE = {
|
|
28
|
+
# integrase, variant, fold_over_wt, basis (cell/context), doi
|
|
29
|
+
"Bxb1_WT": {"integrase": "Bxb1", "fold": 1.00, "basis": "WT anchor (2.7% K562)", "doi": "10.1093/nar/gkae534"},
|
|
30
|
+
"Bxb1_I87L": {"integrase": "Bxb1", "fold": 1.30, "basis": "single mutation (component of c22)", "doi": "10.1093/nar/gkae534"},
|
|
31
|
+
"Bxb1_H95Y": {"integrase": "Bxb1", "fold": 1.30, "basis": "single mutation (component of c22)", "doi": "10.1093/nar/gkae534"},
|
|
32
|
+
"Bxb1_V122M": {"integrase": "Bxb1", "fold": 1.30, "basis": "single mutation (component of c22)", "doi": "10.1093/nar/gkae534"},
|
|
33
|
+
"Bxb1_A369P": {"integrase": "Bxb1", "fold": 1.30, "basis": "single mutation (component of c22)", "doi": "10.1093/nar/gkae534"},
|
|
34
|
+
"Bxb1_E434G": {"integrase": "Bxb1", "fold": 1.30, "basis": "single mutation (component of c22)", "doi": "10.1093/nar/gkae534"},
|
|
35
|
+
"Bxb1_L8-5": {"integrase": "Bxb1", "fold": 2.50, "basis": "best single-step IntePACE variant", "doi": "10.1093/nar/gkae534"},
|
|
36
|
+
"Bxb1_c22": {"integrase": "Bxb1", "fold": 11.2, "basis": "combination (2.7%->30.3% K562)", "doi": "10.1093/nar/gkae534"},
|
|
37
|
+
"PhiC31_WT": {"integrase": "PhiC31", "fold": 1.00, "basis": "WT anchor", "doi": "10.1038/mt.2008.241"},
|
|
38
|
+
"PhiC31_P2": {"integrase": "PhiC31", "fold": 2.00, "basis": "Keravala P2 (2x WT)", "doi": "10.1038/mt.2008.241"},
|
|
39
|
+
"PhiC31_P2-L2-1": {"integrase": "PhiC31", "fold": 9.30, "basis": "evolved from P2 (1.3%->12.1% ROSA26)", "doi": "10.1093/nar/gkae534"},
|
|
40
|
+
"PhiC31_P3-L1-2": {"integrase": "PhiC31", "fold": 14.2, "basis": "evolved P3 (18.4% ROSA26)", "doi": "10.1093/nar/gkae534"},
|
|
41
|
+
}
|
|
42
|
+
# the c22 component mutations (1-based positions), the conserved-residue check should NOT flag these as core
|
|
43
|
+
_C22_MUTATIONS = ["I87L", "H95Y", "V122M", "A369P", "E434G"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class WriterVariantScore:
|
|
48
|
+
variant: str
|
|
49
|
+
integrase: str
|
|
50
|
+
measured_fold: float
|
|
51
|
+
hyperactive: bool
|
|
52
|
+
claimable: bool
|
|
53
|
+
note: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def hyperactive_panel(integrase: str | None = None) -> dict:
|
|
57
|
+
"""The frozen, DOI'd serine-integrase hyperactive-mutant panel (real measured folds). Filtered to a single
|
|
58
|
+
integrase when given (matching the by_integrase filter); the full panel when omitted."""
|
|
59
|
+
return {k: dict(v) for k, v in _HYPERACTIVE.items() if integrase is None or v["integrase"] == integrase}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def hyperactive_recovery(integrase: str | None = None) -> dict:
|
|
63
|
+
"""Retrospective recovery: the measured hyperactive mutants (fold > 1) rank ABOVE wild-type for each
|
|
64
|
+
integrase. A catalogue criterion over the frozen DOI'd panel, recovers the known hyperactive mutations, but
|
|
65
|
+
(like writer_verify.blind_recovery) is NOT a blind sequence-only predictor. Deterministic + CI-safe."""
|
|
66
|
+
panel = {k: v for k, v in _HYPERACTIVE.items()
|
|
67
|
+
if integrase is None or v["integrase"] == integrase}
|
|
68
|
+
by_int: dict = {}
|
|
69
|
+
for k, v in panel.items():
|
|
70
|
+
by_int.setdefault(v["integrase"], []).append((k, v["fold"]))
|
|
71
|
+
results = {}
|
|
72
|
+
for integ, items in by_int.items():
|
|
73
|
+
ranked = sorted(items, key=lambda kv: kv[1], reverse=True)
|
|
74
|
+
wt = next((f for k, f in items if k.endswith("_WT")), 1.0)
|
|
75
|
+
hyper = [k for k, f in items if f > 1.0]
|
|
76
|
+
top = ranked[0][0]
|
|
77
|
+
results[integ] = {
|
|
78
|
+
"n": len(items), "top": top, "wt_fold": wt,
|
|
79
|
+
"hyperactive_variants": hyper,
|
|
80
|
+
"all_hyperactive_outrank_wt": all(f > wt for k, f in items if not k.endswith("_WT")),
|
|
81
|
+
"ranking": [{"variant": k, "fold": f} for k, f in ranked],
|
|
82
|
+
}
|
|
83
|
+
return {"available": True, "model": "frozen_NAR2024_panel", "by_integrase": results,
|
|
84
|
+
"note": "recovers known serine-integrase hyperactive mutants (Hew NAR 2024 / Keravala 2009) above "
|
|
85
|
+
"wild-type, a retrospective catalogue criterion, NOT a blind sequence-only predictor."}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def lm_recovery(model: str = "esm3") -> dict:
|
|
89
|
+
"""The HARD, falsifiable claim: can a protein LM rank the hyperactive mutants above WT *blind*? Attempts the
|
|
90
|
+
LM oracle; defers (no fabrication) when the model server is absent. Reports verbatim, including the expected
|
|
91
|
+
negative, since LMs score naturalness and hyperactive mutants are gain-of-function."""
|
|
92
|
+
try:
|
|
93
|
+
from pen_stack.oracles.protein_design import _oracle_net_enabled
|
|
94
|
+
if not _oracle_net_enabled():
|
|
95
|
+
raise RuntimeError("oracle net disabled")
|
|
96
|
+
# The ESM3 oracle is generative, not a per-variant fitness scorer; a clean blind per-variant likelihood
|
|
97
|
+
# endpoint is not exposed -> we do not fabricate a score. Defer with the scientific caveat.
|
|
98
|
+
return {"available": False, "model": model,
|
|
99
|
+
"note": "deferred: the protein-LM oracle is generative (no blind per-variant fitness endpoint). "
|
|
100
|
+
"Scientifically, LM naturalness need NOT recover gain-of-function hyperactivity, this is "
|
|
101
|
+
"reported as a known limitation, not a manufactured positive."}
|
|
102
|
+
except Exception: # noqa: BLE001
|
|
103
|
+
return {"available": False, "model": model,
|
|
104
|
+
"note": "deferred (model server / oracle net absent); see hyperactive_recovery (retrospective). "
|
|
105
|
+
"LM naturalness != engineered hyperactivity (gain-of-function), limitation."}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def score_writer_variants(integrase: str, variants: list[str]) -> list[WriterVariantScore]:
|
|
109
|
+
"""Score serine-integrase variants against the frozen measured panel. Measured variants are claimable with
|
|
110
|
+
their fold; unmeasured variants are flagged NOT claimable (no activity asserted), the scope spine."""
|
|
111
|
+
out: list[WriterVariantScore] = []
|
|
112
|
+
for v in variants:
|
|
113
|
+
key = v if v in _HYPERACTIVE else f"{integrase}_{v}"
|
|
114
|
+
if key in _HYPERACTIVE:
|
|
115
|
+
rec = _HYPERACTIVE[key]
|
|
116
|
+
out.append(WriterVariantScore(v, rec["integrase"], rec["fold"], rec["fold"] > 1.0, True,
|
|
117
|
+
f"measured fold {rec['fold']}x over WT ({rec['basis']}; {rec['doi']})"))
|
|
118
|
+
else:
|
|
119
|
+
out.append(WriterVariantScore(v, integrase, float("nan"), False, False,
|
|
120
|
+
"not in the measured panel, plausibility only, NO activity claim"))
|
|
121
|
+
return out
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Optional Gymnasium interface for PEN-STACK. Requires the `[env]` extra (gymnasium)."""
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Gymnasium environment for genome-write planning, the train/eval surface.
|
|
2
|
+
|
|
3
|
+
The interface began *thin* (insertion only) and is now a **full environment** whose state is
|
|
4
|
+
a partial design across **all write types**, whose every action is checked by the **rule-grounded
|
|
5
|
+
verifier** (`pen_stack.verify.verify`), and whose reward is the **legal, calibrated plan score** (the planner
|
|
6
|
+
objective scaled by the L4 calibrated confidence, minus soft-rule penalties). An episode is a complete legal
|
|
7
|
+
plan **or a justified refusal** (an explicit abstain action):
|
|
8
|
+
|
|
9
|
+
stage 0: WRITE TYPE -> stage 1: SITE -> stage 2: WRITER family ->
|
|
10
|
+
stage 3: CARGO bucket -> stage 4: DELIVERY vehicle -> terminate (verify -> reward)
|
|
11
|
+
|
|
12
|
+
At any stage the agent may take the reserved **abstain** action (``action == action_space.n - 1``) and end
|
|
13
|
+
the episode with a refusal: refusing beats committing to an *illegal* plan (refusal reward > illegal penalty),
|
|
14
|
+
but a good legal plan beats refusing, the contract that makes "abstention over guessing" measurable.
|
|
15
|
+
|
|
16
|
+
**Explicitly an INTERFACE + EVALUATION HARNESS, not an RL-superiority claim.** The genome-writing decision is
|
|
17
|
+
near-one-shot; the greedy(planner) policy *is* the deterministic optimum and is the reference. No learned
|
|
18
|
+
policy is claimed to beat it (the `greedy >= random` check is a sanity test, not a result). Behind the
|
|
19
|
+
optional ``[env]`` extra (gymnasium); the rest of PEN-STACK does not import this module.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
import pandas as pd
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import gymnasium as gym
|
|
28
|
+
from gymnasium import spaces
|
|
29
|
+
_HAVE_GYM = True
|
|
30
|
+
except Exception: # noqa: BLE001 - gymnasium only in the [env] extra
|
|
31
|
+
_HAVE_GYM = False
|
|
32
|
+
gym = None
|
|
33
|
+
spaces = None
|
|
34
|
+
|
|
35
|
+
from pen_stack.planner.optimize import (
|
|
36
|
+
EditIntent,
|
|
37
|
+
load_intent_weights,
|
|
38
|
+
writer_activity_by_family,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
WRITE_TYPES = ["insertion", "excision", "inversion", "replacement",
|
|
42
|
+
"regulatory_rewrite", "landing_pad_install", "multiplex"]
|
|
43
|
+
WRITER_FAMILIES = ["bridge_IS110", "seek_IS1111", "CAST_VK", "serine_integrase",
|
|
44
|
+
"PE_integrase", "Cas9", "Cas12a"]
|
|
45
|
+
# writers whose output is DNA (AAV/lenti/HDAd-compatible). Cas9/Cas12a deliver RNP.
|
|
46
|
+
_DNA_WRITERS = ["bridge_IS110", "seek_IS1111", "CAST_VK", "serine_integrase", "PE_integrase"]
|
|
47
|
+
CARGO_BUCKETS = [1000, 3000, 6000, 12000, 30000] # bp
|
|
48
|
+
_N_STAGES = 5
|
|
49
|
+
|
|
50
|
+
# reward shaping constants (pre-registered in prereg/ws_env.yaml)
|
|
51
|
+
_ILLEGAL_PENALTY = -1.0 # committing to an illegal plan is the worst outcome
|
|
52
|
+
_ABSTAIN_REWARD = 0.05 # a justified refusal beats an illegal plan, loses to a good legal one
|
|
53
|
+
_SOFT_PENALTY = 0.1 # per soft-rule flag (e.g. split-AAV efficiency)
|
|
54
|
+
_CARGO_SHORT_PENALTY = 0.1 # chosen bucket smaller than the target insert
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def delivery_vehicles() -> list[str]:
|
|
58
|
+
from pen_stack.planner.delivery_vehicles import names
|
|
59
|
+
return list(names())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def demo_candidates(n: int = 8, seed: int = 0) -> pd.DataFrame:
|
|
63
|
+
"""A small synthetic candidate table (safety, p_durable, reachable_tier1) so the env runs without the
|
|
64
|
+
atlas. Real use passes the writability atlas rows instead."""
|
|
65
|
+
rng = np.random.default_rng(seed)
|
|
66
|
+
fams = [";".join(rng.choice(WRITER_FAMILIES, size=rng.integers(2, 5), replace=False)) for _ in range(n)]
|
|
67
|
+
return pd.DataFrame({"chrom": ["chr1"] * n, "bin": list(range(n)),
|
|
68
|
+
"safety": rng.uniform(0.3, 0.95, n).round(3),
|
|
69
|
+
"p_durable": rng.uniform(0.3, 0.95, n).round(3),
|
|
70
|
+
"reachable_tier1": fams})
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _base():
|
|
74
|
+
return gym.Env if _HAVE_GYM else object
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def writer_form(family: str | None) -> str:
|
|
78
|
+
"""DNA for integrase/recombinase/prime-editor writers; RNP for Cas9/Cas12a."""
|
|
79
|
+
return "DNA" if family in _DNA_WRITERS else "RNP"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class GenomeWritingEnv(_base()):
|
|
83
|
+
"""Full Gymnasium environment over the router + verifier (see module docstring).
|
|
84
|
+
|
|
85
|
+
State = partial design; actions build it stage by stage; the terminal reward is the verifier's legality
|
|
86
|
+
gate times the L4 calibrated plan confidence. The reserved abstain action ends the episode with a refusal.
|
|
87
|
+
"""
|
|
88
|
+
metadata = {"render_modes": []}
|
|
89
|
+
|
|
90
|
+
def __init__(self, candidates: pd.DataFrame | None = None,
|
|
91
|
+
intent: str | EditIntent = "safe_harbour_insertion", cargo_bp: int = 3000, seed: int = 0):
|
|
92
|
+
if not _HAVE_GYM:
|
|
93
|
+
raise ImportError("GenomeWritingEnv needs the optional [env] extra: pip install pen-stack[env]")
|
|
94
|
+
super().__init__()
|
|
95
|
+
self.cands = (candidates if candidates is not None else demo_candidates(seed=seed)).reset_index(drop=True)
|
|
96
|
+
self.intent = EditIntent(intent) if not isinstance(intent, EditIntent) else intent
|
|
97
|
+
self.cargo_bp = int(cargo_bp) # target insert size the plan must accommodate
|
|
98
|
+
self.w = load_intent_weights()["intents"][self.intent.value]
|
|
99
|
+
self.activity = writer_activity_by_family()
|
|
100
|
+
self.vehicles = delivery_vehicles()
|
|
101
|
+
self.n_sites = len(self.cands)
|
|
102
|
+
self._stage_sizes = [len(WRITE_TYPES), self.n_sites, len(WRITER_FAMILIES),
|
|
103
|
+
len(CARGO_BUCKETS), len(self.vehicles)]
|
|
104
|
+
# one fixed Discrete space sized to the largest stage + 1 reserved ABSTAIN action.
|
|
105
|
+
self._abstain = max(self._stage_sizes)
|
|
106
|
+
self.action_space = spaces.Discrete(self._abstain + 1)
|
|
107
|
+
# observation: [stage_frac, write_type_frac, site_safety, site_p_durable, writer_activity,
|
|
108
|
+
# cargo_frac, delivery_cap_frac, legal_flag]
|
|
109
|
+
self.observation_space = spaces.Box(low=0.0, high=1.0, shape=(8,), dtype=np.float32)
|
|
110
|
+
self._rng = np.random.default_rng(seed)
|
|
111
|
+
self.reset(seed=seed)
|
|
112
|
+
|
|
113
|
+
# ---- helpers -------------------------------------------------------------------------------
|
|
114
|
+
def _obs(self) -> np.ndarray:
|
|
115
|
+
site = self.cands.iloc[self._site] if self._site is not None else None
|
|
116
|
+
cap = 0.0
|
|
117
|
+
if self._delivery:
|
|
118
|
+
from pen_stack.planner.delivery_vehicles import vehicle
|
|
119
|
+
c = (vehicle(self._delivery) or {}).get("cargo_capacity_bp")
|
|
120
|
+
cap = min(1.0, (c or 0) / 100000.0)
|
|
121
|
+
return np.array([
|
|
122
|
+
self._stage / _N_STAGES,
|
|
123
|
+
(WRITE_TYPES.index(self._write_type) / len(WRITE_TYPES)) if self._write_type else 0.0,
|
|
124
|
+
float(site["safety"]) if site is not None else 0.0,
|
|
125
|
+
float(site["p_durable"]) if site is not None else 0.0,
|
|
126
|
+
float(self.activity.get(self._writer, 0.0)) if self._writer else 0.0,
|
|
127
|
+
(self._cargo / max(CARGO_BUCKETS)) if self._cargo else 0.0,
|
|
128
|
+
cap,
|
|
129
|
+
1.0 if self._legal else 0.0,
|
|
130
|
+
], dtype=np.float32)
|
|
131
|
+
|
|
132
|
+
def site_options(self) -> list[int]:
|
|
133
|
+
return list(range(self.n_sites))
|
|
134
|
+
|
|
135
|
+
def writer_options(self) -> list[str]:
|
|
136
|
+
"""Writer families reachable at the chosen site (tier-1 reachability), or all if no site yet."""
|
|
137
|
+
if self._site is None:
|
|
138
|
+
return WRITER_FAMILIES
|
|
139
|
+
return [f for f in str(self.cands.iloc[self._site]["reachable_tier1"]).split(";") if f] or WRITER_FAMILIES
|
|
140
|
+
|
|
141
|
+
def _build_design(self):
|
|
142
|
+
from pen_stack.rules import Design
|
|
143
|
+
site = self.cands.iloc[self._site] if self._site is not None else None
|
|
144
|
+
return Design(
|
|
145
|
+
write_type=self._write_type or "insertion",
|
|
146
|
+
writer_family=self._writer,
|
|
147
|
+
writer_output_form=writer_form(self._writer),
|
|
148
|
+
cargo_bp=self._cargo,
|
|
149
|
+
delivery_vehicle=self._delivery,
|
|
150
|
+
edit_intent=self.intent.value,
|
|
151
|
+
chrom=str(site["chrom"]) if site is not None else None,
|
|
152
|
+
# per-axis scores let the verifier attach a CALIBRATED confidence (no fabrication otherwise)
|
|
153
|
+
safety=float(site["safety"]) if site is not None else None,
|
|
154
|
+
p_durable=float(site["p_durable"]) if site is not None else None,
|
|
155
|
+
writer_activity=float(self.activity.get(self._writer, 0.4)),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# ---- Gymnasium API -------------------------------------------------------------------------
|
|
159
|
+
def reset(self, seed: int | None = None, options: dict | None = None):
|
|
160
|
+
super().reset(seed=seed) # seeds gymnasium's self.np_random (env-checker contract)
|
|
161
|
+
if seed is not None:
|
|
162
|
+
self._rng = np.random.default_rng(seed)
|
|
163
|
+
self._stage = 0
|
|
164
|
+
self._write_type = None
|
|
165
|
+
self._site = None
|
|
166
|
+
self._writer = None
|
|
167
|
+
self._cargo = None
|
|
168
|
+
self._delivery = None
|
|
169
|
+
self._legal = False
|
|
170
|
+
self._refused = False
|
|
171
|
+
return self._obs(), {"stage": "write_type"}
|
|
172
|
+
|
|
173
|
+
def step(self, action: int):
|
|
174
|
+
action = int(action)
|
|
175
|
+
reward, terminated, info = 0.0, False, {}
|
|
176
|
+
if action == self._abstain: # justified refusal -> end episode
|
|
177
|
+
self._refused = True
|
|
178
|
+
terminated = True
|
|
179
|
+
reward = _ABSTAIN_REWARD
|
|
180
|
+
info = {"stage": "refused", "abstained": True,
|
|
181
|
+
"note": "refusal beats an illegal plan; loses to a good legal one"}
|
|
182
|
+
self._stage += 1
|
|
183
|
+
return self._obs(), float(reward), True, False, info
|
|
184
|
+
|
|
185
|
+
if self._stage == 0: # choose WRITE TYPE
|
|
186
|
+
self._write_type = WRITE_TYPES[action % len(WRITE_TYPES)]
|
|
187
|
+
info = {"stage": "site", "chose_write_type": self._write_type}
|
|
188
|
+
elif self._stage == 1: # choose SITE
|
|
189
|
+
self._site = self.site_options()[action % self.n_sites]
|
|
190
|
+
info = {"stage": "writer", "chose_site": int(self._site)}
|
|
191
|
+
elif self._stage == 2: # choose WRITER family
|
|
192
|
+
self._writer = WRITER_FAMILIES[action % len(WRITER_FAMILIES)]
|
|
193
|
+
info = {"stage": "cargo", "chose_writer": self._writer,
|
|
194
|
+
"writer_reachable": self._writer in self.writer_options()}
|
|
195
|
+
elif self._stage == 3: # choose CARGO bucket
|
|
196
|
+
self._cargo = CARGO_BUCKETS[action % len(CARGO_BUCKETS)]
|
|
197
|
+
info = {"stage": "delivery", "chose_cargo_bp": self._cargo}
|
|
198
|
+
elif self._stage == 4: # choose DELIVERY vehicle -> terminate
|
|
199
|
+
self._delivery = self.vehicles[action % len(self.vehicles)]
|
|
200
|
+
reward, info = self._verified_reward()
|
|
201
|
+
terminated = True
|
|
202
|
+
info = {"stage": "done", "chose_delivery": self._delivery, **info, **self.plan()}
|
|
203
|
+
self._stage += 1
|
|
204
|
+
return self._obs(), float(reward), bool(terminated), False, info
|
|
205
|
+
|
|
206
|
+
# ---- reward = legality gate x calibrated plan score ----------------------------------------
|
|
207
|
+
def _verified_reward(self) -> tuple[float, dict]:
|
|
208
|
+
from pen_stack.verify import verify
|
|
209
|
+
design = self._build_design()
|
|
210
|
+
v = verify(design)
|
|
211
|
+
site = self.cands.iloc[self._site]
|
|
212
|
+
base = (self.w["safety"] * float(site["safety"])
|
|
213
|
+
+ self.w["durability"] * float(site["p_durable"])
|
|
214
|
+
+ self.w["activity"] * float(self.activity.get(self._writer, 0.4)))
|
|
215
|
+
meta = {"legal": v.legal, "deferred": v.deferred, "confidence": v.confidence,
|
|
216
|
+
"violations": [x["rule_id"] for x in v.violations],
|
|
217
|
+
"soft_flags": [s["rule_id"] for s in v.soft_flags]}
|
|
218
|
+
if v.deferred: # unsupported/ambiguous write type -> a deterministic refusal
|
|
219
|
+
self._refused = True
|
|
220
|
+
return _ABSTAIN_REWARD, {**meta, "note": "router deferred (unsupported write type)"}
|
|
221
|
+
if not v.legal: # committed to an illegal plan -> worst outcome
|
|
222
|
+
self._legal = False
|
|
223
|
+
return _ILLEGAL_PENALTY, meta
|
|
224
|
+
self._legal = True
|
|
225
|
+
conf = v.confidence if v.confidence is not None else 0.5
|
|
226
|
+
reward = base * (0.5 + 0.5 * conf) - _SOFT_PENALTY * len(v.soft_flags)
|
|
227
|
+
if self._cargo is not None and self._cargo < self.cargo_bp:
|
|
228
|
+
reward -= _CARGO_SHORT_PENALTY
|
|
229
|
+
return float(reward), meta
|
|
230
|
+
|
|
231
|
+
def plan(self) -> dict:
|
|
232
|
+
return {"write_type": self._write_type,
|
|
233
|
+
"site": None if self._site is None else int(self._site),
|
|
234
|
+
"writer": self._writer, "cargo_bp": self._cargo, "delivery": self._delivery,
|
|
235
|
+
"intent": self.intent.value, "legal": self._legal, "refused": self._refused}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# re-export the reference policies + rollout helpers (defined in policies.py) for backward-compatible imports
|
|
239
|
+
from pen_stack.env.policies import ( # noqa: E402
|
|
240
|
+
compare_policies,
|
|
241
|
+
greedy_planner_policy,
|
|
242
|
+
random_policy,
|
|
243
|
+
rollout,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
__all__ = ["WRITE_TYPES", "WRITER_FAMILIES", "CARGO_BUCKETS", "GenomeWritingEnv", "demo_candidates",
|
|
247
|
+
"delivery_vehicles", "writer_form", "random_policy", "greedy_planner_policy", "rollout",
|
|
248
|
+
"compare_policies"]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Reference policies + rollout for the genome-writing environment.
|
|
2
|
+
|
|
3
|
+
Two reference policies run end-to-end in `GenomeWritingEnv`:
|
|
4
|
+
|
|
5
|
+
* ``random_policy``, uniform over the (fixed) action space, including the reserved abstain action.
|
|
6
|
+
* ``greedy_planner_policy``, the **deterministic optimum**: it reproduces the planner's choices stage by
|
|
7
|
+
stage and always assembles a *rule-legal* plan (a writer/cargo/delivery combination the verifier accepts),
|
|
8
|
+
so it is the reference an RL agent is compared against. **No RL superiority is claimed**, the env is an
|
|
9
|
+
interface + evaluation harness for a near-one-shot decision; ``greedy >= random`` is a sanity check, not a
|
|
10
|
+
result.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from pen_stack.env.genome_writing_env import (
|
|
17
|
+
CARGO_BUCKETS,
|
|
18
|
+
WRITE_TYPES,
|
|
19
|
+
WRITER_FAMILIES,
|
|
20
|
+
GenomeWritingEnv,
|
|
21
|
+
demo_candidates,
|
|
22
|
+
writer_form,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def random_policy(env: GenomeWritingEnv, obs, rng) -> int:
|
|
27
|
+
return int(rng.integers(0, env.action_space.n))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _best_delivery_index(env: GenomeWritingEnv, form: str, cargo: int) -> int:
|
|
31
|
+
"""Smallest-capacity vehicle compatible with the writer's output form that fits the cargo (a legal,
|
|
32
|
+
efficient choice); falls back to a physical (no-capacity) compatible vehicle."""
|
|
33
|
+
from pen_stack.planner.delivery_vehicles import vehicle
|
|
34
|
+
finite, physical = [], []
|
|
35
|
+
for i, name in enumerate(env.vehicles):
|
|
36
|
+
veh = vehicle(name) or {}
|
|
37
|
+
if form not in veh.get("compatible_cargo_form", []):
|
|
38
|
+
continue
|
|
39
|
+
cap = veh.get("cargo_capacity_bp")
|
|
40
|
+
if cap is None:
|
|
41
|
+
physical.append(i)
|
|
42
|
+
elif cap >= cargo:
|
|
43
|
+
finite.append((cap, i))
|
|
44
|
+
if finite:
|
|
45
|
+
return min(finite)[1]
|
|
46
|
+
if physical:
|
|
47
|
+
return physical[0]
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def greedy_planner_policy(env: GenomeWritingEnv, obs, rng) -> int:
|
|
52
|
+
"""The deterministic optimum at each stage: the supported write type, the best site by base score, its
|
|
53
|
+
highest-activity reachable writer, the smallest cargo bucket that fits, and a form-compatible vehicle."""
|
|
54
|
+
if env._stage == 0: # WRITE TYPE: insertion (fully supported)
|
|
55
|
+
return WRITE_TYPES.index("insertion")
|
|
56
|
+
if env._stage == 1: # SITE: best base score
|
|
57
|
+
scores = [(env.w["safety"] * float(r["safety"]) + env.w["durability"] * float(r["p_durable"]))
|
|
58
|
+
for _, r in env.cands.iterrows()]
|
|
59
|
+
return int(np.argmax(scores))
|
|
60
|
+
if env._stage == 2: # WRITER: highest-activity reachable (prefer DNA)
|
|
61
|
+
reachable = env.writer_options()
|
|
62
|
+
dna = [f for f in reachable if writer_form(f) == "DNA"] or reachable
|
|
63
|
+
best = max(dna, key=lambda f: env.activity.get(f, 0.0))
|
|
64
|
+
return WRITER_FAMILIES.index(best)
|
|
65
|
+
if env._stage == 3: # CARGO: smallest bucket that fits the target
|
|
66
|
+
fits = [i for i, b in enumerate(CARGO_BUCKETS) if b >= env.cargo_bp] or [len(CARGO_BUCKETS) - 1]
|
|
67
|
+
return fits[0]
|
|
68
|
+
# DELIVERY: smallest-capacity vehicle compatible with the writer's output form
|
|
69
|
+
return _best_delivery_index(env, writer_form(env._writer), env._cargo or env.cargo_bp)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def rollout(env: GenomeWritingEnv, policy, seed: int = 0) -> dict:
|
|
73
|
+
"""Run one episode under `policy`; return the cumulative reward + the assembled plan."""
|
|
74
|
+
rng = np.random.default_rng(seed)
|
|
75
|
+
obs, _ = env.reset(seed=seed)
|
|
76
|
+
total, term, info = 0.0, False, {}
|
|
77
|
+
while not term:
|
|
78
|
+
a = policy(env, obs, rng)
|
|
79
|
+
obs, r, term, _trunc, info = env.step(a)
|
|
80
|
+
total += r
|
|
81
|
+
return {"reward": round(total, 4), "plan": env.plan(), "terminal_info": info}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def compare_policies(seed: int = 0) -> dict:
|
|
85
|
+
"""Run the random and greedy(planner) policies on the same env, the interface smoke + sanity check. The
|
|
86
|
+
greedy policy is the deterministic optimum and assembles a legal plan; RL is NOT claimed to beat it."""
|
|
87
|
+
rnd = rollout(GenomeWritingEnv(candidates=demo_candidates(n=8, seed=seed), seed=seed),
|
|
88
|
+
random_policy, seed=seed)
|
|
89
|
+
grd = rollout(GenomeWritingEnv(candidates=demo_candidates(n=8, seed=seed), seed=seed),
|
|
90
|
+
greedy_planner_policy, seed=seed)
|
|
91
|
+
return {"random": rnd, "greedy_planner": grd,
|
|
92
|
+
"greedy_at_least_random": bool(grd["reward"] >= rnd["reward"]),
|
|
93
|
+
"greedy_plan_legal": bool(grd["plan"]["legal"]),
|
|
94
|
+
"note": "interface only, greedy(planner) is the deterministic optimum; no RL superiority claimed."}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""The living world-model knowledge graph.
|
|
2
|
+
|
|
3
|
+
`pen_stack.graph` promotes the flat tables (atlas / WT-KB / crosslink / delivery palette / write-type
|
|
4
|
+
taxonomy / GSH loci / documented writes / cell-type coverage cards) into a queryable knowledge graph: typed
|
|
5
|
+
nodes joined by typed edges, each carrying provenance + uncertainty + scope. Multi-hop design questions become
|
|
6
|
+
single grounded traversals; the gated living loop (`pen_stack.graph.ingest`) keeps it current without ever
|
|
7
|
+
auto-editing the curated truth.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pen_stack.graph.build import build_graph
|
|
12
|
+
from pen_stack.graph.query import (
|
|
13
|
+
outcomes_for_writer,
|
|
14
|
+
vehicles_for_writer,
|
|
15
|
+
writers_for_locus,
|
|
16
|
+
writers_reaching_and_deliverable,
|
|
17
|
+
)
|
|
18
|
+
from pen_stack.graph.schema import Edge, Graph, Node
|
|
19
|
+
|
|
20
|
+
__all__ = ["Graph", "Node", "Edge", "build_graph", "vehicles_for_writer", "writers_for_locus",
|
|
21
|
+
"writers_reaching_and_deliverable", "outcomes_for_writer"]
|
pen_stack/graph/build.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Build the world-model knowledge graph from the curated tables.
|
|
2
|
+
|
|
3
|
+
Parity-first (risk register): the graph is assembled from the SAME validated sources the code joins
|
|
4
|
+
, the WT-KB writer families, the delivery-vehicle palette, the write-type taxonomy, the DOI-validated GSH
|
|
5
|
+
loci, the documented writer panel, and the cell-type coverage cards, so its edges reproduce the existing
|
|
6
|
+
table joins (asserted by the parity test) before any multi-hop extension. Every edge is typed by evidence
|
|
7
|
+
kind and carries provenance + scope. Nothing here calls a network or a model; it is deterministic + CI-safe.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
from pen_stack._resources import resource
|
|
16
|
+
from pen_stack.graph.schema import Edge, Graph, Node
|
|
17
|
+
|
|
18
|
+
# writer output form (DNA cargo / RNP) per family, the same map the rule evaluators use (parity).
|
|
19
|
+
_WRITER_FORM = {"bridge_IS110": "DNA", "seek_IS1111": "DNA", "CAST_VK": "DNA", "serine_integrase": "DNA",
|
|
20
|
+
"PE_integrase": "DNA", "Cas9": "RNP", "Cas12a": "RNP", "TnpB_Fanzor": "RNP"}
|
|
21
|
+
# tier-1 reprogrammable families are near-universal at the locus level (crosslink scope: locus-level reach).
|
|
22
|
+
_TIER1 = {"bridge_IS110", "seek_IS1111", "Cas9", "Cas12a"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _yaml(path: str) -> dict:
|
|
26
|
+
return yaml.safe_load(resource(path).read_text(encoding="utf-8"))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _lst(v) -> list:
|
|
30
|
+
"""Coerce a possibly-numpy-array / None cell to a plain list (avoids ambiguous-truthiness)."""
|
|
31
|
+
if v is None:
|
|
32
|
+
return []
|
|
33
|
+
try:
|
|
34
|
+
return [x for x in v]
|
|
35
|
+
except TypeError:
|
|
36
|
+
return [v]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@lru_cache(maxsize=1)
|
|
40
|
+
def build_graph() -> Graph:
|
|
41
|
+
g = Graph()
|
|
42
|
+
import pandas as pd
|
|
43
|
+
|
|
44
|
+
# ---- writer nodes (WT-KB families) ---------------------------------------------------------
|
|
45
|
+
wtkb = pd.read_parquet(resource("pen_stack/atlas/wtkb.parquet"))
|
|
46
|
+
for _, w in wtkb.iterrows():
|
|
47
|
+
fam = str(w["family"])
|
|
48
|
+
g.add_node(Node(id=f"writer:{fam}", type="writer", props={
|
|
49
|
+
"family": fam, "mechanism_bucket": w.get("mechanism_bucket"),
|
|
50
|
+
"output_form": _WRITER_FORM.get(fam), "cargo_capacity_bp": int(w["cargo_capacity_bp"])
|
|
51
|
+
if pd.notna(w.get("cargo_capacity_bp")) else None,
|
|
52
|
+
"reachability_tier": w.get("reachability_tier"), "dsb_free": bool(w.get("dsb_free")),
|
|
53
|
+
"confidence": w.get("confidence"), "dois": _lst(w.get("key_dois"))}))
|
|
54
|
+
|
|
55
|
+
# ---- vehicle + cargo-form nodes (delivery palette) -----------------------------------------
|
|
56
|
+
veh = _yaml("configs/delivery_vehicles.yaml")["vehicles"]
|
|
57
|
+
for form in ("DNA", "mRNA", "RNP"):
|
|
58
|
+
g.add_node(Node(id=f"cargo:{form}", type="cargo", props={"form": form}))
|
|
59
|
+
for name, v in veh.items():
|
|
60
|
+
g.add_node(Node(id=f"vehicle:{name}", type="vehicle", props={
|
|
61
|
+
"cargo_capacity_bp": v.get("cargo_capacity_bp"), "integrating": v.get("integrating"),
|
|
62
|
+
"compatible_cargo_form": v.get("compatible_cargo_form", []), "dois": v.get("dois", [])}))
|
|
63
|
+
for form in v.get("compatible_cargo_form", []):
|
|
64
|
+
g.add_edge(Edge(f"vehicle:{name}", f"cargo:{form}", "carries", "curated",
|
|
65
|
+
scope="documented vehicle cargo-form", provenance={"source": "delivery_vehicles.yaml",
|
|
66
|
+
"doi": v.get("dois", [])}))
|
|
67
|
+
|
|
68
|
+
# ---- write-type nodes ----------------------------------------------------------------------
|
|
69
|
+
wts = _yaml("configs/write_types.yaml")["write_types"]
|
|
70
|
+
for wt, spec in wts.items():
|
|
71
|
+
g.add_node(Node(id=f"write_type:{wt}", type="write_type",
|
|
72
|
+
props={"status": spec.get("status"), "writer_classes": spec.get("writer_classes", [])}))
|
|
73
|
+
|
|
74
|
+
# ---- cell-type nodes (coverage cards) ------------------------------------------------------
|
|
75
|
+
cts = _yaml("configs/cell_types.yaml")["cell_types"]
|
|
76
|
+
for ct, card in cts.items():
|
|
77
|
+
g.add_node(Node(id=f"cell_type:{ct}", type="cell_type", props={
|
|
78
|
+
"tier": card.get("tier"), "ontology": card.get("efo") or card.get("ontology"),
|
|
79
|
+
"coverage": card.get("coverage"), "tracks": card.get("tracks", []), "note": card.get("note")}))
|
|
80
|
+
|
|
81
|
+
# ---- locus nodes (DOI-validated GSH) -------------------------------------------------------
|
|
82
|
+
gsh = _yaml("configs/gsh_validated_heldout.yaml")["gsh"]
|
|
83
|
+
for loc in gsh:
|
|
84
|
+
g.add_node(Node(id=f"locus:{loc['name']}", type="locus", props={
|
|
85
|
+
"tier": loc.get("tier"), "anchor_gene": loc.get("anchor_gene") or loc.get("anchor_gene_note"),
|
|
86
|
+
"doi": loc.get("doi")}))
|
|
87
|
+
|
|
88
|
+
# ---- outcome nodes (documented writes) -----------------------------------------------------
|
|
89
|
+
panel = pd.read_csv(resource("data/writer_panel.csv"))
|
|
90
|
+
|
|
91
|
+
# ---- EDGES ---------------------------------------------------------------------------------
|
|
92
|
+
writers = [f"writer:{f}" for f in wtkb["family"].astype(str)]
|
|
93
|
+
# writer -deliverable_by-> vehicle (cargo-form compatible) - PARITY with the delivery rule
|
|
94
|
+
for wid in writers:
|
|
95
|
+
form = g.nodes[wid].props["output_form"]
|
|
96
|
+
for name, v in veh.items():
|
|
97
|
+
if form in v.get("compatible_cargo_form", []):
|
|
98
|
+
g.add_edge(Edge(wid, f"vehicle:{name}", "deliverable_by", "curated",
|
|
99
|
+
scope="cargo-form compatibility (not tropism)",
|
|
100
|
+
provenance={"source": "delivery rule cargo_form_compatible"}))
|
|
101
|
+
# writer -performs-> write_type (writer_classes membership)
|
|
102
|
+
_CLASS = {"bridge_IS110": "bridge", "seek_IS1111": "bridge", "CAST_VK": "cast",
|
|
103
|
+
"serine_integrase": "serine_integrase", "PE_integrase": "pe_integrase"}
|
|
104
|
+
for wid in writers:
|
|
105
|
+
fam = g.nodes[wid].props["family"]
|
|
106
|
+
for wt, spec in wts.items():
|
|
107
|
+
classes = spec.get("writer_classes", [])
|
|
108
|
+
if "any" in classes or _CLASS.get(fam) in classes:
|
|
109
|
+
g.add_edge(Edge(wid, f"write_type:{wt}", "performs", "curated",
|
|
110
|
+
scope=spec.get("status"), provenance={"source": "write_types.yaml"}))
|
|
111
|
+
# writer -reaches-> locus (locus-level reachability; tier-1 near-universal) - predicted, scope-flagged
|
|
112
|
+
for wid in writers:
|
|
113
|
+
fam = g.nodes[wid].props["family"]
|
|
114
|
+
if fam in _TIER1:
|
|
115
|
+
for loc in gsh:
|
|
116
|
+
g.add_edge(Edge(wid, f"locus:{loc['name']}", "reaches", "predicted", confidence=None,
|
|
117
|
+
scope="locus-level reachability (per-site element check is Planner work)",
|
|
118
|
+
provenance={"source": "crosslink reachability_tier (tier-1 reprogrammable)"}))
|
|
119
|
+
# outcome -used_writer-> writer; outcome -observed_at-> locus (when the panel name maps to a GSH locus)
|
|
120
|
+
gsh_names = {loc["name"] for loc in gsh}
|
|
121
|
+
for _, r in panel.iterrows():
|
|
122
|
+
oid = f"outcome:{r['name']}"
|
|
123
|
+
g.add_node(Node(id=oid, type="outcome", props={"writer_family": str(r["family"]),
|
|
124
|
+
"cargo_bp": int(r["cargo_bp"]), "doi": str(r["doi"]), "note": str(r.get("note", ""))}))
|
|
125
|
+
wid = f"writer:{r['family']}"
|
|
126
|
+
if wid in g.nodes:
|
|
127
|
+
g.add_edge(Edge(oid, wid, "used_writer", "measured", confidence=1.0,
|
|
128
|
+
scope="documented experimental write", provenance={"doi": str(r["doi"])}))
|
|
129
|
+
for ln in gsh_names:
|
|
130
|
+
if ln.lower() in str(r["name"]).lower():
|
|
131
|
+
g.add_edge(Edge(oid, f"locus:{ln}", "observed_at", "measured",
|
|
132
|
+
scope="documented locus of the write", provenance={"doi": str(r["doi"])}))
|
|
133
|
+
return g
|