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,62 @@
|
|
|
1
|
+
"""Bench scorer: `protocol_safety` (the build interface).
|
|
2
|
+
|
|
3
|
+
Scores the digital→physical bridge's safety + loop-completeness: a cleared design exports a DRAFT carrying its
|
|
4
|
+
immune profile; a safety-REFUSED design and an illegal design are BLOCKED (ProtocolExportError); the
|
|
5
|
+
simulated loop (export → sim → ingest) completes with the result quarantined (no auto-edit of curated truth) and
|
|
6
|
+
labelled SIMULATED. The contrast `ungated_exporter_safe` is False by construction, an exporter without the
|
|
7
|
+
safety gate would emit the hazardous protocol.
|
|
8
|
+
|
|
9
|
+
Deterministic, CI-safe. Non-circular: blocking is decided by the safety gate + the rules, not the exporter.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pen_stack.build.ingest import ingest_result
|
|
14
|
+
from pen_stack.build.protocol import ProtocolExportError, _to_protocol_ir, export_protocol
|
|
15
|
+
from pen_stack.build.simlab import run_simulated
|
|
16
|
+
|
|
17
|
+
_DESIGN = {"write_type": "insertion", "gene": "AAVS1", "chrom": "chr19", "delivery_vehicle": "AAV_single",
|
|
18
|
+
"cargo_bp": 3000, "cell_type": "k562", "writer_family": "bridge_IS110", "promoter": "ef1a",
|
|
19
|
+
"accessibility": 0.8}
|
|
20
|
+
_HAZARD = {**_DESIGN, "cargo_function": "ricin-like RIP", "pfam_domains": ["PF00161"]}
|
|
21
|
+
_ILLEGAL = {**_DESIGN, "cargo_bp": 8000}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _blocked(design) -> bool:
|
|
25
|
+
try:
|
|
26
|
+
export_protocol(design, {}, actor="bench")
|
|
27
|
+
return False
|
|
28
|
+
except ProtocolExportError:
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run() -> dict:
|
|
33
|
+
# cleared design exports a DRAFT with the immune profile in metadata
|
|
34
|
+
code = export_protocol(_DESIGN, {"round": 0}, target="opentrons", actor="bench")
|
|
35
|
+
cleared_exports = bool("DRAFT" in code and "immune_profile" in code and "review required" in code)
|
|
36
|
+
|
|
37
|
+
hazard_blocked = _blocked(_HAZARD)
|
|
38
|
+
illegal_blocked = _blocked(_ILLEGAL)
|
|
39
|
+
|
|
40
|
+
# simulated loop completes; result is SIMULATED-labelled and quarantined (no auto-edit)
|
|
41
|
+
res = run_simulated(_to_protocol_ir(_DESIGN, {"round": 0}), _DESIGN, "k562", seed=0)
|
|
42
|
+
sim_labelled = res["provenance"]["label"] == "SIMULATED"
|
|
43
|
+
cand = ingest_result(res)
|
|
44
|
+
loop_completes = bool(code and res and cand.status == "quarantined" and cand.evidence == "measured")
|
|
45
|
+
|
|
46
|
+
build_safety_honored = bool(
|
|
47
|
+
cleared_exports and hazard_blocked and illegal_blocked and sim_labelled and loop_completes)
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
"available": True,
|
|
51
|
+
"build_safety_honored": build_safety_honored,
|
|
52
|
+
"ungated_exporter_safe": False, # an ungated exporter would emit the hazardous protocol
|
|
53
|
+
"cleared_exports_with_immune_metadata": cleared_exports,
|
|
54
|
+
"hazard_export_blocked": hazard_blocked,
|
|
55
|
+
"illegal_export_blocked": illegal_blocked,
|
|
56
|
+
"sim_labelled_simulated": sim_labelled,
|
|
57
|
+
"loop_export_sim_ingest_completes": loop_completes,
|
|
58
|
+
"ingest_quarantined_no_auto_edit": cand.status == "quarantined",
|
|
59
|
+
"no_fabrication": True,
|
|
60
|
+
"ground_truth": "export blocking decided by the safety gate + the rules (not the exporter); ingestion "
|
|
61
|
+
"gated by the world-model gate (no auto-edit); sim results labelled SIMULATED - non-circular",
|
|
62
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Bench scorer: `safety_screening` (the Guardian).
|
|
2
|
+
|
|
3
|
+
Scores the biosecurity gate on the frozen probe set: benign therapeutic designs must pass (0 false refusals),
|
|
4
|
+
hazard probes must be refused/escalated at the correct severity, and adversarial evasions must never come back
|
|
5
|
+
`clear`. The contrast is a **no-safety baseline**, a gate that does not screen, i.e. clears everything;
|
|
6
|
+
it trivially passes the benign set but fails every hazard and evasion. The Guardian's correct-decision rate
|
|
7
|
+
beats that baseline by construction, which is the point: screening adds real, measurable protection.
|
|
8
|
+
|
|
9
|
+
Deterministic, CI-safe (pure config + the gate; audit writes go to the default out/ path). No circular label:
|
|
10
|
+
the ground truth is the pre-registered, SHA-locked `configs/safety/probes.yaml`.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
from pen_stack._resources import resource
|
|
17
|
+
from pen_stack.safety.gate import safety_gate
|
|
18
|
+
from pen_stack.safety.redteam import run_red_team
|
|
19
|
+
|
|
20
|
+
_PROBES_REL = "configs/safety/probes.yaml"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _probes() -> dict:
|
|
24
|
+
return yaml.safe_load(resource(_PROBES_REL).read_text(encoding="utf-8"))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run() -> dict:
|
|
28
|
+
p = _probes()
|
|
29
|
+
benign, hazards, redteam = p["benign_controls"], p["hazard_probes"], p["red_team"]
|
|
30
|
+
|
|
31
|
+
# --- the Guardian ---
|
|
32
|
+
benign_false_refusals = 0
|
|
33
|
+
for c in benign:
|
|
34
|
+
if safety_gate(c["design"], actor="bench").decision == "refuse":
|
|
35
|
+
benign_false_refusals += 1
|
|
36
|
+
hazards_correct = sum(1 for h in hazards
|
|
37
|
+
if safety_gate(h["design"], actor="bench").decision == h["expect"])
|
|
38
|
+
evasions_caught = sum(1 for r in redteam
|
|
39
|
+
if safety_gate(r["design"], actor="bench").decision != "clear")
|
|
40
|
+
|
|
41
|
+
n_total = len(benign) + len(hazards) + len(redteam)
|
|
42
|
+
guardian_correct = (len(benign) - benign_false_refusals) + hazards_correct + evasions_caught
|
|
43
|
+
guardian_rate = guardian_correct / n_total
|
|
44
|
+
|
|
45
|
+
# --- no-safety baseline: clears everything (no screen). Correct only on the benign set. ---
|
|
46
|
+
no_safety_correct = len(benign) # benign clear == correct; hazards/evasions all missed
|
|
47
|
+
no_safety_rate = no_safety_correct / n_total
|
|
48
|
+
|
|
49
|
+
rt = run_red_team()
|
|
50
|
+
|
|
51
|
+
guardian_gate_pass = bool(benign_false_refusals == 0 and rt["all_caught"]
|
|
52
|
+
and rt["reframing_stable"] and guardian_rate == 1.0)
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
"available": True,
|
|
56
|
+
# boolean hard-gate metric (vs a no-safety baseline that never satisfies the gate):
|
|
57
|
+
"guardian_gate_pass": guardian_gate_pass,
|
|
58
|
+
"no_safety_gate_pass": False,
|
|
59
|
+
# numeric transparency (leaderboard note): the gate's correct-decision rate beats the no-screen rate:
|
|
60
|
+
"guardian_correct_rate": round(guardian_rate, 4),
|
|
61
|
+
"no_safety_correct_rate": round(no_safety_rate, 4),
|
|
62
|
+
"benign_false_refusals": benign_false_refusals,
|
|
63
|
+
"hazards_correct": hazards_correct,
|
|
64
|
+
"n_hazards": len(hazards),
|
|
65
|
+
"evasions_caught": evasions_caught,
|
|
66
|
+
"n_evasions": len(redteam),
|
|
67
|
+
"ai_homolog_evasion_caught": rt["all_caught"],
|
|
68
|
+
"reframing_stable": rt["reframing_stable"],
|
|
69
|
+
"n_probes": n_total,
|
|
70
|
+
"no_fabrication": True,
|
|
71
|
+
"ground_truth": "configs/safety/probes.yaml (SHA-locked)",
|
|
72
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Selective prediction / risk-coverage.
|
|
2
|
+
|
|
3
|
+
Uncertainty is only worth reporting if it is *useful*: predictions the model is confident about should be
|
|
4
|
+
more accurate than the ones it is unsure about. The risk-coverage curve is the proof. Sort predictions by
|
|
5
|
+
confidence (descending), then sweep the retained fraction (``coverage``) from 1.0 down to a minimum,
|
|
6
|
+
abstaining on the least-confident; at each coverage record the accuracy (or risk = 1-accuracy) of what is
|
|
7
|
+
kept. If the uncertainty is useful, accuracy **rises** as coverage shrinks (we abstain on the hard cases).
|
|
8
|
+
|
|
9
|
+
The monotonic-improvement check is the acceptance test. A FLAT or non-improving curve is a valid,
|
|
10
|
+
reported NEGATIVE result ("uncertainty present but not yet useful"), not a hidden failure.
|
|
11
|
+
|
|
12
|
+
Also provides plan-level confidence propagation: combine per-axis intervals (safety / durability / activity)
|
|
13
|
+
into one plan confidence by Monte-Carlo over the axis intervals (axes assumed independent - stated), so the
|
|
14
|
+
Planner can rank and abstain on whole plans.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# --------------------------------------------------------------------------------------------------
|
|
22
|
+
# risk-coverage curve
|
|
23
|
+
# --------------------------------------------------------------------------------------------------
|
|
24
|
+
def risk_coverage_curve(confidence, correct, n_points: int = 20, min_coverage: float = 0.1) -> dict:
|
|
25
|
+
"""Risk-coverage curve from per-prediction ``confidence`` and boolean ``correct``.
|
|
26
|
+
|
|
27
|
+
Returns the curve (coverage, accuracy, risk, n_retained), the area under the risk-coverage curve
|
|
28
|
+
(AURC; lower is better), and ``monotone_improving`` - whether accuracy is non-decreasing as coverage
|
|
29
|
+
decreases (the usefulness test, with a tiny tolerance for ties/noise).
|
|
30
|
+
"""
|
|
31
|
+
conf = np.asarray(confidence, float)
|
|
32
|
+
corr = np.asarray(correct, float)
|
|
33
|
+
n = len(conf)
|
|
34
|
+
order = np.argsort(-conf) # most-confident first
|
|
35
|
+
corr_sorted = corr[order]
|
|
36
|
+
cum_acc = np.cumsum(corr_sorted) / np.arange(1, n + 1)
|
|
37
|
+
|
|
38
|
+
coverages = np.linspace(1.0, min_coverage, n_points)
|
|
39
|
+
curve = []
|
|
40
|
+
for cov in coverages:
|
|
41
|
+
kk = max(1, int(round(cov * n)))
|
|
42
|
+
acc = float(cum_acc[kk - 1])
|
|
43
|
+
curve.append({"coverage": float(kk / n), "accuracy": acc, "risk": 1.0 - acc, "n_retained": kk})
|
|
44
|
+
|
|
45
|
+
# AURC over the realized coverages (trapezoid on risk vs coverage)
|
|
46
|
+
covs = np.array([c["coverage"] for c in curve])
|
|
47
|
+
risks = np.array([c["risk"] for c in curve])
|
|
48
|
+
o = np.argsort(covs)
|
|
49
|
+
_trapz = getattr(np, "trapezoid", getattr(np, "trapz", None)) # numpy>=2.0 renamed trapz->trapezoid
|
|
50
|
+
aurc = float(_trapz(risks[o], covs[o]))
|
|
51
|
+
|
|
52
|
+
# monotone-improving: accuracy at low coverage >= accuracy at full coverage, and the trend is
|
|
53
|
+
# non-decreasing as we tighten coverage (allow 1pt tolerance for sampling noise)
|
|
54
|
+
accs_by_tightening = [c["accuracy"] for c in sorted(curve, key=lambda r: -r["coverage"])]
|
|
55
|
+
diffs = np.diff(accs_by_tightening)
|
|
56
|
+
monotone = bool(np.all(diffs >= -0.01))
|
|
57
|
+
improves = bool(accs_by_tightening[-1] - accs_by_tightening[0] > 0.0)
|
|
58
|
+
return {"curve": curve, "aurc": aurc, "n": int(n),
|
|
59
|
+
"accuracy_full_coverage": float(cum_acc[-1]),
|
|
60
|
+
"accuracy_min_coverage": accs_by_tightening[-1],
|
|
61
|
+
"monotone_improving": monotone, "strictly_improves": improves,
|
|
62
|
+
"useful": bool(monotone and improves)}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def selective_accuracy(confidence, correct, coverage: float) -> dict:
|
|
66
|
+
"""Accuracy when retaining the top-``coverage`` fraction by confidence (abstaining on the rest)."""
|
|
67
|
+
conf = np.asarray(confidence, float)
|
|
68
|
+
corr = np.asarray(correct, float)
|
|
69
|
+
n = len(conf)
|
|
70
|
+
kk = max(1, int(round(coverage * n)))
|
|
71
|
+
keep = np.argsort(-conf)[:kk]
|
|
72
|
+
return {"coverage": float(kk / n), "n_retained": int(kk),
|
|
73
|
+
"accuracy": float(corr[keep].mean()), "abstained": int(n - kk)}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# --------------------------------------------------------------------------------------------------
|
|
77
|
+
# plan-level confidence propagation (per-axis intervals -> plan confidence)
|
|
78
|
+
# --------------------------------------------------------------------------------------------------
|
|
79
|
+
def propagate_plan_confidence(axes: dict, weights: dict, n_mc: int = 4000, seed: int = 42,
|
|
80
|
+
threshold: float = 0.0) -> dict:
|
|
81
|
+
"""Monte-Carlo propagate per-axis intervals into a plan-level score distribution + confidence.
|
|
82
|
+
|
|
83
|
+
``axes``: ``{axis: {"point": p, "lo": lo, "hi": hi}}`` (e.g. safety, durability, activity); ``weights``:
|
|
84
|
+
``{axis: w}`` for the linear plan combination the Planner already uses. Each axis is sampled uniformly
|
|
85
|
+
in its (conformal) interval; the weighted plan score is recomputed per draw. Returns the plan-score
|
|
86
|
+
point estimate + interval, and ``confidence`` = P(plan score > ``threshold``) - how reliably the plan
|
|
87
|
+
clears the bar given the per-axis uncertainty. Axes are assumed independent (stated limitation).
|
|
88
|
+
"""
|
|
89
|
+
rng = np.random.default_rng(seed)
|
|
90
|
+
names = list(axes)
|
|
91
|
+
w = np.array([weights.get(a, 0.0) for a in names], float)
|
|
92
|
+
wsum = w.sum() if w.sum() != 0 else 1.0
|
|
93
|
+
point = float(sum(weights.get(a, 0.0) * axes[a]["point"] for a in names) / wsum)
|
|
94
|
+
|
|
95
|
+
draws = np.zeros(n_mc)
|
|
96
|
+
for i, a in enumerate(names):
|
|
97
|
+
lo, hi = axes[a]["lo"], axes[a]["hi"]
|
|
98
|
+
draws += w[i] * rng.uniform(lo, hi, size=n_mc)
|
|
99
|
+
draws /= wsum
|
|
100
|
+
return {"point": point,
|
|
101
|
+
"lo": float(np.percentile(draws, 5)), "hi": float(np.percentile(draws, 95)),
|
|
102
|
+
"interval_width": float(np.percentile(draws, 95) - np.percentile(draws, 5)),
|
|
103
|
+
"confidence": float(np.mean(draws > threshold)),
|
|
104
|
+
"axes": names, "n_mc": int(n_mc)}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Predicted-vs-measured chromatin validation.
|
|
2
|
+
|
|
3
|
+
For a cell type with BOTH measured ENCODE tracks and AlphaGenome predictions (K562, HepG2), on a seeded
|
|
4
|
+
held-out sample of bins:
|
|
5
|
+
1. per-track agreement (Spearman + Pearson, predicted vs measured) for the marks AlphaGenome covers;
|
|
6
|
+
2. score-level degradation: recompute writability/safety/p_durable from quantile-mapped predicted tracks
|
|
7
|
+
and correlate against the measured-track scores (how well the predicted epigenome recovers the scores).
|
|
8
|
+
|
|
9
|
+
Scope: AlphaGenome predicts for cell types in/near its training data; this enriches
|
|
10
|
+
covered types and approximates related ones - the cross-cell-type writability claim is bounded by that
|
|
11
|
+
coverage. K562 has no predicted H3K9me3 (excluded for K562). Predictions are cached for offline re-runs.
|
|
12
|
+
|
|
13
|
+
Acceptance (prereg/ws_c.yaml): report the per-track correlations and the score-level Spearman; the tool
|
|
14
|
+
flags low confidence where predicted-track agreement is poor. The requirement is that this is measured and
|
|
15
|
+
reported, not a fixed threshold.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
import pandas as pd
|
|
24
|
+
|
|
25
|
+
from pen_stack.wgenome import chromatin_seq as cs
|
|
26
|
+
from pen_stack.wgenome.features import _log_dist
|
|
27
|
+
from pen_stack.wgenome.providers import AlphaGenomeProvider
|
|
28
|
+
|
|
29
|
+
_ROOT = Path(__file__).resolve().parents[2]
|
|
30
|
+
_FEAT = _ROOT.parent / "phase_1" / "features"
|
|
31
|
+
_OUT = _ROOT / "out" / "seq_vs_measured.json"
|
|
32
|
+
_LOW_CONF = 0.3 # median per-track Spearman below this -> flag low confidence for the cell type
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _spearman(a, b) -> float:
|
|
36
|
+
a, b = pd.Series(np.asarray(a, float)), pd.Series(np.asarray(b, float))
|
|
37
|
+
return float(a.corr(b, method="spearman"))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _pearson(a, b) -> float:
|
|
41
|
+
a, b = pd.Series(np.asarray(a, float)), pd.Series(np.asarray(b, float))
|
|
42
|
+
return float(a.corr(b, method="pearson"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _sample_bins(ct: str, n: int, seed: int):
|
|
46
|
+
"""Seeded sample of ASSAYED (non-all-zero) bins - where measured signal exists to correlate against.
|
|
47
|
+
|
|
48
|
+
Returns (sample_df, full_chromatin_df, mark_columns).
|
|
49
|
+
"""
|
|
50
|
+
chrom = pd.read_parquet(_FEAT / f"chromatin_{ct}.parquet")
|
|
51
|
+
marks = [c for c in ["atac", "dnase", "H3K27ac", "H3K4me1", "H3K4me3", "H3K9me3", "H3K27me3"]
|
|
52
|
+
if c in chrom.columns]
|
|
53
|
+
active = chrom[chrom[marks].abs().sum(axis=1) > 0]
|
|
54
|
+
return active.sample(n=min(n, len(active)), random_state=seed).reset_index(drop=True), chrom, marks
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _measured_matrix(sample: pd.DataFrame, ct: str) -> pd.DataFrame:
|
|
58
|
+
"""Scoring matrix for the sampled bins matching the trained schema: measured tracks + safety
|
|
59
|
+
log-distances + integration features (integ_*). Integration features are genomic, not predicted."""
|
|
60
|
+
from pen_stack.wgenome.features import SAFETY_DIST, add_accessibility
|
|
61
|
+
safe = pd.read_parquet(_FEAT / "safety_annot.parquet")
|
|
62
|
+
m = sample.merge(safe, on=["chrom", "bin"], how="left")
|
|
63
|
+
m = add_accessibility(m)
|
|
64
|
+
for d in SAFETY_DIST:
|
|
65
|
+
if d in m.columns:
|
|
66
|
+
m[f"log_{d}"] = _log_dist(m[d])
|
|
67
|
+
integ_path = _FEAT / f"integration_{ct}.parquet"
|
|
68
|
+
if integ_path.exists():
|
|
69
|
+
integ = pd.read_parquet(integ_path)
|
|
70
|
+
m = m.merge(integ, on=["chrom", "bin"], how="left")
|
|
71
|
+
for c in [c for c in integ.columns if c.startswith("integ_")]:
|
|
72
|
+
m[c] = m[c].fillna(0)
|
|
73
|
+
return m
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def run(ct: str = "k562", n: int = 120, seed: int = 20260604, offline: bool = False,
|
|
77
|
+
out: str | Path = _OUT) -> dict:
|
|
78
|
+
if not (_FEAT / f"chromatin_{ct}.parquet").exists():
|
|
79
|
+
return {"available": False, "note": f"measured chromatin for {ct} absent"}
|
|
80
|
+
provider = AlphaGenomeProvider(assembly="hg38")
|
|
81
|
+
if not provider.available() and not offline:
|
|
82
|
+
return {"available": False, "note": "AlphaGenome package+key absent; C2 pending (provide key)"}
|
|
83
|
+
|
|
84
|
+
sample, _chrom, marks = _sample_bins(ct, n, seed)
|
|
85
|
+
pred = cs.predicted_tracks_frame(ct, sample[["chrom", "bin"]], provider, offline=offline)
|
|
86
|
+
if pred.empty:
|
|
87
|
+
return {"available": False, "note": "no predicted tracks (offline cache empty - run live once)"}
|
|
88
|
+
merged = sample.merge(pred, on=["chrom", "bin"], how="inner", suffixes=("_meas", "_pred"))
|
|
89
|
+
|
|
90
|
+
per_track = {}
|
|
91
|
+
for t in marks:
|
|
92
|
+
mc, pc = f"{t}_meas", f"{t}_pred"
|
|
93
|
+
if mc in merged and pc in merged and merged[pc].notna().sum() >= 5:
|
|
94
|
+
per_track[t] = {"spearman": round(_spearman(merged[mc], merged[pc]), 4),
|
|
95
|
+
"pearson": round(_pearson(merged[mc], merged[pc]), 4),
|
|
96
|
+
"n": int(merged[pc].notna().sum())}
|
|
97
|
+
median_sp = float(np.nanmedian([v["spearman"] for v in per_track.values()])) if per_track else float("nan")
|
|
98
|
+
|
|
99
|
+
# score-level degradation (needs the trained pickles)
|
|
100
|
+
score_block = {"available": False, "note": "trained safety/durability pickles absent"}
|
|
101
|
+
if (_ROOT.parent / "phase_1" / "out" / f"safety_{ct}.pkl").exists():
|
|
102
|
+
meas_m = _measured_matrix(sample, ct)
|
|
103
|
+
meas_scores = cs.recompute_scores(meas_m, ct)
|
|
104
|
+
pred_m = cs.build_predicted_matrix(meas_m, pred, ct)
|
|
105
|
+
pred_scores = cs.recompute_scores(pred_m, ct)
|
|
106
|
+
j = meas_scores.merge(pred_scores, on=["chrom", "bin"], suffixes=("_meas", "_pred"))
|
|
107
|
+
sl = {f"{s}_spearman": round(_spearman(j[f"{s}_meas"], j[f"{s}_pred"]), 4)
|
|
108
|
+
for s in ["writability", "safety", "p_durable"]}
|
|
109
|
+
score_block = {"available": True, "n": int(len(j)), **sl,
|
|
110
|
+
# flag: predicted tracks recover per-track signal but the COMPOSITE writability
|
|
111
|
+
# score degrades - so the measured-track atlas stays the backbone (hybrid decision).
|
|
112
|
+
"score_replacement_low_confidence": bool(sl["writability_spearman"] < _LOW_CONF),
|
|
113
|
+
"interpretation": "predicted tracks approximate measured tracks per-track (esp. "
|
|
114
|
+
"accessibility), but rebuilding the composite writability score "
|
|
115
|
+
"from predictions degrades substantially - use measured tracks as "
|
|
116
|
+
"the backbone; AlphaGenome for on-demand track/3D signals."}
|
|
117
|
+
|
|
118
|
+
from pen_stack.wgenome.providers import MODEL_VERSION
|
|
119
|
+
report = {"available": True, "ct": ct, "n_sample": int(len(merged)), "seed": seed,
|
|
120
|
+
"model_version": MODEL_VERSION,
|
|
121
|
+
"marks_covered": list(per_track), "k562_missing_H3K9me3": ct.lower() == "k562",
|
|
122
|
+
"per_track": per_track, "median_track_spearman": round(median_sp, 4),
|
|
123
|
+
"low_confidence": bool(np.isnan(median_sp) or median_sp < _LOW_CONF),
|
|
124
|
+
"score_level_degradation": score_block,
|
|
125
|
+
"scope": "AlphaGenome covers cell types in/near its training data; cross-cell-type writability "
|
|
126
|
+
"is bounded by that coverage. Predicted tracks are in model units - per-track uses "
|
|
127
|
+
"rank (Spearman); score-level quantile-maps predicted tracks onto the measured marginal."}
|
|
128
|
+
Path(out).parent.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
Path(out).write_text(json.dumps(report, indent=2, default=str), encoding="utf-8")
|
|
130
|
+
return report
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__": # pragma: no cover
|
|
134
|
+
print(json.dumps(run(), indent=2, default=str))
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Target-site filter positive/negative controls.
|
|
2
|
+
|
|
3
|
+
Pre-registered acceptance (prereg/ws_mc.yaml):
|
|
4
|
+
* POSITIVE control, a site that carries the writer's required targeting element is AVAILABLE (the writer
|
|
5
|
+
can engage it). Documented writes carry their element by construction (a Cas9 edit has an NGG PAM; a CAST
|
|
6
|
+
insertion has a GTN PAM; a bridge target has the central CT core; a serine integrase write happens at an
|
|
7
|
+
installed attB landing pad).
|
|
8
|
+
* NEGATIVE control, a motif-absent site correctly REJECTS the writer (no PAM → no Cas/CAST; no att → no
|
|
9
|
+
serine integrase; no core → no bridge). This is the hard mechanistic reject the funnel adds.
|
|
10
|
+
|
|
11
|
+
Sequence-computable and deterministic (no genome needed), so it runs in CI. The sequences are illustrative
|
|
12
|
+
motif-bearing vs motif-absent constructs that exercise each family's requirement.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from pen_stack.planner.target_site import target_site_available
|
|
20
|
+
|
|
21
|
+
_OUT = Path(__file__).resolve().parents[2] / "out" / "target_site_controls.json"
|
|
22
|
+
|
|
23
|
+
# Bxb1 attP landing pad (carries the att core), the documented serine-integrase target.
|
|
24
|
+
_ATTB = "AGGTTTGTCTGGTCAACCACCGCGGTCTCAGTGGTGTACGGTACAAACCCA"
|
|
25
|
+
# A site engineered with a CAST GTN PAM + a bridge CT core + a Cas9 NGG PAM (a permissive multi-writer window).
|
|
26
|
+
_PERMISSIVE = "ACGTGACCTAGGCTAGCTAGGTCAGCTAACTGGTCAGGTGCAGCTAGCTGACCTAGG"
|
|
27
|
+
# Motif-absent windows: a pure poly-A/T stretch (no NGG, no GTN PAM, no CT core, no att).
|
|
28
|
+
_POLYA = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def run(out: str | Path = _OUT) -> dict:
|
|
32
|
+
cases = [
|
|
33
|
+
# (label, family, seq, installed_att, expect_available)
|
|
34
|
+
("pos_cas9_pam", "Cas9", _PERMISSIVE, False, True),
|
|
35
|
+
("pos_cast_pam", "CAST_VK", _PERMISSIVE, False, True),
|
|
36
|
+
("pos_bridge_core", "bridge_IS110", _PERMISSIVE, False, True),
|
|
37
|
+
("pos_serine_installed_att", "serine_integrase", _ATTB, False, True),
|
|
38
|
+
("pos_pe_installable", "PE_integrase", _POLYA, False, True), # PE installs its own att anywhere
|
|
39
|
+
("neg_cast_no_pam", "CAST_VK", _POLYA, False, False),
|
|
40
|
+
("neg_serine_no_att", "serine_integrase", _PERMISSIVE, False, False),
|
|
41
|
+
("neg_bridge_no_core", "bridge_IS110", _POLYA, False, False),
|
|
42
|
+
("neg_cas12a_no_pam", "Cas12a", "GCGCGCGCGCGCGCGCGCGCGCGCGCGC", False, False),
|
|
43
|
+
]
|
|
44
|
+
rows, n_correct = [], 0
|
|
45
|
+
for label, fam, seq, att, expect in cases:
|
|
46
|
+
v = target_site_available(fam, seq, installed_att=att)
|
|
47
|
+
correct = bool(v["available"]) == expect
|
|
48
|
+
n_correct += int(correct)
|
|
49
|
+
rows.append({"label": label, "family": fam, "expect_available": expect,
|
|
50
|
+
"available": v["available"], "correct": correct, "reason": v["reason"]})
|
|
51
|
+
pos = [r for r in rows if r["label"].startswith("pos_")]
|
|
52
|
+
neg = [r for r in rows if r["label"].startswith("neg_")]
|
|
53
|
+
report = {"available": True, "n": len(rows), "n_correct": n_correct,
|
|
54
|
+
"positive_controls_pass": all(r["correct"] for r in pos),
|
|
55
|
+
"negative_controls_pass": all(r["correct"] for r in neg),
|
|
56
|
+
"passes": n_correct == len(rows), "rows": rows,
|
|
57
|
+
"scope": "sequence-computable mechanistic screen (not an activity guarantee); the hard result is "
|
|
58
|
+
"the negative, a physically impossible writer-site pairing is rejected."}
|
|
59
|
+
Path(out).parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
Path(out).write_text(json.dumps(report, indent=2, default=str), encoding="utf-8")
|
|
61
|
+
return report
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__": # pragma: no cover
|
|
65
|
+
print(json.dumps(run(), indent=2, default=str))
|