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,71 @@
1
+ """Mechanism-aware classification at scale.
2
+
3
+ Run the audited Pfam-whitelist classifier over the expanded Writer Atlas. For every system, derive a
4
+ ``mech_pred`` bucket + ``mech_conf`` *independently* from its Pfam domain architecture (homology), then
5
+ compare against the inherited/audited ``mechanism_bucket`` - keeping homology and mechanism distinct, as
6
+ the program requires. Low-confidence / conflicting / disagreeing calls are written to a review queue and
7
+ flagged, never hidden.
8
+
9
+ Inputs : pen_stack/atlas/atlas.parquet, the 18-family whitelist.
10
+ Outputs: atlas.parquet updated with mech_pred / mech_conf / mech_basis / mech_agrees,
11
+ out/mech_review_queue.csv.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+
17
+ import pandas as pd
18
+
19
+ from pen_stack.mech.whitelist import PfamWhitelist
20
+
21
+ _ROOT = Path(__file__).resolve().parents[2]
22
+ _ATLAS = _ROOT / "pen_stack" / "atlas" / "atlas.parquet"
23
+ _QUEUE = _ROOT / "out" / "mech_review_queue.csv"
24
+
25
+
26
+ def classify_atlas(atlas_parquet: str | Path = _ATLAS, out: str | Path = _ATLAS,
27
+ queue: str | Path = _QUEUE) -> pd.DataFrame:
28
+ atlas = pd.read_parquet(atlas_parquet)
29
+ wl = PfamWhitelist()
30
+
31
+ calls = atlas["pfam_signature"].apply(lambda s: wl.classify(list(s) if s is not None else []))
32
+ atlas["mech_pred"] = [c.bucket for c in calls]
33
+ atlas["mech_conf"] = [c.confidence for c in calls]
34
+ atlas["mech_basis"] = [c.basis for c in calls]
35
+ # agreement with the inherited/audited mechanism label (None where one side is missing)
36
+ atlas["mech_agrees"] = [
37
+ (mp == mb) if (mp is not None and pd.notna(mb)) else None
38
+ for mp, mb in zip(atlas["mech_pred"], atlas["mechanism_bucket"])
39
+ ]
40
+ atlas["mech_class_version"] = wl.version
41
+
42
+ # review queue: no domain evidence, conflicting evidence, or disagreement with the audited label
43
+ flag = (
44
+ atlas["mech_conf"].isin(["none", "conflicting"])
45
+ | atlas["mech_agrees"].eq(False) # explicit False (disagreement), not NaN
46
+ )
47
+ q = atlas.loc[flag, ["representative_system", "family", "pfam_signature",
48
+ "mechanism_bucket", "mech_pred", "mech_conf", "mech_basis",
49
+ "mech_agrees", "confidence"]]
50
+ Path(queue).parent.mkdir(parents=True, exist_ok=True)
51
+ q.to_csv(queue, index=False)
52
+
53
+ atlas.to_parquet(out, index=False)
54
+ return atlas
55
+
56
+
57
+ def core_agreement(atlas: pd.DataFrame) -> dict:
58
+ """Agreement on the curated 8-family core against the audited 18-family labels."""
59
+ core = atlas[atlas["entry_kind"] == "curated_core"]
60
+ scored = core[core["mech_pred"].notna()]
61
+ agree = int((scored["mech_pred"] == scored["mechanism_bucket"]).sum())
62
+ return {"n_core": len(core), "n_scored": len(scored), "n_agree": agree,
63
+ "agreement": round(agree / len(scored), 4) if len(scored) else None}
64
+
65
+
66
+ if __name__ == "__main__": # pragma: no cover
67
+ a = classify_atlas()
68
+ print("mech_conf distribution:\n", a["mech_conf"].value_counts())
69
+ print("\ncore agreement:", core_agreement(a))
70
+ n_flag = int((a["mech_conf"].isin(["none", "conflicting"]) | (a["mech_agrees"].eq(False))).sum())
71
+ print("\nreview queue rows:", n_flag)
@@ -0,0 +1,247 @@
1
+ # Pfam whitelist v1.2.2 - PEN-STACK genome-writing enzyme families
2
+ # AUDIT STATUS:
3
+ # * Pfam ACCESSIONS (the entry criteria): independently verified against the InterPro/EBI API
4
+ # (all 26 resolve with the correct domain name) on 2026-04-22 and re-confirmed 2026-06-09.
5
+ # * example_uniprot proteins (illustrative metadata only, NOT entry criteria): on 2026-06-09 each was
6
+ # checked against its UniProt Pfam cross-references; 13 of 22 did NOT actually contain the claimed
7
+ # domain (e.g. a marine-worm Histone H3, a mouse mannosyltransferase, I-AniI [a LAGLIDADG enzyme]
8
+ # mislabelled HNH, an obsolete 404 accession). ALL corrected in v1.2.2 to reviewed/curated proteins
9
+ # whose UniProt entry genuinely carries the domain (membership re-verified).
10
+ #
11
+ # Verification discipline:
12
+ # 1. InterPro API name-match against stored interpro_name (accessions)
13
+ # 2. UniProt Pfam-xref MEMBERSHIP check for every example_uniprot (the claimed domain must be in the
14
+ # protein's UniProt Pfam cross-references) - not just "the accession exists"
15
+ # 3. Literature cross-check for biological relevance
16
+
17
+ # Mechanism buckets (Tier A of MECH-CLASS):
18
+ # DSB_NUCLEASE | DSB_FREE_TRANSEST_RECOMBINASE | TRANSPOSASE
19
+
20
+ domains:
21
+ # === CRISPR-Cas Class 2 - Cas9 (multi-domain; >=2 of 3 co-occurring define Cas9) ===
22
+ - accession: PF13395
23
+ name: HNH_4
24
+ interpro_name: "HNH endonuclease"
25
+ mechanism_bucket: DSB_NUCLEASE
26
+ example_systems: [SpCas9, SaCas9]
27
+ example_uniprot: [Q99ZW2]
28
+ reference: "Jinek 2012 Science"
29
+ doi: "10.1126/science.1225829"
30
+ co_occurs_with: [PF18541, PF16595]
31
+ notes: "HNH nuclease domain found in CRISPR-related proteins (including Cas9). Not exclusively Cas9-specific - also found in other HNH endonucleases. Cleaves target DNA strand in Cas9."
32
+
33
+ - accession: PF18541
34
+ name: RuvC_III
35
+ interpro_name: "RuvC endonuclease subdomain 3"
36
+ mechanism_bucket: DSB_NUCLEASE
37
+ example_systems: [SpCas9]
38
+ example_uniprot: [Q99ZW2]
39
+ reference: "Jinek 2012 Science"
40
+ doi: "10.1126/science.1225829"
41
+ co_occurs_with: [PF13395, PF16595]
42
+ notes: "Cas9 RuvC subdomain 3; cleaves non-target strand. Part of composite Cas9 set. NOT the same as Cas12a RuvC (PF18516). example_uniprot Q99ZW2 IS SpCas9 (the correct biological example); UniProt annotates its RuvC under the merged Cas9 set, so the PF18541 sub-accession is not individually cross-referenced - kept because the protein genuinely bears this subdomain."
43
+
44
+ - accession: PF16595
45
+ name: Cas9_PI
46
+ interpro_name: "PAM-interacting domain of CRISPR-associated endonuclease Cas9"
47
+ mechanism_bucket: DSB_NUCLEASE
48
+ example_systems: [SpCas9]
49
+ example_uniprot: [Q99ZW2]
50
+ reference: "Nishimasu 2014 Cell"
51
+ doi: "10.1016/j.cell.2014.02.001"
52
+ co_occurs_with: [PF13395, PF18541]
53
+ notes: "PAM-interacting domain; recognises NGG PAM in SpCas9. Part of composite Cas9 set."
54
+
55
+ # === CRISPR-Cas Class 2 - Cas12a (RuvC-only single lobe) ===
56
+ - accession: PF18516
57
+ name: RuvC_1
58
+ interpro_name: "RuvC nuclease domain"
59
+ mechanism_bucket: DSB_NUCLEASE
60
+ example_systems: [AsCas12a, LbCas12a, FnCas12a]
61
+ example_uniprot: [U2UMQ6]
62
+ reference: "Zetsche 2015 Cell; Yamano 2016 Cell"
63
+ doi: "10.1016/j.cell.2015.09.038"
64
+ notes: "Cas12a RuvC nuclease domain. Single-lobe architecture; produces staggered cuts. Distinct from Cas9 RuvC (PF18541)."
65
+
66
+ # === IS110 / Bridge Recombinase composite (both domains required) ===
67
+ - accession: PF01548
68
+ name: DEDD_Tnp_IS110
69
+ interpro_name: "Transposase"
70
+ mechanism_bucket: DSB_FREE_TRANSEST_RECOMBINASE
71
+ example_systems: [ISCro4, IS621, IS110]
72
+ example_uniprot: [D2TGM5] # ISCro4 transposase (carries PF01548 + PF02371) - the bridge recombinase
73
+ reference: "Durrant 2024 Nature; Hiraizumi 2024 Nature"
74
+ doi: "10.1038/s41586-024-07552-4"
75
+ co_occurs_with: [PF02371]
76
+ notes: "N-terminal RuvC-fold DEDD catalytic domain of IS110/IS1111 transposases. Paired with PF02371 CTD. Uses bridge/seeker RNA for targeting."
77
+
78
+ - accession: PF02371
79
+ name: Transposase_20
80
+ interpro_name: "Transposase IS116/IS110/IS902 family"
81
+ mechanism_bucket: DSB_FREE_TRANSEST_RECOMBINASE
82
+ example_systems: [ISCro4, IS621, IS110, IS1111]
83
+ example_uniprot: [D2TGM5] # ISCro4 transposase (carries PF01548 + PF02371)
84
+ reference: "InterPro: Transposase IS116/IS110/IS902 family (PF01548 + PF02371)"
85
+ doi: null # the previously-listed Prudhomme 2002 J Bacteriol DOI did not resolve; classification rests on the Pfam domains
86
+ co_occurs_with: [PF01548]
87
+ notes: "C-terminal CTD of IS110 transposase. Contains HHH motif. DEDD chemistry (not serine recombinase). DNA binding + complex assembly."
88
+
89
+ # === TnpB / IscB / Fanzor ancestor family ===
90
+ - accession: PF07282
91
+ name: Cas12f1-like_TNB
92
+ interpro_name: "Cas12f1-like, TNB domain"
93
+ mechanism_bucket: DSB_NUCLEASE
94
+ example_systems: [ISDra2_TnpB, IscB, Fanzor1, Fanzor2]
95
+ example_uniprot: [Q7DF80]
96
+ reference: "Altae-Tran 2021 Science; Saito 2023 Nature"
97
+ doi: "10.1126/science.abj6856"
98
+ notes: "C-terminal zinc-ribbon / TNB domain of TnpB/IscB/Fanzor/Cas12f umbrella. Formerly called OrfB_Zn_ribbon. Broad ancestral marker; specific Fanzor/Cas12f ID needs structural features (Paper 2)."
99
+
100
+ # === Classical DDE transposases ===
101
+ - accession: PF00665
102
+ name: rve
103
+ interpro_name: "Integrase core domain"
104
+ mechanism_bucket: TRANSPOSASE
105
+ example_systems: [HIV_integrase, TnsB]
106
+ example_uniprot: [P03366] # HIV-1 Gag-Pol (integrase, rve core); Tn3 P03008 removed (it is PF01526, not rve)
107
+ reference: "Rice & Mizuuchi 1995 Cell"
108
+ doi: "10.1016/0092-8674(95)90308-9"
109
+ notes: "RNase H / integrase core fold (rve superfamily). Shared by retroviral integrases and TnsB of Tn7. TnsB specificity comes from co-occurrence with TnsA (PF08721)."
110
+
111
+ - accession: PF01609
112
+ name: DDE_Tnp_1
113
+ interpro_name: "Transposase DDE domain"
114
+ mechanism_bucket: TRANSPOSASE
115
+ example_systems: [Tn5, IS3_family, IS4_family]
116
+ example_uniprot: [Q46731] # Tn5 transposase (carries PF01609); was P03008 (Tn3, PF01526 - wrong fold)
117
+ reference: "Reznikoff 2008 Annu Rev Genet"
118
+ doi: "10.1146/annurev.genet.42.110807.091656"
119
+ notes: "Canonical DDE transposase catalytic domain. Found in Tn5 and many insertion sequences."
120
+
121
+ - accession: PF13586
122
+ name: DDE_Tnp
123
+ interpro_name: "Transposase DDE domain"
124
+ mechanism_bucket: TRANSPOSASE
125
+ example_systems: [DDE_transposases_diverse]
126
+ example_uniprot: [A0A009P861] # Transposase DDE-domain protein (carries PF13586); was Q9U6K4 (Histone H3 - wrong)
127
+ reference: "Chandler 2013 Nat Rev Microbiol"
128
+ doi: "10.1038/nrmicro3067"
129
+ notes: "Additional DDE transposase domain family. Generic DDE-fold diversity - not exclusively hAT or Mariner. Complements PF01609 coverage."
130
+
131
+ # === Tn7 and CAST (I-F and V-K) system components ===
132
+ - accession: PF08721
133
+ name: Tn7_Tnp_TnsA_C
134
+ interpro_name: "TnsA endonuclease C terminal"
135
+ mechanism_bucket: DSB_FREE_TRANSEST_RECOMBINASE
136
+ example_systems: [Tn7, CAST-I-F]
137
+ example_uniprot: [P13988] # Tn7 TnsA (carries PF08721); was P13990 (obsolete/404)
138
+ reference: "Hickman 2000 Mol Cell"
139
+ doi: "10.1016/S1097-2765(00)80267-1"
140
+ notes: "TnsA endonuclease C-terminal domain. Cleaves 5' transposon end. CAST V-K lacks TnsA; detect via TnsB+TnsC co-occurrence instead."
141
+
142
+ - accession: PF11426
143
+ name: TnsC
144
+ interpro_name: "Tn7 transposition regulator TnsC"
145
+ mechanism_bucket: DSB_FREE_TRANSEST_RECOMBINASE
146
+ example_systems: [Tn7, CAST-I-F, CAST-V-K]
147
+ example_uniprot: [P05846]
148
+ reference: "Hoffmann 2022 Nature; Peters 2017 PNAS"
149
+ doi: "10.1038/s41586-022-05059-4"
150
+ notes: "AAA+ ATPase regulator of Tn7 transposition. Bridges target selection (TniQ/Cascade) and transposase (TnsB). Core to all CAST systems. CORRECTED from PF03400 (was IS1 transposase - completely unrelated)."
151
+
152
+ - accession: PF05621
153
+ name: TniB
154
+ interpro_name: "Bacterial TniB protein"
155
+ mechanism_bucket: DSB_FREE_TRANSEST_RECOMBINASE
156
+ example_systems: [Tn5053, Tn7-related_targeting]
157
+ example_uniprot: [A0A059KVU2] # AAA+ ATPase / TniB (carries PF05621); was P0CW70 (mouse mannosyltransferase - wrong)
158
+ reference: "Kholodii 1993 J Mol Biol (Tn5053)"
159
+ doi: "10.1006/jmbi.1993.1228"
160
+ notes: "TniB P-loop NTPase. Related to TnsC-family regulators but distinct protein. Found in Tn5053 and related Tn7-like elements."
161
+
162
+ # === Site-specific recombinases ===
163
+ - accession: PF00589
164
+ name: Phage_integrase
165
+ interpro_name: "Phage integrase family"
166
+ mechanism_bucket: DSB_FREE_TRANSEST_RECOMBINASE
167
+ example_systems: [Cre, Flp, lambda-Int, XerC, XerD]
168
+ example_uniprot: [P06956, P03700]
169
+ reference: "Grainge 2002 J Mol Biol"
170
+ doi: "10.1016/S0022-2836(02)00517-X"
171
+ notes: "Tyrosine recombinase family. 5'-phosphotyrosine covalent intermediate."
172
+
173
+ - accession: PF00239
174
+ name: Resolvase
175
+ interpro_name: "Resolvase, N terminal domain"
176
+ mechanism_bucket: DSB_FREE_TRANSEST_RECOMBINASE
177
+ example_systems: [gamma-delta_resolvase, Sin, Tn3_resolvase]
178
+ example_uniprot: [P03012, P20384] # gamma-delta (Tn3) resolvase + Tn552 invertase; was P03016 (no Pfam xref)
179
+ reference: "Smith 2004 Mol Microbiol"
180
+ doi: "10.1111/j.1365-2958.2003.03927.x"
181
+ notes: "Small serine recombinase family. 5'-phosphoserine covalent intermediate."
182
+
183
+ - accession: PF07508
184
+ name: Recombinase
185
+ interpro_name: "Recombinase"
186
+ mechanism_bucket: DSB_FREE_TRANSEST_RECOMBINASE
187
+ example_systems: [Bxb1, phiC31, TP901-1, R4]
188
+ example_uniprot: [Q9B086] # Bxb1 serine integrase (carries PF07508); was O25753/Q9T1S6 (no Pfam xref - wrong)
189
+ reference: "Stark 2014 Microbiol Spectr"
190
+ doi: "10.1128/microbiolspec.MDNA3-0046-2014"
191
+ notes: "Large serine integrase family. Used in eePASSIGE and landing-pad strategies."
192
+
193
+ # === Other DNA-cleaving enzymes relevant to genome writing ===
194
+ - accession: PF01844
195
+ name: HNH
196
+ interpro_name: "HNH endonuclease"
197
+ mechanism_bucket: DSB_NUCLEASE
198
+ example_systems: [McrA, HNH_endonucleases]
199
+ example_uniprot: [P24200] # E. coli McrA, an HNH endonuclease (carries PF01844); was P03880 (I-AniI, a LAGLIDADG enzyme - wrong fold)
200
+ reference: "Stoddard 2011 Structure"
201
+ doi: "10.1016/j.str.2010.12.005"
202
+ notes: "Standalone HNH endonuclease family. Distinct from HNH_4 (PF13395). Appears in IscB and homing endonucleases."
203
+
204
+ - accession: PF02486
205
+ name: Rep_3
206
+ interpro_name: "Replication initiation factor"
207
+ mechanism_bucket: TRANSPOSASE
208
+ example_systems: [IS91, ISCR, helitrons, rolling-circle_initiators]
209
+ example_uniprot: [P03064] # Staph rolling-circle Rep initiator (carries PF02486); was P03856 (PF01051, not PF02486)
210
+ reference: "Chandler 2013 Nat Rev Microbiol"
211
+ doi: "10.1038/nrmicro3067"
212
+ notes: "HUH-domain rolling-circle replication initiator. 5'-phosphotyrosine intermediate. Covers IS91 / ISCR / helitron families."
213
+
214
+ # === Auxiliary domains (NOT entry criteria - recorded if co-occurring with primary) ===
215
+ auxiliary:
216
+ - accession: PF18061
217
+ name: WED
218
+ interpro_name: "CRISPR-Cas9 WED domain"
219
+ notes: "Wedge domain between REC and NUC lobes in Cas9."
220
+ - accession: PF16592
221
+ name: REC_lobe
222
+ interpro_name: "REC lobe of CRISPR-associated endonuclease Cas9"
223
+ notes: "Alpha-helical recognition lobe of Cas9. CORRECTED from PF18543 (was unrelated intracellular delivery domain)."
224
+ - accession: PF16593
225
+ name: Bridge_helix
226
+ interpro_name: "Bridge helix of CRISPR-associated endonuclease Cas9"
227
+ notes: "Arginine-rich helix connecting REC and NUC lobes in Cas9."
228
+ - accession: PF13639
229
+ name: ZnF_RING
230
+ interpro_name: "Ring finger domain"
231
+ notes: "RING/zinc-finger; sometimes appears in engineered fusions."
232
+ - accession: PF03377
233
+ name: TAL_effector
234
+ interpro_name: "TAL effector repeat"
235
+ notes: "TALE repeat - outgroup comparison for programmable DNA binding. CORRECTED from PF03374 (was KilAC phage antirepressor)."
236
+
237
+ # === Version metadata ===
238
+ version: "1.2.2"
239
+ created: "2026-04-22"
240
+ previous_version: "1.2.1"
241
+ curator: "Anees Ahmed"
242
+ audit_status: "Pfam accessions verified vs InterPro API; example_uniprot membership re-verified vs UniProt Pfam cross-references on 2026-06-09 (13 wrong example proteins corrected)"
243
+ change_log:
244
+ - "2026-06-09 v1.2.2: example_uniprot MEMBERSHIP audit (UniProt Pfam xrefs). 13/22 example proteins did not contain their claimed domain despite the v1.2.1 header claiming a spot-check; all corrected to proteins whose UniProt entry carries the domain: PF01548/PF02371 Q8X7L4->D2TGM5 (ISCro4); PF00665 dropped P03008 (Tn3, not rve), kept P03366; PF01609 P03008->Q46731 (Tn5); PF13586 Q9U6K4 (Histone H3!)->A0A009P861; PF08721 P13990 (404)->P13988 (Tn7 TnsA); PF05621 P0CW70 (mouse mannosyltransferase)->A0A059KVU2; PF00239 P03016->P03012 (gamma-delta resolvase); PF07508 O25753/Q9T1S6->Q9B086 (Bxb1 integrase); PF01844 P03880 (I-AniI, LAGLIDADG)->P24200 (McrA HNH); PF02486 P03856->P03064. PF18541 kept Q99ZW2 (SpCas9; merged-xref granularity, documented). Pfam accessions themselves all re-confirmed correct."
245
+ - "2026-04-22 v1.2.0: Comprehensive audit. Replaced 13 incorrect/mismatched accessions from v1.1.0."
246
+ - "2026-04-22 v1.1.0: Added DOIs; fixed 3 reference mismatches."
247
+ - "2026-04-22 v1.0.0: Initial release."
@@ -0,0 +1,66 @@
1
+ """The InterPro-audited 18-family Pfam whitelist (imported from genome-atlas v1.2.1).
2
+
3
+ Tier-A of MECH-CLASS: maps a Pfam domain architecture to a mechanism bucket
4
+ (``DSB_NUCLEASE`` / ``DSB_FREE_TRANSEST_RECOMBINASE`` / ``TRANSPOSASE``) using domain presence plus
5
+ *composite co-occurrence rules* (e.g. Cas9 requires >=2 of its 3 signature domains; IS110 requires
6
+ both PF01548 and PF02371). This is the audited backbone the program carries forward (Section 9); the retired
7
+ ESM-2 discovery head is not used - domain evidence is the load-bearing mechanism signal.
8
+
9
+ The source YAML ``pfam_whitelist.yaml`` is the genome-atlas asset, accessions verified against InterPro
10
+ on 2026-04-22 (v1.2.1 corrected three v1.2.0 accession errors).
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from collections import Counter
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+
18
+ import yaml
19
+
20
+ _WL_PATH = Path(__file__).resolve().parent / "pfam_whitelist.yaml"
21
+
22
+ # Composite architectures: a call is only "composite-grade" (highest confidence) when the required
23
+ # co-occurring domains are all present. Derived from the whitelist co_occurs_with fields.
24
+ _COMPOSITES = {
25
+ "Cas9": {"min": 2, "of": {"PF13395", "PF18541", "PF16595"}, "bucket": "DSB_NUCLEASE"},
26
+ "IS110_bridge": {"min": 2, "of": {"PF01548", "PF02371"}, "bucket": "DSB_FREE_TRANSEST_RECOMBINASE"},
27
+ }
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class MechCall:
32
+ bucket: str | None # mechanism bucket, or None if no whitelisted domain present
33
+ confidence: str # composite | single | conflicting | none
34
+ basis: str # human-readable evidence trail
35
+ matched: tuple[str, ...] # whitelisted accessions that fired
36
+
37
+
38
+ class PfamWhitelist:
39
+ def __init__(self, path: str | Path = _WL_PATH):
40
+ doms = yaml.safe_load(Path(path).read_text(encoding="utf-8"))["domains"]
41
+ self.bucket_of: dict[str, str] = {d["accession"]: d["mechanism_bucket"] for d in doms}
42
+ self.name_of: dict[str, str] = {d["accession"]: d.get("name", "") for d in doms}
43
+ self.version = "1.2.1"
44
+
45
+ def classify(self, pfam_signature) -> MechCall:
46
+ """Mechanism from a Pfam domain set - independent of any inherited/family label."""
47
+ sig = {str(a).strip() for a in (pfam_signature or []) if str(a).strip()}
48
+ hits = sorted(sig & set(self.bucket_of))
49
+ if not hits:
50
+ return MechCall(None, "none", "no whitelisted Pfam domain present", ())
51
+
52
+ # composite rule wins (most specific, highest confidence)
53
+ for name, rule in _COMPOSITES.items():
54
+ present = sig & rule["of"]
55
+ if len(present) >= rule["min"]:
56
+ return MechCall(rule["bucket"], "composite",
57
+ f"{name}: {len(present)}/{len(rule['of'])} signature domains "
58
+ f"({','.join(sorted(present))})", tuple(hits))
59
+
60
+ buckets = Counter(self.bucket_of[a] for a in hits)
61
+ top, n = buckets.most_common(1)[0]
62
+ if len(buckets) > 1:
63
+ return MechCall(top, "conflicting",
64
+ f"mixed domain evidence: {dict(buckets)}", tuple(hits))
65
+ return MechCall(top, "single",
66
+ f"single-bucket domain evidence: {','.join(hits)}", tuple(hits))
@@ -0,0 +1 @@
1
+ """pen_stack.monitor - see the PEN-STACK program doc."""
@@ -0,0 +1,32 @@
1
+ """Europe PMC client for the living-literature monitor.
2
+
3
+ Europe PMC is the right primary source: open REST API, full-text + preprints, no licence friction.
4
+ This module only *fetches* - triage + queueing live in triage.py / run.py.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import time
9
+ import urllib.parse
10
+ import urllib.request
11
+ from io import BytesIO
12
+ import json
13
+
14
+ EPMC = "https://www.ebi.ac.uk/europepmc/webservices/rest/search"
15
+
16
+
17
+ def search(query: str, since_date: str | None = None, page_size: int = 100,
18
+ timeout: int = 30, retries: int = 3) -> list[dict]:
19
+ """Search Europe PMC. ``since_date`` (YYYY-MM-DD) filters on first publication date."""
20
+ q = query if not since_date else f"{query} AND FIRST_PDATE:[{since_date} TO *]"
21
+ params = {"query": q, "format": "json", "pageSize": page_size, "resultType": "core"}
22
+ url = EPMC + "?" + urllib.parse.urlencode(params)
23
+ last = None
24
+ for attempt in range(retries):
25
+ try:
26
+ with urllib.request.urlopen(url, timeout=timeout) as r:
27
+ data = json.load(BytesIO(r.read()))
28
+ return data.get("resultList", {}).get("result", [])
29
+ except Exception as e: # noqa: BLE001 - network best-effort
30
+ last = e
31
+ time.sleep(2 * (attempt + 1))
32
+ raise RuntimeError(f"Europe PMC search failed for {query!r}: {last}")
@@ -0,0 +1,57 @@
1
+ """The living-literature monitor orchestrator - the Europe PMC living-database engine.
2
+
3
+ Poll Europe PMC for every writer-family query, triage each hit into a candidate row (always cited),
4
+ de-duplicate, and write a human-reviewed curation queue. The atlas is **never** auto-edited; accepted
5
+ entries flow into the WT-KB/atlas with confidence=inferred only after a human accepts them.
6
+
7
+ Back-test: with ``back_test=True`` and a date window covering March 2026, the engine must surface the
8
+ known recent writer ISPpu10 (Europe PMC PPR1218813) into the queue - the pre-registered success check.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from pathlib import Path
13
+
14
+ import pandas as pd
15
+
16
+ from pen_stack.monitor.europepmc import search
17
+ from pen_stack.monitor.triage import _load_cues, triage_hit
18
+
19
+ _OUT = Path(__file__).resolve().parents[2] / "out" / "monitor_queue.csv"
20
+
21
+
22
+ def run_monitor(since: str = "2026-01-01", page_size: int = 50, back_test: bool = False,
23
+ out: str | Path = _OUT, cfg_path: str | Path | None = None) -> dict:
24
+ cfg = _load_cues(cfg_path) if cfg_path else _load_cues()
25
+ rows, n_hits = [], 0
26
+ for q in cfg["queries"]:
27
+ try:
28
+ hits = search(q["terms"], since_date=since, page_size=page_size)
29
+ except RuntimeError:
30
+ continue
31
+ n_hits += len(hits)
32
+ for h in hits:
33
+ rows.append(triage_hit(h, default_family=q.get("family"), cfg=cfg))
34
+
35
+ queue = pd.DataFrame(rows)
36
+ if not queue.empty:
37
+ queue = queue.drop_duplicates(subset=["source_id"]).reset_index(drop=True)
38
+ # every queued candidate must carry a citation (source_id or doi)
39
+ queue = queue[queue["source_id"].notna() | queue["doi"].notna()]
40
+
41
+ Path(out).parent.mkdir(parents=True, exist_ok=True)
42
+ queue.to_csv(out, index=False)
43
+
44
+ res = {"since": since, "n_hits": n_hits, "n_candidates": int(len(queue)), "queue": str(out)}
45
+ if back_test:
46
+ found = False
47
+ if not queue.empty:
48
+ blob = (queue["title"].fillna("") + " " + queue["source_id"].fillna("")).str.lower()
49
+ found = bool(blob.str.contains("isppu10").any() or
50
+ (queue["source_id"] == "PPR1218813").any())
51
+ res["isppu10_found"] = found
52
+ return res
53
+
54
+
55
+ if __name__ == "__main__": # pragma: no cover
56
+ r = run_monitor(since="2026-01-01", back_test=True)
57
+ print(r)
@@ -0,0 +1,63 @@
1
+ """Triage Europe PMC hits into candidate writer-system rows.
2
+
3
+ Grounded extraction: pull candidate fields (family, organism cue, human-cell evidence) from a hit's
4
+ title/abstract using documented keyword cues, **always** carrying the source citation (Europe PMC id +
5
+ DOI). An optional LLM pass (Ollama/Qwen via litellm) can enrich the abstract extraction, but it never
6
+ invents a citation and never auto-edits the atlas - its output is just another candidate for the queue.
7
+
8
+ The rule-based path is the reliable default (works offline, fully reproducible, satisfies the back-test).
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from pathlib import Path
14
+
15
+ import yaml
16
+
17
+ _CFG = Path(__file__).resolve().parents[2] / "configs" / "monitor_queries.yaml"
18
+
19
+
20
+ def _load_cues(path: str | Path = _CFG) -> dict:
21
+ return yaml.safe_load(Path(path).read_text(encoding="utf-8"))
22
+
23
+
24
+ def classify_family(text: str, cfg: dict) -> tuple[str | None, list[str]]:
25
+ """Best-guess writer family from keyword cues; returns (family, matched_cues)."""
26
+ t = text.lower()
27
+ best, best_hits = None, []
28
+ for fam, cues in cfg.get("family_cues", {}).items():
29
+ hits = [c for c in cues if c in t]
30
+ if len(hits) > len(best_hits):
31
+ best, best_hits = fam, hits
32
+ return best, best_hits
33
+
34
+
35
+ def has_human_cell_evidence(text: str, cfg: dict) -> bool:
36
+ t = text.lower()
37
+ return any(cue in t for cue in cfg.get("human_cell_cues", []))
38
+
39
+
40
+ _ORG_RE = re.compile(r"\b([A-Z][a-z]+ [a-z]{3,})\b") # coarse "Genus species" cue
41
+
42
+
43
+ def triage_hit(hit: dict, default_family: str | None = None, cfg: dict | None = None) -> dict:
44
+ """Return a candidate row for the curation queue. Always carries a citation; never auto-edits."""
45
+ cfg = cfg or _load_cues()
46
+ title = hit.get("title", "") or ""
47
+ abstract = hit.get("abstractText", "") or ""
48
+ text = f"{title}. {abstract}"
49
+ fam, cues = classify_family(text, cfg)
50
+ org = _ORG_RE.search(abstract)
51
+ return {
52
+ "candidate_family": fam or default_family,
53
+ "matched_cues": ";".join(cues),
54
+ "organism_cue": org.group(1) if org else None,
55
+ "human_cell_evidence": has_human_cell_evidence(text, cfg),
56
+ "title": title[:300],
57
+ "source_id": hit.get("id"),
58
+ "source_db": hit.get("source"),
59
+ "doi": hit.get("doi"),
60
+ "pub_date": hit.get("firstPublicationDate"),
61
+ "confidence": "inferred", # candidate - stays inferred until a human reviews/measures
62
+ "status": "pending_review", # NEVER auto-accepted into the atlas
63
+ }
@@ -0,0 +1,65 @@
1
+ """The L1 oracle mesh, one contract over the biomolecular foundation models.
2
+
3
+ `pen_stack.oracles` wraps AlphaGenome / Evo2 / structure predictors (AF3, Boltz-2, Chai-1, Protenix) /
4
+ protein-design models (ESM3, RFdiffusion, ProteinMPNN) / ViennaRNA / the bridge energetics model under a
5
+ single `OracleResult` contract (value + provenance + native uncertainty + scope card). Heavy backends run
6
+ on-demand (hosted API / local GPU) and are cached + version-pinned; when a backend is absent the adapter
7
+ returns a *deferred* result (available=False), the core stays runnable offline from cache.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from pen_stack.oracles.cache import cache_get, cache_key, cache_put, scope_card
14
+ from pen_stack.oracles.schema import OracleResult, Provenance
15
+
16
+ __all__ = ["OracleResult", "Provenance", "build_result", "consensus", "assert_claimable",
17
+ "cache_get", "cache_key", "cache_put", "scope_card"]
18
+
19
+
20
+ def build_result(oracle: str, model: str, *, value: Any = None, inputs: dict | None = None,
21
+ native_uncertainty: float | None = None, available: bool = True, cached: bool = False,
22
+ source: str = "adapter", extrapolating: bool = False, in_scope: bool = True,
23
+ output_kind: str | None = None, note: str | None = None) -> OracleResult:
24
+ """Assemble an `OracleResult`, filling version / output_kind / scope from the model's scope card.
25
+
26
+ `output_kind` defaults to the scope card's, but may be overridden per call (e.g. an Evo2 *likelihood*
27
+ score is a claim-scope scalar, while an Evo2 *generated sequence* is a candidate)."""
28
+ card = scope_card(model) or {}
29
+ key = cache_key(oracle, model, card.get("version", "0"), inputs or {})
30
+ return OracleResult(
31
+ oracle=oracle, value=value,
32
+ provenance=Provenance(model=model, version=card.get("version", "0"), source=source, cache_key=key),
33
+ native_uncertainty=native_uncertainty,
34
+ scope_card=model, in_scope=in_scope, extrapolating=extrapolating,
35
+ output_kind=output_kind or card.get("output_kind", "claim"),
36
+ available=available, cached=cached, note=note)
37
+
38
+
39
+ def assert_claimable(result: OracleResult) -> OracleResult:
40
+ """Guard: a generative candidate cannot enter a claim path without writer-verification (Principle 1)."""
41
+ return result.as_claim()
42
+
43
+
44
+ def consensus(results: list[OracleResult], oracle: str = "structure") -> OracleResult:
45
+ """Cross-oracle self-consistency (Principle 3): agreement is a confidence signal, **divergence
46
+ widens the interval**. Combines redundant numeric oracles (e.g. AF3 / Boltz-2 / Chai-1 / Protenix); the
47
+ reported native_uncertainty is increased by the spread across the available oracles."""
48
+ avail = [r for r in results if r.available and isinstance(r.value, (int, float))]
49
+ if not avail:
50
+ return build_result(oracle, "consensus", available=False,
51
+ note="no available numeric oracle to combine")
52
+ vals = [float(r.value) for r in avail]
53
+ mean = sum(vals) / len(vals)
54
+ spread = (max(vals) - min(vals)) if len(vals) > 1 else 0.0
55
+ base_unc = max((r.native_uncertainty or 0.0) for r in avail)
56
+ card = scope_card("boltz-2") or {}
57
+ return OracleResult(
58
+ oracle=oracle, value=round(mean, 4),
59
+ provenance=Provenance(model="consensus", version=card.get("version", "0"), source="adapter",
60
+ extra={"members": [r.provenance.model for r in avail], "spread": round(spread, 4)}),
61
+ # disagreement widens the interval: native uncertainty + half the cross-oracle spread
62
+ native_uncertainty=round(base_unc + 0.5 * spread, 4),
63
+ scope_card="boltz-2", in_scope=all(r.in_scope for r in avail),
64
+ extrapolating=any(r.extrapolating for r in avail), output_kind="claim",
65
+ available=True, note=f"consensus of {len(avail)} oracles; spread {round(spread, 4)} widens the interval")