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,201 @@
1
+ """Safety annotations per 1 kb bin (license-clean source).
2
+
3
+ Builds per-bin safety features from CancerMine (CC0; oncogene/TSG loci, the default, shipped) or COSMIC CGC
4
+ (local-only, bring-your-own-license), DepMap CRISPRGeneEffect (essential genes), and GENCODE (gene/TSS distances):
5
+ - dist_oncogene, dist_tsg, dist_essential, dist_tss (bp to nearest, via bedtools closest)
6
+ - genotoxic_cis flag (bins within a window of LMO2/MECOM/CCND2/PRDM16/HMGA2)
7
+
8
+ Inputs are staged on the VM under /data/external (COSMIC tsv, DepMap csv); GENCODE is downloaded.
9
+ Runs CPU-only in the penstack:phase1 image (bedtools in-image). Output keyed on (chrom, bin).
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import gzip
15
+ import os
16
+ from pathlib import Path
17
+
18
+ import numpy as np
19
+ import pandas as pd
20
+ import pybedtools
21
+ import requests
22
+
23
+ from pen_stack.data.genome import MAIN_CHROMS
24
+
25
+ GENCODE_GTF = ("https://ftp.ebi.ac.uk/pub/databases/gencode/Gencode_human/"
26
+ "release_46/gencode.v46.basic.annotation.gtf.gz")
27
+ GENOTOXIC = ["LMO2", "MECOM", "EVI1", "CCND2", "PRDM16", "HMGA2"]
28
+ BIN_BP = 1000
29
+ CIS_WINDOW = 50000 # bp window around a genotoxic gene to flag
30
+
31
+
32
+ def _chr(c: str) -> str:
33
+ c = str(c)
34
+ return c if c.startswith("chr") else f"chr{c}"
35
+
36
+
37
+ def load_cosmic(tsv: str) -> pd.DataFrame:
38
+ df = pd.read_csv(tsv, sep="\t", dtype=str)
39
+ df = df.dropna(subset=["CHROMOSOME", "GENOME_START", "GENOME_STOP"])
40
+ df["chrom"] = df["CHROMOSOME"].map(_chr)
41
+ df["start"] = pd.to_numeric(df["GENOME_START"], errors="coerce")
42
+ df["end"] = pd.to_numeric(df["GENOME_STOP"], errors="coerce")
43
+ df = df.dropna(subset=["start", "end"])
44
+ df["role"] = df.get("ROLE_IN_CANCER", "").fillna("")
45
+ df = df[df["chrom"].isin(MAIN_CHROMS)]
46
+ df["start"] = df["start"].astype(int)
47
+ df["end"] = df["end"].astype(int)
48
+ return df[["chrom", "start", "end", "GENE_SYMBOL", "role"]]
49
+
50
+
51
+ # CancerMine (CC0) is the license-clean oncogene/TSG/driver source that REPLACES COSMIC CGC in the shipped
52
+ # artifact. Lever et al., Nat Methods 16:505-507 (2019), doi:10.1038/s41592-019-0422-y, CC0; Zenodo record 7689627.
53
+ # COSMIC stays available (load_cosmic) but OFF by default, for local enrichment under the user's own license (BYO).
54
+ CANCERMINE_URL = "https://zenodo.org/records/7689627/files/cancermine_collated.tsv?download=1"
55
+ _ROLE_MAP = {"Oncogene": "oncogene", "Tumor_Suppressor": "TSG", "Driver": "driver"}
56
+
57
+
58
+ def load_cancermine(tsv: str, genes: pd.DataFrame, min_citations: int = 3) -> pd.DataFrame:
59
+ """CC0 oncogene/TSG/driver list. The collated file has one row per (gene, cancer, role) with a citation_count;
60
+ we aggregate per gene-role across cancers (sum citations), keep roles with >= min_citations, and map the HUGO
61
+ symbol -> genomic coordinates via GENCODE. Returns the SAME schema as load_cosmic (chrom,start,end,GENE_SYMBOL,
62
+ role) with the role string containing 'oncogene'/'TSG'/'driver' so the downstream filters are unchanged."""
63
+ cm = pd.read_csv(tsv, sep="\t", dtype=str)
64
+ cm["cites"] = pd.to_numeric(cm.get("citation_count"), errors="coerce").fillna(0)
65
+ agg = cm.groupby(["gene_normalized", "role"], as_index=False)["cites"].sum()
66
+ agg = agg[agg["cites"] >= float(min_citations)]
67
+ agg["rn"] = agg["role"].map(_ROLE_MAP).fillna(agg["role"])
68
+ roles = (agg.groupby("gene_normalized")["rn"]
69
+ .apply(lambda s: ",".join(sorted(set(s)))).reset_index(name="role"))
70
+ g = genes[["gene_name", "chrom", "start", "end"]].drop_duplicates("gene_name")
71
+ m = roles.merge(g, left_on="gene_normalized", right_on="gene_name", how="inner")
72
+ return m.rename(columns={"gene_normalized": "GENE_SYMBOL"})[["chrom", "start", "end", "GENE_SYMBOL", "role"]]
73
+
74
+
75
+ def load_depmap_essential(csv: str, thresh: float = -0.5) -> set[str]:
76
+ """Common-essential genes: mean Chronos effect across cell lines < thresh."""
77
+ df = pd.read_csv(csv, index_col=0)
78
+ means = df.mean(axis=0)
79
+ genes = {c.split(" (")[0] for c, m in means.items() if m < thresh}
80
+ return genes
81
+
82
+
83
+ def download_gencode(dest: str, url: str = GENCODE_GTF) -> str:
84
+ if not (os.path.exists(dest) and os.path.getsize(dest) > 0):
85
+ Path(dest).parent.mkdir(parents=True, exist_ok=True)
86
+ with requests.get(url, stream=True, timeout=600) as r:
87
+ r.raise_for_status()
88
+ with open(dest, "wb") as fh:
89
+ for ch in r.iter_content(1 << 20):
90
+ fh.write(ch)
91
+ return dest
92
+
93
+
94
+ def parse_gencode_genes(gtf_gz: str) -> pd.DataFrame:
95
+ rows = []
96
+ with gzip.open(gtf_gz, "rt") as fh:
97
+ for line in fh:
98
+ if line.startswith("#"):
99
+ continue
100
+ f = line.rstrip("\n").split("\t")
101
+ if f[2] != "gene":
102
+ continue
103
+ chrom = f[0]
104
+ if chrom not in MAIN_CHROMS:
105
+ continue
106
+ start, end, strand = int(f[3]), int(f[4]), f[6]
107
+ attrs = f[8]
108
+ name = ""
109
+ for kv in attrs.split(";"):
110
+ kv = kv.strip()
111
+ if kv.startswith("gene_name"):
112
+ name = kv.split('"')[1]
113
+ break
114
+ tss = start if strand == "+" else end
115
+ rows.append((chrom, start, end, strand, name, tss))
116
+ return pd.DataFrame(rows, columns=["chrom", "start", "end", "strand", "gene_name", "tss"])
117
+
118
+
119
+ def _bed(df: pd.DataFrame, cols=("chrom", "start", "end")) -> pybedtools.BedTool:
120
+ b = df[list(cols)].copy()
121
+ b.columns = ["chrom", "start", "end"]
122
+ b = b.sort_values(["chrom", "start"])
123
+ return pybedtools.BedTool.from_dataframe(b)
124
+
125
+
126
+ def nearest_dist(bins_bed: pybedtools.BedTool, feat_df: pd.DataFrame, name: str) -> pd.DataFrame:
127
+ if feat_df.empty:
128
+ return pd.DataFrame(columns=["chrom", "start", name])
129
+ fb = _bed(feat_df).sort()
130
+ closest = bins_bed.closest(fb, d=True)
131
+ out = closest.to_dataframe(header=None, usecols=[0, 1, closest.field_count() - 1],
132
+ names=["chrom", "start", name])
133
+ # bedtools closest -d returns -1 when there is NO feature on that chromosome; that means
134
+ # "no nearby feature" (effectively infinite distance), NOT distance 0. Map the sentinel to NaN.
135
+ out[name] = out[name].where(out[name] >= 0, other=np.nan)
136
+ return out.groupby(["chrom", "start"], as_index=False)[name].min()
137
+
138
+
139
+ def build(bin_grid: str, depmap_csv: str, gencode_dest: str, sizes_tsv: str, out_parquet: str, *,
140
+ source: str = "cancermine", cancermine_tsv: str | None = None, cosmic_tsv: str | None = None,
141
+ min_citations: int = 3) -> pd.DataFrame:
142
+ """Build per-bin safety features. `source` selects the LICENSE-CLEAN oncogene/TSG list: 'cancermine' (CC0,
143
+ default, shipped) or 'cosmic' (local-only, under the user's own license, bring-your-own-license enrichment)."""
144
+ grid = pd.read_parquet(bin_grid)[["chrom", "start", "bin"]]
145
+ bins_bed = _bed(grid.assign(end=grid["start"] + BIN_BP)).sort()
146
+
147
+ gtf = download_gencode(gencode_dest)
148
+ genes = parse_gencode_genes(gtf)
149
+
150
+ if source == "cosmic":
151
+ cg = load_cosmic(cosmic_tsv)
152
+ else: # default: CancerMine (CC0)
153
+ cg = load_cancermine(cancermine_tsv, genes, min_citations=min_citations)
154
+ onco = cg[cg["role"].str.contains("oncogene", case=False, na=False)]
155
+ tsg = cg[cg["role"].str.contains("TSG", case=False, na=False)]
156
+
157
+ ess_syms = load_depmap_essential(depmap_csv)
158
+ ess = genes[genes["gene_name"].isin(ess_syms)]
159
+
160
+ out = grid.copy()
161
+ for nm, fdf in [("dist_oncogene", onco), ("dist_tsg", tsg),
162
+ ("dist_essential", ess), ("dist_tss", genes.assign(end=genes["tss"] + 1, start=genes["tss"]))]:
163
+ d = nearest_dist(bins_bed, fdf, nm)
164
+ out = out.merge(d, on=["chrom", "start"], how="left")
165
+
166
+ # genotoxic CIS flag
167
+ gtox = genes[genes["gene_name"].isin(GENOTOXIC)].copy()
168
+ gtox["start"] = (gtox["start"] - CIS_WINDOW).clip(lower=0)
169
+ gtox["end"] = gtox["end"] + CIS_WINDOW
170
+ gflag = nearest_dist(bins_bed, gtox, "dist_gtox")
171
+ out = out.merge(gflag, on=["chrom", "start"], how="left")
172
+ out["genotoxic_cis"] = (out["dist_gtox"].fillna(1e9) == 0)
173
+
174
+ Path(out_parquet).parent.mkdir(parents=True, exist_ok=True)
175
+ out.to_parquet(out_parquet, index=False)
176
+ return out
177
+
178
+
179
+ def main() -> None:
180
+ ap = argparse.ArgumentParser()
181
+ ap.add_argument("--bin-grid", default="/data/features/bin_grid_1kb.parquet")
182
+ ap.add_argument("--source", choices=["cancermine", "cosmic"], default="cancermine",
183
+ help="oncogene/TSG source: cancermine (CC0, shipped default) or cosmic (local-only BYO-license)")
184
+ ap.add_argument("--cancermine", default="/data/external/cancermine_collated.tsv")
185
+ ap.add_argument("--min-citations", type=int, default=3,
186
+ help="CancerMine per-gene-role citation threshold (3 = validated precision: safety AUROC 0.74)")
187
+ ap.add_argument("--cosmic", default="/data/external/Cosmic_CancerGeneCensus_v104_GRCh38.tsv")
188
+ ap.add_argument("--depmap", default="/data/external/CRISPRGeneEffect.csv")
189
+ ap.add_argument("--gencode", default="/data/raw/gencode.v46.basic.gtf.gz")
190
+ ap.add_argument("--sizes", default="/data/raw/hg38.chrom.sizes")
191
+ ap.add_argument("--out", default="/data/features/safety_annot.parquet")
192
+ a = ap.parse_args()
193
+ df = build(a.bin_grid, a.depmap, a.gencode, a.sizes, a.out, source=a.source,
194
+ cancermine_tsv=a.cancermine, cosmic_tsv=a.cosmic, min_citations=a.min_citations)
195
+ n_onco = (df["dist_oncogene"] == 0).sum()
196
+ print(f"safety_annot[{a.source}] bins={len(df)} cols={[c for c in df.columns if c.startswith('dist') or c=='genotoxic_cis']}")
197
+ print(f"bins in an oncogene={n_onco} genotoxic_cis bins={int(df['genotoxic_cis'].sum())}")
198
+
199
+
200
+ if __name__ == "__main__":
201
+ main()
@@ -0,0 +1,76 @@
1
+ """TRIP durability supervision.
2
+
3
+ Ingests the Akhtar et al. 2013 TRIP data (GEO GSE49806 tet-O + GSE49807 mPGK; mouse mESC): each row is
4
+ one integrated reporter at a genomic position with expression. Produces (position, expression level,
5
+ silenced/expressed label) - the supervision for the conditional chromatin-context durability model.
6
+
7
+ The model learns `local chromatin features -> expression`; it never sees the coordinate. So TRIP being
8
+ mouse is fine: attach mouse (mES) chromatin features at these positions, train the function, then apply
9
+ it to a human epigenome (the headline function-transfer test).
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import gzip
15
+ from pathlib import Path
16
+
17
+ import numpy as np
18
+ import pandas as pd
19
+
20
+
21
+ def load_trip(txt_gz: str, promoter: str) -> pd.DataFrame:
22
+ """Robust to both TRIP schemas: GSE49807 (plain) and GSE49806 (leading '#' comment + multi-Dox
23
+ columns; we use the 100 ng full-induction normalization/expression pair)."""
24
+ with gzip.open(txt_gz, "rt") as fh:
25
+ raw = pd.read_csv(fh, sep="\t", comment="#", dtype=str)
26
+ cols = {c.lower().strip(): c for c in raw.columns}
27
+ chrom_c = cols.get("chromosome")
28
+ pos_c = cols.get("position")
29
+ norm_c = cols.get("normalization_counts_100ng_1") or cols.get("normalization_counts")
30
+ expr_c = cols.get("expression_counts_100ng_1") or cols.get("expression_counts")
31
+ if not all([chrom_c, pos_c, norm_c, expr_c]):
32
+ raise ValueError(f"{txt_gz}: missing expected columns; have {list(raw.columns)[:8]}")
33
+ df = pd.DataFrame({
34
+ "chrom": raw[chrom_c].astype(str),
35
+ "pos": pd.to_numeric(raw[pos_c], errors="coerce"),
36
+ "norm_counts": pd.to_numeric(raw[norm_c], errors="coerce"),
37
+ "expr_counts": pd.to_numeric(raw[expr_c], errors="coerce"),
38
+ }).dropna()
39
+ df["pos"] = df["pos"].astype(int)
40
+ df["promoter"] = promoter
41
+ return df
42
+
43
+
44
+ def assemble(files: dict[str, str], out_parquet: str, silenced_quantile: float = 0.25) -> pd.DataFrame:
45
+ parts = [load_trip(path, prom) for prom, path in files.items()]
46
+ df = pd.concat(parts, ignore_index=True)
47
+ # normalized expression (expression per normalization read), log scale
48
+ df["expression"] = np.log2((df["expr_counts"] + 1) / (df["norm_counts"] + 1))
49
+ # silenced/expressed: low tail of expression flagged silenced (per promoter, to control for promoter strength)
50
+ df["silenced"] = False
51
+ for prom, g in df.groupby("promoter"):
52
+ thr = g["expression"].quantile(silenced_quantile)
53
+ df.loc[g.index, "silenced"] = g["expression"] <= thr
54
+ df["stable"] = ~df["silenced"]
55
+ Path(out_parquet).parent.mkdir(parents=True, exist_ok=True)
56
+ df.to_parquet(out_parquet, index=False)
57
+ return df
58
+
59
+
60
+ def main() -> None:
61
+ ap = argparse.ArgumentParser()
62
+ ap.add_argument("--teto", default="/data/external/trip/GSE49806_S2.txt.gz")
63
+ ap.add_argument("--mpgk", default="/data/external/trip/GSE49807_S3.txt.gz")
64
+ ap.add_argument("--out", default="/data/features/trip_mesc.parquet")
65
+ a = ap.parse_args()
66
+ files = {k: v for k, v in {"tetO": a.teto, "mPGK": a.mpgk}.items() if Path(v).exists()}
67
+ df = assemble(files, a.out)
68
+ print(f"TRIP integrations: {len(df)} promoters={df['promoter'].value_counts().to_dict()}")
69
+ print(f"expression range (log2): [{df['expression'].min():.2f}, {df['expression'].max():.2f}] "
70
+ f"~{2**(df['expression'].max()-df['expression'].min()):.0f}-fold")
71
+ print(f"silenced={int(df['silenced'].sum())} stable={int(df['stable'].sum())}")
72
+ print(f"chroms: {sorted(df['chrom'].unique())[:6]}... (mouse build)")
73
+
74
+
75
+ if __name__ == "__main__":
76
+ main()
@@ -0,0 +1,14 @@
1
+ """pen_stack.design, the grounded generative designer.
2
+
3
+ Generate candidate end-to-end writing systems, keep only those that pass safety + legality + calibration
4
+ (verifier-as-discriminator), and return the Pareto frontier of real tradeoffs, including an immune-risk axis
5
+ grounded in the profile. Nothing unvalidated is asserted: every survivor is `output_kind="candidate"`.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from pen_stack.design.generate import generate_designs
10
+ from pen_stack.design.pareto import AXES, neg_immune_risk, pareto_front
11
+ from pen_stack.design.space import candidate_space, deliverability_score
12
+
13
+ __all__ = ["generate_designs", "candidate_space", "deliverability_score",
14
+ "pareto_front", "neg_immune_risk", "AXES"]
@@ -0,0 +1,62 @@
1
+ """Verify-gated generative AAV capsid candidates.
2
+
3
+ Proposes engineered AAV capsid variants in the mutagenized VP1 region, scores them with the learned FLIP-AAV
4
+ capsid-fitness model, and keeps only candidates above a fitness threshold, the verifier-as-discriminator pattern
5
+ GENERATE proposes, the fitness model + biosecurity context DISPOSE. Every survivor is a CANDIDATE,
6
+ never asserted to assemble or to have any in-vivo tropism. Abstains when the fitness model is not present (no
7
+ fabricated capsids).
8
+
9
+ a generated capsid's predicted fitness is for the MEASURED packaging axis only; assembly, in-vivo tropism,
10
+ and immunogenicity are not claimed (the latter routes through the immune-risk axis). This is a design proposer, not a validation.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ _AAS = "ACDEFGHIKLMNPQRSTVWY"
15
+
16
+
17
+ def _mutate(seq: str, region: tuple[int, int], n_mut: int, rng) -> str:
18
+ s = list(seq)
19
+ lo, hi = region
20
+ positions = rng.sample(range(lo, min(hi, len(s))), min(n_mut, max(0, min(hi, len(s)) - lo)))
21
+ for p in positions:
22
+ s[p] = _AAS[rng.randrange(len(_AAS))]
23
+ return "".join(s)
24
+
25
+
26
+ def generate_capsid_candidates(wt_vp1: str, n: int = 200, max_mut: int = 4, fitness_threshold: float | None = None,
27
+ seed: int = 7, top: int = 20) -> dict:
28
+ """Propose AAV capsid variants (random substitutions in the VP1 555-595 mutagenized window), score with the
29
+ learned capsid-fitness model, and return the top survivors above ``fitness_threshold`` (defaults to the WT's
30
+ predicted fitness). Candidates are flagged; abstains when the fitness model is absent, never fabricates."""
31
+ import random
32
+ from pen_stack.planner.delivery_predict import capsid_fitness
33
+ wt = capsid_fitness(wt_vp1)
34
+ if not wt.get("available"):
35
+ return {"available": False, "abstain": True, "candidates": [], "output_kind": "candidate",
36
+ "note": "capsid-fitness model not present -> cannot score generated capsids; no fabrication.",
37
+ "bench": wt.get("bench")}
38
+ rng = random.Random(seed)
39
+ thr = fitness_threshold if fitness_threshold is not None else wt["predicted_fitness"]
40
+ region = (555, 595)
41
+ seen = set()
42
+ scored = []
43
+ for _ in range(max(1, n)):
44
+ nm = rng.randint(1, max_mut)
45
+ cand = _mutate(wt_vp1, region, nm, rng)
46
+ if cand == wt_vp1 or cand in seen:
47
+ continue
48
+ seen.add(cand)
49
+ f = capsid_fitness(cand)["predicted_fitness"]
50
+ scored.append({"vp1_window": cand[region[0]:region[1]], "predicted_fitness": f,
51
+ "n_mut_in_region": sum(1 for a, b in zip(cand[region[0]:region[1]],
52
+ wt_vp1[region[0]:region[1]]) if a != b),
53
+ "passes_threshold": bool(f >= thr), "output_kind": "candidate"})
54
+ survivors = sorted([c for c in scored if c["passes_threshold"]],
55
+ key=lambda c: c["predicted_fitness"], reverse=True)[:top]
56
+ return {"available": True, "abstain": False, "wt_predicted_fitness": wt["predicted_fitness"],
57
+ "fitness_threshold": round(float(thr), 4), "n_proposed": len(scored), "n_survivors": len(survivors),
58
+ "candidates": survivors, "output_kind": "candidate",
59
+ "verifier": "learned FLIP-AAV capsid-fitness (verifier-as-discriminator); fitness >= WT kept",
60
+ "honesty": "CANDIDATES, predicted packaging fitness only; assembly, in-vivo tropism, and "
61
+ "immunogenicity are NOT claimed (immunogenicity routes through the immune-risk profile). A design proposer, "
62
+ "not a validation. Run a biosecurity/verify() gate before any synthesis."}
@@ -0,0 +1,70 @@
1
+ """Generative inverse design, verifier-as-discriminator.
2
+
3
+ Generation PROPOSES candidate end-to-end writing systems; the `verify()`, now safety-gated,
4
+ legality-checked, calibrated, and immune-profiled, DISPOSES. A candidate that is hazardous
5
+ (safety refuse/escalate) or illegal is DISCARDED, never returned as a claim: the `as_claim()` guard generalised
6
+ to whole designs. Survivors are explicitly `output_kind="candidate"`, calibrated and immune-profiled, but never
7
+ asserted to work.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from pen_stack.design.space import candidate_space, vehicle_sweep
14
+ from pen_stack.verify import verify
15
+
16
+ # a survivor must be legal AND safe (cleared or low-severity advisory); refuse/escalate are discarded.
17
+ _SAFE_DECISIONS = {"clear", "flag"}
18
+
19
+
20
+ def _confidence_key(d: dict) -> tuple:
21
+ c = d.get("confidence")
22
+ return (c is not None, c if c is not None else -1.0)
23
+
24
+
25
+ def generate_designs(goal: dict | None = None, *, candidates: list[dict] | None = None,
26
+ n: int = 200, keep: int = 25, actor: str = "generator") -> list[dict[str, Any]]:
27
+ """Generate -> discriminate with `verify()`. Returns the surviving candidates, each carrying a calibrated
28
+ confidence (or explicit abstention), the immune profile, the safety decision, and scope flags, sorted
29
+ by confidence. Hazardous (refuse/escalate) or illegal proposals are discarded, never returned.
30
+
31
+ Pass an explicit ``candidates`` list to discriminate a known pool (atlas-independent); otherwise candidates
32
+ are enumerated from ``goal`` via the planner-backed candidate space.
33
+
34
+ When the goal's cell type has NO measured writability atlas (so the planner-backed candidate_space
35
+ yields nothing), fall back to an atlas-free vehicle sweep so the legality + biosecurity discrimination still
36
+ runs; those survivors carry no calibrated confidence (it abstains, surfaced in the UI) rather than
37
+ the page showing an empty table for a perfectly legal, screened design. The explicit-``candidates`` path is
38
+ untouched (``generate_designs(candidates=[])`` still returns [])."""
39
+ if candidates is not None:
40
+ pool = candidates
41
+ else:
42
+ pool = candidate_space(goal or {}, n=n)
43
+ if not pool and goal: # atlas absent for this cell type -> atlas-free sweep (confidence will abstain)
44
+ pool = vehicle_sweep(goal, n=n)
45
+ survivors: list[dict] = []
46
+ for d in pool:
47
+ v = verify(dict(d), actor=actor)
48
+ decision = v.safety.decision if v.safety is not None else "clear"
49
+ if v.legal is not True or decision not in _SAFE_DECISIONS:
50
+ continue # discarded by the discriminator
51
+ survivors.append({
52
+ **d,
53
+ "confidence": v.confidence, "interval": v.interval,
54
+ "immune_profile": v.immune_profile, # grounded per-axis vector
55
+ "safety_decision": decision, "legal": v.legal,
56
+ "scope_flags": v.scope_flags, "output_kind": "candidate",
57
+ })
58
+ survivors.sort(key=_confidence_key, reverse=True)
59
+ # Dedupe: candidate_space pairs every top SITE with every vehicle, so several same-(writer, vehicle, cargo)
60
+ # candidates differ only by site and render identically in the sweep table. Collapse them, keeping the
61
+ # highest-confidence one (already first after the sort). The site remains available on the survivor.
62
+ seen: set = set()
63
+ deduped: list[dict] = []
64
+ for s in survivors:
65
+ key = (s.get("write_type"), s.get("writer_family"), s.get("delivery_vehicle"), s.get("cargo_bp"))
66
+ if key in seen:
67
+ continue
68
+ seen.add(key)
69
+ deduped.append(s)
70
+ return deduped[:keep]
@@ -0,0 +1,70 @@
1
+ """Multi-objective Pareto frontier with a GROUNDED immune-risk axis.
2
+
3
+ Returns the non-dominated set of generated candidates over the design tradeoff axes. The `neg_immune_risk` axis
4
+ is sourced from the immune profile (no longer a placeholder): it is the WORST-CASE per-axis in-scope score
5
+ (lower score = higher risk) with the per-axis uncertainty carried as a band, never collapsing the profile into
6
+ one confident number, and keeping the in-vivo magnitude scope-flagged. Unknown axes enter as flagged bounds,
7
+ never fabricated points; the per-axis profile remains the source of truth.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ # higher is better for every axis (neg_* are already oriented so that higher = better).
14
+ AXES = ("efficiency", "durability", "safety", "deliverability", "neg_immune_risk", "neg_cost")
15
+ _COST_CAP_BP = 10000
16
+
17
+
18
+ def neg_immune_risk(design: dict) -> dict:
19
+ """Aggregate the immune profile into ONE Pareto axis WITHOUT collapsing it into a confident number:
20
+ worst-case per-axis in-scope value + the largest per-axis uncertainty as a band. Returns the value (or None
21
+ when no axis is in scope) + the band + which axes were used + the standing in-vivo scope flag."""
22
+ prof = (design.get("immune_profile") or {}).get("axes", {})
23
+ in_scope = {k: a for k, a in prof.items()
24
+ if a.get("in_scope") and a.get("value") is not None}
25
+ worst = min((a["value"] for a in in_scope.values()), default=None) # lower score = higher risk
26
+ band = max((a.get("uncertainty") or 0.0 for a in in_scope.values()), default=0.0)
27
+ return {"value": worst, "uncertainty": band, "axes_used": sorted(in_scope),
28
+ "scope_flag": "in_vivo_magnitude_unknown"}
29
+
30
+
31
+ def _scores(design: dict) -> dict[str, float | None]:
32
+ cargo = float(design.get("cargo_bp") or 0)
33
+ nir = neg_immune_risk(design)
34
+ return {
35
+ "efficiency": _f(design.get("writer_activity")),
36
+ "durability": _f(design.get("p_durable")),
37
+ "safety": _f(design.get("safety")),
38
+ "deliverability": _f(design.get("deliverability")),
39
+ "neg_immune_risk": nir["value"],
40
+ "neg_cost": max(0.0, 1.0 - min(cargo, _COST_CAP_BP) / _COST_CAP_BP),
41
+ }
42
+
43
+
44
+ def _f(x) -> float | None:
45
+ try:
46
+ return float(x)
47
+ except (TypeError, ValueError):
48
+ return None
49
+
50
+
51
+ def _dominates(a: dict, b: dict) -> bool:
52
+ """`a` dominates `b` iff a is >= on every COMPARABLE axis and strictly > on at least one. Axes that are
53
+ None (unknown) on either design are skipped (a flagged bound never fabricates a winning point)."""
54
+ comparable = [k for k in AXES if a["scores"][k] is not None and b["scores"][k] is not None]
55
+ if not comparable:
56
+ return False
57
+ ge = all(a["scores"][k] >= b["scores"][k] for k in comparable)
58
+ gt = any(a["scores"][k] > b["scores"][k] for k in comparable)
59
+ return ge and gt
60
+
61
+
62
+ def pareto_front(designs: list[dict]) -> list[dict[str, Any]]:
63
+ """Non-dominated set over AXES. Each returned design gets a `scores` dict and a `neg_immune_risk_detail`
64
+ (value + uncertainty band + axes used + in-vivo scope flag). The immune axis is grounded."""
65
+ enriched = []
66
+ for d in designs:
67
+ d = {**d, "scores": _scores(d), "neg_immune_risk_detail": neg_immune_risk(d)}
68
+ enriched.append(d)
69
+ front = [d for d in enriched if not any(_dominates(o, d) for o in enriched if o is not d)]
70
+ return front
@@ -0,0 +1,137 @@
1
+ """Candidate-space generation for the generative designer.
2
+
3
+ `candidate_space(goal)` enumerates candidate end-to-end writing systems, writer x site x cargo x delivery,
4
+ by wrapping the validated inverse-design planner (`plan_write`) for the site x writer x scores, then
5
+ pairing each with every *compatible* delivery vehicle from the curated palette (cargo fits the vehicle
6
+ capacity). Every candidate carries the planner's grounded scores (safety / p_durable / writer_activity) so the
7
+ verifier-as-discriminator can compute a calibrated confidence and the immune profile downstream.
8
+
9
+ Atlas-dependent: when the writability atlas is absent (e.g. a bare laptop checkout), `plan_write`
10
+ yields nothing and `candidate_space` returns []. The discriminator/Pareto logic is independent of the atlas and
11
+ is exercised on explicit candidates (tests/bench fixtures); the atlas path runs on the VM/with data.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from typing import Any
16
+
17
+ from pen_stack.planner.delivery_vehicles import names as _vehicle_names
18
+ from pen_stack.planner.delivery_vehicles import vehicle as _vehicle
19
+
20
+ # edit-intent -> write_type (the router/verifier dispatches on write_type)
21
+ _INTENT_WRITE_TYPE = {
22
+ "safe_harbour_insertion": "insertion",
23
+ "high_durability_insertion": "insertion",
24
+ "knock_in_with_disruption": "insertion",
25
+ "landing_pad_insertion": "landing_pad",
26
+ "regulatory_element_excision": "excision",
27
+ "repeat_excision": "excision",
28
+ "regulatory_rewrite": "regulatory_rewrite",
29
+ }
30
+
31
+
32
+ def _compatible_vehicles(cargo_bp: int, in_vivo: bool | None = None) -> list[str]:
33
+ """Vehicles whose curated cargo capacity fits the cargo (capacity None = no DNA-packaging limit).
34
+
35
+ When an administration ``in_vivo`` context is given, also filter by the vehicle's curated route
36
+ (the `in_vivo` / `ex_vivo` flags in configs/delivery_vehicles.yaml) - an in-vivo goal keeps the in-vivo-capable
37
+ vehicles, an ex-vivo goal keeps the ex-vivo-capable ones (e.g. lentivirus / electroporation / eVLP). This is a
38
+ grounded compatibility filter from the curated palette, not a clinical claim. ``in_vivo=None`` keeps all."""
39
+ out = []
40
+ for n in _vehicle_names():
41
+ v = _vehicle(n) or {}
42
+ cap = v.get("cargo_capacity_bp")
43
+ if cap is not None and cargo_bp > cap:
44
+ continue
45
+ if in_vivo is True and not v.get("in_vivo"):
46
+ continue
47
+ if in_vivo is False and not v.get("ex_vivo"):
48
+ continue
49
+ out.append(n)
50
+ return out
51
+
52
+
53
+ def deliverability_score(vehicle_name: str, cargo_bp: int) -> float:
54
+ """Grounded deliverability proxy: capacity headroom (more spare capacity = more deliverable),
55
+ clipped to [0,1]. Vehicles with no packaging limit score 1.0. NOT a clinical claim."""
56
+ cap = (_vehicle(vehicle_name) or {}).get("cargo_capacity_bp")
57
+ if cap is None:
58
+ return 1.0
59
+ if cargo_bp > cap:
60
+ return 0.0
61
+ return max(0.0, min(1.0, (cap - cargo_bp) / cap))
62
+
63
+
64
+ def candidate_space(goal: dict, *, n: int = 200, k: int = 8) -> list[dict[str, Any]]:
65
+ """Enumerate candidate designs for a goal {gene, intent, cargo_bp, cell_type, cargo_function?}. Each candidate
66
+ is a plain dict consumable by `verify()` and carries the planner's grounded scores. Returns [] if the atlas is
67
+ absent.
68
+
69
+ The goal's screening-relevant fields (cargo_function, cargo_seq, in_vivo, delivery_tropism,
70
+ replication_competent) are PROPAGATED onto every candidate so the Guardian biosecurity gate in verify() screens
71
+ the cargo function for each swept vehicle. Previously these were dropped here, so a hazardous cargo function
72
+ reached NO screen on the goal-based path."""
73
+ gene = goal["gene"]
74
+ intent = goal.get("intent") or goal.get("edit_intent") or "safe_harbour_insertion"
75
+ cargo_bp = int(goal.get("cargo_bp") or goal.get("payload_bp") or 3000)
76
+ ct = goal.get("cell_type") or goal.get("ct") or "k562"
77
+ write_type = _INTENT_WRITE_TYPE.get(intent, "insertion")
78
+ # screening-relevant goal fields propagated onto every candidate (the Guardian reads cargo_function).
79
+ screen_extra = {f: goal[f] for f in ("cargo_function", "cargo_seq", "in_vivo", "delivery_tropism",
80
+ "replication_competent") if goal.get(f) is not None}
81
+
82
+ from pen_stack.planner.pipeline import plan_write
83
+ try:
84
+ plans = plan_write(gene, intent, cargo_bp, ct, k=k)
85
+ except Exception: # atlas/data absent -> no candidates (discriminator/Pareto unaffected)
86
+ return []
87
+
88
+ vehicles = _compatible_vehicles(cargo_bp, in_vivo=goal.get("in_vivo"))
89
+ cands: list[dict] = []
90
+ for p in plans:
91
+ for veh in vehicles:
92
+ cands.append({
93
+ **screen_extra, # cargo_function et al. so the Guardian screens this candidate
94
+ "write_type": write_type, "gene": gene, "chrom": p["site"]["chrom"],
95
+ "edit_intent": intent, "writer_family": p["writer"], "cargo_bp": cargo_bp,
96
+ "cell_type": ct, "delivery_vehicle": veh,
97
+ # grounded planner scores (model_extra) -> verify() calibrated confidence + downstream:
98
+ "safety": p["safety"], "p_durable": p["durability"], "writer_activity": p["writer_activity"],
99
+ "on_target": p["on_target"], "reachability_tier": p.get("reachability_tier"),
100
+ "deliverability": deliverability_score(veh, cargo_bp),
101
+ "site": p.get("site"), "cargo": p.get("cargo"), # carry the site + assembled cassette to the survivor
102
+ "_planner_score": p["score"],
103
+ "provenance": {**p.get("provenance", {}), "candidate_space": "plan_write x delivery palette"},
104
+ })
105
+ if len(cands) >= n:
106
+ return cands
107
+ return cands
108
+
109
+
110
+ def vehicle_sweep(goal: dict, *, n: int = 200) -> list[dict[str, Any]]:
111
+ """Atlas-FREE candidate enumeration: sweep the compatible delivery vehicles for the goal WITHOUT planner
112
+ scores. Used when the writability atlas has no coverage for the requested cell type (e.g. cd8_t / pbmc /
113
+ h1_hesc), so `candidate_space` (which needs `plan_write`) yields nothing.
114
+
115
+ Each candidate carries the goal's cargo_function (so the Guardian still screens it), the cargo size (so the
116
+ capacity rules still filter oversize vehicles), and NO safety/p_durable/writer_activity (so verify()
117
+ ABSTAINS on the calibrated confidence rather than inventing one). The legality + biosecurity discrimination is
118
+ fully exercised; only the calibrated-confidence column is left uncomputed (and labelled as such in the UI)."""
119
+ intent = goal.get("intent") or goal.get("edit_intent") or "safe_harbour_insertion"
120
+ cargo_bp = int(goal.get("cargo_bp") or goal.get("payload_bp") or 3000)
121
+ ct = goal.get("cell_type") or goal.get("ct") or "k562"
122
+ write_type = _INTENT_WRITE_TYPE.get(intent, "insertion")
123
+ screen_extra = {f: goal[f] for f in ("cargo_function", "cargo_seq", "in_vivo", "delivery_tropism",
124
+ "replication_competent") if goal.get(f) is not None}
125
+ cands: list[dict] = []
126
+ for veh in _compatible_vehicles(cargo_bp, in_vivo=goal.get("in_vivo")):
127
+ cands.append({
128
+ **screen_extra,
129
+ "write_type": write_type, "gene": goal.get("gene"), "chrom": goal.get("chrom"),
130
+ "edit_intent": intent, "cargo_bp": cargo_bp, "cell_type": ct, "delivery_vehicle": veh,
131
+ "deliverability": deliverability_score(veh, cargo_bp),
132
+ "provenance": {"candidate_space": "atlas-free vehicle sweep "
133
+ "(no measured writability atlas for this cell type; confidence abstains)"},
134
+ })
135
+ if len(cands) >= n:
136
+ break
137
+ return cands