pen-stack 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pen_stack/__init__.py +2 -0
- pen_stack/_resources.py +34 -0
- pen_stack/active/__init__.py +20 -0
- pen_stack/active/acquire.py +165 -0
- pen_stack/active/brains.py +74 -0
- pen_stack/active/campaign.py +109 -0
- pen_stack/active/design.py +66 -0
- pen_stack/active/validate.py +104 -0
- pen_stack/adapt/__init__.py +14 -0
- pen_stack/adapt/finetune.py +33 -0
- pen_stack/adapt/ingest.py +86 -0
- pen_stack/adapt/pipeline.py +101 -0
- pen_stack/adapt/recalibrate.py +58 -0
- pen_stack/adapt/report.py +130 -0
- pen_stack/agent/__init__.py +1 -0
- pen_stack/agent/cite.py +175 -0
- pen_stack/agent/co_scientist.py +262 -0
- pen_stack/agent/epistemic.py +102 -0
- pen_stack/agent/guardrails.py +67 -0
- pen_stack/agent/mcp_server.py +215 -0
- pen_stack/agent/orchestrator.py +112 -0
- pen_stack/agent/orchestrator_live.py +56 -0
- pen_stack/agent/pen_agent.py +242 -0
- pen_stack/agent/scope.py +60 -0
- pen_stack/agent/tools.py +130 -0
- pen_stack/api/__init__.py +12 -0
- pen_stack/api/manifest.py +160 -0
- pen_stack/atlas/__init__.py +1 -0
- pen_stack/atlas/atlas.parquet +0 -0
- pen_stack/atlas/build_wtkb.py +80 -0
- pen_stack/atlas/crosslink.py +179 -0
- pen_stack/atlas/expand.py +190 -0
- pen_stack/atlas/guide_design.py +178 -0
- pen_stack/atlas/schema.py +59 -0
- pen_stack/atlas/scorecard.py +134 -0
- pen_stack/atlas/scorecard_v3.parquet +0 -0
- pen_stack/atlas/universe.py +75 -0
- pen_stack/atlas/universe_v3.parquet +0 -0
- pen_stack/atlas/variant_propose.py +155 -0
- pen_stack/atlas/writer_efficiency.py +184 -0
- pen_stack/atlas/writer_predict.py +229 -0
- pen_stack/atlas/writer_recommend.py +170 -0
- pen_stack/atlas/writer_verify.py +167 -0
- pen_stack/atlas/wtkb.parquet +0 -0
- pen_stack/bridge/__init__.py +1 -0
- pen_stack/bridge/activity.py +52 -0
- pen_stack/bridge/cli.py +65 -0
- pen_stack/bridge/fold_qc.py +53 -0
- pen_stack/bridge/guide_qc.py +87 -0
- pen_stack/bridge/ingest.py +139 -0
- pen_stack/bridge/offtarget.py +191 -0
- pen_stack/bridge/offtarget_energetics.py +105 -0
- pen_stack/bridge/ortholog_screen.py +73 -0
- pen_stack/bridge/pipeline.py +83 -0
- pen_stack/build/__init__.py +16 -0
- pen_stack/build/cloudlab.py +74 -0
- pen_stack/build/ingest.py +47 -0
- pen_stack/build/protocol.py +82 -0
- pen_stack/build/simlab.py +30 -0
- pen_stack/cli.py +126 -0
- pen_stack/data/__init__.py +1 -0
- pen_stack/data/encode.py +84 -0
- pen_stack/data/genome.py +71 -0
- pen_stack/data/ingest_chromatin.py +119 -0
- pen_stack/data/ingest_integration.py +112 -0
- pen_stack/data/ingest_safety_annot.py +201 -0
- pen_stack/data/ingest_trip.py +76 -0
- pen_stack/design/__init__.py +14 -0
- pen_stack/design/capsid_generate.py +62 -0
- pen_stack/design/generate.py +70 -0
- pen_stack/design/pareto.py +70 -0
- pen_stack/design/space.py +137 -0
- pen_stack/design/writer_variants.py +121 -0
- pen_stack/env/__init__.py +1 -0
- pen_stack/env/genome_writing_env.py +248 -0
- pen_stack/env/policies.py +94 -0
- pen_stack/graph/__init__.py +21 -0
- pen_stack/graph/build.py +133 -0
- pen_stack/graph/cell_types.py +58 -0
- pen_stack/graph/ingest.py +132 -0
- pen_stack/graph/query.py +148 -0
- pen_stack/graph/schema.py +100 -0
- pen_stack/loop/__init__.py +15 -0
- pen_stack/loop/continual.py +61 -0
- pen_stack/loop/cycle.py +84 -0
- pen_stack/loop/drift.py +41 -0
- pen_stack/mech/__init__.py +1 -0
- pen_stack/mech/classify_atlas.py +71 -0
- pen_stack/mech/pfam_whitelist.yaml +247 -0
- pen_stack/mech/whitelist.py +66 -0
- pen_stack/monitor/__init__.py +1 -0
- pen_stack/monitor/europepmc.py +32 -0
- pen_stack/monitor/run.py +57 -0
- pen_stack/monitor/triage.py +63 -0
- pen_stack/oracles/__init__.py +65 -0
- pen_stack/oracles/affinity.py +116 -0
- pen_stack/oracles/cache.py +53 -0
- pen_stack/oracles/energetics.py +33 -0
- pen_stack/oracles/genome.py +167 -0
- pen_stack/oracles/protein_design.py +136 -0
- pen_stack/oracles/reliability.py +64 -0
- pen_stack/oracles/rna.py +28 -0
- pen_stack/oracles/schema.py +77 -0
- pen_stack/oracles/status.py +123 -0
- pen_stack/oracles/structure.py +42 -0
- pen_stack/oracles/structure_run.py +76 -0
- pen_stack/oracles/vcell.py +74 -0
- pen_stack/planner/__init__.py +1 -0
- pen_stack/planner/ada_risk.py +64 -0
- pen_stack/planner/antipeg_oracle.py +75 -0
- pen_stack/planner/capsid_epitope_oracle.py +135 -0
- pen_stack/planner/cargo.py +56 -0
- pen_stack/planner/cargo_polish.py +146 -0
- pen_stack/planner/chromosome.py +106 -0
- pen_stack/planner/delivery.py +55 -0
- pen_stack/planner/delivery_constraints.py +110 -0
- pen_stack/planner/delivery_immune.py +61 -0
- pen_stack/planner/delivery_immunology.py +222 -0
- pen_stack/planner/delivery_predict.py +196 -0
- pen_stack/planner/delivery_vehicles.py +37 -0
- pen_stack/planner/genotoxicity_oracle.py +112 -0
- pen_stack/planner/immune_mhc2.py +154 -0
- pen_stack/planner/immune_profile.py +292 -0
- pen_stack/planner/innate_sensing.py +135 -0
- pen_stack/planner/multiplex.py +110 -0
- pen_stack/planner/optimize.py +278 -0
- pen_stack/planner/pipeline.py +87 -0
- pen_stack/planner/report.py +26 -0
- pen_stack/planner/router.py +57 -0
- pen_stack/planner/seroprevalence_oracle.py +92 -0
- pen_stack/planner/target_site.py +118 -0
- pen_stack/rag/__init__.py +1 -0
- pen_stack/rag/corpus.py +133 -0
- pen_stack/rag/embed.py +98 -0
- pen_stack/rag/ground.py +131 -0
- pen_stack/rag/index.py +53 -0
- pen_stack/rag/llm.py +215 -0
- pen_stack/rag/qa.py +105 -0
- pen_stack/rag/retrieve.py +48 -0
- pen_stack/rules/__init__.py +9 -0
- pen_stack/rules/evaluators.py +318 -0
- pen_stack/rules/loader.py +31 -0
- pen_stack/rules/schema.py +99 -0
- pen_stack/rules/solver.py +43 -0
- pen_stack/rules/spec.py +78 -0
- pen_stack/safety/__init__.py +21 -0
- pen_stack/safety/audit.py +90 -0
- pen_stack/safety/gate.py +58 -0
- pen_stack/safety/pfam_scan.py +157 -0
- pen_stack/safety/policy.py +69 -0
- pen_stack/safety/redteam.py +71 -0
- pen_stack/safety/registry.py +255 -0
- pen_stack/safety/screen.py +59 -0
- pen_stack/safety/standards.py +141 -0
- pen_stack/score/__init__.py +1 -0
- pen_stack/score/recalibrate.py +77 -0
- pen_stack/score/therapeutic.py +85 -0
- pen_stack/server/__init__.py +1 -0
- pen_stack/server/api.py +647 -0
- pen_stack/spec/__init__.py +18 -0
- pen_stack/spec/clarify.py +42 -0
- pen_stack/spec/extract.py +406 -0
- pen_stack/spec/resolvers/__init__.py +17 -0
- pen_stack/spec/resolvers/cell.py +51 -0
- pen_stack/spec/resolvers/chem.py +32 -0
- pen_stack/spec/resolvers/feature.py +36 -0
- pen_stack/spec/resolvers/gene.py +43 -0
- pen_stack/spec/resolvers/locus.py +29 -0
- pen_stack/spec/resolvers/phenotype.py +37 -0
- pen_stack/spec/satisfy.py +114 -0
- pen_stack/spec/service.py +33 -0
- pen_stack/spec/writespec.py +252 -0
- pen_stack/twin/__init__.py +14 -0
- pen_stack/twin/calibrate.py +61 -0
- pen_stack/twin/data/__init__.py +12 -0
- pen_stack/twin/data/position_effect.py +245 -0
- pen_stack/twin/mechanistic.py +147 -0
- pen_stack/twin/outcome.py +132 -0
- pen_stack/twin/position_effect.py +454 -0
- pen_stack/ui/__init__.py +1 -0
- pen_stack/ui/app.py +713 -0
- pen_stack/validate/__init__.py +1 -0
- pen_stack/validate/adapt_demo.py +69 -0
- pen_stack/validate/agent_eval.py +117 -0
- pen_stack/validate/bench_adversarial_tasks.py +118 -0
- pen_stack/validate/bench_coscientist_tasks.py +60 -0
- pen_stack/validate/bench_graph_tasks.py +64 -0
- pen_stack/validate/bench_rule_tasks.py +84 -0
- pen_stack/validate/bench_trust_tasks.py +92 -0
- pen_stack/validate/bench_writetype_tasks.py +101 -0
- pen_stack/validate/blind_gsh_discovery.py +261 -0
- pen_stack/validate/cargo_directionality.py +57 -0
- pen_stack/validate/closed_loop.py +63 -0
- pen_stack/validate/durability_baselines.py +185 -0
- pen_stack/validate/experiment_design.py +65 -0
- pen_stack/validate/expr_controls.py +39 -0
- pen_stack/validate/forward_hypotheses.py +104 -0
- pen_stack/validate/generative_design.py +62 -0
- pen_stack/validate/guide_qc_demo.py +69 -0
- pen_stack/validate/heldout_celltype_expr.py +32 -0
- pen_stack/validate/immune_calibration.py +133 -0
- pen_stack/validate/intent_specification.py +82 -0
- pen_stack/validate/known_biology_expr.py +38 -0
- pen_stack/validate/offtarget_energetics_eval.py +144 -0
- pen_stack/validate/out_of_scope_refusal.py +82 -0
- pen_stack/validate/outcome_calibration.py +194 -0
- pen_stack/validate/outcome_prediction.py +76 -0
- pen_stack/validate/paper3_benchmark.py +165 -0
- pen_stack/validate/paper4_real_validation.py +144 -0
- pen_stack/validate/paper4_validation.py +82 -0
- pen_stack/validate/protocol_safety.py +62 -0
- pen_stack/validate/safety_screening.py +72 -0
- pen_stack/validate/selective_prediction.py +104 -0
- pen_stack/validate/seq_vs_measured.py +134 -0
- pen_stack/validate/target_site_controls.py +65 -0
- pen_stack/validate/uncertainty_eval.py +244 -0
- pen_stack/validate/ungrounded_baseline.py +234 -0
- pen_stack/validate/within_locus_ranking.py +84 -0
- pen_stack/validate/writer_recovery.py +91 -0
- pen_stack/verify/__init__.py +5 -0
- pen_stack/verify/proof.py +206 -0
- pen_stack/verify/schema.py +53 -0
- pen_stack/verify/service.py +191 -0
- pen_stack/web/__init__.py +18 -0
- pen_stack/web/guide.py +110 -0
- pen_stack/web/llm.py +393 -0
- pen_stack/web/llm_provider.py +119 -0
- pen_stack/web/router.py +119 -0
- pen_stack/web/server.py +96 -0
- pen_stack/web/tools.py +197 -0
- pen_stack/wgenome/__init__.py +1 -0
- pen_stack/wgenome/chromatin_seq.py +83 -0
- pen_stack/wgenome/durability.py +108 -0
- pen_stack/wgenome/export_tracks.py +52 -0
- pen_stack/wgenome/features.py +82 -0
- pen_stack/wgenome/genotoxic_blocklist.py +88 -0
- pen_stack/wgenome/gsh_baseline.py +154 -0
- pen_stack/wgenome/mesh_features.py +61 -0
- pen_stack/wgenome/offtarget_assay.py +80 -0
- pen_stack/wgenome/offtarget_bridge.py +47 -0
- pen_stack/wgenome/offtarget_cast.py +97 -0
- pen_stack/wgenome/offtarget_data.py +148 -0
- pen_stack/wgenome/offtarget_enumerate.py +274 -0
- pen_stack/wgenome/offtarget_integrase.py +155 -0
- pen_stack/wgenome/offtarget_nuclease.py +123 -0
- pen_stack/wgenome/offtarget_paste.py +41 -0
- pen_stack/wgenome/offtarget_predict.py +282 -0
- pen_stack/wgenome/ood.py +135 -0
- pen_stack/wgenome/providers.py +278 -0
- pen_stack/wgenome/safety.py +69 -0
- pen_stack/wgenome/structure3d.py +212 -0
- pen_stack/wgenome/uncertainty.py +250 -0
- pen_stack/wgenome/writability.py +72 -0
- pen_stack-0.1.0.dist-info/METADATA +401 -0
- pen_stack-0.1.0.dist-info/RECORD +259 -0
- pen_stack-0.1.0.dist-info/WHEEL +5 -0
- pen_stack-0.1.0.dist-info/entry_points.txt +3 -0
- pen_stack-0.1.0.dist-info/licenses/LICENSE +21 -0
- pen_stack-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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
|