Spaces:
Sleeping
Sleeping
File size: 11,252 Bytes
0fff343 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 | """Multi-seed HNSC/HPV stability report.
Runs the same HNSC/HPV genetic-programming pipeline the Lab launches
across a small grid of seeds, then reports (a) per-seed held-out AUROC
+ permutation p + GSE65858 transfer AUROC + p + gene coverage, (b) a
gene-recurrence tally across the winners with each symbol tagged by
its HNSC reference set (p16 / cell_cycle), and (c) held-out and
transfer AUROC ranges across seeds.
Read-only orchestration; no engine / API / airgap change. `scripts/`
is biology-aware β the same layer that owns `run_h2.py` β so this
file is allowed to import `airgap.reveal` and the named-side
validation helpers.
Airgap discipline
-----------------
Only each seed's WINNER's own opaque IDs are ever revealed (via the
bounded ``airgap.reveal`` β the same call ``/evaluate`` uses). The
sealed map is never dumped and the GSE65858 gene list never crosses
back into the engine. GSE65858 lives on the named (reveal) side; the
engine only sees TCGA HNSC via the anonymised matrix.
Usage
-----
python -m scripts.multiseed_hpv
python -m scripts.multiseed_hpv --seeds 1 3 7
python -m scripts.multiseed_hpv --generations 30 --population 300
"""
from __future__ import annotations
import argparse
import sys
import time
from typing import Sequence
from airgap import reveal
from api.app import (
COHERENCE_DEFAULT_WEIGHT,
REFERENCE_SETS_BY_DATASET,
_prepare_lab_data,
)
from engine_v2 import run_v2_pipeline
from engine_v2.fitness import objective_from_spec as v2_objective_from_spec
from validate.transfer_gse65858 import transfer_score
DEFAULT_SEEDS = [1, 3, 7, 11, 13, 17, 23, 29]
# Same numbers the Lab writes into RunParamsModel via DEFAULT_PARAMS.
DEFAULT_GENERATIONS = 60
DEFAULT_POPULATION = 300
DEFAULT_GENES_PER_SET = 8
DEFAULT_LAMBDA = 0.005
DEFAULT_PERMUTATIONS = 200
DEFAULT_PREFILTER_N: int | None = None # Match the Lab's "off" default.
def _tag_reference(sym: str) -> str:
"""Return the HNSC reference-set name that contains ``sym``, or ``-``.
``p16`` wins over ``cell_cycle`` if a symbol appears in both.
"""
hnsc = REFERENCE_SETS_BY_DATASET.get("hnsc", {})
for name in ("p16", "cell_cycle"):
if sym in hnsc.get(name, ()):
return name
return "-"
def _fmt(v: float | None, digits: int = 3) -> str:
if v is None:
return "β"
try:
f = float(v)
except (TypeError, ValueError):
return "β"
return f"{f:.{digits}f}" if f == f and f not in (float("inf"), float("-inf")) else "β"
def _run_one(
M, y, clinical, extra_labels,
*,
seed: int,
generations: int,
population: int,
genes_per_set: int,
lambda_size: float,
permutations: int,
prefilter_n: int | None,
coherence_weight: float,
confounders: tuple[str, ...],
transfer_perms: int,
) -> dict:
"""Run the pipeline for one seed and score the winner on GSE65858.
Returns a summary dict."""
objective_v2 = v2_objective_from_spec({"target": "hpv", "metric": "auroc"})
t0 = time.time()
_log, result = run_v2_pipeline(
M, y,
objective=objective_v2,
seed=seed,
prefilter_n=prefilter_n,
population_size=population,
n_generations=generations,
n_permutations=permutations,
lambda_size=lambda_size,
max_genes_per_set=genes_per_set,
clinical=clinical,
extra_labels=extra_labels,
coherence_weight=coherence_weight,
confounders=confounders,
)
elapsed = time.time() - t0
winning = result.get("winning", {}) or {}
gene_ids: list[str] = list(winning.get("gene_ids") or [])
# Bounded reveal β same discipline as /evaluate. Only this seed's
# winner's opaque IDs are ever translated; the sealed map is
# never dumped.
symbols: list[str] = reveal(gene_ids) if gene_ids else []
symbols = [s for s in symbols if isinstance(s, str) and s]
if symbols:
try:
transfer = transfer_score(
symbols, n_permutations=transfer_perms, seed=seed,
)
except FileNotFoundError:
transfer = None
else:
transfer = None
return {
"seed": seed,
"elapsed_s": round(elapsed, 1),
"holdout": winning.get("holdout_score"),
"permutation_p": winning.get("permutation_p"),
"gene_ids": gene_ids,
"symbols": symbols,
"transfer": transfer,
}
def main(argv: Sequence[str] | None = None) -> int:
ap = argparse.ArgumentParser(
description=(
"Run HNSC/HPV across several seeds and report per-seed stability "
"+ gene recurrence. Read-only; no engine / API / airgap change."
),
)
ap.add_argument(
"--seeds", type=int, nargs="+", default=None,
help=f"Seeds to run (default {DEFAULT_SEEDS}).",
)
ap.add_argument("--generations", type=int, default=DEFAULT_GENERATIONS)
ap.add_argument("--population", type=int, default=DEFAULT_POPULATION)
ap.add_argument("--genes-per-set", type=int, default=DEFAULT_GENES_PER_SET)
ap.add_argument("--lambda-size", type=float, default=DEFAULT_LAMBDA)
ap.add_argument("--permutations", type=int, default=DEFAULT_PERMUTATIONS)
ap.add_argument(
"--prefilter-n", type=int, default=DEFAULT_PREFILTER_N,
help=(
"Prefilter top-N (default: off, matches the Lab). Pass an int to "
"narrow the initial pool."
),
)
ap.add_argument(
"--transfer-perms", type=int, default=500,
help="Permutation-null size for the GSE65858 transfer AUROC.",
)
ap.add_argument(
"--no-coherence", action="store_true",
help="Skip the coherence prior (default: on, matches the Lab).",
)
args = ap.parse_args(argv or sys.argv[1:])
seeds = list(args.seeds) if args.seeds else list(DEFAULT_SEEDS)
coherence_weight = 0.0 if args.no_coherence else COHERENCE_DEFAULT_WEIGHT
print("=" * 78)
print("Multi-seed HNSC/HPV stability report")
print("=" * 78)
print(f"Seeds : {seeds}")
print(f"Generations Γ pop : {args.generations} Γ {args.population}")
print(f"Prefilter top-N : {args.prefilter_n if args.prefilter_n else 'off (all genes)'}")
print(f"Coherence prior : {'ON' if coherence_weight > 0 else 'OFF'} (weight {coherence_weight})")
print(f"Permutations (GP) : {args.permutations}")
print(f"Permutations (xfer): {args.transfer_perms}")
print(
"Airgap : only each winner's own genes are revealed "
"(bounded reveal via airgap.reveal β same discipline as /evaluate)."
)
print(
" The sealed map is never dumped; the GSE65858 "
"gene list never crosses back into the engine."
)
print("=" * 78)
# Load HNSC HPV cohort ONCE and reuse across seeds β the anonymised
# M / y / clinical / extra_labels are seed-independent.
print("\nLoading HNSC HPV cohort (anonymised matrix + clinical) β¦")
M, y, clinical, extra_labels = _prepare_lab_data("hpv", "hnsc")
print(f" M: {M.shape[0]} patients Γ {M.shape[1]} opaque columns")
print(f" y: HPV+ {int((y == 1).sum())} Β· HPVβ {int((y == 0).sum())}")
if clinical is not None:
cols = list(clinical.columns)
print(f" clinical columns: {cols}")
# Match the API worker's HNSC confounder set.
extras = [c for c in ("sex", "race") if c in cols]
confounders = ("stage", "age", *extras)
else:
confounders = ("stage", "age")
print(f" Effect confounders: {confounders}")
# ----- Per-seed runs -----------------------------------------------------
rows: list[dict] = []
for i, seed in enumerate(seeds, start=1):
print(f"\n[{i}/{len(seeds)}] seed={seed} β running β¦", flush=True)
row = _run_one(
M, y, clinical, extra_labels,
seed=seed,
generations=args.generations,
population=args.population,
genes_per_set=args.genes_per_set,
lambda_size=args.lambda_size,
permutations=args.permutations,
prefilter_n=args.prefilter_n,
coherence_weight=coherence_weight,
confounders=confounders,
transfer_perms=args.transfer_perms,
)
symbols_short = ", ".join(row["symbols"][:6])
if len(row["symbols"]) > 6:
symbols_short += f", β¦ (+{len(row['symbols']) - 6})"
print(
f" held-out AUROC={_fmt(row['holdout'])} "
f"p={_fmt(row['permutation_p'])} "
f"elapsed={row['elapsed_s']:.1f}s "
f"winner=[{symbols_short}]"
)
rows.append(row)
# ----- Per-seed table ----------------------------------------------------
print("\n" + "-" * 78)
print("PER-SEED SUMMARY")
print("-" * 78)
header = (
f"{'seed':>5} {'held-out':>9} {'GP p':>6} "
f"{'xfer AUROC':>10} {'xfer p':>7} {'genes found':>12}"
)
print(header)
print("-" * len(header))
for r in rows:
t = r["transfer"] or {}
found = int(t.get("n_found", 0)) if t else 0
total_req = (
int(t.get("n_found", 0)) + int(t.get("n_missing", 0)) if t else 0
)
if total_req == 0:
total_req = len(r["symbols"])
print(
f"{r['seed']:>5} {_fmt(r['holdout']):>9} "
f"{_fmt(r['permutation_p']):>6} "
f"{_fmt(t.get('auroc') if t else None):>10} "
f"{_fmt(t.get('p') if t else None):>7} "
f"{found:>4} / {total_req:<5}"
)
# ----- Range summary -----------------------------------------------------
def _range(vs: list[float | None]) -> str:
ok = [v for v in vs if v is not None]
if not ok:
return "β"
return f"{min(ok):.3f}β{max(ok):.3f}"
holdouts = [r["holdout"] for r in rows]
xfer_aurocs = [
(r["transfer"] or {}).get("auroc") if r["transfer"] else None
for r in rows
]
print("\n" + "-" * 78)
print("RANGES ACROSS SEEDS")
print("-" * 78)
print(f" held-out AUROC : {_range(holdouts)}")
print(f" transfer AUROC : {_range(xfer_aurocs)}")
# ----- Gene recurrence tally --------------------------------------------
tally: dict[str, int] = {}
for r in rows:
for sym in dict.fromkeys(r["symbols"]): # dedup within a winner
tally[sym] = tally.get(sym, 0) + 1
print("\n" + "-" * 78)
print("GENE RECURRENCE ACROSS WINNERS")
print(f" ({len(seeds)} seeds; symbols listed by seeds they appear in, desc)")
print("-" * 78)
if not tally:
print(" (no winners had revealed symbols β nothing to tally)")
else:
header = f" {'symbol':<14} {'seeds':>6} {'reference':<11}"
print(header)
for sym, count in sorted(
tally.items(),
key=lambda kv: (-kv[1], kv[0]),
):
tag = _tag_reference(sym)
print(f" {sym:<14} {count:>4} / {len(seeds):<3} {tag:<11}")
print("\n" + "=" * 78)
print("Done.")
return 0
if __name__ == "__main__":
sys.exit(main())
|