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,58 @@
|
|
|
1
|
+
"""Cell-type coverage cards + cross-type OOD labelling + graceful degradation.
|
|
2
|
+
|
|
3
|
+
Each cell type is a graph node carrying a **coverage card** (which data tracks exist). A score is only as
|
|
4
|
+
trustworthy as its coverage: a partial-coverage cell type **degrades gracefully** (its confidence is capped),
|
|
5
|
+
and a score computed in one cell type but *queried* for another is **OOD-labelled**, the finding that
|
|
6
|
+
chromatin marks are conserved, so cross-cell-type context is intrinsically weak/heuristic, not a guarantee.
|
|
7
|
+
Tier-B cell types are a documented roadmap, never silently extrapolated.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
from pen_stack._resources import resource
|
|
16
|
+
|
|
17
|
+
# graceful-degradation policy: the maximum trustworthy confidence a cell type's coverage supports.
|
|
18
|
+
_MAX_CONF = {"full": 1.0, "partial": 0.6, "none": 0.0}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@lru_cache(maxsize=1)
|
|
22
|
+
def _cfg() -> dict:
|
|
23
|
+
return yaml.safe_load(resource("configs/cell_types.yaml").read_text(encoding="utf-8"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def coverage_card(cell_type: str) -> dict | None:
|
|
27
|
+
return _cfg()["cell_types"].get(cell_type)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def cell_types() -> list[str]:
|
|
31
|
+
return list(_cfg()["cell_types"])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def tier_b_roadmap() -> list[dict]:
|
|
35
|
+
return list(_cfg().get("tier_b_roadmap", []))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def degrade(raw_confidence: float, cell_type: str) -> dict:
|
|
39
|
+
"""Cap a confidence by the cell type's coverage (graceful degradation). Returns the degraded value +
|
|
40
|
+
whether degradation was applied + the coverage label, never silently inflates."""
|
|
41
|
+
card = coverage_card(cell_type) or {}
|
|
42
|
+
cov = card.get("coverage", "none")
|
|
43
|
+
cap = _MAX_CONF.get(cov, 0.0)
|
|
44
|
+
degraded = min(float(raw_confidence), cap)
|
|
45
|
+
return {"cell_type": cell_type, "coverage": cov, "raw_confidence": round(float(raw_confidence), 4),
|
|
46
|
+
"confidence": round(degraded, 4), "degraded": degraded < float(raw_confidence),
|
|
47
|
+
"cap": cap, "tracks": card.get("tracks", [])}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def cross_cell_type_ood(query_cell_type: str, scored_in_cell_type: str) -> dict:
|
|
51
|
+
"""Label a cross-cell-type query as OOD/extrapolating (cross-type signal is weak, heuristic).
|
|
52
|
+
Same cell type = in-distribution; different = extrapolating."""
|
|
53
|
+
ood = query_cell_type != scored_in_cell_type
|
|
54
|
+
return {"query_cell_type": query_cell_type, "scored_in_cell_type": scored_in_cell_type,
|
|
55
|
+
"ood": ood,
|
|
56
|
+
"label": "extrapolating (cross-cell-type; chromatin conserved -> weak heuristic)"
|
|
57
|
+
if ood else "in-distribution",
|
|
58
|
+
"note": "cross-cell-type transfer is a heuristic signal, not a guarantee; reported, not hidden"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""The gated living-ingestion loop, the agent proposes; a gate disposes.
|
|
2
|
+
|
|
3
|
+
The living-literature monitor (Europe PMC scan) and agents emit **candidate** nodes/edges from new evidence.
|
|
4
|
+
Principle 1 is inviolable and encoded here: **no process auto-edits the curated world-model.** Candidates are
|
|
5
|
+
*quarantined*; the ONLY way one enters the graph is `gate_admit(...)` with (a) automated checks passing AND
|
|
6
|
+
(b) explicit approval. Every admission is versioned with date + evidence + the gate that admitted it
|
|
7
|
+
(auditable history, Principle 3).
|
|
8
|
+
|
|
9
|
+
There is deliberately no bulk-merge / auto-accept function: `propose(...)` only appends to the quarantine and
|
|
10
|
+
never receives a `Graph`, so it *cannot* mutate the graph (asserted by test).
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import datetime as _dt
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Any, Literal
|
|
17
|
+
|
|
18
|
+
from pen_stack.graph.schema import Edge, Graph, Node
|
|
19
|
+
|
|
20
|
+
CandidateKind = Literal["node", "edge"]
|
|
21
|
+
Status = Literal["quarantined", "admitted", "rejected"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Candidate:
|
|
26
|
+
kind: CandidateKind
|
|
27
|
+
payload: dict[str, Any] # a Node-like or Edge-like dict
|
|
28
|
+
provenance: dict[str, Any] # MUST carry a source + (doi or europepmc id)
|
|
29
|
+
evidence: str = "predicted" # measured / curated / predicted
|
|
30
|
+
status: Status = "quarantined"
|
|
31
|
+
note: str | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Quarantine:
|
|
36
|
+
"""Holds candidates; NEVER holds or touches a Graph. The only sink for `propose`."""
|
|
37
|
+
items: list[Candidate] = field(default_factory=list)
|
|
38
|
+
|
|
39
|
+
def propose(self, c: Candidate) -> None:
|
|
40
|
+
c.status = "quarantined" # forced; a proposal is never pre-approved
|
|
41
|
+
self.items.append(c)
|
|
42
|
+
|
|
43
|
+
def pending(self) -> list[Candidate]:
|
|
44
|
+
return [c for c in self.items if c.status == "quarantined"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def automated_checks(c: Candidate) -> tuple[bool, list[str]]:
|
|
48
|
+
"""Gate stage 1: a candidate must be provenance-bearing + well-typed before a human can even approve it."""
|
|
49
|
+
reasons = []
|
|
50
|
+
prov = c.provenance or {}
|
|
51
|
+
if not (prov.get("source") or prov.get("doi") or prov.get("europepmc")):
|
|
52
|
+
reasons.append("no provenance (need source / doi / europepmc id)")
|
|
53
|
+
if c.evidence not in ("measured", "curated", "predicted"):
|
|
54
|
+
reasons.append(f"invalid evidence kind {c.evidence!r}")
|
|
55
|
+
if c.kind == "node" and not (c.payload.get("id") and c.payload.get("type")):
|
|
56
|
+
reasons.append("node payload missing id/type")
|
|
57
|
+
if c.kind == "edge" and not all(c.payload.get(k) for k in ("src", "dst", "etype")):
|
|
58
|
+
reasons.append("edge payload missing src/dst/etype")
|
|
59
|
+
return (not reasons), reasons
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def gate_admit(graph: Graph, candidate: Candidate, *, approved: bool, admitted_by: str,
|
|
63
|
+
log: list[dict] | None = None) -> dict:
|
|
64
|
+
"""The ONLY path a candidate enters the curated graph. Requires automated checks AND explicit approval;
|
|
65
|
+
records a versioned admission. Returns the decision record; the graph is mutated ONLY on admit."""
|
|
66
|
+
log = log if log is not None else []
|
|
67
|
+
ok, reasons = automated_checks(candidate)
|
|
68
|
+
decision = {"date": _dt.date.today().isoformat(), "kind": candidate.kind,
|
|
69
|
+
"payload_id": candidate.payload.get("id") or candidate.payload.get("etype"),
|
|
70
|
+
"evidence": candidate.evidence, "provenance": candidate.provenance,
|
|
71
|
+
"automated_checks_passed": ok, "approved": bool(approved), "admitted_by": admitted_by}
|
|
72
|
+
if not (ok and approved):
|
|
73
|
+
candidate.status = "rejected"
|
|
74
|
+
decision["admitted"] = False
|
|
75
|
+
decision["reasons"] = reasons or ["not approved by gate"]
|
|
76
|
+
log.append(decision)
|
|
77
|
+
return decision
|
|
78
|
+
# admit
|
|
79
|
+
if candidate.kind == "node":
|
|
80
|
+
graph.add_node(Node(id=candidate.payload["id"], type=candidate.payload["type"],
|
|
81
|
+
props=candidate.payload.get("props", {})))
|
|
82
|
+
else:
|
|
83
|
+
graph.add_edge(Edge(src=candidate.payload["src"], dst=candidate.payload["dst"],
|
|
84
|
+
etype=candidate.payload["etype"], evidence=candidate.evidence,
|
|
85
|
+
confidence=candidate.payload.get("confidence"),
|
|
86
|
+
scope=candidate.payload.get("scope"), provenance=candidate.provenance))
|
|
87
|
+
candidate.status = "admitted"
|
|
88
|
+
decision["admitted"] = True
|
|
89
|
+
log.append(decision)
|
|
90
|
+
return decision
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --------------------------------------------------------------------------------------------------
|
|
94
|
+
# back-test: surface a KNOWN recent addition as a candidate (deterministic / CI-safe, no network)
|
|
95
|
+
# --------------------------------------------------------------------------------------------------
|
|
96
|
+
def back_test_candidates() -> list[Candidate]:
|
|
97
|
+
"""The pre-registered back-test fixture: the recently-published bridge system **ISPpu10** (Europe PMC
|
|
98
|
+
PPR1218813; bioRxiv 2026-03-19), which the live monitor surfaces. Proposed as a candidate writer node +
|
|
99
|
+
a `performs insertion` edge, to be admitted only through the gate."""
|
|
100
|
+
prov = {"source": "Europe PMC monitor", "europepmc": "PPR1218813",
|
|
101
|
+
"doi": "10.64898/2026.03.19.712850", "date": "2026-03-19"}
|
|
102
|
+
return [
|
|
103
|
+
Candidate(kind="node", evidence="predicted", provenance=prov,
|
|
104
|
+
payload={"id": "writer:ISPpu10", "type": "writer",
|
|
105
|
+
"props": {"family": "bridge_IS110", "system": "ISPpu10",
|
|
106
|
+
"output_form": "DNA", "note": "structure-gated bridge recombinase"}}),
|
|
107
|
+
Candidate(kind="edge", evidence="predicted", provenance=prov,
|
|
108
|
+
payload={"src": "writer:ISPpu10", "dst": "write_type:insertion", "etype": "performs",
|
|
109
|
+
"scope": "coverage_only (newly reported; not yet human-cell measured)"}),
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def propose_from_monitor(since: str = "2026-01-01", back_test: bool = False) -> Quarantine:
|
|
114
|
+
"""Run the monitor and quarantine its hits as candidates (live path; network-guarded). The back-test
|
|
115
|
+
path is deterministic. Candidates are ALWAYS quarantined, never merged here."""
|
|
116
|
+
q = Quarantine()
|
|
117
|
+
if back_test:
|
|
118
|
+
for c in back_test_candidates():
|
|
119
|
+
q.propose(c)
|
|
120
|
+
return q
|
|
121
|
+
try:
|
|
122
|
+
from pen_stack.monitor.run import run_monitor
|
|
123
|
+
rep = run_monitor(since=since)
|
|
124
|
+
for row in rep.get("queue", []) if isinstance(rep, dict) else []:
|
|
125
|
+
q.propose(Candidate(kind="node", evidence="predicted",
|
|
126
|
+
provenance={"source": "monitor", "doi": row.get("doi"),
|
|
127
|
+
"europepmc": row.get("id")},
|
|
128
|
+
payload={"id": f"writer:{row.get('name', 'candidate')}", "type": "writer",
|
|
129
|
+
"props": row}))
|
|
130
|
+
except Exception: # noqa: BLE001 - network/Europe PMC absent -> empty quarantine, never fabricates
|
|
131
|
+
pass
|
|
132
|
+
return q
|
pen_stack/graph/query.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Multi-hop queries over the world-model graph.
|
|
2
|
+
|
|
3
|
+
An agent asks a design question as ONE grounded multi-hop traversal, "which writers reach locus L AND are
|
|
4
|
+
deliverable by a vehicle carrying their cargo form?", and every answer carries the **provenanced edges** it
|
|
5
|
+
traversed, so the result is grounded by construction (no-fabrication: the answer is the path, not free text).
|
|
6
|
+
The flat atlas/crosslink joins become graph *views* (`writers_for_locus`, `vehicles_for_writer`) for parity.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pen_stack.graph.build import build_graph
|
|
11
|
+
from pen_stack.graph.schema import Edge, Graph
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def graph() -> Graph:
|
|
15
|
+
return build_graph()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ---- table-view parity queries (reproduce the joins) --------------------------------------
|
|
19
|
+
def vehicles_for_writer(family: str, g: Graph | None = None) -> list[dict]:
|
|
20
|
+
g = g or graph()
|
|
21
|
+
wid = f"writer:{family}"
|
|
22
|
+
return [{"vehicle": e.dst.split(":", 1)[1], "evidence": e.evidence, "scope": e.scope,
|
|
23
|
+
"provenance": e.provenance} for e in g.out_edges(wid, "deliverable_by")]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def writers_for_locus(locus: str, g: Graph | None = None) -> list[dict]:
|
|
27
|
+
g = g or graph()
|
|
28
|
+
lid = f"locus:{locus}"
|
|
29
|
+
out = []
|
|
30
|
+
for w in g.nodes_of("writer"):
|
|
31
|
+
for e in g.out_edges(w.id, "reaches"):
|
|
32
|
+
if e.dst == lid:
|
|
33
|
+
out.append({"writer": w.props["family"], "evidence": e.evidence, "scope": e.scope,
|
|
34
|
+
"provenance": e.provenance})
|
|
35
|
+
return out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# common safe-harbour aliases not captured by the GSH name or its anchor gene (case-insensitive).
|
|
39
|
+
_LOCUS_ALIASES = {"rosa26": "hrosa26", "rosa-26": "hrosa26", "hipp11": "h11", "hipp-11": "h11"}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _resolve_gsh_locus(query: str, g: Graph) -> str | None:
|
|
43
|
+
"""Resolve a user's locus query to a curated GSH node id, matching (case-insensitively) the GSH NAME, its
|
|
44
|
+
ANCHOR GENE, or a common alias, so AAVS1, its anchor PPP1R12C, and ROSA26 (the curated node is hRosa26)
|
|
45
|
+
all reach the same node. Returns the node id (``locus:<name>``) or None if it is not a curated safe harbour."""
|
|
46
|
+
q = str(query or "").strip().lower()
|
|
47
|
+
if not q:
|
|
48
|
+
return None
|
|
49
|
+
q = _LOCUS_ALIASES.get(q, q)
|
|
50
|
+
for n in g.nodes_of("locus"):
|
|
51
|
+
name = n.id.split(":", 1)[1]
|
|
52
|
+
anchor = str(n.props.get("anchor_gene") or "")
|
|
53
|
+
if q in {name.lower(), anchor.lower()}:
|
|
54
|
+
return n.id
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _answer(w, reach_edges: list[Edge], deliv: list[Edge]) -> dict:
|
|
59
|
+
path: list[Edge] = reach_edges[:1] + deliv
|
|
60
|
+
return {
|
|
61
|
+
"writer": w.props["family"], "output_form": w.props["output_form"],
|
|
62
|
+
"vehicles": [e.dst.split(":", 1)[1] for e in deliv],
|
|
63
|
+
"provenance_path": [{"src": e.src, "dst": e.dst, "etype": e.etype, "evidence": e.evidence,
|
|
64
|
+
"scope": e.scope, "provenance": e.provenance} for e in path],
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---- multi-hop design query (the headline graph capability) ------------------------------------
|
|
69
|
+
def writers_reaching_and_deliverable(locus: str, cargo_form: str | None = None,
|
|
70
|
+
g: Graph | None = None) -> dict:
|
|
71
|
+
"""Multi-hop: writers that REACH `locus` AND are DELIVERABLE_BY a vehicle (optionally carrying
|
|
72
|
+
`cargo_form`). Returns each answer with the full provenanced edge path (grounded answer).
|
|
73
|
+
|
|
74
|
+
Locus resolution (the graph only materialised curated safe-harbour nodes, so anything but their
|
|
75
|
+
exact name returned nothing): a query is first resolved to a curated GSH node by name / anchor gene / alias.
|
|
76
|
+
Failing that, if it resolves to a REAL gene, the answer is the tier-1 reprogrammable writers, which reach
|
|
77
|
+
ANY locus at the locus level (the documented crosslink property), with a scope flag saying so and that it is
|
|
78
|
+
NOT a curated safe harbour. A query that is neither returns a clean, explained empty result, never an error."""
|
|
79
|
+
g = g or graph()
|
|
80
|
+
gsh_id = _resolve_gsh_locus(locus, g)
|
|
81
|
+
|
|
82
|
+
if gsh_id is not None: # curated safe harbour: the existing grounded traversal
|
|
83
|
+
resolved = gsh_id.split(":", 1)[1]
|
|
84
|
+
answers = []
|
|
85
|
+
for w in g.nodes_of("writer"):
|
|
86
|
+
if cargo_form is not None and w.props.get("output_form") != cargo_form:
|
|
87
|
+
continue
|
|
88
|
+
reach = [e for e in g.out_edges(w.id, "reaches") if e.dst == gsh_id]
|
|
89
|
+
deliv = g.out_edges(w.id, "deliverable_by")
|
|
90
|
+
if reach and deliv:
|
|
91
|
+
answers.append(_answer(w, reach, deliv))
|
|
92
|
+
note = ("every answer is a provenanced multi-hop path over the graph"
|
|
93
|
+
+ (f"; '{locus}' resolved to curated safe harbour '{resolved}'" if resolved.lower() != str(locus).strip().lower() else ""))
|
|
94
|
+
return {"locus": locus, "resolved_locus": resolved, "is_curated_safe_harbour": True,
|
|
95
|
+
"cargo_form": cargo_form, "n_answers": len(answers), "answers": answers,
|
|
96
|
+
"grounded": all(a["provenance_path"] for a in answers), "no_fabrication": True, "note": note}
|
|
97
|
+
|
|
98
|
+
# not a curated GSH: does it resolve to a real gene? -> tier-1 reprogrammable writers reach ANY locus.
|
|
99
|
+
resolved_gene = None
|
|
100
|
+
try:
|
|
101
|
+
from pen_stack.planner.optimize import gene_region, resolve_gene
|
|
102
|
+
rg = resolve_gene(str(locus))
|
|
103
|
+
resolved_gene = rg if gene_region(rg) else None
|
|
104
|
+
except Exception: # noqa: BLE001 - gene-coords absent (CI/offline) -> no fallback, clean empty
|
|
105
|
+
resolved_gene = None
|
|
106
|
+
|
|
107
|
+
if resolved_gene is None:
|
|
108
|
+
return {"locus": locus, "resolved_locus": None, "is_curated_safe_harbour": False,
|
|
109
|
+
"cargo_form": cargo_form, "n_answers": 0, "answers": [], "grounded": True, "no_fabrication": True,
|
|
110
|
+
"note": (f"'{locus}' is neither a curated safe-harbour locus nor a resolvable gene symbol, no "
|
|
111
|
+
"grounded writer→locus→vehicle path (a clean empty result, not an error).")}
|
|
112
|
+
|
|
113
|
+
# tier-1 = the writers the graph gives locus-level `reaches` edges (near-universal reprogrammable reach).
|
|
114
|
+
tier1 = {e.src for e in g.edges if e.etype == "reaches"}
|
|
115
|
+
answers = []
|
|
116
|
+
for w in g.nodes_of("writer"):
|
|
117
|
+
if w.id not in tier1:
|
|
118
|
+
continue
|
|
119
|
+
if cargo_form is not None and w.props.get("output_form") != cargo_form:
|
|
120
|
+
continue
|
|
121
|
+
deliv = g.out_edges(w.id, "deliverable_by")
|
|
122
|
+
if not deliv:
|
|
123
|
+
continue
|
|
124
|
+
synth = Edge(w.id, f"locus:{resolved_gene}", "reaches", "predicted",
|
|
125
|
+
scope="locus-level reachability (per-site element check is Planner work); NOT a curated "
|
|
126
|
+
"safe-harbour node, tier-1 reprogrammable near-universal reach",
|
|
127
|
+
provenance={"source": "crosslink reachability_tier (tier-1 reprogrammable); locus resolved "
|
|
128
|
+
"to a real gene, not a curated GSH, treat as a candidate, verify per-site"})
|
|
129
|
+
answers.append(_answer(w, [synth], deliv))
|
|
130
|
+
return {"locus": locus, "resolved_locus": resolved_gene, "is_curated_safe_harbour": False,
|
|
131
|
+
"cargo_form": cargo_form, "n_answers": len(answers), "answers": answers,
|
|
132
|
+
"grounded": all(a["provenance_path"] for a in answers), "no_fabrication": True,
|
|
133
|
+
"note": (f"'{resolved_gene}' is not a curated safe harbour; these are the tier-1 reprogrammable writers, "
|
|
134
|
+
"which reach any locus at the locus level (a candidate, not a validated safe-harbour path, "
|
|
135
|
+
"per-site reachability + safety are the Planner / Guardian's job).")}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def outcomes_for_writer(family: str, g: Graph | None = None) -> list[dict]:
|
|
139
|
+
"""Documented (measured) writes performed by a writer family, the outcome edges."""
|
|
140
|
+
g = g or graph()
|
|
141
|
+
wid = f"writer:{family}"
|
|
142
|
+
out = []
|
|
143
|
+
for o in g.nodes_of("outcome"):
|
|
144
|
+
for e in g.out_edges(o.id, "used_writer"):
|
|
145
|
+
if e.dst == wid:
|
|
146
|
+
out.append({"outcome": o.id.split(":", 1)[1], "cargo_bp": o.props["cargo_bp"],
|
|
147
|
+
"evidence": e.evidence, "doi": o.props["doi"]})
|
|
148
|
+
return out
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""The living world-model knowledge graph, schema.
|
|
2
|
+
|
|
3
|
+
The substrate's ground truth (L3) was flat tables joined by code. Promotes it to a queryable
|
|
4
|
+
**knowledge graph**: typed nodes (writer / locus / cargo / vehicle / cell_type / write_type / outcome) joined
|
|
5
|
+
by typed edges (reaches / deliverable_by / performs / durable_in / carries / used_writer / observed_at), where
|
|
6
|
+
**every edge carries its provenance, its uncertainty, and the scope within which it holds** (Principle 2).
|
|
7
|
+
|
|
8
|
+
Edges are typed by evidence kind, `measured` (a documented experimental outcome) > `curated` (a
|
|
9
|
+
hand-verified fact, e.g. a delivery-vehicle property) > `predicted` (a model/homology inference), so an
|
|
10
|
+
agent traversing the graph always knows how much to trust each hop. Nodes/edges are plain dataclasses; the
|
|
11
|
+
store (build/query) is pure-Python + JSON/sqlite (Docker-friendly, no graph-DB dependency).
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import asdict, dataclass, field
|
|
16
|
+
from typing import Any, Literal
|
|
17
|
+
|
|
18
|
+
NodeType = Literal["writer", "locus", "cargo", "vehicle", "cell_type", "write_type", "outcome"]
|
|
19
|
+
EdgeType = Literal["reaches", "deliverable_by", "performs", "durable_in", "carries",
|
|
20
|
+
"used_writer", "observed_at"]
|
|
21
|
+
EvidenceKind = Literal["measured", "curated", "predicted"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Node:
|
|
26
|
+
id: str
|
|
27
|
+
type: NodeType
|
|
28
|
+
props: dict[str, Any] = field(default_factory=dict)
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> dict:
|
|
31
|
+
return asdict(self)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Edge:
|
|
36
|
+
src: str
|
|
37
|
+
dst: str
|
|
38
|
+
etype: EdgeType
|
|
39
|
+
evidence: EvidenceKind # measured / curated / predicted (trust ordering)
|
|
40
|
+
confidence: float | None = None # [0,1] where known; None = not quantified (abstain)
|
|
41
|
+
scope: str | None = None # the context within which the edge holds (limit)
|
|
42
|
+
provenance: dict[str, Any] = field(default_factory=dict) # {source, doi, date, ...}
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> dict:
|
|
45
|
+
return asdict(self)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def key(self) -> tuple:
|
|
49
|
+
return (self.src, self.dst, self.etype)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Graph:
|
|
53
|
+
"""An in-memory typed multigraph with provenance-tagged edges. Pure Python; serialises to JSON."""
|
|
54
|
+
|
|
55
|
+
def __init__(self) -> None:
|
|
56
|
+
self.nodes: dict[str, Node] = {}
|
|
57
|
+
self.edges: list[Edge] = []
|
|
58
|
+
self._adj: dict[str, list[Edge]] = {} # src -> edges (built lazily)
|
|
59
|
+
|
|
60
|
+
# ---- construction --------------------------------------------------------------------------
|
|
61
|
+
def add_node(self, node: Node) -> None:
|
|
62
|
+
self.nodes[node.id] = node
|
|
63
|
+
|
|
64
|
+
def add_edge(self, edge: Edge) -> None:
|
|
65
|
+
if edge.src not in self.nodes or edge.dst not in self.nodes:
|
|
66
|
+
raise KeyError(f"edge {edge.key} references an unknown node")
|
|
67
|
+
self.edges.append(edge)
|
|
68
|
+
self._adj.setdefault(edge.src, []).append(edge)
|
|
69
|
+
|
|
70
|
+
# ---- traversal -----------------------------------------------------------------------------
|
|
71
|
+
def out_edges(self, node_id: str, etype: EdgeType | None = None) -> list[Edge]:
|
|
72
|
+
es = self._adj.get(node_id, [])
|
|
73
|
+
return [e for e in es if etype is None or e.etype == etype]
|
|
74
|
+
|
|
75
|
+
def neighbors(self, node_id: str, etype: EdgeType | None = None) -> list[str]:
|
|
76
|
+
return [e.dst for e in self.out_edges(node_id, etype)]
|
|
77
|
+
|
|
78
|
+
def nodes_of(self, ntype: NodeType) -> list[Node]:
|
|
79
|
+
return [n for n in self.nodes.values() if n.type == ntype]
|
|
80
|
+
|
|
81
|
+
# ---- serialisation -------------------------------------------------------------------------
|
|
82
|
+
def to_dict(self) -> dict:
|
|
83
|
+
return {"nodes": [n.to_dict() for n in self.nodes.values()],
|
|
84
|
+
"edges": [e.to_dict() for e in self.edges]}
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def from_dict(cls, d: dict) -> "Graph":
|
|
88
|
+
g = cls()
|
|
89
|
+
for n in d["nodes"]:
|
|
90
|
+
g.add_node(Node(**n))
|
|
91
|
+
for e in d["edges"]:
|
|
92
|
+
g.add_edge(Edge(**e))
|
|
93
|
+
return g
|
|
94
|
+
|
|
95
|
+
def summary(self) -> dict:
|
|
96
|
+
from collections import Counter
|
|
97
|
+
return {"n_nodes": len(self.nodes), "n_edges": len(self.edges),
|
|
98
|
+
"nodes_by_type": dict(Counter(n.type for n in self.nodes.values())),
|
|
99
|
+
"edges_by_type": dict(Counter(e.etype for e in self.edges)),
|
|
100
|
+
"edges_by_evidence": dict(Counter(e.evidence for e in self.edges))}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""pen_stack.loop, the closed loop, autonomy Level 3.
|
|
2
|
+
|
|
3
|
+
PEN-STACK generates safe legal designs, predicts their outcomes, chooses the most informative experiments,
|
|
4
|
+
exports gated protocols, runs them in the simulated lab or a real one, ingests results through the world-model's
|
|
5
|
+
gate, and updates its calibration, twin, and immune labels round after round, detecting when observations drift
|
|
6
|
+
from predictions and widening uncertainty rather than over-trusting a stale model. It keeps a human in control at
|
|
7
|
+
every gate, never fabricates a number, and stops deliberately at autonomy Level 3.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pen_stack.loop.continual import continual_update
|
|
12
|
+
from pen_stack.loop.cycle import AUTONOMY_LEVEL, loop_converges_faster_than_random, run_loop
|
|
13
|
+
from pen_stack.loop.drift import detect_drift
|
|
14
|
+
|
|
15
|
+
__all__ = ["run_loop", "continual_update", "detect_drift", "loop_converges_faster_than_random", "AUTONOMY_LEVEL"]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Gated, versioned, reversible continual learning, the closed loop.
|
|
2
|
+
|
|
3
|
+
Recalibrates the trust layer + the twin + the immune proxies on ADMITTED outcomes only, never on
|
|
4
|
+
un-gated evidence. Every update is versioned and reversible (carries a `rollback_to` pointer), attributable to
|
|
5
|
+
the admitted evidence and the approver. High drift widens intervals rather than over-trusting a shifting model.
|
|
6
|
+
This is recalibration, NOT full retraining of the foundation models.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
from statistics import mean
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _evidence_digest(admitted_results: list) -> str:
|
|
16
|
+
payloads = [getattr(r, "payload", r) for r in admitted_results]
|
|
17
|
+
return hashlib.sha256(json.dumps(payloads, sort_keys=True, default=str).encode()).hexdigest()[:12]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _readouts(admitted_results: list) -> list[float]:
|
|
21
|
+
out = []
|
|
22
|
+
for r in admitted_results:
|
|
23
|
+
payload = getattr(r, "payload", r)
|
|
24
|
+
ro = payload.get("readout") if isinstance(payload, dict) else None
|
|
25
|
+
if ro is not None:
|
|
26
|
+
out.append(float(ro))
|
|
27
|
+
return out
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _update_immune_labels(admitted_results: list) -> dict:
|
|
31
|
+
"""Immune-measurement results can move an axis proxy -> outcome-validated (recorded). Requires an
|
|
32
|
+
admitted measurement that names an immune axis + a CI (the calibration rule); else no graduation."""
|
|
33
|
+
graduated = []
|
|
34
|
+
for r in admitted_results:
|
|
35
|
+
payload = getattr(r, "payload", r)
|
|
36
|
+
if isinstance(payload, dict) and payload.get("immune_axis_measured") and payload.get("ci"):
|
|
37
|
+
graduated.append(payload["immune_axis_measured"])
|
|
38
|
+
return {"graduated_to_validated": graduated,
|
|
39
|
+
"note": "graduation requires an admitted immune measurement with a CI (the immune-calibration gate)"}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def continual_update(admitted_results: list, *, drift: dict | None = None, approver: str,
|
|
43
|
+
prev_version: str | None = None) -> dict:
|
|
44
|
+
"""Recalibrate trust + twin + immune proxies on ADMITTED outcomes only. Versioned + reversible. High drift
|
|
45
|
+
widens intervals rather than over-trusting a shifting model."""
|
|
46
|
+
if not admitted_results:
|
|
47
|
+
return {"updated": False, "reason": "no admitted evidence", "rollback_to": prev_version}
|
|
48
|
+
readouts = _readouts(admitted_results)
|
|
49
|
+
calibration = {"mean_readout": round(mean(readouts), 4) if readouts else None, "n": len(readouts)}
|
|
50
|
+
interval_inflation = 1.0
|
|
51
|
+
if drift and drift.get("severity") == "high":
|
|
52
|
+
interval_inflation = 1.0 + min(1.0, drift.get("ece", 0.0)) # widen, don't over-trust
|
|
53
|
+
imm_lbls = _update_immune_labels(admitted_results)
|
|
54
|
+
version = f"v{_evidence_digest(admitted_results)}"
|
|
55
|
+
return {
|
|
56
|
+
"updated": True, "version": version, "by": approver,
|
|
57
|
+
"n_evidence": len(admitted_results), "calibration": calibration,
|
|
58
|
+
"interval_inflation": round(interval_inflation, 4), "immune_labels": imm_lbls,
|
|
59
|
+
"rollback_to": prev_version, # reversible: revert to the prior belief version
|
|
60
|
+
"note": "recalibration only (not foundation-model retraining); admitted-evidence-only; versioned+reversible",
|
|
61
|
+
}
|
pen_stack/loop/cycle.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""The DBTL orchestrator, the closed loop.
|
|
2
|
+
|
|
3
|
+
One continual design→build→test→learn cycle, integrating every prior cycle:
|
|
4
|
+
generate → decide/batch → safety-gated export → run (sim-lab / real lab) →
|
|
5
|
+
ingest (gate) → drift → continual learn.
|
|
6
|
+
**Autonomy Level 3**: closed, but with humans/lab IN CONTROL at every gate (safety, build, belief-admission),
|
|
7
|
+
not autonomous. Numbers come only from tools; no stage fabricates a value.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
AUTONOMY_LEVEL = 3
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _converged(history: list[dict]) -> bool:
|
|
17
|
+
"""Converge when the best observed readout stops improving meaningfully over the last two rounds."""
|
|
18
|
+
bests = [h.get("best_readout") for h in history if h.get("best_readout") is not None]
|
|
19
|
+
return len(bests) >= 2 and abs(bests[-1] - bests[-2]) < 0.01
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _best_readout(results: list) -> float | None:
|
|
23
|
+
vals = []
|
|
24
|
+
for r in results:
|
|
25
|
+
payload = getattr(r, "payload", r)
|
|
26
|
+
ro = payload.get("readout") if isinstance(payload, dict) else None
|
|
27
|
+
if ro is not None:
|
|
28
|
+
vals.append(float(ro))
|
|
29
|
+
return max(vals) if vals else None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run_loop(goal: dict, cell_state: str, *, candidates: list[dict] | None = None, rounds: int = 5,
|
|
33
|
+
use_lab: bool = False, approver: str = "human", seed: int = 0) -> dict[str, Any]:
|
|
34
|
+
"""One Level-3 DBTL campaign. Gated: safety + build + belief-admission each await `approver`. Optimises
|
|
35
|
+
efficacy AND immune-risk; numbers come only from tools; no stage fabricates. Pass an explicit `candidates`
|
|
36
|
+
pool to run atlas-independently (sim-lab is the default; a real lab attaches at the same interface)."""
|
|
37
|
+
from pen_stack.active.design import select_batch
|
|
38
|
+
from pen_stack.build.ingest import ingest_result
|
|
39
|
+
from pen_stack.build.protocol import ProtocolExportError, _to_protocol_ir, export_protocol
|
|
40
|
+
from pen_stack.build.simlab import run_simulated
|
|
41
|
+
from pen_stack.design.generate import generate_designs
|
|
42
|
+
from pen_stack.loop.continual import continual_update
|
|
43
|
+
from pen_stack.loop.drift import detect_drift
|
|
44
|
+
|
|
45
|
+
history: list[dict] = []
|
|
46
|
+
prev_version: str | None = None
|
|
47
|
+
for r in range(rounds):
|
|
48
|
+
cands = generate_designs(goal, candidates=candidates, keep=8, actor=approver) # (safe+legal+calibrated)
|
|
49
|
+
if not cands:
|
|
50
|
+
history.append({"round": r, "n": 0, "note": "no surviving candidates (atlas absent or all discarded)"})
|
|
51
|
+
break
|
|
52
|
+
batch = select_batch(cands, cell_state, {"round": r}, k=4) # (info + immune-VOI)
|
|
53
|
+
admitted, blocked = [], 0
|
|
54
|
+
for d in batch:
|
|
55
|
+
try:
|
|
56
|
+
export_protocol(d, {"round": r}, actor=approver) # safety-gate (may block)
|
|
57
|
+
except ProtocolExportError:
|
|
58
|
+
blocked += 1
|
|
59
|
+
continue
|
|
60
|
+
ir = _to_protocol_ir(d, {"round": r})
|
|
61
|
+
res = (_run_real(ir, d) if use_lab else run_simulated(ir, d, cell_state, seed=seed + r))
|
|
62
|
+
admitted.append(ingest_result(res, admitted_by=approver)) # gate (human approves)
|
|
63
|
+
drift = detect_drift(batch, admitted, cell_state=cell_state)
|
|
64
|
+
update = continual_update(admitted, drift=drift, approver=approver, prev_version=prev_version)
|
|
65
|
+
prev_version = update.get("version", prev_version)
|
|
66
|
+
history.append({"round": r, "n": len(admitted), "blocked": blocked, "best_readout": _best_readout(admitted),
|
|
67
|
+
"drift": drift, "update": update})
|
|
68
|
+
if _converged(history):
|
|
69
|
+
break
|
|
70
|
+
return {"goal": goal, "history": history, "final_model_version": prev_version,
|
|
71
|
+
"autonomy_level": AUTONOMY_LEVEL, "human_in_control": True, "no_fabrication": True}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _run_real(protocol_ir: dict, design: dict): # pragma: no cover - a real lab attaches here
|
|
75
|
+
raise NotImplementedError("attach a real lab at this interface (same shape as the sim-lab)")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def loop_converges_faster_than_random(reps: int = 15, rounds: int = 6) -> dict:
|
|
79
|
+
"""The loop's active Learn stage reaches a target model quality in FEWER rounds than random,
|
|
80
|
+
proven retrospectively with reps + a bootstrap CI (the convergence headline; reuses the prior validation)."""
|
|
81
|
+
from pen_stack.active.validate import retrospective_active_learning
|
|
82
|
+
r = retrospective_active_learning(reps=reps, rounds=rounds)
|
|
83
|
+
return {"reaches_optimum_faster_than_random": r["active_beats_random"],
|
|
84
|
+
"active_vs_random_ci": r["active_vs_random"]["ci"], "honest_note": r["honest_note"]}
|
pen_stack/loop/drift.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Drift detection, the closed loop.
|
|
2
|
+
|
|
3
|
+
Compares what the twin PREDICTED against what the experiments OBSERVED. Growing miscalibration => drift => widen
|
|
4
|
+
uncertainty and flag, rather than over-trusting a stale model. Covers calibration/residual shift, not every
|
|
5
|
+
failure mode.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _predicted(design: dict, cell_state: str) -> float | None:
|
|
11
|
+
from pen_stack.twin.outcome import predict_outcome
|
|
12
|
+
v = predict_outcome(design, design.get("cell_state") or cell_state or "k562")
|
|
13
|
+
return v["predicted_outcome"].get("relative_expression")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _empirical_calibration_error(designs: list, results: list, cell_state: str = "") -> tuple[float, int]:
|
|
17
|
+
errs = []
|
|
18
|
+
for d, r in zip(designs, results):
|
|
19
|
+
pred = _predicted(d, cell_state)
|
|
20
|
+
obs = (r.get("payload", {}) if hasattr(r, "get") else {}).get("readout") if not isinstance(r, dict) else r.get("readout")
|
|
21
|
+
# results may be raw dicts (sim/ingest) or Candidates; read the readout from either
|
|
22
|
+
if obs is None and hasattr(r, "payload"):
|
|
23
|
+
obs = r.payload.get("readout")
|
|
24
|
+
if pred is not None and obs is not None:
|
|
25
|
+
errs.append(abs(float(pred) - float(obs)))
|
|
26
|
+
if not errs:
|
|
27
|
+
return 0.0, 0
|
|
28
|
+
return sum(errs) / len(errs), len(errs)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _drift_threshold() -> float:
|
|
32
|
+
return 0.20 # mean abs predicted-vs-observed error above which we flag drift
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def detect_drift(designs: list, results: list, *, cell_state: str = "") -> dict:
|
|
36
|
+
"""Predicted (twin) vs observed (results). Growing miscalibration => drift => widen + flag."""
|
|
37
|
+
ece_now, n = _empirical_calibration_error(designs, results, cell_state)
|
|
38
|
+
sev = "high" if ece_now > _drift_threshold() else "low"
|
|
39
|
+
return {"severity": sev, "ece": round(ece_now, 4), "n": n,
|
|
40
|
+
"action": "inflate_intervals" if sev == "high" else "monitor",
|
|
41
|
+
"note": "calibration/residual drift only, not every failure mode"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""pen_stack.mech - see the PEN-STACK program doc."""
|