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,255 @@
|
|
|
1
|
+
"""Hazard registry, the curated, version-pinned signature source for the Guardian.
|
|
2
|
+
|
|
3
|
+
A *defensive* biosecurity screen. The registry holds, at the FUNCTION / FAMILY / TAXON level only (public
|
|
4
|
+
Pfam accessions + public control-list references), the controlled-hazard categories a genome-writing design
|
|
5
|
+
is screened against. It deliberately contains no hazard sequences and no operational/synthesis detail, it
|
|
6
|
+
is a safeguard, not a guarantee, and not a substitute for institutional biosafety review.
|
|
7
|
+
|
|
8
|
+
The matcher is intentionally EXPLICIT (keyword/Pfam/alias tokens from the curated config), not a heuristic:
|
|
9
|
+
a safety control should be auditable. Flags come ONLY from the registry (or, when enabled, a wrapped external
|
|
10
|
+
screener), never fabricated. The function screen is what catches an AI-designed homolog: a low-identity
|
|
11
|
+
sequence still carries a hazardous FUNCTION annotation, and function, not homology, is the load-bearing axis.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from functools import lru_cache
|
|
18
|
+
from typing import Any, Callable
|
|
19
|
+
|
|
20
|
+
import yaml
|
|
21
|
+
|
|
22
|
+
from pen_stack._resources import resource
|
|
23
|
+
from pen_stack.safety.screen import ScreenHit
|
|
24
|
+
|
|
25
|
+
_REGISTRY_REL = "configs/safety/hazard_registry.yaml"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@lru_cache(maxsize=1)
|
|
29
|
+
def _default_registry() -> "HazardRegistry":
|
|
30
|
+
from pen_stack.safety.pfam_scan import pfam_domain_screen
|
|
31
|
+
return HazardRegistry.load(external_hook=pfam_domain_screen)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _norm(s: Any) -> str:
|
|
35
|
+
return str(s or "").strip().lower()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _sep_norm(s: Any) -> str:
|
|
39
|
+
"""Lowercase and collapse separator runs (whitespace / hyphen / underscore) to a single space, so
|
|
40
|
+
"furin-cleavage", "furin_cleavage" and "furin cleavage" are one token. Keeps the matcher robust to the
|
|
41
|
+
hyphen/space/underscore variation that is ubiquitous in free-text function descriptions."""
|
|
42
|
+
return re.sub(r"[\s_\-]+", " ", _norm(s)).strip()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _kw_match(kw: str, text: str) -> bool:
|
|
46
|
+
"""Separator-insensitive, word-boundary keyword match. Two failure modes of a plain substring test:
|
|
47
|
+
(1) over-broad - the short ricin keyword "rip" matches inside "transc-RIP-tion", a word in almost every
|
|
48
|
+
genome-editing design, so a benign promoter cassette would false-refuse as ricin; (2) too-narrow - a
|
|
49
|
+
hyphenated "furin-cleavage" would slip past the space-form keyword "furin cleavage". Normalising separators
|
|
50
|
+
then matching on alphanumeric boundaries fixes both: "rip" matches "RIP" standalone but not inside a longer
|
|
51
|
+
word, and "furin cleavage" / "furin-cleavage" / "furin_cleavage" all match."""
|
|
52
|
+
k = _sep_norm(kw)
|
|
53
|
+
if not k:
|
|
54
|
+
return False
|
|
55
|
+
return re.search(r"(?<![a-z0-9])" + re.escape(k) + r"(?![a-z0-9])", _sep_norm(text)) is not None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _design_function_tokens(design: dict) -> set[str]:
|
|
59
|
+
"""Normalised function annotations declared on a design (NOT free-text justification)."""
|
|
60
|
+
toks: set[str] = set()
|
|
61
|
+
for k in ("cargo_function", "function_annotation", "goal_function"):
|
|
62
|
+
v = design.get(k)
|
|
63
|
+
if v:
|
|
64
|
+
toks.add(_norm(v))
|
|
65
|
+
for k in ("function_tags", "pfam_domains", "annotations"):
|
|
66
|
+
for v in design.get(k) or []:
|
|
67
|
+
toks.add(_norm(v))
|
|
68
|
+
return {t for t in toks if t}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _design_pfam(design: dict) -> set[str]:
|
|
72
|
+
return {_norm(p) for p in (design.get("pfam_domains") or []) if p}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _design_taxon_text(design: dict) -> str:
|
|
76
|
+
return " ".join(_norm(design.get(k)) for k in ("source_taxon", "organism", "host_taxon") if design.get(k))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class HazardRegistry:
|
|
81
|
+
"""Loaded, version-pinned hazard signatures + the screen methods that consume them."""
|
|
82
|
+
|
|
83
|
+
version: str
|
|
84
|
+
toxin_functions: list[dict] = field(default_factory=list)
|
|
85
|
+
regulated_taxa: list[dict] = field(default_factory=list)
|
|
86
|
+
controlled_functions: list[dict] = field(default_factory=list)
|
|
87
|
+
chimera_rules: list[dict] = field(default_factory=list)
|
|
88
|
+
oncogenic_manipulation: dict = field(default_factory=dict)
|
|
89
|
+
external_enabled: bool = False
|
|
90
|
+
external_hook: Callable[[str], list[ScreenHit]] | None = None
|
|
91
|
+
|
|
92
|
+
# ---- loading -----------------------------------------------------------
|
|
93
|
+
@classmethod
|
|
94
|
+
def load(cls, path: str | None = None, *, external_hook: Callable[[str], list[ScreenHit]] | None = None
|
|
95
|
+
) -> "HazardRegistry":
|
|
96
|
+
raw = yaml.safe_load(resource(path or _REGISTRY_REL).read_text(encoding="utf-8"))
|
|
97
|
+
return cls(
|
|
98
|
+
version=raw.get("registry_version", "unknown"),
|
|
99
|
+
toxin_functions=raw.get("toxin_functions", []),
|
|
100
|
+
regulated_taxa=raw.get("regulated_taxa", []),
|
|
101
|
+
controlled_functions=raw.get("controlled_functions", []),
|
|
102
|
+
chimera_rules=raw.get("chimera_rules", []),
|
|
103
|
+
oncogenic_manipulation=raw.get("oncogenic_manipulation", {}),
|
|
104
|
+
external_enabled=external_hook is not None,
|
|
105
|
+
external_hook=external_hook,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def default() -> "HazardRegistry":
|
|
110
|
+
"""The registry `screen_design()` uses when no explicit registry is passed: the curated
|
|
111
|
+
function/taxon signatures PLUS the local Pfam/HMMER sequence-domain screen wired in as
|
|
112
|
+
the `external_hook`, so a raw cargo sequence for one of the curated toxin families (the SAME
|
|
113
|
+
accessions in `toxin_functions` above) is caught even with no declared cargo_function. Cached: the
|
|
114
|
+
HMM profiles parse once, not per request. `HazardRegistry.load()` itself is unchanged (still
|
|
115
|
+
hookless by default) for callers who want the bare registry or their own hook."""
|
|
116
|
+
return _default_registry()
|
|
117
|
+
|
|
118
|
+
def _prov(self, entry: dict) -> dict:
|
|
119
|
+
return {"registry_version": self.version, "signature_id": entry.get("id"),
|
|
120
|
+
"control_ref": entry.get("control_ref"), "source": "hazard_registry (function/family-level)"}
|
|
121
|
+
|
|
122
|
+
# ---- screens (each returns typed, provenanced hits) --------------------
|
|
123
|
+
def function_flags(self, design: dict) -> list[ScreenHit]:
|
|
124
|
+
"""Toxin / controlled-FUNCTION screen. Matches declared Pfam domains OR function keywords.
|
|
125
|
+
Catches AI-homologs: a low-identity sequence with a hazardous function annotation still flags."""
|
|
126
|
+
hits: list[ScreenHit] = []
|
|
127
|
+
toks = _design_function_tokens(design)
|
|
128
|
+
pfam = _design_pfam(design)
|
|
129
|
+
for entry in self.toxin_functions + self.controlled_functions:
|
|
130
|
+
by_pfam = pfam & {_norm(p) for p in entry.get("pfam", [])}
|
|
131
|
+
by_kw = {kw for kw in (entry.get("keywords") or []) if any(_kw_match(kw, t) for t in toks)}
|
|
132
|
+
if by_pfam or by_kw:
|
|
133
|
+
ev = {}
|
|
134
|
+
if by_pfam:
|
|
135
|
+
ev["matched_pfam"] = sorted(by_pfam)
|
|
136
|
+
if by_kw:
|
|
137
|
+
ev["matched_keywords"] = sorted(by_kw)
|
|
138
|
+
hits.append(ScreenHit(kind="function_flag", detail=entry["name"],
|
|
139
|
+
severity=entry.get("severity", "medium"),
|
|
140
|
+
provenance=self._prov(entry), evidence=ev))
|
|
141
|
+
return hits
|
|
142
|
+
|
|
143
|
+
def oncogenic_flags(self, design: dict) -> list[ScreenHit]:
|
|
144
|
+
"""Oncogenic-manipulation PATTERN screen. Flags the COMBINATION that a flat keyword list misses:
|
|
145
|
+
a tumor-suppressor gene with a disruptive verb, an oncogene with an activating signature, or an
|
|
146
|
+
immortalization signature. The asymmetry spares therapy without an allow-list (restoring a suppressor /
|
|
147
|
+
silencing an oncogene matches neither). Escalates to human review (dual-use, legitimate cancer-model path)."""
|
|
148
|
+
cfg = self.oncogenic_manipulation
|
|
149
|
+
if not cfg:
|
|
150
|
+
return []
|
|
151
|
+
# screen the declared function annotations only (the artifact, not any free-text justification)
|
|
152
|
+
text = " " + " | ".join(sorted(_design_function_tokens(design))) + " "
|
|
153
|
+
if not text.strip(" |"):
|
|
154
|
+
return []
|
|
155
|
+
|
|
156
|
+
def has(keys: list[str]) -> list[str]:
|
|
157
|
+
return [k for k in (keys or []) if _norm(k) in text]
|
|
158
|
+
|
|
159
|
+
supp = has(cfg.get("tumor_suppressors"))
|
|
160
|
+
onco = has(cfg.get("oncogenes"))
|
|
161
|
+
disrupt = has(cfg.get("disrupt_verbs"))
|
|
162
|
+
activate = has(cfg.get("activate_signatures"))
|
|
163
|
+
immortal = has(cfg.get("immortalization"))
|
|
164
|
+
therapy = has(cfg.get("therapy_context"))
|
|
165
|
+
|
|
166
|
+
reasons = []
|
|
167
|
+
if supp and disrupt:
|
|
168
|
+
reasons.append(f"tumor-suppressor disruption ({supp[0]} + {disrupt[0]})")
|
|
169
|
+
if onco and activate:
|
|
170
|
+
reasons.append(f"oncogene activation ({onco[0]} + {activate[0]})")
|
|
171
|
+
if immortal:
|
|
172
|
+
reasons.append(f"immortalization signature ({immortal[0]})")
|
|
173
|
+
if not reasons:
|
|
174
|
+
return []
|
|
175
|
+
# therapeutic restoration / supplementation with NO disruptive/activating/immortalization signal -> clear.
|
|
176
|
+
if therapy and not (disrupt or activate or immortal):
|
|
177
|
+
return []
|
|
178
|
+
return [ScreenHit(kind="oncogenic_flag", detail=cfg.get("name", "oncogenic manipulation"),
|
|
179
|
+
severity=cfg.get("severity", "medium"), provenance=self._prov(cfg),
|
|
180
|
+
evidence={"patterns": reasons,
|
|
181
|
+
"suppressor": supp[:2], "oncogene": onco[:2], "disrupt": disrupt[:2],
|
|
182
|
+
"activate": activate[:2], "immortalization": immortal[:2]})]
|
|
183
|
+
|
|
184
|
+
def taxon_flags(self, design: dict) -> list[ScreenHit]:
|
|
185
|
+
"""Regulated-pathogen-TAXON screen (Select Agent / Australia Group membership by declared source)."""
|
|
186
|
+
hits: list[ScreenHit] = []
|
|
187
|
+
text = _design_taxon_text(design)
|
|
188
|
+
if not text:
|
|
189
|
+
return hits
|
|
190
|
+
for entry in self.regulated_taxa:
|
|
191
|
+
aliases = [_norm(a) for a in ([entry.get("name")] + (entry.get("aliases") or [])) if a]
|
|
192
|
+
matched = [a for a in aliases if a and a in text]
|
|
193
|
+
if matched:
|
|
194
|
+
hits.append(ScreenHit(kind="taxon_flag", detail=entry["name"],
|
|
195
|
+
severity=entry.get("severity", "medium"),
|
|
196
|
+
provenance=self._prov(entry), evidence={"matched_alias": matched[0]}))
|
|
197
|
+
return hits
|
|
198
|
+
|
|
199
|
+
def chimera_context(self, design: dict) -> list[ScreenHit]:
|
|
200
|
+
"""Hazardous ASSEMBLY of individually-benign parts (toxin+broad delivery; virulence+replication;
|
|
201
|
+
split-hazard across sub-designs). Operates on declared structure, never invents a part."""
|
|
202
|
+
hits: list[ScreenHit] = []
|
|
203
|
+
rules = {r["id"]: r for r in self.chimera_rules}
|
|
204
|
+
|
|
205
|
+
cf = _norm(design.get("cargo_function"))
|
|
206
|
+
has_toxin = bool(self.function_flags(design)) or bool(cf) and any(
|
|
207
|
+
_kw_match(kw, cf) for e in self.toxin_functions for kw in (e.get("keywords") or []))
|
|
208
|
+
broad = _norm(design.get("delivery_tropism")) in {"broad", "broad_systemic", "systemic"} or \
|
|
209
|
+
_norm(design.get("delivery_vehicle")) in {"aav9", "aavrh10"}
|
|
210
|
+
replicating = bool(design.get("replication_competent"))
|
|
211
|
+
patho = any(_norm(kw) in _design_function_tokens(design)
|
|
212
|
+
for e in self.controlled_functions if e["id"] == "FUNC-PATHO-ESSENTIAL"
|
|
213
|
+
for kw in (e.get("keywords") or [])) or "pathogen_essential" in _design_function_tokens(design)
|
|
214
|
+
|
|
215
|
+
if has_toxin and broad and "CHIM-TOXIN-PAYLOAD" in rules:
|
|
216
|
+
r = rules["CHIM-TOXIN-PAYLOAD"]
|
|
217
|
+
hits.append(ScreenHit(kind="chimera_context", detail=r["detail"], severity=r["severity"],
|
|
218
|
+
provenance=self._prov(r), evidence={"toxin_payload": True, "broad_delivery": True}))
|
|
219
|
+
if (patho or replicating and self.taxon_flags(design)) and replicating and "CHIM-PATHO-REPLICATION" in rules:
|
|
220
|
+
r = rules["CHIM-PATHO-REPLICATION"]
|
|
221
|
+
hits.append(ScreenHit(kind="chimera_context", detail=r["detail"], severity=r["severity"],
|
|
222
|
+
provenance=self._prov(r), evidence={"virulence_or_taxon": True, "replication_competent": True}))
|
|
223
|
+
|
|
224
|
+
# split-hazard: scan sub-designs / multi-edit plan; if the assembled plan carries a hazardous
|
|
225
|
+
# function/taxon that no single sub-design fully declares, flag the assembly. Type-guarded so an
|
|
226
|
+
# unexpected field shape can never crash the gate (the safety gate must never break verify()).
|
|
227
|
+
subs: list[dict] = []
|
|
228
|
+
for key in ("sub_designs", "multiplex", "edits"):
|
|
229
|
+
v = design.get(key)
|
|
230
|
+
if isinstance(v, list):
|
|
231
|
+
subs += [s for s in v if isinstance(s, dict)]
|
|
232
|
+
if subs and "CHIM-SPLIT-HAZARD" in rules:
|
|
233
|
+
sub_hits = [h for s in subs for h in (self.function_flags(s) + self.taxon_flags(s))]
|
|
234
|
+
if sub_hits or self.taxon_flags(design) or patho:
|
|
235
|
+
r = rules["CHIM-SPLIT-HAZARD"]
|
|
236
|
+
worst = "high" if any(h.severity == "high" for h in sub_hits) or \
|
|
237
|
+
any(h.severity == "high" for h in self.taxon_flags(design)) else r["severity"]
|
|
238
|
+
hits.append(ScreenHit(kind="chimera_context", detail=r["detail"], severity=worst,
|
|
239
|
+
provenance=self._prov(r),
|
|
240
|
+
evidence={"n_sub_designs": len(subs), "split_hazard": True}))
|
|
241
|
+
return hits
|
|
242
|
+
|
|
243
|
+
def sequence_homology(self, seq: str | None) -> list[ScreenHit]:
|
|
244
|
+
"""Baseline sequence-homology screen. The transparent baseline does NOT embed hazard sequences;
|
|
245
|
+
real homology screening is delegated to a wrapped external screener (IBBIS Common Mechanism /
|
|
246
|
+
SecureDNA-style) via `external_screen`. Returns [] here (a safeguard, not a guarantee)."""
|
|
247
|
+
if self.external_enabled and seq:
|
|
248
|
+
return self.external_screen(seq)
|
|
249
|
+
return []
|
|
250
|
+
|
|
251
|
+
def external_screen(self, seq: str | None) -> list[ScreenHit]:
|
|
252
|
+
"""Wrapped external sequence screener (disabled unless an `external_hook` was supplied at load)."""
|
|
253
|
+
if not (self.external_enabled and self.external_hook and seq):
|
|
254
|
+
return []
|
|
255
|
+
return list(self.external_hook(seq))
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Sequence + function screening, the Guardian linchpin.
|
|
2
|
+
|
|
3
|
+
Runs the hazard screens over a design and returns typed, provenanced hits. Three+ screens:
|
|
4
|
+
* function_flag, toxin / controlled-function domains (the screen that catches AI-homologs),
|
|
5
|
+
* taxon_flag, regulated-pathogen taxon membership (Select Agent / Australia Group),
|
|
6
|
+
* chimera_context, hazardous assembly of individually-benign parts,
|
|
7
|
+
* sequence_homology, delegated to a wrapped external screener when enabled (baseline: no-op + a note).
|
|
8
|
+
|
|
9
|
+
Flags come ONLY from the registry / wrapped screeners, never fabricated. Screening reduces, not eliminates,
|
|
10
|
+
risk; it is a documented, audited safeguard, not a guarantee.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Literal
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
17
|
+
|
|
18
|
+
ScreenKind = Literal["sequence_homology", "function_flag", "taxon_flag", "chimera_context", "oncogenic_flag"]
|
|
19
|
+
Severity = Literal["low", "medium", "high"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ScreenHit(BaseModel):
|
|
23
|
+
"""Frozen: a hazard hit's severity cannot be downgraded after screening (a mutable `severity` could be
|
|
24
|
+
lowered to change the policy decision). Screens construct fresh hits, so immutability has no cost."""
|
|
25
|
+
model_config = ConfigDict(frozen=True)
|
|
26
|
+
|
|
27
|
+
kind: ScreenKind
|
|
28
|
+
detail: str
|
|
29
|
+
severity: Severity
|
|
30
|
+
provenance: dict = Field(default_factory=dict)
|
|
31
|
+
evidence: dict = Field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _assemble_cargo(design: dict) -> str | None:
|
|
35
|
+
"""Best-effort cargo sequence for the (optional) external homology screen. The baseline does not
|
|
36
|
+
require it, function/taxon/chimera screens read declared annotations, not raw sequence."""
|
|
37
|
+
return design.get("cargo_sequence") or design.get("cargo_seq")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def screen_design(design: dict, registry=None) -> list[ScreenHit]:
|
|
41
|
+
"""Run all hazard screens over a design. `design` is the structured proposal (declared function tags /
|
|
42
|
+
Pfam domains / source taxon / delivery / sub-designs), NOT any free-text justification (the artifact
|
|
43
|
+
decides, not the framing). Returns the list of typed hits (empty == no hazard signal)."""
|
|
44
|
+
from pen_stack.safety.registry import HazardRegistry # lazy: registry imports ScreenHit from here
|
|
45
|
+
# the default registry (no explicit override) wires the local Pfam/HMMER sequence screen as its
|
|
46
|
+
# external_hook, so a raw cargo sequence is checked against the curated toxin families too, not just a
|
|
47
|
+
# declared cargo_function. A caller wanting the bare, hookless registry still gets it via HazardRegistry.load().
|
|
48
|
+
reg = registry or HazardRegistry.default()
|
|
49
|
+
if not isinstance(design, dict):
|
|
50
|
+
design = dict(design)
|
|
51
|
+
hits: list[ScreenHit] = []
|
|
52
|
+
hits += reg.function_flags(design) # toxin / pathogen-essential function domains
|
|
53
|
+
hits += reg.oncogenic_flags(design) # oncogenic-manipulation pattern (mechanism/synonym-robust)
|
|
54
|
+
hits += reg.taxon_flags(design) # regulated pathogen taxa
|
|
55
|
+
hits += reg.chimera_context(design) # hazardous assembly of benign parts
|
|
56
|
+
seq = _assemble_cargo(design)
|
|
57
|
+
if seq:
|
|
58
|
+
hits += reg.sequence_homology(seq) # local Pfam/HMMER screen (default) or a wrapped external screener
|
|
59
|
+
return hits
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Alignment of the Guardian biosecurity screen to community synthesis-screening standards.
|
|
2
|
+
|
|
3
|
+
The Guardian is an IN-DESIGN, function/family/taxon-level screen. The community synthesis-screening standards
|
|
4
|
+
(the IBBIS Common Mechanism, SecureDNA) screen synthesis orders at the sequence level. This module maps the
|
|
5
|
+
Guardian's four screen kinds and its four decisions onto the Common Mechanism's reported categories and
|
|
6
|
+
ScreenStatus values, and reports the concordance on a labelled set, so the two interoperate. It is a
|
|
7
|
+
concordance, not a certification: the full sequence-screening pipeline is BioFirewall, and the
|
|
8
|
+
authoritative screen is the standard's own tool. The mapping never changes a Guardian decision; it only
|
|
9
|
+
expresses it in the standard's vocabulary.
|
|
10
|
+
|
|
11
|
+
References (verified):
|
|
12
|
+
IBBIS Common Mechanism for DNA Synthesis Screening (CLI: commec), IBBIS, MIT-licensed,
|
|
13
|
+
https://github.com/ibbis-bio/common-mechanism ; Wheeler et al. 2024, Applied Biosafety 29(2):71-78,
|
|
14
|
+
DOI 10.1089/apb.2023.0034.
|
|
15
|
+
SecureDNA, SecureDNA Foundation, privacy-preserving cryptographic screening; Baum et al.,
|
|
16
|
+
arXiv:2403.14023 (DOI 10.48550/arXiv.2403.14023).
|
|
17
|
+
US Federal Select Agents Program: 42 CFR 73, 7 CFR 331, 9 CFR 121 (selectagents.gov).
|
|
18
|
+
Australia Group control lists for human/animal pathogens and toxins and for plant pathogens
|
|
19
|
+
(australiagroup.net).
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from pen_stack.safety.policy import SafetyVerdict, decide
|
|
26
|
+
from pen_stack.safety.screen import screen_design
|
|
27
|
+
|
|
28
|
+
COMMON_MECHANISM = {
|
|
29
|
+
"name": "The Common Mechanism for DNA Synthesis Screening (commec)",
|
|
30
|
+
"run_by": "IBBIS (International Biosecurity and Biosafety Initiative for Science)",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repo": "https://github.com/ibbis-bio/common-mechanism",
|
|
33
|
+
"citation_doi": "10.1089/apb.2023.0034",
|
|
34
|
+
# ScreenStatus values reported by the Common Mechanism (commec/config/result.py).
|
|
35
|
+
"screen_status": ["Pass", "Warning", "Flag", "Warning (Cleared)", "Flag (Cleared)"],
|
|
36
|
+
"screen_steps": ["Biorisk Search", "Nucleotide Taxonomy Search", "Protein Taxonomy Search",
|
|
37
|
+
"Low Concern Search"],
|
|
38
|
+
# the three user-facing output categories (IBBIS FAQ).
|
|
39
|
+
"categories": [
|
|
40
|
+
"flagged sequences of concern (virulence factors and toxins)",
|
|
41
|
+
"areas of similarity to regulated pathogens",
|
|
42
|
+
"matches to genes with a known benign function",
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
SECUREDNA = {
|
|
47
|
+
"name": "SecureDNA",
|
|
48
|
+
"run_by": "SecureDNA Foundation",
|
|
49
|
+
"mechanism": "privacy-preserving cryptographic exact-match window screening (>= 30 nt)",
|
|
50
|
+
"citation_arxiv": "2403.14023",
|
|
51
|
+
"output": "binary match to deny (exemption-token override); no public per-category taxonomy, so the "
|
|
52
|
+
"Guardian aligns to it only at the pass/deny level.",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
CONTROL_LISTS = {
|
|
56
|
+
"us_select_agents": {"refs": ["42 CFR 73", "7 CFR 331", "9 CFR 121"], "url": "https://www.selectagents.gov/"},
|
|
57
|
+
"australia_group": {
|
|
58
|
+
"lists": ["List of Human and Animal Pathogens and Toxins for Export Control",
|
|
59
|
+
"List of Plant Pathogens for Export Control"],
|
|
60
|
+
"url": "https://www.australiagroup.net/",
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Guardian screen kind -> Common Mechanism step + reported category.
|
|
65
|
+
SCREEN_KIND_TO_CM = {
|
|
66
|
+
"function_flag": {"cm_step": "Biorisk Search",
|
|
67
|
+
"cm_category": "flagged sequences of concern (virulence factors and toxins)"},
|
|
68
|
+
"taxon_flag": {"cm_step": "Nucleotide/Protein Taxonomy Search",
|
|
69
|
+
"cm_category": "areas of similarity to regulated pathogens"},
|
|
70
|
+
"sequence_homology": {"cm_step": "Biorisk/Taxonomy homology",
|
|
71
|
+
"cm_category": "homology to a regulated toxin, virus or non-viral pathogen"},
|
|
72
|
+
# the Common Mechanism screens per-window sequences, not declared assemblies; chimera-context has no direct
|
|
73
|
+
# CM step and is reported as an in-design signal the per-window standard does not cover.
|
|
74
|
+
"chimera_context": {"cm_step": "(none: assembly-level, not per-window)",
|
|
75
|
+
"cm_category": "assembly-level hazard not screened by the per-window standard"},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Guardian decision -> Common Mechanism ScreenStatus (escalate maps to Warning, the CM "equally-good matches"
|
|
79
|
+
# ambiguous tie; refuse maps to Flag, the CM higher-concern hit).
|
|
80
|
+
DECISION_TO_CM_STATUS = {"clear": "Pass", "flag": "Warning", "escalate": "Warning", "refuse": "Flag"}
|
|
81
|
+
# Guardian decision -> SecureDNA order-level outcome (binary, with review for the ambiguous middle).
|
|
82
|
+
DECISION_TO_SECUREDNA = {"clear": "pass", "flag": "review", "escalate": "review", "refuse": "deny"}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def align_to_common_mechanism(safety: SafetyVerdict) -> dict[str, Any]:
|
|
86
|
+
"""Express a Guardian SafetyVerdict in the standards' vocabulary: the order-level Common Mechanism
|
|
87
|
+
ScreenStatus, the SecureDNA pass/deny outcome, and the per-hit category mapping. No decision is changed."""
|
|
88
|
+
hits = [{"guardian_kind": h.kind, "detail": h.detail, "severity": h.severity,
|
|
89
|
+
**SCREEN_KIND_TO_CM.get(h.kind, {"cm_step": "(unmapped)", "cm_category": "(unmapped)"})}
|
|
90
|
+
for h in (safety.hits or [])]
|
|
91
|
+
return {
|
|
92
|
+
"guardian_decision": safety.decision,
|
|
93
|
+
"common_mechanism_status": DECISION_TO_CM_STATUS.get(safety.decision, "Warning"),
|
|
94
|
+
"securedna_outcome": DECISION_TO_SECUREDNA.get(safety.decision, "review"),
|
|
95
|
+
"hits": hits,
|
|
96
|
+
"note": "in-design concordance, not certification; BioFirewall is the downstream "
|
|
97
|
+
"sequence-screening gate, and the standard's own tool is authoritative.",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _decision_for(design: dict) -> SafetyVerdict:
|
|
102
|
+
"""Screen a design and decide, without the audit side-effect of the full gate (this is a report path)."""
|
|
103
|
+
hits = screen_design(design)
|
|
104
|
+
decision, reason = decide(hits)
|
|
105
|
+
return SafetyVerdict(decision=decision, hits=hits, reason=reason,
|
|
106
|
+
provenance={"source": "standards.concordance"})
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def concordance_report(probes: list[dict] | None = None) -> dict[str, Any]:
|
|
110
|
+
"""Run the Guardian over a labelled set and report, verbatim, the concordance between its decisions
|
|
111
|
+
(expressed as Common Mechanism ScreenStatus) and the expected status (Pass for benign, Flag/Warning for a
|
|
112
|
+
hazard). Loads the committed safety probes when none are supplied."""
|
|
113
|
+
if probes is None:
|
|
114
|
+
import yaml
|
|
115
|
+
|
|
116
|
+
from pen_stack._resources import resource
|
|
117
|
+
raw = yaml.safe_load(resource("configs/safety/probes.yaml").read_text(encoding="utf-8"))
|
|
118
|
+
probes = [{"name": e.get("name"), "design": e["design"], "label": "benign"}
|
|
119
|
+
for e in raw.get("benign_controls", [])]
|
|
120
|
+
probes += [{"name": e.get("name"), "design": e["design"], "label": "hazard"}
|
|
121
|
+
for e in raw.get("hazard_probes", [])]
|
|
122
|
+
rows = []
|
|
123
|
+
for p in probes:
|
|
124
|
+
sv = _decision_for(p["design"])
|
|
125
|
+
cm = DECISION_TO_CM_STATUS.get(sv.decision, "Warning")
|
|
126
|
+
# benign should map to Pass; a hazard should map to a non-Pass status (Warning/Flag).
|
|
127
|
+
expected_nonpass = p["label"] == "hazard"
|
|
128
|
+
concordant = (cm != "Pass") == expected_nonpass
|
|
129
|
+
rows.append({"name": p["name"], "label": p["label"], "guardian_decision": sv.decision,
|
|
130
|
+
"common_mechanism_status": cm, "concordant": concordant})
|
|
131
|
+
n = len(rows)
|
|
132
|
+
n_conc = sum(r["concordant"] for r in rows)
|
|
133
|
+
return {
|
|
134
|
+
"n": n, "n_concordant": n_conc, "concordance": round(n_conc / n, 4) if n else None,
|
|
135
|
+
"discordances": [r for r in rows if not r["concordant"]],
|
|
136
|
+
"rows": rows,
|
|
137
|
+
"standard": COMMON_MECHANISM["name"], "standard_doi": COMMON_MECHANISM["citation_doi"],
|
|
138
|
+
"note": "Reported verbatim. A concordance between the in-design Guardian and the Common Mechanism's "
|
|
139
|
+
"reported status, not a certification; the standard's own tool remains authoritative and "
|
|
140
|
+
"BioFirewall is the downstream sequence-screening gate.",
|
|
141
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""pen_stack.score - see the PEN-STACK program doc."""
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Re-ground the scoring axes.
|
|
2
|
+
|
|
3
|
+
The prior `prog`/`cargo` axes were effectively hand-set flags (`s_prog=1.0` for everything) that
|
|
4
|
+
required per-enzyme overrides to pass any gate. Here each axis is a documented, continuous function
|
|
5
|
+
of a *measured* input read from ``configs/score_axes.yaml``. There are NO per-enzyme override
|
|
6
|
+
constants in this module - that invariant is checked by ``tests/unit/test_no_overrides.py``.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
_CFG_PATH = Path(__file__).resolve().parents[2] / "configs" / "score_axes.yaml"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_axes_config(path: str | Path = _CFG_PATH) -> dict:
|
|
20
|
+
return yaml.safe_load(Path(path).read_text(encoding="utf-8"))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def recalibrate_cargo(df: pd.DataFrame, cfg: dict) -> pd.DataFrame:
|
|
24
|
+
"""S_Cargo from measured cargo bp (monotone); fall back to upstream s_cargo if bp unknown."""
|
|
25
|
+
cap = float(cfg["cargo"]["cap_bp"])
|
|
26
|
+
out = df.copy()
|
|
27
|
+
if "cargo_capacity_bp" in out.columns:
|
|
28
|
+
bp = out["cargo_capacity_bp"].astype("float64").clip(0, cap)
|
|
29
|
+
recal = np.log1p(bp) / np.log1p(cap)
|
|
30
|
+
# only override where we actually have a measured bp; otherwise keep upstream s_cargo
|
|
31
|
+
out["S_Cargo"] = np.where(bp.notna() & (bp > 0), recal, out.get("s_cargo"))
|
|
32
|
+
else:
|
|
33
|
+
out["S_Cargo"] = out.get("s_cargo")
|
|
34
|
+
return out
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def recalibrate_prog(df: pd.DataFrame, cfg: dict) -> pd.DataFrame:
|
|
38
|
+
"""S_Prog from MEASURED targeting modality (documented anchors), not a 0/1 flag."""
|
|
39
|
+
p = cfg["programmability"]
|
|
40
|
+
anchor = p["modality_anchor"]
|
|
41
|
+
bip_fams = set(p.get("bipartite_reprogrammable_families", []))
|
|
42
|
+
bonus = float(p.get("bipartite_bonus_to", 1.0))
|
|
43
|
+
|
|
44
|
+
def _prog(row) -> float:
|
|
45
|
+
fam = row.get("family")
|
|
46
|
+
if fam in bip_fams:
|
|
47
|
+
return bonus
|
|
48
|
+
modality = row.get("targeting_modality")
|
|
49
|
+
if modality in anchor:
|
|
50
|
+
return float(anchor[modality])
|
|
51
|
+
# fall back to upstream s_prog if no modality info (documented degradation, not an override)
|
|
52
|
+
return float(row["s_prog"]) if pd.notna(row.get("s_prog")) else np.nan
|
|
53
|
+
|
|
54
|
+
out = df.copy()
|
|
55
|
+
out["S_Prog"] = out.apply(_prog, axis=1)
|
|
56
|
+
return out
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def backfill_length(df: pd.DataFrame, cfg: dict) -> pd.DataFrame:
|
|
60
|
+
"""Backfill length_aa from independently-verified UniProt lengths (upstream has all None)."""
|
|
61
|
+
table = cfg["length_aa_backfill"]
|
|
62
|
+
out = df.copy()
|
|
63
|
+
key = "entity_id" if "entity_id" in out.columns else "representative_system"
|
|
64
|
+
filled = out["length_aa"] if "length_aa" in out.columns else pd.Series([None] * len(out))
|
|
65
|
+
out["length_aa"] = [
|
|
66
|
+
(table.get(k) if (pd.isna(v) or v is None) else v)
|
|
67
|
+
for k, v in zip(out[key], filled)
|
|
68
|
+
]
|
|
69
|
+
return out
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def recalibrate_all(df: pd.DataFrame, cfg: dict | None = None) -> pd.DataFrame:
|
|
73
|
+
cfg = cfg or load_axes_config()
|
|
74
|
+
out = backfill_length(df, cfg)
|
|
75
|
+
out = recalibrate_cargo(out, cfg)
|
|
76
|
+
out = recalibrate_prog(out, cfg)
|
|
77
|
+
return out
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Therapeutic-readiness scoring across families.
|
|
2
|
+
|
|
3
|
+
The motto's "therapeutic-ready" axis, realised and *measured*: score every Writer-Atlas system for
|
|
4
|
+
deliverability, cargo capacity, immunogenicity proxy, and human-cell compatibility - using the
|
|
5
|
+
re-grounded axes (configs/score_axes.yaml is the single source of thresholds; no per-enzyme overrides).
|
|
6
|
+
All components are retained on the row (a transparent profile, never collapsed to one opaque number).
|
|
7
|
+
|
|
8
|
+
Inputs : pen_stack/atlas/atlas.parquet, configs/score_axes.yaml.
|
|
9
|
+
Outputs: atlas.parquet updated with deliv_class / S_Deliv / S_Cargo / S_HumanCell / readiness.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
|
|
18
|
+
from pen_stack.score.recalibrate import load_axes_config
|
|
19
|
+
|
|
20
|
+
_ROOT = Path(__file__).resolve().parents[2]
|
|
21
|
+
_ATLAS = _ROOT / "pen_stack" / "atlas" / "atlas.parquet"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def deliverability_class(length_aa: float | None, cfg: dict) -> str:
|
|
25
|
+
"""AAV single (<=~730 aa effector) / split-AAV (<=1500) / mRNA-RNP, from effector size."""
|
|
26
|
+
d = cfg["deliverability"]
|
|
27
|
+
if length_aa is None or (isinstance(length_aa, float) and np.isnan(length_aa)):
|
|
28
|
+
return "unknown"
|
|
29
|
+
L = float(length_aa)
|
|
30
|
+
if L <= d["aav_single_max_aa"]:
|
|
31
|
+
return "AAV"
|
|
32
|
+
if L <= d["split_aav_max_aa"]:
|
|
33
|
+
return "split-AAV"
|
|
34
|
+
return "mRNA-RNP"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _s_cargo(bp, cfg) -> float:
|
|
38
|
+
cap = float(cfg["cargo"]["cap_bp"])
|
|
39
|
+
if bp is None or (isinstance(bp, float) and np.isnan(bp)) or bp <= 0:
|
|
40
|
+
return np.nan
|
|
41
|
+
return float(np.log1p(min(float(bp), cap)) / np.log1p(cap))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _s_humancell(hca: str | None) -> float:
|
|
45
|
+
"""Coarse human-cell compatibility from the curated activity string (measured > demonstrated > none)."""
|
|
46
|
+
t = (hca or "").lower()
|
|
47
|
+
if "not measured" in t or "bacterial" in t:
|
|
48
|
+
return 0.0
|
|
49
|
+
if "low in human" in t or "modest" in t:
|
|
50
|
+
return 0.4
|
|
51
|
+
if "human cell" in t or "human cells" in t or "primary t cell" in t or "hepatocyte" in t or "clinical" in t:
|
|
52
|
+
return 1.0
|
|
53
|
+
return np.nan
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def therapeutic_profile(atlas_df: pd.DataFrame, cfg: dict | None = None) -> pd.DataFrame:
|
|
57
|
+
cfg = cfg or load_axes_config()
|
|
58
|
+
df = atlas_df.copy()
|
|
59
|
+
classes = cfg["deliverability"]["classes"]
|
|
60
|
+
|
|
61
|
+
df["deliv_class"] = df["length_aa"].apply(lambda L: deliverability_class(L, cfg))
|
|
62
|
+
df["S_Deliv"] = df["deliv_class"].map(classes) # unknown -> NaN
|
|
63
|
+
df["S_Cargo"] = df["cargo_capacity_bp"].apply(lambda bp: _s_cargo(bp, cfg))
|
|
64
|
+
df["S_HumanCell"] = df["human_cell_activity"].apply(_s_humancell)
|
|
65
|
+
df["S_DSBfree"] = df["dsb_free"].apply(lambda b: 1.0 if b is True else (0.0 if b is False else np.nan))
|
|
66
|
+
|
|
67
|
+
# transparent composite (mean of available components); components remain on the row
|
|
68
|
+
comp = df[["S_Deliv", "S_Cargo", "S_HumanCell", "S_DSBfree"]]
|
|
69
|
+
df["readiness"] = comp.mean(axis=1, skipna=True)
|
|
70
|
+
return df
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def apply_to_atlas(atlas_parquet: str | Path = _ATLAS, out: str | Path = _ATLAS) -> pd.DataFrame:
|
|
74
|
+
atlas = pd.read_parquet(atlas_parquet)
|
|
75
|
+
out_df = therapeutic_profile(atlas)
|
|
76
|
+
out_df.to_parquet(out, index=False)
|
|
77
|
+
return out_df
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__": # pragma: no cover
|
|
81
|
+
a = apply_to_atlas()
|
|
82
|
+
cores = a[a.entry_kind == "curated_core"]
|
|
83
|
+
print(cores[["representative_system", "length_aa", "deliv_class", "S_Deliv",
|
|
84
|
+
"S_Cargo", "S_HumanCell", "readiness"]].to_string(index=False))
|
|
85
|
+
print("\ndeliv_class distribution:\n", a["deliv_class"].value_counts(dropna=False))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""pen_stack.server - see the PEN-STACK program doc."""
|