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,454 @@
|
|
|
1
|
+
"""Learned cassette x context position-effect model.
|
|
2
|
+
|
|
3
|
+
Replaces the closed-form expression heuristic with a learned, **trained-conformal** model. Factored, decomposable
|
|
4
|
+
(not opaque):
|
|
5
|
+
|
|
6
|
+
E_raw ~= f_cassette(cassette) # the cassette's intrinsic strength (per-cassette mean)
|
|
7
|
+
+ g_context(chromatin features) # the position effect, a LightGBM on local chromatin
|
|
8
|
+
(+ h_interaction, reported) # does the context function differ by cassette? (separability)
|
|
9
|
+
|
|
10
|
+
`g_context` is supervised on the residual `E_raw - f_cassette`, so it learns the *position* effect on a scale
|
|
11
|
+
comparable across cassettes. A `silenced` classifier shares the same features. Both are wrapped with the EXISTING
|
|
12
|
+
`wgenome.uncertainty.ConformalRegressor` (chromosome-Mondrian) and `wgenome.ood.OODDetector`, so a prediction is a
|
|
13
|
+
*calibrated interval that widens out of distribution*, which is exactly the "trained conformal" property this model
|
|
14
|
+
previously lacked (it was a heuristic +/-0.20 band).
|
|
15
|
+
|
|
16
|
+
Pre-registered acceptance gate: the learned model ships ONLY if it beats the prior durability head
|
|
17
|
+
(context-only) AND the heuristic on chromosome-blocked CV; otherwise the baseline is retained and the negative is
|
|
18
|
+
reported. Never tune to the test. No fabrication: every metric here comes from a real CV run on real supervision.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import pickle
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
import pandas as pd
|
|
28
|
+
from scipy.stats import spearmanr
|
|
29
|
+
from sklearn.metrics import roc_auc_score
|
|
30
|
+
from sklearn.model_selection import GroupKFold
|
|
31
|
+
|
|
32
|
+
from pen_stack.twin.data.position_effect import FEATURE_COLS
|
|
33
|
+
from pen_stack.wgenome.ood import OODDetector
|
|
34
|
+
from pen_stack.wgenome.uncertainty import ConformalRegressor
|
|
35
|
+
|
|
36
|
+
_LGB = dict(n_estimators=400, learning_rate=0.03, num_leaves=31, subsample=0.8,
|
|
37
|
+
random_state=42, n_jobs=-1, verbosity=-1)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _lgb_reg(seed: int = 42):
|
|
41
|
+
import lightgbm as lgb
|
|
42
|
+
return lgb.LGBMRegressor(**{**_LGB, "random_state": seed})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _lgb_clf(seed: int = 42):
|
|
46
|
+
import lightgbm as lgb
|
|
47
|
+
return lgb.LGBMClassifier(**{**_LGB, "random_state": seed})
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _feats(df: pd.DataFrame) -> list[str]:
|
|
51
|
+
return [c for c in FEATURE_COLS if c in df.columns]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _groups(df: pd.DataFrame) -> np.ndarray:
|
|
55
|
+
return df["chrom"].astype("category").cat.codes.to_numpy()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _boot_delta_ci(a_pred, b_pred, y, kind: str, groups=None, reps: int = 2000, seed: int = 20260619):
|
|
59
|
+
"""Paired bootstrap 95% CI on (metric(a) - metric(b)). kind in {'spearman','auroc'}. Resamples whole
|
|
60
|
+
chromosome groups when `groups` is given (respects the blocked structure), else rows."""
|
|
61
|
+
rng = np.random.default_rng(seed)
|
|
62
|
+
a_pred, b_pred, y = map(lambda x: np.asarray(x, float), (a_pred, b_pred, y))
|
|
63
|
+
n = len(y)
|
|
64
|
+
|
|
65
|
+
def _metric(p, yy):
|
|
66
|
+
if kind == "spearman":
|
|
67
|
+
return spearmanr(p, yy).statistic
|
|
68
|
+
return roc_auc_score(yy.astype(int), p) if len(np.unique(yy)) > 1 else np.nan
|
|
69
|
+
|
|
70
|
+
if groups is not None:
|
|
71
|
+
groups = np.asarray(groups)
|
|
72
|
+
uniq = np.unique(groups)
|
|
73
|
+
members = {g: np.where(groups == g)[0] for g in uniq}
|
|
74
|
+
deltas = []
|
|
75
|
+
for _ in range(reps):
|
|
76
|
+
if groups is not None:
|
|
77
|
+
pick = rng.choice(uniq, size=len(uniq), replace=True)
|
|
78
|
+
idx = np.concatenate([members[g] for g in pick])
|
|
79
|
+
else:
|
|
80
|
+
idx = rng.integers(0, n, n)
|
|
81
|
+
if len(np.unique(y[idx])) < 2 and kind == "auroc":
|
|
82
|
+
continue
|
|
83
|
+
deltas.append(_metric(a_pred[idx], y[idx]) - _metric(b_pred[idx], y[idx]))
|
|
84
|
+
deltas = np.asarray([d for d in deltas if np.isfinite(d)])
|
|
85
|
+
if not len(deltas):
|
|
86
|
+
return {"delta_mean": float("nan"), "ci95": [float("nan"), float("nan")], "excludes_zero": False}
|
|
87
|
+
lo, hi = np.percentile(deltas, [2.5, 97.5])
|
|
88
|
+
return {"delta_mean": float(np.mean(deltas)), "ci95": [float(lo), float(hi)],
|
|
89
|
+
"excludes_zero": bool(lo > 0 or hi < 0)}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class PositionEffectModel:
|
|
94
|
+
"""The learned, factored, trained-conformal position-effect model. Decomposable: `cassette_means`
|
|
95
|
+
(f_cassette), `reg` (g_context on the residual), `clf` (silenced), `conformal` (interval), `ood` (widening)."""
|
|
96
|
+
features: list[str] = field(default_factory=list)
|
|
97
|
+
cassette_means: dict = field(default_factory=dict)
|
|
98
|
+
global_mean: float = 0.0
|
|
99
|
+
reg: object | None = None # g_context: chromatin -> residual expression
|
|
100
|
+
clf: object | None = None # silenced classifier
|
|
101
|
+
conformal: ConformalRegressor | None = None
|
|
102
|
+
ood: OODDetector | None = None
|
|
103
|
+
meta: dict = field(default_factory=dict)
|
|
104
|
+
|
|
105
|
+
# ---- fit ------------------------------------------------------------------------------------
|
|
106
|
+
def fit(self, df: pd.DataFrame, seed: int = 42) -> "PositionEffectModel":
|
|
107
|
+
self.features = _feats(df)
|
|
108
|
+
self.global_mean = float(df["expression_raw"].mean())
|
|
109
|
+
self.cassette_means = {str(c): float(g["expression_raw"].mean())
|
|
110
|
+
for c, g in df.groupby("cassette")}
|
|
111
|
+
resid = df["expression_raw"].to_numpy() - self._f_cassette(df["cassette"])
|
|
112
|
+
X = df[self.features].astype("float32").fillna(0.0)
|
|
113
|
+
self.reg = _lgb_reg(seed).fit(X, resid)
|
|
114
|
+
self.clf = _lgb_clf(seed).fit(X, df["silenced"].astype(int).to_numpy())
|
|
115
|
+
self.ood = OODDetector(method="mahalanobis").fit(X.to_numpy())
|
|
116
|
+
self.meta = {"n": int(len(df)), "features": self.features,
|
|
117
|
+
"cassettes": list(self.cassette_means), "global_mean": self.global_mean,
|
|
118
|
+
"expr_min": float(df["expression_raw"].min()),
|
|
119
|
+
"expr_max": float(df["expression_raw"].max()),
|
|
120
|
+
"datasets": sorted(df["dataset"].unique()), "cell_types": sorted(df["cell_type"].unique())}
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
def relative(self, yhat: float) -> float | None:
|
|
124
|
+
"""Monotone min-max rescale of the native (log2) prediction to [0,1] over the training range, for
|
|
125
|
+
UI comparability with the mechanistic relative_expression. Clearly a rescale, not a new quantity."""
|
|
126
|
+
lo, hi = self.meta.get("expr_min"), self.meta.get("expr_max")
|
|
127
|
+
if lo is None or hi is None or hi <= lo:
|
|
128
|
+
return None
|
|
129
|
+
return float(min(1.0, max(0.0, (float(yhat) - lo) / (hi - lo))))
|
|
130
|
+
|
|
131
|
+
def _f_cassette(self, cassette: pd.Series | list) -> np.ndarray:
|
|
132
|
+
return np.array([self.cassette_means.get(str(c), self.global_mean) for c in cassette], float)
|
|
133
|
+
|
|
134
|
+
# ---- point prediction (decomposed) ----------------------------------------------------------
|
|
135
|
+
def predict_expression(self, df: pd.DataFrame) -> np.ndarray:
|
|
136
|
+
X = df[self.features].astype("float32").fillna(0.0)
|
|
137
|
+
return self._f_cassette(df["cassette"]) + self.reg.predict(X)
|
|
138
|
+
|
|
139
|
+
def predict_silenced(self, df: pd.DataFrame) -> np.ndarray:
|
|
140
|
+
X = df[self.features].astype("float32").fillna(0.0)
|
|
141
|
+
return self.clf.predict_proba(X)[:, 1]
|
|
142
|
+
|
|
143
|
+
# ---- interval (trained conformal, OOD-widened) ----------------------------------------------
|
|
144
|
+
def predict_interval(self, df: pd.DataFrame) -> dict:
|
|
145
|
+
yhat = self.predict_expression(df)
|
|
146
|
+
if self.conformal is None or not np.isfinite(self.conformal.qhat):
|
|
147
|
+
return {"yhat": yhat, "lo": None, "hi": None, "interval_kind": "uncalibrated (no conformal qhat)"}
|
|
148
|
+
X = df[self.features].astype("float32").fillna(0.0).to_numpy()
|
|
149
|
+
widen = self.ood.widen_factor(X) if self.ood is not None else np.ones(len(df))
|
|
150
|
+
lo, hi = self.conformal.interval(yhat, sigma=widen)
|
|
151
|
+
return {"yhat": yhat, "lo": lo, "hi": hi, "ood_widen": widen,
|
|
152
|
+
"nominal_coverage": 1 - self.conformal.alpha,
|
|
153
|
+
"interval_kind": "trained split-conformal (chromosome-blocked OOF; per-chromosome Mondrian qhats "
|
|
154
|
+
"computed, GLOBAL qhat served, a query has no chromosome at serve time), OOD-widened"}
|
|
155
|
+
|
|
156
|
+
# ---- persistence ----------------------------------------------------------------------------
|
|
157
|
+
def save(self, path: str | Path) -> str:
|
|
158
|
+
path = Path(path)
|
|
159
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
with open(path, "wb") as fh:
|
|
161
|
+
pickle.dump({"features": self.features, "cassette_means": self.cassette_means,
|
|
162
|
+
"global_mean": self.global_mean, "reg": self.reg, "clf": self.clf,
|
|
163
|
+
"conformal": self.conformal, "ood": self.ood, "meta": self.meta}, fh)
|
|
164
|
+
return str(path)
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def load(cls, path: str | Path) -> "PositionEffectModel":
|
|
168
|
+
with open(path, "rb") as fh:
|
|
169
|
+
d = pickle.load(fh)
|
|
170
|
+
return cls(**d)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# --------------------------------------------------------------------------------------------------
|
|
174
|
+
# evaluation: chromosome-blocked CV, learned model vs context-only durability-head baseline
|
|
175
|
+
# --------------------------------------------------------------------------------------------------
|
|
176
|
+
def evaluate(df: pd.DataFrame, seed: int = 42, n_splits: int = 5) -> dict:
|
|
177
|
+
"""Chromosome-blocked GroupKFold OOF. Compares the FACTORED model (f_cassette + g_context) to the
|
|
178
|
+
CONTEXT-ONLY baseline (the prior durability head: chromatin -> expression, no cassette term) and to a
|
|
179
|
+
cassette-only baseline. Reports expression Spearman, silenced AUROC, paired bootstrap CIs on the deltas,
|
|
180
|
+
and the interaction (separability) variance share. Returns OOF arrays for conformal calibration.
|
|
181
|
+
"""
|
|
182
|
+
feats = _feats(df)
|
|
183
|
+
X = df[feats].astype("float32").fillna(0.0)
|
|
184
|
+
y = df["expression_raw"].to_numpy()
|
|
185
|
+
ysil = df["silenced"].astype(int).to_numpy()
|
|
186
|
+
cassette = df["cassette"]
|
|
187
|
+
g = _groups(df)
|
|
188
|
+
k = min(n_splits, len(np.unique(g)))
|
|
189
|
+
gkf = GroupKFold(n_splits=k)
|
|
190
|
+
|
|
191
|
+
oof_factored = np.zeros(len(df))
|
|
192
|
+
oof_context = np.zeros(len(df)) # durability head = chromatin-only
|
|
193
|
+
oof_cassette = np.zeros(len(df)) # cassette-only (f_cassette)
|
|
194
|
+
oof_sil_factored = np.zeros(len(df))
|
|
195
|
+
oof_sil_context = np.zeros(len(df))
|
|
196
|
+
oof_resid = np.full(len(df), np.nan)
|
|
197
|
+
|
|
198
|
+
for tr, te in gkf.split(X, y, g):
|
|
199
|
+
cm = {str(c): float(y[tr][cassette.iloc[tr] == c].mean()) for c in cassette.iloc[tr].unique()}
|
|
200
|
+
gm = float(y[tr].mean())
|
|
201
|
+
f_tr = np.array([cm.get(str(c), gm) for c in cassette.iloc[tr]])
|
|
202
|
+
f_te = np.array([cm.get(str(c), gm) for c in cassette.iloc[te]])
|
|
203
|
+
# factored: cassette mean + g_context on residual
|
|
204
|
+
reg_ctx = _lgb_reg(seed).fit(X.iloc[tr], y[tr] - f_tr)
|
|
205
|
+
oof_factored[te] = f_te + reg_ctx.predict(X.iloc[te])
|
|
206
|
+
oof_resid[te] = y[te] - oof_factored[te]
|
|
207
|
+
# context-only baseline (durability head): chromatin -> expression directly
|
|
208
|
+
reg_base = _lgb_reg(seed).fit(X.iloc[tr], y[tr])
|
|
209
|
+
oof_context[te] = reg_base.predict(X.iloc[te])
|
|
210
|
+
oof_cassette[te] = f_te
|
|
211
|
+
# silenced: factored (cassette one-hot + chromatin) vs context-only
|
|
212
|
+
cas_oh_tr = pd.get_dummies(cassette.iloc[tr]).astype("float32")
|
|
213
|
+
cas_oh_te = pd.get_dummies(cassette.iloc[te]).reindex(columns=cas_oh_tr.columns, fill_value=0).astype("float32")
|
|
214
|
+
Xf_tr = pd.concat([X.iloc[tr].reset_index(drop=True), cas_oh_tr.reset_index(drop=True)], axis=1)
|
|
215
|
+
Xf_te = pd.concat([X.iloc[te].reset_index(drop=True), cas_oh_te.reset_index(drop=True)], axis=1)
|
|
216
|
+
oof_sil_factored[te] = _lgb_clf(seed).fit(Xf_tr, ysil[tr]).predict_proba(Xf_te)[:, 1]
|
|
217
|
+
oof_sil_context[te] = _lgb_clf(seed).fit(X.iloc[tr], ysil[tr]).predict_proba(X.iloc[te])[:, 1]
|
|
218
|
+
|
|
219
|
+
rho_factored = float(spearmanr(oof_factored, y).statistic)
|
|
220
|
+
rho_context = float(spearmanr(oof_context, y).statistic)
|
|
221
|
+
rho_cassette = float(spearmanr(oof_cassette, y).statistic)
|
|
222
|
+
auroc_factored = float(roc_auc_score(ysil, oof_sil_factored))
|
|
223
|
+
auroc_context = float(roc_auc_score(ysil, oof_sil_context))
|
|
224
|
+
|
|
225
|
+
# separability: variance share of the interaction term, fit g per cassette vs pooled on OOF residual scale
|
|
226
|
+
inter = _interaction_share(df, feats, g, seed)
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
"n": int(len(df)), "n_chrom_folds": int(k), "features": feats,
|
|
230
|
+
"expression": {
|
|
231
|
+
"rho_factored": rho_factored, "rho_context_only_durability_head": rho_context,
|
|
232
|
+
"rho_cassette_only": rho_cassette,
|
|
233
|
+
"delta_factored_vs_context": _boot_delta_ci(oof_factored, oof_context, y, "spearman", groups=g),
|
|
234
|
+
"delta_factored_vs_cassette": _boot_delta_ci(oof_factored, oof_cassette, y, "spearman", groups=g),
|
|
235
|
+
},
|
|
236
|
+
"silenced": {
|
|
237
|
+
"auroc_factored": auroc_factored, "auroc_context_only_durability_head": auroc_context,
|
|
238
|
+
"delta_factored_vs_context": _boot_delta_ci(oof_sil_factored, oof_sil_context, ysil, "auroc", groups=g),
|
|
239
|
+
},
|
|
240
|
+
"separability": inter,
|
|
241
|
+
"_oof": {"expression_pred": oof_factored, "expression_true": y, "residual": oof_resid,
|
|
242
|
+
"silenced_pred": oof_sil_factored, "silenced_true": ysil, "groups": g},
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _interaction_share(df: pd.DataFrame, feats: list[str], g: np.ndarray, seed: int) -> dict:
|
|
247
|
+
"""How much does the context function differ BY cassette (the h_interaction term)? Compare a pooled
|
|
248
|
+
g_context to per-cassette g_context on chromosome-blocked OOF; report the extra variance the interaction
|
|
249
|
+
explains. With 1 cassette dominating, expect ~0 (additive suffices), reported either way."""
|
|
250
|
+
X = df[feats].astype("float32").fillna(0.0)
|
|
251
|
+
y = df["expression_raw"].to_numpy()
|
|
252
|
+
cassette = df["cassette"]
|
|
253
|
+
cms = {str(c): float(v["expression_raw"].mean()) for c, v in df.groupby("cassette")}
|
|
254
|
+
resid = y - np.array([cms.get(str(c), y.mean()) for c in cassette])
|
|
255
|
+
gkf = GroupKFold(n_splits=min(5, len(np.unique(g))))
|
|
256
|
+
oof_pool = np.zeros(len(df))
|
|
257
|
+
oof_inter = np.zeros(len(df))
|
|
258
|
+
for tr, te in gkf.split(X, resid, g):
|
|
259
|
+
oof_pool[te] = _lgb_reg(seed).fit(X.iloc[tr], resid[tr]).predict(X.iloc[te])
|
|
260
|
+
# per-cassette model (interaction); fall back to pooled where a cassette is absent in train
|
|
261
|
+
pred = oof_pool[te].copy()
|
|
262
|
+
for c in cassette.iloc[te].unique():
|
|
263
|
+
m_tr = (cassette.iloc[tr] == c).to_numpy()
|
|
264
|
+
m_te = (cassette.iloc[te] == c).to_numpy()
|
|
265
|
+
if m_tr.sum() >= 50:
|
|
266
|
+
pred[m_te] = _lgb_reg(seed).fit(X.iloc[tr][m_tr], resid[tr][m_tr]).predict(X.iloc[te][m_te])
|
|
267
|
+
oof_inter[te] = pred
|
|
268
|
+
ss_tot = float(np.var(resid) * len(resid))
|
|
269
|
+
r2_pool = 1 - float(np.sum((resid - oof_pool) ** 2)) / ss_tot if ss_tot else 0.0
|
|
270
|
+
r2_inter = 1 - float(np.sum((resid - oof_inter) ** 2)) / ss_tot if ss_tot else 0.0
|
|
271
|
+
return {"r2_pooled_context": round(r2_pool, 4), "r2_with_interaction": round(r2_inter, 4),
|
|
272
|
+
"interaction_extra_r2": round(r2_inter - r2_pool, 4),
|
|
273
|
+
"interpretation": ("interaction adds signal (cassette x context separability is real)"
|
|
274
|
+
if (r2_inter - r2_pool) > 0.01 else
|
|
275
|
+
"additive f_cassette + g_context suffices (no material interaction at this N)")}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def calibrate_conformal(oof: dict, alpha: float = 0.10) -> ConformalRegressor:
|
|
279
|
+
"""Trained split-conformal on the chromosome-blocked OOF residuals (Mondrian per chromosome group), the
|
|
280
|
+
finite-sample (1-alpha) interval the EXISTING ConformalRegressor provides. This is what makes the model
|
|
281
|
+
'trained conformal'. The shipped qhat is calibrated on ALL OOF residuals; true *held-out* coverage is
|
|
282
|
+
reported separately by `conformal_heldout_coverage` (calibrating and evaluating on the same set would
|
|
283
|
+
trivially hit nominal)."""
|
|
284
|
+
y, yhat, groups = oof["expression_true"], oof["expression_pred"], oof["groups"]
|
|
285
|
+
return ConformalRegressor(alpha=alpha).calibrate(y, yhat, groups=groups)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# --------------------------------------------------------------------------------------------------
|
|
289
|
+
# serving seam, used by twin.outcome when a chromatin context is supplied
|
|
290
|
+
# --------------------------------------------------------------------------------------------------
|
|
291
|
+
_MODEL_CACHE: dict = {}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def load_cached_model(root=None) -> "PositionEffectModel | None":
|
|
295
|
+
"""Load the trained position-effect model if its artifact is present (VM/checkout); else None (installed
|
|
296
|
+
library / CI -> falls back to the heuristic). Cached per root."""
|
|
297
|
+
from pen_stack._resources import project_root
|
|
298
|
+
root = root or project_root()
|
|
299
|
+
key = str(root)
|
|
300
|
+
if key not in _MODEL_CACHE:
|
|
301
|
+
p = root / "models/position_effect.pkl"
|
|
302
|
+
try:
|
|
303
|
+
_MODEL_CACHE[key] = PositionEffectModel.load(p) if p.exists() else None
|
|
304
|
+
except Exception: # noqa: BLE001 - corrupt/absent artifact -> heuristic fallback, never crash the serving path
|
|
305
|
+
_MODEL_CACHE[key] = None
|
|
306
|
+
return _MODEL_CACHE[key]
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def load_human_k562_model(root=None) -> "PositionEffectModel | None":
|
|
310
|
+
"""Load the human-K562 position-effect head (Leemans 2019) if its artifact is present, else None:
|
|
311
|
+
a cell-type-matched head so a human K562 design is served by human supervision instead of the mESC model
|
|
312
|
+
applied cross-species (the cross-cell-type transfer axis is now fetched, not data-gated). Cached."""
|
|
313
|
+
from pen_stack._resources import project_root
|
|
314
|
+
root = root or project_root()
|
|
315
|
+
key = "human_k562::" + str(root)
|
|
316
|
+
if key not in _MODEL_CACHE:
|
|
317
|
+
p = root / "models/position_effect_human_k562.pkl"
|
|
318
|
+
try:
|
|
319
|
+
_MODEL_CACHE[key] = PositionEffectModel.load(p) if p.exists() else None
|
|
320
|
+
except Exception: # noqa: BLE001 - corrupt/absent artifact -> fall back, never crash the serving path
|
|
321
|
+
_MODEL_CACHE[key] = None
|
|
322
|
+
return _MODEL_CACHE[key]
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _is_human_k562(design: dict, cell_type=None) -> bool:
|
|
326
|
+
ct = str(cell_type or design.get("cell_type") or design.get("cell_state") or "").lower()
|
|
327
|
+
return "k562" in ct
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
# The human K562 head's held-out validation (Leemans 2019; grounded_human_headroom, rho~0.57, +0.065 over a
|
|
331
|
+
# learned-heterochromatin baseline, CI[0.026,0.103]) was measured with EXACT-SITE per-integration chromatin
|
|
332
|
+
# features. On coarse 1 kb-binned features even a human-trained model realizes only rho~0.16 (P2_MATCHED_TRANSFER).
|
|
333
|
+
# So the `outcome_validated` scope is earned ONLY when the served features are declared exact-site; otherwise the
|
|
334
|
+
# provenance is downgraded. Undeclared -> treated as non-exact (under-claim, never over-claim).
|
|
335
|
+
_EXACT_SITE_RES = {"exact_site", "exact-site", "per_integration", "per-integration",
|
|
336
|
+
"single_bp", "single-bp", "point", "site", "basepair", "bp"}
|
|
337
|
+
|
|
338
|
+
# The silenced/escaper classifier (`p_silenced`) shares the expression head's features but is NOT validated:
|
|
339
|
+
# on the Leemans human K562 held-out set it scores AUROC 0.50 (chance) -- the escaper/repressed label is not
|
|
340
|
+
# predictable from chromatin marks. We ship the expression head (predicted_log2_expression / relative) and
|
|
341
|
+
# label p_silenced as a not-validated candidate, never a claim, so a chance-level number is never presented as
|
|
342
|
+
# a platform result. (benchmarks/position_effect_human/metrics.json: silencing_classifier.auroc = 0.50.)
|
|
343
|
+
_SILENCING_NOT_VALIDATED = (
|
|
344
|
+
"silencing_classifier_not_validated (p_silenced is reported as a candidate only: the escaper/repressed "
|
|
345
|
+
"label is not predictable from chromatin marks -- AUROC 0.50, chance, on the Leemans human K562 held-out "
|
|
346
|
+
"set; use predicted_log2_expression / relative_expression_learned, not p_silenced)")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _served_feature_resolution(design: dict) -> str:
|
|
350
|
+
"""The caller-declared resolution of the supplied chromatin features ('undeclared' if absent). The caller
|
|
351
|
+
must assert exact-site resolution to earn the `outcome_validated` scope; we never infer it from the data."""
|
|
352
|
+
ctx = design.get("chromatin_ctx")
|
|
353
|
+
res = (design.get("chromatin_features_resolution") or design.get("feature_resolution")
|
|
354
|
+
or (ctx.get("resolution") if isinstance(ctx, dict) else None))
|
|
355
|
+
return str(res).lower().strip() if res else "undeclared"
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _is_exact_site(res: str) -> bool:
|
|
359
|
+
return res in _EXACT_SITE_RES
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def predict_stage_h(design: dict, root=None, cell_type=None) -> dict | None:
|
|
363
|
+
"""The learned, trained-conformal position-effect prediction, returned ONLY when a chromatin
|
|
364
|
+
context (the model's marks) is supplied AND a model artifact is present; else None and the caller falls
|
|
365
|
+
back to the closed-form heuristic. No fabrication: missing context -> no learned claim.
|
|
366
|
+
|
|
367
|
+
A human K562 design is served by the human K562 head (Leemans 2019) when its artifact is present,
|
|
368
|
+
falling back to the mESC model otherwise -- so the served provenance/scope reflect which
|
|
369
|
+
supervision was used (cross-species transfer no longer silently asserted)."""
|
|
370
|
+
hmodel = load_human_k562_model(root) if _is_human_k562(design, cell_type) else None
|
|
371
|
+
model = hmodel or load_cached_model(root)
|
|
372
|
+
served_human = hmodel is not None
|
|
373
|
+
if model is None or not model.features:
|
|
374
|
+
return None
|
|
375
|
+
cf = design.get("chromatin_features")
|
|
376
|
+
if not isinstance(cf, dict) and isinstance(design.get("chromatin_ctx"), dict):
|
|
377
|
+
cf = design["chromatin_ctx"].get("features")
|
|
378
|
+
if not isinstance(cf, dict):
|
|
379
|
+
return None
|
|
380
|
+
present = [f for f in model.features if f in cf]
|
|
381
|
+
if len(present) < max(3, len(model.features) // 2): # need enough real context to be meaningful
|
|
382
|
+
return None
|
|
383
|
+
row = {f: float(cf.get(f, 0.0)) for f in model.features}
|
|
384
|
+
cassette = str(design.get("cassette") or design.get("promoter")
|
|
385
|
+
or (next(iter(model.cassette_means), "_")))
|
|
386
|
+
one = pd.DataFrame([{**row, "cassette": cassette}])
|
|
387
|
+
iv = model.predict_interval(one)
|
|
388
|
+
yhat = float(iv["yhat"][0])
|
|
389
|
+
lo = float(iv["lo"][0]) if iv.get("lo") is not None else None
|
|
390
|
+
hi = float(iv["hi"][0]) if iv.get("hi") is not None else None
|
|
391
|
+
|
|
392
|
+
# Resolution-aware provenance: the human head's held-out validation holds at EXACT-SITE feature resolution;
|
|
393
|
+
# on coarse features the realized performance is far lower (rho~0.16). Only stamp `outcome_validated` when the
|
|
394
|
+
# caller declares exact-site features; otherwise downgrade (never claim more than the served resolution
|
|
395
|
+
# delivers). The mESC head makes no cross-cell-type outcome claim regardless of resolution.
|
|
396
|
+
res = _served_feature_resolution(design)
|
|
397
|
+
validated = bool(served_human and _is_exact_site(res))
|
|
398
|
+
if served_human and validated:
|
|
399
|
+
ct_flag = ("human_K562_outcome_validated (Leemans 2019; chromosome-blocked held-out; exact-site features "
|
|
400
|
+
"rho~0.57, +0.065 over a learned-heterochromatin baseline, CI[0.026,0.103])")
|
|
401
|
+
prov = ("twin.position_effect, human K562 head (Leemans 2019, cassette x context + split-conformal); "
|
|
402
|
+
"served at exact-site feature resolution -> the validated regime")
|
|
403
|
+
elif served_human:
|
|
404
|
+
ct_flag = (f"human_K562_head_served_UNVALIDATED_RESOLUTION (validated ONLY at exact-site resolution: "
|
|
405
|
+
f"rho~0.57, +0.065 over a learned-heterochromatin baseline, CI[0.026,0.103]; served-feature "
|
|
406
|
+
f"resolution '{res}' is not exact-site -> realized rho~0.16 (P2_MATCHED_TRANSFER); supply "
|
|
407
|
+
f"exact-site per-locus chromatin features to realize the validated performance)")
|
|
408
|
+
prov = ("twin.position_effect, human K562 head (Leemans 2019, cassette x context + split-conformal); "
|
|
409
|
+
f"served at '{res}' feature resolution -- BELOW the exact-site regime the head was validated at "
|
|
410
|
+
"(realized rho~0.16; not outcome_validated at this resolution)")
|
|
411
|
+
else:
|
|
412
|
+
ct_flag = "single_context_supervision (mESC) - cross-cell-type transfer data-gated"
|
|
413
|
+
prov = "twin.position_effect, learned cassette x context + split-conformal (TRIP mESC supervision)"
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
"predicted_log2_expression": round(yhat, 4),
|
|
417
|
+
"interval_log2": [round(lo, 4), round(hi, 4)] if lo is not None else None,
|
|
418
|
+
"relative_expression_learned": (round(model.relative(yhat), 4) if model.relative(yhat) is not None else None),
|
|
419
|
+
"p_silenced": round(float(model.predict_silenced(one)[0]), 4),
|
|
420
|
+
"interval_kind": iv["interval_kind"],
|
|
421
|
+
"ood_widen": (round(float(iv["ood_widen"][0]), 3) if iv.get("ood_widen") is not None else None),
|
|
422
|
+
"n_context_features_used": len(present),
|
|
423
|
+
"served_feature_resolution": res,
|
|
424
|
+
"outcome_validated": validated,
|
|
425
|
+
"cassette": cassette,
|
|
426
|
+
"trained_on": {"n": model.meta.get("n"), "datasets": model.meta.get("datasets"),
|
|
427
|
+
"cell_types": model.meta.get("cell_types")},
|
|
428
|
+
"scope_flags": ["learned_position_effect", "trained_conformal", ct_flag,
|
|
429
|
+
_SILENCING_NOT_VALIDATED],
|
|
430
|
+
"provenance": prov,
|
|
431
|
+
"output_kind": "candidate",
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def conformal_heldout_coverage(oof: dict, alpha: float = 0.10, seed: int = 20260619) -> dict:
|
|
436
|
+
"""Conformal coverage: calibrate qhat on a RANDOM HALF of the chromosomes and evaluate coverage on
|
|
437
|
+
the held-out half (exchangeable split). Evaluating on the calibration set itself would trivially report
|
|
438
|
+
nominal coverage, this does not. This is the validation number; the shipped qhat uses all OOF residuals."""
|
|
439
|
+
from pen_stack.wgenome.uncertainty import conformal_quantile
|
|
440
|
+
y = np.asarray(oof["expression_true"], float)
|
|
441
|
+
yhat = np.asarray(oof["expression_pred"], float)
|
|
442
|
+
groups = np.asarray(oof["groups"])
|
|
443
|
+
rng = np.random.default_rng(seed)
|
|
444
|
+
uniq = np.unique(groups)
|
|
445
|
+
rng.shuffle(uniq)
|
|
446
|
+
calib_g = set(uniq[: len(uniq) // 2])
|
|
447
|
+
cm = np.array([g in calib_g for g in groups])
|
|
448
|
+
tm = ~cm
|
|
449
|
+
qhat = conformal_quantile(np.abs(y[cm] - yhat[cm]), alpha)
|
|
450
|
+
covered = (y[tm] >= yhat[tm] - qhat) & (y[tm] <= yhat[tm] + qhat)
|
|
451
|
+
return {"nominal": round(1 - alpha, 3), "heldout_coverage": float(np.mean(covered)),
|
|
452
|
+
"qhat_calib": float(qhat), "mean_width": float(2 * qhat),
|
|
453
|
+
"n_calib": int(cm.sum()), "n_test": int(tm.sum()),
|
|
454
|
+
"within_tol": bool(abs(float(np.mean(covered)) - (1 - alpha)) <= 0.03)}
|
pen_stack/ui/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""pen_stack.ui - see the PEN-STACK program doc."""
|