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,29 @@
1
+ """Locus resolver: free text (gene / safe-harbour) -> GRCh38 chromosome, reusing the atlas resolver.
2
+
3
+ Wraps :func:`pen_stack.planner.optimize.gene_region` (with the safe-harbour-aware ``resolve_gene``). An
4
+ unresolvable term stays null. The coordinate granularity is the chromosome the gene body lies on (the full
5
+ base-range comes from the atlas when a downstream reachability query is run).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from pen_stack.spec.writespec import Resolved
10
+
11
+
12
+ def resolve_locus(text: str) -> Resolved:
13
+ """Resolve a gene / safe-harbour locus to its GRCh38 chromosome; unrecognized stays null."""
14
+ if not text:
15
+ return Resolved(text=text, ontology="GRCh38", note="empty")
16
+ sym = text.strip().upper()
17
+ reg = None
18
+ try:
19
+ from pen_stack.planner.optimize import gene_region, resolve_gene
20
+ reg = gene_region(resolve_gene(sym)) # -> (chrom, start, end) | None
21
+ except Exception: # noqa: BLE001
22
+ reg = None
23
+ if not reg:
24
+ return Resolved(text=text, ontology="GRCh38", confidence=None, note="unresolved locus")
25
+ chrom, start, end = reg
26
+ chrom = chrom if str(chrom).startswith("chr") else f"chr{chrom}"
27
+ cid = f"{chrom}:{int(start)}-{int(end)}"
28
+ return Resolved(text=text, id=cid, label=cid, ontology="GRCh38", confidence=0.85,
29
+ note="GRCh38 gene-body region (+/- flank) from the atlas")
@@ -0,0 +1,37 @@
1
+ """Phenotype / disease-goal resolver: free text -> MONDO. Ids verified against EBI OLS."""
2
+ from __future__ import annotations
3
+
4
+ from pen_stack.spec.writespec import Resolved
5
+
6
+ # (MONDO id, canonical label) - verified live before commit
7
+ _PHENO: dict[str, tuple[str, str]] = {
8
+ "sickle cell disease": ("MONDO:0011382", "sickle cell disease"),
9
+ "sickle cell": ("MONDO:0011382", "sickle cell disease"), "scd": ("MONDO:0011382", "sickle cell disease"),
10
+ "beta thalassemia": ("MONDO:0019402", "beta thalassemia"),
11
+ "beta-thalassemia": ("MONDO:0019402", "beta thalassemia"),
12
+ "duchenne muscular dystrophy": ("MONDO:0010679", "Duchenne muscular dystrophy"),
13
+ "duchenne": ("MONDO:0010679", "Duchenne muscular dystrophy"), "dmd": ("MONDO:0010679", "Duchenne muscular dystrophy"),
14
+ "cystic fibrosis": ("MONDO:0009061", "cystic fibrosis"), "cf": ("MONDO:0009061", "cystic fibrosis"),
15
+ "rett syndrome": ("MONDO:0010726", "Rett syndrome"), "rett": ("MONDO:0010726", "Rett syndrome"),
16
+ "hemophilia b": ("MONDO:0010604", "hemophilia B"),
17
+ "hemophilia a": ("MONDO:0010602", "hemophilia A"),
18
+ "leber congenital amaurosis 10": ("MONDO:0012723", "Leber congenital amaurosis 10"),
19
+ "lca10": ("MONDO:0012723", "Leber congenital amaurosis 10"),
20
+ }
21
+
22
+
23
+ def resolve_phenotype(text: str) -> Resolved:
24
+ """Resolve a disease / phenotype goal to MONDO; unrecognized stays null (never invented)."""
25
+ if not text:
26
+ return Resolved(text=text, ontology="MONDO", note="empty")
27
+ key = text.strip().lower()
28
+ hit = _PHENO.get(key)
29
+ if hit is None:
30
+ for k, v in _PHENO.items():
31
+ if k in key or key in k:
32
+ hit = v
33
+ break
34
+ if hit is None:
35
+ return Resolved(text=text, ontology="MONDO", confidence=None, note="unresolved phenotype term")
36
+ mid, label = hit
37
+ return Resolved(text=text, id=mid, label=label, ontology="MONDO", confidence=0.9)
@@ -0,0 +1,114 @@
1
+ """WriteSpec feasibility / satisfiability check.
2
+
3
+ Given a :class:`WriteRequest`, test the NECESSARY conditions a write must meet, by wrapping the existing stages:
4
+
5
+ * reachability - is the target locus / gene reachable by a writer in the writable-genome atlas?
6
+ * deliverability - does any vehicle fit the cargo size (the delivery recommender)?
7
+ * legality - does the design pass the rule set (the repair-oriented proof object)?
8
+
9
+ Returns ``feasible`` or ``infeasible`` plus the NAMED blocking constraint(s) and repair hints (from the rule-set
10
+ proof and a delivery suggestion), so an agent can repair the spec and re-check. A check whose backend is absent is
11
+ reported ``indeterminate`` and does not, by itself, block (it is surfaced, never silently passed). Feasibility is
12
+ necessary-conditions only: it rules out unreachable / undeliverable / illegal, NOT whether the write will WORK
13
+ (that is the downstream stages' calibrated, still-uncertain prediction).
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from typing import Any
18
+
19
+ from pydantic import BaseModel, Field
20
+
21
+ from pen_stack.spec.writespec import WriteRequest
22
+
23
+
24
+ class SatisfyResult(BaseModel):
25
+ feasible: bool
26
+ checks: dict[str, Any] = Field(default_factory=dict) # reachability / deliverability / legality verdicts
27
+ blocking: list[dict] = Field(default_factory=list) # named blocking constraints
28
+ repairs: list[dict] = Field(default_factory=list) # repair hints
29
+ note: str | None = None
30
+
31
+
32
+ def _reachability(spec: WriteRequest) -> dict:
33
+ gene = spec.target.gene.id if (spec.target.gene and spec.target.gene.id) else None
34
+ if spec.target.kind == "att_site":
35
+ return {"ok": True, "status": "att/landing site supplied; reachability is by the installed att",
36
+ "determinable": True}
37
+ if gene is None:
38
+ return {"ok": None, "status": "no resolved target gene/locus; supply one for a reachability check",
39
+ "determinable": False}
40
+ try:
41
+ from pen_stack.atlas.crosslink import loci_for_gene
42
+ df = loci_for_gene(gene, ct="k562")
43
+ n = 0 if df is None else len(df)
44
+ if n > 0:
45
+ return {"ok": True, "status": f"{n} writable bins overlap {gene} in the atlas", "determinable": True}
46
+ return {"ok": False, "status": f"no writable bin overlaps {gene} in the atlas (cross-check the locus)",
47
+ "determinable": True}
48
+ except Exception as e: # noqa: BLE001
49
+ return {"ok": None, "status": f"atlas reachability unavailable ({type(e).__name__})", "determinable": False}
50
+
51
+
52
+ def _deliverability(spec: WriteRequest) -> dict:
53
+ bp = spec.constraints.max_cargo_bp or sum((c.length_bp or 0) for c in spec.cargo) or None
54
+ if bp is None:
55
+ return {"ok": None, "status": "no cargo size given; supply bp/kb for a deliverability check",
56
+ "determinable": False}
57
+ try:
58
+ from pen_stack.planner.delivery_predict import recommend_delivery_plus
59
+ rec = recommend_delivery_plus("DNA", bp, None)
60
+ ranked = rec.get("ranked") or [] # the recommender already filters to vehicles that fit the cargo
61
+ excluded = rec.get("excluded") or []
62
+ if ranked:
63
+ top = ranked[0].get("vehicle") or ranked[0].get("name")
64
+ return {"ok": True, "status": f"{len(ranked)} vehicle(s) fit {bp} bp (top: {top}; "
65
+ f"{len(excluded)} excluded by capacity)", "determinable": True, "top": top}
66
+ return {"ok": False, "status": f"no vehicle in the palette fits {bp} bp", "determinable": True}
67
+ except Exception: # noqa: BLE001
68
+ if bp <= 8000:
69
+ return {"ok": True, "status": f"{bp} bp is within typical single-vector capacity (<= ~8 kb)",
70
+ "determinable": True}
71
+ return {"ok": False, "status": f"{bp} bp exceeds typical single-vector capacity (~8 kb); needs a "
72
+ "dual/high-capacity strategy", "determinable": True}
73
+
74
+
75
+ def _legality(spec: WriteRequest) -> dict:
76
+ try:
77
+ from pen_stack.verify.proof import verify_proof
78
+ proof = verify_proof(spec.to_legacy_design())
79
+ ax = next((a for a in proof.axes if a.axis == "legality"), None)
80
+ if ax is None:
81
+ return {"ok": None, "status": "no legality axis returned", "determinable": False}
82
+ ok = ax.status in ("pass", "clear")
83
+ out = {"ok": ok, "status": ax.status, "determinable": True}
84
+ if not ok:
85
+ out["violated"] = [v for v in (ax.violated or [])]
86
+ if ax.repair_hint:
87
+ out["repair_hint"] = ax.repair_hint
88
+ return out
89
+ except Exception as e: # noqa: BLE001
90
+ return {"ok": None, "status": f"legality check unavailable ({type(e).__name__})", "determinable": False}
91
+
92
+
93
+ def check_satisfiable(spec: WriteRequest) -> SatisfyResult:
94
+ """Run the reachability + deliverability + legality necessary-condition checks; name blocking constraints."""
95
+ checks = {"reachability": _reachability(spec), "deliverability": _deliverability(spec),
96
+ "legality": _legality(spec)}
97
+ blocking: list[dict] = []
98
+ repairs: list[dict] = []
99
+ for name, c in checks.items():
100
+ if c.get("ok") is False:
101
+ blocking.append({"constraint": name, "reason": c.get("status")})
102
+ if name == "deliverability":
103
+ repairs.append({"constraint": "deliverability",
104
+ "hint": "reduce the cargo, split across a dual-vector strategy, or use an "
105
+ "integrating vehicle"})
106
+ if name == "legality" and c.get("repair_hint"):
107
+ repairs.append({"constraint": "legality", "hint": c["repair_hint"]})
108
+ if name == "reachability":
109
+ repairs.append({"constraint": "reachability",
110
+ "hint": "choose a writable locus (the atlas Site Finder ranks reachable, safe loci)"})
111
+ feasible = not blocking
112
+ note = ("all determinable necessary conditions met (feasibility is necessary, not sufficient: efficacy is the "
113
+ "downstream prediction)" if feasible else "blocked by the named constraint(s); repairs suggested")
114
+ return SatisfyResult(feasible=feasible, checks=checks, blocking=blocking, repairs=repairs, note=note)
@@ -0,0 +1,33 @@
1
+ """WriteSpec service: the one call the surfaces (REST / MCP / web builder) use.
2
+
3
+ ``parse_request`` runs the grounded extractor, plans clarifying questions, and (optionally) the feasibility
4
+ check, returning a single JSON-able payload: the typed WriteSpec, the assumptions behind every inferred field,
5
+ the clarifying questions for anything underspecified, the unresolved terms, the downstream design adapter, and
6
+ the feasibility verdict. Nothing is fabricated: unresolved stays null, inferred is labelled, ambiguous asks.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from pen_stack.spec.clarify import clarifying_questions
13
+ from pen_stack.spec.extract import extract_writespec
14
+
15
+
16
+ def parse_request(prose: str, *, overrides: dict | None = None, check_feasibility: bool = True) -> dict[str, Any]:
17
+ """Parse prose into a typed WriteSpec + clarifications + (optionally) a feasibility verdict."""
18
+ spec = extract_writespec(prose, overrides=overrides)
19
+ qs = clarifying_questions(spec)
20
+ out: dict[str, Any] = {
21
+ "writespec": spec.model_dump(),
22
+ "assumptions": spec.assumptions,
23
+ "clarifications": qs,
24
+ "actionable": not qs,
25
+ "unresolved": spec.unresolved,
26
+ "ontology_validation": spec.ontology_validation(),
27
+ "legacy_design": spec.to_legacy_design(),
28
+ "no_fabrication": True,
29
+ }
30
+ if check_feasibility:
31
+ from pen_stack.spec.satisfy import check_satisfiable
32
+ out["feasibility"] = check_satisfiable(spec).model_dump()
33
+ return out
@@ -0,0 +1,252 @@
1
+ """The WriteRequest schema: a typed, ontology-backed genome-writing request.
2
+
3
+ ``WriteRequest`` is an SBOL3 profile expressed as a pydantic model. It carries the write semantics (write-type,
4
+ cargo with Sequence-Ontology roles, target locus/gene/att-site/phenotype, cell type, constraints) PLUS a
5
+ grounding discipline: a per-field provenance map (explicit | inferred | user | unresolved), the assumptions behind
6
+ every inferred field, clarifying questions for underspecified required fields, and the list of terms that could
7
+ not be resolved (kept null, never invented). A WriteRequest is a REQUEST, not a claim.
8
+
9
+ Round-trips: JSON always; SBOL3 via the real ``sbol3`` library when the ``[spec]`` extra is installed (native
10
+ Components + SO roles for interoperability, with the full typed spec carried losslessly in a PROV-O-namespaced
11
+ annotation); GenBank for a cargo component that carries a DNA sequence. ``to_legacy_design`` is the adapter that
12
+ emits the dict the existing downstream stages (verify / plan / safety) already consume, so the whole stack reads
13
+ one contract without a rewrite.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ from typing import Any, Literal
19
+
20
+ from pydantic import BaseModel, Field
21
+
22
+ WRITE_TYPES = ["insertion", "excision", "inversion", "replacement", "regulatory_rewrite",
23
+ "landing_pad_install", "multiplex"]
24
+
25
+ ProvenanceKind = Literal["explicit", "inferred", "user", "unresolved"]
26
+
27
+ # id-format validators per ontology (a resolved id must match, else it is flagged not-valid).
28
+ _ID_FORMATS = {
29
+ "SO": re.compile(r"^SO:\d{7}$"),
30
+ "Cellosaurus": re.compile(r"^CVCL_[A-Z0-9]{4}$"),
31
+ "CL": re.compile(r"^CL:\d{7}$"),
32
+ "MONDO": re.compile(r"^MONDO:\d{7}$"),
33
+ "HPO": re.compile(r"^HP:\d{7}$"),
34
+ "ChEBI": re.compile(r"^CHEBI:\d+$"),
35
+ "HGNC": re.compile(r"^[A-Z0-9-]+$"),
36
+ "GRCh38": re.compile(r"^chr[0-9XYM]+(:\d+-\d+)?$"),
37
+ }
38
+
39
+
40
+ class Resolved(BaseModel):
41
+ """A free-text term resolved to a canonical ontology id, with confidence and the candidate set when ambiguous."""
42
+ text: str | None = None # the surface term from the prose
43
+ id: str | None = None # canonical id (CVCL_0063, SO:0000167, MONDO:..., a gene symbol, ...)
44
+ label: str | None = None # canonical label
45
+ ontology: str | None = None # Cellosaurus | CL | SO | HGNC | MONDO | HPO | ChEBI | GRCh38
46
+ confidence: float | None = None
47
+ candidates: list[dict] = Field(default_factory=list) # ranked alternatives when ambiguous
48
+ note: str | None = None
49
+
50
+ @property
51
+ def resolved(self) -> bool:
52
+ return self.id is not None
53
+
54
+
55
+ class CargoComponent(BaseModel):
56
+ """A component to be written (CDS, promoter, insulator, attB, polyA, ...), with its SO/SBO role."""
57
+ name: str
58
+ role: Resolved | None = None # Sequence-Ontology role
59
+ sequence: str | None = None # DNA sequence if given (else intent-only)
60
+ length_bp: int | None = None
61
+
62
+
63
+ class Target(BaseModel):
64
+ """Where the write goes: a locus, a gene, an att/landing site, or a phenotype goal."""
65
+ kind: Literal["locus", "gene", "att_site", "phenotype", "unspecified"] = "unspecified"
66
+ gene: Resolved | None = None
67
+ locus: Resolved | None = None # GRCh38 coordinate / safe-harbour
68
+ att_site: str | None = None # attB/attP/landing-pad id
69
+ phenotype: Resolved | None = None # MONDO/HPO disease goal
70
+ # Further genes named alongside ``gene`` in one request ("knock out TRAC and B2M"). ``gene`` stays the primary
71
+ # target so every existing consumer reads the same field; the rest are carried here rather than dropped, since
72
+ # a multiplex request that silently keeps only its first target would be designed against an incomplete spec.
73
+ additional_genes: list[Resolved] = Field(default_factory=list)
74
+
75
+
76
+ class Constraints(BaseModel):
77
+ """The request's constraints (efficiency floor, scarless, safety switch, copy number, guardrails, delivery)."""
78
+ efficiency_floor: float | None = None
79
+ scarless: bool | None = None
80
+ safety_switch_required: bool | None = None
81
+ copy_number: int | None = None
82
+ germline: bool | None = None # germline guardrail (True = a germline edit was requested -> flagged)
83
+ max_cargo_bp: int | None = None
84
+ delivery_limit: str | None = None # e.g. "non_integrating", "AAV_only"
85
+ inducer: Resolved | None = None # ChEBI small-molecule inducer / selection agent
86
+
87
+
88
+ class WriteRequest(BaseModel):
89
+ """A typed, ontology-backed, machine-checkable genome-writing request (an SBOL3 profile). A REQUEST, not a claim.
90
+
91
+ Every field that was not explicit in the source prose is recorded in ``provenance`` as ``inferred`` (with the
92
+ rationale in ``assumptions``); a required field that is unspecified or ambiguous yields a ``clarifications``
93
+ question rather than a guess; a term that could not be resolved is listed in ``unresolved`` and kept null.
94
+ """
95
+ write_type: str # one of WRITE_TYPES
96
+ cargo: list[CargoComponent] = Field(default_factory=list)
97
+ target: Target = Field(default_factory=Target)
98
+ cell_type: Resolved | None = None
99
+ constraints: Constraints = Field(default_factory=Constraints)
100
+ # --- grounding discipline ---
101
+ provenance: dict[str, ProvenanceKind] = Field(default_factory=dict) # field -> origin
102
+ assumptions: list[str] = Field(default_factory=list) # inferred fields + rationale
103
+ clarifications: list[str] = Field(default_factory=list) # questions for underspecified fields
104
+ unresolved: list[str] = Field(default_factory=list) # terms that could not be resolved
105
+ free_text_note: str | None = None # the original prose (the cargo-function the Guardian must screen)
106
+ extensions: dict[str, Any] = Field(default_factory=dict) # edge intents the schema does not cover
107
+
108
+ # ---- validation -------------------------------------------------------------------------------------------
109
+ def ontology_validation(self) -> dict[str, Any]:
110
+ """Check every resolved field's id against its ontology id-format. Returns the per-field verdict."""
111
+ bad: list[dict] = []
112
+ checked = 0
113
+ for field, r in self._resolved_fields().items():
114
+ if r.id is None or r.ontology is None:
115
+ continue
116
+ checked += 1
117
+ fmt = _ID_FORMATS.get(r.ontology)
118
+ if fmt is not None and not fmt.match(r.id):
119
+ bad.append({"field": field, "id": r.id, "ontology": r.ontology})
120
+ return {"checked": checked, "invalid": bad, "all_valid": not bad}
121
+
122
+ def _resolved_fields(self) -> dict[str, Resolved]:
123
+ out: dict[str, Resolved] = {}
124
+ if self.cell_type:
125
+ out["cell_type"] = self.cell_type
126
+ if self.target.gene:
127
+ out["target.gene"] = self.target.gene
128
+ if self.target.locus:
129
+ out["target.locus"] = self.target.locus
130
+ if self.target.phenotype:
131
+ out["target.phenotype"] = self.target.phenotype
132
+ if self.constraints.inducer:
133
+ out["constraints.inducer"] = self.constraints.inducer
134
+ for i, c in enumerate(self.cargo):
135
+ if c.role:
136
+ out[f"cargo[{i}].role"] = c.role
137
+ return out
138
+
139
+ @property
140
+ def is_grounded(self) -> bool:
141
+ """No fabrication: every field is either explicit, user, inferred-and-labelled, or unresolved-and-null."""
142
+ return self.ontology_validation()["all_valid"]
143
+
144
+ # ---- the downstream adapter -------------------------------------------------------------------------------
145
+ def to_legacy_design(self) -> dict:
146
+ """Emit the dict the existing downstream stages (verify / plan / safety / delivery) already consume.
147
+
148
+ This is the wrap-not-rewrite adapter: the whole stack reads ``WriteRequest`` through this one mapping
149
+ instead of a per-stage rewrite. Unspecified fields fall back to the stack's documented defaults.
150
+
151
+ The design it emits carries ONE target, ``target.gene``. A request naming several
152
+ (``target.additional_genes``) describes several writes, so it needs one design per target: this adapter
153
+ does not fold them together, and a caller iterating a multiplex request must build a design for each.
154
+ """
155
+ cargo_bp = self.constraints.max_cargo_bp
156
+ if cargo_bp is None:
157
+ cargo_bp = sum((c.length_bp or 0) for c in self.cargo) or None
158
+ gene = self.target.gene.id if (self.target.gene and self.target.gene.id) else None
159
+ locus = self.target.locus.id if (self.target.locus and self.target.locus.id) else None
160
+ chrom = None
161
+ if locus and locus.startswith("chr"):
162
+ chrom = locus.split(":")[0]
163
+ design = {
164
+ "write_type": self.write_type,
165
+ "gene": gene,
166
+ "chrom": chrom,
167
+ "cargo_bp": cargo_bp,
168
+ "cell_type": self.cell_type.id if (self.cell_type and self.cell_type.id) else None,
169
+ # delivery_limit holds either a named vehicle or the "non_integrating" flag
170
+ "delivery_vehicle": (self.constraints.delivery_limit
171
+ if self.constraints.delivery_limit not in (None, "non_integrating") else None),
172
+ "non_integrating": (self.constraints.delivery_limit == "non_integrating") or None,
173
+ "installed_att": bool(self.target.att_site),
174
+ "cargo_function": self.free_text_note,
175
+ }
176
+ return {k: v for k, v in design.items() if v is not None}
177
+
178
+ # ---- serialization ----------------------------------------------------------------------------------------
179
+ def to_json(self) -> str:
180
+ return self.model_dump_json(indent=2)
181
+
182
+ @classmethod
183
+ def from_json(cls, s: str) -> "WriteRequest":
184
+ return cls.model_validate_json(s)
185
+
186
+ def to_sbol3(self, namespace: str = "https://penstack.org/writespec") -> str:
187
+ """Serialize to SBOL3 (sorted N-triples) via the ``sbol3`` library: native Components + Sequence-Ontology
188
+ roles for interoperability, with the full typed spec carried losslessly in a PROV-O-namespaced annotation.
189
+
190
+ Requires the ``[spec]`` extra (``pip install sbol3``); raises ImportError otherwise (never fabricates).
191
+ """
192
+ import sbol3
193
+ sbol3.set_namespace(namespace)
194
+ doc = sbol3.Document()
195
+ top = sbol3.Component(f"write_{self.write_type}", sbol3.SBO_DNA)
196
+ top.name = f"WriteRequest: {self.write_type}"
197
+ # native SO roles for the cargo (interoperable with the SBOL3 / GenBank ecosystem)
198
+ for i, c in enumerate(self.cargo):
199
+ sub = sbol3.Component(f"cargo_{i}_{re.sub(r'[^A-Za-z0-9]', '_', c.name)[:24]}", sbol3.SBO_DNA)
200
+ if c.role and c.role.id and c.role.ontology == "SO":
201
+ sub.roles = [f"https://identifiers.org/SO:{c.role.id.split(':')[1]}"]
202
+ if c.sequence:
203
+ seq = sbol3.Sequence(f"seq_{i}", elements=c.sequence.upper(), encoding=sbol3.IUPAC_DNA_ENCODING)
204
+ doc.add(seq)
205
+ sub.sequences = [seq.identity]
206
+ doc.add(sub)
207
+ top.features.append(sbol3.SubComponent(sub))
208
+ # the full typed spec rides losslessly as a custom (PROV-O-style) annotation
209
+ top.write_spec_json = sbol3.TextProperty(top, f"{namespace}#writeSpecJson", 0, 1)
210
+ top.write_spec_json = self.to_json()
211
+ doc.add(top)
212
+ return doc.write_string(sbol3.SORTED_NTRIPLES)
213
+
214
+ @classmethod
215
+ def from_sbol3(cls, ttl: str, namespace: str = "https://penstack.org/writespec") -> "WriteRequest":
216
+ """Reconstruct a WriteRequest from its SBOL3 serialization (the lossless PROV-O annotation)."""
217
+ import sbol3
218
+ sbol3.set_namespace(namespace)
219
+ doc = sbol3.Document()
220
+ doc.read_string(ttl, sbol3.SORTED_NTRIPLES)
221
+ for obj in doc.objects:
222
+ prop = f"{namespace}#writeSpecJson"
223
+ val = obj._properties.get(prop)
224
+ if val:
225
+ raw = str(val[0])
226
+ return cls.from_json(raw)
227
+ raise ValueError("no WriteRequest annotation found in the SBOL3 document")
228
+
229
+ def to_genbank(self) -> str | None:
230
+ """Export the cargo as a GenBank record when it carries a DNA sequence; None for an intent-only spec."""
231
+ seqs = [c for c in self.cargo if c.sequence]
232
+ if not seqs:
233
+ return None
234
+ from io import StringIO
235
+
236
+ from Bio.Seq import Seq
237
+ from Bio.SeqFeature import FeatureLocation, SeqFeature
238
+ from Bio.SeqRecord import SeqRecord
239
+ full = "".join(c.sequence.upper() for c in seqs)
240
+ rec = SeqRecord(Seq(full), id="WRITESPEC", name="WriteSpec", description=f"{self.write_type} cargo")
241
+ rec.annotations["molecule_type"] = "DNA"
242
+ pos = 0
243
+ for c in seqs:
244
+ ln = len(c.sequence)
245
+ role = (c.role.label if (c.role and c.role.label) else c.name)
246
+ rec.features.append(SeqFeature(FeatureLocation(pos, pos + ln), type="misc_feature",
247
+ qualifiers={"label": [role]}))
248
+ pos += ln
249
+ buf = StringIO()
250
+ from Bio import SeqIO
251
+ SeqIO.write(rec, buf, "genbank")
252
+ return buf.getvalue()
@@ -0,0 +1,14 @@
1
+ """pen_stack.twin, the digital twin for genome writing.
2
+
3
+ A calibrated, scope-bounded write-outcome predictor: mechanism where computable (cassette expression), an
4
+ in-distribution virtual-cell transcriptional-response estimate (OOD-gated), and an immune-outcome dimension
5
+ sourced from the immune profile, fused into one prediction with an interval that widens under OOD and an explicit
6
+ boundary at phenotype. A hypothesis engine, grounded where it is weak; never an oracle of truth.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from pen_stack.twin.calibrate import calibrate_outcome, interval_coverage
11
+ from pen_stack.twin.mechanistic import cassette_expression
12
+ from pen_stack.twin.outcome import predict_outcome
13
+
14
+ __all__ = ["predict_outcome", "cassette_expression", "calibrate_outcome", "interval_coverage"]
@@ -0,0 +1,61 @@
1
+ """Twin calibration, two-sided.
2
+
3
+ Reports the twin's calibration on held-out (predicted, observed) outcomes WHATEVER the shape: interval coverage
4
+ vs nominal, and a skill comparison against a NAIVE baseline (predict the mean) with a bootstrap CI on the MAE
5
+ gap. A twin "beats" the baseline only when the CI excludes zero in its favour, otherwise the negative is
6
+ reported verbatim. This mirrors the field's own result (Arc VCC): perturbation prediction does not yet
7
+ consistently beat naive baselines, and the twin says so rather than hiding it.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import numpy as np
12
+
13
+
14
+ def _mae(pred, obs) -> float:
15
+ return float(np.mean(np.abs(np.asarray(pred, float) - np.asarray(obs, float))))
16
+
17
+
18
+ def _bootstrap_gap(twin_pred, naive_pred, obs, *, reps: int = 300, seed: int = 0) -> tuple[float, list[float]]:
19
+ """Bootstrap the MAE gap (naive - twin); positive => twin better. Returns mean gap + 95% CI."""
20
+ rng = np.random.default_rng(seed)
21
+ twin_pred, naive_pred, obs = map(lambda a: np.asarray(a, float), (twin_pred, naive_pred, obs))
22
+ n = len(obs)
23
+ gaps = []
24
+ for _ in range(reps):
25
+ idx = rng.integers(0, n, n)
26
+ gaps.append(_mae(naive_pred[idx], obs[idx]) - _mae(twin_pred[idx], obs[idx]))
27
+ lo, hi = np.percentile(gaps, [2.5, 97.5])
28
+ return float(np.mean(gaps)), [round(float(lo), 4), round(float(hi), 4)]
29
+
30
+
31
+ def interval_coverage(intervals, obs) -> dict:
32
+ """Empirical coverage of the prediction intervals vs the observed outcomes."""
33
+ obs = np.asarray(obs, float)
34
+ covered = np.array([lo <= y <= hi for (lo, hi), y in zip(intervals, obs)])
35
+ return {"empirical_coverage": round(float(np.mean(covered)), 4), "n": int(len(obs))}
36
+
37
+
38
+ def calibrate_outcome(predictions, observations, *, intervals=None, reps: int = 300, seed: int = 0) -> dict:
39
+ """Two-sided calibration of the twin against a naive mean baseline. Reports the MAE gap with a
40
+ bootstrap CI (beats-or-not), and interval coverage when intervals are supplied."""
41
+ pred = np.asarray(predictions, float)
42
+ obs = np.asarray(observations, float)
43
+ n = len(obs)
44
+ if n < 3:
45
+ return {"available": False, "n": n, "reason": "too few held-out outcomes to calibrate"}
46
+ naive = np.full(n, float(np.mean(obs)))
47
+ mae_twin, mae_naive = _mae(pred, obs), _mae(naive, obs)
48
+ gap, ci = _bootstrap_gap(pred, naive, obs, reps=reps, seed=seed)
49
+ beats = bool(ci[0] > 0) # CI excludes 0 in the twin's favour
50
+ out = {
51
+ "available": True, "n": n,
52
+ "mae_twin": round(mae_twin, 4), "mae_naive": round(mae_naive, 4),
53
+ "mae_gap_naive_minus_twin": round(gap, 4), "gap_ci": ci,
54
+ "beats_naive_baseline": beats,
55
+ "honest_note": ("twin beats naive (CI excludes 0)" if beats
56
+ else "twin does NOT beat naive on this data (CI spans 0) - reported, not hidden"),
57
+ "no_fabrication": True,
58
+ }
59
+ if intervals is not None:
60
+ out["interval_coverage"] = interval_coverage(intervals, obs)
61
+ return out
@@ -0,0 +1,12 @@
1
+ """Position-effect / expression supervision data."""
2
+ from pen_stack.twin.data.position_effect import ( # noqa: F401
3
+ DATASETS,
4
+ FEATURE_COLS,
5
+ SCHEMA,
6
+ available_datasets,
7
+ blocked_splits,
8
+ heldout_celltype_splits,
9
+ leakage_report,
10
+ load_position_effect,
11
+ normalize_within,
12
+ )