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