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,250 @@
1
+ """Calibrated uncertainty for the writable-genome heads.
2
+
3
+ Split / CV+ conformal *wrappers* around the EXISTING LightGBM heads (safety, durability-silenced
4
+ classification; durability-expression regression). Nothing is retrained here - we take a head's
5
+ out-of-fold (or held-out) predictions, learn a nonconformity quantile on a calibration set, and turn
6
+ point scores into:
7
+
8
+ * regression -> prediction INTERVALS at a nominal coverage (durability expression);
9
+ * classification -> calibrated prediction SETS + a coverage guarantee (safety, silenced).
10
+
11
+ Why conformal (not just isotonic): conformal gives a *distribution-free, finite-sample* coverage
12
+ guarantee - "the 90% interval covers the truth >= 90% of the time" - which is exactly the trust claim
13
+ the design makes. The guarantee is **marginal** (population-level), not conditional; small calibration N (the
14
+ GSH/TRIP gold sets are small) yields **wide** intervals - that width is a reported output, reported with N,
15
+ never hidden. When marginal coverage fails under the chromosome-grouped distribution shift we fall back to
16
+ **group / Mondrian (class- or group-conditional) conformal** and report the gap rather than relax the target.
17
+
18
+ Design: model-agnostic - the calibrators take arrays of (prediction, truth), so the same code calibrates a
19
+ synthetic head in a unit test and the real OOF predictions on the VM. Reuses ``adapt.report`` for ECE /
20
+ reliability so there is one calibration vocabulary across the stack.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import math
26
+ from dataclasses import dataclass, field
27
+ from pathlib import Path
28
+
29
+ import numpy as np
30
+
31
+ from pen_stack.adapt.report import _auroc, _ece
32
+
33
+
34
+ # finite-sample conformal quantile
35
+ def conformal_quantile(scores: np.ndarray, alpha: float) -> float:
36
+ """The conformal (1-alpha) quantile of calibration nonconformity ``scores`` with the finite-sample
37
+ correction: the rank is ceil((n+1)(1-alpha)) / n. Returns +inf when the correction exceeds n (too few
38
+ calibration points to guarantee the requested coverage - a 'cannot certify' rather than a
39
+ falsely tight bound)."""
40
+ s = np.sort(np.asarray(scores, float))
41
+ n = len(s)
42
+ if n == 0:
43
+ return float("inf")
44
+ k = math.ceil((n + 1) * (1.0 - alpha))
45
+ if k > n:
46
+ return float("inf") # cannot certify this coverage at this N
47
+ return float(s[k - 1])
48
+
49
+
50
+ # regression: normalized-residual conformal -> prediction intervals
51
+ @dataclass
52
+ class ConformalRegressor:
53
+ """Split/CV conformal for a regression head (durability expression).
54
+
55
+ Nonconformity = ``|y - yhat| / sigma`` (normalized absolute residual; ``sigma`` is an optional
56
+ per-point spread estimate, e.g. a quantile-spread model - defaults to 1.0, i.e. plain absolute
57
+ residual). ``calibrate`` stores the conformal quantile ``qhat``; ``interval`` returns a symmetric
58
+ band ``yhat +/- qhat * sigma``. With ``groups`` the quantile is computed per group (Mondrian /
59
+ group-conformal) for conditional coverage under the chromosome-grouped split.
60
+ """
61
+ alpha: float = 0.10
62
+ qhat: float = float("nan")
63
+ qhat_by_group: dict = field(default_factory=dict)
64
+ n_cal: int = 0
65
+
66
+ def calibrate(self, y_true, y_pred, sigma=None, groups=None) -> "ConformalRegressor":
67
+ y_true = np.asarray(y_true, float)
68
+ y_pred = np.asarray(y_pred, float)
69
+ sigma = np.ones_like(y_true) if sigma is None else np.asarray(sigma, float)
70
+ sigma = np.where(sigma <= 0, 1e-9, sigma)
71
+ scores = np.abs(y_true - y_pred) / sigma
72
+ self.n_cal = int(len(scores))
73
+ self.qhat = conformal_quantile(scores, self.alpha)
74
+ if groups is not None:
75
+ groups = np.asarray(groups)
76
+ self.qhat_by_group = {
77
+ str(g): conformal_quantile(scores[groups == g], self.alpha)
78
+ for g in np.unique(groups)
79
+ }
80
+ return self
81
+
82
+ def _q(self, group=None) -> float:
83
+ if group is not None and self.qhat_by_group:
84
+ return self.qhat_by_group.get(str(group), self.qhat)
85
+ return self.qhat
86
+
87
+ def interval(self, y_pred, sigma=None, group=None):
88
+ y_pred = np.asarray(y_pred, float)
89
+ sigma = np.ones_like(y_pred) if sigma is None else np.asarray(sigma, float)
90
+ half = self._q(group) * sigma
91
+ return y_pred - half, y_pred + half
92
+
93
+ def coverage(self, y_true, y_pred, sigma=None, group=None) -> dict:
94
+ y_true = np.asarray(y_true, float)
95
+ lo, hi = self.interval(y_pred, sigma, group)
96
+ covered = (y_true >= lo) & (y_true <= hi)
97
+ width = hi - lo
98
+ return {"nominal": 1 - self.alpha, "empirical_coverage": float(np.mean(covered)),
99
+ "mean_width": float(np.mean(width)), "median_width": float(np.median(width)),
100
+ "n": int(len(y_true)), "qhat": self._q(group),
101
+ "within_tol": bool(abs(np.mean(covered) - (1 - self.alpha)) <= 0.03)}
102
+
103
+
104
+ # classification: APS conformal -> calibrated prediction sets
105
+ @dataclass
106
+ class ConformalClassifier:
107
+ """Adaptive-Prediction-Set (APS) conformal for a binary head (safety genotoxic / durability silenced).
108
+
109
+ APS nonconformity (Romano et al. 2020): for a calibration point, sort class probabilities descending
110
+ and accumulate until the TRUE class is reached - that cumulative mass is the score. ``qhat`` is the
111
+ conformal (1-alpha) quantile of those scores. A test prediction set adds classes from the top until the
112
+ cumulative probability reaches ``qhat``, guaranteeing the set contains the truth with prob >= 1-alpha.
113
+ With ``mondrian=True`` the quantile is computed per true class (class-conditional coverage) - the
114
+ the choice under class imbalance (genotoxic sites are rare).
115
+
116
+ Binary classes are 0/1; ``p1`` is P(class=1). Also exposes the set as one of {}, {0}, {1}, {0,1} and a
117
+ simple calibrated confidence (1 - alpha when the set is a singleton, lower when ambiguous/empty).
118
+ """
119
+ alpha: float = 0.10
120
+ mondrian: bool = True
121
+ qhat: float = float("nan")
122
+ qhat_by_class: dict = field(default_factory=dict)
123
+ n_cal: int = 0
124
+
125
+ @staticmethod
126
+ def _aps_score(p1: float, y: int) -> float:
127
+ p = [(1 - p1, 0), (p1, 1)]
128
+ p.sort(reverse=True) # descending probability
129
+ cum = 0.0
130
+ for prob, cls in p:
131
+ cum += prob
132
+ if cls == y:
133
+ return cum
134
+ return cum
135
+
136
+ def calibrate(self, y_true, p1) -> "ConformalClassifier":
137
+ y_true = np.asarray(y_true, int)
138
+ p1 = np.asarray(p1, float)
139
+ scores = np.array([self._aps_score(pp, yy) for pp, yy in zip(p1, y_true)])
140
+ self.n_cal = int(len(scores))
141
+ self.qhat = conformal_quantile(scores, self.alpha)
142
+ if self.mondrian:
143
+ self.qhat_by_class = {
144
+ int(c): conformal_quantile(scores[y_true == c], self.alpha)
145
+ for c in np.unique(y_true)
146
+ }
147
+ return self
148
+
149
+ def _q(self, cls: int) -> float:
150
+ if self.mondrian and self.qhat_by_class:
151
+ return self.qhat_by_class.get(int(cls), self.qhat)
152
+ return self.qhat
153
+
154
+ def predict_set(self, p1: float) -> set:
155
+ """Smallest top-down set whose cumulative probability reaches the (class-appropriate) qhat."""
156
+ p = [(1 - float(p1), 0), (float(p1), 1)]
157
+ p.sort(reverse=True)
158
+ out, cum = set(), 0.0
159
+ for prob, cls in p:
160
+ out.add(cls)
161
+ cum += prob
162
+ if cum >= self._q(cls):
163
+ break
164
+ return out
165
+
166
+ def coverage(self, y_true, p1) -> dict:
167
+ y_true = np.asarray(y_true, int)
168
+ p1 = np.asarray(p1, float)
169
+ sets = [self.predict_set(pp) for pp in p1]
170
+ covered = np.array([yy in s for s, yy in zip(sets, y_true)])
171
+ sizes = np.array([len(s) for s in sets])
172
+ per_class = {int(c): float(np.mean(covered[y_true == c]))
173
+ for c in np.unique(y_true)} if self.mondrian else {}
174
+ biny = y_true if set(np.unique(y_true)) <= {0, 1} else (y_true >= 0.5).astype(int)
175
+ return {"nominal": 1 - self.alpha, "empirical_coverage": float(np.mean(covered)),
176
+ "per_class_coverage": per_class, "mean_set_size": float(np.mean(sizes)),
177
+ "singleton_rate": float(np.mean(sizes == 1)), "n": int(len(y_true)),
178
+ "ece": _ece(p1, biny), "auroc": _auroc(list(p1), list(biny)),
179
+ "within_tol": bool(abs(np.mean(covered) - (1 - self.alpha)) <= 0.03)}
180
+
181
+
182
+ # reliability diagram (for figures / the model card)
183
+ def reliability_curve(p1, y_true, n_bins: int = 10) -> dict:
184
+ """Binned (mean predicted prob, empirical frequency, count) - the reliability diagram data + ECE."""
185
+ p1 = np.asarray(p1, float)
186
+ y = np.asarray(y_true, float)
187
+ edges = np.linspace(0, 1, n_bins + 1)
188
+ rows = []
189
+ for i in range(n_bins):
190
+ hi_incl = i == n_bins - 1
191
+ m = (p1 >= edges[i]) & (p1 <= edges[i + 1] if hi_incl else p1 < edges[i + 1])
192
+ if m.sum():
193
+ rows.append({"bin_lo": float(edges[i]), "bin_hi": float(edges[i + 1]),
194
+ "mean_pred": float(p1[m].mean()), "emp_freq": float(y[m].mean()),
195
+ "count": int(m.sum())})
196
+ return {"bins": rows, "ece": _ece(p1, (y >= 0.5).astype(float)), "n": int(len(p1))}
197
+
198
+
199
+ # bundle: calibrate every head + serialize the calibration artifact + model card
200
+ class ConformalWrapper:
201
+ """Bundles the per-head conformal calibrators and (de)serializes the calibration artifact.
202
+
203
+ Holds an optional ``ood`` widening hook (a callable score-> multiplier from :mod:`wgenome.ood`) so an
204
+ OOD query widens its interval / inflates its nonconformity. Persisted as plain JSON (qhat values + N +
205
+ nominal) so a release ships the calibration, not the calibration data.
206
+ """
207
+
208
+ def __init__(self, alpha: float = 0.10):
209
+ self.alpha = alpha
210
+ self.reg: dict[str, ConformalRegressor] = {}
211
+ self.clf: dict[str, ConformalClassifier] = {}
212
+ self.meta: dict = {"alpha": alpha, "nominal_coverage": 1 - alpha}
213
+
214
+ def add_regressor(self, name: str, r: ConformalRegressor) -> None:
215
+ self.reg[name] = r
216
+
217
+ def add_classifier(self, name: str, c: ConformalClassifier) -> None:
218
+ self.clf[name] = c
219
+
220
+ def to_dict(self) -> dict:
221
+ return {"alpha": self.alpha, "nominal_coverage": 1 - self.alpha,
222
+ "regressors": {k: {"qhat": v.qhat, "qhat_by_group": v.qhat_by_group, "n_cal": v.n_cal}
223
+ for k, v in self.reg.items()},
224
+ "classifiers": {k: {"qhat": v.qhat, "qhat_by_class": v.qhat_by_class,
225
+ "mondrian": v.mondrian, "n_cal": v.n_cal}
226
+ for k, v in self.clf.items()},
227
+ "meta": self.meta}
228
+
229
+ def save(self, path: str | Path) -> str:
230
+ path = Path(path)
231
+ path.parent.mkdir(parents=True, exist_ok=True)
232
+ path.write_text(json.dumps(self.to_dict(), indent=2, default=str), encoding="utf-8")
233
+ return str(path)
234
+
235
+ @classmethod
236
+ def load(cls, path: str | Path) -> "ConformalWrapper":
237
+ d = json.loads(Path(path).read_text(encoding="utf-8"))
238
+ w = cls(alpha=d["alpha"])
239
+ for k, v in d.get("regressors", {}).items():
240
+ r = ConformalRegressor(alpha=d["alpha"], qhat=v["qhat"],
241
+ qhat_by_group=v.get("qhat_by_group", {}), n_cal=v.get("n_cal", 0))
242
+ w.reg[k] = r
243
+ for k, v in d.get("classifiers", {}).items():
244
+ c = ConformalClassifier(alpha=d["alpha"], mondrian=v.get("mondrian", True),
245
+ qhat=v["qhat"],
246
+ qhat_by_class={int(kk): vv for kk, vv in v.get("qhat_by_class", {}).items()},
247
+ n_cal=v.get("n_cal", 0))
248
+ w.clf[k] = c
249
+ w.meta = d.get("meta", {})
250
+ return w
@@ -0,0 +1,72 @@
1
+ """Writability integration.
2
+
3
+ Combines the three layers into a transparent, DECOMPOSABLE per-locus writability profile (components
4
+ kept visible; never collapsed into one opaque number):
5
+
6
+ writability = f(safety, durability, reachability)
7
+
8
+ - safety: 1 - P(genotoxic) from the safety model (calibrated risk; safe-harbour discriminating).
9
+ - durability: P(durable | epigenome) = 1 - P(silenced), the mouse-trained conditional function APPLIED
10
+ to the human epigenome's histone marks (the cell-type-transfer the design hinges on).
11
+ - reachability: WT-KB writer set + tier (Tier-1 reprogrammable writers are broadly available at 1 kb;
12
+ fine-grained site choice is a design-time concern handled by the Planner).
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import pickle
17
+ from pathlib import Path
18
+
19
+ import numpy as np
20
+ import pandas as pd
21
+
22
+ from pen_stack.wgenome.features import feature_columns
23
+
24
+
25
+ def load_pickle(path: str):
26
+ with open(path, "rb") as fh:
27
+ return pickle.load(fh)
28
+
29
+
30
+ def apply_safety(matrix: pd.DataFrame, safety_model) -> np.ndarray:
31
+ feats = feature_columns(matrix)
32
+ p_genotoxic = safety_model.predict_proba(matrix[feats].astype("float32").fillna(0.0))[:, 1]
33
+ return 1.0 - p_genotoxic # safety = 1 - risk
34
+
35
+
36
+ def apply_durability(matrix: pd.DataFrame, dur_models: dict) -> tuple[np.ndarray, np.ndarray]:
37
+ """Apply the mouse-trained conditional function to the human epigenome's histone marks.
38
+
39
+ ROBUST to partial chromatin panels (e.g. CD34+ HSPC lacks some tracks): every model feature is
40
+ provided in the trained order; tracks absent from this cell type are passed as NaN, which LightGBM
41
+ handles natively. This is the 'graceful degradation under partial annotation' behaviour, by design.
42
+ """
43
+ X = pd.DataFrame(index=matrix.index)
44
+ for f in dur_models["features"]: # exact training feature set + order
45
+ X[f] = matrix[f].astype("float32") if f in matrix.columns else np.nan
46
+ expr = dur_models["reg"].predict(X)
47
+ p_silenced = dur_models["clf"].predict_proba(X)[:, 1]
48
+ return expr, 1.0 - p_silenced # predicted expression, P(durable)
49
+
50
+
51
+ def build_writability(matrix: pd.DataFrame, safety_model, dur_models: dict,
52
+ w_safety: float = 0.5, w_durability: float = 0.5,
53
+ out_parquet: str | None = None) -> pd.DataFrame:
54
+ out = matrix[["chrom", "bin"]].copy()
55
+ out["safety"] = apply_safety(matrix, safety_model)
56
+ expr, p_durable = apply_durability(matrix, dur_models)
57
+ out["pred_expression"] = expr
58
+ out["p_durable"] = p_durable
59
+ # reachability: Tier-1 reprogrammable writers broadly available at locus level (annotation)
60
+ out["reachable_tier1"] = "bridge_IS110;Cas9;Cas12a"
61
+ # decomposable composite (documented weights; components above stay visible)
62
+ out["writability"] = w_safety * out["safety"] + w_durability * out["p_durable"]
63
+ if out_parquet:
64
+ Path(out_parquet).parent.mkdir(parents=True, exist_ok=True)
65
+ out.to_parquet(out_parquet, index=False)
66
+ return out
67
+
68
+
69
+ def rank_loci_near(writ_df: pd.DataFrame, chrom: str, start: int, end: int, k: int = 10) -> pd.DataFrame:
70
+ """Inverse query seed (inverse-design planner): rank writable bins in a window."""
71
+ w = writ_df.query("chrom == @chrom and bin*1000 >= @start and bin*1000 <= @end")
72
+ return w.sort_values("writability", ascending=False).head(k)