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,154 @@
|
|
|
1
|
+
"""CD4 / MHC-II epitope-load axis for the writer enzyme AND the capsid.
|
|
2
|
+
|
|
3
|
+
The dominant immunogenicity driver for a protein therapeutic is **MHC-II / CD4 help -> anti-drug antibodies
|
|
4
|
+
(ADA)**, exactly what the earlier immune profile omitted (it did CD8/MHC-I via MHCflurry only). And the **editor
|
|
5
|
+
protein itself is immunogenic** (Cas9 elicits MHC-II-presented CD4 responses, Simhadri et al., Nat Commun 2021,
|
|
6
|
+
10.1038/s41467-021-25414-9; bridge recombinases / serine integrases are bacterial/phage), yet the immune-risk
|
|
7
|
+
profile scored only the capsid. This module adds an MHC-II epitope-load axis over the **writer** as a distinct antigen.
|
|
8
|
+
|
|
9
|
+
PRODUCTION method: the **real, licensed NetMHCIIpan-4.0** eluted-ligand %Rank over a frequent HLA-II
|
|
10
|
+
panel (residue-coverage metric), run on the VM with only the derived fractions cached (`configs/mhc_epitope_oracle.yaml`);
|
|
11
|
+
`mhc2_epitope_load(seq, name)` returns the real value for a cached antigen and otherwise **abstains** (no production
|
|
12
|
+
proxy). A population-level proxy, never a patient-HLA-specific magnitude (a known-unknown). Whether the epitopes
|
|
13
|
+
drive ADA depends on self-tolerance (foreign vs self), handled in `ada_risk`.
|
|
14
|
+
|
|
15
|
+
OFFLINE-ONLY fallback (`mhc2_proxy_estimate`, NOT the production axis): a documented, dependency-free PROMISCUOUS-
|
|
16
|
+
binder density, MHC-II presents a 9-mer core in an open groove whose **P1 pocket** is the dominant hydrophobic
|
|
17
|
+
anchor (M/F/Y/W/L/I/V; Stern & Wiley, Nature 1994) with secondary pockets at P4/P6/P9 (Southwood 1998). Provided for
|
|
18
|
+
offline triage where NetMHCIIpan cannot be run; the production axis uses the real tool and abstains otherwise.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from functools import lru_cache
|
|
23
|
+
|
|
24
|
+
from pen_stack._resources import resource
|
|
25
|
+
|
|
26
|
+
_AA = set("ACDEFGHIKLMNPQRSTVWY")
|
|
27
|
+
P1_ANCHOR = set("MFYWLIV") # large hydrophobic, the dominant MHC-II P1 anchor (Stern & Wiley 1994)
|
|
28
|
+
_FAVORABLE = set("AVLIMFYWSTNQGC") # favorable at secondary pockets (hydrophobic / small / polar-uncharged)
|
|
29
|
+
_DISFAVORED = set("DEKRP") # charged / proline disfavored at anchor positions
|
|
30
|
+
MHC2_DOIS = ["10.1038/35030019", "10.1038/s41467-021-25414-9"] # Stern&Wiley groove; Cas9 MHC-II (Simhadri 2021)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _clean(seq: str) -> str:
|
|
34
|
+
return "".join(c for c in str(seq).upper() if c in _AA)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _core_is_binder(core: str) -> tuple[bool, float]:
|
|
38
|
+
"""A 9-mer core is a promiscuous-binder candidate iff P1 (pos0) is a strong hydrophobic anchor AND >=2 of the
|
|
39
|
+
secondary pockets P4/P6/P9 are favorable with no disfavored residue at an anchor position."""
|
|
40
|
+
if core[0] not in P1_ANCHOR:
|
|
41
|
+
return False, 0.0
|
|
42
|
+
sec = sum(1 for p in (3, 5, 8) if core[p] in _FAVORABLE)
|
|
43
|
+
pen = sum(1 for p in (0, 3, 5, 8) if core[p] in _DISFAVORED)
|
|
44
|
+
score = 1.0 + 0.5 * sec - 0.5 * pen
|
|
45
|
+
return (sec >= 2 and pen == 0), round(score, 3)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def mhc2_binder_cores(seq: str) -> list[tuple[int, str, float]]:
|
|
49
|
+
"""All promiscuous-binder 9-mer cores in the sequence: (start_index, core, score)."""
|
|
50
|
+
s = _clean(seq)
|
|
51
|
+
out = []
|
|
52
|
+
for i in range(len(s) - 8):
|
|
53
|
+
ok, sc = _core_is_binder(s[i:i + 9])
|
|
54
|
+
if ok:
|
|
55
|
+
out.append((i, s[i:i + 9], sc))
|
|
56
|
+
return out
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@lru_cache(maxsize=1)
|
|
60
|
+
def _real_cache() -> dict:
|
|
61
|
+
"""The REAL NetMHCIIpan-4.0 EL %Rank epitope-load cache (configs/mhc_epitope_oracle.yaml), computed over a
|
|
62
|
+
frequent HLA-II panel on the VM. Only the derived fractions are shipped (the licensed binary is never
|
|
63
|
+
distributed). Empty dict if the cache is absent (then the documented proxy is used)."""
|
|
64
|
+
try:
|
|
65
|
+
import yaml
|
|
66
|
+
return yaml.safe_load(resource("configs/mhc_epitope_oracle.yaml").read_text(encoding="utf-8")) or {}
|
|
67
|
+
except Exception: # noqa: BLE001
|
|
68
|
+
return {}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def real_mhc2_load(name: str) -> dict | None:
|
|
72
|
+
"""The real NetMHCIIpan-4.0 epitope load for a bundled antigen by name, or None when not cached."""
|
|
73
|
+
rec = (_real_cache().get("mhc2") or {}).get(name)
|
|
74
|
+
if not rec:
|
|
75
|
+
return None
|
|
76
|
+
panel = (_real_cache().get("method") or {}).get("hla2_panel", [])
|
|
77
|
+
return {"epitope_density": rec["epitope_fraction_strong"], "mhc2_immune_score": rec["immune_score"],
|
|
78
|
+
"n_covered": rec.get("n_covered"), "length": rec.get("length"),
|
|
79
|
+
"method": f"NetMHCIIpan-4.0 EL %Rank<=2, {rec.get('metric', 'residue coverage')}, frequent HLA-II "
|
|
80
|
+
f"panel ({len(panel)} alleles): {', '.join(panel)}",
|
|
81
|
+
"status": "population-level (frequent-HLA panel; NetMHCIIpan-4.0 eluted-ligand), coverage-gated "
|
|
82
|
+
"(abstains for uncached antigens, NOT a distributional OOD gate); NOT a patient-HLA-specific "
|
|
83
|
+
"magnitude (known-unknown)", "backend": "netmhciipan_cache"}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def mhc2_epitope_load(seq: str, name: str | None = None) -> dict:
|
|
87
|
+
"""MHC-II epitope load, the REAL NetMHCIIpan-4.0 cache when the antigen `name` is cached; otherwise ABSTAINS
|
|
88
|
+
(no proxy: an uncached sequence is a known-unknown, not a guessed number). To ground a new sequence,
|
|
89
|
+
add it to configs/writer_sequences.fasta and re-run scripts/p1_build_mhc.py on the VM. Score convention:
|
|
90
|
+
1 = least presentable (matches the MHC-I axis)."""
|
|
91
|
+
if name:
|
|
92
|
+
real = real_mhc2_load(name)
|
|
93
|
+
if real:
|
|
94
|
+
return real
|
|
95
|
+
return {"epitope_density": None, "mhc2_immune_score": None, "backend": "abstain",
|
|
96
|
+
"method": "NetMHCIIpan-4.0 (real)", "available": False,
|
|
97
|
+
"status": "NetMHCIIpan-4.0 not run for this sequence, NO proxy (abstains, known-unknown). Run "
|
|
98
|
+
"scripts/p1_build_mhc.py over its FASTA on the VM to ground it."}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def mhc2_proxy_estimate(seq: str) -> dict:
|
|
102
|
+
"""OFFLINE-ONLY estimate (NOT used by the profile): a documented promiscuous-binder density (P1 hydrophobic
|
|
103
|
+
anchor + P4/P6/P9 pockets; Stern & Wiley 1994; Southwood 1998). Provided for offline triage where NetMHCIIpan
|
|
104
|
+
cannot be run; the production axis uses the real tool and abstains otherwise."""
|
|
105
|
+
s = _clean(seq)
|
|
106
|
+
n_cores = max(0, len(s) - 8)
|
|
107
|
+
binders = mhc2_binder_cores(s)
|
|
108
|
+
density = (len(binders) / n_cores) if n_cores else 0.0
|
|
109
|
+
return {"epitope_density": round(density, 4), "mhc2_immune_score": round(1.0 - min(density, 1.0), 4),
|
|
110
|
+
"n_promiscuous_binders": len(binders), "backend": "proxy_offline_only",
|
|
111
|
+
"method": "DOCUMENTED PROXY, offline triage only, NOT the production axis (which uses NetMHCIIpan-4.0)"}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ---- bundled writer / control sequences (real UniProt, configs/writer_sequences.fasta) --------
|
|
115
|
+
@lru_cache(maxsize=1)
|
|
116
|
+
def writer_sequences() -> dict:
|
|
117
|
+
"""Parse the bundled writer/control FASTA -> {name: {seq, origin, family, accession}}."""
|
|
118
|
+
txt = resource("configs/writer_sequences.fasta").read_text(encoding="utf-8")
|
|
119
|
+
out: dict = {}
|
|
120
|
+
name = seq = origin = family = acc = None
|
|
121
|
+
|
|
122
|
+
def _flush():
|
|
123
|
+
if name:
|
|
124
|
+
out[name] = {"seq": seq, "origin": origin, "family": family, "accession": acc}
|
|
125
|
+
for line in txt.splitlines():
|
|
126
|
+
if line.startswith("#"):
|
|
127
|
+
continue
|
|
128
|
+
if line.startswith(">"):
|
|
129
|
+
_flush()
|
|
130
|
+
head = line[1:].strip()
|
|
131
|
+
tok = head.split()
|
|
132
|
+
name, acc = (tok[0].split("|") + [None])[:2]
|
|
133
|
+
kv = dict(p.split("=", 1) for p in tok[1:] if "=" in p)
|
|
134
|
+
origin, family, seq = kv.get("origin"), kv.get("family"), ""
|
|
135
|
+
else:
|
|
136
|
+
seq = (seq or "") + line.strip()
|
|
137
|
+
_flush()
|
|
138
|
+
return out
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def writer_family_to_sequence(writer_family: str) -> dict | None:
|
|
142
|
+
"""Map a design's writer family (e.g. 'bridge_IS110', 'serine_integrase', 'Cas9') to a bundled writer
|
|
143
|
+
sequence record, or None when no representative sequence is bundled (then the axis abstains)."""
|
|
144
|
+
fam = (writer_family or "").lower()
|
|
145
|
+
seqs = writer_sequences()
|
|
146
|
+
for name, rec in seqs.items():
|
|
147
|
+
f = (rec.get("family") or "").lower()
|
|
148
|
+
if rec.get("origin") == "self":
|
|
149
|
+
continue
|
|
150
|
+
if fam and (fam in f or f in fam or fam in name.lower()):
|
|
151
|
+
return {"name": name, **rec}
|
|
152
|
+
if "cas9" in fam or "nuclease" in fam:
|
|
153
|
+
return {"name": "SpCas9", **seqs.get("SpCas9", {})} if "SpCas9" in seqs else None
|
|
154
|
+
return None
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Unified per-design immune-risk PROFILE.
|
|
2
|
+
|
|
3
|
+
One object across every immune-risk axis (genotoxicity, CD8 epitope load, innate sensing, pre-existing
|
|
4
|
+
anti-vector NAb, anti-PEG), each carrying its OWN value + native uncertainty + scope + calibration validation
|
|
5
|
+
label. The axes are reported as a VECTOR, never fused into a single number, which would fake confidence and
|
|
6
|
+
is forbidden (asserted by test). Abstaining axes report ``None``, not a guess. The in-vivo response magnitude
|
|
7
|
+
and the patient-specific titer are listed as declared known-unknowns.
|
|
8
|
+
|
|
9
|
+
This is the "immune-risk screen" the closed-loop arc consumes, a relative screen across axes,
|
|
10
|
+
not a patient-level prediction.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pen_stack.planner.antipeg_oracle import antipeg_oracle
|
|
15
|
+
from pen_stack.planner.ada_risk import ada_risk
|
|
16
|
+
from pen_stack.planner.capsid_epitope_oracle import capsid_epitope_oracle
|
|
17
|
+
from pen_stack.planner.genotoxicity_oracle import genotoxicity_oracle
|
|
18
|
+
from pen_stack.planner.immune_mhc2 import mhc2_epitope_load, writer_family_to_sequence, writer_sequences
|
|
19
|
+
from pen_stack.planner.innate_sensing import innate_sensing
|
|
20
|
+
from pen_stack.planner.seroprevalence_oracle import seroprevalence_oracle
|
|
21
|
+
from pen_stack.validate.immune_calibration import axis_label
|
|
22
|
+
|
|
23
|
+
# magnitude and patient-level state are NEVER predicted, listed so every consumer sees the boundary.
|
|
24
|
+
# Also registers cd4_mhcii_help / preexisting_capsid_tcell / complement_carpa as known-unknowns
|
|
25
|
+
# (configs/known_unknowns.yaml), mechanistically distinct axes PEN-STACK does not model.
|
|
26
|
+
KNOWN_UNKNOWNS = ["patient_specific_titer", "in_vivo_response_magnitude", "induced_immunity_post_dose1",
|
|
27
|
+
"cd4_mhcii_help", "preexisting_capsid_tcell", "complement_carpa"]
|
|
28
|
+
|
|
29
|
+
# Route/dose modifier. Immune-privileged delivery sites (eye, CNS) materially LOWER the realized
|
|
30
|
+
# immunogenicity of the SAME vector vs systemic delivery (Streilein 2003, 10.1038/nri1224). This is a
|
|
31
|
+
# DOCUMENTED, QUALITATIVE modifier on the realized response, never a fabricated magnitude.
|
|
32
|
+
_IMMUNE_PRIVILEGED = {"subretinal", "intravitreal", "intraocular", "cns", "intrathecal", "intracranial",
|
|
33
|
+
"intraparenchymal", "eye", "retina", "brain"}
|
|
34
|
+
ROUTE_MODIFIER_DOI = "10.1038/nri1224" # Streilein 2003, ocular/CNS immune privilege
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _route_modifier(route: str | None) -> dict | None:
|
|
38
|
+
if not route:
|
|
39
|
+
return None
|
|
40
|
+
r = str(route).strip().lower()
|
|
41
|
+
privileged = any(k in r for k in _IMMUNE_PRIVILEGED)
|
|
42
|
+
return {"route": route,
|
|
43
|
+
"immune_privileged": privileged,
|
|
44
|
+
"effect": ("immune-privileged site: realized immunogenicity is materially LOWER than systemic "
|
|
45
|
+
"delivery of the same vector (qualitative)" if privileged
|
|
46
|
+
else "systemic / non-privileged route: no immune-privilege reduction applied"),
|
|
47
|
+
"doi": "10.1038/nri1224",
|
|
48
|
+
"note": "DOCUMENTED qualitative modifier on the realized response, NOT a quantified magnitude "
|
|
49
|
+
"(the magnitude stays a known-unknown)."}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Administration-context (in-vivo vs ex-vivo) modifier on the VECTOR-FACING immune axes.
|
|
53
|
+
# Ex-vivo delivery (cells transduced in a dish and washed before transplant) does NOT expose the vector to the
|
|
54
|
+
# patient's circulating antibodies, so the pre-existing anti-vector NAb axis - a humoral, bloodstream eligibility
|
|
55
|
+
# barrier - does not gate ex-vivo use (it is muted to "no barrier"). The systemic anti-capsid CD8 response is also
|
|
56
|
+
# muted ex vivo (the host barely sees the capsid), but transduced cells can still present capsid epitopes (a real,
|
|
57
|
+
# residual concern), so that axis is FLAGGED muted without overwriting its intrinsic value. This is the SAME
|
|
58
|
+
# in-vivo/ex-vivo distinction the delivery_immunology profile already encodes ("computed_ex_vivo_muted").
|
|
59
|
+
# A DOCUMENTED, mechanistic modifier - never a fabricated magnitude; the realized response stays a known-unknown.
|
|
60
|
+
_EX_VIVO_MUTED_AXES = ("preexisting_nab", "cd8_epitope")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _is_ex_vivo(in_vivo) -> bool:
|
|
64
|
+
return str(in_vivo).strip().lower() in ("false", "0", "no", "ex_vivo", "ex vivo", "exvivo")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _administration_modifier(in_vivo) -> dict | None:
|
|
68
|
+
"""Documented administration-context modifier (in-vivo / ex-vivo) on the vector-facing axes, or None when the
|
|
69
|
+
design does not state an administration context. Qualitative, never a fabricated magnitude."""
|
|
70
|
+
if in_vivo is None:
|
|
71
|
+
return None
|
|
72
|
+
if _is_ex_vivo(in_vivo):
|
|
73
|
+
return {"context": "ex_vivo", "muted_axes": list(_EX_VIVO_MUTED_AXES),
|
|
74
|
+
"effect": "ex-vivo administration: the vector contacts cells in a dish and is washed before "
|
|
75
|
+
"transplant, so it is not exposed to the patient's circulating antibodies. Pre-existing "
|
|
76
|
+
"anti-vector NAb does not gate eligibility (humoral barrier bypassed); the systemic "
|
|
77
|
+
"anti-capsid CD8 response is muted, though transduced cells may still present capsid "
|
|
78
|
+
"epitopes (a residual, separate consideration).",
|
|
79
|
+
"note": "DOCUMENTED qualitative administration modifier (the same in-vivo/ex-vivo distinction as the "
|
|
80
|
+
"delivery profile); the realized immune MAGNITUDE remains a known-unknown."}
|
|
81
|
+
return {"context": "in_vivo", "muted_axes": [],
|
|
82
|
+
"effect": "in-vivo (systemic / local) administration: the vector is exposed to the patient's "
|
|
83
|
+
"circulating antibodies and immune system, so the pre-existing NAb and anti-capsid CD8 "
|
|
84
|
+
"axes apply as reported.",
|
|
85
|
+
"note": "DOCUMENTED qualitative administration modifier; the realized magnitude is a known-unknown."}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _apply_administration(axes: dict, admin: dict | None) -> None:
|
|
89
|
+
"""Mutate the vector-facing axes in place for an ex-vivo administration context. Pre-existing NAb is muted to
|
|
90
|
+
'no barrier' (eligibility not gated when the vector never meets circulating antibody); CD8 capsid is flagged
|
|
91
|
+
muted but its intrinsic value is kept (transduced cells can still present). No-op for in-vivo / unspecified."""
|
|
92
|
+
if not admin or admin.get("context") != "ex_vivo":
|
|
93
|
+
return
|
|
94
|
+
na = axes.get("preexisting_nab")
|
|
95
|
+
if na and na.get("available") and na.get("value") is not None:
|
|
96
|
+
na["pre_admin_value"] = na["value"]
|
|
97
|
+
na["value"] = 1.0
|
|
98
|
+
na["uncertainty"] = 0.0
|
|
99
|
+
na["administration_muted"] = True
|
|
100
|
+
na["note"] = (f"ex-vivo administration: pre-existing circulating anti-vector antibodies do not reach the "
|
|
101
|
+
f"vector (transduction in a dish), so pre-existing NAb does not gate eligibility "
|
|
102
|
+
f"(muted from {na['pre_admin_value']} to 1.0 = no barrier). " + (na.get("note") or ""))
|
|
103
|
+
cd = axes.get("cd8_epitope")
|
|
104
|
+
if cd and cd.get("available") and cd.get("value") is not None:
|
|
105
|
+
cd["administration_muted"] = True
|
|
106
|
+
cd["note"] = ("ex-vivo administration: the systemic anti-capsid CD8 response is muted (minimal host capsid "
|
|
107
|
+
"exposure); transduced cells may still present capsid epitopes, so the intrinsic value is "
|
|
108
|
+
"kept. " + (cd.get("note") or ""))
|
|
109
|
+
|
|
110
|
+
# headline score key inside each oracle's value dict, per axis.
|
|
111
|
+
_SCORE_KEY = {"genotoxicity": "genotox_score", "cd8_epitope": "capsid_immune_score",
|
|
112
|
+
"innate": "innate_score", "preexisting_nab": "preexisting_score",
|
|
113
|
+
"anti_peg": "preexisting_antipeg_score"}
|
|
114
|
+
|
|
115
|
+
# Canonical cargo nucleic-acid FORM per delivery vehicle, used to drive the innate-sensing pathway (DNA -> TLR9
|
|
116
|
+
# CpG; mRNA -> TLR7/8 + RIG-I/MDA5; RNP -> transient) when a design does not state ``cargo_form`` explicitly.
|
|
117
|
+
# This is the vehicle's defining cargo class, NOT a fabricated quantity: an LNP-mRNA vehicle delivers mRNA, a
|
|
118
|
+
# DNA-virus / electroporated-plasmid vehicle delivers DNA, an (e)VLP delivers a transient ribonucleoprotein. A
|
|
119
|
+
# lentivirus is packaged as RNA but the persistent, innate-relevant cassette is the integrated proviral DNA.
|
|
120
|
+
# An unknown vehicle returns None so the innate axis ABSTAINS rather than guesses a form.
|
|
121
|
+
_VEHICLE_CARGO_FORM = {
|
|
122
|
+
"lnp_mrna": "mRNA",
|
|
123
|
+
"aav_single": "DNA", "aav_dual": "DNA", "lentivirus": "DNA",
|
|
124
|
+
"helper_dependent_adenovirus": "DNA", "hsv_amplicon": "DNA", "electroporation": "DNA",
|
|
125
|
+
"evlp": "RNP", "vlp": "RNP",
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _vehicle_cargo_form(vehicle: str | None) -> str | None:
|
|
130
|
+
"""Derive the cargo nucleic-acid form (DNA / mRNA / RNP) from the delivery vehicle, or None when unknown.
|
|
131
|
+
Used only when the design does not supply ``cargo_form`` / ``writer_output_form`` explicitly."""
|
|
132
|
+
if not vehicle:
|
|
133
|
+
return None
|
|
134
|
+
v = str(vehicle).strip().lower()
|
|
135
|
+
if v in _VEHICLE_CARGO_FORM:
|
|
136
|
+
return _VEHICLE_CARGO_FORM[v]
|
|
137
|
+
if "mrna" in v or "lnp" in v:
|
|
138
|
+
return "mRNA"
|
|
139
|
+
if "vlp" in v:
|
|
140
|
+
return "RNP"
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _axis(result, axis: str) -> dict:
|
|
145
|
+
"""One axis record: value (headline score or None when abstaining) + native uncertainty + scope + the
|
|
146
|
+
calibration validation label. Never fabricates, an abstaining oracle yields value None."""
|
|
147
|
+
val = None
|
|
148
|
+
if result.available and result.value:
|
|
149
|
+
val = result.value.get(_SCORE_KEY[axis])
|
|
150
|
+
return {"value": val,
|
|
151
|
+
"uncertainty": result.native_uncertainty,
|
|
152
|
+
"in_scope": result.in_scope,
|
|
153
|
+
"available": result.available,
|
|
154
|
+
"validation": axis_label(axis), # 'mechanistic/population proxy' until calibration validates
|
|
155
|
+
"scope_card": result.scope_card,
|
|
156
|
+
"note": result.note}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _proxy_axis(value, note: str, axis: str = "mhc2_writer") -> dict:
|
|
160
|
+
"""An axis record for the sequence-intrinsic proxies (MHC-II / ADA). Value None = abstained (no writer
|
|
161
|
+
sequence). Population-level proxy until calibration validates, never a patient magnitude."""
|
|
162
|
+
return {"value": value, "uncertainty": None, "in_scope": value is not None, "available": value is not None,
|
|
163
|
+
"validation": axis_label(axis), "scope_card": axis, "note": note}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _writer_antigen_card(design: dict) -> dict | None:
|
|
167
|
+
"""The WRITER enzyme as a distinct antigen: MHC-II epitope load + ADA-risk over the writer's real
|
|
168
|
+
sequence. Returns None when no representative writer sequence is bundled (axis then abstains)."""
|
|
169
|
+
wf = design.get("writer_family") or design.get("writer")
|
|
170
|
+
rec = writer_family_to_sequence(wf) if wf else None
|
|
171
|
+
if not rec or not rec.get("seq"):
|
|
172
|
+
return None
|
|
173
|
+
nm = rec.get("name")
|
|
174
|
+
el = mhc2_epitope_load(rec["seq"], nm) # real NetMHCIIpan-4.0 when cached, else proxy
|
|
175
|
+
ad = ada_risk(rec["seq"], rec.get("origin"), name=nm)
|
|
176
|
+
return {"writer_family": wf, "representative": rec.get("name"), "accession": rec.get("accession"),
|
|
177
|
+
"origin": rec.get("origin"), "is_foreign": rec.get("origin") == "foreign",
|
|
178
|
+
"mhc2_immune_score": el["mhc2_immune_score"], "epitope_density": el["epitope_density"],
|
|
179
|
+
"ada_risk_score": ad["ada_risk_score"], "ada_immune_score": ad["ada_immune_score"],
|
|
180
|
+
"foreignness": ad.get("foreignness"),
|
|
181
|
+
"self_match_human_proteome": ad.get("self_match_human_proteome"),
|
|
182
|
+
"ada_backend": ad.get("backend"), "mhc2_backend": el.get("backend"),
|
|
183
|
+
"note": "the WRITER enzyme scored as a distinct antigen (real NetMHCIIpan-4.0 MHC-II/CD4 + ADA, "
|
|
184
|
+
"origin-authoritative foreignness with a real human-proteome 9-mer self-match cross-check); "
|
|
185
|
+
"bacterial/phage writers are foreign -> ADA-driving (Cas9 MHC-II: Simhadri 2021). "
|
|
186
|
+
"Population-level proxy; realized CD4 response / ADA titer is a known-unknown."}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# Genome-WRITER families surfaced in the Writer Atlas immunogenicity view: the bundled writers that are actual
|
|
190
|
+
# genome writers (integrase / recombinase), each with a committed real NetMHCIIpan-4.0 MHC-II load + ADA-risk cache.
|
|
191
|
+
# The Cas9 nuclease (an editor, not a large-cargo writer) is excluded; the human self control IS surfaced as a
|
|
192
|
+
# labelled reference row (see below).
|
|
193
|
+
_WRITER_IMMUNE_FAMILIES = ("serine_integrase", "bridge_IS110")
|
|
194
|
+
_SELF_CONTROL = "HumanAlbumin" # a human self protein: MHC-II epitopes present, yet tolerated (foreignness 0)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def writer_immunogenicity_table() -> list[dict]:
|
|
198
|
+
"""Per-writer immunogenicity (MHC-II/CD4 epitope load + ADA risk) for the bundled genome-writer families, read
|
|
199
|
+
from the committed NetMHCIIpan-4.0 cache (no recomputation). Surfaced in the Writer Atlas as the writer's
|
|
200
|
+
antigen profile, PLUS the human self control as a labelled reference row: a self protein carries MHC-II
|
|
201
|
+
epitopes (density > 0) yet is TOLERATED (origin=self -> foreignness 0 -> ADA-risk 0), so ADA decouples from the
|
|
202
|
+
MHC-II load, the row that shows ADA-risk = MHC-II density x foreignness(origin) is genuinely multiplicative and
|
|
203
|
+
not a pass-through of the load. Excludes the Cas9 nuclease."""
|
|
204
|
+
out = []
|
|
205
|
+
for fam in _WRITER_IMMUNE_FAMILIES:
|
|
206
|
+
card = _writer_antigen_card({"writer_family": fam})
|
|
207
|
+
if card:
|
|
208
|
+
out.append({"writer_family": fam, **card})
|
|
209
|
+
ctrl = writer_sequences().get(_SELF_CONTROL)
|
|
210
|
+
if ctrl and ctrl.get("seq"):
|
|
211
|
+
el = mhc2_epitope_load(ctrl["seq"], _SELF_CONTROL)
|
|
212
|
+
ad = ada_risk(ctrl["seq"], ctrl.get("origin"), name=_SELF_CONTROL)
|
|
213
|
+
out.append({"writer_family": ctrl.get("family"), "representative": _SELF_CONTROL,
|
|
214
|
+
"accession": ctrl.get("accession"), "origin": ctrl.get("origin"),
|
|
215
|
+
"is_foreign": ctrl.get("origin") == "foreign", "is_control": True,
|
|
216
|
+
"mhc2_immune_score": el["mhc2_immune_score"], "epitope_density": el["epitope_density"],
|
|
217
|
+
"ada_risk_score": ad["ada_risk_score"], "ada_immune_score": ad["ada_immune_score"],
|
|
218
|
+
"foreignness": ad.get("foreignness"),
|
|
219
|
+
"self_match_human_proteome": ad.get("self_match_human_proteome"),
|
|
220
|
+
"ada_backend": ad.get("backend"), "mhc2_backend": el.get("backend"),
|
|
221
|
+
"note": ("human self control (reference): a self protein carries MHC-II epitopes (density > 0) "
|
|
222
|
+
"yet is TOLERATED, origin=self makes foreignness 0, so ADA-risk = density x 0 = 0 "
|
|
223
|
+
"(central tolerance). This is the row that shows the foreignness term does real "
|
|
224
|
+
"multiplicative work, not a pass-through of the MHC-II load.")})
|
|
225
|
+
return out
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def immune_profile(design: dict) -> dict:
|
|
229
|
+
"""Per-design immune-risk profile across all axes. ``design`` keys: ``delivery_vehicle`` (or ``vehicle``),
|
|
230
|
+
``serotype``, ``cargo_seq``, ``writer_output_form`` (or ``cargo_form``), ``pegylated``.
|
|
231
|
+
|
|
232
|
+
Returns the axes vector with per-axis uncertainty + validation label, an explicit ``collapsed_score: None``
|
|
233
|
+
(no fused number), the known-unknowns, and ``no_fabrication: True``."""
|
|
234
|
+
veh = design.get("delivery_vehicle") or design.get("vehicle")
|
|
235
|
+
sero = design.get("serotype")
|
|
236
|
+
cargo_seq = design.get("cargo_seq") or ""
|
|
237
|
+
# cargo form: honour an explicit design field, else derive it from the vehicle's defining cargo class so the
|
|
238
|
+
# innate axis computes for any caller (chat / MCP / REST) once a cargo sequence is supplied. Still abstains
|
|
239
|
+
# with no sequence and for an unknown vehicle (form None) - never fabricates.
|
|
240
|
+
form = (design.get("writer_output_form") or design.get("cargo_form")
|
|
241
|
+
or _vehicle_cargo_form(veh) or "")
|
|
242
|
+
peg = design.get("pegylated")
|
|
243
|
+
|
|
244
|
+
capsid_cd8 = capsid_epitope_oracle(veh)
|
|
245
|
+
writer_card = _writer_antigen_card(design) # the writer enzyme as a distinct antigen
|
|
246
|
+
|
|
247
|
+
axes = {
|
|
248
|
+
"genotoxicity": _axis(genotoxicity_oracle(veh), "genotoxicity"),
|
|
249
|
+
"cd8_epitope": _axis(capsid_cd8, "cd8_epitope"), # capsid CD8/MHC-I
|
|
250
|
+
"innate": _axis(innate_sensing(cargo_seq, form), "innate"),
|
|
251
|
+
"preexisting_nab": _axis(seroprevalence_oracle(veh, sero), "preexisting_nab"),
|
|
252
|
+
"anti_peg": _axis(antipeg_oracle(veh, peg), "anti_peg"),
|
|
253
|
+
# CD4/MHC-II + ADA over the WRITER enzyme (the dominant immunogenicity driver, previously omitted)
|
|
254
|
+
"mhc2_writer": _proxy_axis(writer_card["mhc2_immune_score"] if writer_card else None,
|
|
255
|
+
(writer_card or {}).get("note", "no bundled writer sequence -> abstains"),
|
|
256
|
+
"mhc2_writer"),
|
|
257
|
+
"ada_writer": _proxy_axis(writer_card["ada_immune_score"] if writer_card else None,
|
|
258
|
+
"ADA-risk (higher ada_immune_score = safer) over the writer enzyme, self-"
|
|
259
|
+
"tolerance filtered; population proxy" if writer_card else
|
|
260
|
+
"no bundled writer sequence -> abstains", "ada_writer"),
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
# Administration context: ex-vivo delivery bypasses the patient's circulating antibodies, so the
|
|
264
|
+
# vector-facing axes (pre-existing NAb, capsid CD8) are muted - documented, not a fabricated magnitude.
|
|
265
|
+
admin = _administration_modifier(design.get("in_vivo"))
|
|
266
|
+
_apply_administration(axes, admin)
|
|
267
|
+
|
|
268
|
+
# writer-as-antigen comparison: for non-viral delivery (no foreign capsid) or a foreign writer outscoring the
|
|
269
|
+
# capsid, the WRITER is the dominant antigen, never collapsed into the other axes.
|
|
270
|
+
dominant = None
|
|
271
|
+
writer_dominant_risk = False
|
|
272
|
+
if writer_card:
|
|
273
|
+
capsid_present = bool(capsid_cd8.available and capsid_cd8.value
|
|
274
|
+
and (capsid_cd8.value.get("capsid_immune_score") or 1.0) < 1.0)
|
|
275
|
+
writer_risk = writer_card["ada_risk_score"]
|
|
276
|
+
capsid_risk = (1.0 - (capsid_cd8.value or {}).get("capsid_immune_score", 1.0)) if capsid_present else 0.0
|
|
277
|
+
writer_dominant_risk = bool(writer_card["is_foreign"] and (not capsid_present or writer_risk >= capsid_risk))
|
|
278
|
+
dominant = "writer" if writer_dominant_risk else ("capsid" if capsid_present else "writer")
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
"axes": axes,
|
|
282
|
+
"collapsed_score": None, # deliberately None, a profile, never a fused number
|
|
283
|
+
"writer_as_antigen": ({**writer_card, "dominant_antigen": dominant,
|
|
284
|
+
"writer_dominant_risk": writer_dominant_risk} if writer_card else None),
|
|
285
|
+
"route_modifier": _route_modifier(design.get("route")), # documented route modifier (or None)
|
|
286
|
+
"administration_modifier": admin, # in-vivo / ex-vivo documented modifier on the vector-facing axes
|
|
287
|
+
"known_unknowns": KNOWN_UNKNOWNS,
|
|
288
|
+
"no_fabrication": True,
|
|
289
|
+
"note": ("relative immune-risk SCREEN across axes (now incl. CD4/MHC-II + ADA over the writer enzyme); each "
|
|
290
|
+
"axis keeps its own value + uncertainty + scope + validation label; NEVER fused. NOT a patient-"
|
|
291
|
+
"level prediction. The realized CD4 response / ADA titer / in-vivo magnitude are known-unknowns."),
|
|
292
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Computed innate-immune-sensing scorer for nucleic-acid cargo.
|
|
2
|
+
|
|
3
|
+
Innate sensing of a delivered nucleic acid is a property of the CARGO SEQUENCE (and its form), computed here
|
|
4
|
+
directly from sequence - the third computed delivery-immunology signal (after genotoxicity and capsid
|
|
5
|
+
epitope load). It covers every cargo form the palette carries:
|
|
6
|
+
|
|
7
|
+
* DNA (AAV / HDAd / HSV / electroporated plasmid) -> TLR9 / cGAS sensing of unmethylated CpG. The standard
|
|
8
|
+
sequence statistic is the CpG observed/expected ratio (Gardiner-Garden & Frommer): vertebrate genomes are
|
|
9
|
+
CpG-DEPLETED (O/E ~ 0.2) and tolerated, while non-depleted plasmid/viral DNA (O/E -> 1) is TLR9-stimulatory;
|
|
10
|
+
CpG-DEPLETED vectors evade detection. innate_score = max(0, 1 - CpG_O/E).
|
|
11
|
+
* mRNA (LNP-mRNA / electroporated mRNA) -> TLR7/8 (U-rich ssRNA) + RIG-I / MDA5 / PKR (dsRNA). Computed from
|
|
12
|
+
uridine fraction + ViennaRNA base-pairing. This signal is PARTIAL and flagged `extrapolating`: the dominant
|
|
13
|
+
innate-evasion lever for mRNA is NUCLEOSIDE MODIFICATION (m1-pseudouridine), which is NOT derivable from the
|
|
14
|
+
nucleotide sequence (a manufacturing choice) - a stated known-limitation.
|
|
15
|
+
* RNP / protein -> minimal, transient nucleic-acid exposure (no DNA; short-lived gRNA): score ~ high.
|
|
16
|
+
|
|
17
|
+
Answers through the OracleResult contract (output_kind="baseline"). SCOPE: this is a sequence-intrinsic
|
|
18
|
+
motif-LOAD signal; the realized in-vivo innate RESPONSE magnitude in a patient is NOT modelled and stays a
|
|
19
|
+
known-unknown; DNA methylation state and RNA nucleoside modification are out of sequence scope.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from pen_stack.oracles.schema import OracleResult, Provenance
|
|
24
|
+
|
|
25
|
+
_SCOPE_CARD = "innate_sensing"
|
|
26
|
+
_DNA = set("ACGT")
|
|
27
|
+
_RNA = set("ACGU")
|
|
28
|
+
# CpG-TLR9 (Krieg 1995 / Bauer 2001), CpG-depleted AAV evasion (Faust 2013), RNA nucleoside modification
|
|
29
|
+
# (Kariko 2005), 5'ppp dsRNA RIG-I (Hornung 2006).
|
|
30
|
+
PROVENANCE_DOIS = ["10.1038/374546a0", "10.1073/pnas.161293498", "10.1172/JCI68205",
|
|
31
|
+
"10.1016/j.immuni.2005.06.008", "10.1126/science.1132505"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _clean(seq: str) -> str:
|
|
35
|
+
return "".join(c for c in (seq or "").upper() if c.isalpha())
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def cpg_observed_expected(dna: str) -> dict:
|
|
39
|
+
"""CpG observed/expected ratio (Gardiner-Garden & Frommer): (n_CpG / (n_C * n_G)) * length. Vertebrate
|
|
40
|
+
genome ~0.2 (depleted); non-depleted plasmid/viral DNA approaches 1; engineered CpG-free -> 0."""
|
|
41
|
+
s = _clean(dna).replace("U", "T")
|
|
42
|
+
L = len(s)
|
|
43
|
+
nC, nG = s.count("C"), s.count("G")
|
|
44
|
+
n_cpg = s.count("CG")
|
|
45
|
+
oe = (n_cpg / (nC * nG) * L) if (nC and nG) else 0.0
|
|
46
|
+
gc = (nC + nG) / L if L else 0.0
|
|
47
|
+
return {"length": L, "cpg_count": n_cpg, "cpg_oe": round(oe, 4), "gc": round(gc, 4)}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _dsrna_paired_fraction(rna: str) -> float | None:
|
|
51
|
+
"""Fraction of bases paired in the ViennaRNA MFE structure (dsRNA -> RIG-I/MDA5/PKR). None if ViennaRNA
|
|
52
|
+
absent (graceful degradation, as in bridge/fold_qc.py)."""
|
|
53
|
+
try:
|
|
54
|
+
import RNA
|
|
55
|
+
except Exception: # noqa: BLE001
|
|
56
|
+
return None
|
|
57
|
+
s = _clean(rna).replace("T", "U")
|
|
58
|
+
if not s:
|
|
59
|
+
return None
|
|
60
|
+
struct, _ = RNA.fold_compound(s).mfe()
|
|
61
|
+
paired = sum(1 for c in struct if c in "()")
|
|
62
|
+
return round(paired / len(struct), 4) if struct else None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _prov() -> Provenance:
|
|
66
|
+
return Provenance(model="cargo_innate_sensing", version="1.0", source="adapter",
|
|
67
|
+
extra={"provenance_dois": PROVENANCE_DOIS})
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def innate_sensing(seq: str, cargo_form: str) -> OracleResult:
|
|
71
|
+
"""Computed innate-sensing score for a cargo sequence + form, as an OracleResult.
|
|
72
|
+
|
|
73
|
+
`cargo_form` in {DNA, mRNA, RNP}. Returns innate_score in [0,1] (1 = least innate-stimulatory). Abstains
|
|
74
|
+
(available=False) on an empty sequence or an unrecognised/uncomputable form. Never fabricates."""
|
|
75
|
+
s = _clean(seq)
|
|
76
|
+
form = (cargo_form or "").strip()
|
|
77
|
+
if not s:
|
|
78
|
+
return OracleResult(oracle="genome", value=None, provenance=_prov(), scope_card=_SCOPE_CARD,
|
|
79
|
+
in_scope=False, available=False, output_kind="baseline",
|
|
80
|
+
note="no cargo sequence supplied")
|
|
81
|
+
|
|
82
|
+
if form == "DNA":
|
|
83
|
+
c = cpg_observed_expected(s)
|
|
84
|
+
score = max(0.0, min(1.0, 1.0 - c["cpg_oe"]))
|
|
85
|
+
return OracleResult(
|
|
86
|
+
oracle="genome",
|
|
87
|
+
value={"innate_score": round(score, 3), "pathway": "TLR9/cGAS (unmethylated CpG)",
|
|
88
|
+
"cpg_oe": c["cpg_oe"], "cpg_count": c["cpg_count"], "gc": c["gc"], "length": c["length"]},
|
|
89
|
+
provenance=_prov(), native_uncertainty=None, scope_card=_SCOPE_CARD, in_scope=True,
|
|
90
|
+
extrapolating=False, output_kind="baseline", available=True,
|
|
91
|
+
note=(f"CpG O/E={c['cpg_oe']} ({c['cpg_count']} CpG, {c['length']} bp); innate_score=max(0,1-O/E). "
|
|
92
|
+
"Vertebrate genome O/E~0.2 (tolerated), non-depleted DNA ->1 (TLR9-stimulatory). DNA "
|
|
93
|
+
"methylation state and the realized in-vivo innate RESPONSE are known-unknowns (not modelled)."))
|
|
94
|
+
|
|
95
|
+
if form == "mRNA":
|
|
96
|
+
u_frac = s.replace("T", "U").count("U") / len(s)
|
|
97
|
+
paired = _dsrna_paired_fraction(s)
|
|
98
|
+
# partial sequence-only signal: U-richness (TLR7/8) + dsRNA pairing (RIG-I/PKR). The dominant evasion
|
|
99
|
+
# lever (nucleoside modification, m1-pseudouridine) is NOT sequence-derivable -> flagged extrapolating.
|
|
100
|
+
if paired is None:
|
|
101
|
+
score = max(0.0, min(1.0, 1.0 - u_frac))
|
|
102
|
+
note_ds = "ViennaRNA absent: dsRNA term omitted; score from U-fraction only."
|
|
103
|
+
else:
|
|
104
|
+
score = max(0.0, min(1.0, 1.0 - 0.5 * u_frac - 0.5 * paired))
|
|
105
|
+
note_ds = f"dsRNA paired_fraction={paired} (RIG-I/MDA5/PKR)."
|
|
106
|
+
return OracleResult(
|
|
107
|
+
oracle="rna",
|
|
108
|
+
value={"innate_score": round(score, 3), "pathway": "TLR7/8 (U-rich ssRNA) + RIG-I/MDA5/PKR (dsRNA)",
|
|
109
|
+
"u_fraction": round(u_frac, 4), "dsrna_paired_fraction": paired, "length": len(s)},
|
|
110
|
+
provenance=_prov(), native_uncertainty=None, scope_card=_SCOPE_CARD, in_scope=True,
|
|
111
|
+
extrapolating=True, output_kind="baseline", available=True,
|
|
112
|
+
note=("PARTIAL sequence-only signal. " + note_ds + " The dominant mRNA innate-evasion lever - "
|
|
113
|
+
"NUCLEOSIDE MODIFICATION (m1-pseudouridine) - is NOT sequence-derivable and is out of scope; "
|
|
114
|
+
"the realized in-vivo innate response is a known-unknown."))
|
|
115
|
+
|
|
116
|
+
if form == "RNP":
|
|
117
|
+
return OracleResult(
|
|
118
|
+
oracle="rna",
|
|
119
|
+
value={"innate_score": 0.9, "pathway": "minimal (transient gRNA; no DNA)", "length": len(s)},
|
|
120
|
+
provenance=_prov(), native_uncertainty=None, scope_card=_SCOPE_CARD, in_scope=True,
|
|
121
|
+
extrapolating=True, output_kind="baseline", available=True,
|
|
122
|
+
note=("RNP cargo: transient, no DNA -> minimal nucleic-acid innate sensing (synthetic gRNA may "
|
|
123
|
+
"trigger RIG-I via 5'-triphosphate; modification mitigates - not sequence-derivable). "
|
|
124
|
+
"Realized response is a known-unknown."))
|
|
125
|
+
|
|
126
|
+
return OracleResult(oracle="genome", value=None, provenance=_prov(), scope_card=_SCOPE_CARD,
|
|
127
|
+
in_scope=False, available=False, output_kind="baseline",
|
|
128
|
+
note=f"unrecognised cargo_form {cargo_form!r} (expected DNA / mRNA / RNP)")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def computed_innate_score(seq: str, cargo_form: str) -> tuple[float | None, OracleResult]:
|
|
132
|
+
"""Convenience: (innate_score or None, full OracleResult). None when the scorer abstains. Never fabricates."""
|
|
133
|
+
r = innate_sensing(seq, cargo_form)
|
|
134
|
+
val = (r.value or {}).get("innate_score") if (r.available and r.value) else None
|
|
135
|
+
return val, r
|