pen-stack 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. pen_stack/__init__.py +2 -0
  2. pen_stack/_resources.py +34 -0
  3. pen_stack/active/__init__.py +20 -0
  4. pen_stack/active/acquire.py +165 -0
  5. pen_stack/active/brains.py +74 -0
  6. pen_stack/active/campaign.py +109 -0
  7. pen_stack/active/design.py +66 -0
  8. pen_stack/active/validate.py +104 -0
  9. pen_stack/adapt/__init__.py +14 -0
  10. pen_stack/adapt/finetune.py +33 -0
  11. pen_stack/adapt/ingest.py +86 -0
  12. pen_stack/adapt/pipeline.py +101 -0
  13. pen_stack/adapt/recalibrate.py +58 -0
  14. pen_stack/adapt/report.py +130 -0
  15. pen_stack/agent/__init__.py +1 -0
  16. pen_stack/agent/cite.py +175 -0
  17. pen_stack/agent/co_scientist.py +262 -0
  18. pen_stack/agent/epistemic.py +102 -0
  19. pen_stack/agent/guardrails.py +67 -0
  20. pen_stack/agent/mcp_server.py +215 -0
  21. pen_stack/agent/orchestrator.py +112 -0
  22. pen_stack/agent/orchestrator_live.py +56 -0
  23. pen_stack/agent/pen_agent.py +242 -0
  24. pen_stack/agent/scope.py +60 -0
  25. pen_stack/agent/tools.py +130 -0
  26. pen_stack/api/__init__.py +12 -0
  27. pen_stack/api/manifest.py +160 -0
  28. pen_stack/atlas/__init__.py +1 -0
  29. pen_stack/atlas/atlas.parquet +0 -0
  30. pen_stack/atlas/build_wtkb.py +80 -0
  31. pen_stack/atlas/crosslink.py +179 -0
  32. pen_stack/atlas/expand.py +190 -0
  33. pen_stack/atlas/guide_design.py +178 -0
  34. pen_stack/atlas/schema.py +59 -0
  35. pen_stack/atlas/scorecard.py +134 -0
  36. pen_stack/atlas/scorecard_v3.parquet +0 -0
  37. pen_stack/atlas/universe.py +75 -0
  38. pen_stack/atlas/universe_v3.parquet +0 -0
  39. pen_stack/atlas/variant_propose.py +155 -0
  40. pen_stack/atlas/writer_efficiency.py +184 -0
  41. pen_stack/atlas/writer_predict.py +229 -0
  42. pen_stack/atlas/writer_recommend.py +170 -0
  43. pen_stack/atlas/writer_verify.py +167 -0
  44. pen_stack/atlas/wtkb.parquet +0 -0
  45. pen_stack/bridge/__init__.py +1 -0
  46. pen_stack/bridge/activity.py +52 -0
  47. pen_stack/bridge/cli.py +65 -0
  48. pen_stack/bridge/fold_qc.py +53 -0
  49. pen_stack/bridge/guide_qc.py +87 -0
  50. pen_stack/bridge/ingest.py +139 -0
  51. pen_stack/bridge/offtarget.py +191 -0
  52. pen_stack/bridge/offtarget_energetics.py +105 -0
  53. pen_stack/bridge/ortholog_screen.py +73 -0
  54. pen_stack/bridge/pipeline.py +83 -0
  55. pen_stack/build/__init__.py +16 -0
  56. pen_stack/build/cloudlab.py +74 -0
  57. pen_stack/build/ingest.py +47 -0
  58. pen_stack/build/protocol.py +82 -0
  59. pen_stack/build/simlab.py +30 -0
  60. pen_stack/cli.py +126 -0
  61. pen_stack/data/__init__.py +1 -0
  62. pen_stack/data/encode.py +84 -0
  63. pen_stack/data/genome.py +71 -0
  64. pen_stack/data/ingest_chromatin.py +119 -0
  65. pen_stack/data/ingest_integration.py +112 -0
  66. pen_stack/data/ingest_safety_annot.py +201 -0
  67. pen_stack/data/ingest_trip.py +76 -0
  68. pen_stack/design/__init__.py +14 -0
  69. pen_stack/design/capsid_generate.py +62 -0
  70. pen_stack/design/generate.py +70 -0
  71. pen_stack/design/pareto.py +70 -0
  72. pen_stack/design/space.py +137 -0
  73. pen_stack/design/writer_variants.py +121 -0
  74. pen_stack/env/__init__.py +1 -0
  75. pen_stack/env/genome_writing_env.py +248 -0
  76. pen_stack/env/policies.py +94 -0
  77. pen_stack/graph/__init__.py +21 -0
  78. pen_stack/graph/build.py +133 -0
  79. pen_stack/graph/cell_types.py +58 -0
  80. pen_stack/graph/ingest.py +132 -0
  81. pen_stack/graph/query.py +148 -0
  82. pen_stack/graph/schema.py +100 -0
  83. pen_stack/loop/__init__.py +15 -0
  84. pen_stack/loop/continual.py +61 -0
  85. pen_stack/loop/cycle.py +84 -0
  86. pen_stack/loop/drift.py +41 -0
  87. pen_stack/mech/__init__.py +1 -0
  88. pen_stack/mech/classify_atlas.py +71 -0
  89. pen_stack/mech/pfam_whitelist.yaml +247 -0
  90. pen_stack/mech/whitelist.py +66 -0
  91. pen_stack/monitor/__init__.py +1 -0
  92. pen_stack/monitor/europepmc.py +32 -0
  93. pen_stack/monitor/run.py +57 -0
  94. pen_stack/monitor/triage.py +63 -0
  95. pen_stack/oracles/__init__.py +65 -0
  96. pen_stack/oracles/affinity.py +116 -0
  97. pen_stack/oracles/cache.py +53 -0
  98. pen_stack/oracles/energetics.py +33 -0
  99. pen_stack/oracles/genome.py +167 -0
  100. pen_stack/oracles/protein_design.py +136 -0
  101. pen_stack/oracles/reliability.py +64 -0
  102. pen_stack/oracles/rna.py +28 -0
  103. pen_stack/oracles/schema.py +77 -0
  104. pen_stack/oracles/status.py +123 -0
  105. pen_stack/oracles/structure.py +42 -0
  106. pen_stack/oracles/structure_run.py +76 -0
  107. pen_stack/oracles/vcell.py +74 -0
  108. pen_stack/planner/__init__.py +1 -0
  109. pen_stack/planner/ada_risk.py +64 -0
  110. pen_stack/planner/antipeg_oracle.py +75 -0
  111. pen_stack/planner/capsid_epitope_oracle.py +135 -0
  112. pen_stack/planner/cargo.py +56 -0
  113. pen_stack/planner/cargo_polish.py +146 -0
  114. pen_stack/planner/chromosome.py +106 -0
  115. pen_stack/planner/delivery.py +55 -0
  116. pen_stack/planner/delivery_constraints.py +110 -0
  117. pen_stack/planner/delivery_immune.py +61 -0
  118. pen_stack/planner/delivery_immunology.py +222 -0
  119. pen_stack/planner/delivery_predict.py +196 -0
  120. pen_stack/planner/delivery_vehicles.py +37 -0
  121. pen_stack/planner/genotoxicity_oracle.py +112 -0
  122. pen_stack/planner/immune_mhc2.py +154 -0
  123. pen_stack/planner/immune_profile.py +292 -0
  124. pen_stack/planner/innate_sensing.py +135 -0
  125. pen_stack/planner/multiplex.py +110 -0
  126. pen_stack/planner/optimize.py +278 -0
  127. pen_stack/planner/pipeline.py +87 -0
  128. pen_stack/planner/report.py +26 -0
  129. pen_stack/planner/router.py +57 -0
  130. pen_stack/planner/seroprevalence_oracle.py +92 -0
  131. pen_stack/planner/target_site.py +118 -0
  132. pen_stack/rag/__init__.py +1 -0
  133. pen_stack/rag/corpus.py +133 -0
  134. pen_stack/rag/embed.py +98 -0
  135. pen_stack/rag/ground.py +131 -0
  136. pen_stack/rag/index.py +53 -0
  137. pen_stack/rag/llm.py +215 -0
  138. pen_stack/rag/qa.py +105 -0
  139. pen_stack/rag/retrieve.py +48 -0
  140. pen_stack/rules/__init__.py +9 -0
  141. pen_stack/rules/evaluators.py +318 -0
  142. pen_stack/rules/loader.py +31 -0
  143. pen_stack/rules/schema.py +99 -0
  144. pen_stack/rules/solver.py +43 -0
  145. pen_stack/rules/spec.py +78 -0
  146. pen_stack/safety/__init__.py +21 -0
  147. pen_stack/safety/audit.py +90 -0
  148. pen_stack/safety/gate.py +58 -0
  149. pen_stack/safety/pfam_scan.py +157 -0
  150. pen_stack/safety/policy.py +69 -0
  151. pen_stack/safety/redteam.py +71 -0
  152. pen_stack/safety/registry.py +255 -0
  153. pen_stack/safety/screen.py +59 -0
  154. pen_stack/safety/standards.py +141 -0
  155. pen_stack/score/__init__.py +1 -0
  156. pen_stack/score/recalibrate.py +77 -0
  157. pen_stack/score/therapeutic.py +85 -0
  158. pen_stack/server/__init__.py +1 -0
  159. pen_stack/server/api.py +647 -0
  160. pen_stack/spec/__init__.py +18 -0
  161. pen_stack/spec/clarify.py +42 -0
  162. pen_stack/spec/extract.py +406 -0
  163. pen_stack/spec/resolvers/__init__.py +17 -0
  164. pen_stack/spec/resolvers/cell.py +51 -0
  165. pen_stack/spec/resolvers/chem.py +32 -0
  166. pen_stack/spec/resolvers/feature.py +36 -0
  167. pen_stack/spec/resolvers/gene.py +43 -0
  168. pen_stack/spec/resolvers/locus.py +29 -0
  169. pen_stack/spec/resolvers/phenotype.py +37 -0
  170. pen_stack/spec/satisfy.py +114 -0
  171. pen_stack/spec/service.py +33 -0
  172. pen_stack/spec/writespec.py +252 -0
  173. pen_stack/twin/__init__.py +14 -0
  174. pen_stack/twin/calibrate.py +61 -0
  175. pen_stack/twin/data/__init__.py +12 -0
  176. pen_stack/twin/data/position_effect.py +245 -0
  177. pen_stack/twin/mechanistic.py +147 -0
  178. pen_stack/twin/outcome.py +132 -0
  179. pen_stack/twin/position_effect.py +454 -0
  180. pen_stack/ui/__init__.py +1 -0
  181. pen_stack/ui/app.py +713 -0
  182. pen_stack/validate/__init__.py +1 -0
  183. pen_stack/validate/adapt_demo.py +69 -0
  184. pen_stack/validate/agent_eval.py +117 -0
  185. pen_stack/validate/bench_adversarial_tasks.py +118 -0
  186. pen_stack/validate/bench_coscientist_tasks.py +60 -0
  187. pen_stack/validate/bench_graph_tasks.py +64 -0
  188. pen_stack/validate/bench_rule_tasks.py +84 -0
  189. pen_stack/validate/bench_trust_tasks.py +92 -0
  190. pen_stack/validate/bench_writetype_tasks.py +101 -0
  191. pen_stack/validate/blind_gsh_discovery.py +261 -0
  192. pen_stack/validate/cargo_directionality.py +57 -0
  193. pen_stack/validate/closed_loop.py +63 -0
  194. pen_stack/validate/durability_baselines.py +185 -0
  195. pen_stack/validate/experiment_design.py +65 -0
  196. pen_stack/validate/expr_controls.py +39 -0
  197. pen_stack/validate/forward_hypotheses.py +104 -0
  198. pen_stack/validate/generative_design.py +62 -0
  199. pen_stack/validate/guide_qc_demo.py +69 -0
  200. pen_stack/validate/heldout_celltype_expr.py +32 -0
  201. pen_stack/validate/immune_calibration.py +133 -0
  202. pen_stack/validate/intent_specification.py +82 -0
  203. pen_stack/validate/known_biology_expr.py +38 -0
  204. pen_stack/validate/offtarget_energetics_eval.py +144 -0
  205. pen_stack/validate/out_of_scope_refusal.py +82 -0
  206. pen_stack/validate/outcome_calibration.py +194 -0
  207. pen_stack/validate/outcome_prediction.py +76 -0
  208. pen_stack/validate/paper3_benchmark.py +165 -0
  209. pen_stack/validate/paper4_real_validation.py +144 -0
  210. pen_stack/validate/paper4_validation.py +82 -0
  211. pen_stack/validate/protocol_safety.py +62 -0
  212. pen_stack/validate/safety_screening.py +72 -0
  213. pen_stack/validate/selective_prediction.py +104 -0
  214. pen_stack/validate/seq_vs_measured.py +134 -0
  215. pen_stack/validate/target_site_controls.py +65 -0
  216. pen_stack/validate/uncertainty_eval.py +244 -0
  217. pen_stack/validate/ungrounded_baseline.py +234 -0
  218. pen_stack/validate/within_locus_ranking.py +84 -0
  219. pen_stack/validate/writer_recovery.py +91 -0
  220. pen_stack/verify/__init__.py +5 -0
  221. pen_stack/verify/proof.py +206 -0
  222. pen_stack/verify/schema.py +53 -0
  223. pen_stack/verify/service.py +191 -0
  224. pen_stack/web/__init__.py +18 -0
  225. pen_stack/web/guide.py +110 -0
  226. pen_stack/web/llm.py +393 -0
  227. pen_stack/web/llm_provider.py +119 -0
  228. pen_stack/web/router.py +119 -0
  229. pen_stack/web/server.py +96 -0
  230. pen_stack/web/tools.py +197 -0
  231. pen_stack/wgenome/__init__.py +1 -0
  232. pen_stack/wgenome/chromatin_seq.py +83 -0
  233. pen_stack/wgenome/durability.py +108 -0
  234. pen_stack/wgenome/export_tracks.py +52 -0
  235. pen_stack/wgenome/features.py +82 -0
  236. pen_stack/wgenome/genotoxic_blocklist.py +88 -0
  237. pen_stack/wgenome/gsh_baseline.py +154 -0
  238. pen_stack/wgenome/mesh_features.py +61 -0
  239. pen_stack/wgenome/offtarget_assay.py +80 -0
  240. pen_stack/wgenome/offtarget_bridge.py +47 -0
  241. pen_stack/wgenome/offtarget_cast.py +97 -0
  242. pen_stack/wgenome/offtarget_data.py +148 -0
  243. pen_stack/wgenome/offtarget_enumerate.py +274 -0
  244. pen_stack/wgenome/offtarget_integrase.py +155 -0
  245. pen_stack/wgenome/offtarget_nuclease.py +123 -0
  246. pen_stack/wgenome/offtarget_paste.py +41 -0
  247. pen_stack/wgenome/offtarget_predict.py +282 -0
  248. pen_stack/wgenome/ood.py +135 -0
  249. pen_stack/wgenome/providers.py +278 -0
  250. pen_stack/wgenome/safety.py +69 -0
  251. pen_stack/wgenome/structure3d.py +212 -0
  252. pen_stack/wgenome/uncertainty.py +250 -0
  253. pen_stack/wgenome/writability.py +72 -0
  254. pen_stack-0.1.0.dist-info/METADATA +401 -0
  255. pen_stack-0.1.0.dist-info/RECORD +259 -0
  256. pen_stack-0.1.0.dist-info/WHEEL +5 -0
  257. pen_stack-0.1.0.dist-info/entry_points.txt +3 -0
  258. pen_stack-0.1.0.dist-info/licenses/LICENSE +21 -0
  259. pen_stack-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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."""