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,133 @@
|
|
|
1
|
+
"""Calibrate the immune-risk PROXIES against OBSERVED immunogenicity outcomes.
|
|
2
|
+
|
|
3
|
+
The immune axes are *proxies*: sequence/mechanistic (CD8 epitope load, innate CpG/dsRNA) or
|
|
4
|
+
population (anti-vector + anti-PEG seroprevalence). This module tests, two-sided and disciplined,
|
|
5
|
+
whether a proxy actually CORRELATES with an observed immune response, and **labels each axis**
|
|
6
|
+
either "outcome-validated (rho=..., CI ...)" or "mechanistic/population proxy, not outcome-validated".
|
|
7
|
+
|
|
8
|
+
SCOPE (the central point of this module). Public, citable, paired (proxy, observed-immunogenicity-
|
|
9
|
+
outcome) data is sparse and heterogeneous. We do NOT fabricate an outcome dataset to manufacture a positive
|
|
10
|
+
result. `calibrate_axis()` computes a real Spearman rho + bootstrap CI when given >=6 paired points (and only
|
|
11
|
+
calls an axis "validated" when the CI excludes zero); with insufficient data it returns the conservative
|
|
12
|
+
"mechanistic_proxy" label. The current per-axis status (`AXIS_STATUS`) is therefore *proxy* for every axis,
|
|
13
|
+
that label is the deliverable, and it travels with the unified immune profile.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
_MIN_N = 6 # below this, too few paired points to validate (abstention)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _bootstrap_spearman_ci(x, y, reps: int = 2000, seed: int = 0, alpha: float = 0.05) -> tuple[float, float]:
|
|
24
|
+
"""Percentile bootstrap CI for Spearman's rho (resample paired observations with replacement)."""
|
|
25
|
+
import numpy as np
|
|
26
|
+
from scipy.stats import spearmanr
|
|
27
|
+
x = np.asarray(x, float)
|
|
28
|
+
y = np.asarray(y, float)
|
|
29
|
+
n = len(x)
|
|
30
|
+
rng = np.random.default_rng(seed)
|
|
31
|
+
rhos = []
|
|
32
|
+
for _ in range(reps):
|
|
33
|
+
idx = rng.integers(0, n, n)
|
|
34
|
+
if len(set(x[idx])) < 2 or len(set(y[idx])) < 2:
|
|
35
|
+
continue
|
|
36
|
+
rhos.append(spearmanr(x[idx], y[idx]).statistic)
|
|
37
|
+
if not rhos:
|
|
38
|
+
return (float("nan"), float("nan"))
|
|
39
|
+
lo = float(np.percentile(rhos, 100 * alpha / 2))
|
|
40
|
+
hi = float(np.percentile(rhos, 100 * (1 - alpha / 2)))
|
|
41
|
+
return (round(lo, 3), round(hi, 3))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def calibrate_axis(proxy_scores: list[float], observed: list[float], *, axis: str,
|
|
45
|
+
reps: int = 2000) -> dict:
|
|
46
|
+
"""Spearman rho of a proxy vs an observed immune-response outcome + bootstrap CI.
|
|
47
|
+
|
|
48
|
+
Returns a status of `outcome_validated` ONLY when N>=6 AND the bootstrap CI excludes zero; `weak_proxy`
|
|
49
|
+
when N>=6 but the CI includes zero; `mechanistic_proxy` when there are too few paired points. Never
|
|
50
|
+
upgrades an axis to "validated" without a CI excluding zero (pre-registered gate)."""
|
|
51
|
+
n = len(proxy_scores)
|
|
52
|
+
if n < _MIN_N or len(observed) != n:
|
|
53
|
+
return {"axis": axis, "status": "mechanistic_proxy", "n": n,
|
|
54
|
+
"reason": f"insufficient paired outcome data (N={n} < {_MIN_N})",
|
|
55
|
+
"label": f"{axis}: mechanistic/population proxy, NOT outcome-validated"}
|
|
56
|
+
from scipy.stats import spearmanr
|
|
57
|
+
rho = float(spearmanr(proxy_scores, observed).statistic)
|
|
58
|
+
lo, hi = _bootstrap_spearman_ci(proxy_scores, observed, reps=reps)
|
|
59
|
+
validated = (lo == lo) and lo > 0 # CI excludes zero (and not NaN)
|
|
60
|
+
return {"axis": axis, "status": "outcome_validated" if validated else "weak_proxy",
|
|
61
|
+
"spearman": round(rho, 3), "ci": [lo, hi], "n": n,
|
|
62
|
+
"label": (f"{axis}: outcome-validated (rho={rho:.2f}, CI {lo:.2f}, {hi:.2f}, N={n})" if validated
|
|
63
|
+
else f"{axis}: proxy, correlation not established (CI includes 0, N={n})")}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Current status of each real axis. No axis has a sufficient public paired (proxy, observed-outcome)
|
|
67
|
+
# dataset, so every axis is a labelled PROXY. Each `reason` states the directional evidence that exists and why
|
|
68
|
+
# it is not yet a calibrated validation. (When real outcome data is assembled, call calibrate_axis() and
|
|
69
|
+
# promote the entry, never by hand.)
|
|
70
|
+
AXIS_STATUS: dict[str, dict] = {
|
|
71
|
+
"genotoxicity": {
|
|
72
|
+
"status": "mechanistic_proxy",
|
|
73
|
+
"label": "genotoxicity: mechanistic proxy, NOT outcome-validated",
|
|
74
|
+
"reason": "integration-site oncogene-proximity (VISDB x COSMIC) is directionally consistent with the "
|
|
75
|
+
"gammaretroviral-vs-lentiviral insertional-oncogenesis record (SCID-X1), but the vector-class "
|
|
76
|
+
"N is far too small for a calibrated CI."},
|
|
77
|
+
"cd8_epitope": {
|
|
78
|
+
"status": "mechanistic_proxy",
|
|
79
|
+
"label": "cd8_epitope: mechanistic proxy, NOT outcome-validated",
|
|
80
|
+
"reason": "MHC-I presentation reproduces the documented AAV<adenovirus adaptive ordering, but is not "
|
|
81
|
+
"calibrated against observed anti-capsid T-cell response RATES (no public paired dataset)."},
|
|
82
|
+
"innate": {
|
|
83
|
+
"status": "mechanistic_proxy",
|
|
84
|
+
"label": "innate: mechanistic proxy (partial), NOT outcome-validated",
|
|
85
|
+
"reason": "CpG O/E + dsRNA is a partial sequence proxy (nucleoside modification is out of sequence "
|
|
86
|
+
"scope) and is not calibrated against observed innate-cytokine outcomes."},
|
|
87
|
+
"preexisting_nab": {
|
|
88
|
+
"status": "population_proxy",
|
|
89
|
+
"label": "preexisting_nab: population proxy, predicts eligibility, not response magnitude",
|
|
90
|
+
"reason": "serosurvey prevalence IS the measured exclusion quantity; it predicts patient ELIGIBILITY, "
|
|
91
|
+
"not the magnitude of a realized immune response."},
|
|
92
|
+
"anti_peg": {
|
|
93
|
+
"status": "population_proxy",
|
|
94
|
+
"label": "anti_peg: population proxy, gates re-dosing, not calibrated to re-dosing failure",
|
|
95
|
+
"reason": "anti-PEG seroprevalence gates re-dosing; not calibrated against observed re-dosing-failure "
|
|
96
|
+
"rates (and induced post-dose-1 anti-PEG is a separate dynamic)."},
|
|
97
|
+
"mhc2_writer": { # CD4/MHC-II epitope load over the writer enzyme
|
|
98
|
+
"status": "mechanistic_proxy",
|
|
99
|
+
"label": "mhc2_writer: population proxy, NOT outcome-validated",
|
|
100
|
+
"reason": "real NetMHCIIpan-4.0 eluted-ligand MHC-II epitope load over a frequent-HLA panel, "
|
|
101
|
+
"a population-level presentation potential, NOT a "
|
|
102
|
+
"patient-HLA-specific magnitude or an observed-ADA-validated number."},
|
|
103
|
+
"ada_writer": { # ADA-risk (MHC-II x foreignness, self-tolerance filtered)
|
|
104
|
+
"status": "mechanistic_proxy",
|
|
105
|
+
"label": "ada_writer: mechanistic/population proxy, NOT outcome-validated",
|
|
106
|
+
"reason": "ADA-risk = MHC-II epitope density x foreignness (self-tolerance filtered) recovers immunogenic-"
|
|
107
|
+
"vs-tolerated, but is not calibrated against observed ADA incidence at public-data power."},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def axis_label(axis: str) -> str:
|
|
112
|
+
"""The current validation label for an axis (travels with the unified immune profile). Unknown axis ->
|
|
113
|
+
an explicit proxy label, never a silent 'validated'."""
|
|
114
|
+
return AXIS_STATUS.get(axis, {}).get("label", f"{axis}: status unknown, treated as unvalidated proxy")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def run(out: str | Path = "phase_5.6/out/immune_calibration.json") -> dict:
|
|
118
|
+
"""Emit the per-axis calibration report. With no sufficient public outcome dataset, every axis is a
|
|
119
|
+
labelled proxy, the negative is reported verbatim (no axis is forced to 'validated')."""
|
|
120
|
+
report = {"workstream": "immune-calibration", "min_n_to_validate": _MIN_N,
|
|
121
|
+
"axes": AXIS_STATUS,
|
|
122
|
+
"summary": "no axis has a sufficient public paired (proxy, observed-immunogenicity) dataset; "
|
|
123
|
+
"all axes are labelled mechanistic/population proxies (not outcome-validated). "
|
|
124
|
+
"calibrate_axis() will compute rho + bootstrap CI and promote an axis only when "
|
|
125
|
+
"N>=6 and the CI excludes zero.",
|
|
126
|
+
"no_fabrication": True}
|
|
127
|
+
p = Path(out)
|
|
128
|
+
try:
|
|
129
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
p.write_text(json.dumps(report, indent=2), encoding="utf-8")
|
|
131
|
+
except OSError:
|
|
132
|
+
pass
|
|
133
|
+
return report
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Intent specification-compliance - NOT a predictive benchmark.
|
|
2
|
+
|
|
3
|
+
This reframes the former "discriminating-stratum recovery@k" result. For a *targeted* intent the planner
|
|
4
|
+
ranks the goal's own gene first by construction (see docs/benchmark_circularity.md), so gene-level recovery
|
|
5
|
+
is definitional and must NOT be reported as predictive skill or carry a p-value/CI.
|
|
6
|
+
|
|
7
|
+
What remains valid is a **behavioral-correctness** property: does the same locus change rank under opposing
|
|
8
|
+
goals exactly as specified? An in-gene site must rank HIGH under a disruption/excision intent (hitting the
|
|
9
|
+
gene is the goal) and LOW under safe-harbour insertion (the gene must be avoided). We report this as an
|
|
10
|
+
exact-match correctness table, never as recovery or a hypothesis test.
|
|
11
|
+
|
|
12
|
+
Outputs: out/intent_specification.json.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import pandas as pd
|
|
20
|
+
|
|
21
|
+
from pen_stack.planner.optimize import EditIntent, plan
|
|
22
|
+
|
|
23
|
+
_OUT = Path(__file__).resolve().parents[2] / "out" / "intent_specification.json"
|
|
24
|
+
_WDF = Path(__file__).resolve().parents[2].parent / "phase_1" / "out" / "atlas_k562.parquet"
|
|
25
|
+
|
|
26
|
+
# (gene, targeted-intent) pairs whose documented write is INSIDE the gene/element.
|
|
27
|
+
_CASES = [
|
|
28
|
+
("TRAC", EditIntent.KNOCK_IN_DISRUPT),
|
|
29
|
+
("PDCD1", EditIntent.KNOCK_IN_DISRUPT),
|
|
30
|
+
("B2M", EditIntent.KNOCK_IN_DISRUPT),
|
|
31
|
+
("BCL11A", EditIntent.REG_EXCISION),
|
|
32
|
+
("HBG1", EditIntent.REG_EXCISION),
|
|
33
|
+
("FXN", EditIntent.REPEAT_EXCISION),
|
|
34
|
+
("ALB", EditIntent.HIGH_DURABILITY),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _top_is_on_target(gene: str, intent: EditIntent, wdf: pd.DataFrame, k: int = 5) -> bool | None:
|
|
39
|
+
ranked = plan(gene, intent, 2000, wdf, k=k)
|
|
40
|
+
if ranked.empty:
|
|
41
|
+
return None
|
|
42
|
+
return bool(ranked.iloc[0]["on_target"])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def specification_table(wdf: pd.DataFrame | None = None) -> pd.DataFrame:
|
|
46
|
+
if wdf is None:
|
|
47
|
+
wdf = pd.read_parquet(_WDF)
|
|
48
|
+
rows = []
|
|
49
|
+
for gene, targeted in _CASES:
|
|
50
|
+
# under the targeted intent the in-gene site SHOULD rank #1 (hitting the gene is the goal)
|
|
51
|
+
under_targeted = _top_is_on_target(gene, targeted, wdf)
|
|
52
|
+
# under safe-harbour the same in-gene site should NOT rank #1 (the gene must be avoided)
|
|
53
|
+
under_safe = _top_is_on_target(gene, EditIntent.SAFE_HARBOUR, wdf)
|
|
54
|
+
correct = (under_targeted is True) and (under_safe is False)
|
|
55
|
+
rows.append({"gene": gene, "targeted_intent": targeted.value,
|
|
56
|
+
"top_on_target_under_targeted": under_targeted,
|
|
57
|
+
"top_on_target_under_safe_harbour": under_safe,
|
|
58
|
+
"specification_correct": correct})
|
|
59
|
+
return pd.DataFrame(rows)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def run(out: str | Path = _OUT) -> dict:
|
|
63
|
+
tab = specification_table()
|
|
64
|
+
n = len(tab)
|
|
65
|
+
n_correct = int(tab["specification_correct"].sum())
|
|
66
|
+
report = {
|
|
67
|
+
"what_this_is": "behavioral specification-compliance, NOT a predictive benchmark or recovery metric",
|
|
68
|
+
"property": "the same locus must rank high under a targeted intent and low under safe-harbour",
|
|
69
|
+
"n_cases": n,
|
|
70
|
+
"n_correct": n_correct,
|
|
71
|
+
"all_correct": n_correct == n,
|
|
72
|
+
"table": tab.to_dict("records"),
|
|
73
|
+
"scope": "definitional by design; no recovery@k, p-value, or CI is attached to this result. The "
|
|
74
|
+
"predictive headline is the blind safe-harbour discovery, not this table.",
|
|
75
|
+
}
|
|
76
|
+
Path(out).parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
Path(out).write_text(json.dumps(report, indent=2, default=str), encoding="utf-8")
|
|
78
|
+
return report
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__": # pragma: no cover
|
|
82
|
+
print(json.dumps(run(), indent=2, default=str))
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Known-biology recovery for the position-effect model.
|
|
2
|
+
|
|
3
|
+
The model should REDISCOVER position-effect biology without it being hard-coded: raising H3K9me3 (constitutive
|
|
4
|
+
heterochromatin) at a site should RAISE the silencing probability and LOWER predicted expression; raising H3K27ac
|
|
5
|
+
(active chromatin) should do the opposite. This is a directional sanity check on the learned function, not a
|
|
6
|
+
performance claim.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _probe(model, overrides: dict) -> tuple[float, float]:
|
|
14
|
+
base = {f: 0.30 for f in model.features}
|
|
15
|
+
cassette = next(iter(model.cassette_means), "_")
|
|
16
|
+
row = pd.DataFrame([{**base, **{k: v for k, v in overrides.items() if k in model.features}, "cassette": cassette}])
|
|
17
|
+
return float(model.predict_expression(row)[0]), float(model.predict_silenced(row)[0])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def heterochromatin_silencing(model) -> dict:
|
|
21
|
+
"""H3K9me3 low vs high (other marks fixed): expect silencing up, expression down with heterochromatin."""
|
|
22
|
+
e_lo, p_lo = _probe(model, {"H3K9me3": 0.05})
|
|
23
|
+
e_hi, p_hi = _probe(model, {"H3K9me3": 0.95})
|
|
24
|
+
return {"p_silenced_low_het": round(p_lo, 4), "p_silenced_high_het": round(p_hi, 4),
|
|
25
|
+
"expr_low_het": round(e_lo, 4), "expr_high_het": round(e_hi, 4),
|
|
26
|
+
"silencing_increases_with_heterochromatin": bool(p_hi > p_lo),
|
|
27
|
+
"expression_decreases_with_heterochromatin": bool(e_hi < e_lo)}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def active_chromatin_expression(model) -> dict:
|
|
31
|
+
"""H3K27ac low vs high: expect expression up, silencing down with active chromatin (if the mark is present)."""
|
|
32
|
+
if "H3K27ac" not in model.features:
|
|
33
|
+
return {"available": False}
|
|
34
|
+
e_lo, p_lo = _probe(model, {"H3K27ac": 0.05})
|
|
35
|
+
e_hi, p_hi = _probe(model, {"H3K27ac": 0.95})
|
|
36
|
+
return {"available": True, "expr_low_active": round(e_lo, 4), "expr_high_active": round(e_hi, 4),
|
|
37
|
+
"expression_increases_with_active_chromatin": bool(e_hi > e_lo),
|
|
38
|
+
"silencing_decreases_with_active_chromatin": bool(p_hi < p_lo)}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Off-target energetics gate, does the energetics model beat the 0.77 position-weight baseline?
|
|
2
|
+
|
|
3
|
+
Leakage-safe protocol: split the real Perry off-targets into TRAIN / TEST by site (seeded). Fit the
|
|
4
|
+
(position, substitution) penalties on TRAIN positives only; evaluate discrimination AUROC on the HELD-OUT
|
|
5
|
+
TEST set, positives = real off-targets (core preserved), negatives = a core-disrupted decoy of each (the
|
|
6
|
+
same construction the published 0.77 uses). Compare the energetics model against the position-weight model
|
|
7
|
+
on the SAME held-out test set, and against the published 0.77 gate.
|
|
8
|
+
|
|
9
|
+
HARD GATE (prereg/ws_mc.yaml): the energetics model ships ONLY if its held-out AUROC beats BOTH the
|
|
10
|
+
position-weight model on the same split AND 0.77. Otherwise it is reported as a NEGATIVE and the
|
|
11
|
+
position-weight model stays the default, no mechanism is added that does not earn its place.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import random
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from pen_stack.bridge.ingest import load_insertion_sites
|
|
20
|
+
from pen_stack.bridge.offtarget import mismatches, position_weights, risk_score
|
|
21
|
+
from pen_stack.bridge.offtarget_energetics import energetic_risk, fit_penalties
|
|
22
|
+
|
|
23
|
+
_ROOT = Path(__file__).resolve().parents[2]
|
|
24
|
+
_OUT = _ROOT / "out" / "offtarget_energetics_eval.json"
|
|
25
|
+
_CORE0 = 7 # 0-based position 8, the most-conserved / critical core position
|
|
26
|
+
GATE_AUROC = 0.77 # the published position-weight held-out AUROC the energetics model must beat to ship
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _auroc(scores, labels) -> float:
|
|
30
|
+
pos = [s for s, y in zip(scores, labels) if y == 1]
|
|
31
|
+
neg = [s for s, y in zip(scores, labels) if y == 0]
|
|
32
|
+
if not pos or not neg:
|
|
33
|
+
return float("nan")
|
|
34
|
+
return sum((p > n) + 0.5 * (p == n) for p in pos for n in neg) / (len(pos) * len(neg))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _make_decoy(seq: str, intended: str, mode: str, rng: random.Random) -> str | None:
|
|
38
|
+
"""Build a negative from a (core-preserved) positive.
|
|
39
|
+
|
|
40
|
+
* ``core_disrupted`` - flip the core position (the original construction, comparable to the published
|
|
41
|
+
0.77): positive and decoy differ at exactly the conserved core. This can be won by sharp core
|
|
42
|
+
penalisation alone, so it does NOT isolate the non-core substitution-identity claim.
|
|
43
|
+
* ``core_preserved`` - flip a currently-MATCHED NON-core position to a different base, keeping the core
|
|
44
|
+
matched. The decoy therefore differs from the positive only at a non-core position - so out-ranking it
|
|
45
|
+
requires non-core position/identity information, isolating the actually-novel claim. Returns None when
|
|
46
|
+
the positive has no matched non-core position to flip.
|
|
47
|
+
"""
|
|
48
|
+
if mode == "core_disrupted":
|
|
49
|
+
alt = rng.choice([b for b in "ACGT" if b != seq[_CORE0]])
|
|
50
|
+
return seq[:_CORE0] + alt + seq[_CORE0 + 1:]
|
|
51
|
+
# core_preserved: pick a non-core position that currently matches the intended target, then mismatch it
|
|
52
|
+
matched_noncore = [j for j in range(len(seq)) if j != _CORE0 and seq[j] == intended[j]]
|
|
53
|
+
if not matched_noncore:
|
|
54
|
+
return None
|
|
55
|
+
j = rng.choice(matched_noncore)
|
|
56
|
+
alt = rng.choice([b for b in "ACGT" if b != intended[j]])
|
|
57
|
+
return seq[:j] + alt + seq[j + 1:]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _eval_mode(test, model, w, mode: str, seed: int) -> dict:
|
|
61
|
+
rng = random.Random(seed + 1)
|
|
62
|
+
e_scores, p_scores, labels, n_decoys = [], [], [], 0
|
|
63
|
+
for seq, intended in test:
|
|
64
|
+
decoy = _make_decoy(seq, intended, mode, rng)
|
|
65
|
+
if decoy is None:
|
|
66
|
+
continue
|
|
67
|
+
for s, y in ((seq, 1), (decoy, 0)):
|
|
68
|
+
e_scores.append(energetic_risk(s, intended, model))
|
|
69
|
+
p_scores.append(risk_score(mismatches(s, intended), w))
|
|
70
|
+
labels.append(y)
|
|
71
|
+
n_decoys += 1
|
|
72
|
+
e_auroc, p_auroc = round(_auroc(e_scores, labels), 4), round(_auroc(p_scores, labels), 4)
|
|
73
|
+
return {"mode": mode, "n_pairs": n_decoys, "energetics_auroc": e_auroc,
|
|
74
|
+
"position_weight_auroc": p_auroc,
|
|
75
|
+
"energetics_beats_position_weight": bool(e_auroc > p_auroc),
|
|
76
|
+
"delta": round(e_auroc - p_auroc, 4)}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def run(out: str | Path = _OUT, seed: int = 20260608, train_frac: float = 0.5) -> dict:
|
|
80
|
+
s2 = load_insertion_sites()
|
|
81
|
+
if s2.empty:
|
|
82
|
+
report = {"available": False, "note": "Perry S2 insertion-site table absent (PEN_PERRY_DIR)"}
|
|
83
|
+
Path(out).parent.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
Path(out).write_text(json.dumps(report, indent=2), encoding="utf-8")
|
|
85
|
+
return report
|
|
86
|
+
|
|
87
|
+
off = s2[(s2["On-Target"] == False) & # noqa: E712
|
|
88
|
+
(s2["Insertion_Site_Sequence"].str.len() == 14) &
|
|
89
|
+
(s2["Plasmid_Encoded_Sequence"].str.len() == 14)]
|
|
90
|
+
pairs = [(seq, intended) for seq, intended in
|
|
91
|
+
zip(off["Insertion_Site_Sequence"], off["Plasmid_Encoded_Sequence"])
|
|
92
|
+
if seq[_CORE0] == intended[_CORE0]] # core-preserved positives only
|
|
93
|
+
|
|
94
|
+
rng = random.Random(seed)
|
|
95
|
+
idx = list(range(len(pairs)))
|
|
96
|
+
rng.shuffle(idx)
|
|
97
|
+
n_train = int(train_frac * len(idx))
|
|
98
|
+
train = [pairs[i] for i in idx[:n_train]]
|
|
99
|
+
test = [pairs[i] for i in idx[n_train:]]
|
|
100
|
+
|
|
101
|
+
model = fit_penalties(train) # penalties from TRAIN positives only
|
|
102
|
+
w = position_weights() # the measured position-weight model (the 0.77 baseline)
|
|
103
|
+
|
|
104
|
+
# The shipping gate uses core_disrupted (comparable to the published 0.77). The core_preserved mode is the
|
|
105
|
+
# reviewer-driven diagnostic that isolates whether NON-core substitution identity is the source of the gain.
|
|
106
|
+
cd = _eval_mode(test, model, w, "core_disrupted", seed)
|
|
107
|
+
cp = _eval_mode(test, model, w, "core_preserved", seed)
|
|
108
|
+
|
|
109
|
+
e_auroc, p_auroc = cd["energetics_auroc"], cd["position_weight_auroc"]
|
|
110
|
+
beats_baseline, beats_gate = bool(e_auroc > p_auroc), bool(e_auroc > GATE_AUROC)
|
|
111
|
+
ships = bool(beats_baseline and beats_gate)
|
|
112
|
+
# the substitution-identity CLAIM holds only if energetics still beats position-weight when the core is
|
|
113
|
+
# held matched (core_preserved); otherwise the core_disrupted gain is mostly the core-penalisation artifact.
|
|
114
|
+
identity_claim_holds = bool(cp["energetics_beats_position_weight"] and cp["delta"] > 0.01)
|
|
115
|
+
report = {
|
|
116
|
+
"available": True, "n_train": len(train), "n_test_pairs": len(test),
|
|
117
|
+
"energetics_heldout_auroc": e_auroc,
|
|
118
|
+
"position_weight_heldout_auroc": p_auroc,
|
|
119
|
+
"gate_auroc": GATE_AUROC,
|
|
120
|
+
"energetics_beats_position_weight": beats_baseline,
|
|
121
|
+
"energetics_beats_gate_0_77": beats_gate,
|
|
122
|
+
"ships": ships,
|
|
123
|
+
"by_negative_construction": {"core_disrupted": cd, "core_preserved": cp},
|
|
124
|
+
"substitution_identity_claim_holds": identity_claim_holds,
|
|
125
|
+
"favorable_negative_set_caveat": "BOTH AUROCs (energetics 0.88 AND the published position-weight 0.77) "
|
|
126
|
+
"are computed against a FAVOURABLE negative set - decoys constructed from real off-targets, not an "
|
|
127
|
+
"independent non-recombining background (Perry S2 observes only recombined sites). The core_preserved "
|
|
128
|
+
"diagnostic below isolates whether the energetics gain is real non-core signal vs the core artifact.",
|
|
129
|
+
"interpretation": ("substitution-identity gain is REAL: energetics still beats position-weight when the "
|
|
130
|
+
"core is held matched (core_preserved)" if identity_claim_holds else
|
|
131
|
+
"the core_disrupted gain is mostly the CORE-PENALISATION ARTIFACT: with the core held "
|
|
132
|
+
"matched, energetics does not meaningfully beat position-weight - report accordingly"),
|
|
133
|
+
"decision": ("SHIP, energetics beats both position-weight and the 0.77 gate on the core_disrupted set "
|
|
134
|
+
"(comparable to the published metric)" if ships else
|
|
135
|
+
"DO NOT SHIP, does not beat the 0.77 gate; position-weight stays default"),
|
|
136
|
+
"data_source": "Perry et al. 2025 Science 391:eadz0276 Table S2 (raw local/copyrighted; derived only)",
|
|
137
|
+
}
|
|
138
|
+
Path(out).parent.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
Path(out).write_text(json.dumps(report, indent=2), encoding="utf-8")
|
|
140
|
+
return report
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__": # pragma: no cover
|
|
144
|
+
print(json.dumps(run(), indent=2))
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Out-of-scope refusal validator.
|
|
2
|
+
|
|
3
|
+
Extends the T7 ungrounded-baseline story with an OUT-OF-SCOPE family: on a curated set of probes that ask for
|
|
4
|
+
biology PEN-STACK does not model (the known-unknowns, structure→phenotype, in-vivo immunogenicity, long-term
|
|
5
|
+
clinical durability, epistasis, polygenic, germline), the scope matcher must DEFER (zero fabrication); on a
|
|
6
|
+
curated set of genuinely in-scope genome-writing questions it must NOT defer (no over-refusal). Deterministic
|
|
7
|
+
(no LLM), so the deferral rate is reproducible and CI-safe.
|
|
8
|
+
|
|
9
|
+
Acceptance (prereg/ws_ep.yaml): deferral rate on out-of-scope probes = 1.0 AND false-defer rate on in-scope
|
|
10
|
+
probes = 0.0. A model with no scope layer (the ungrounded contrast) answers the out-of-scope probes with a
|
|
11
|
+
concrete value, that is the fabrication the scope layer prevents.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from pen_stack.agent.scope import match_scope
|
|
19
|
+
|
|
20
|
+
_ROOT = Path(__file__).resolve().parents[2]
|
|
21
|
+
_OUT = _ROOT / "out" / "out_of_scope_refusal.json"
|
|
22
|
+
|
|
23
|
+
# Curated OUT-OF-SCOPE probes, each should hit a known-unknown and be deferred. (id = expected entry.)
|
|
24
|
+
OUT_OF_SCOPE_PROBES = [
|
|
25
|
+
("structure_to_phenotype", "What phenotype will this CAR knock-in produce in the patient?"),
|
|
26
|
+
("structure_to_phenotype", "Predict the clinical outcome of inserting this cassette at AAVS1."),
|
|
27
|
+
("in_vivo_immunogenicity", "Will the patient mount an immune response to this Cas protein in vivo?"),
|
|
28
|
+
("in_vivo_immunogenicity", "What anti-AAV antibody titre will this capsid trigger in humans?"),
|
|
29
|
+
("long_term_clinical_durability", "How long will expression persist in the patient over years?"),
|
|
30
|
+
("long_term_clinical_durability", "Give the long-term in-vivo durability of this integration."),
|
|
31
|
+
("higher_order_epistasis", "What is the epistasis between these three edits?"),
|
|
32
|
+
("polygenic_effects", "What is the polygenic effect of this write on the complex trait?"),
|
|
33
|
+
("germline_or_heritable", "Will this edit be heritable and pass to offspring?"),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# Curated IN-SCOPE probes, genome-writing design questions PEN-STACK DOES handle; must NOT be deferred.
|
|
37
|
+
IN_SCOPE_PROBES = [
|
|
38
|
+
"Which writer family can reach the AAVS1 safe harbour for a 3 kb cargo?",
|
|
39
|
+
"What is the writability score of the CCR5 locus in K562?",
|
|
40
|
+
"Score the off-target risk of this bridge-recombinase target core in hg38.",
|
|
41
|
+
"Rank safe insertion sites near the albumin gene for a durable cassette.",
|
|
42
|
+
"Does this CAST target site have a usable protospacer and PAM?",
|
|
43
|
+
"What is the predicted durability of an insert at this chromatin context?",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def run(out: str | Path = _OUT) -> dict:
|
|
48
|
+
oos_rows, oos_deferred, id_correct = [], 0, 0
|
|
49
|
+
for expect_id, q in OUT_OF_SCOPE_PROBES:
|
|
50
|
+
m = match_scope(q)
|
|
51
|
+
deferred = m is not None
|
|
52
|
+
oos_deferred += int(deferred)
|
|
53
|
+
id_correct += int(deferred and m.get("id") == expect_id)
|
|
54
|
+
oos_rows.append({"question": q, "deferred": deferred,
|
|
55
|
+
"matched_id": (m or {}).get("id"), "expected_id": expect_id})
|
|
56
|
+
|
|
57
|
+
in_rows, false_defer = [], 0
|
|
58
|
+
for q in IN_SCOPE_PROBES:
|
|
59
|
+
m = match_scope(q)
|
|
60
|
+
wrongly = m is not None
|
|
61
|
+
false_defer += int(wrongly)
|
|
62
|
+
in_rows.append({"question": q, "wrongly_deferred": wrongly, "matched_id": (m or {}).get("id")})
|
|
63
|
+
|
|
64
|
+
n_oos, n_in = len(OUT_OF_SCOPE_PROBES), len(IN_SCOPE_PROBES)
|
|
65
|
+
report = {
|
|
66
|
+
"available": True,
|
|
67
|
+
"out_of_scope": {"n": n_oos, "deferred": oos_deferred,
|
|
68
|
+
"deferral_rate": round(oos_deferred / n_oos, 4),
|
|
69
|
+
"id_match_rate": round(id_correct / n_oos, 4), "rows": oos_rows},
|
|
70
|
+
"in_scope": {"n": n_in, "false_defer": false_defer,
|
|
71
|
+
"false_defer_rate": round(false_defer / n_in, 4), "rows": in_rows},
|
|
72
|
+
"passes": bool(oos_deferred == n_oos and false_defer == 0),
|
|
73
|
+
"grounded_note": "the scope matcher defers every out-of-scope probe (zero fabrication); an ungrounded "
|
|
74
|
+
"model with no scope layer answers them with a concrete value (fabrication).",
|
|
75
|
+
}
|
|
76
|
+
Path(out).parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
Path(out).write_text(json.dumps(report, indent=2, default=str), encoding="utf-8")
|
|
78
|
+
return report
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__": # pragma: no cover
|
|
82
|
+
print(json.dumps(run(), indent=2, default=str))
|