pen-stack 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. pen_stack/__init__.py +2 -0
  2. pen_stack/_resources.py +34 -0
  3. pen_stack/active/__init__.py +20 -0
  4. pen_stack/active/acquire.py +165 -0
  5. pen_stack/active/brains.py +74 -0
  6. pen_stack/active/campaign.py +109 -0
  7. pen_stack/active/design.py +66 -0
  8. pen_stack/active/validate.py +104 -0
  9. pen_stack/adapt/__init__.py +14 -0
  10. pen_stack/adapt/finetune.py +33 -0
  11. pen_stack/adapt/ingest.py +86 -0
  12. pen_stack/adapt/pipeline.py +101 -0
  13. pen_stack/adapt/recalibrate.py +58 -0
  14. pen_stack/adapt/report.py +130 -0
  15. pen_stack/agent/__init__.py +1 -0
  16. pen_stack/agent/cite.py +175 -0
  17. pen_stack/agent/co_scientist.py +262 -0
  18. pen_stack/agent/epistemic.py +102 -0
  19. pen_stack/agent/guardrails.py +67 -0
  20. pen_stack/agent/mcp_server.py +215 -0
  21. pen_stack/agent/orchestrator.py +112 -0
  22. pen_stack/agent/orchestrator_live.py +56 -0
  23. pen_stack/agent/pen_agent.py +242 -0
  24. pen_stack/agent/scope.py +60 -0
  25. pen_stack/agent/tools.py +130 -0
  26. pen_stack/api/__init__.py +12 -0
  27. pen_stack/api/manifest.py +160 -0
  28. pen_stack/atlas/__init__.py +1 -0
  29. pen_stack/atlas/atlas.parquet +0 -0
  30. pen_stack/atlas/build_wtkb.py +80 -0
  31. pen_stack/atlas/crosslink.py +179 -0
  32. pen_stack/atlas/expand.py +190 -0
  33. pen_stack/atlas/guide_design.py +178 -0
  34. pen_stack/atlas/schema.py +59 -0
  35. pen_stack/atlas/scorecard.py +134 -0
  36. pen_stack/atlas/scorecard_v3.parquet +0 -0
  37. pen_stack/atlas/universe.py +75 -0
  38. pen_stack/atlas/universe_v3.parquet +0 -0
  39. pen_stack/atlas/variant_propose.py +155 -0
  40. pen_stack/atlas/writer_efficiency.py +184 -0
  41. pen_stack/atlas/writer_predict.py +229 -0
  42. pen_stack/atlas/writer_recommend.py +170 -0
  43. pen_stack/atlas/writer_verify.py +167 -0
  44. pen_stack/atlas/wtkb.parquet +0 -0
  45. pen_stack/bridge/__init__.py +1 -0
  46. pen_stack/bridge/activity.py +52 -0
  47. pen_stack/bridge/cli.py +65 -0
  48. pen_stack/bridge/fold_qc.py +53 -0
  49. pen_stack/bridge/guide_qc.py +87 -0
  50. pen_stack/bridge/ingest.py +139 -0
  51. pen_stack/bridge/offtarget.py +191 -0
  52. pen_stack/bridge/offtarget_energetics.py +105 -0
  53. pen_stack/bridge/ortholog_screen.py +73 -0
  54. pen_stack/bridge/pipeline.py +83 -0
  55. pen_stack/build/__init__.py +16 -0
  56. pen_stack/build/cloudlab.py +74 -0
  57. pen_stack/build/ingest.py +47 -0
  58. pen_stack/build/protocol.py +82 -0
  59. pen_stack/build/simlab.py +30 -0
  60. pen_stack/cli.py +126 -0
  61. pen_stack/data/__init__.py +1 -0
  62. pen_stack/data/encode.py +84 -0
  63. pen_stack/data/genome.py +71 -0
  64. pen_stack/data/ingest_chromatin.py +119 -0
  65. pen_stack/data/ingest_integration.py +112 -0
  66. pen_stack/data/ingest_safety_annot.py +201 -0
  67. pen_stack/data/ingest_trip.py +76 -0
  68. pen_stack/design/__init__.py +14 -0
  69. pen_stack/design/capsid_generate.py +62 -0
  70. pen_stack/design/generate.py +70 -0
  71. pen_stack/design/pareto.py +70 -0
  72. pen_stack/design/space.py +137 -0
  73. pen_stack/design/writer_variants.py +121 -0
  74. pen_stack/env/__init__.py +1 -0
  75. pen_stack/env/genome_writing_env.py +248 -0
  76. pen_stack/env/policies.py +94 -0
  77. pen_stack/graph/__init__.py +21 -0
  78. pen_stack/graph/build.py +133 -0
  79. pen_stack/graph/cell_types.py +58 -0
  80. pen_stack/graph/ingest.py +132 -0
  81. pen_stack/graph/query.py +148 -0
  82. pen_stack/graph/schema.py +100 -0
  83. pen_stack/loop/__init__.py +15 -0
  84. pen_stack/loop/continual.py +61 -0
  85. pen_stack/loop/cycle.py +84 -0
  86. pen_stack/loop/drift.py +41 -0
  87. pen_stack/mech/__init__.py +1 -0
  88. pen_stack/mech/classify_atlas.py +71 -0
  89. pen_stack/mech/pfam_whitelist.yaml +247 -0
  90. pen_stack/mech/whitelist.py +66 -0
  91. pen_stack/monitor/__init__.py +1 -0
  92. pen_stack/monitor/europepmc.py +32 -0
  93. pen_stack/monitor/run.py +57 -0
  94. pen_stack/monitor/triage.py +63 -0
  95. pen_stack/oracles/__init__.py +65 -0
  96. pen_stack/oracles/affinity.py +116 -0
  97. pen_stack/oracles/cache.py +53 -0
  98. pen_stack/oracles/energetics.py +33 -0
  99. pen_stack/oracles/genome.py +167 -0
  100. pen_stack/oracles/protein_design.py +136 -0
  101. pen_stack/oracles/reliability.py +64 -0
  102. pen_stack/oracles/rna.py +28 -0
  103. pen_stack/oracles/schema.py +77 -0
  104. pen_stack/oracles/status.py +123 -0
  105. pen_stack/oracles/structure.py +42 -0
  106. pen_stack/oracles/structure_run.py +76 -0
  107. pen_stack/oracles/vcell.py +74 -0
  108. pen_stack/planner/__init__.py +1 -0
  109. pen_stack/planner/ada_risk.py +64 -0
  110. pen_stack/planner/antipeg_oracle.py +75 -0
  111. pen_stack/planner/capsid_epitope_oracle.py +135 -0
  112. pen_stack/planner/cargo.py +56 -0
  113. pen_stack/planner/cargo_polish.py +146 -0
  114. pen_stack/planner/chromosome.py +106 -0
  115. pen_stack/planner/delivery.py +55 -0
  116. pen_stack/planner/delivery_constraints.py +110 -0
  117. pen_stack/planner/delivery_immune.py +61 -0
  118. pen_stack/planner/delivery_immunology.py +222 -0
  119. pen_stack/planner/delivery_predict.py +196 -0
  120. pen_stack/planner/delivery_vehicles.py +37 -0
  121. pen_stack/planner/genotoxicity_oracle.py +112 -0
  122. pen_stack/planner/immune_mhc2.py +154 -0
  123. pen_stack/planner/immune_profile.py +292 -0
  124. pen_stack/planner/innate_sensing.py +135 -0
  125. pen_stack/planner/multiplex.py +110 -0
  126. pen_stack/planner/optimize.py +278 -0
  127. pen_stack/planner/pipeline.py +87 -0
  128. pen_stack/planner/report.py +26 -0
  129. pen_stack/planner/router.py +57 -0
  130. pen_stack/planner/seroprevalence_oracle.py +92 -0
  131. pen_stack/planner/target_site.py +118 -0
  132. pen_stack/rag/__init__.py +1 -0
  133. pen_stack/rag/corpus.py +133 -0
  134. pen_stack/rag/embed.py +98 -0
  135. pen_stack/rag/ground.py +131 -0
  136. pen_stack/rag/index.py +53 -0
  137. pen_stack/rag/llm.py +215 -0
  138. pen_stack/rag/qa.py +105 -0
  139. pen_stack/rag/retrieve.py +48 -0
  140. pen_stack/rules/__init__.py +9 -0
  141. pen_stack/rules/evaluators.py +318 -0
  142. pen_stack/rules/loader.py +31 -0
  143. pen_stack/rules/schema.py +99 -0
  144. pen_stack/rules/solver.py +43 -0
  145. pen_stack/rules/spec.py +78 -0
  146. pen_stack/safety/__init__.py +21 -0
  147. pen_stack/safety/audit.py +90 -0
  148. pen_stack/safety/gate.py +58 -0
  149. pen_stack/safety/pfam_scan.py +157 -0
  150. pen_stack/safety/policy.py +69 -0
  151. pen_stack/safety/redteam.py +71 -0
  152. pen_stack/safety/registry.py +255 -0
  153. pen_stack/safety/screen.py +59 -0
  154. pen_stack/safety/standards.py +141 -0
  155. pen_stack/score/__init__.py +1 -0
  156. pen_stack/score/recalibrate.py +77 -0
  157. pen_stack/score/therapeutic.py +85 -0
  158. pen_stack/server/__init__.py +1 -0
  159. pen_stack/server/api.py +647 -0
  160. pen_stack/spec/__init__.py +18 -0
  161. pen_stack/spec/clarify.py +42 -0
  162. pen_stack/spec/extract.py +406 -0
  163. pen_stack/spec/resolvers/__init__.py +17 -0
  164. pen_stack/spec/resolvers/cell.py +51 -0
  165. pen_stack/spec/resolvers/chem.py +32 -0
  166. pen_stack/spec/resolvers/feature.py +36 -0
  167. pen_stack/spec/resolvers/gene.py +43 -0
  168. pen_stack/spec/resolvers/locus.py +29 -0
  169. pen_stack/spec/resolvers/phenotype.py +37 -0
  170. pen_stack/spec/satisfy.py +114 -0
  171. pen_stack/spec/service.py +33 -0
  172. pen_stack/spec/writespec.py +252 -0
  173. pen_stack/twin/__init__.py +14 -0
  174. pen_stack/twin/calibrate.py +61 -0
  175. pen_stack/twin/data/__init__.py +12 -0
  176. pen_stack/twin/data/position_effect.py +245 -0
  177. pen_stack/twin/mechanistic.py +147 -0
  178. pen_stack/twin/outcome.py +132 -0
  179. pen_stack/twin/position_effect.py +454 -0
  180. pen_stack/ui/__init__.py +1 -0
  181. pen_stack/ui/app.py +713 -0
  182. pen_stack/validate/__init__.py +1 -0
  183. pen_stack/validate/adapt_demo.py +69 -0
  184. pen_stack/validate/agent_eval.py +117 -0
  185. pen_stack/validate/bench_adversarial_tasks.py +118 -0
  186. pen_stack/validate/bench_coscientist_tasks.py +60 -0
  187. pen_stack/validate/bench_graph_tasks.py +64 -0
  188. pen_stack/validate/bench_rule_tasks.py +84 -0
  189. pen_stack/validate/bench_trust_tasks.py +92 -0
  190. pen_stack/validate/bench_writetype_tasks.py +101 -0
  191. pen_stack/validate/blind_gsh_discovery.py +261 -0
  192. pen_stack/validate/cargo_directionality.py +57 -0
  193. pen_stack/validate/closed_loop.py +63 -0
  194. pen_stack/validate/durability_baselines.py +185 -0
  195. pen_stack/validate/experiment_design.py +65 -0
  196. pen_stack/validate/expr_controls.py +39 -0
  197. pen_stack/validate/forward_hypotheses.py +104 -0
  198. pen_stack/validate/generative_design.py +62 -0
  199. pen_stack/validate/guide_qc_demo.py +69 -0
  200. pen_stack/validate/heldout_celltype_expr.py +32 -0
  201. pen_stack/validate/immune_calibration.py +133 -0
  202. pen_stack/validate/intent_specification.py +82 -0
  203. pen_stack/validate/known_biology_expr.py +38 -0
  204. pen_stack/validate/offtarget_energetics_eval.py +144 -0
  205. pen_stack/validate/out_of_scope_refusal.py +82 -0
  206. pen_stack/validate/outcome_calibration.py +194 -0
  207. pen_stack/validate/outcome_prediction.py +76 -0
  208. pen_stack/validate/paper3_benchmark.py +165 -0
  209. pen_stack/validate/paper4_real_validation.py +144 -0
  210. pen_stack/validate/paper4_validation.py +82 -0
  211. pen_stack/validate/protocol_safety.py +62 -0
  212. pen_stack/validate/safety_screening.py +72 -0
  213. pen_stack/validate/selective_prediction.py +104 -0
  214. pen_stack/validate/seq_vs_measured.py +134 -0
  215. pen_stack/validate/target_site_controls.py +65 -0
  216. pen_stack/validate/uncertainty_eval.py +244 -0
  217. pen_stack/validate/ungrounded_baseline.py +234 -0
  218. pen_stack/validate/within_locus_ranking.py +84 -0
  219. pen_stack/validate/writer_recovery.py +91 -0
  220. pen_stack/verify/__init__.py +5 -0
  221. pen_stack/verify/proof.py +206 -0
  222. pen_stack/verify/schema.py +53 -0
  223. pen_stack/verify/service.py +191 -0
  224. pen_stack/web/__init__.py +18 -0
  225. pen_stack/web/guide.py +110 -0
  226. pen_stack/web/llm.py +393 -0
  227. pen_stack/web/llm_provider.py +119 -0
  228. pen_stack/web/router.py +119 -0
  229. pen_stack/web/server.py +96 -0
  230. pen_stack/web/tools.py +197 -0
  231. pen_stack/wgenome/__init__.py +1 -0
  232. pen_stack/wgenome/chromatin_seq.py +83 -0
  233. pen_stack/wgenome/durability.py +108 -0
  234. pen_stack/wgenome/export_tracks.py +52 -0
  235. pen_stack/wgenome/features.py +82 -0
  236. pen_stack/wgenome/genotoxic_blocklist.py +88 -0
  237. pen_stack/wgenome/gsh_baseline.py +154 -0
  238. pen_stack/wgenome/mesh_features.py +61 -0
  239. pen_stack/wgenome/offtarget_assay.py +80 -0
  240. pen_stack/wgenome/offtarget_bridge.py +47 -0
  241. pen_stack/wgenome/offtarget_cast.py +97 -0
  242. pen_stack/wgenome/offtarget_data.py +148 -0
  243. pen_stack/wgenome/offtarget_enumerate.py +274 -0
  244. pen_stack/wgenome/offtarget_integrase.py +155 -0
  245. pen_stack/wgenome/offtarget_nuclease.py +123 -0
  246. pen_stack/wgenome/offtarget_paste.py +41 -0
  247. pen_stack/wgenome/offtarget_predict.py +282 -0
  248. pen_stack/wgenome/ood.py +135 -0
  249. pen_stack/wgenome/providers.py +278 -0
  250. pen_stack/wgenome/safety.py +69 -0
  251. pen_stack/wgenome/structure3d.py +212 -0
  252. pen_stack/wgenome/uncertainty.py +250 -0
  253. pen_stack/wgenome/writability.py +72 -0
  254. pen_stack-0.1.0.dist-info/METADATA +401 -0
  255. pen_stack-0.1.0.dist-info/RECORD +259 -0
  256. pen_stack-0.1.0.dist-info/WHEEL +5 -0
  257. pen_stack-0.1.0.dist-info/entry_points.txt +3 -0
  258. pen_stack-0.1.0.dist-info/licenses/LICENSE +21 -0
  259. pen_stack-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,61 @@
1
+ """Immune-coupled delivery selection.
2
+
3
+ Fuses the cross-modality deliverability recommendation with the per-axis immune-risk profile and
4
+ surfaces the **dose <-> immunogenicity tradeoff** as a VECTOR, never collapsed into one number. The point: a vehicle
5
+ that is most deliverable may carry the highest immune liability (e.g. AAV: efficient in vivo, but pre-existing NAbs +
6
+ capsid CD8 + dose-dependent innate sensing), and that tension must be visible, not hidden.
7
+
8
+ the immune axes are population-level proxies; the realized in-vivo magnitude / patient titer is a
9
+ declared known-unknown. The dose<->immune tradeoff is informative, NOT a clinical dosing directive.
10
+ """
11
+ from __future__ import annotations
12
+
13
+
14
+ def _highest_risk_axis(design: dict) -> dict | None:
15
+ """The in-scope immune axis with the LOWEST score (= highest risk) from the immune-risk profile, with its value."""
16
+ from pen_stack.planner.immune_profile import immune_profile
17
+ axes = immune_profile(dict(design)).get("axes", {})
18
+ in_scope = {k: a for k, a in axes.items() if a.get("in_scope") and a.get("value") is not None}
19
+ if not in_scope:
20
+ return None
21
+ k = min(in_scope, key=lambda x: in_scope[x]["value"])
22
+ return {"axis": k, "score": in_scope[k]["value"], "note": in_scope[k].get("note")}
23
+
24
+
25
+ def delivery_immune_tradeoff(cargo_form: str, cargo_bp: int | None = None, target_tissue: str | None = None,
26
+ *, writer_family: str | None = None, serotype: str | None = None,
27
+ safety_weight: float = 0.5, in_vivo: bool | None = None) -> dict:
28
+ """Rank deliverability AND attach each vehicle's immune profile, surfacing the dose<->immune tradeoff per
29
+ vehicle as a vector. Never collapses deliverability and immunogenicity into one score."""
30
+ from pen_stack.planner.delivery_predict import recommend_delivery_plus
31
+ rec = recommend_delivery_plus(cargo_form, cargo_bp, target_tissue, safety_weight=safety_weight,
32
+ in_vivo=in_vivo, serotype=serotype)
33
+ coupled = []
34
+ for prof in rec.get("ranked", []) or rec.get("eligible", []) or []:
35
+ veh = prof.get("vehicle") or prof.get("name")
36
+ if not veh:
37
+ continue
38
+ design = {"delivery_vehicle": veh, "serotype": serotype, "writer_family": writer_family}
39
+ risk = _highest_risk_axis(design)
40
+ coupled.append({
41
+ "vehicle": veh,
42
+ "deliverability_balance": prof.get("balance"),
43
+ "efficacy_score": prof.get("efficacy_score"),
44
+ "immune_highest_risk_axis": risk, # the dominant immune liability
45
+ "tradeoff": (f"deliverability {prof.get('balance')} vs dominant immune liability "
46
+ f"'{risk['axis']}' (score {risk['score']})" if risk else
47
+ "deliverability ranked; immune axes abstain for this vehicle"),
48
+ })
49
+ return {
50
+ "cargo_form": cargo_form, "cargo_bp": cargo_bp, "target_tissue": target_tissue,
51
+ "serotype_tropism_prior": rec.get("serotype_tropism_prior"),
52
+ "coupled": coupled,
53
+ "collapsed_score": None, # deliberately None, dose<->immune is a vector, NEVER fused
54
+ "dose_immune_note": ("AAV/viral vehicles are efficient in vivo but carry the highest immune liability "
55
+ "(pre-existing NAbs, capsid CD8, dose-dependent innate sensing); higher dose raises both "
56
+ "transduction AND immunogenicity. The tradeoff is surfaced per vehicle, never collapsed."),
57
+ "known_unknowns": ["in_vivo_immunogenicity_magnitude", "patient_specific_titer", "realized_dose_response"],
58
+ "honesty": "immune axes are population-level proxies; realized magnitude/titer is a known-unknown; "
59
+ "this is decision-support, NOT a clinical dosing directive. No collapsed score.",
60
+ "no_fabrication": True,
61
+ }
@@ -0,0 +1,222 @@
1
+ """Delivery safety<->efficacy balance over DOCUMENTED immune priors.
2
+
3
+ Motivation (user): "we should be able to give a good balance between safety and efficacy. AAV is
4
+ safe but neutralizing antibodies / pre-existing immunity will be there; lentivirus is highly efficacious at
5
+ integrating but its safety is bad." This module turns the per-vehicle ``immune_safety`` block in
6
+ configs/delivery_vehicles.yaml into a transparent, user-weightable ranking of the delivery palette.
7
+
8
+ SCOPE NOTE (unchanged across the program): the immune MAGNITUDE, how strongly a given patient or
9
+ construct will react in vivo, is a declared known-unknown (configs/known_unknowns.yaml: ``in_vivo_immunogenicity``)
10
+ and is NEVER predicted here. What this module exposes is strictly:
11
+
12
+ * the DOCUMENTED, CITED, ORDINAL (low/moderate/high) literature priors per vehicle, and
13
+ * a transparent ranking that combines those ordinal tiers under a USER-SUPPLIED safety<->efficacy weight.
14
+
15
+ The composite scores are an explicit, reproducible function of the documented tiers and the user's weight, not
16
+ a learned or predicted immunogenicity. Every profile carries the scope flag that the magnitude is out of scope,
17
+ plus the curated DOIs so the prior is auditable.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ from functools import lru_cache
22
+
23
+ from pen_stack.planner.delivery_vehicles import load_vehicles, names, vehicle
24
+
25
+ # Ordinal map for the documented qualitative tiers. Higher = more of the named property.
26
+ # For the BURDEN axes (immunity / NAb / innate / adaptive / genotoxicity) higher = worse (less safe);
27
+ # for the EFFICACY axis higher = better.
28
+ _TIER = {"low": 0.0, "moderate": 1.0, "high": 2.0}
29
+ # Two DISTINCT safety sub-axes are kept separate, never collapsed into one mean, because they are different
30
+ # KINDS of risk: immunogenicity is (largely) reversible and bears on eligibility/re-dosing, whereas
31
+ # genotoxicity (insertional mutagenesis) is a permanent, potentially oncogenic risk. This is exactly the
32
+ # tradeoff the request is about ("AAV is safe but NAbs; LV is highly efficacious at integrating but its
33
+ # safety is bad"), so genotoxicity must be a first-class, separately-visible axis, not 1/5 of an average.
34
+ _IMMUNE_AXES = ("preexisting_immunity", "neutralizing_antibody", "innate_immune", "adaptive_immune")
35
+ _GENOTOX_AXIS = "genotoxicity"
36
+ # magnitude is NEVER predicted; this is the known-unknown the profile points back to.
37
+ _MAGNITUDE_SCOPE_ID = "in_vivo_immunogenicity"
38
+
39
+
40
+ def _tier(val) -> float | None:
41
+ if val is None:
42
+ return None
43
+ return _TIER.get(str(val).strip().lower())
44
+
45
+
46
+ def safety_efficacy_profile(name: str) -> dict | None:
47
+ """Documented safety<->efficacy profile for one vehicle, derived ONLY from its ``immune_safety`` block.
48
+
49
+ Reports the ordinal tiers verbatim and THREE normalised sub-scores (1 = best):
50
+ * ``immune_score`` = 1 - mean(immune-burden axes)/2 (1 = least immunogenic)
51
+ * ``genotox_score`` = 1 - genotoxicity/2 (1 = least insertional/oncogenic risk)
52
+ * ``efficacy_score`` = efficacy/2 (1 = most efficacious)
53
+ plus a headline ``safety_score = min(immune_score, genotox_score)``, a precautionary worst-axis aggregation
54
+ so a vehicle is only as "safe" as its WORST safety dimension (a non-immunogenic but genotoxic vector is not
55
+ called safe). Also returns the documented tradeoff sentence, curated DOIs, and the standing scope flag that
56
+ the immune MAGNITUDE is not predicted. Returns ``None`` for an unknown vehicle; invents no number."""
57
+ rec = vehicle(name)
58
+ if rec is None:
59
+ return None
60
+ imm = rec.get("immune_safety") or {}
61
+ # per immune axis -> a 0..1 "safety" score (1 = least immunogenic). The ADAPTIVE (CD8 T-cell) axis can be
62
+ # COMPUTED from the capsid epitope load (primary = NetMHCpan-4.1 over the
63
+ # capsid/envelope antigen, MHCflurry cross-check). The computed signal is INTRINSIC antigen presentability; it
64
+ # only translates to a realized host
65
+ # response when the host is actually exposed to the capsid (IN-VIVO use). So it is folded into the adaptive
66
+ # axis only for in-vivo vehicles; for EX-VIVO-only viral vectors (e.g. lentivirus, whose VSV-G envelope is
67
+ # intrinsically epitope-dense but barely seen by the host ex vivo) it is surfaced but NOT folded, and the
68
+ # documented tier is kept. The other three axes stay documented ordinal tiers. No number is fabricated.
69
+ from pen_stack.planner.capsid_epitope_oracle import computed_capsid_immune_score
70
+ cap_score, cap_oracle = computed_capsid_immune_score(name)
71
+ in_vivo = bool(rec.get("in_vivo"))
72
+ axis_scores: dict[str, float | None] = {}
73
+ for ax in _IMMUNE_AXES:
74
+ t = _tier(imm.get(ax))
75
+ axis_scores[ax] = (1.0 - t / 2.0) if t is not None else None
76
+ adaptive_source = "documented"
77
+ if cap_score is not None and in_vivo:
78
+ axis_scores["adaptive_immune"] = cap_score # in-vivo: host sees the capsid -> fold computed
79
+ adaptive_source = "computed"
80
+ elif cap_score is not None and not in_vivo:
81
+ adaptive_source = "computed_ex_vivo_muted" # reported, but ex-vivo mutes the realized response
82
+ # PRE-EXISTING immunity (B-cell / NAb): grounded in published serosurvey data. Folded
83
+ # only for IN-VIVO vehicles (serum NAb neutralises the vector in vivo; ex-vivo transduction in a dish is
84
+ # not reached by host antibody, so for ex-vivo vehicles it is reported but muted).
85
+ from pen_stack.planner.seroprevalence_oracle import computed_preexisting_score
86
+ pre_score, pre_oracle = computed_preexisting_score(name)
87
+ preexisting_source = "documented"
88
+ if pre_score is not None and in_vivo:
89
+ axis_scores["preexisting_immunity"] = pre_score
90
+ preexisting_source = "computed"
91
+ elif pre_score is not None and not in_vivo:
92
+ preexisting_source = "computed_ex_vivo_muted"
93
+ immune_present = [s for s in axis_scores.values() if s is not None]
94
+ immune_score = (sum(immune_present) / len(immune_present)) if immune_present else None
95
+ # genotoxicity: prefer the COMPUTED oracle (integration-site x COSMIC-oncogene
96
+ # enrichment, from VISDB) for integrating vectors; fall back to the documented ordinal tier when the
97
+ # oracle abstains. No number is fabricated either way.
98
+ from pen_stack.planner.genotoxicity_oracle import computed_genotox_score
99
+ computed, gtox_oracle = computed_genotox_score(name)
100
+ gtox_tier = _tier(imm.get(_GENOTOX_AXIS))
101
+ documented_genotox = (1.0 - gtox_tier / 2.0) if gtox_tier is not None else None
102
+ genotox_score = computed if computed is not None else documented_genotox
103
+ genotox_source = "computed" if computed is not None else ("documented" if documented_genotox is not None
104
+ else None)
105
+ sub = [s for s in (immune_score, genotox_score) if s is not None]
106
+ safety_score = min(sub) if sub else None # worst-axis; abstain if neither documented
107
+ eff = _tier(imm.get("efficacy"))
108
+ efficacy_score = (eff / 2.0) if eff is not None else None
109
+ _r = lambda x: None if x is None else round(x, 3) # noqa: E731
110
+ return {
111
+ "vehicle": name,
112
+ "tiers": {ax: imm.get(ax) for ax in (*_IMMUNE_AXES, _GENOTOX_AXIS)} | {"efficacy": imm.get("efficacy")},
113
+ "immune_score": _r(immune_score),
114
+ "adaptive_source": adaptive_source, # computed | computed_ex_vivo_muted | documented
115
+ "capsid_presentability_score": _r(cap_score), # computed intrinsic capsid CD8 presentability (or None)
116
+ "adaptive_provenance": (cap_oracle.note if cap_score is not None else None),
117
+ "preexisting_source": preexisting_source, # computed | computed_ex_vivo_muted | documented
118
+ "seroprevalence_score": _r(pre_score), # computed pre-existing-NAb score (or None)
119
+ "preexisting_provenance": (pre_oracle.note if pre_score is not None else None),
120
+ "genotox_score": _r(genotox_score),
121
+ "genotox_source": genotox_source, # "computed" (VISDBxCOSMIC oracle) | "documented" (ordinal tier)
122
+ "genotox_provenance": (gtox_oracle.note if genotox_source == "computed" else None),
123
+ "safety_score": _r(safety_score),
124
+ "efficacy_score": _r(efficacy_score),
125
+ "re_dosable": imm.get("re_dosable", rec.get("re_dosable")),
126
+ "integrating": rec.get("integrating"),
127
+ "tradeoff": imm.get("tradeoff"),
128
+ "immune_dois": list(imm.get("immune_dois", []) or []),
129
+ "magnitude_scope_flag": {
130
+ "kind": "known_unknown", "id": _MAGNITUDE_SCOPE_ID,
131
+ "reason": "the in-vivo immune MAGNITUDE (patient/construct-specific response) is a known-unknown; "
132
+ "only documented ordinal priors are surfaced, never a predicted magnitude"},
133
+ "note": "safety_score = min(immune_score, genotox_score) over DOCUMENTED ordinal tiers, a precautionary "
134
+ "worst-axis aggregation, not a predicted immunogenicity.",
135
+ }
136
+
137
+
138
+ @lru_cache(maxsize=1)
139
+ def all_profiles() -> tuple:
140
+ """All vehicle profiles (tuple so it is hashable/cacheable)."""
141
+ return tuple(p for n in names() if (p := safety_efficacy_profile(n)) is not None)
142
+
143
+
144
+ def _balance(profile: dict, safety_weight: float) -> float | None:
145
+ """Composite = safety_weight * safety_score + (1 - safety_weight) * efficacy_score. None if either axis is
146
+ undocumented (abstain rather than impute)."""
147
+ s, e = profile["safety_score"], profile["efficacy_score"]
148
+ if s is None or e is None:
149
+ return None
150
+ w = min(max(float(safety_weight), 0.0), 1.0)
151
+ return round(w * s + (1.0 - w) * e, 4)
152
+
153
+
154
+ def recommend_delivery(cargo_form: str, cargo_bp: int | None = None, *, safety_weight: float = 0.5,
155
+ in_vivo: bool | None = None) -> dict:
156
+ """Rank the delivery palette for a cargo by a USER-WEIGHTED safety<->efficacy balance over documented priors.
157
+
158
+ Args:
159
+ cargo_form: required cargo form (DNA / mRNA / RNP), only vehicles compatible with it are considered.
160
+ cargo_bp: if given, vehicles whose ``cargo_capacity_bp`` is smaller are excluded (hard packaging limit).
161
+ safety_weight: in [0, 1]. 1.0 = rank purely on documented safety; 0.0 = purely on efficacy; 0.5 = balance.
162
+ in_vivo: if True, exclude ex-vivo-only vehicles (and vice-versa) when the field is documented.
163
+
164
+ Returns a dict with the eligible vehicles ranked best-first by the composite, each carrying its documented
165
+ tiers, tradeoff, and curated DOIs, plus the standing magnitude scope flag. No magnitude is predicted; the
166
+ composite is an explicit function of the documented ordinal tiers and the caller's weight."""
167
+ form = (cargo_form or "").strip()
168
+ veh = load_vehicles()
169
+ # Input validation, surfaced rather than silently absorbed: an unrecognized cargo form matches no vehicle and
170
+ # would otherwise return an unexplained empty ranking, and a non-physical cargo length can never exceed a
171
+ # capacity so it would otherwise pass every packaging check unnoticed.
172
+ known_forms = sorted({f for rec in veh.values() for f in (rec.get("compatible_cargo_form") or [])})
173
+ input_flags: list[dict] = []
174
+ if form and form not in known_forms:
175
+ input_flags.append({"field": "cargo_form", "value": cargo_form, "issue": "unrecognized",
176
+ "detail": f"no vehicle declares compatibility with cargo form {cargo_form!r}; "
177
+ f"known forms: {', '.join(known_forms)}. No vehicle is eligible."})
178
+ if cargo_bp is not None and int(cargo_bp) <= 0:
179
+ input_flags.append({"field": "cargo_bp", "value": cargo_bp, "issue": "not_physical",
180
+ "detail": "cargo length must be a positive number of base pairs; the packaging-capacity "
181
+ "filter cannot be applied to it."})
182
+ eligible: list[dict] = []
183
+ excluded: list[dict] = []
184
+ for n, rec in veh.items():
185
+ forms = rec.get("compatible_cargo_form", []) or []
186
+ if form and form not in forms:
187
+ continue
188
+ if cargo_bp is not None and rec.get("cargo_capacity_bp") is not None \
189
+ and int(cargo_bp) > int(rec["cargo_capacity_bp"]):
190
+ excluded.append({"vehicle": n, "reason": f"cargo {cargo_bp} bp exceeds capacity "
191
+ f"{rec['cargo_capacity_bp']} bp"})
192
+ continue
193
+ if in_vivo is True and rec.get("ex_vivo") and not rec.get("in_vivo"):
194
+ excluded.append({"vehicle": n, "reason": "ex-vivo-only; an in-vivo route was required"})
195
+ continue
196
+ if in_vivo is False and rec.get("in_vivo") and not rec.get("ex_vivo"):
197
+ excluded.append({"vehicle": n, "reason": "in-vivo-only; an ex-vivo route was required"})
198
+ continue
199
+ prof = safety_efficacy_profile(n)
200
+ if prof is None:
201
+ continue
202
+ prof = dict(prof)
203
+ prof["balance"] = _balance(prof, safety_weight)
204
+ eligible.append(prof)
205
+
206
+ # rank: documented balance first (None last); tie-break by efficacy then safety.
207
+ eligible.sort(key=lambda p: (p["balance"] is None, -(p["balance"] or 0.0),
208
+ -(p["efficacy_score"] or 0.0), -(p["safety_score"] or 0.0)))
209
+ return {
210
+ "cargo_form": form, "cargo_bp": cargo_bp, "safety_weight": min(max(float(safety_weight), 0.0), 1.0),
211
+ "in_vivo": in_vivo,
212
+ "ranked": eligible,
213
+ "recommended": eligible[0]["vehicle"] if eligible else None,
214
+ "excluded": excluded,
215
+ "input_flags": input_flags,
216
+ "scope_flags": [{"kind": "known_unknown", "id": _MAGNITUDE_SCOPE_ID,
217
+ "reason": "ranking is over DOCUMENTED ordinal immune priors; the patient/construct-"
218
+ "specific immune MAGNITUDE is a known-unknown and is not predicted"}],
219
+ "no_fabrication": True,
220
+ "note": "balance = safety_weight*safety + (1-safety_weight)*efficacy over documented low/moderate/high "
221
+ "tiers; change safety_weight to move along the safety<->efficacy frontier.",
222
+ }
@@ -0,0 +1,196 @@
1
+ """Cross-modality deliverability + learned AAV capsid-fitness.
2
+
3
+ Extends the rule-level `recommend_delivery` (documented safety<->efficacy balance) with two grounded layers:
4
+
5
+ * **serotype -> tissue tropism PRIOR** (`configs/aav_serotype_tropism.yaml`), real serotype<->tissue mappings
6
+ evidenced by APPROVED AAV gene therapies (AAV9->CNS, AAVrh74->skeletal muscle, AAV5->liver, AAV2->retina/putamen
7
+ via LOCAL injection). A grounded prior for an approved serotype; a known-unknown (abstain) for a novel capsid.
8
+ * **learned capsid-fitness**, a model trained on the FLIP-AAV benchmark (Bryant 2021 packaging fitness; Dallago
9
+ 2021 splits) that scores an AAV capsid VP1 sequence. The Delivery-Bench shows it beats a mutation-burden baseline
10
+ on held-out splits; the licensed datasets stay on the VM, and the derived metrics + reproducible build script are
11
+ committed. The ~3 MB model itself is gitignored (regenerated via ``scripts/build_capsid_fitness.py`` and mounted
12
+ into the deployed app, like ``position_effect.pkl``); the axis abstains gracefully when it is absent. Predicted
13
+ fitness is a CANDIDATE for the MEASURED axis (packaging viability) and extrapolative for in-vivo human tropism.
14
+
15
+ no fabricated tropism; predicted fitness is a candidate; abstains without inputs / without the model.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from functools import lru_cache
20
+
21
+ import yaml
22
+
23
+ from pen_stack._resources import project_root, resource
24
+
25
+ # Delivery-Bench headline (REAL FLIP-AAV result; learned vs mutation-burden baseline, Spearman, bootstrap CI).
26
+ # Filled from benchmarks/delivery/capsid_fitness_metrics.json (computed on the VM); kept in-code so the axis is
27
+ # available everywhere (CI / bare wheel / live app) without the licensed data tree.
28
+ CAPSID_FITNESS_BENCH = {
29
+ "benchmark": "FLIP-AAV (Dallago 2021; Bryant 2021 packaging fitness, 10.1038/s41587-020-00793-4)",
30
+ "model": "windowed one-hot (VP1 555-595) gradient boosting",
31
+ "baseline": "mutation burden (Hamming from the train consensus)",
32
+ "splits": {}, # populated below from the committed metrics
33
+ }
34
+
35
+
36
+ @lru_cache(maxsize=1)
37
+ def _bench_metrics() -> dict:
38
+ """The committed Delivery-Bench metrics (capsid-fitness learned-vs-baseline), or {} if the data tree is absent."""
39
+ try:
40
+ import json
41
+ return json.loads(resource("benchmarks/delivery/capsid_fitness_metrics.json").read_text(encoding="utf-8"))
42
+ except Exception: # noqa: BLE001
43
+ return {}
44
+
45
+
46
+ @lru_cache(maxsize=1)
47
+ def _tropism() -> dict:
48
+ try:
49
+ return yaml.safe_load(resource("configs/aav_serotype_tropism.yaml").read_text(encoding="utf-8")) or {}
50
+ except Exception: # noqa: BLE001
51
+ return {}
52
+
53
+
54
+ def serotype_tropism(serotype: str) -> dict:
55
+ """The grounded tissue prior for an AAV serotype (from approved therapies), or a known-unknown."""
56
+ rec = (_tropism().get("serotypes") or {}).get(serotype)
57
+ if rec:
58
+ return {"serotype": serotype, "tissue": rec["tissue"], "route": rec.get("route"),
59
+ "evidence": rec.get("example_product"), "indication": rec.get("indication"),
60
+ "approval": rec.get("approval"), "doi": rec.get("doi"),
61
+ "confidence": "grounded (approved therapy)", "output_kind": "prior"}
62
+ return {"serotype": serotype, "tissue": None, "confidence": "known-unknown",
63
+ "note": "no approved-therapy precedent for this serotype/capsid -> in-vivo human tropism is a "
64
+ "known-unknown; not fabricated", "output_kind": "abstain"}
65
+
66
+
67
+ def serotypes_for_tissue(target_tissue: str) -> dict:
68
+ """Which approved AAV serotypes are GROUNDED priors for a target tissue (e.g. liver -> AAV5/AAVRh74var). Abstains
69
+ (empty) when no approved serotype targets the tissue, never invents one."""
70
+ t = (target_tissue or "").strip().lower()
71
+ hits = []
72
+ for s, rec in (_tropism().get("serotypes") or {}).items():
73
+ tissues = [str(x).lower() for x in (rec.get("tissue") or [])]
74
+ if any(t in x or x in t for x in tissues):
75
+ hits.append({"serotype": s, "tissue": rec["tissue"], "route": rec.get("route"),
76
+ "evidence": rec.get("example_product")})
77
+ return {"target_tissue": target_tissue, "grounded_serotypes": hits,
78
+ "note": ("grounded serotype->tissue priors from approved therapies" if hits else
79
+ f"no approved AAV serotype has a grounded prior for {target_tissue!r} -> known-unknown (abstain)")}
80
+
81
+
82
+ @lru_cache(maxsize=1)
83
+ def _fitness_model():
84
+ """Load the FLIP-AAV-trained capsid-fitness model (gitignored .pkl, regenerated on the VM; mounted into the live
85
+ container like position_effect.pkl). None when absent -> the fitness call abstains. The committed bench metrics
86
+ document the model's measured performance even when the .pkl is not present."""
87
+ import pickle
88
+ for base in (project_root() / "models", project_root() / "data" / "delivery_models"):
89
+ p = base / "capsid_fitness.pkl"
90
+ if p.exists():
91
+ try:
92
+ return pickle.load(open(p, "rb"))
93
+ except Exception: # noqa: BLE001
94
+ return None
95
+ return None
96
+
97
+
98
+ def capsid_fitness(vp1_sequence: str, vector: str = "AAV") -> dict:
99
+ """Predicted AAV capsid packaging-fitness for a VP1 sequence (FLIP-AAV-trained), or an abstention when the
100
+ model is not present (CI / bare wheel). The learned model is AAV-capsid-specific: for other vectors (LV,
101
+ adenovirus, HSV) there is NO packaging/assembly-fitness model, so it abstains with that stated rather than
102
+ fabricate a score. Predicted fitness is a CANDIDATE for the measured packaging axis, NOT an in-vivo tropism claim."""
103
+ import numpy as np
104
+ vec = (vector or "AAV").strip()
105
+ if vec.upper() != "AAV":
106
+ return {"available": False, "abstain": True, "vector": vec, "predicted_fitness": None, "output_kind": "n/a",
107
+ "note": f"no learned packaging/assembly-fitness model for {vec}: the FLIP-AAV model is AAV-capsid-"
108
+ "specific. Non-AAV vectors get vehicle-level checks (capacity, immune risk), not a "
109
+ "capsid-fitness score -- not fabricated.", "bench": None}
110
+ m = _fitness_model()
111
+ bench = _bench_metrics()
112
+ if m is None:
113
+ return {"available": False, "abstain": True, "vector": "AAV", "predicted_fitness": None,
114
+ "output_kind": "candidate",
115
+ "note": "capsid_fitness.pkl not present (VM-only/regenerated); the committed Delivery-Bench documents "
116
+ "its measured performance.", "bench": bench}
117
+ w0, w1 = m["window"]
118
+ aas = m["aas"]
119
+ ai = {a: i for i, a in enumerate(aas)}
120
+ wl = w1 - w0
121
+ # clean the input: drop FASTA header lines, strip whitespace, upper-case (a pasted VP1 usually has newlines).
122
+ seq = "".join(ln for ln in str(vp1_sequence).splitlines() if not ln.lstrip().startswith(">"))
123
+ seq = "".join(seq.split()).upper()
124
+ # INPUT GUARD: the model reads AAV VP1 residues w0-w1 ONLY. A sequence that never reaches that window, or a
125
+ # nucleotide sequence pasted by mistake, would collapse to a degenerate constant -> abstain, never fake a score.
126
+ nuc_only = bool(seq) and set(seq) <= set("ACGTUN")
127
+ if len(seq) <= w0 or nuc_only:
128
+ why = ("this looks like a nucleotide (DNA/RNA) sequence; the model needs the VP1 capsid PROTEIN (amino acids)"
129
+ if nuc_only else
130
+ f"the input is only {len(seq)} aa and never reaches VP1 residues {w0}-{w1} -- the region the model reads")
131
+ return {"available": False, "abstain": True, "vector": "AAV", "predicted_fitness": None,
132
+ "output_kind": "candidate", "input_issue": why,
133
+ "note": f"no meaningful score: {why}. Paste a full-length AAV VP1 capsid protein (>= {w1} aa, AAV2 "
134
+ f"numbering) so the model can read residues {w0}-{w1}.", "bench": bench}
135
+ win = (seq[w0:w1] + "-" * wl)[:wl]
136
+ x = np.zeros(wl * 20, dtype="float32")
137
+ for i, a in enumerate(win):
138
+ if a in ai:
139
+ x[i * 20 + ai[a]] = 1.0
140
+ val = float(m["model"].predict(x.reshape(1, -1))[0])
141
+ # Readable verdict: the PERCENTILE of this prediction within the model's own predicted-fitness distribution on
142
+ # the held-out FLIP-AAV test set (a grounded, unambiguous "top X% of measured variants" instead of a bare float).
143
+ pcts = m.get("pred_percentiles")
144
+ percentile = None
145
+ if pcts:
146
+ percentile = max(0, min(100, int(np.searchsorted(np.asarray(pcts, dtype="float64"), val))))
147
+ if percentile is None:
148
+ bucket, verdict = "unknown", f"Predicted packaging fitness {round(val, 3)} (no reference distribution in this model build)."
149
+ else:
150
+ bucket = "better" if percentile >= 67 else ("similar" if percentile >= 33 else "worse")
151
+ verdict = {"better": "High packaging fitness", "similar": "Mid-range packaging fitness",
152
+ "worse": "Low packaging fitness"}[bucket] # the percentile carries the "better than X%" detail
153
+ # a compact 24-bin histogram of the reference distribution (for a UI sparkline). Binning the equally-spaced
154
+ # percentiles reconstructs the density shape (dense near the mode, sparse at the tails); cached on the model.
155
+ dist = m.get("_ref_hist")
156
+ if dist is None and pcts:
157
+ arr = np.asarray(pcts, dtype="float64")
158
+ lo, hi = float(arr[0]), float(arr[-1])
159
+ counts, _ = np.histogram(arr, bins=24, range=(lo, hi))
160
+ dist = {"lo": round(lo, 4), "hi": round(hi, 4), "counts": [int(x) for x in counts]}
161
+ m["_ref_hist"] = dist
162
+ input_warning = None
163
+ if len(seq) < w1: # partial window: the tail is padded, so only part of the 555-595 window informs the score
164
+ input_warning = (f"sequence is {len(seq)} aa (< {w1}); residues {len(seq)}-{w1} are padded, so only part of "
165
+ f"the {w0}-{w1} window informs the score -- treat with extra caution.")
166
+ return {"available": True, "abstain": False, "vector": "AAV", "predicted_fitness": round(val, 4),
167
+ "percentile": percentile, "verdict": verdict, "verdict_bucket": bucket, "distribution": dist,
168
+ "scale": "FLIP-AAV packaging fitness (log-enrichment; higher = more fit)", "output_kind": "candidate",
169
+ "window": [w0, w1], "sequence_len": len(seq), "input_warning": input_warning,
170
+ "status": f"CANDIDATE for the measured packaging axis; NOT an in-vivo human-tropism claim (known-unknown). "
171
+ f"The model reads VP1 residues {w0}-{w1} only; the percentile is vs the model's held-out "
172
+ f"FLIP-AAV test predictions.",
173
+ "bench": bench}
174
+
175
+
176
+ def recommend_delivery_plus(cargo_form: str, cargo_bp: int | None = None, target_tissue: str | None = None,
177
+ *, safety_weight: float = 0.5, in_vivo: bool | None = None,
178
+ serotype: str | None = None) -> dict:
179
+ """The extended recommender: the rule-level safety<->efficacy ranking (recommend_delivery) PLUS a grounded
180
+ serotype->tissue tropism prior and the learned capsid-fitness capability. The tropism prior answers whichever
181
+ direction the caller asked: a specific ``serotype`` -> its approved-therapy tissue (e.g. AAV9 -> CNS/Zolgensma);
182
+ otherwise a ``target_tissue`` -> the approved serotypes that reach it. Grounded for approved serotypes, a
183
+ known-unknown otherwise. Never fabricates."""
184
+ from pen_stack.planner.delivery_immunology import recommend_delivery
185
+ base = recommend_delivery(cargo_form, cargo_bp, safety_weight=safety_weight, in_vivo=in_vivo)
186
+ if serotype:
187
+ tropism = serotype_tropism(serotype) # serotype -> tissue (matches the field name; report path)
188
+ elif target_tissue:
189
+ tropism = serotypes_for_tissue(target_tissue) # tissue -> approved serotypes
190
+ else:
191
+ tropism = None
192
+ return {**base, "serotype": serotype, "target_tissue": target_tissue, "serotype_tropism_prior": tropism,
193
+ "capsid_fitness": {"capability": "learned FLIP-AAV capsid-fitness (call capsid_fitness(vp1_seq))",
194
+ "bench": _bench_metrics().get("mut_des") or CAPSID_FITNESS_BENCH},
195
+ "honesty": "tropism is a grounded prior for approved serotypes, a known-unknown otherwise; predicted "
196
+ "capsid-fitness is a candidate for the measured packaging axis; no fabricated tropism."}
@@ -0,0 +1,37 @@
1
+ """Delivery-vehicle palette loader. Reads configs/delivery_vehicles.yaml (>=6 vehicles:
2
+ AAV single/dual, lentivirus, helper-dependent adenovirus, HSV amplicon, LNP-mRNA, eVLP, electroporation).
3
+ Replaces the single dual-AAV assumption with a queryable palette the rule engine constrains."""
4
+ from __future__ import annotations
5
+
6
+ from functools import lru_cache
7
+
8
+ import yaml
9
+
10
+ from pen_stack._resources import resource
11
+
12
+
13
+ @lru_cache(maxsize=1)
14
+ def load_vehicles(path=None) -> dict:
15
+ p = resource("configs/delivery_vehicles.yaml") if path is None else path
16
+ return yaml.safe_load(open(p, encoding="utf-8").read())["vehicles"]
17
+
18
+
19
+ def vehicle(name: str) -> dict | None:
20
+ """Vehicle record by name (case-insensitive; tolerant of aliases like 'aav'->AAV_single, 'lvv')."""
21
+ if not name:
22
+ return None
23
+ veh = load_vehicles()
24
+ if name in veh:
25
+ return veh[name]
26
+ low = name.lower()
27
+ alias = {"aav": "AAV_single", "aav_single": "AAV_single", "dual_aav": "AAV_dual",
28
+ "aav_dual": "AAV_dual", "lvv": "lentivirus", "lenti": "lentivirus",
29
+ "hdad": "helper_dependent_adenovirus", "hsv": "hsv_amplicon",
30
+ "lnp": "lnp_mrna", "mrna": "lnp_mrna", "vlp": "evlp", "ep": "electroporation",
31
+ "nucleofection": "electroporation"}
32
+ key = alias.get(low) or next((k for k in veh if k.lower() == low), None)
33
+ return veh.get(key) if key else None
34
+
35
+
36
+ def names() -> list[str]:
37
+ return list(load_vehicles().keys())
@@ -0,0 +1,112 @@
1
+ """Computed genotoxicity oracle for integrating delivery vectors.
2
+
3
+ Replaces the hard-coded `genotoxicity` ordinal tier (documented prior) with a DATA-COMPUTED signal for
4
+ INTEGRATING vehicles: the observed enrichment of a vector class's integration sites near CancerMine (CC0)
5
+ oncogenes, from VISDB integration catalogues x the oncogene annotation (configs/genotoxicity_oracle.yaml,
6
+ built by scripts/p52_build_genotox_oracle.py on the VM where the data lives).
7
+
8
+ genotox_score = min(1, 1 / enrichment) # 1 = safest; episomal/non-targeting ~ 1.0
9
+
10
+ This reproduces the lentivirus-safer-than-gammaretrovirus ordering FROM DATA (lentiviral ~2x oncogene-proximity
11
+ enrichment vs gammaretroviral ~5-6x, the LMO2 / SCID-X1 pattern), instead of asserting it. It answers through
12
+ the OracleResult contract (value + provenance + native_uncertainty + scope_card + output_kind) and is an
13
+ `output_kind="baseline"` observed-data comparator, NOT a generative claim.
14
+
15
+ SCOPE: this is a RELATIVE, integration-PREFERENCE signal. The in-vivo clonal-expansion / leukemogenesis
16
+ OUTCOME in a patient is NOT modelled and stays a known-unknown; a class with too few catalogued sites is
17
+ flagged `extrapolating`; non-integrating vehicles have no insertional mechanism (score 1.0 by mechanism).
18
+ """
19
+ from __future__ import annotations
20
+
21
+ from functools import lru_cache
22
+
23
+ import yaml
24
+
25
+ from pen_stack._resources import resource
26
+ from pen_stack.oracles.schema import OracleResult, Provenance
27
+ from pen_stack.planner.delivery_vehicles import vehicle
28
+
29
+ _SCOPE_CARD = "delivery_genotoxicity"
30
+
31
+
32
+ @lru_cache(maxsize=1)
33
+ def _artifact() -> dict:
34
+ return yaml.safe_load(resource("configs/genotoxicity_oracle.yaml").read_text(encoding="utf-8"))
35
+
36
+
37
+ @lru_cache(maxsize=1)
38
+ def _vehicle_to_class() -> dict:
39
+ art = _artifact()
40
+ out: dict[str, str] = {}
41
+ for cls, vehs in (art.get("vehicle_class") or {}).items():
42
+ for v in vehs or []:
43
+ out[v] = cls
44
+ return out
45
+
46
+
47
+ def _prov(source: str, **extra) -> Provenance:
48
+ art = _artifact()
49
+ return Provenance(model="visdb_integration_x_cancermine", version=str(art.get("version", "1.0")),
50
+ source=source, extra={"built": art.get("built"),
51
+ "provenance_dois": art.get("provenance_dois", []), **extra})
52
+
53
+
54
+ def genotoxicity_oracle(vehicle_name: str) -> OracleResult:
55
+ """Computed genotoxicity for a delivery vehicle, as an OracleResult.
56
+
57
+ - non-integrating vehicle -> genotox_score 1.0 by mechanism (episomal/transient; no insertional risk).
58
+ - integrating + computed class -> data-derived score from VISDB x CancerMine; small-n class -> extrapolating.
59
+ - integrating but no computed class / unknown vehicle -> available=False (caller falls back to the
60
+ documented `immune_safety.genotoxicity` tier; no number is fabricated)."""
61
+ rec = vehicle(vehicle_name)
62
+ if rec is None:
63
+ return OracleResult(oracle="genome", value=None, provenance=_prov("cache"),
64
+ scope_card=_SCOPE_CARD, in_scope=False, available=False, output_kind="baseline",
65
+ note=f"unknown vehicle {vehicle_name!r}")
66
+
67
+ if not rec.get("integrating"):
68
+ return OracleResult(
69
+ oracle="genome",
70
+ value={"genotox_score": 1.0, "enrichment": None, "mechanism": "non-integrating"},
71
+ provenance=_prov("cache"), native_uncertainty=0.0, scope_card=_SCOPE_CARD, in_scope=True,
72
+ extrapolating=False, output_kind="baseline", available=True,
73
+ note="episomal/transient vector: no integration -> no insertional-mutagenesis mechanism (score 1.0).")
74
+
75
+ cls = _vehicle_to_class().get(vehicle_name)
76
+ classes = _artifact().get("classes") or {}
77
+ if not cls or cls not in classes:
78
+ return OracleResult(oracle="genome", value=None, provenance=_prov("cache"),
79
+ scope_card=_SCOPE_CARD, in_scope=False, available=False, output_kind="baseline",
80
+ note=f"integrating vehicle {vehicle_name!r} has no computed class; "
81
+ "fall back to the documented genotoxicity tier.")
82
+
83
+ rc = classes[cls]
84
+ enrich = rc.get("enrichment")
85
+ frac, ci95, n = rc.get("frac_oncogene_50kb"), rc.get("ci95"), rc.get("n_sites")
86
+ score = min(1.0, 1.0 / enrich) if enrich else None
87
+ robust = bool(rc.get("robust"))
88
+ # native uncertainty = relative CI on the observed oncogene-proximity fraction (coefficient of variation)
89
+ nu = round(ci95 / frac, 4) if (ci95 and frac) else None
90
+ return OracleResult(
91
+ oracle="genome",
92
+ value={"genotox_score": round(score, 3) if score is not None else None,
93
+ "enrichment": enrich, "frac_oncogene_50kb": frac, "ci95": ci95, "n_sites": n,
94
+ "frac_genotoxic_cis": rc.get("frac_genotoxic_cis"), "vector_class": cls,
95
+ "background_frac": _artifact().get("genome_background_frac_oncogene_50kb")},
96
+ provenance=_prov("cache", virus=rc.get("virus"), vector_class=cls),
97
+ native_uncertainty=nu, scope_card=_SCOPE_CARD, in_scope=True,
98
+ extrapolating=not robust, output_kind="baseline", available=True,
99
+ note=(f"{cls} integration is {enrich}x enriched within "
100
+ f"{_artifact().get('window_bp')} bp of a CancerMine oncogene vs background "
101
+ f"({frac:.3%}, n={n}); genotox_score=min(1,1/enrichment)."
102
+ + ("" if robust else " SMALL-N class: directional only (extrapolating).")
103
+ + " In-vivo clonal outcome is a known-unknown (not modelled).")
104
+ )
105
+
106
+
107
+ def computed_genotox_score(vehicle_name: str) -> tuple[float | None, OracleResult]:
108
+ """Convenience: (genotox_score or None, full OracleResult). None when the oracle abstains (caller then
109
+ uses the documented tier). Never fabricates a number."""
110
+ r = genotoxicity_oracle(vehicle_name)
111
+ val = (r.value or {}).get("genotox_score") if (r.available and r.value) else None
112
+ return val, r