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,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}")
|
pen_stack/monitor/run.py
ADDED
|
@@ -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")
|