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