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