pen-stack 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. pen_stack/__init__.py +2 -0
  2. pen_stack/_resources.py +34 -0
  3. pen_stack/active/__init__.py +20 -0
  4. pen_stack/active/acquire.py +165 -0
  5. pen_stack/active/brains.py +74 -0
  6. pen_stack/active/campaign.py +109 -0
  7. pen_stack/active/design.py +66 -0
  8. pen_stack/active/validate.py +104 -0
  9. pen_stack/adapt/__init__.py +14 -0
  10. pen_stack/adapt/finetune.py +33 -0
  11. pen_stack/adapt/ingest.py +86 -0
  12. pen_stack/adapt/pipeline.py +101 -0
  13. pen_stack/adapt/recalibrate.py +58 -0
  14. pen_stack/adapt/report.py +130 -0
  15. pen_stack/agent/__init__.py +1 -0
  16. pen_stack/agent/cite.py +175 -0
  17. pen_stack/agent/co_scientist.py +262 -0
  18. pen_stack/agent/epistemic.py +102 -0
  19. pen_stack/agent/guardrails.py +67 -0
  20. pen_stack/agent/mcp_server.py +215 -0
  21. pen_stack/agent/orchestrator.py +112 -0
  22. pen_stack/agent/orchestrator_live.py +56 -0
  23. pen_stack/agent/pen_agent.py +242 -0
  24. pen_stack/agent/scope.py +60 -0
  25. pen_stack/agent/tools.py +130 -0
  26. pen_stack/api/__init__.py +12 -0
  27. pen_stack/api/manifest.py +160 -0
  28. pen_stack/atlas/__init__.py +1 -0
  29. pen_stack/atlas/atlas.parquet +0 -0
  30. pen_stack/atlas/build_wtkb.py +80 -0
  31. pen_stack/atlas/crosslink.py +179 -0
  32. pen_stack/atlas/expand.py +190 -0
  33. pen_stack/atlas/guide_design.py +178 -0
  34. pen_stack/atlas/schema.py +59 -0
  35. pen_stack/atlas/scorecard.py +134 -0
  36. pen_stack/atlas/scorecard_v3.parquet +0 -0
  37. pen_stack/atlas/universe.py +75 -0
  38. pen_stack/atlas/universe_v3.parquet +0 -0
  39. pen_stack/atlas/variant_propose.py +155 -0
  40. pen_stack/atlas/writer_efficiency.py +184 -0
  41. pen_stack/atlas/writer_predict.py +229 -0
  42. pen_stack/atlas/writer_recommend.py +170 -0
  43. pen_stack/atlas/writer_verify.py +167 -0
  44. pen_stack/atlas/wtkb.parquet +0 -0
  45. pen_stack/bridge/__init__.py +1 -0
  46. pen_stack/bridge/activity.py +52 -0
  47. pen_stack/bridge/cli.py +65 -0
  48. pen_stack/bridge/fold_qc.py +53 -0
  49. pen_stack/bridge/guide_qc.py +87 -0
  50. pen_stack/bridge/ingest.py +139 -0
  51. pen_stack/bridge/offtarget.py +191 -0
  52. pen_stack/bridge/offtarget_energetics.py +105 -0
  53. pen_stack/bridge/ortholog_screen.py +73 -0
  54. pen_stack/bridge/pipeline.py +83 -0
  55. pen_stack/build/__init__.py +16 -0
  56. pen_stack/build/cloudlab.py +74 -0
  57. pen_stack/build/ingest.py +47 -0
  58. pen_stack/build/protocol.py +82 -0
  59. pen_stack/build/simlab.py +30 -0
  60. pen_stack/cli.py +126 -0
  61. pen_stack/data/__init__.py +1 -0
  62. pen_stack/data/encode.py +84 -0
  63. pen_stack/data/genome.py +71 -0
  64. pen_stack/data/ingest_chromatin.py +119 -0
  65. pen_stack/data/ingest_integration.py +112 -0
  66. pen_stack/data/ingest_safety_annot.py +201 -0
  67. pen_stack/data/ingest_trip.py +76 -0
  68. pen_stack/design/__init__.py +14 -0
  69. pen_stack/design/capsid_generate.py +62 -0
  70. pen_stack/design/generate.py +70 -0
  71. pen_stack/design/pareto.py +70 -0
  72. pen_stack/design/space.py +137 -0
  73. pen_stack/design/writer_variants.py +121 -0
  74. pen_stack/env/__init__.py +1 -0
  75. pen_stack/env/genome_writing_env.py +248 -0
  76. pen_stack/env/policies.py +94 -0
  77. pen_stack/graph/__init__.py +21 -0
  78. pen_stack/graph/build.py +133 -0
  79. pen_stack/graph/cell_types.py +58 -0
  80. pen_stack/graph/ingest.py +132 -0
  81. pen_stack/graph/query.py +148 -0
  82. pen_stack/graph/schema.py +100 -0
  83. pen_stack/loop/__init__.py +15 -0
  84. pen_stack/loop/continual.py +61 -0
  85. pen_stack/loop/cycle.py +84 -0
  86. pen_stack/loop/drift.py +41 -0
  87. pen_stack/mech/__init__.py +1 -0
  88. pen_stack/mech/classify_atlas.py +71 -0
  89. pen_stack/mech/pfam_whitelist.yaml +247 -0
  90. pen_stack/mech/whitelist.py +66 -0
  91. pen_stack/monitor/__init__.py +1 -0
  92. pen_stack/monitor/europepmc.py +32 -0
  93. pen_stack/monitor/run.py +57 -0
  94. pen_stack/monitor/triage.py +63 -0
  95. pen_stack/oracles/__init__.py +65 -0
  96. pen_stack/oracles/affinity.py +116 -0
  97. pen_stack/oracles/cache.py +53 -0
  98. pen_stack/oracles/energetics.py +33 -0
  99. pen_stack/oracles/genome.py +167 -0
  100. pen_stack/oracles/protein_design.py +136 -0
  101. pen_stack/oracles/reliability.py +64 -0
  102. pen_stack/oracles/rna.py +28 -0
  103. pen_stack/oracles/schema.py +77 -0
  104. pen_stack/oracles/status.py +123 -0
  105. pen_stack/oracles/structure.py +42 -0
  106. pen_stack/oracles/structure_run.py +76 -0
  107. pen_stack/oracles/vcell.py +74 -0
  108. pen_stack/planner/__init__.py +1 -0
  109. pen_stack/planner/ada_risk.py +64 -0
  110. pen_stack/planner/antipeg_oracle.py +75 -0
  111. pen_stack/planner/capsid_epitope_oracle.py +135 -0
  112. pen_stack/planner/cargo.py +56 -0
  113. pen_stack/planner/cargo_polish.py +146 -0
  114. pen_stack/planner/chromosome.py +106 -0
  115. pen_stack/planner/delivery.py +55 -0
  116. pen_stack/planner/delivery_constraints.py +110 -0
  117. pen_stack/planner/delivery_immune.py +61 -0
  118. pen_stack/planner/delivery_immunology.py +222 -0
  119. pen_stack/planner/delivery_predict.py +196 -0
  120. pen_stack/planner/delivery_vehicles.py +37 -0
  121. pen_stack/planner/genotoxicity_oracle.py +112 -0
  122. pen_stack/planner/immune_mhc2.py +154 -0
  123. pen_stack/planner/immune_profile.py +292 -0
  124. pen_stack/planner/innate_sensing.py +135 -0
  125. pen_stack/planner/multiplex.py +110 -0
  126. pen_stack/planner/optimize.py +278 -0
  127. pen_stack/planner/pipeline.py +87 -0
  128. pen_stack/planner/report.py +26 -0
  129. pen_stack/planner/router.py +57 -0
  130. pen_stack/planner/seroprevalence_oracle.py +92 -0
  131. pen_stack/planner/target_site.py +118 -0
  132. pen_stack/rag/__init__.py +1 -0
  133. pen_stack/rag/corpus.py +133 -0
  134. pen_stack/rag/embed.py +98 -0
  135. pen_stack/rag/ground.py +131 -0
  136. pen_stack/rag/index.py +53 -0
  137. pen_stack/rag/llm.py +215 -0
  138. pen_stack/rag/qa.py +105 -0
  139. pen_stack/rag/retrieve.py +48 -0
  140. pen_stack/rules/__init__.py +9 -0
  141. pen_stack/rules/evaluators.py +318 -0
  142. pen_stack/rules/loader.py +31 -0
  143. pen_stack/rules/schema.py +99 -0
  144. pen_stack/rules/solver.py +43 -0
  145. pen_stack/rules/spec.py +78 -0
  146. pen_stack/safety/__init__.py +21 -0
  147. pen_stack/safety/audit.py +90 -0
  148. pen_stack/safety/gate.py +58 -0
  149. pen_stack/safety/pfam_scan.py +157 -0
  150. pen_stack/safety/policy.py +69 -0
  151. pen_stack/safety/redteam.py +71 -0
  152. pen_stack/safety/registry.py +255 -0
  153. pen_stack/safety/screen.py +59 -0
  154. pen_stack/safety/standards.py +141 -0
  155. pen_stack/score/__init__.py +1 -0
  156. pen_stack/score/recalibrate.py +77 -0
  157. pen_stack/score/therapeutic.py +85 -0
  158. pen_stack/server/__init__.py +1 -0
  159. pen_stack/server/api.py +647 -0
  160. pen_stack/spec/__init__.py +18 -0
  161. pen_stack/spec/clarify.py +42 -0
  162. pen_stack/spec/extract.py +406 -0
  163. pen_stack/spec/resolvers/__init__.py +17 -0
  164. pen_stack/spec/resolvers/cell.py +51 -0
  165. pen_stack/spec/resolvers/chem.py +32 -0
  166. pen_stack/spec/resolvers/feature.py +36 -0
  167. pen_stack/spec/resolvers/gene.py +43 -0
  168. pen_stack/spec/resolvers/locus.py +29 -0
  169. pen_stack/spec/resolvers/phenotype.py +37 -0
  170. pen_stack/spec/satisfy.py +114 -0
  171. pen_stack/spec/service.py +33 -0
  172. pen_stack/spec/writespec.py +252 -0
  173. pen_stack/twin/__init__.py +14 -0
  174. pen_stack/twin/calibrate.py +61 -0
  175. pen_stack/twin/data/__init__.py +12 -0
  176. pen_stack/twin/data/position_effect.py +245 -0
  177. pen_stack/twin/mechanistic.py +147 -0
  178. pen_stack/twin/outcome.py +132 -0
  179. pen_stack/twin/position_effect.py +454 -0
  180. pen_stack/ui/__init__.py +1 -0
  181. pen_stack/ui/app.py +713 -0
  182. pen_stack/validate/__init__.py +1 -0
  183. pen_stack/validate/adapt_demo.py +69 -0
  184. pen_stack/validate/agent_eval.py +117 -0
  185. pen_stack/validate/bench_adversarial_tasks.py +118 -0
  186. pen_stack/validate/bench_coscientist_tasks.py +60 -0
  187. pen_stack/validate/bench_graph_tasks.py +64 -0
  188. pen_stack/validate/bench_rule_tasks.py +84 -0
  189. pen_stack/validate/bench_trust_tasks.py +92 -0
  190. pen_stack/validate/bench_writetype_tasks.py +101 -0
  191. pen_stack/validate/blind_gsh_discovery.py +261 -0
  192. pen_stack/validate/cargo_directionality.py +57 -0
  193. pen_stack/validate/closed_loop.py +63 -0
  194. pen_stack/validate/durability_baselines.py +185 -0
  195. pen_stack/validate/experiment_design.py +65 -0
  196. pen_stack/validate/expr_controls.py +39 -0
  197. pen_stack/validate/forward_hypotheses.py +104 -0
  198. pen_stack/validate/generative_design.py +62 -0
  199. pen_stack/validate/guide_qc_demo.py +69 -0
  200. pen_stack/validate/heldout_celltype_expr.py +32 -0
  201. pen_stack/validate/immune_calibration.py +133 -0
  202. pen_stack/validate/intent_specification.py +82 -0
  203. pen_stack/validate/known_biology_expr.py +38 -0
  204. pen_stack/validate/offtarget_energetics_eval.py +144 -0
  205. pen_stack/validate/out_of_scope_refusal.py +82 -0
  206. pen_stack/validate/outcome_calibration.py +194 -0
  207. pen_stack/validate/outcome_prediction.py +76 -0
  208. pen_stack/validate/paper3_benchmark.py +165 -0
  209. pen_stack/validate/paper4_real_validation.py +144 -0
  210. pen_stack/validate/paper4_validation.py +82 -0
  211. pen_stack/validate/protocol_safety.py +62 -0
  212. pen_stack/validate/safety_screening.py +72 -0
  213. pen_stack/validate/selective_prediction.py +104 -0
  214. pen_stack/validate/seq_vs_measured.py +134 -0
  215. pen_stack/validate/target_site_controls.py +65 -0
  216. pen_stack/validate/uncertainty_eval.py +244 -0
  217. pen_stack/validate/ungrounded_baseline.py +234 -0
  218. pen_stack/validate/within_locus_ranking.py +84 -0
  219. pen_stack/validate/writer_recovery.py +91 -0
  220. pen_stack/verify/__init__.py +5 -0
  221. pen_stack/verify/proof.py +206 -0
  222. pen_stack/verify/schema.py +53 -0
  223. pen_stack/verify/service.py +191 -0
  224. pen_stack/web/__init__.py +18 -0
  225. pen_stack/web/guide.py +110 -0
  226. pen_stack/web/llm.py +393 -0
  227. pen_stack/web/llm_provider.py +119 -0
  228. pen_stack/web/router.py +119 -0
  229. pen_stack/web/server.py +96 -0
  230. pen_stack/web/tools.py +197 -0
  231. pen_stack/wgenome/__init__.py +1 -0
  232. pen_stack/wgenome/chromatin_seq.py +83 -0
  233. pen_stack/wgenome/durability.py +108 -0
  234. pen_stack/wgenome/export_tracks.py +52 -0
  235. pen_stack/wgenome/features.py +82 -0
  236. pen_stack/wgenome/genotoxic_blocklist.py +88 -0
  237. pen_stack/wgenome/gsh_baseline.py +154 -0
  238. pen_stack/wgenome/mesh_features.py +61 -0
  239. pen_stack/wgenome/offtarget_assay.py +80 -0
  240. pen_stack/wgenome/offtarget_bridge.py +47 -0
  241. pen_stack/wgenome/offtarget_cast.py +97 -0
  242. pen_stack/wgenome/offtarget_data.py +148 -0
  243. pen_stack/wgenome/offtarget_enumerate.py +274 -0
  244. pen_stack/wgenome/offtarget_integrase.py +155 -0
  245. pen_stack/wgenome/offtarget_nuclease.py +123 -0
  246. pen_stack/wgenome/offtarget_paste.py +41 -0
  247. pen_stack/wgenome/offtarget_predict.py +282 -0
  248. pen_stack/wgenome/ood.py +135 -0
  249. pen_stack/wgenome/providers.py +278 -0
  250. pen_stack/wgenome/safety.py +69 -0
  251. pen_stack/wgenome/structure3d.py +212 -0
  252. pen_stack/wgenome/uncertainty.py +250 -0
  253. pen_stack/wgenome/writability.py +72 -0
  254. pen_stack-0.1.0.dist-info/METADATA +401 -0
  255. pen_stack-0.1.0.dist-info/RECORD +259 -0
  256. pen_stack-0.1.0.dist-info/WHEEL +5 -0
  257. pen_stack-0.1.0.dist-info/entry_points.txt +3 -0
  258. pen_stack-0.1.0.dist-info/licenses/LICENSE +21 -0
  259. pen_stack-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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"]
@@ -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