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,160 @@
1
+ """Capability + scope manifests, the self-describing AI contract.
2
+
3
+ The differentiator: PEN-STACK describes not only WHAT it can do (capability manifest) but, uniquely,
4
+ machine-readably WHAT IT REFUSES TO ANSWER (scope manifest: the known-unknowns registry + the oracle scope
5
+ cards). An external agent fetches both and ROUTES on them, instead of reading prose. Both are generated from the
6
+ live registry / scope cards (never hand-written), versioned under the public API stability commitment.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ import pen_stack
13
+
14
+ # the stable public tools (the 1.0 surface). Each is a thin contract over a validated entry point; none fabricate.
15
+ _TOOLS = [
16
+ {"name": "verify_write", "summary": "legality + safety + calibrated confidence + immune profile for a design",
17
+ "input": "Design", "output": "Verdict", "entrypoint": "pen_stack.verify.verify", "fabricates": False},
18
+ {"name": "verify_proof", "summary": "repair-oriented proof object: legality / confidence / biosecurity as "
19
+ "separate axes with repair hints (never collapsed)",
20
+ "input": "Design", "output": "Proof", "entrypoint": "pen_stack.verify.proof.verify_proof",
21
+ "fabricates": False},
22
+ {"name": "safety_screen", "summary": "biosecurity / dual-use gate (clear/flag/escalate/refuse)",
23
+ "input": "Design", "output": "SafetyVerdict", "entrypoint": "pen_stack.safety.safety_gate",
24
+ "fabricates": False},
25
+ {"name": "generate_designs", "summary": "grounded candidate writing systems (verifier-as-discriminator)",
26
+ "input": "Goal | candidates", "output": "list[Design(candidate)]",
27
+ "entrypoint": "pen_stack.design.generate_designs", "fabricates": False},
28
+ {"name": "pareto_front", "summary": "non-dominated tradeoff frontier (incl. grounded immune-risk axis)",
29
+ "input": "list[Design]", "output": "list[Design(candidate)]",
30
+ "entrypoint": "pen_stack.design.pareto_front", "fabricates": False},
31
+ {"name": "predict_outcome", "summary": "calibrated, OOD-gated, phenotype-bounded write outcome",
32
+ "input": "Design + cell_state", "output": "Outcome(candidate)",
33
+ "entrypoint": "pen_stack.twin.predict_outcome", "fabricates": False},
34
+ {"name": "immune_profile", "summary": "per-axis immune-risk screen (never collapsed into one number)",
35
+ "input": "Design", "output": "ImmuneProfile", "entrypoint": "pen_stack.planner.immune_profile.immune_profile",
36
+ "fabricates": False},
37
+ {"name": "suggest_experiment", "summary": "active-learning next-experiment batch (EIG + immune-VOI)",
38
+ "input": "Designs + cell_state", "output": "ExperimentBatch",
39
+ "entrypoint": "pen_stack.active.select_batch", "fabricates": False},
40
+ {"name": "co_scientist_session", "summary": "drive the full loop: strategies + outcomes + experiments + safety",
41
+ "input": "Goal + cell_state", "output": "Session", "entrypoint": "pen_stack.agent.co_scientist.co_scientist_session",
42
+ "fabricates": False},
43
+ {"name": "run_loop", "summary": "one gated DBTL loop (autonomy Level 3, human in control)",
44
+ "input": "Goal + cell_state", "output": "LoopHistory", "entrypoint": "pen_stack.loop.run_loop",
45
+ "fabricates": False},
46
+ {"name": "challenge_evaluate", "summary": "score a submission on a held-out Genome-Writing Challenge round",
47
+ "input": "Submission + round_id", "output": "ChallengeResult",
48
+ "entrypoint": "benchmarks.genome_writing_challenge.harness.evaluate", "fabricates": False},
49
+ {"name": "recommend_writers", "summary": "rank writer families (KB-grounded primary) + candidate predicted "
50
+ "efficiency w/ conformal interval + auto-designed guide/att",
51
+ "input": "write request (write-type, cargo, cell type, optional target/donor seq)", "output": "WriterRanking",
52
+ "entrypoint": "pen_stack.atlas.writer_recommend.recommend_writers", "fabricates": False},
53
+ {"name": "nominate_offtargets", "summary": "genome-wide, per-mechanism off-target FINDER: "
54
+ "enumerates the off-target set over GRCh38 (Cas-OFFinder) and applies the correct mechanism per writer class "
55
+ "- nuclease cleavage (validated: CRISOT + mismatch-calibrated risk; chromatin accessibility is a validated "
56
+ "annotation surfaced when the accessibility track is mounted, not a re-ranker), integrase pseudo-attP (semi-validated), bridge "
57
+ "target-specificity (unvalidated), CAST guide + untargeted transposition (unvalidated), PASTE composition - "
58
+ "each with a truthful status label; nomination is NOT a clearance",
59
+ "input": "writer family + guide/target (enumerated genome-wide) or supplied candidate sites",
60
+ "output": "genome-wide ranked off-target candidates + per-mechanism status + calibrated risk + validation assay",
61
+ "entrypoint": "pen_stack.wgenome.offtarget_predict.nominate_offtargets", "fabricates": False},
62
+ {"name": "recommend_delivery", "summary": "cross-modality delivery recommender: rank vehicles by cargo-form + "
63
+ "safety<->efficacy + a grounded serotype->tissue tropism prior (approved therapies) + the learned FLIP-AAV "
64
+ "capsid-fitness capability; tropism is a known-unknown for novel capsids",
65
+ "input": "cargo form/size + target tissue (+ safety weight, in/ex-vivo)",
66
+ "output": "ranked vehicles + serotype tropism prior + capsid-fitness bench; never fabricates tropism",
67
+ "entrypoint": "pen_stack.planner.delivery_predict.recommend_delivery_plus", "fabricates": False},
68
+ {"name": "capsid_fitness", "summary": "learned AAV capsid packaging-fitness for a VP1 sequence (FLIP-AAV-"
69
+ "trained; beats a mutation-burden baseline on held-out splits); a CANDIDATE for the measured packaging axis, "
70
+ "NOT an in-vivo tropism claim",
71
+ "input": "AAV VP1 capsid amino-acid sequence", "output": "predicted packaging-fitness (candidate) or abstain",
72
+ "entrypoint": "pen_stack.planner.delivery_predict.capsid_fitness", "fabricates": False},
73
+ {"name": "validation_campaign", "summary": "the validation-campaign engine: the next batch of (cassette x "
74
+ "locus x cell type) expression measurements ordered by expected information gain, the calibrate_axis gate it "
75
+ "targets (the path to the program's first outcome-validated axis), and the active-vs-random result reported "
76
+ "verbatim; cloud-lab-executable, Level 3, experiments are candidates",
77
+ "input": "none", "output": "the expression-validation campaign (batch + target gate + acquisition result)",
78
+ "entrypoint": "pen_stack.active.campaign.design_campaign", "fabricates": False},
79
+ {"name": "cloudlab_submit", "summary": "safety-gated cloud-lab submission: the biosecurity gate runs BEFORE "
80
+ "submission, a flagged design returns a structured refusal (no protocol emitted), a cleared design returns a "
81
+ "mock/dry-run job receipt; Level 3, human in control",
82
+ "input": "a design (+ optional experiment)", "output": "a mock job receipt or a structured biosecurity "
83
+ "refusal", "entrypoint": "pen_stack.build.cloudlab.submit_gated", "fabricates": False},
84
+ {"name": "writespec_parse", "summary": "parse a plain-language genome-writing request into a typed, "
85
+ "ontology-backed WriteSpec (an SBOL3 profile): per-field provenance (explicit/inferred/user/unresolved), "
86
+ "assumptions, clarifying questions on underspecification, and a feasibility verdict (reachability + "
87
+ "deliverability + legality); a REQUEST not a claim, never fabricates intent",
88
+ "input": "prose request (+ optional structured overrides)", "output": "typed WriteSpec + clarifications + "
89
+ "feasibility; unresolved stays null", "entrypoint": "pen_stack.spec.service.parse_request", "fabricates": False},
90
+ {"name": "oracle_query", "summary": "query the oracle mesh under one contract: per-oracle execution + latency "
91
+ "+ live status + PUBLISHED reliability (verbatim from public benchmarks, cited) + disagreement-to-interval; "
92
+ "or a CANDIDATE protein-ligand binding affinity (Boltz-2 head) with native uncertainty, cache-or-abstain",
93
+ "input": "oracle name, or {protein_seq, ligand_smiles, pair_type}", "output": "oracle status + reliability, "
94
+ "or a candidate affinity (binder probability + value) or abstain; protein-protein/protein-DNA flagged OOD",
95
+ "entrypoint": "pen_stack.oracles.affinity.predict_affinity", "fabricates": False},
96
+ {"name": "chat_answer", "summary": "the grounded conversational co-scientist: a provenance-"
97
+ "partitioned 4-lane agent (design/explain/meta = engine-grounded with the guard ON; general = retrieval-"
98
+ "grounded over a provenance-tagged corpus under citation-or-silence, abstaining below a retrieval-confidence "
99
+ "threshold). The LLM narrates and is swappable; it NEVER originates a claim or a number. Measured by the "
100
+ "chat_routing / chat_grounding / chat_safety benchmarks (routing-safety ~0, citation coverage 1.0, "
101
+ "false-grounding ~0)",
102
+ "input": "{message, history?, allow_llm?}", "output": "{reply, mode (lane), provenance, grounded, sources?, "
103
+ "backend}; general answers are 'literature-cited' or 'abstained', never a PEN-STACK-computed result",
104
+ "entrypoint": "pen_stack.web.llm.grounded_reply", "fabricates": False},
105
+ ]
106
+
107
+ _POLICY = ("outputs outside scope are returned as `out_of_scope` (known-unknown) or `extrapolating` (OOD) and are "
108
+ "NEVER asserted; every number is tool-sourced (no fabrication); contracts are versioned + "
109
+ "deprecation-policed under the public API stability commitment (docs/STABILITY.md).")
110
+
111
+
112
+ def capability_manifest() -> dict[str, Any]:
113
+ """Machine-readable: WHAT PEN-STACK can do. An agent routes on this, not on prose."""
114
+ return {
115
+ "name": "pen-stack", "version": pen_stack.__version__, "stability": "stable",
116
+ "contract_version": "0.1.0",
117
+ "guarantees": ["rule-grounded legality", "calibrated confidence", "explicit scope / known-unknowns",
118
+ "biosecurity safety gate", "no fabrication"],
119
+ "tools": _TOOLS,
120
+ "oracles": _oracle_summary(),
121
+ "surfaces": {"rest": "/capabilities, /scope, /oracles, /verify, /generate, /predict, /immune, /safety, "
122
+ "/suggest, /session, /openapi.json", "mcp": "pen_stack.agent.mcp_server",
123
+ "challenge": "benchmarks/genome_writing_challenge"},
124
+ }
125
+
126
+
127
+ def _oracle_summary() -> dict:
128
+ """Live-oracle roll-up: which foundation models execute live, their latency class, what is held/deferred."""
129
+ from pen_stack.oracles.status import summary
130
+ return summary()
131
+
132
+
133
+ def _known_unknowns() -> list[dict]:
134
+ """The known-unknowns as PUBLIC data (id/title/requires/why), internal matcher fields are not exposed."""
135
+ from pen_stack.agent.scope import load_registry
136
+ return [{"id": e["id"], "title": e["title"], "requires": e.get("requires"),
137
+ "why": " ".join((e.get("why") or "").split())} for e in load_registry()]
138
+
139
+
140
+ def _oracle_scope_cards() -> list[dict]:
141
+ """Each wrapped model's in-distribution envelope (what it is valid for, and what it is NOT)."""
142
+ from pen_stack.oracles.cache import load_scope_cards
143
+ out = []
144
+ for model, c in load_scope_cards().items():
145
+ out.append({"model": model, "family": c.get("family"), "version": c.get("version"),
146
+ "output_kind": c.get("output_kind"),
147
+ "valid_for": " ".join((c.get("valid_for") or "").split()),
148
+ "not_valid_for": " ".join((c.get("not_valid_for") or "").split()),
149
+ "generalizes_to_unseen_loci": c.get("generalizes_to_unseen_loci")})
150
+ return out
151
+
152
+
153
+ def scope_manifest() -> dict[str, Any]:
154
+ """Machine-readable: WHAT PEN-STACK REFUSES TO ANSWER. The contract that makes depending on it safe."""
155
+ return {
156
+ "name": "pen-stack", "version": pen_stack.__version__, "contract_version": "0.1.0",
157
+ "known_unknowns": _known_unknowns(),
158
+ "oracle_scope_cards": _oracle_scope_cards(),
159
+ "policy": _POLICY,
160
+ }
@@ -0,0 +1 @@
1
+ """pen_stack.atlas - see the PEN-STACK program doc."""
Binary file
@@ -0,0 +1,80 @@
1
+ """Build the Writer-Targeting Knowledge Base (WT-KB).
2
+
3
+ Reads the curated YAML (one block per writer family), validates every row against the
4
+ ``WriterEntry`` schema (which enforces the sourcing rule: >=1 DOI per row), and emits both a
5
+ parquet (for the pipeline) and a human-readable markdown table (for literature cross-check).
6
+
7
+ Usage:
8
+ python -m pen_stack.atlas.build_wtkb --curated configs/wtkb_curated.yaml \
9
+ --out pen_stack/atlas/wtkb.parquet --md docs/wtkb.md
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ from pathlib import Path
15
+
16
+ import pandas as pd
17
+ import yaml
18
+
19
+ from pen_stack.atlas.schema import WriterEntry
20
+
21
+
22
+ def build(curated_yaml: str, out_parquet: str | None = None, out_md: str | None = None) -> pd.DataFrame:
23
+ curated = yaml.safe_load(Path(curated_yaml).read_text(encoding="utf-8"))
24
+ rows = []
25
+ for key, block in curated.items():
26
+ entry = WriterEntry(**block) # validates (raises on missing DOI / bad enum)
27
+ d = entry.model_dump()
28
+ d["_key"] = key
29
+ rows.append(d)
30
+ df = pd.DataFrame(rows)
31
+ # stable column order, _key first
32
+ cols = ["_key"] + [c for c in df.columns if c != "_key"]
33
+ df = df[cols]
34
+ if out_parquet:
35
+ Path(out_parquet).parent.mkdir(parents=True, exist_ok=True)
36
+ df.to_parquet(out_parquet, index=False)
37
+ if out_md:
38
+ Path(out_md).parent.mkdir(parents=True, exist_ok=True)
39
+ Path(out_md).write_text(_to_markdown(df), encoding="utf-8")
40
+ return df
41
+
42
+
43
+ def _to_markdown(df: pd.DataFrame) -> str:
44
+ lines = [
45
+ "# Writer-Targeting Knowledge Base (WT-KB)",
46
+ "",
47
+ f"_Generated from `configs/wtkb_curated.yaml` - {len(df)} writer families. "
48
+ "Every row is schema-validated and carries >=1 DOI (sourcing rule)._",
49
+ "",
50
+ "| Family | Representative | Mechanism | Modality | Target site | Tier | Confidence | DOIs |",
51
+ "|---|---|---|---|---|---|---|---|",
52
+ ]
53
+ for _, r in df.iterrows():
54
+ dois = "; ".join(r["key_dois"])
55
+ lines.append(
56
+ f"| {r['family']} | {r['representative_system']} | {r['mechanism_bucket']} | "
57
+ f"{r['targeting_modality']} | {r['target_site_spec']} | {r['reachability_tier']} | "
58
+ f"{r['confidence']} | {dois} |"
59
+ )
60
+ lines += ["", "## Reachability constraints (per family)", ""]
61
+ for _, r in df.iterrows():
62
+ lines.append(f"- **{r['family']}** ({r['reachability_tier']}): {r['reachability_constraints']}")
63
+ return "\n".join(lines) + "\n"
64
+
65
+
66
+ def main() -> None:
67
+ ap = argparse.ArgumentParser()
68
+ ap.add_argument("--curated", default="configs/wtkb_curated.yaml")
69
+ ap.add_argument("--out", default="pen_stack/atlas/wtkb.parquet")
70
+ ap.add_argument("--md", default="docs/wtkb.md")
71
+ a = ap.parse_args()
72
+ df = build(a.curated, a.out, a.md)
73
+ print(f"WT-KB built: {len(df)} families -> {a.out}")
74
+ tiers = df["reachability_tier"].value_counts().to_dict()
75
+ print(f"tiers: {tiers}")
76
+ print(f"fully-specified families: {len(df)} (target >=6)")
77
+
78
+
79
+ if __name__ == "__main__":
80
+ main()
@@ -0,0 +1,179 @@
1
+ """Cross-link the Writer Atlas <-> the Writable Genome.
2
+
3
+ The integration that makes PEN-STACK more than a catalogue: bidirectional queries between writers (the
4
+ atlas, 33k systems by family) and loci (the Writable Genome, 3M bins x cell type with a
5
+ ``reachable_tier1`` annotation + a decomposable ``writability`` score).
6
+
7
+ - ``loci_for_writer(family, ct)`` -> loci that family can reach, ranked by writability.
8
+ - ``writers_for_locus(chrom, bin)`` -> atlas systems whose family reaches that locus, with readiness.
9
+ - ``loci_for_gene(gene, ct)`` -> writable bins overlapping a gene (forward query helper).
10
+
11
+ Scope: reachability is released at the *locus* level - the Tier-1
12
+ reprogrammable families (bridge_IS110 / Cas9 / Cas12a) are near-universal at 1 kb, so the cross-link's
13
+ discriminating signal is the *writability ranking* and the *family -> atlas-system* join (each carrying
14
+ therapeutic readiness). Per-site reachability (does a specific bridge core exist here?) is Planner work.
15
+
16
+ Inputs : atlas_<ct>.parquet (chrom, bin, safety, p_durable, reachable_tier1, writability),
17
+ the atlas.parquet, gene_coords.parquet.
18
+ Outputs: out/crosslink_cache_<ct>.parquet (per-family reachable-loci summary).
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import os
23
+ from functools import lru_cache
24
+ from pathlib import Path
25
+
26
+ import pandas as pd
27
+
28
+ _ROOT = Path(__file__).resolve().parents[2]
29
+ _FINAL = _ROOT.parent # parent (Final_Part) directory
30
+ _ATLAS = _ROOT / "pen_stack" / "atlas" / "atlas.parquet"
31
+ _OUT = _ROOT / "out"
32
+ BIN_BP = 1000
33
+
34
+ # writability atlas can live in a few places (fetched-not-committed). First match wins.
35
+ # PEN_ATLAS_DIR (also used by the UI) is honoured first so every cross-link-backed feature - the Write
36
+ # Planner, the agent, and the RAG numeric route - finds the same atlas the UI does in any deployment.
37
+ def _writability_search() -> list[Path]:
38
+ bases: list[Path] = []
39
+ env = os.environ.get("PEN_ATLAS_DIR")
40
+ if env:
41
+ bases.append(Path(env))
42
+ bases += [_ROOT / "data" / "out", _FINAL / "phase_1" / "out"]
43
+ return bases
44
+
45
+
46
+ def writability_path(ct: str) -> Path:
47
+ bases = _writability_search()
48
+ for base in bases:
49
+ p = base / f"atlas_{ct}.parquet"
50
+ if p.exists():
51
+ return p
52
+ raise FileNotFoundError(f"atlas_{ct}.parquet not found in {[str(b) for b in bases]}")
53
+
54
+
55
+ # Cell types whose per-ct genotoxic-safety model is a partial / degraded computation (the atlas ships them as
56
+ # "partial" coverage). Their safety column can fail UNSAFE, claim a locus is safe where a full-coverage cell type
57
+ # correctly flags it (e.g. HSPC scoring bins beside the LMO2 oncogene as safe). Genotoxic proximity is a GENOMIC,
58
+ # cell-type-independent property, so for these we take a CONSERVATIVE floor from the full-coverage atlases.
59
+ _PARTIAL_SAFETY_CT = {"hspc"}
60
+ _FULL_COVERAGE_CT = ("k562", "hepg2")
61
+
62
+
63
+ @lru_cache(maxsize=1)
64
+ def _full_coverage_safety_floor() -> "pd.Series | None":
65
+ """min(safety) over the full-coverage atlases per (chrom, bin), the reliable, cell-type-independent genotoxic
66
+ floor used to correct a partial-coverage cell type's safety. None if no full-coverage atlas is present."""
67
+ frames = []
68
+ for r in _FULL_COVERAGE_CT:
69
+ try:
70
+ frames.append(pd.read_parquet(writability_path(r))[["chrom", "bin", "safety"]])
71
+ except FileNotFoundError:
72
+ continue
73
+ if not frames:
74
+ return None
75
+ allf = pd.concat(frames, ignore_index=True)
76
+ return allf.groupby(["chrom", "bin"])["safety"].min()
77
+
78
+
79
+ @lru_cache(maxsize=4)
80
+ def load_writability(ct: str) -> pd.DataFrame:
81
+ df = pd.read_parquet(writability_path(ct))
82
+ if ct.lower() in _PARTIAL_SAFETY_CT:
83
+ floor = _full_coverage_safety_floor()
84
+ if floor is not None:
85
+ df = df.merge(floor.rename("_ref_safety"), on=["chrom", "bin"], how="left")
86
+ df["safety_raw"] = df["safety"]
87
+ # fail-SAFE: never claim safer than the reliable full-coverage reference at the same genomic bin
88
+ df["safety"] = df[["safety", "_ref_safety"]].min(axis=1).fillna(df["safety"])
89
+ df["writability"] = 0.5 * df["safety"] + 0.5 * df["p_durable"]
90
+ df["safety_coverage"] = "partial: conservative floor from full-coverage atlases (genomic genotoxic signal)"
91
+ df = df.drop(columns=["_ref_safety"])
92
+ # Reject-known-bad genotoxicity blocklist: documented clinical insertional-oncogenesis loci are flagged
93
+ # unsafe (safety -> 0) by rule, overriding the soft model (closes the BMI1/SETBP1/MN1 false negatives).
94
+ from pen_stack.wgenome.genotoxic_blocklist import apply_genotoxic_blocklist
95
+ df = apply_genotoxic_blocklist(df)
96
+ df["_reach"] = df["reachable_tier1"].fillna("").str.split(";")
97
+ return df
98
+
99
+
100
+ @lru_cache(maxsize=1)
101
+ def load_writer_atlas() -> pd.DataFrame:
102
+ return pd.read_parquet(_ATLAS)
103
+
104
+
105
+ def reachable_families(ct: str) -> set[str]:
106
+ """The writer families annotated as Tier-1 reachable in the atlas for this cell type."""
107
+ df = load_writability(ct)
108
+ fams: set[str] = set()
109
+ for r in df["reachable_tier1"].dropna().unique():
110
+ fams.update(x for x in str(r).split(";") if x)
111
+ return fams
112
+
113
+
114
+ def loci_for_writer(family: str, ct: str = "k562", top: int = 20) -> pd.DataFrame:
115
+ """Top-writability loci reachable by a writer family (genomic coords + writability components)."""
116
+ df = load_writability(ct)
117
+ mask = df["_reach"].apply(lambda fams: family in fams)
118
+ hit = df.loc[mask].nlargest(top, "writability").copy()
119
+ hit["chrom_start"] = hit["bin"] * BIN_BP
120
+ return hit[["chrom", "bin", "chrom_start", "safety", "p_durable", "writability", "reachable_tier1"]]
121
+
122
+
123
+ def writers_for_locus(chrom: str, bin_idx: int, ct: str = "k562") -> pd.DataFrame:
124
+ """Atlas systems whose family reaches a locus, with therapeutic readiness (if scored)."""
125
+ df = load_writability(ct)
126
+ row = df[(df["chrom"] == chrom) & (df["bin"] == bin_idx)]
127
+ if row.empty:
128
+ return pd.DataFrame()
129
+ fams = {x for x in str(row.iloc[0]["reachable_tier1"]).split(";") if x}
130
+ atlas = load_writer_atlas()
131
+ cols = [c for c in ["representative_system", "family", "confidence", "deliv_class",
132
+ "readiness", "cargo_capacity_bp", "reachability_tier"] if c in atlas.columns]
133
+ out = atlas[atlas["family"].isin(fams)][cols].copy()
134
+ out["locus_writability"] = float(row.iloc[0]["writability"])
135
+ return out
136
+
137
+
138
+ def loci_for_gene(gene: str, ct: str = "k562", gene_coords: str | Path | None = None) -> pd.DataFrame:
139
+ """Writable bins overlapping a gene body (forward query helper)."""
140
+ from pen_stack.planner.optimize import gene_coords_path, resolve_gene
141
+ gc_path = Path(gene_coords) if gene_coords else gene_coords_path()
142
+ gc = pd.read_parquet(gc_path)
143
+ g = gc[gc["gene"] == resolve_gene(gene)] # resolve safe-harbour nicknames (AAVS1 -> PPP1R12C)
144
+ if g.empty:
145
+ return pd.DataFrame()
146
+ r = g.iloc[0]
147
+ df = load_writability(ct)
148
+ lo, hi = int(r["start"]) // BIN_BP, int(r["end"]) // BIN_BP
149
+ return df[(df["chrom"] == r["chrom"]) & (df["bin"].between(lo, hi))].sort_values(
150
+ "writability", ascending=False)
151
+
152
+
153
+ def build_crosslink_cache(ct: str = "k562", out: str | Path | None = None) -> pd.DataFrame:
154
+ """Per-family reachable-loci summary (count + median writability + top bin), cached."""
155
+ df = load_writability(ct)
156
+ rows = []
157
+ for fam in sorted(reachable_families(ct)):
158
+ sub = df[df["_reach"].apply(lambda fams, f=fam: f in fams)]
159
+ top = sub.nlargest(1, "writability")
160
+ rows.append({
161
+ "family": fam, "cell_type": ct, "n_reachable_loci": len(sub),
162
+ "median_writability": round(float(sub["writability"].median()), 4),
163
+ "top_chrom": top.iloc[0]["chrom"], "top_bin": int(top.iloc[0]["bin"]),
164
+ "top_writability": round(float(top.iloc[0]["writability"]), 4),
165
+ })
166
+ cache = pd.DataFrame(rows)
167
+ out = Path(out) if out else _OUT / f"crosslink_cache_{ct}.parquet"
168
+ out.parent.mkdir(parents=True, exist_ok=True)
169
+ cache.to_parquet(out, index=False)
170
+ return cache
171
+
172
+
173
+ if __name__ == "__main__": # pragma: no cover
174
+ for ct in ("k562", "hepg2", "hspc"):
175
+ try:
176
+ c = build_crosslink_cache(ct)
177
+ print(f"[{ct}] crosslink cache:\n{c.to_string(index=False)}\n")
178
+ except Exception as e: # noqa: BLE001 - a missing/partial cell-type atlas is non-fatal
179
+ print(f"[{ct}] skip: {e}")
@@ -0,0 +1,190 @@
1
+ """Expand the Writer Atlas across families.
2
+
3
+ Grow the curated 8-family core into a comprehensive cross-family catalogue: ingest ortholog
4
+ sets at scale (the IS110/IS1111 superfamily, CAST, large serine integrases, Cas12a, TnpB/Fanzor) from
5
+ UniProt, place every entry on the WT-KB targeting axes by *inheriting* family-level metadata from the
6
+ ``wtkb.parquet`` (single source of truth - the classifier/scorer must not re-derive it), and
7
+ tag each row with an explicit ``confidence`` (measured / inferred / predicted) and provenance.
8
+
9
+ Heavy per-ortholog featurisation (ESM embeddings for mechanism classification at scale) runs
10
+ in Docker on the GPU; this module only assembles the *catalogue* metadata (lightweight, network-bound).
11
+
12
+ Inputs : configs/atlas_families.yaml, pen_stack/atlas/wtkb.parquet, UniProt REST.
13
+ Outputs: pen_stack/atlas/atlas.parquet (one row per system), cached TSVs under data/external/atlas/.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import time
18
+ import urllib.parse
19
+ import urllib.request
20
+ from io import StringIO
21
+ from pathlib import Path
22
+
23
+ import pandas as pd
24
+ import yaml
25
+
26
+ _ROOT = Path(__file__).resolve().parents[2]
27
+ _CFG = _ROOT / "configs" / "atlas_families.yaml"
28
+ _WTKB = _ROOT / "pen_stack" / "atlas" / "wtkb.parquet"
29
+ _CACHE = _ROOT / "data" / "external" / "atlas"
30
+ _OUT = _ROOT / "pen_stack" / "atlas" / "atlas.parquet"
31
+
32
+ _UNIPROT_STREAM = "https://rest.uniprot.org/uniprotkb/stream"
33
+
34
+ # WT-KB family-level fields every atlas row inherits (so targeting metadata has ONE source).
35
+ _INHERIT = [
36
+ "mechanism_bucket", "targeting_modality", "target_site_spec", "guide_architecture",
37
+ "cargo_mechanism", "cargo_capacity_bp", "dsb_free", "reachability_tier",
38
+ "reachability_constraints",
39
+ ]
40
+
41
+
42
+ def load_config(path: str | Path = _CFG) -> dict:
43
+ return yaml.safe_load(Path(path).read_text(encoding="utf-8"))
44
+
45
+
46
+ def fetch_uniprot(query: str, fields: str, cache: Path, timeout: int = 120,
47
+ retries: int = 3) -> pd.DataFrame:
48
+ """Stream a UniProt query to TSV (cached). Returns the raw per-accession metadata frame."""
49
+ cache.parent.mkdir(parents=True, exist_ok=True)
50
+ if cache.exists():
51
+ return pd.read_csv(cache, sep="\t", dtype=str)
52
+ params = {"query": query, "format": "tsv", "fields": fields}
53
+ url = _UNIPROT_STREAM + "?" + urllib.parse.urlencode(params)
54
+ last = None
55
+ for attempt in range(retries):
56
+ try:
57
+ with urllib.request.urlopen(url, timeout=timeout) as r:
58
+ text = r.read().decode("utf-8")
59
+ df = pd.read_csv(StringIO(text), sep="\t", dtype=str)
60
+ cache.write_text(text, encoding="utf-8")
61
+ return df
62
+ except Exception as e: # noqa: BLE001 - network is best-effort; surfaced after retries
63
+ last = e
64
+ time.sleep(2 * (attempt + 1))
65
+ raise RuntimeError(f"UniProt fetch failed for query {query!r}: {last}")
66
+
67
+
68
+ def _orthologs_for_family(fam_key: str, fam: dict, fields: str, wtkb_row: pd.Series,
69
+ cache_dir: Path) -> pd.DataFrame:
70
+ """One ortholog table for a family, inheriting WT-KB targeting metadata by family."""
71
+ cache = cache_dir / f"{fam_key}.tsv"
72
+ raw = fetch_uniprot(fam["query"], fields, cache)
73
+ cap = int(fam.get("cap", len(raw)))
74
+ raw = raw.head(cap).copy()
75
+
76
+ # UniProt TSV column names (from the requested fields)
77
+ acc_col = "Entry"
78
+ org_col = "Organism"
79
+ len_col = "Length"
80
+ df = pd.DataFrame({
81
+ "representative_system": raw.get(acc_col),
82
+ "uniprot": raw.get(acc_col),
83
+ "organism": raw.get(org_col),
84
+ "length_aa": pd.to_numeric(raw.get(len_col), errors="coerce"),
85
+ })
86
+ df["family"] = fam["wtkb_family"]
87
+ df["pfam_signature"] = [list(fam["pfam_signature"])] * len(df)
88
+ df["confidence"] = fam.get("default_confidence", "predicted")
89
+ df["human_cell_activity"] = "not measured (sequence homolog)"
90
+ df["key_dois"] = [[fam["discovery_doi"]]] * len(df)
91
+ df["entry_kind"] = "ortholog"
92
+ # inherit targeting metadata from the WT-KB family row (single source of truth)
93
+ for col in _INHERIT:
94
+ df[col] = wtkb_row.get(col)
95
+ return df
96
+
97
+
98
+ def _curated_rows(cfg: dict, wtkb: pd.DataFrame) -> pd.DataFrame:
99
+ """Named, characterised systems: the 8 WT-KB families themselves + extra reps from config."""
100
+ rows = []
101
+ # (a) the WT-KB curated core - measured/inferred, full targeting spec
102
+ for _, w in wtkb.iterrows():
103
+ rows.append({
104
+ "representative_system": w["representative_system"],
105
+ "uniprot": w.get("uniprot"),
106
+ "organism": None,
107
+ "length_aa": w.get("length_aa"),
108
+ "family": w["family"],
109
+ "pfam_signature": list(w["pfam_signature"]) if w.get("pfam_signature") is not None else [],
110
+ "confidence": w.get("confidence", "measured"),
111
+ "human_cell_activity": w.get("human_cell_activity"),
112
+ "key_dois": list(w["key_dois"]) if w.get("key_dois") is not None else [],
113
+ "entry_kind": "curated_core",
114
+ **{c: w.get(c) for c in _INHERIT},
115
+ })
116
+ # (b) extra curated representatives (named systems w/o a clean single-Pfam query)
117
+ wt_by_fam = wtkb.set_index("family")
118
+ for rep in cfg.get("curated_representatives", []):
119
+ fam = rep["family"]
120
+ wrow = wt_by_fam.loc[fam] if fam in wt_by_fam.index else pd.Series(dtype=object)
121
+ if isinstance(wrow, pd.DataFrame):
122
+ wrow = wrow.iloc[0]
123
+ rows.append({
124
+ "representative_system": rep["representative_system"],
125
+ "uniprot": rep.get("uniprot"),
126
+ "organism": None,
127
+ "length_aa": rep.get("length_aa") or (wrow.get("length_aa") if len(wrow) else None),
128
+ "family": fam,
129
+ "pfam_signature": list(wrow.get("pfam_signature")) if len(wrow) and wrow.get("pfam_signature") is not None else [],
130
+ "confidence": rep.get("confidence", "inferred"),
131
+ "human_cell_activity": rep.get("human_cell_activity"),
132
+ "key_dois": list(rep.get("key_dois", [])),
133
+ "entry_kind": "curated_rep",
134
+ **{c: (wrow.get(c) if len(wrow) else None) for c in _INHERIT},
135
+ })
136
+ return pd.DataFrame(rows)
137
+
138
+
139
+ def confidence_tag(row: pd.Series) -> str:
140
+ """measured (human-cell data) > inferred (characterised, non-human) > predicted (homolog only)."""
141
+ c = row.get("confidence")
142
+ if c in {"measured", "inferred", "predicted"}:
143
+ return c
144
+ hca = (row.get("human_cell_activity") or "").lower()
145
+ if "human cell" in hca and "not measured" not in hca:
146
+ return "measured"
147
+ return "predicted"
148
+
149
+
150
+ def build_atlas(cfg_path: str | Path = _CFG, wtkb_path: str | Path = _WTKB,
151
+ out: str | Path = _OUT, cache_dir: str | Path = _CACHE,
152
+ offline_ok: bool = False) -> pd.DataFrame:
153
+ cfg = load_config(cfg_path)
154
+ fields = cfg["defaults"]["uniprot_fields"]
155
+ wtkb = pd.read_parquet(wtkb_path)
156
+ wt_by_fam = wtkb.drop_duplicates("family").set_index("family")
157
+ cache_dir = Path(cache_dir)
158
+
159
+ tables: list[pd.DataFrame] = [_curated_rows(cfg, wtkb)]
160
+ for fam_key, fam in cfg["families"].items():
161
+ wrow = wt_by_fam.loc[fam["wtkb_family"]] if fam["wtkb_family"] in wt_by_fam.index else pd.Series(dtype=object)
162
+ try:
163
+ tables.append(_orthologs_for_family(fam_key, fam, fields, wrow, cache_dir))
164
+ except Exception as e: # noqa: BLE001
165
+ if offline_ok:
166
+ print(f"[expand] skip {fam_key} (offline_ok): {e}")
167
+ continue
168
+ raise
169
+
170
+ atlas = pd.concat(tables, ignore_index=True)
171
+ # named curated rows win over a homolog row for the same accession. Dedup ONLY among rows that
172
+ # carry a UniProt id - rows without one (seekRNA, PASTE, ShCAST, Bxb1, ISPpu10) are all distinct
173
+ # systems and must never collapse together (pandas treats every NaN as equal under drop_duplicates).
174
+ atlas["_pri"] = atlas["entry_kind"].map({"curated_core": 0, "curated_rep": 1, "ortholog": 2})
175
+ has_acc = atlas["uniprot"].notna() & (atlas["uniprot"].astype(str).str.strip() != "")
176
+ with_acc = (atlas[has_acc].sort_values("_pri")
177
+ .drop_duplicates(subset=["uniprot"], keep="first"))
178
+ atlas = pd.concat([with_acc, atlas[~has_acc]], ignore_index=True).drop(columns="_pri")
179
+ atlas["confidence"] = atlas.apply(confidence_tag, axis=1)
180
+ atlas = atlas.reset_index(drop=True)
181
+
182
+ Path(out).parent.mkdir(parents=True, exist_ok=True)
183
+ atlas.to_parquet(out, index=False)
184
+ return atlas
185
+
186
+
187
+ if __name__ == "__main__": # pragma: no cover
188
+ a = build_atlas()
189
+ print(f"atlas rows: {len(a):,}")
190
+ print(a.groupby(["family", "confidence"]).size())