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,29 @@
|
|
|
1
|
+
"""Locus resolver: free text (gene / safe-harbour) -> GRCh38 chromosome, reusing the atlas resolver.
|
|
2
|
+
|
|
3
|
+
Wraps :func:`pen_stack.planner.optimize.gene_region` (with the safe-harbour-aware ``resolve_gene``). An
|
|
4
|
+
unresolvable term stays null. The coordinate granularity is the chromosome the gene body lies on (the full
|
|
5
|
+
base-range comes from the atlas when a downstream reachability query is run).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pen_stack.spec.writespec import Resolved
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def resolve_locus(text: str) -> Resolved:
|
|
13
|
+
"""Resolve a gene / safe-harbour locus to its GRCh38 chromosome; unrecognized stays null."""
|
|
14
|
+
if not text:
|
|
15
|
+
return Resolved(text=text, ontology="GRCh38", note="empty")
|
|
16
|
+
sym = text.strip().upper()
|
|
17
|
+
reg = None
|
|
18
|
+
try:
|
|
19
|
+
from pen_stack.planner.optimize import gene_region, resolve_gene
|
|
20
|
+
reg = gene_region(resolve_gene(sym)) # -> (chrom, start, end) | None
|
|
21
|
+
except Exception: # noqa: BLE001
|
|
22
|
+
reg = None
|
|
23
|
+
if not reg:
|
|
24
|
+
return Resolved(text=text, ontology="GRCh38", confidence=None, note="unresolved locus")
|
|
25
|
+
chrom, start, end = reg
|
|
26
|
+
chrom = chrom if str(chrom).startswith("chr") else f"chr{chrom}"
|
|
27
|
+
cid = f"{chrom}:{int(start)}-{int(end)}"
|
|
28
|
+
return Resolved(text=text, id=cid, label=cid, ontology="GRCh38", confidence=0.85,
|
|
29
|
+
note="GRCh38 gene-body region (+/- flank) from the atlas")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Phenotype / disease-goal resolver: free text -> MONDO. Ids verified against EBI OLS."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pen_stack.spec.writespec import Resolved
|
|
5
|
+
|
|
6
|
+
# (MONDO id, canonical label) - verified live before commit
|
|
7
|
+
_PHENO: dict[str, tuple[str, str]] = {
|
|
8
|
+
"sickle cell disease": ("MONDO:0011382", "sickle cell disease"),
|
|
9
|
+
"sickle cell": ("MONDO:0011382", "sickle cell disease"), "scd": ("MONDO:0011382", "sickle cell disease"),
|
|
10
|
+
"beta thalassemia": ("MONDO:0019402", "beta thalassemia"),
|
|
11
|
+
"beta-thalassemia": ("MONDO:0019402", "beta thalassemia"),
|
|
12
|
+
"duchenne muscular dystrophy": ("MONDO:0010679", "Duchenne muscular dystrophy"),
|
|
13
|
+
"duchenne": ("MONDO:0010679", "Duchenne muscular dystrophy"), "dmd": ("MONDO:0010679", "Duchenne muscular dystrophy"),
|
|
14
|
+
"cystic fibrosis": ("MONDO:0009061", "cystic fibrosis"), "cf": ("MONDO:0009061", "cystic fibrosis"),
|
|
15
|
+
"rett syndrome": ("MONDO:0010726", "Rett syndrome"), "rett": ("MONDO:0010726", "Rett syndrome"),
|
|
16
|
+
"hemophilia b": ("MONDO:0010604", "hemophilia B"),
|
|
17
|
+
"hemophilia a": ("MONDO:0010602", "hemophilia A"),
|
|
18
|
+
"leber congenital amaurosis 10": ("MONDO:0012723", "Leber congenital amaurosis 10"),
|
|
19
|
+
"lca10": ("MONDO:0012723", "Leber congenital amaurosis 10"),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def resolve_phenotype(text: str) -> Resolved:
|
|
24
|
+
"""Resolve a disease / phenotype goal to MONDO; unrecognized stays null (never invented)."""
|
|
25
|
+
if not text:
|
|
26
|
+
return Resolved(text=text, ontology="MONDO", note="empty")
|
|
27
|
+
key = text.strip().lower()
|
|
28
|
+
hit = _PHENO.get(key)
|
|
29
|
+
if hit is None:
|
|
30
|
+
for k, v in _PHENO.items():
|
|
31
|
+
if k in key or key in k:
|
|
32
|
+
hit = v
|
|
33
|
+
break
|
|
34
|
+
if hit is None:
|
|
35
|
+
return Resolved(text=text, ontology="MONDO", confidence=None, note="unresolved phenotype term")
|
|
36
|
+
mid, label = hit
|
|
37
|
+
return Resolved(text=text, id=mid, label=label, ontology="MONDO", confidence=0.9)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""WriteSpec feasibility / satisfiability check.
|
|
2
|
+
|
|
3
|
+
Given a :class:`WriteRequest`, test the NECESSARY conditions a write must meet, by wrapping the existing stages:
|
|
4
|
+
|
|
5
|
+
* reachability - is the target locus / gene reachable by a writer in the writable-genome atlas?
|
|
6
|
+
* deliverability - does any vehicle fit the cargo size (the delivery recommender)?
|
|
7
|
+
* legality - does the design pass the rule set (the repair-oriented proof object)?
|
|
8
|
+
|
|
9
|
+
Returns ``feasible`` or ``infeasible`` plus the NAMED blocking constraint(s) and repair hints (from the rule-set
|
|
10
|
+
proof and a delivery suggestion), so an agent can repair the spec and re-check. A check whose backend is absent is
|
|
11
|
+
reported ``indeterminate`` and does not, by itself, block (it is surfaced, never silently passed). Feasibility is
|
|
12
|
+
necessary-conditions only: it rules out unreachable / undeliverable / illegal, NOT whether the write will WORK
|
|
13
|
+
(that is the downstream stages' calibrated, still-uncertain prediction).
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
|
|
21
|
+
from pen_stack.spec.writespec import WriteRequest
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SatisfyResult(BaseModel):
|
|
25
|
+
feasible: bool
|
|
26
|
+
checks: dict[str, Any] = Field(default_factory=dict) # reachability / deliverability / legality verdicts
|
|
27
|
+
blocking: list[dict] = Field(default_factory=list) # named blocking constraints
|
|
28
|
+
repairs: list[dict] = Field(default_factory=list) # repair hints
|
|
29
|
+
note: str | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _reachability(spec: WriteRequest) -> dict:
|
|
33
|
+
gene = spec.target.gene.id if (spec.target.gene and spec.target.gene.id) else None
|
|
34
|
+
if spec.target.kind == "att_site":
|
|
35
|
+
return {"ok": True, "status": "att/landing site supplied; reachability is by the installed att",
|
|
36
|
+
"determinable": True}
|
|
37
|
+
if gene is None:
|
|
38
|
+
return {"ok": None, "status": "no resolved target gene/locus; supply one for a reachability check",
|
|
39
|
+
"determinable": False}
|
|
40
|
+
try:
|
|
41
|
+
from pen_stack.atlas.crosslink import loci_for_gene
|
|
42
|
+
df = loci_for_gene(gene, ct="k562")
|
|
43
|
+
n = 0 if df is None else len(df)
|
|
44
|
+
if n > 0:
|
|
45
|
+
return {"ok": True, "status": f"{n} writable bins overlap {gene} in the atlas", "determinable": True}
|
|
46
|
+
return {"ok": False, "status": f"no writable bin overlaps {gene} in the atlas (cross-check the locus)",
|
|
47
|
+
"determinable": True}
|
|
48
|
+
except Exception as e: # noqa: BLE001
|
|
49
|
+
return {"ok": None, "status": f"atlas reachability unavailable ({type(e).__name__})", "determinable": False}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _deliverability(spec: WriteRequest) -> dict:
|
|
53
|
+
bp = spec.constraints.max_cargo_bp or sum((c.length_bp or 0) for c in spec.cargo) or None
|
|
54
|
+
if bp is None:
|
|
55
|
+
return {"ok": None, "status": "no cargo size given; supply bp/kb for a deliverability check",
|
|
56
|
+
"determinable": False}
|
|
57
|
+
try:
|
|
58
|
+
from pen_stack.planner.delivery_predict import recommend_delivery_plus
|
|
59
|
+
rec = recommend_delivery_plus("DNA", bp, None)
|
|
60
|
+
ranked = rec.get("ranked") or [] # the recommender already filters to vehicles that fit the cargo
|
|
61
|
+
excluded = rec.get("excluded") or []
|
|
62
|
+
if ranked:
|
|
63
|
+
top = ranked[0].get("vehicle") or ranked[0].get("name")
|
|
64
|
+
return {"ok": True, "status": f"{len(ranked)} vehicle(s) fit {bp} bp (top: {top}; "
|
|
65
|
+
f"{len(excluded)} excluded by capacity)", "determinable": True, "top": top}
|
|
66
|
+
return {"ok": False, "status": f"no vehicle in the palette fits {bp} bp", "determinable": True}
|
|
67
|
+
except Exception: # noqa: BLE001
|
|
68
|
+
if bp <= 8000:
|
|
69
|
+
return {"ok": True, "status": f"{bp} bp is within typical single-vector capacity (<= ~8 kb)",
|
|
70
|
+
"determinable": True}
|
|
71
|
+
return {"ok": False, "status": f"{bp} bp exceeds typical single-vector capacity (~8 kb); needs a "
|
|
72
|
+
"dual/high-capacity strategy", "determinable": True}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _legality(spec: WriteRequest) -> dict:
|
|
76
|
+
try:
|
|
77
|
+
from pen_stack.verify.proof import verify_proof
|
|
78
|
+
proof = verify_proof(spec.to_legacy_design())
|
|
79
|
+
ax = next((a for a in proof.axes if a.axis == "legality"), None)
|
|
80
|
+
if ax is None:
|
|
81
|
+
return {"ok": None, "status": "no legality axis returned", "determinable": False}
|
|
82
|
+
ok = ax.status in ("pass", "clear")
|
|
83
|
+
out = {"ok": ok, "status": ax.status, "determinable": True}
|
|
84
|
+
if not ok:
|
|
85
|
+
out["violated"] = [v for v in (ax.violated or [])]
|
|
86
|
+
if ax.repair_hint:
|
|
87
|
+
out["repair_hint"] = ax.repair_hint
|
|
88
|
+
return out
|
|
89
|
+
except Exception as e: # noqa: BLE001
|
|
90
|
+
return {"ok": None, "status": f"legality check unavailable ({type(e).__name__})", "determinable": False}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def check_satisfiable(spec: WriteRequest) -> SatisfyResult:
|
|
94
|
+
"""Run the reachability + deliverability + legality necessary-condition checks; name blocking constraints."""
|
|
95
|
+
checks = {"reachability": _reachability(spec), "deliverability": _deliverability(spec),
|
|
96
|
+
"legality": _legality(spec)}
|
|
97
|
+
blocking: list[dict] = []
|
|
98
|
+
repairs: list[dict] = []
|
|
99
|
+
for name, c in checks.items():
|
|
100
|
+
if c.get("ok") is False:
|
|
101
|
+
blocking.append({"constraint": name, "reason": c.get("status")})
|
|
102
|
+
if name == "deliverability":
|
|
103
|
+
repairs.append({"constraint": "deliverability",
|
|
104
|
+
"hint": "reduce the cargo, split across a dual-vector strategy, or use an "
|
|
105
|
+
"integrating vehicle"})
|
|
106
|
+
if name == "legality" and c.get("repair_hint"):
|
|
107
|
+
repairs.append({"constraint": "legality", "hint": c["repair_hint"]})
|
|
108
|
+
if name == "reachability":
|
|
109
|
+
repairs.append({"constraint": "reachability",
|
|
110
|
+
"hint": "choose a writable locus (the atlas Site Finder ranks reachable, safe loci)"})
|
|
111
|
+
feasible = not blocking
|
|
112
|
+
note = ("all determinable necessary conditions met (feasibility is necessary, not sufficient: efficacy is the "
|
|
113
|
+
"downstream prediction)" if feasible else "blocked by the named constraint(s); repairs suggested")
|
|
114
|
+
return SatisfyResult(feasible=feasible, checks=checks, blocking=blocking, repairs=repairs, note=note)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""WriteSpec service: the one call the surfaces (REST / MCP / web builder) use.
|
|
2
|
+
|
|
3
|
+
``parse_request`` runs the grounded extractor, plans clarifying questions, and (optionally) the feasibility
|
|
4
|
+
check, returning a single JSON-able payload: the typed WriteSpec, the assumptions behind every inferred field,
|
|
5
|
+
the clarifying questions for anything underspecified, the unresolved terms, the downstream design adapter, and
|
|
6
|
+
the feasibility verdict. Nothing is fabricated: unresolved stays null, inferred is labelled, ambiguous asks.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pen_stack.spec.clarify import clarifying_questions
|
|
13
|
+
from pen_stack.spec.extract import extract_writespec
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_request(prose: str, *, overrides: dict | None = None, check_feasibility: bool = True) -> dict[str, Any]:
|
|
17
|
+
"""Parse prose into a typed WriteSpec + clarifications + (optionally) a feasibility verdict."""
|
|
18
|
+
spec = extract_writespec(prose, overrides=overrides)
|
|
19
|
+
qs = clarifying_questions(spec)
|
|
20
|
+
out: dict[str, Any] = {
|
|
21
|
+
"writespec": spec.model_dump(),
|
|
22
|
+
"assumptions": spec.assumptions,
|
|
23
|
+
"clarifications": qs,
|
|
24
|
+
"actionable": not qs,
|
|
25
|
+
"unresolved": spec.unresolved,
|
|
26
|
+
"ontology_validation": spec.ontology_validation(),
|
|
27
|
+
"legacy_design": spec.to_legacy_design(),
|
|
28
|
+
"no_fabrication": True,
|
|
29
|
+
}
|
|
30
|
+
if check_feasibility:
|
|
31
|
+
from pen_stack.spec.satisfy import check_satisfiable
|
|
32
|
+
out["feasibility"] = check_satisfiable(spec).model_dump()
|
|
33
|
+
return out
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""The WriteRequest schema: a typed, ontology-backed genome-writing request.
|
|
2
|
+
|
|
3
|
+
``WriteRequest`` is an SBOL3 profile expressed as a pydantic model. It carries the write semantics (write-type,
|
|
4
|
+
cargo with Sequence-Ontology roles, target locus/gene/att-site/phenotype, cell type, constraints) PLUS a
|
|
5
|
+
grounding discipline: a per-field provenance map (explicit | inferred | user | unresolved), the assumptions behind
|
|
6
|
+
every inferred field, clarifying questions for underspecified required fields, and the list of terms that could
|
|
7
|
+
not be resolved (kept null, never invented). A WriteRequest is a REQUEST, not a claim.
|
|
8
|
+
|
|
9
|
+
Round-trips: JSON always; SBOL3 via the real ``sbol3`` library when the ``[spec]`` extra is installed (native
|
|
10
|
+
Components + SO roles for interoperability, with the full typed spec carried losslessly in a PROV-O-namespaced
|
|
11
|
+
annotation); GenBank for a cargo component that carries a DNA sequence. ``to_legacy_design`` is the adapter that
|
|
12
|
+
emits the dict the existing downstream stages (verify / plan / safety) already consume, so the whole stack reads
|
|
13
|
+
one contract without a rewrite.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from typing import Any, Literal
|
|
19
|
+
|
|
20
|
+
from pydantic import BaseModel, Field
|
|
21
|
+
|
|
22
|
+
WRITE_TYPES = ["insertion", "excision", "inversion", "replacement", "regulatory_rewrite",
|
|
23
|
+
"landing_pad_install", "multiplex"]
|
|
24
|
+
|
|
25
|
+
ProvenanceKind = Literal["explicit", "inferred", "user", "unresolved"]
|
|
26
|
+
|
|
27
|
+
# id-format validators per ontology (a resolved id must match, else it is flagged not-valid).
|
|
28
|
+
_ID_FORMATS = {
|
|
29
|
+
"SO": re.compile(r"^SO:\d{7}$"),
|
|
30
|
+
"Cellosaurus": re.compile(r"^CVCL_[A-Z0-9]{4}$"),
|
|
31
|
+
"CL": re.compile(r"^CL:\d{7}$"),
|
|
32
|
+
"MONDO": re.compile(r"^MONDO:\d{7}$"),
|
|
33
|
+
"HPO": re.compile(r"^HP:\d{7}$"),
|
|
34
|
+
"ChEBI": re.compile(r"^CHEBI:\d+$"),
|
|
35
|
+
"HGNC": re.compile(r"^[A-Z0-9-]+$"),
|
|
36
|
+
"GRCh38": re.compile(r"^chr[0-9XYM]+(:\d+-\d+)?$"),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Resolved(BaseModel):
|
|
41
|
+
"""A free-text term resolved to a canonical ontology id, with confidence and the candidate set when ambiguous."""
|
|
42
|
+
text: str | None = None # the surface term from the prose
|
|
43
|
+
id: str | None = None # canonical id (CVCL_0063, SO:0000167, MONDO:..., a gene symbol, ...)
|
|
44
|
+
label: str | None = None # canonical label
|
|
45
|
+
ontology: str | None = None # Cellosaurus | CL | SO | HGNC | MONDO | HPO | ChEBI | GRCh38
|
|
46
|
+
confidence: float | None = None
|
|
47
|
+
candidates: list[dict] = Field(default_factory=list) # ranked alternatives when ambiguous
|
|
48
|
+
note: str | None = None
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def resolved(self) -> bool:
|
|
52
|
+
return self.id is not None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CargoComponent(BaseModel):
|
|
56
|
+
"""A component to be written (CDS, promoter, insulator, attB, polyA, ...), with its SO/SBO role."""
|
|
57
|
+
name: str
|
|
58
|
+
role: Resolved | None = None # Sequence-Ontology role
|
|
59
|
+
sequence: str | None = None # DNA sequence if given (else intent-only)
|
|
60
|
+
length_bp: int | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Target(BaseModel):
|
|
64
|
+
"""Where the write goes: a locus, a gene, an att/landing site, or a phenotype goal."""
|
|
65
|
+
kind: Literal["locus", "gene", "att_site", "phenotype", "unspecified"] = "unspecified"
|
|
66
|
+
gene: Resolved | None = None
|
|
67
|
+
locus: Resolved | None = None # GRCh38 coordinate / safe-harbour
|
|
68
|
+
att_site: str | None = None # attB/attP/landing-pad id
|
|
69
|
+
phenotype: Resolved | None = None # MONDO/HPO disease goal
|
|
70
|
+
# Further genes named alongside ``gene`` in one request ("knock out TRAC and B2M"). ``gene`` stays the primary
|
|
71
|
+
# target so every existing consumer reads the same field; the rest are carried here rather than dropped, since
|
|
72
|
+
# a multiplex request that silently keeps only its first target would be designed against an incomplete spec.
|
|
73
|
+
additional_genes: list[Resolved] = Field(default_factory=list)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Constraints(BaseModel):
|
|
77
|
+
"""The request's constraints (efficiency floor, scarless, safety switch, copy number, guardrails, delivery)."""
|
|
78
|
+
efficiency_floor: float | None = None
|
|
79
|
+
scarless: bool | None = None
|
|
80
|
+
safety_switch_required: bool | None = None
|
|
81
|
+
copy_number: int | None = None
|
|
82
|
+
germline: bool | None = None # germline guardrail (True = a germline edit was requested -> flagged)
|
|
83
|
+
max_cargo_bp: int | None = None
|
|
84
|
+
delivery_limit: str | None = None # e.g. "non_integrating", "AAV_only"
|
|
85
|
+
inducer: Resolved | None = None # ChEBI small-molecule inducer / selection agent
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class WriteRequest(BaseModel):
|
|
89
|
+
"""A typed, ontology-backed, machine-checkable genome-writing request (an SBOL3 profile). A REQUEST, not a claim.
|
|
90
|
+
|
|
91
|
+
Every field that was not explicit in the source prose is recorded in ``provenance`` as ``inferred`` (with the
|
|
92
|
+
rationale in ``assumptions``); a required field that is unspecified or ambiguous yields a ``clarifications``
|
|
93
|
+
question rather than a guess; a term that could not be resolved is listed in ``unresolved`` and kept null.
|
|
94
|
+
"""
|
|
95
|
+
write_type: str # one of WRITE_TYPES
|
|
96
|
+
cargo: list[CargoComponent] = Field(default_factory=list)
|
|
97
|
+
target: Target = Field(default_factory=Target)
|
|
98
|
+
cell_type: Resolved | None = None
|
|
99
|
+
constraints: Constraints = Field(default_factory=Constraints)
|
|
100
|
+
# --- grounding discipline ---
|
|
101
|
+
provenance: dict[str, ProvenanceKind] = Field(default_factory=dict) # field -> origin
|
|
102
|
+
assumptions: list[str] = Field(default_factory=list) # inferred fields + rationale
|
|
103
|
+
clarifications: list[str] = Field(default_factory=list) # questions for underspecified fields
|
|
104
|
+
unresolved: list[str] = Field(default_factory=list) # terms that could not be resolved
|
|
105
|
+
free_text_note: str | None = None # the original prose (the cargo-function the Guardian must screen)
|
|
106
|
+
extensions: dict[str, Any] = Field(default_factory=dict) # edge intents the schema does not cover
|
|
107
|
+
|
|
108
|
+
# ---- validation -------------------------------------------------------------------------------------------
|
|
109
|
+
def ontology_validation(self) -> dict[str, Any]:
|
|
110
|
+
"""Check every resolved field's id against its ontology id-format. Returns the per-field verdict."""
|
|
111
|
+
bad: list[dict] = []
|
|
112
|
+
checked = 0
|
|
113
|
+
for field, r in self._resolved_fields().items():
|
|
114
|
+
if r.id is None or r.ontology is None:
|
|
115
|
+
continue
|
|
116
|
+
checked += 1
|
|
117
|
+
fmt = _ID_FORMATS.get(r.ontology)
|
|
118
|
+
if fmt is not None and not fmt.match(r.id):
|
|
119
|
+
bad.append({"field": field, "id": r.id, "ontology": r.ontology})
|
|
120
|
+
return {"checked": checked, "invalid": bad, "all_valid": not bad}
|
|
121
|
+
|
|
122
|
+
def _resolved_fields(self) -> dict[str, Resolved]:
|
|
123
|
+
out: dict[str, Resolved] = {}
|
|
124
|
+
if self.cell_type:
|
|
125
|
+
out["cell_type"] = self.cell_type
|
|
126
|
+
if self.target.gene:
|
|
127
|
+
out["target.gene"] = self.target.gene
|
|
128
|
+
if self.target.locus:
|
|
129
|
+
out["target.locus"] = self.target.locus
|
|
130
|
+
if self.target.phenotype:
|
|
131
|
+
out["target.phenotype"] = self.target.phenotype
|
|
132
|
+
if self.constraints.inducer:
|
|
133
|
+
out["constraints.inducer"] = self.constraints.inducer
|
|
134
|
+
for i, c in enumerate(self.cargo):
|
|
135
|
+
if c.role:
|
|
136
|
+
out[f"cargo[{i}].role"] = c.role
|
|
137
|
+
return out
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def is_grounded(self) -> bool:
|
|
141
|
+
"""No fabrication: every field is either explicit, user, inferred-and-labelled, or unresolved-and-null."""
|
|
142
|
+
return self.ontology_validation()["all_valid"]
|
|
143
|
+
|
|
144
|
+
# ---- the downstream adapter -------------------------------------------------------------------------------
|
|
145
|
+
def to_legacy_design(self) -> dict:
|
|
146
|
+
"""Emit the dict the existing downstream stages (verify / plan / safety / delivery) already consume.
|
|
147
|
+
|
|
148
|
+
This is the wrap-not-rewrite adapter: the whole stack reads ``WriteRequest`` through this one mapping
|
|
149
|
+
instead of a per-stage rewrite. Unspecified fields fall back to the stack's documented defaults.
|
|
150
|
+
|
|
151
|
+
The design it emits carries ONE target, ``target.gene``. A request naming several
|
|
152
|
+
(``target.additional_genes``) describes several writes, so it needs one design per target: this adapter
|
|
153
|
+
does not fold them together, and a caller iterating a multiplex request must build a design for each.
|
|
154
|
+
"""
|
|
155
|
+
cargo_bp = self.constraints.max_cargo_bp
|
|
156
|
+
if cargo_bp is None:
|
|
157
|
+
cargo_bp = sum((c.length_bp or 0) for c in self.cargo) or None
|
|
158
|
+
gene = self.target.gene.id if (self.target.gene and self.target.gene.id) else None
|
|
159
|
+
locus = self.target.locus.id if (self.target.locus and self.target.locus.id) else None
|
|
160
|
+
chrom = None
|
|
161
|
+
if locus and locus.startswith("chr"):
|
|
162
|
+
chrom = locus.split(":")[0]
|
|
163
|
+
design = {
|
|
164
|
+
"write_type": self.write_type,
|
|
165
|
+
"gene": gene,
|
|
166
|
+
"chrom": chrom,
|
|
167
|
+
"cargo_bp": cargo_bp,
|
|
168
|
+
"cell_type": self.cell_type.id if (self.cell_type and self.cell_type.id) else None,
|
|
169
|
+
# delivery_limit holds either a named vehicle or the "non_integrating" flag
|
|
170
|
+
"delivery_vehicle": (self.constraints.delivery_limit
|
|
171
|
+
if self.constraints.delivery_limit not in (None, "non_integrating") else None),
|
|
172
|
+
"non_integrating": (self.constraints.delivery_limit == "non_integrating") or None,
|
|
173
|
+
"installed_att": bool(self.target.att_site),
|
|
174
|
+
"cargo_function": self.free_text_note,
|
|
175
|
+
}
|
|
176
|
+
return {k: v for k, v in design.items() if v is not None}
|
|
177
|
+
|
|
178
|
+
# ---- serialization ----------------------------------------------------------------------------------------
|
|
179
|
+
def to_json(self) -> str:
|
|
180
|
+
return self.model_dump_json(indent=2)
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def from_json(cls, s: str) -> "WriteRequest":
|
|
184
|
+
return cls.model_validate_json(s)
|
|
185
|
+
|
|
186
|
+
def to_sbol3(self, namespace: str = "https://penstack.org/writespec") -> str:
|
|
187
|
+
"""Serialize to SBOL3 (sorted N-triples) via the ``sbol3`` library: native Components + Sequence-Ontology
|
|
188
|
+
roles for interoperability, with the full typed spec carried losslessly in a PROV-O-namespaced annotation.
|
|
189
|
+
|
|
190
|
+
Requires the ``[spec]`` extra (``pip install sbol3``); raises ImportError otherwise (never fabricates).
|
|
191
|
+
"""
|
|
192
|
+
import sbol3
|
|
193
|
+
sbol3.set_namespace(namespace)
|
|
194
|
+
doc = sbol3.Document()
|
|
195
|
+
top = sbol3.Component(f"write_{self.write_type}", sbol3.SBO_DNA)
|
|
196
|
+
top.name = f"WriteRequest: {self.write_type}"
|
|
197
|
+
# native SO roles for the cargo (interoperable with the SBOL3 / GenBank ecosystem)
|
|
198
|
+
for i, c in enumerate(self.cargo):
|
|
199
|
+
sub = sbol3.Component(f"cargo_{i}_{re.sub(r'[^A-Za-z0-9]', '_', c.name)[:24]}", sbol3.SBO_DNA)
|
|
200
|
+
if c.role and c.role.id and c.role.ontology == "SO":
|
|
201
|
+
sub.roles = [f"https://identifiers.org/SO:{c.role.id.split(':')[1]}"]
|
|
202
|
+
if c.sequence:
|
|
203
|
+
seq = sbol3.Sequence(f"seq_{i}", elements=c.sequence.upper(), encoding=sbol3.IUPAC_DNA_ENCODING)
|
|
204
|
+
doc.add(seq)
|
|
205
|
+
sub.sequences = [seq.identity]
|
|
206
|
+
doc.add(sub)
|
|
207
|
+
top.features.append(sbol3.SubComponent(sub))
|
|
208
|
+
# the full typed spec rides losslessly as a custom (PROV-O-style) annotation
|
|
209
|
+
top.write_spec_json = sbol3.TextProperty(top, f"{namespace}#writeSpecJson", 0, 1)
|
|
210
|
+
top.write_spec_json = self.to_json()
|
|
211
|
+
doc.add(top)
|
|
212
|
+
return doc.write_string(sbol3.SORTED_NTRIPLES)
|
|
213
|
+
|
|
214
|
+
@classmethod
|
|
215
|
+
def from_sbol3(cls, ttl: str, namespace: str = "https://penstack.org/writespec") -> "WriteRequest":
|
|
216
|
+
"""Reconstruct a WriteRequest from its SBOL3 serialization (the lossless PROV-O annotation)."""
|
|
217
|
+
import sbol3
|
|
218
|
+
sbol3.set_namespace(namespace)
|
|
219
|
+
doc = sbol3.Document()
|
|
220
|
+
doc.read_string(ttl, sbol3.SORTED_NTRIPLES)
|
|
221
|
+
for obj in doc.objects:
|
|
222
|
+
prop = f"{namespace}#writeSpecJson"
|
|
223
|
+
val = obj._properties.get(prop)
|
|
224
|
+
if val:
|
|
225
|
+
raw = str(val[0])
|
|
226
|
+
return cls.from_json(raw)
|
|
227
|
+
raise ValueError("no WriteRequest annotation found in the SBOL3 document")
|
|
228
|
+
|
|
229
|
+
def to_genbank(self) -> str | None:
|
|
230
|
+
"""Export the cargo as a GenBank record when it carries a DNA sequence; None for an intent-only spec."""
|
|
231
|
+
seqs = [c for c in self.cargo if c.sequence]
|
|
232
|
+
if not seqs:
|
|
233
|
+
return None
|
|
234
|
+
from io import StringIO
|
|
235
|
+
|
|
236
|
+
from Bio.Seq import Seq
|
|
237
|
+
from Bio.SeqFeature import FeatureLocation, SeqFeature
|
|
238
|
+
from Bio.SeqRecord import SeqRecord
|
|
239
|
+
full = "".join(c.sequence.upper() for c in seqs)
|
|
240
|
+
rec = SeqRecord(Seq(full), id="WRITESPEC", name="WriteSpec", description=f"{self.write_type} cargo")
|
|
241
|
+
rec.annotations["molecule_type"] = "DNA"
|
|
242
|
+
pos = 0
|
|
243
|
+
for c in seqs:
|
|
244
|
+
ln = len(c.sequence)
|
|
245
|
+
role = (c.role.label if (c.role and c.role.label) else c.name)
|
|
246
|
+
rec.features.append(SeqFeature(FeatureLocation(pos, pos + ln), type="misc_feature",
|
|
247
|
+
qualifiers={"label": [role]}))
|
|
248
|
+
pos += ln
|
|
249
|
+
buf = StringIO()
|
|
250
|
+
from Bio import SeqIO
|
|
251
|
+
SeqIO.write(rec, buf, "genbank")
|
|
252
|
+
return buf.getvalue()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""pen_stack.twin, the digital twin for genome writing.
|
|
2
|
+
|
|
3
|
+
A calibrated, scope-bounded write-outcome predictor: mechanism where computable (cassette expression), an
|
|
4
|
+
in-distribution virtual-cell transcriptional-response estimate (OOD-gated), and an immune-outcome dimension
|
|
5
|
+
sourced from the immune profile, fused into one prediction with an interval that widens under OOD and an explicit
|
|
6
|
+
boundary at phenotype. A hypothesis engine, grounded where it is weak; never an oracle of truth.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pen_stack.twin.calibrate import calibrate_outcome, interval_coverage
|
|
11
|
+
from pen_stack.twin.mechanistic import cassette_expression
|
|
12
|
+
from pen_stack.twin.outcome import predict_outcome
|
|
13
|
+
|
|
14
|
+
__all__ = ["predict_outcome", "cassette_expression", "calibrate_outcome", "interval_coverage"]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Twin calibration, two-sided.
|
|
2
|
+
|
|
3
|
+
Reports the twin's calibration on held-out (predicted, observed) outcomes WHATEVER the shape: interval coverage
|
|
4
|
+
vs nominal, and a skill comparison against a NAIVE baseline (predict the mean) with a bootstrap CI on the MAE
|
|
5
|
+
gap. A twin "beats" the baseline only when the CI excludes zero in its favour, otherwise the negative is
|
|
6
|
+
reported verbatim. This mirrors the field's own result (Arc VCC): perturbation prediction does not yet
|
|
7
|
+
consistently beat naive baselines, and the twin says so rather than hiding it.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _mae(pred, obs) -> float:
|
|
15
|
+
return float(np.mean(np.abs(np.asarray(pred, float) - np.asarray(obs, float))))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _bootstrap_gap(twin_pred, naive_pred, obs, *, reps: int = 300, seed: int = 0) -> tuple[float, list[float]]:
|
|
19
|
+
"""Bootstrap the MAE gap (naive - twin); positive => twin better. Returns mean gap + 95% CI."""
|
|
20
|
+
rng = np.random.default_rng(seed)
|
|
21
|
+
twin_pred, naive_pred, obs = map(lambda a: np.asarray(a, float), (twin_pred, naive_pred, obs))
|
|
22
|
+
n = len(obs)
|
|
23
|
+
gaps = []
|
|
24
|
+
for _ in range(reps):
|
|
25
|
+
idx = rng.integers(0, n, n)
|
|
26
|
+
gaps.append(_mae(naive_pred[idx], obs[idx]) - _mae(twin_pred[idx], obs[idx]))
|
|
27
|
+
lo, hi = np.percentile(gaps, [2.5, 97.5])
|
|
28
|
+
return float(np.mean(gaps)), [round(float(lo), 4), round(float(hi), 4)]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def interval_coverage(intervals, obs) -> dict:
|
|
32
|
+
"""Empirical coverage of the prediction intervals vs the observed outcomes."""
|
|
33
|
+
obs = np.asarray(obs, float)
|
|
34
|
+
covered = np.array([lo <= y <= hi for (lo, hi), y in zip(intervals, obs)])
|
|
35
|
+
return {"empirical_coverage": round(float(np.mean(covered)), 4), "n": int(len(obs))}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def calibrate_outcome(predictions, observations, *, intervals=None, reps: int = 300, seed: int = 0) -> dict:
|
|
39
|
+
"""Two-sided calibration of the twin against a naive mean baseline. Reports the MAE gap with a
|
|
40
|
+
bootstrap CI (beats-or-not), and interval coverage when intervals are supplied."""
|
|
41
|
+
pred = np.asarray(predictions, float)
|
|
42
|
+
obs = np.asarray(observations, float)
|
|
43
|
+
n = len(obs)
|
|
44
|
+
if n < 3:
|
|
45
|
+
return {"available": False, "n": n, "reason": "too few held-out outcomes to calibrate"}
|
|
46
|
+
naive = np.full(n, float(np.mean(obs)))
|
|
47
|
+
mae_twin, mae_naive = _mae(pred, obs), _mae(naive, obs)
|
|
48
|
+
gap, ci = _bootstrap_gap(pred, naive, obs, reps=reps, seed=seed)
|
|
49
|
+
beats = bool(ci[0] > 0) # CI excludes 0 in the twin's favour
|
|
50
|
+
out = {
|
|
51
|
+
"available": True, "n": n,
|
|
52
|
+
"mae_twin": round(mae_twin, 4), "mae_naive": round(mae_naive, 4),
|
|
53
|
+
"mae_gap_naive_minus_twin": round(gap, 4), "gap_ci": ci,
|
|
54
|
+
"beats_naive_baseline": beats,
|
|
55
|
+
"honest_note": ("twin beats naive (CI excludes 0)" if beats
|
|
56
|
+
else "twin does NOT beat naive on this data (CI spans 0) - reported, not hidden"),
|
|
57
|
+
"no_fabrication": True,
|
|
58
|
+
}
|
|
59
|
+
if intervals is not None:
|
|
60
|
+
out["interval_coverage"] = interval_coverage(intervals, obs)
|
|
61
|
+
return out
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Position-effect / expression supervision data."""
|
|
2
|
+
from pen_stack.twin.data.position_effect import ( # noqa: F401
|
|
3
|
+
DATASETS,
|
|
4
|
+
FEATURE_COLS,
|
|
5
|
+
SCHEMA,
|
|
6
|
+
available_datasets,
|
|
7
|
+
blocked_splits,
|
|
8
|
+
heldout_celltype_splits,
|
|
9
|
+
leakage_report,
|
|
10
|
+
load_position_effect,
|
|
11
|
+
normalize_within,
|
|
12
|
+
)
|