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,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
|