"use client"; import { Fragment, useEffect, useId, useMemo, useRef, useState } from "react"; import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; import { API_URL, DatasetId, EvaluateResponse, FullRankDiagnostic, GeneRankRow, GenerationEvent, HPVRankDiagnostic, ObjectiveSpec, ModuleRanking, RankedModule, ReferenceMark, RunParams, RunResult, TMBRankDiagnostic, Target, getFullRankDiagnostic, getHPVRankDiagnostic, getRunModules, getRunOperatorUsage, getRunTransfer, TransferResult, OperatorUsage, getTMBRankDiagnostic, postEvaluate, postReveal, postRun, } from "../lib/api"; import InfoTip from "./InfoTip"; import ProgramGraph from "./ProgramGraph"; import PopulationTiles from "./PopulationTiles"; import PasteToDraw from "./PasteToDraw"; import ParameterFlow from "./ParameterFlow"; import MMRBiologyPanel from "./MMRBiologyPanel"; import HPVBiologyPanel from "./HPVBiologyPanel"; import ParamHelp from "./ParamHelp"; import ParamHelpProvider from "./ParamHelpProvider"; import CopyButton from "./CopyButton"; import { REFERENCE_SETS_BY_DATASET, ReferenceSetKey, refSetsFor, } from "../lib/referenceSets"; import { fmtFit } from "../lib/fmt"; import { ParamKey } from "./paramHelpContent"; // ---------- Tooltip copy (verbatim, never edited inline) ------------------- // Objective tooltips moved to paramHelpContent.tsx (obj_msi / obj_tmb / // obj_survival / obj_unsupervised) so the SHORT + DETAILED + diagrams // share the single ParamHelp infrastructure. const PARAM_TIPS: Record = { generations: "How many rounds of evolution the engine runs. Each round it " + "keeps the best programs and breeds variations. More rounds refine " + "further, but take longer.", population: "How many programs the engine tries in each round. More programs " + "explore more options, but the round takes longer.", genes_per_set: "The most genes a single score is allowed to use. Smaller keeps " + "programs simple and easy to read.", max_sets: "How many separate gene-scores a program is allowed to combine (1 " + "or 2). Two lets it build a small “score made of scores.”", lambda: "How much the engine is penalised for using more genes. Higher pushes " + "it toward fewer genes (simpler answers).", seed: "The starting point for the engine’s randomness. The same seed " + "reproduces the exact same run; change it to see a different one.", prefilter_n: "By default the engine searches all ~20,000 genes, so nothing is " + "pre-excluded. Turning this on narrows to the N most promising genes " + "first — faster, but it can drop a real gene that only helps in " + "combination.", permutations: "How many times we re-run the search on deliberately scrambled " + "labels — a chance baseline. The real result must beat these random " + "runs (that’s the permutation p).", }; const TIPS = { run: "Starts the search with the settings above.", runId: "Just a label for this run, so its results can be fetched.", status: "Where the run is: queued → running → done.", // NB: fitness-curve InfoTip is now objective-aware — see // FITNESS_TIP_BY_TARGET below. This single string was only correct // for TMB and was misleading on MSI / unsup runs. nodes: "How many steps the program has.", genes: "How many genes the program uses.", bestVsMedian: "Each round, the engine keeps the best programs and breeds " + "variations. The solid line is the best program so far; the dashed " + "line is the typical one. When they meet and flatten, the " + "population has converged — it’s stopped finding anything " + "new.", populationGrid: "Every program in this round. Darker = higher score; outlined = " + "kept for the next round.", survivedVsDiscarded: "Solid cards survived to breed the next generation; faded cards were " + "culled for scoring worse.", opaqueIds: "Anonymous stand-in labels for real genes. The engine works blind " + "and never sees gene names until we reveal them at the end — so it " + "can’t lean on what’s already known about them.", winningProgram: "The best program found, showing which (still-anonymous) genes it " + "uses and how it combines them.", // Held-out and permutation-p have target-aware variants (see the // BY_TARGET records below the TIPS block). These plain strings stay // for any legacy call site. heldOut: "The winner's score on patients the engine never saw during the " + "search (the held-out set). It tests that the result isn't just " + "memorising the training data.", permutationP: "How often pure chance matches this score across many random " + "comparisons. Small (e.g. below 0.05) = unlikely to be a fluke.", baseline: "A simple, no-frills method's score, for comparison — the evolved " + "program should beat it.", // Reference-set tip has a dataset-aware variant (see // REFERENCE_SET_TIP_BY_DATASET). This is the colorectal default. referenceSet: "A known gene list to check the answer against. MMR = the 4 DNA " + "repair / 'spell-checker' genes (MLH1, MSH2, MSH6, PMS2); " + "immune = immune-attack genes (CD8A, GZMA, PRF1).", revealEvaluateButton: "Unlocks the real gene names for the winning program and checks how " + "many match the chosen known list.", revealedGenes: "The real names behind the program's gene IDs — shown only now, " + "after the blind search is done.", overlap: "How many of the engine's genes appear in the known list (matches " + "highlighted). More overlap = it rediscovered known biology on its " + "own.", programGraph: "The winning program drawn as a graph: boxes are operators (verbs), " + "pills are data and scores. Tier-1 turns genes into a score; Tier-2 " + "combines scores into the prediction.", dslVocabulary: "Rate = how often the engine reaches for an operator when it " + "builds or mutates a program — roughly the chance, at each " + "eligible spot, that it picks that operator instead of a simpler " + "one. 0.1 ≈ 1 in 10; 0.05 ≈ 1 in 20; 0 = never. Higher means the " + "operator is tried in more programs (more exploration) — it does " + "NOT mean the operator is better; selection still decides whether " + "it survives into the winners. (Effect's rate is its share of the " + "correlation-style programs versus Associate.)", // Discovered-axes + post-hoc tips have dataset-aware variants below. // The plain strings below are the colorectal defaults. discoveredAxes: "After each unsupervised run, click \"Find next axis\" to peel the " + "discovered split out of the data (linear residualisation — we " + "subtract its signal from every gene) and search again. The chain " + "is a ranked, blind list of independent directions in the gene-" + "expression matrix — each named only afterwards by how well it " + "lines up with a known label. The residualisation is LINEAR, so " + "deeper non-linear structure may remain. The chain is in-memory " + "only — it resets on server restart or a fresh Run.", posthoc: "After the unsupervised search finishes, the discovered split is " + "compared to the named labels the engine never saw. AUROC = how " + "well the score sorts the two groups apart (0.5 = no relationship, " + "1.0 = a perfect match).", tmbRank: "Rank every gene by signed Spearman with TMB on the engine's TMB " + "cohort, ascending (most-negative = rank 1, since the TMB objective " + "rewards the most-negative association). Where each known gene " + "lands tells you whether the signal is even there: MMR genes near " + "rank 1 = findable from expression alone; high rank = the signal " + "isn't strongly present in this matrix.", rankingResult: "Where each KNOWN marker ranks on its own. Every gene in the " + "matrix is scored as a single-gene separator of the label on the " + "engine's TRAIN split (~20k opaque columns), sorted strongest " + "first; this panel pins the dataset's reference markers (p16 / " + "cell_cycle for HNSC; MMR / immune for CRC) so you can see how " + "recoverable the canonical biology is — a recovery diagnostic. " + "The engine's actual picks (the genetic-programming top programs, their genes, " + "their group scores, their confound survival) live in the Result " + "panel above; the after-the-fact re-scoring of every explored " + "group is the Coordinated modules panel below.", rankingHighlighted: "Rows pinned here are the dataset's KNOWN markers, revealed by " + "name. p16 / cell_cycle (HNSC) or MMR / immune (CRC). Each row is " + "one gene scored alone — a high rank (low %, near 1) = a strong " + "standalone marker; a low rank (high %, far down the list) = a " + "gene that only helps in combination with others. The winning " + "program's constituent genes live in the Result panel above as " + "part of its self-contained group view; they are deliberately " + "NOT pinned in this known-marker recovery diagnostic.", coherence: "Off: the engine rewards whatever genes separate the label best, " + "even a single lucky gene. On: it also rewards genes that move " + "together across patients — a real co-expressed program — so it " + "tends to surface coordinated modules (e.g. a cell-cycle program) " + "rather than one sharp correlate. It names no pathway; it just " + "prefers coordinated signals.", diversity: "Off: the population can collapse to near-clones early (the " + "fitness curve's best and median lines meet and go flat). On: " + "lowers selection pressure and injects fresh random programs " + "each generation, so the population keeps exploring — watch the " + "best-vs-median gap stay open longer. Held-out detection should " + "be preserved or improved, not hurt.", dslRates: "How often the engine reaches for each OPTIONAL operator while " + "growing a program. Each rate is in [0, 1]; setting one to 0 " + "removes that operator from the search. " + "Search is the heaviest operator (every Search node runs an " + "inner gene-ranking) — set its rate to 0 for faster runs. " + "Scalar share = the fraction of programs the engine seeds with a " + "Scalar root (Associate / Effect) versus a Vector root.", moduleRanking: "Why these columns exist: once the engine could detect the trait, " + "the real question was whether the signal came from a few genes " + "working together or from one strong gene, and single-gene " + "rankings can't tell you that. These measures judge whole groups " + "instead. Coherence tells a genuine co-expressed module from a " + "random bag of genes. Combined AUROC scores the group as a group, " + "on patients it never saw. Synergy separates real teamwork from " + "one good gene with passengers along for the ride. " + "Genetic-programming fitness sits beside them as the engine's own " + "verdict, so you can compare what the engine actually preferred " + "against these after-the-fact re-scores. Together they answer one " + "question: is the result carried by gene teamwork or by lone " + "strong genes? Note that the re-scores are computed after the " + "run, so they can rank groups differently from the engine's own " + "picks, and the very top re-score values are optimistically " + "biased because they are the best of thousands.", moduleSurvival: "What “Survives” checks: whether a group still separates HPV when " + "you take away a possible confounder — something that travels with " + "HPV but isn't HPV biology.\n\n" + "Site. HPV+ tumours are mostly in the oropharynx (back of the " + "throat), so a gene could look like an \"HPV gene\" just by " + "marking that location. The site check re-scores the group using " + "only oropharynx patients (everyone the same location). The two " + "numbers are full-cohort → oropharynx-only.\n\n" + "Purity. A tumour sample is a mix of cancer cells and immune " + "cells; HPV+ tumours carry more immune cells, so a gene could " + "look like an \"HPV gene\" just by marking immune content. The " + "purity check re-scores using only the \"purest\" (least-immune) " + "tumours. It's usually \"—\" here because those tumours include " + "almost no HPV+ patients, so there's nothing to test.\n\n" + "✓ = held up when the confounder was held constant (likely real " + "signal). ✗ = dropped past the tolerance (part of it was the " + "confounder). — = couldn't test (too few patients).", singleGeneRank: "Each gene's rank as a SINGLE-gene separator of the active target " + "(out of ~20,000). Low rank (near 1) = the gene separates the label " + "on its own — a real alternate marker. High rank = it only helps in " + "combination with others. AUROC for binary targets (MSI/HPV), signed " + "Spearman (ρ) for TMB.", hpvRank: "Rank every gene by single-gene HPV+/HPV− AUROC on the engine's " + "HNSC training split, descending (best separator = rank 1; " + "orientation-agnostic, so a gene that goes UP or DOWN in HPV+ " + "scores equally). Where each known HPV marker lands tells you " + "whether the signal is even there: CDKN2A / cell-cycle genes near " + "rank 1 = recoverable as single features; high rank = out-competed " + "by other separators (the engine may still combine them).", }; // ---------- Top-level page ------------------------------------------------- type Status = "idle" | "running" | "done" | "error"; // Two profiles: prefilter OFF is the default (integrity over speed) so // we bump the GP budget; prefilter ON keeps the snappier 30/150 defaults. const DEFAULTS_NO_PREFILTER: RunParams = { generations: 60, population: 300, genes_per_set: 8, max_sets: 2, lambda: 0.005, seed: 42, prefilter_n: null, permutations: 200, }; const DEFAULTS_WITH_PREFILTER: RunParams = { generations: 30, population: 150, genes_per_set: 8, max_sets: 2, lambda: 0.005, seed: 42, prefilter_n: 2000, permutations: 200, }; const DEFAULT_PARAMS: RunParams = { generations: 60, population: 300, genes_per_set: 8, max_sets: 2, lambda: 0.005, seed: 42, prefilter_n: null, permutations: 200, }; // Engine-side defaults for the editable DSL injection rates and the // scalar-share field. Mirror engine_v2/synthesize.py:DEFAULT_RATES + // ramped_population's default scalar_share=0.20. Mandatory operators // (Select / Reduce / Combine; Associate is the leftover of the // Scalar branch) get no rate — they're structural. type DslRates = { split: number; effect: number; fitapply: number; search: number; scalar_share: number; }; const DSL_DEFAULT_RATES: DslRates = { split: 0.10, effect: 0.40, fitapply: 0.10, search: 0.05, scalar_share: 0.20, }; const OBJECTIVE_PRESETS: Record = { msi: { target: "msi", metric: "auroc" }, tmb: { target: "tmb", metric: "correlation", direction: "neg" }, none: { target: "none", metric: "structure" }, hpv: { target: "hpv", metric: "auroc" }, }; // Short y-axis titles, calibrated to fit the rotated centred label // inside the chart without overflowing. The actual fitness numbers are // signed (TMB) / [0,1] (MSI) / [-1,1] (unsup) — see engine_v2's // V2Objective.fitness_label() for the verbose form used in logs. const FITNESS_LABEL_BY_TARGET: Record = { msi: "separation (AUROC — 0.5 coin-flip, 1.0 perfect)", tmb: "neg. association with TMB", none: "cluster separation (0–1)", hpv: "separation (AUROC — 0.5 coin-flip, 1.0 perfect)", }; // Per-target "this is a real result" thresholds. The ResultVerdict at // the top of the Result panel turns green only when the winner's // held-out clears its target's bar AND the permutation p is < 0.05. // Defaults are tunable; bump if real-world runs ask. const HOLDOUT_THRESHOLD: Record = { msi: 0.75, tmb: 0.30, none: 0.30, hpv: 0.75, }; const P_VALUE_SIGNIFICANT = 0.05; const POSTHOC_MSI_ALIGN = 0.75; const POSTHOC_HPV_ALIGN = 0.75; // ---------- Dataset registry -------------------------------------------- // One registry entry per supported cancer / study. The engine, DSL, // parameters, live view, population, program graph, and verdict are // IDENTICAL across datasets — only the loader (in api/_prepare_lab_data) // and the per-dataset config below swap. interface ObjectiveCard { key: Target; label: string; sub: string; } interface DatasetEntry { label: string; longLabel: string; objectives: ObjectiveCard[]; // ordered: first entry = default target BiologyPanel: () => JSX.Element; refSetKeys: ReferenceSetKey[]; } // The "none" (Unsupervised) objective is intentionally absent from the // UI registry — the backend capability + tests are intact (see // UNSUP_OBJECTIVE), it's just not surfaced as a card. The frontend // code paths for target==="none" stay working but unreachable from // here. const DATASET_REGISTRY: Record = { coadread: { label: "Colorectal", longLabel: "Colorectal (TCGA COADREAD)", objectives: [ { key: "msi", label: "MSI separation", sub: "AUROC of MSI-H vs MSS" }, { key: "tmb", label: "Mutation burden", sub: "negative correlation with TMB" }, ], BiologyPanel: MMRBiologyPanel, refSetKeys: refSetsFor("coadread"), }, hnsc: { label: "Head & Neck", longLabel: "Head & Neck (TCGA HNSC)", objectives: [ { key: "hpv", label: "HPV detection", sub: "AUROC of HPV+ vs HPV−" }, ], BiologyPanel: HPVBiologyPanel, refSetKeys: refSetsFor("hnsc"), }, }; // Per-target tooltip for the fitness-curve InfoTip. Replaces the old // single TMB-centric TIPS.fitnessCurve string — now the tooltip mirrors // the y-axis title's objective-awareness. const FITNESS_TIP_BY_TARGET: Record = { hpv: "This shows how well the best program SEPARATES HPV+ from HPV− " + "tumours, measured by AUROC: 0.5 = coin-flip (no separation), " + "1.0 = perfect; higher = a cleaner split. We use AUROC because " + "HPV+ is only ~15% of patients, so it isn't fooled by always " + "guessing the majority.", msi: "This shows how well the best program SEPARATES the two subtypes " + "(MSI-H vs MSS), measured by AUROC: 0.5 = coin-flip (no separation), " + "1.0 = perfect; higher = a cleaner split. We use AUROC because MSI-H " + "is only ~15% of patients, so it isn't fooled by always guessing the " + "majority.", tmb: "This shows how strongly the program's score moves OPPOSITE to " + "mutation burden — low score where mutations are high (the " + "broken-spell-checker pattern). 0 = no relationship; higher = a " + "stronger opposite-direction link; the top = score and mutation " + "count move almost perfectly oppositely. The underlying correlation " + "is negative (e.g. −0.49); we plot its strength (0.49) so up = better.", none: "There is no target here. This measures how cleanly the program's " + "score splits patients into TWO groups: 0 ≈ no real split, 1 = two " + "clean, well-separated clusters. After the run we check what that " + "split lines up with on the named label this cohort carries — " + "that's the post-hoc alignment in the Result panel.", }; // Held-out card label per objective. Binary objectives ARE an AUROC; // say so. TMB is a (signed Spearman) correlation; unsup is silhouette. const HELD_OUT_LABEL_BY_TARGET: Record = { msi: "Held-out AUROC", hpv: "Held-out AUROC", tmb: "Held-out (|spearman|)", none: "Held-out (silhouette)", }; // Held-out tooltip per objective — same data, but the explanation of // what 'held-out' MEANS for the active metric differs. const HELD_OUT_TIP_BY_TARGET: Record = { msi: "Held-out AUROC — how well the winner sorts MSI-H apart from MSS " + "on patients it never saw during the search. AUROC is the chance " + "the score ranks a random MSI-H patient above a random MSS " + "patient: 0.5 = coin-flip, 1.0 = perfect. Tests the result isn't " + "just memorising the training data.", hpv: "How well the winner separates HPV+ from HPV− on patients it " + "never saw during the search. 0.5 = coin-flip, 1.0 = perfect. " + "This is the honest test that it didn’t just memorise the " + "training patients.", tmb: "Held-out |spearman| — the strength of the negative link between " + "the winner's score and mutation burden on patients it never saw. " + "0 = no link, higher = stronger (low score tracks high TMB). " + "Not AUROC: TMB is a continuous label, so the metric is a " + "correlation. Tests the result isn't just memorising training data.", none: "Held-out silhouette — how cleanly the winner splits unseen " + "patients into two groups. Scale: −1 = no real split, ~1 = two " + "very clean groups. The engine is told nothing about which two " + "groups; the held-out score checks that the split it found holds " + "up on new patients. Not AUROC: there's no label to compare to.", }; // Permutation-p tooltip per objective — the null shape changes for // unsupervised (random programs, no labels to shuffle). const PERMUTATION_P_TIP_BY_TARGET: Record = { msi: "How often pure chance matches this score across many runs with " + "the MSI labels shuffled. Small (e.g. below 0.05) = unlikely to " + "be a fluke.", hpv: "How often a random program would score this well by luck. Small " + "(e.g. below 0.05) = unlikely to be a fluke.", tmb: "How often pure chance matches this score across many runs with " + "the TMB values shuffled. Small (e.g. below 0.05) = unlikely to " + "be a fluke.", none: "How often a random gene-based program produces a split this clean " + "on the same patients. Small (e.g. below 0.05) = the winner is " + "unlikely to be a fluke (no labels are shuffled here because the " + "unsupervised search never saw any).", }; // Reference-set + post-hoc + discovered-axes tips swap with the active // dataset, since the reference vocabulary and the named labels differ // between colorectal (MMR/immune, MSI/TMB) and head & neck (p16 / // cell-cycle, HPV). const REFERENCE_SET_TIP_BY_DATASET: Record = { coadread: "A known gene list to check the answer against. MMR = the 4 DNA " + "repair / 'spell-checker' genes (MLH1, MSH2, MSH6, PMS2); " + "immune = immune-attack genes (CD8A, GZMA, PRF1).", hnsc: "A known gene list to check the answer against. p16 = CDKN2A — " + "the canonical HPV+ surrogate marker pathologists use; cell-cycle " + "= a standard set of DNA-replication / E2F-target genes that go " + "UP when HPV-E7 wrecks the RB brake.", }; const POSTHOC_TIP_BY_DATASET: Record = { coadread: "After the unsupervised search finishes, the discovered split is " + "compared to the MSI and TMB labels the engine never saw. AUROC " + "tells us whether the split tracks the MSI subtype; |spearman| " + "with TMB tells us whether the score moves with mutation burden. " + "The search itself was label-free.", hnsc: "After the unsupervised search finishes, the discovered split is " + "compared to the HPV label the engine never saw. AUROC tells us " + "whether the split tracks the HPV+ subtype: 0.5 = no relationship, " + "1.0 = a perfect match. The search itself was label-free.", }; const DISCOVERED_AXES_TIP_BY_DATASET: Record = { coadread: "After each unsupervised run, click \"Find next axis\" to peel the " + "discovered split out of the data (linear residualisation — we " + "subtract its signal from every gene) and search again. The chain " + "is a ranked, blind list of independent directions in the gene-" + "expression matrix — each named only afterwards by how well it " + "lines up with MSI / TMB. The residualisation is LINEAR, so " + "deeper non-linear structure may remain. The chain is in-memory " + "only — it resets on server restart or a fresh Run.", hnsc: "After each unsupervised run, click \"Find next axis\" to peel the " + "discovered split out of the data (linear residualisation — we " + "subtract its signal from every gene) and search again. The chain " + "is a ranked, blind list of independent directions in the gene-" + "expression matrix — each named only afterwards by how well it " + "lines up with HPV. The residualisation is LINEAR, so deeper non-" + "linear structure may remain. The chain is in-memory only — it " + "resets on server restart or a fresh Run.", }; export default function Lab() { // Default to Head & Neck + HPV detection on first load. Colorectal // stays selectable; first-load just lands on the dataset whose // detection objective is the live demo of the workflow. const [dataset, setDatasetState] = useState("hnsc"); const [target, setTarget] = useState( DATASET_REGISTRY.hnsc.objectives[0].key, ); const [params, setParams] = useState(DEFAULT_PARAMS); const [coherence, setCoherence] = useState(false); const [diversity, setDiversity] = useState(false); // Editable DSL injection rates. Only changed keys travel on the // wire so the default run is byte-for-byte unchanged. The DslRates // type is hoisted to module scope so the StabilityPanel (and any // other consumer) can accept it as a prop. const [dslRates, setDslRates] = useState({ ...DSL_DEFAULT_RATES }); const [status, setStatus] = useState("idle"); const [errorMsg, setErrorMsg] = useState(null); const [runId, setRunId] = useState(null); const [generations, setGenerations] = useState([]); const [result, setResult] = useState(null); const [operatorUsage, setOperatorUsage] = useState(null); // Graph view state — single graph area, swapped by winner / candidate / // pasted text. The string is a program_repr that the shared parser // turns into a tree. const [graphProgramRepr, setGraphProgramRepr] = useState(null); const [graphSourceLabel, setGraphSourceLabel] = useState("winner"); const [graphSelectedCandidateId, setGraphSelectedCandidateId] = useState(null); // Textarea text lives at the Lab level so tile clicks AND the paste tool // share the same controlled string. const [pasteText, setPasteText] = useState(""); const [revealData, setRevealData] = useState(null); // Iterative-unsupervised discovery: the ordered chain of axes // discovered so far (Axis 1, Axis 2, …). Each new unsup result — // whether it's the first run or a "Find next axis" follow-up — is // appended. A fresh Run resets the chain. const [axes, setAxes] = useState<{ run_id: string; result: RunResult }[]>( [], ); const esRef = useRef(null); useEffect(() => { return () => { esRef.current?.close(); }; }, []); // When a fresh result arrives, default the graph to the winning program. useEffect(() => { if (result) { setGraphProgramRepr(result.winning.program_repr); setPasteText(result.winning.program_repr); setGraphSourceLabel("winner"); setGraphSelectedCandidateId(null); } }, [result]); // Switching the cancer resets the chain + per-dataset state and // snaps the objective to the new dataset's default (first card) so // the request body never carries a stale (dataset, target) combo. function setDataset(next: DatasetId) { if (next === dataset) return; setDatasetState(next); const firstObjective = DATASET_REGISTRY[next].objectives[0].key; setTarget(firstObjective); setResult(null); setOperatorUsage(null); setGenerations([]); setRevealData(null); setRunId(null); setAxes([]); setGraphProgramRepr(null); setPasteText(""); setGraphSelectedCandidateId(null); } async function launchRun( opts: { residualize_against?: string[] } = {}, ) { const chained = !!opts.residualize_against && opts.residualize_against.length > 0; setStatus("running"); setErrorMsg(null); setGenerations([]); setResult(null); setOperatorUsage(null); setRunId(null); setRevealData(null); setGraphProgramRepr(null); setPasteText(""); setGraphSelectedCandidateId(null); // Fresh runs reset the chain; "Find next axis" preserves it. if (!chained) setAxes([]); try { // Only the keys the user actually changed travel on the wire — // an untouched DSL config reproduces the engine's defaults // byte-for-byte. const ratesDiff: Record = {}; (Object.keys(DSL_DEFAULT_RATES) as (keyof DslRates)[]).forEach((k) => { if (dslRates[k] !== DSL_DEFAULT_RATES[k]) { ratesDiff[k] = dslRates[k]; } }); const body: Parameters[0] = { objective_spec: OBJECTIVE_PRESETS[target], params, engine: "v2", dataset, coherence, diversity, }; if (Object.keys(ratesDiff).length > 0) body.rates_override = ratesDiff; if (chained) body.residualize_against = opts.residualize_against; const { run_id } = await postRun(body); setRunId(run_id); const es = new EventSource(`${API_URL}/runs/${run_id}/stream`); esRef.current?.close(); esRef.current = es; es.addEventListener("generation", (ev) => { const data = JSON.parse((ev as MessageEvent).data) as GenerationEvent; setGenerations((prev) => [...prev, data]); }); es.addEventListener("done", (ev) => { const data = JSON.parse((ev as MessageEvent).data) as RunResult; setResult(data); setStatus("done"); // Peel-off chain: append every result so the user sees a // ranked stack of axes — supervised objectives now too. setAxes((prev) => [...prev, { run_id, result: data }]); // Fetch the operator-usage payload now that the run is done. // Cached server-side; opaque-safe (keywords + counts only). // Non-fatal — the DSL vocabulary tiles just render without // counts on failure. getRunOperatorUsage(run_id) .then((u) => setOperatorUsage(u)) .catch(() => setOperatorUsage(null)); es.close(); }); es.addEventListener("error", (ev) => { try { const data = JSON.parse((ev as MessageEvent).data) as { detail?: string; }; setErrorMsg(data.detail ?? "stream error"); } catch { setErrorMsg("stream error"); } setStatus("error"); es.close(); }); es.onerror = () => { if (status === "running") { // Only flag if we haven't received a clean done already. setErrorMsg((m) => m ?? "lost connection to /stream"); setStatus((s) => (s === "running" ? "error" : s)); es.close(); } }; } catch (e) { setErrorMsg(e instanceof Error ? e.message : String(e)); setStatus("error"); } } async function start() { await launchRun(); } async function findNextAxis() { await launchRun({ residualize_against: axes.map((a) => a.run_id), }); } const fitnessLabel = FITNESS_LABEL_BY_TARGET[target]; return (

OncoDSL Lab

The engine hunts for a gene-expression pattern that predicts a cancer trait — blind: gene names are hidden as codes so it can’t lean on what we already know. You set the goal; it evolves a small program to hit it; then we reveal the genes it chose and check them against known biology.

{/* DSL vocabulary moved ABOVE the Run section now that its rate inputs are interactive — they're config the user edits before pressing Run. */} {operatorUsage && (

How often the engine reached for each building block across the whole search. Select and Reduce are the backbone; the rest show up only where they helped.

)}
{runId && status !== "idle" && ( { setGraphProgramRepr(c.program_repr); setPasteText(c.program_repr); setGraphSourceLabel(`candidate ${c.id}`); setGraphSelectedCandidateId(c.id); }} /> )} {graphProgramRepr ? ( ) : (
No program drawn yet.
)}
{ setGraphProgramRepr(repr); setGraphSourceLabel(label); setGraphSelectedCandidateId(null); }} />
{result && ( )} {result && target !== "none" && ( )} {result && target !== "none" && coherence && runId && ( )} {result && target === "none" && ( )} {/* DiscoveredAxes (peel-off chain) is no longer rendered. Peel-off only makes sense for the unsupervised objective — on supervised HPV / MSI / TMB runs "Find next axis" re-finds the same signal and the "aligns with MSI / TMB" framing is nonsensical. The unsupervised objective isn't a UI objective anymore, so this never fires. Gate kept as `target === "none"` for documentation; the component definition + backend chain remain untouched in case we want to surface it again behind a developer flag. */} {target === "none" && axes.length > 0 && ( )}
); } // --------------------------------------------------------------------------- // Iterative unsupervised discovery: the "peel off axes" stack. // --------------------------------------------------------------------------- function DiscoveredAxes({ axes, status, dataset, onFindNext, }: { axes: { run_id: string; result: RunResult }[]; status: Status; dataset: DatasetId; onFindNext: () => void; }) { const busy = status === "running"; return (
    {axes.map((a, i) => ( ))}
Chains within this session only. Start a fresh Run to reset.
); } function AxisCard({ index, result }: { index: number; result: RunResult }) { const w = result.winning; const target = result.objective_spec.target as Target; const threshold = HOLDOUT_THRESHOLD[target]; const generalises = Number.isFinite(w.holdout_score) && w.holdout_score >= threshold; const significant = Number.isFinite(w.permutation_p) && w.permutation_p < P_VALUE_SIGNIFICANT; const success = generalises && significant; const posthoc = result.posthoc; const msi = posthoc?.msi_auroc; const tmb = posthoc?.tmb_abs_spearman; return (
  • Axis {index}
    held-out {fmtFit(w.holdout_score, 3)} · p {fmtFit(w.permutation_p, 4)} {!success && ( (not above the verdict threshold) )}
    {w.gene_ids.length > 0 ? w.gene_ids.join(", ") : "(no genes)"}
    Aligns with{" "} MSI{" "} {msi != null && Number.isFinite(msi) ? ( <> at AUROC {fmtFit(msi, 3)} {msi >= POSTHOC_MSI_ALIGN && ( — matches the subtype )} ) : ( — (too few labels) )} ; TMB |spearman|{" "} {tmb != null && Number.isFinite(tmb) ? ( {fmtFit(tmb, 3)} ) : ( )} .
  • ); } // Optional-operator rate-input keys recognised by the DSL panel. // Mandatory operators (Select / Reduce / Combine; Associate is the // leftover of the Scalar branch) have no editable rate. type DslRateKey = "split" | "effect" | "fitapply" | "search"; function DSLVerbCard({ name, hint, usage, maxUses, nCandidates, rateKey, rateValue, rateDefault, onRate, structuralNote, }: { name: string; hint: string; usage: { total_uses: number; programs_using: number } | null; maxUses: number; nCandidates: number; // When `rateKey` is set the tile renders a rate input; otherwise // it shows `structuralNote` (always-on operators). rateKey?: DslRateKey; rateValue?: number; rateDefault?: number; onRate?: (k: DslRateKey, v: number) => void; structuralNote?: string; }) { const showUsage = usage != null && nCandidates > 0; const barFrac = showUsage && maxUses > 0 ? usage!.total_uses / maxUses : 0; const sharePct = showUsage && nCandidates > 0 ? Math.round((usage!.programs_using / nCandidates) * 100) : 0; const showRate = rateKey != null && onRate != null; const isDefault = showRate && rateValue != null && rateDefault != null && Math.abs(rateValue - rateDefault) < 1e-9; return (
    {name}
    {showRate ? ( ) : structuralNote ? ( {structuralNote} ) : null}
    {hint}
    {showUsage && ( <>
    used{" "} {usage!.total_uses.toLocaleString()} × — in{" "} {sharePct}% {" "} of programs
    0 ? "#3A6B7E" : "#C9CCC4", transition: "width 200ms ease", }} />
    )}
    ); } function DSLVocabularyTiles({ usage, rates, onRates, }: { usage: OperatorUsage | null; rates: { split: number; effect: number; fitapply: number; search: number; scalar_share: number; }; onRates: React.Dispatch>; }) { // Mandatory (structural) operators come first; the engine always // closes Vector to Reduce(Select(M, …), agg) and Combine glues // scores together, so they have no rate. const STRUCTURAL: { name: string; hint: string }[] = [ { name: "Select", hint: "Pick specific genes." }, { name: "Reduce", hint: "Combine those genes into one score per patient (average, max, etc.)." }, { name: "Combine", hint: "Merge two scores into one." }, { name: "Associate", hint: "Ask how strongly the score correlates with the target — the default of the two correlation-style options." }, ]; const OPTIONAL: { name: string; hint: string; key: DslRateKey }[] = [ { name: "Split", key: "split", hint: "Split patients into two subgroups, score each, then bring them back together." }, { name: "Effect", key: "effect", hint: "Same correlation as Associate, but after removing the effect of things like stage, age, sex, and race." }, { name: "Fit/Apply", key: "fitapply", hint: "Fit a small model that maps the score to the target, then use it." }, { name: "Search", key: "search", hint: "Runs a tiny gene-ranker inside the program (up to 4 genes, from at most 200 candidates). Rate 0 turns it off." }, ]; const byName = new Map(); if (usage) for (const op of usage.operators) byName.set(op.name, op); const maxUses = usage ? usage.operators.reduce((m, op) => Math.max(m, op.total_uses), 0) : 0; const nCandidates = usage?.n_candidates ?? 0; function setRate(k: DslRateKey | "scalar_share", v: number) { onRates((prev) => ({ ...prev, [k]: v })); } // For the "Set all equal" button: split the (default) sum of the // four optional rates evenly across them so the average grammar // mix stays near the defaults. (Default sum = 0.10 + 0.40 + 0.10 // + 0.05 = 0.65 → ~0.16 each.) const equalEach = (DSL_DEFAULT_RATES.split + DSL_DEFAULT_RATES.effect + DSL_DEFAULT_RATES.fitapply + DSL_DEFAULT_RATES.search) / 4; const isDefault = ( rates.split === DSL_DEFAULT_RATES.split && rates.effect === DSL_DEFAULT_RATES.effect && rates.fitapply === DSL_DEFAULT_RATES.fitapply && rates.search === DSL_DEFAULT_RATES.search && rates.scalar_share === DSL_DEFAULT_RATES.scalar_share ); return ( <>
    {STRUCTURAL.map((v) => ( ))} {OPTIONAL.map((v) => ( setRate(k, val)} /> ))}
    how many starting programs are built around Associate / Effect (which score a gene set by how well it correlates with the target) instead of giving each patient a score. 0.20 ≈ 1 in 5; 0 = never.
    ); } function buildRevealMap( data: EvaluateResponse | null, ): Record | undefined { if (!data) return undefined; const out: Record = {}; for (const row of data.revealed) { out[row.id] = { symbol: row.symbol, matched: row.matched }; } return out; } // ---------- Building blocks ------------------------------------------------ function SectionCard({ title, titleTip, subtitle, children, }: { title: string; titleTip?: string; subtitle?: string; children: React.ReactNode; }) { return (

    {title} {titleTip && }

    {subtitle && (

    {subtitle}

    )}
    {children}
    ); } // Top-level cancer / problem selector. Mirrors the segmented style of // the Objective control. Swapping a cancer swaps the objective options // + biology panel + reference sets via the dataset registry; the engine // underneath is identical. function CancerSelector({ dataset, onDataset, }: { dataset: DatasetId; onDataset: (d: DatasetId) => void; }) { const entries = Object.entries(DATASET_REGISTRY) as [DatasetId, DatasetEntry][]; return (
    = 3 ? "sm:grid-cols-3" : "sm:grid-cols-2") } > {entries.map(([id, entry]) => { const selected = id === dataset; return ( ); })}
    ); } function BiologyPanelForDataset({ dataset }: { dataset: DatasetId }) { const Panel = DATASET_REGISTRY[dataset].BiologyPanel; return ; } function ObjectiveBuilder({ dataset, target, onTarget, }: { dataset: DatasetId; target: Target; onTarget: (t: Target) => void; }) { const options = DATASET_REGISTRY[dataset].objectives; const cols = options.length >= 3 ? "sm:grid-cols-3" : "sm:grid-cols-2"; return (
    {options.map((o) => { const selected = o.key === target; const paramKey = ( o.key === "none" ? "obj_unsupervised" : `obj_${o.key}` ) as ParamKey; return (
    ); })}
    ); } type NumField = { key: Exclude; label: string; min: number; max: number; step: number; }; function ParamsControls({ params, onParams, coherence, onCoherence, diversity, onDiversity, }: { params: RunParams; onParams: (p: RunParams) => void; coherence: boolean; onCoherence: (c: boolean) => void; diversity: boolean; onDiversity: (d: boolean) => void; }) { const fields: NumField[] = [ { key: "generations", label: "Generations", min: 2, max: 1000, step: 1 }, { key: "population", label: "Population", min: 10, max: 3000, step: 10 }, { key: "genes_per_set", label: "Genes / set", min: 2, max: 8, step: 1 }, { key: "max_sets", label: "Max sets", min: 1, max: 2, step: 1 }, { key: "lambda", label: "λ (size penalty)", min: 0, max: 0.05, step: 0.001 }, { key: "seed", label: "Seed", min: 0, max: 9999, step: 1 }, { key: "permutations", label: "Permutations", min: 10, max: 500, step: 10 }, ]; const prefilterOff = params.prefilter_n === null; function setPrefilterOff() { onParams({ ...DEFAULTS_NO_PREFILTER, seed: params.seed }); } function setPrefilterOn() { onParams({ ...DEFAULTS_WITH_PREFILTER, seed: params.seed }); } return ( {prefilterOff && (

    Searching all genes is a bigger space — higher population/generations help, and results vary by seed, so try a few seeds and compare.

    )}
    {fields.map((f) => ( ))}
    Prefilter top-N
    {!prefilterOff && ( onParams({ ...params, prefilter_n: Number.parseInt(e.target.value, 10), }) } className="mt-2 rounded-md border border-border bg-white px-3 py-2 text-sm text-ink focus:border-accent focus:outline-none" /> )}
    onCoherence(e.target.checked)} className="mt-0.5 h-4 w-4 cursor-pointer accent-accent" />
    onDiversity(e.target.checked)} className="mt-0.5 h-4 w-4 cursor-pointer accent-accent" />
    {/* The Search-operator toggle moved into the DSL vocabulary panel as an editable rate (rate = 0 means disabled). See below. */}
    ); } function RunBar({ status, onRun, runId, error, }: { status: Status; onRun: () => void; runId: string | null; error: string | null; }) { const isRunning = status === "running"; return (
    {runId && ( run id {runId} · status {status} )}
    {error && (

    {error}

    )}
    ); } // Centered Y-axis label: rotated placed at the plot's vertical // midpoint via the YAxis viewBox. Default Recharts label sits at the top // of the axis with `position="insideLeft"`; this puts it where readers // expect — beside the middle of the axis. function CenteredYAxisLabel({ value, viewBox, }: { value: string; // Recharts injects viewBox automatically when the component is passed // as a label. viewBox?: { x: number; y: number; width: number; height: number }; }) { if (!viewBox) return null; const cx = viewBox.x - 4; const cy = viewBox.y + viewBox.height / 2; return ( {value} ); } // Custom legend rendered OUTSIDE the chart SVG so it lives on its own // row, below the X-axis title. Swatches mirror the line styles exactly: // solid accent for best, dashed muted for median. function ChartLegend() { return (
    best generation median
    ); } function LiveView({ generations, fitnessLabel, target, status, }: { generations: GenerationEvent[]; fitnessLabel: string; target: Target; status: Status; }) { if (generations.length === 0 && status !== "running") return null; const series = generations .map((g) => ({ generation: g.generation, best: typeof g.best_fitness === "number" && Number.isFinite(g.best_fitness) ? Number(g.best_fitness.toFixed(4)) : null, median: typeof g.median_fitness === "number" && Number.isFinite(g.median_fitness) ? Number(g.median_fitness.toFixed(4)) : null, })); const latest = generations[generations.length - 1]; return (

    Fitness curve

    best vs median
    } domain={["auto", "auto"]} />
    generation
    {latest && (
    Generation {latest.generation} · best{" "} {fmtFit(latest.best_fitness, 4)} {" "} · median{" "} {fmtFit(latest.median_fitness, 4)} gene codes
    )}
    ); } // Plain-reading verdict at the top of the Result panel. Green when the // run cleared its objective's held-out bar AND beat chance (p < 0.05); // amber otherwise. Unsupervised appends the post-hoc MSI-alignment read. function ResultVerdict({ result }: { result: RunResult }) { const target = result.objective_spec.target as Target; const holdout = result.winning.holdout_score; const p = result.winning.permutation_p; const posthoc = result.posthoc; const threshold = HOLDOUT_THRESHOLD[target]; const generalises = Number.isFinite(holdout) && holdout >= threshold; const significant = Number.isFinite(p) && p < P_VALUE_SIGNIFICANT; const success = generalises && significant; const pctChance = Number.isFinite(p) ? Math.round((p as number) * 100) : null; const callout = success ? (

    Real result: it holds up on unseen patients ({fmtFit(holdout, 3)}) and beats chance (p {fmtFit(p, 4)}). {target === "none" && }

    ) : (
    !

    This run found nothing reliable: held-out {fmtFit(holdout, 3)}{" "} (fell apart on unseen patients) · p {fmtFit(p, 4)}{" "} {pctChance != null && ( <>(a random program beats it ~{pctChance}% of the time) )} . Don’t read the genes as a discovery.

    ); return (
    {callout}
    ); } // "What does this mean?" — expandable plain-language explanation under // the verdict headline. Closed by default; mirrors ParameterFlow's // disclosure pattern (button + chevron + aria-expanded + useId panel). // Content branches on objective_spec.target since MSI/TMB are // single-question (held-out IS the answer) while unsupervised is // two-question (split is real vs. is it MSI). function VerdictBreakdown({ target, holdout, p, posthoc, success, generalises, significant, }: { target: Target; holdout: number; p: number; posthoc: RunResult["posthoc"] | undefined; success: boolean; generalises: boolean; significant: boolean; }) { const [open, setOpen] = useState(false); const panelId = useId(); return (
    {open && (
    {target === "none" ? ( ) : target === "tmb" ? ( ) : target === "hpv" ? ( ) : ( )}
    )}
    ); } function MSIBreakdownContent({ holdout, p, generalises, significant, success, }: { holdout: number; p: number; generalises: boolean; significant: boolean; success: boolean; }) { const pctChance = Number.isFinite(p) ? Math.round(p * 100) : null; return ( <>

    Is this result real and useful?

    • Held-out AUROC{" "} ({fmtFit(holdout, 3)}). On patients it never trained on, how well the score tells MSI-H from MSS. AUROC is the chance the score ranks a random MSI-H patient above a random MSS patient: 0.5 = coin-flip, 1.0 = perfect.{" "} {generalises ? "Strong." : "Weak — barely better than guessing."}
    • Beats chance (p {fmtFit(p, 4)}). Only about {pctChance ?? "—"}% of shuffled-label runs match this — so{" "} {significant ? "unlikely a fluke." : "it could be luck."}

    →{" "} {success ? "Real and useful — it separates MSI-H from MSS on patients it never saw." : "Not reliable — don’t read the genes as a discovery."}

    This is a detection result. Whether the engine found the known MMR / immune marker genes is a separate question — see Reveal & evaluate below.

    ); } function HPVBreakdownContent({ holdout, p, generalises, significant, success, }: { holdout: number; p: number; generalises: boolean; significant: boolean; success: boolean; }) { const pctChance = Number.isFinite(p) ? Math.round(p * 100) : null; return ( <>

    Is this result real and useful?

    • Held-out AUROC{" "} ({fmtFit(holdout, 3)}). On patients it never trained on, how well the score tells HPV+ from HPV− tumours. AUROC is the chance the score ranks a random HPV+ patient above a random HPV− patient: 0.5 = coin-flip, 1.0 = perfect.{" "} {generalises ? "Strong." : "Weak — barely better than guessing."}
    • Beats chance (p {fmtFit(p, 4)}). Only about {pctChance ?? "—"}% of shuffled-label runs match this — so{" "} {significant ? "unlikely a fluke." : "it could be luck."}

    →{" "} {success ? "Real and useful — it separates HPV+ from HPV− tumours on patients it never saw." : "Not reliable — don’t read the genes as a discovery."}

    This is a detection result. Whether the engine found the known marker genes (CDKN2A/p16, the cell-cycle program) is a separate question — see Reveal & evaluate below for the gene overlap and each revealed gene's individual rank.

    ); } function TMBBreakdownContent({ holdout, p, generalises, significant, success, }: { holdout: number; p: number; generalises: boolean; significant: boolean; success: boolean; }) { const pctChance = Number.isFinite(p) ? Math.round(p * 100) : null; return ( <>

    Is this result real and useful?

    • Held-out association{" "} ({fmtFit(holdout, 3)}). On patients it never trained on, the strength of the negative link between the score and mutation burden. Scale: 0 = no link, higher = stronger; ≥ 0.30 = a real link.{" "} {generalises ? "Strong." : "Weak — barely better than guessing."}
    • Beats chance (p {fmtFit(p, 4)}). Only about {pctChance ?? "—"}% of shuffled-label runs match this — so{" "} {significant ? "unlikely a fluke." : "it could be luck."}

    →{" "} {success ? "Real and useful." : "Not reliable — don’t read the genes as a discovery."}

    ); } function UnsupBreakdownContent({ holdout, p, posthoc, success, }: { holdout: number; p: number; posthoc: RunResult["posthoc"] | undefined; success: boolean; }) { const pctChance = Number.isFinite(p) ? Math.round(p * 100) : null; const hpv = posthoc?.hpv_auroc; const msi = posthoc?.msi_auroc; const tmb = posthoc?.tmb_abs_spearman; const hpvKnown = hpv != null && Number.isFinite(hpv); const msiKnown = msi != null && Number.isFinite(msi); // HNSC unsup carries hpv; coadread unsup carries msi+tmb. Each cohort // shows the alignment question that's actually answerable. const showHPV = hpvKnown; const hpvAligned = hpvKnown && (hpv as number) >= POSTHOC_HPV_ALIGN; const msiAligned = msiKnown && (msi as number) >= POSTHOC_MSI_ALIGN; return ( <>

    This is answering two separate questions.

    1) Is the split real?

    • Holds up on unseen patients{" "} ({fmtFit(holdout, 3)}). The engine found the split on one set of patients, then it still cleanly separated a different set it never saw. Scale: −1 = no real split, ~1 = two very clean, well-separated groups.
    • Beats chance (p {fmtFit(p, 4)}). Only about {pctChance ?? "—"}% of random programs produce a split this clean — so it’s very unlikely to be a fluke.

    → Together:{" "} {success ? "yes, it found a real, reproducible way to split patients into two groups." : "no — the split didn’t hold up on unseen patients or could be luck. Don’t read the genes as a discovery."}

    {showHPV ? ( <>

    2) Is that split the HPV+ subtype?

    • Match to HPV:{" "} {fmtFit(hpv, 3)}. Scale: 0.5 = no relationship (coin-flip), 1.0 = a perfect match.{" "} {hpvAligned ? "So this split largely IS the HPV+ subtype — the engine rediscovered the viral signature blind." : "So this split is NOT the HPV+ subtype — it’s a real grouping about something else (see the revealed genes)."}
    ) : ( <>

    2) Is that split the MSI subtype?

    • Match to MSI:{" "} {msiKnown ? fmtFit(msi, 3) : "—"}. Scale: 0.5 = no relationship (coin-flip), 1.0 = a perfect match.{" "} {msiKnown ? ( {msiAligned ? "So this split largely IS MSI — the engine rediscovered the subtype blind." : "So this split is NOT the MSI subtype — it’s a real grouping about something else (see the revealed genes)."} ) : ( (Too few held-out labels to check alignment.) )}
    • (Match to mutation burden, TMB:{" "} {tmb != null && Number.isFinite(tmb) ? fmtFit(tmb, 3) : "—"} — 0 = unrelated, 1 = move together perfectly.)
    )}

    Key idea: a split can be completely real and still be about a different thing than you hoped — like sorting a crowd cleanly by hair colour when you wanted left- vs right-handed.

    ); } function UnsupAlignmentTail({ posthoc, }: { posthoc: RunResult["posthoc"] | undefined; }) { if (!posthoc) return null; // Pick whichever supervised label the cohort actually carries. // HNSC unsup runs report hpv_auroc; coadread unsup runs report msi_auroc. const hpv = posthoc.hpv_auroc; if (hpv != null && Number.isFinite(hpv)) { if (hpv >= POSTHOC_HPV_ALIGN) { return ( <> {" "}and the blind split matches HPV (AUROC {fmtFit(hpv, 3)}) — it rediscovered the viral subtype. ); } return ( <> {" "}but the split doesn’t line up with HPV (AUROC{" "} {fmtFit(hpv, 3)}). ); } const auroc = posthoc.msi_auroc; if (auroc == null || !Number.isFinite(auroc)) { return <> (too few held-out labels to check alignment); } if (auroc >= POSTHOC_MSI_ALIGN) { return ( <> {" "}and the blind split matches MSI (AUROC {fmtFit(auroc, 3)}) — it rediscovered the subtype. ); } return ( <> {" "}but the split doesn’t line up with MSI (AUROC{" "} {fmtFit(auroc, 3)}). ); } function ResultPanel({ result, dataset, target, runId, coherence, diversity, params, dslRates, }: { result: RunResult; dataset: DatasetId; target: Target; runId: string | null; coherence: boolean; diversity: boolean; params: RunParams; dslRates: DslRates; }) { const w = result.winning; const b = result.baseline; const t = result.objective_spec.target as Target; // The merged Groups table is gated to coherence-on supervised runs // — same gate uses. Only point at it when it // will actually render. const pointToGroups = target !== "none" && coherence; return (
    {b ? ( ) : ( )}
    {pointToGroups && (

    The winning program is badged in{" "} Groups the engine explored {" "} below — that panel is the engine’s actual picks ranked by how the search judged them, and each row shows its genes and where each gene ranks on its own.

    )}
    Winning program
    {w.program_repr}
    {w.feature_sets && w.feature_sets.length > 0 && (
    feature sets:{" "} {w.feature_sets.map((s) => `[${s.join(", ")}]`).join(" ")}
    )} {result.posthoc && result.objective_spec.target === "none" && ( )} {dataset === "hnsc" && target === "hpv" && runId && ( )} {dataset === "hnsc" && target === "hpv" && ( )}
    ); } // Post-hoc alignment block for an unsupervised run: show how the // discovered cluster split lines up with the MSI label and TMB, // computed AFTER the GP finished by the api worker (engine remains // blind during the search). function PosthocAlignment({ posthoc }: { posthoc: NonNullable }) { // HNSC unsup runs carry hpv_auroc; coadread unsup runs carry msi / // tmb. Pick the alignment that has actual data — neither cohort has // both, so showing the empty one is just noise. const showHPV = posthoc.hpv_auroc != null && Number.isFinite(posthoc.hpv_auroc); const tipKey: DatasetId = showHPV ? "hnsc" : "coadread"; return (
    Post-hoc alignment
    {showHPV ? (

    The discovered 2-cluster split aligns with{" "} HPV at AUROC{" "} {fmtFit(posthoc.hpv_auroc, 3)}.

    ) : (

    The discovered 2-cluster split aligns with{" "} MSI{" "} {posthoc.msi_auroc != null ? ( <> at AUROC{" "} {fmtFit(posthoc.msi_auroc, 3)} ) : ( — (too few held-out labels) )} ; the score’s |spearman| with{" "} TMB is{" "} {posthoc.tmb_abs_spearman != null ? ( {fmtFit(posthoc.tmb_abs_spearman, 3)} ) : ( — (insufficient TMB) )} .

    )}

    {posthoc.n_holdout} held-out patients · labels were never visible to the engine during search.

    ); } // Independent-cohort validation panel — GSE65858 transfer test for // HNSC/HPV runs. Fetches /runs/{id}/transfer once the run is done; // renders a green "holds on strangers" verdict callout (or an amber // "found nothing reliable" one), three metric cards, an amber // cross-platform pill, a three-step "how this works" strip, an airgap // footnote, and one honest note. Only surfaces symbols that were // already revealed on the blind side (the winner's own genes). function ExternalValidation({ runId }: { runId: string }) { const [data, setData] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { let cancelled = false; setLoading(true); setError(null); setData(null); getRunTransfer(runId) .then((d) => { if (!cancelled) setData(d); }) .catch((e) => { if (!cancelled) setError(e instanceof Error ? e.message : String(e)); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [runId]); return (
    Independent-cohort validation

    The winning pattern was found blind on TCGA. Here it’s applied, unchanged, to a completely separate group of head & neck patients — a different country, hospital, and measuring machine.

    {loading && (

    running the transfer test…

    )} {!loading && (error || !data) && (

    Couldn’t validate externally: {error ?? "unknown error"}. The main result stands — this extra check is available once the GSE65858 cohort is built (see data_pipeline/build_gse65858 ).

    )} {!loading && data && data.n_found === 0 && (

    Couldn’t validate externally — none of the winner’s genes are measured on GSE65858’s platform (Illumina HumanHT-12 v4 microarray). The main result stands; this extra check is only meaningful when at least some of the winner’s genes carry over.

    )} {!loading && data && data.n_found > 0 && ( )}
    ); } function ExternalValidationBody({ data }: { data: TransferResult }) { const isReal = data.auroc != null && data.p != null && data.auroc >= 0.75 && data.p < 0.05; const pPct = data.p != null && Number.isFinite(data.p) ? Math.max(0.01, Math.min(99.99, data.p * 100)) : null; return ( <>
    {isReal ? "✓" : "?"}

    {isReal ? ( <> It holds on strangers.{" "} The pattern still separates HPV+ from HPV− in an independent cohort — AUROC{" "} {fmtFit(data.auroc, 3)}, and only {pPct != null ? pPct.toFixed(1) : "—"}% of random labellings do this well (p{" "} {fmtFit(data.p, 3)}). ) : ( <> Not confirmed on strangers.{" "} On the independent cohort the pattern reached AUROC{" "} {fmtFit(data.auroc, 3)} {data.p != null && ( <> {" "}with p{" "} {fmtFit(data.p, 3)} )}{" "} — below the “holds up” bar. The main result stands; treat the transfer step as inconclusive here. )}

    Validated across two platforms.{" "} TCGA RNA-seq → GSE65858 microarray. Agreement across different machines is a stronger result, not a weaker one.
    How this validation works
    {[ { n: "1", head: "reveal", body: "Unmask only the winner's handful of genes — the one thing that crosses over.", }, { n: "2", head: "match & level", body: "Find those genes in GSE65858 and put them on a common scale, so RNA-seq vs microarray doesn't matter.", }, { n: "3", head: "score", body: "Average them per patient and check how well that separates HPV+ from HPV− — against the virus-typed labels.", }, ].map((step) => (
    {step.n} · {step.head}
    {step.body}
    ))}
    🔒

    The airgap holds. The engine never sees this cohort. Only the winning gene names cross over, once, at the end — so a result here is genuine rediscovery, not a lookup.

    One honest note. This checks the gene signature the winner chose. For a simple winner that is the whole program; for a multi-part one it’s the gene set rather than the exact arithmetic — which, across two platforms, is the more honest thing to measure anyway.

    ); } // --------------------------------------------------------------------------- // Stability across seeds. Reruns the HPV pipeline across a list of // seeds and shows (a) how tightly the result holds and (b) which // genes recur across independent searches. Frontend-only — reuses // postRun / SSE / getRunResult / getRunTransfer / getFullRankDiagnostic; // no new endpoint. Sequential (backend runs one at a time). Gated to // HNSC/HPV by the caller. // --------------------------------------------------------------------------- const STABILITY_DEFAULT_SEEDS = [1, 3, 7, 11, 13, 17, 23, 29]; type SeedRow = { seed: number; holdout: number | null; permutation_p: number | null; symbols: string[]; transfer: TransferResult | null; error: string | null; }; type StabilityStatus = "idle" | "running" | "done" | "error"; function parseSeedList(input: string): number[] { return Array.from( new Set( input .split(/[\s,]+/) .map((s) => s.trim()) .filter((s) => s.length > 0) .map((s) => Number(s)) .filter((n) => Number.isFinite(n) && Number.isInteger(n) && n >= 0 && n < 1_000_000_000), ), ); } /** Fire a run's SSE stream and resolve on ``done``. Does NOT touch the * main single-run state (setResult / setGenerations / setStatus). */ function awaitRunDone(runId: string): Promise { return new Promise((resolve, reject) => { const es = new EventSource(`${API_URL}/runs/${runId}/stream`); let settled = false; const finish = (done: () => void) => { if (settled) return; settled = true; try { es.close(); } catch { /* noop */ } done(); }; es.addEventListener("done", (ev) => { finish(() => { try { const data = JSON.parse((ev as MessageEvent).data) as RunResult; resolve(data); } catch (e) { reject(e instanceof Error ? e : new Error(String(e))); } }); }); es.addEventListener("error", (ev) => { finish(() => { try { const data = JSON.parse((ev as MessageEvent).data) as { detail?: string }; reject(new Error(data.detail ?? "run error")); } catch { // Some SSE errors don't carry a JSON body — fall back to a // generic message. reject(new Error("stream error")); } }); }); // Note: EventSource emits "error" for network hiccups too. We treat // it as a hard reject — the sweep records the error and continues // with the next seed. }); } function StabilityPanel({ dataset, target, params, coherence, diversity, dslRates, }: { dataset: DatasetId; target: Target; params: RunParams; coherence: boolean; diversity: boolean; dslRates: DslRates; }) { const [seedInput, setSeedInput] = useState( STABILITY_DEFAULT_SEEDS.join(", "), ); const [status, setStatus] = useState("idle"); const [progress, setProgress] = useState<{ k: number; total: number } | null>(null); const [rows, setRows] = useState([]); const [errorMsg, setErrorMsg] = useState(null); const cancelRef = useRef(false); const [tagBySymbol, setTagBySymbol] = useState>({}); const running = status === "running"; // A global signal the outer Run bar can honour to disable itself // while a sweep is in flight. We set it on window so it doesn't // require plumbing changes; the RunBar reads it via the same-file // check below. useEffect(() => { if (typeof window === "undefined") return; (window as unknown as { __stabilitySweepActive?: boolean }).__stabilitySweepActive = running; }, [running]); async function runSweep() { const seeds = parseSeedList(seedInput); if (seeds.length === 0) { setErrorMsg("Enter at least one seed."); return; } setStatus("running"); setErrorMsg(null); setRows([]); setProgress({ k: 0, total: seeds.length }); cancelRef.current = false; // Only send DSL rates that differ from defaults — same discipline // launchRun() uses so unaltered runs stay byte-for-byte identical // to the current defaults. const ratesDiff: Record = {}; (Object.keys(DSL_DEFAULT_RATES) as (keyof DslRates)[]).forEach((k) => { if (dslRates[k] !== DSL_DEFAULT_RATES[k]) ratesDiff[k] = dslRates[k]; }); const collected: SeedRow[] = []; for (let i = 0; i < seeds.length; i++) { if (cancelRef.current) break; const seed = seeds[i]; setProgress({ k: i + 1, total: seeds.length }); let row: SeedRow = { seed, holdout: null, permutation_p: null, symbols: [], transfer: null, error: null, }; try { const body: Parameters[0] = { objective_spec: OBJECTIVE_PRESETS[target], params: { ...params, seed }, engine: "v2", dataset, coherence, diversity, }; if (Object.keys(ratesDiff).length > 0) body.rates_override = ratesDiff; const { run_id } = await postRun(body); const result = await awaitRunDone(run_id); row.holdout = result.winning.holdout_score; row.permutation_p = result.winning.permutation_p; // Reveal only this seed's winner's genes (bounded — same // discipline as /transfer). const winnerIds = result.winning.gene_ids ?? []; if (winnerIds.length > 0) { try { const r = await postReveal(winnerIds); row.symbols = r.symbols .filter((s: string) => typeof s === "string" && s.length > 0); } catch { // Non-fatal — keep the row without symbols. } } // Transfer test. Nulls on failure (e.g. GSE65858 not built); // never crash the sweep. try { row.transfer = await getRunTransfer(run_id); } catch { row.transfer = null; } } catch (e) { row.error = e instanceof Error ? e.message : String(e); } collected.push(row); setRows([...collected]); } // Tag genes by reference set (via reference_marks from // /diagnostic/full-rank — bounded, already-revealed known set). try { const diag = await getFullRankDiagnostic(dataset, target); const tags: Record = {}; for (const m of diag.reference_marks ?? []) { // p16 wins over cell_cycle if a symbol is in both. if (!tags[m.symbol] || m.set_name === "p16") { tags[m.symbol] = m.set_name; } } setTagBySymbol(tags); } catch { setTagBySymbol({}); } setProgress(null); setStatus(cancelRef.current ? "idle" : "done"); cancelRef.current = false; } function stopSweep() { cancelRef.current = true; } return (
    Stability across seeds

    Rerun the HPV search from different random starting points and see whether the answer holds — the same held-out AUROC bracket, the same genes coming back.

    {rows.length > 0 && ( )}
    ); } function StabilityControls({ seedInput, onSeedInput, running, onStart, onStop, progress, error, }: { seedInput: string; onSeedInput: (s: string) => void; running: boolean; onStart: () => void; onStop: () => void; progress: { k: number; total: number } | null; error: string | null; }) { return (
    {!running ? ( ) : ( )} {progress && ( Running seed {progress.k} of {progress.total}… )}
    {error && (

    {error}

    )}
    ); } function StabilityResults({ rows, tagBySymbol, done, }: { rows: SeedRow[]; tagBySymbol: Record; done: boolean; }) { // ----- Section 1: dot strips ----- const holdouts = rows.map((r) => r.holdout).filter((v): v is number => v != null); const xfers = rows .map((r) => r.transfer?.auroc ?? null) .filter((v): v is number => v != null); const heldRange: [number, number] | null = holdouts.length > 0 ? [Math.min(...holdouts), Math.max(...holdouts)] : null; const xferRange: [number, number] | null = xfers.length > 0 ? [Math.min(...xfers), Math.max(...xfers)] : null; // ----- Section 2: recurrence ----- const symbolCounts: Record = {}; for (const r of rows) { // dedup within a winner const uniq = Array.from(new Set(r.symbols)); for (const s of uniq) symbolCounts[s] = (symbolCounts[s] ?? 0) + 1; } const N = rows.length; type RecEntry = { symbol: string; count: number; tag: string }; const rec: RecEntry[] = Object.entries(symbolCounts) .map(([symbol, count]) => ({ symbol, count, tag: tagBySymbol[symbol] ?? "alternate", })) .sort((a, b) => b.count - a.count || a.symbol.localeCompare(b.symbol)); const recurring = rec.filter((e) => e.count >= 2); const passengers = rec.filter((e) => e.count === 1); return (
    {done && ( 0 && rows.every( (r) => r.permutation_p != null && r.permutation_p < 0.05, ) } allIndepSig={ rows.length > 0 && rows.every( (r) => r.transfer?.p != null && r.transfer.p < 0.05, ) } topGeneEntry={rec.find((e) => e.count >= 2) ?? null} nOnce={passengers.length} /> )}
    ); } // Deterministic, LLM-free plain-language summary composed from the // completed sweep's aggregates. Rendered above the dot strips when // the sweep finishes. Border/tone: green if perfStable && geneStable; // teal if perfStable && !geneStable; amber otherwise. function StabilitySummaryCallout({ nSeeds, heldRange, xferRange, allHeldSig, allIndepSig, topGeneEntry, nOnce, }: { nSeeds: number; heldRange: [number, number] | null; xferRange: [number, number] | null; allHeldSig: boolean; allIndepSig: boolean; topGeneEntry: { symbol: string; count: number; tag: string } | null; nOnce: number; }) { if (nSeeds === 0) return null; const heldMin = heldRange ? heldRange[0] : null; const heldMax = heldRange ? heldRange[1] : null; const indepMin = xferRange ? xferRange[0] : null; const indepMax = xferRange ? xferRange[1] : null; const perfStable = heldMin != null && heldMin >= 0.75 && allHeldSig; const topGene = topGeneEntry?.symbol ?? null; const topCount = topGeneEntry?.count ?? 0; const geneStable = topGene != null && topCount >= Math.ceil(nSeeds / 2); // Tone. let border = "#B3402F"; let bg = "#FBE6E6"; let fg = "#8A3225"; if (perfStable && geneStable) { border = "#2F6E4C"; bg = "#E3F2EA"; fg = "#1F4A34"; } else if (perfStable && !geneStable) { border = "#3A6B7E"; bg = "#EAF0F2"; fg = "#2C5563"; } // Line 1 — performance. const heldPart = heldMin != null && heldMax != null ? `held-out AUROC ${fmtFit(heldMin, 3)}–${fmtFit(heldMax, 3)}` : "held-out AUROC not available"; const indepPart = indepMin != null && indepMax != null ? ` on the independent cohort (${fmtFit(indepMin, 3)}–${fmtFit(indepMax, 3)})` : ""; const chanceTail = allIndepSig ? ", and every run beat chance." : ", and most runs beat chance on the independent cohort."; const line1 = `Across ${nSeeds} independent searches, the engine detected HPV every time — ${heldPart}${indepPart}${chanceTail}`; // Line 2 — gene recurrence. let line2: string; if (geneStable && topGene) { line2 = `The same genes kept coming back — ${topGene} in ${topCount}/${nSeeds} runs — which is strong evidence the signature itself is real, not a fluke of one search.`; } else if (topGene) { line2 = `But the specific genes differed almost every run — the most repeated was ${topGene} (${topCount}/${nSeeds}), and ${nOnce} genes appeared only once. That's the mark of a strong but redundant signal: many different gene sets separate HPV equally well.`; } else { // No gene appeared more than once at all. line2 = `But the specific genes differed every run — no single gene showed up in more than one seed's winner, and ${nOnce} genes appeared only once. That's the mark of a strong but redundant signal: many different gene sets separate HPV equally well.`; } const line3 = geneStable ? null : `So trust the detection, but don't read any single run's gene list as THE gene list — for a specific, stable gene program you'd need a target where the answer is concentrated in a few genes.`; return (

    {line1}

    {line2}

    {line3 &&

    {line3}

    }
    ); } function StabilityDotStrips({ heldRange, xferRange, rows, }: { heldRange: [number, number] | null; xferRange: [number, number] | null; rows: SeedRow[]; }) { return (
    Does the result hold across seeds?
    r.holdout)} color="#3A6B7E" band="#D9E4EA" /> r.transfer?.auroc ?? null)} color="#BC6B2E" band="#F5E5D4" />

    Tight and high across seeds = the result isn’t a lucky random start. The independent cohort usually costs a little — that’s expected across countries, hospitals, and platforms.

    ); } function DotStrip({ label, sublabel, values, color, band, }: { label: string; sublabel: string; values: (number | null)[]; color: string; band: string; }) { const W = 640; const H = 44; const padL = 40; const padR = 40; const innerW = W - padL - padR; const AXIS_MIN = 0.5; const AXIS_MAX = 1.0; const y = 26; function xOf(v: number): number { const t = Math.max(0, Math.min(1, (v - AXIS_MIN) / (AXIS_MAX - AXIS_MIN))); return padL + t * innerW; } const finite = values.filter((v): v is number => v != null && Number.isFinite(v)); const vmin = finite.length > 0 ? Math.min(...finite) : null; const vmax = finite.length > 0 ? Math.max(...finite) : null; return (
    {label}{" "} {sublabel} {finite.length} of {values.length} seeds
    {/* min–max band */} {vmin != null && vmax != null && ( )} {/* baseline */} {/* tick labels: 0.5, 0.75, 1.0 */} {[0.5, 0.75, 1.0].map((t) => ( {t === 0.5 ? "0.5 coin-flip" : t.toFixed(2)} ))} {/* dots */} {values.map((v, i) => ( v != null && Number.isFinite(v) ? ( ) : null ))}
    ); } const REC_COLOURS: Record = { cell_cycle: { fill: "#F5E5D4", stroke: "#BC6B2E", label: "cell_cycle" }, p16: { fill: "#FBF0CC", stroke: "#B8860B", label: "p16" }, alternate: { fill: "#E6F0F3", stroke: "#3A6B7E", label: "alternate" }, passengers: { fill: "#EDEDED", stroke: "#9AA0A6", label: "passengers" }, }; function StabilityRecurrence({ recurring, passengers, totalSeeds, }: { recurring: { symbol: string; count: number; tag: string }[]; passengers: { symbol: string; count: number; tag: string }[]; totalSeeds: number; }) { return (
    Do the same genes keep coming back?

    Recurring proliferation / cell-cycle genes = real signal. Genes that appear only once are usually passengers.

    {recurring.length === 0 && (

    No gene appeared in more than one seed’s winner.

    )} {recurring.map((r) => { const pal = REC_COLOURS[r.tag] ?? REC_COLOURS.alternate; const pct = Math.round((r.count / Math.max(1, totalSeeds)) * 100); return (
    {r.symbol}
    {r.count} / {totalSeeds} {r.tag === "alternate" ? "alternate" : r.tag}
    ); })} {passengers.length > 0 && (
    passengers
    1 / {totalSeeds} p.symbol) .join(", ") + (passengers.length > 40 ? ", …" : "") } > {passengers.length} gene{passengers.length === 1 ? "" : "s"} · 1 seed each
    )}
    ); } function StabilityTable({ rows }: { rows: SeedRow[] }) { return (
    Per-seed detail
    {rows.map((r) => { const t = r.transfer; const total = t ? t.n_found + t.n_missing : r.symbols.length; const found = t ? t.n_found : r.symbols.length; return ( ); })}
    seed held-out AUROC p indep. AUROC indep. p genes found / total
    {r.seed} {fmtFit(r.holdout, 3)} {fmtFit(r.permutation_p, 3)} {fmtFit(t?.auroc ?? null, 3)} {fmtFit(t?.p ?? null, 3)} {r.error ? ( error ) : ( `${found} / ${total || "—"}` )}
    ); } function Metric({ label, value, tip, sub, }: { label: string; value: string; tip?: string; sub?: string; }) { return (
    {label} {tip && }
    {value}
    {sub && (
    {sub}
    )}
    ); } // --------------------------------------------------------------------------- // RankingResult — the consolidated reveal panel for supervised // objectives. Shows the FULL single-gene ranking computed on the // engine's TRAIN split (~20k opaque rows), with the GP winner gene(s) // and the dataset's reference-set genes marked along a rank track and // pinned to the top of a browsable list. Symbols are revealed ONLY for // those highlighted rows via the single-ID /evaluate reveal path. // Airgap-clean: the full list never carries symbols on the wire. // --------------------------------------------------------------------------- // Three-way colour scheme for the ranking result. The winner stays in // the project's accent teal; reference sets get their own distinct // colours so e.g. p16 (a single canonical marker) reads separately // from the broader cell-cycle group. Falls back to the muted-amber // "set" colour for any future set name. interface RankPalette { stroke: string; fill: string; bg: string; } const RANK_COLORS: Record & { winner: RankPalette; p16: RankPalette; immune: RankPalette; cell_cycle: RankPalette; MMR: RankPalette; } = { winner: { stroke: "#3A6B7E", fill: "#3A6B7E", bg: "#EAF0F2" }, // p16 / immune — the "single canonical marker" slot — deep gold. p16: { stroke: "#B8860B", fill: "#B8860B", bg: "#FBF0CC" }, immune: { stroke: "#B8860B", fill: "#B8860B", bg: "#FBF0CC" }, // cell_cycle / MMR — the "broader set" slot — muted amber. cell_cycle: { stroke: "#A75D2A", fill: "#D9A074", bg: "#FBF1E6" }, MMR: { stroke: "#A75D2A", fill: "#D9A074", bg: "#FBF1E6" }, }; function colorForSet(setName: string | undefined): RankPalette { if (!setName) return RANK_COLORS.cell_cycle; return RANK_COLORS[setName] ?? RANK_COLORS.cell_cycle; } // "top 0.5%" / "top 21%" / "bottom 4.9%" — two sig figs. Use "top X%" // for ranks in the upper half; flip to "bottom Y%" once past the // midpoint so very-poor ranks read as low. function rankPercentile(rank: number, n: number): string { if (n <= 0) return "—"; const topPct = (rank / n) * 100; if (topPct <= 50) return `top ${formatPct(topPct)}%`; const bottomPct = 100 - topPct; return `bottom ${formatPct(bottomPct)}%`; } function formatPct(p: number): string { // 2 sig figs across the whole range. if (p >= 10) return p.toFixed(0); if (p >= 1) return p.toFixed(1); if (p >= 0.1) return p.toFixed(2); return p.toFixed(3); } interface RevealedWinnerRow { opaque_id: string; symbol: string; rank: number | null; score: number | null; } function RankingResult({ result, dataset, target, onRevealed, }: { result: RunResult; dataset: DatasetId; target: Target; onRevealed?: (r: EvaluateResponse) => void; }) { const [diag, setDiag] = useState(null); const [diagError, setDiagError] = useState(null); const [diagLoading, setDiagLoading] = useState(false); const [winnerReveal, setWinnerReveal] = useState([]); const [refKey, setRefKey] = useState(""); const [search, setSearch] = useState(""); // Fetch the full opaque ranking once per (dataset, target). useEffect(() => { let cancelled = false; setDiagLoading(true); setDiagError(null); setDiag(null); getFullRankDiagnostic(dataset, target) .then((d) => { if (!cancelled) setDiag(d); }) .catch((e) => { if (!cancelled) setDiagError(e instanceof Error ? e.message : String(e)); }) .finally(() => { if (!cancelled) setDiagLoading(false); }); return () => { cancelled = true; }; }, [dataset, target]); // Build a map from opaque_id → rank/score from the full diagnostic. const rankByOpaque = useMemo(() => { const m = new Map(); if (diag) { for (const r of diag.ranks) m.set(r.opaque_id, { rank: r.rank, score: r.score }); } return m; }, [diag]); // Reveal the winner's opaque IDs → symbols, plus look up their rank // in the diagnostic for the rank-track marks. Uses the single-ID // /evaluate path — never the whole map. const winnerIds = result.winning.gene_ids; const refKeysForDataset = useMemo( () => DATASET_REGISTRY[dataset].refSetKeys, [dataset], ); useEffect(() => { if (refKey === "" || !refKeysForDataset.includes(refKey as never)) { setRefKey(refKeysForDataset[0] as string); } }, [refKeysForDataset, refKey]); useEffect(() => { let cancelled = false; if (!winnerIds.length || !diag) { setWinnerReveal([]); return; } const referenceSet = (refKey || refKeysForDataset[0]) as string; postEvaluate({ gene_ids: winnerIds, reference_set: referenceSet, dataset, target, }) .then((r) => { if (cancelled) return; const rows: RevealedWinnerRow[] = r.revealed.map((row) => { const lookup = rankByOpaque.get(row.id); return { opaque_id: row.id, symbol: row.symbol, rank: lookup?.rank ?? row.rank ?? null, score: lookup?.score ?? (row.single_gene_metric != null ? row.single_gene_metric : null), }; }); setWinnerReveal(rows); if (onRevealed) onRevealed(r); }) .catch(() => { if (!cancelled) setWinnerReveal([]); }); return () => { cancelled = true; }; }, [winnerIds, diag, dataset, target, refKey, rankByOpaque, onRevealed, refKeysForDataset]); if (diagLoading && !diag) { return (

    Computing the full ranking…

    ); } if (diagError || !diag) { return (

    {diagError ?? "No ranking available for this (dataset, target)."}

    ); } const winnerOpaqueSet = new Set(winnerIds); const winnerSymbolByOpaque = new Map( winnerReveal.map((w) => [w.opaque_id, w.symbol]), ); const referenceByOpaque = new Map(); for (const m of diag.reference_marks) referenceByOpaque.set(m.opaque_id, m); const metricLabel = diag.metric_kind === "auroc" ? "AUROC" : "signed Spearman ρ"; // Pinned rows: known reference markers only (p16 / cell_cycle for // HNSC; MMR / immune for CRC). The winner's constituent genes used // to be pinned here too, but that mixed the GP's GROUP into a table // of INDIVIDUALLY-scored reference markers — confusing. Winner // genes + their ranks now live in the Result panel itself // (self-contained "GP's group" view); this panel is the // known-marker recovery diagnostic. const pinnedRows: { opaque_id: string; symbol: string; rank: number; score: number; kind: "reference"; set_name?: string; }[] = []; for (const m of [...diag.reference_marks].sort((a, b) => a.rank - b.rank)) { pinnedRows.push({ opaque_id: m.opaque_id, symbol: m.symbol, rank: m.rank, score: m.score, kind: "reference", set_name: m.set_name, }); } // Browsable list: top-200 of the ranks PLUS any rows that match the // search filter — pin the highlighted rows above. (Anti-pattern to // render 20k s; this is more than enough for browsing.) const TOP_N = 200; const trimmedSearch = search.trim().toLowerCase(); let listRows: FullRankDiagnostic["ranks"]; if (trimmedSearch.length > 0) { listRows = diag.ranks.filter((r) => r.opaque_id.toLowerCase().includes(trimmedSearch), ); } else { listRows = diag.ranks.slice(0, TOP_N); } return ( 0 ? ` (${ target === "hpv" ? "HPV+" : "MSI-H" } ${diag.n_pos} / ${ target === "hpv" ? "HPV−" : "MSS" } ${diag.n_neg}.)` : "") } titleTip={TIPS.rankingResult} > {/* Pinned highlighted rows */}
    Known markers
    {pinnedRows.length === 0 && ( )} {pinnedRows.map((r) => { const palette = colorForSet(r.set_name); const label = r.set_name ?? "reference"; return ( ); })}
    Source Symbol Gene code Rank / N Percentile {metricLabel}
    No reference markers configured for this (dataset, target).
    {label} {r.symbol} {r.opaque_id} {r.rank.toLocaleString()} / {diag.n_genes.toLocaleString()} {rankPercentile(r.rank, diag.n_genes)} {fmtFit(r.score, 3)}

    A recovery diagnostic — where each known marker{" "} (p16, cell_cycle for HNSC; MMR, immune for CRC) ranks as a single-gene separator. The winning program (its genes, its group score, its confound survival) lives in the Result panel above.

    {/* Raw-anonymous-ranking — collapsed disclosure */}

    The genetic-programming search isn’t an exhaustive top-1 ranker — it’s stochastic. The diagnostic ranking scores all{" "}~{diag.n_genes.toLocaleString()} genes and sorts them; genetic programming does not. It samples programs, mutates, recombines, and keeps what survives tournaments — exploring only a subset of the space. So it finds an excellent solution, not provably the best one: it may never have tried the rank-1 gene, or tried it in a generation that got out-competed, and settled into a neighbouring solution. Evolution finds a very good answer, not the global maximum.

    ); } // Collapsed disclosure for the raw opaque-ID ranking. Only purpose // is to demonstrate the ranking IS computed blind — the analytic // content lives in the highlighted table + the rank chart. function RawRankingDisclosure({ diag, search, setSearch, winnerOpaqueSet, winnerSymbolByOpaque, referenceByOpaque, listRows, trimmedSearch, topN, metricLabel, }: { diag: FullRankDiagnostic; search: string; setSearch: (s: string) => void; winnerOpaqueSet: Set; winnerSymbolByOpaque: Map; referenceByOpaque: Map; listRows: FullRankDiagnostic["ranks"]; trimmedSearch: string; topN: number; metricLabel: string; }) { const [open, setOpen] = useState(false); const panelId = useId(); return (
    {open && (

    The ranking above is computed only on the anonymous gene codes. The full list below stays anonymous on purpose — revealing every name would defeat the whole blind-search point. This view is just to show that the ranking really is blind.

    Full ranking (browse by gene code) setSearch(e.target.value)} className="ml-auto w-56 rounded-md border border-border bg-white px-2 py-1 text-xs focus:border-accent focus:outline-none" />
    {listRows.map((r) => { const isWinner = winnerOpaqueSet.has(r.opaque_id); const refMark = referenceByOpaque.get(r.opaque_id); const palette = isWinner ? RANK_COLORS.winner : refMark ? colorForSet(refMark.set_name) : null; return ( ); })} {listRows.length === 0 && ( )}
    Rank Gene code {metricLabel}
    {r.rank.toLocaleString()} {r.opaque_id} {isWinner && ( ← winner ( {winnerSymbolByOpaque.get(r.opaque_id) ?? "…"}) )} {!isWinner && refMark && ( ← {refMark.set_name}: {refMark.symbol} )} {fmtFit(r.score, 3)}
    No matches for “{search}”.
    {trimmedSearch.length === 0 && (

    Showing the top {topN} of {diag.n_genes.toLocaleString()}. Use the search box to find any gene code.

    )}
    )}
    ); } // Where the winner and known markers rank (1 = best single-gene // separator, log scale). Log-rank axis spreads the top cluster out; // each highlighted gene is a labelled dot, coloured by its source // (winner / p16 / cell_cycle, or winner / MMR / immune for CRC). // Labels stagger vertically with leader lines so they don't overlap. function RankTrack({ diag, winnerOpaqueIds, winnerReveal, }: { diag: FullRankDiagnostic; winnerOpaqueIds: string[]; winnerReveal: RevealedWinnerRow[]; }) { const W = 760; const padL = 56; const padR = 40; const padTop = 30; const padBot = 40; const innerW = W - padL - padR; const labelByOpaque = new Map( winnerReveal.map((w) => [w.opaque_id, w.symbol]), ); const rankByOpaque = new Map(diag.ranks.map((r) => [r.opaque_id, r.rank])); // Collect every highlighted point (winner + reference) with its // colour palette and source label. type Mark = { key: string; symbol: string; rank: number; palette: RankPalette; kind: "winner" | "reference"; set_name?: string; }; const marks: Mark[] = []; for (const id of winnerOpaqueIds) { const rank = rankByOpaque.get(id); if (rank == null) continue; marks.push({ key: `w-${id}`, symbol: labelByOpaque.get(id) ?? "…", rank, palette: RANK_COLORS.winner, kind: "winner", }); } const winnerOpaqueSet = new Set(winnerOpaqueIds); for (const m of diag.reference_marks) { if (winnerOpaqueSet.has(m.opaque_id)) continue; marks.push({ key: `r-${m.opaque_id}`, symbol: m.symbol, rank: m.rank, palette: colorForSet(m.set_name), kind: "reference", set_name: m.set_name, }); } // Sort by rank ascending so labels stagger predictably left → right. marks.sort((a, b) => a.rank - b.rank); // Log scale: x = padL + (log10(rank) / log10(N)) * innerW. const logN = Math.log10(Math.max(2, diag.n_genes)); function rankToX(r: number): number { const lr = Math.log10(Math.max(1, r)); return padL + (lr / logN) * innerW; } // Gridline ticks at 1, 10, 100, 1k, 10k, N (skipping ones too close // to N). const tickRanks: number[] = [1, 10, 100, 1000, 10000].filter( (t) => t < diag.n_genes, ); tickRanks.push(diag.n_genes); const tickLabels: Record = { 1: "1", 10: "10", 100: "100", 1000: "1k", 10000: "10k", }; tickLabels[diag.n_genes] = diag.n_genes.toLocaleString(); // Collision-free label placement. Each label is `textAnchor="middle"` // around its dot's x position, so its bounding box spans // [x - w/2, x + w/2] where w ≈ text.length × fontSize × 0.6. // Walk marks left-to-right (they're already sorted by rank) and // greedily place each label in the LOWEST row whose previous // label's right edge + a min gap is ≤ this label's left edge; if // no row fits, open a new one. No fixed cap — the SVG height // expands so nothing clips. With this in place the MCM / // AURKB / CCNB1 clusters render cleanly even when several genes // share nearby ranks. const LABEL_FONT_SIZE = 10; const LABEL_CHAR_W = LABEL_FONT_SIZE * 0.6; const LABEL_GAP_PX = 6; const ROW_GAP = 14; const rowLastRight: number[] = []; const rowOfLabel: number[] = new Array(marks.length).fill(0); marks.forEach((m, i) => { const x = rankToX(m.rank); const w = Math.max(LABEL_FONT_SIZE, m.symbol.length * LABEL_CHAR_W); const left = x - w / 2; const right = x + w / 2; let chosen = -1; for (let r = 0; r < rowLastRight.length; r++) { if (rowLastRight[r] + LABEL_GAP_PX <= left) { chosen = r; break; } } if (chosen === -1) { chosen = rowLastRight.length; rowLastRight.push(right); } else { rowLastRight[chosen] = right; } rowOfLabel[i] = chosen; }); const numRows = Math.max(1, rowLastRight.length); // Labels stack from padTop downward; the axis sits below the // stack with a small breathing gap so leader lines from the // top-row labels are clearly readable. const LABEL_TO_AXIS_GAP = 24; const labelStackH = numRows * ROW_GAP; const axisY = padTop + labelStackH + LABEL_TO_AXIS_GAP; const H = axisY + padBot; // Convert row index → label baseline y (one row below the top). const labelY: number[] = rowOfLabel.map( (r) => padTop + (r + 1) * ROW_GAP, ); const showWinner = winnerOpaqueIds.length > 0; const legendItems = [ ...(showWinner ? [{ label: "winner", palette: RANK_COLORS.winner }] : []), ...Array.from( new Set(diag.reference_marks.map((m) => m.set_name)), ).map((name) => ({ label: name, palette: colorForSet(name) })), ]; return (

    {showWinner ? "Where the winner and known markers rank" : "Where the known markers rank"} (1 = best single-gene separator)

    {legendItems.map((it) => ( {it.label} ))}
    {/* gridlines + tick labels */} {tickRanks.map((t) => { const x = rankToX(t); return ( {tickLabels[t]} ); })} {/* axis line */} best single-gene separator → ← worst {/* highlighted points: lollipop with leader line + dot + label */} {marks.map((m, i) => { const x = rankToX(m.rank); const ly = labelY[i]; return ( {m.symbol} ); })}

    Each dot is one known-marker gene at its single-gene rank on the engine’s training split. Log axis stretches the top so a recoverable marker reads cleanly; weak ones sit at the right. A marker near rank 1 means the canonical biology is findable from expression; a marker far down the axis means it only helps in combination.

    ); } function Evaluator({ geneIds, fitnessLabel, dataset, target, onRevealed, }: { geneIds: string[]; fitnessLabel: string; dataset: DatasetId; target: Target; onRevealed?: (r: EvaluateResponse) => void; }) { const refKeys = DATASET_REGISTRY[dataset].refSetKeys; const [ref, setRef] = useState(refKeys[0]); // Switching cancers swaps the available reference sets — keep `ref` // valid for the active dataset. useEffect(() => { if (!refKeys.includes(ref)) { setRef(refKeys[0]); } }, [refKeys, ref]); // ``data`` is the raw last /evaluate response. We re-derive overlap // locally against the toggled reference set, so switching MMR ↔ immune // is instant and never needs another API round-trip. const [data, setData] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); // TMB-rank diagnostic lifted from TMBRankPanel so its data also drives // the MLH1 success callout and the OverlapSummary's muted secondary // sentence. One fetch, three consumers. Colorectal-only. const [diagnostic, setDiagnostic] = useState(null); const [diagError, setDiagError] = useState(null); const [diagLoading, setDiagLoading] = useState(false); useEffect(() => { if (dataset !== "coadread" || target !== "tmb") { setDiagnostic(null); setDiagError(null); setDiagLoading(false); return; } let cancelled = false; setDiagLoading(true); setDiagError(null); getTMBRankDiagnostic() .then((d) => { if (!cancelled) setDiagnostic(d); }) .catch((e) => { if (!cancelled) setDiagError(e instanceof Error ? e.message : String(e)); }) .finally(() => { if (!cancelled) setDiagLoading(false); }); return () => { cancelled = true; }; }, [dataset, target]); // HPV-rank diagnostic — sibling of TMB-rank, gated to HNSC + HPV. const [hpvDiag, setHpvDiag] = useState(null); const [hpvDiagError, setHpvDiagError] = useState(null); const [hpvDiagLoading, setHpvDiagLoading] = useState(false); useEffect(() => { if (dataset !== "hnsc" || target !== "hpv") { setHpvDiag(null); setHpvDiagError(null); setHpvDiagLoading(false); return; } let cancelled = false; setHpvDiagLoading(true); setHpvDiagError(null); getHPVRankDiagnostic() .then((d) => { if (!cancelled) setHpvDiag(d); }) .catch((e) => { if (!cancelled) setHpvDiagError(e instanceof Error ? e.message : String(e)); }) .finally(() => { if (!cancelled) setHpvDiagLoading(false); }); return () => { cancelled = true; }; }, [dataset, target]); const mlh1Row = useMemo(() => { if (!diagnostic) return undefined; return diagnostic.mmr.find((r) => r.symbol === "MLH1"); }, [diagnostic]); async function go() { setBusy(true); setError(null); try { const r = await postEvaluate({ gene_ids: geneIds, reference_set: ref, dataset, target, }); setData(r); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setBusy(false); } } // Derive matches locally so the toggle always changes something — // even when overlap is 0 the visible reference list, the count line, // and the table rows all update. const effectiveData = useMemo(() => { if (!data) return null; const datasetSets = REFERENCE_SETS_BY_DATASET[dataset] as Record< string, readonly string[] >; const memberSet = new Set(datasetSets[ref] ?? []); const revealed = data.revealed.map((row) => ({ ...row, matched: memberSet.has(row.symbol), })); return { revealed, overlap_count: revealed.filter((r) => r.matched).length, reference_set: ref, }; }, [data, ref, dataset]); // Push the re-derived matches up to Lab so the ProgramGraph's Select // nodes re-tint the matched gene pills on toggle as well. useEffect(() => { if (effectiveData && onRevealed) onRevealed(effectiveData); }, [effectiveData, onRevealed]); const sortedRows = useMemo(() => { if (!effectiveData) return null; return [...effectiveData.revealed].sort( (a, b) => Number(b.matched) - Number(a.matched), ); }, [effectiveData]); const matchedSymbols = useMemo( () => effectiveData?.revealed.filter((r) => r.matched).map((r) => r.symbol) ?? [], [effectiveData], ); return (
    {target !== "none" && (
    reference set {refKeys.map((r) => ( ))}
    )}
    objective: {fitnessLabel}
    {target !== "none" && (
    Checking the winning genes against this set:{" "} {( REFERENCE_SETS_BY_DATASET[dataset] as Record )[ref]?.join(", ") ?? ""}
    )} {target === "none" && (

    Unsupervised run — no reference set; the post-hoc alignment (above, in Result) shows how the discovered split lines up with the named label this cohort carries.

    )} {error && (

    {error}

    )} {dataset === "coadread" && target === "tmb" && mlh1Row?.present && ( )} {effectiveData && sortedRows && (
    {target !== "none" && (
    )}
    Revealed genes
    {/* Single-gene rank column is populated only for (HNSC,HPV) and (coadread,TMB) — the (dataset, target) pairs with a single-gene diagnostic. Computed server-side via /evaluate (airgap-clean: reveal already happens there). */} {(() => { const showRank = sortedRows.some((r) => r.rank != null); return (
    {target !== "none" && ( )} {showRank && ( )} {sortedRows.map((row) => ( {target !== "none" && ( )} {showRank && ( )} ))}
    Gene code SymbolMatched Single-gene rank
    {row.id} {row.symbol} {row.matched ? ( ) : ( )} {row.rank != null && row.total != null ? ( <> {row.rank} / {row.total.toLocaleString()} {row.single_gene_metric != null && ( {row.metric_kind === "auroc" ? `AUROC ${fmtFit(row.single_gene_metric, 3)}` : `ρ ${fmtFit(row.single_gene_metric, 3)}`} )} ) : ( )}
    ); })()}
    )} {dataset === "coadread" && target === "tmb" && ( )} {dataset === "hnsc" && target === "hpv" && ( )}
    ); } // Accent SUCCESS callout for the TMB objective: MLH1's rank on the TMB // negative-association axis. Surfaces what's already in the diagnostic // data, so the "0 of N winning genes in MMR" line stops reading as a // flat failure. function MLH1SuccessCallout({ mlh1, totalGenes, }: { mlh1: GeneRankRow; totalGenes: number; }) { if (!mlh1.present || mlh1.rank == null) return null; const pct = Math.round((mlh1.rank / totalGenes) * 1000) / 10; return (

    Blind, MLH1 ranks{" "} {mlh1.rank} / {totalGenes.toLocaleString()} {" "} (top {pct}%) on the TMB objective — the causal gene surfaced near the top without ever seeing gene names.

    ); } // TMB-rank diagnostic: where each MMR / IMMUNE gene lands when every // gene is ranked by signed Spearman with TMB on the engine's TMB cohort. // "rank near 1 = findable; high rank = the signal isn't in the // expression data." Data is fetched by the parent Evaluator and passed // in so the success callout and OverlapSummary share the same response. function TMBRankPanel({ data, loading, error, }: { data: TMBRankDiagnostic | null; loading: boolean; error: string | null; }) { return (
    Reference-gene diagnostic

    Where each known gene sits on the engine's TMB target — rank near 1 = findable; high rank = the signal isn't in the expression data.

    {loading && (

    computing ranks…

    )} {error && (

    {error}

    )} {data && (

    {data.n_samples} samples · {data.n_genes.toLocaleString()} genes ranked.

    Top {data.top_negative.length} most-negatively-correlated genes

    What outranks MLH1 — every gene above MLH1's rank on the TMB axis.

    {data.top_negative.map((r) => ( ))}
    Symbol Correlation Rank / N
    {r.symbol} {r.corr == null ? "—" : r.corr.toFixed(4)} {r.rank == null ? "—" : `${r.rank} / ${data.n_genes}`}
    )}
    ); } // HNSC-side mirror of . Reads /diagnostic/hpv-rank: per- // gene single-gene HPV+/HPV− AUROC on the engine's TRAIN split. Shows // CDKN2A (p16), each cell-cycle / E2F-target gene, and the top single- // gene separators. Caption matches the prompt verbatim. function HPVRankPanel({ data, loading, error, }: { data: HPVRankDiagnostic | null; loading: boolean; error: string | null; }) { return (
    Reference-gene diagnostic

    Where each known HPV marker sits as a single-gene HPV+/HPV− separator — rank near 1 = recoverable; high rank = out-competed.

    {loading && (

    computing ranks…

    )} {error && (

    {error}

    )} {data && (

    {data.n_samples}{" "} train samples (HPV+ {data.n_pos}{" "} / HPV− {data.n_neg}) · {data.n_genes.toLocaleString()} genes ranked.

    Top {data.top_separators.length} single-gene HPV+/HPV− separators

    What out-competes CDKN2A as a single feature on this cohort.

    {data.top_separators.map((r) => ( ))}
    Symbol AUROC Rank / N
    {r.symbol} {r.corr == null ? "—" : r.corr.toFixed(4)} {r.rank == null ? "—" : `${r.rank} / ${data.n_genes}`}
    )}
    ); } // Synergy = how much the group's combined held-out beats its best // single gene's metric. Captures the additive lift the GROUP gives // beyond its strongest constituent — high synergy = a real group // effect, low / negative = a "best gene + passengers" pattern. // Lifted to module scope so both the merged Groups table and the // landscape scatter share one source of truth. function synergyOf(m: RankedModule): number | null { if (m.combined_holdout == null || !Number.isFinite(m.combined_holdout)) { return null; } let best: number | null = null; for (const g of m.per_gene) { const v = g.single_gene_metric; if (v == null || !Number.isFinite(v)) continue; if (best == null || v > best) best = v; } if (best == null) return null; return m.combined_holdout - best; } // Category buckets a module by its ref_sets membership. Single-marker // sets (p16 / immune) win over broader proliferation sets // (cell_cycle / MMR) so a module with both gets the more specific // colour. "other" = the faint grey background dot. type ModuleCategory = "winner" | "p16" | "immune" | "cell_cycle" | "MMR" | "other"; function categoriseModule( m: RankedModule, winnerSet: Set, ): ModuleCategory { // Winner takes priority over reference-set membership so the // ringed dot is visible even if the winner happens to overlap // a known set. if (m.gene_ids.length === winnerSet.size) { let ok = true; for (const g of m.gene_ids) if (!winnerSet.has(g)) { ok = false; break; } if (ok) return "winner"; } if (m.ref_sets.includes("p16")) return "p16"; if (m.ref_sets.includes("immune")) return "immune"; if (m.ref_sets.includes("cell_cycle")) return "cell_cycle"; if (m.ref_sets.includes("MMR")) return "MMR"; return "other"; } // Canvas-based landscape scatter — ~6k dots, so SVG is too heavy. // Renders the dots on and the chrome (axes, gridlines, // quadrant guides, labels, legend) as an absolutely-positioned SVG // overlay sharing the same plot rect. function FitnessSynergyScatter({ modules, winnerGeneIds, dataset, }: { modules: RankedModule[]; winnerGeneIds: string[]; dataset: DatasetId; }) { const wrapperRef = useRef(null); const canvasRef = useRef(null); const [width, setWidth] = useState(720); const HEIGHT = 320; const PAD = useMemo( () => ({ top: 28, right: 16, bottom: 44, left: 56 }), [], ); // Hit-test record: screen positions of HIGHLIGHTED dots only // (winner + ref-set members). Populated when the canvas paints // and consulted by the mousemove handler. The ~6k background dots // are deliberately not in here — they're never revealed and would // be a waste to hit-test. type Hit = { cx: number; cy: number; cat: ModuleCategory; m: RankedModule }; const hitsRef = useRef([]); const [hover, setHover] = useState<{ cat: ModuleCategory; m: RankedModule; cx: number; cy: number; } | null>(null); // "Hide failed-site groups" toggle: when on, highlighted dots whose // group did NOT survive the oropharynx-subgroup check are dropped // from the plot AND from the hit-test. Background dots are // unaffected (they're not site-tested anyway). const [hideFailedSite, setHideFailedSite] = useState(false); // Track the wrapper width so the canvas + overlay stay sized to // the column the panel renders into. useEffect(() => { const el = wrapperRef.current; if (!el) return; function update() { if (!el) return; setWidth(Math.max(320, el.clientWidth)); } update(); const ro = new ResizeObserver(update); ro.observe(el); return () => ro.disconnect(); }, []); // Project + categorise once per `modules`. const winnerSet = useMemo>( () => new Set(winnerGeneIds), [winnerGeneIds], ); const points = useMemo(() => { type P = { x: number; y: number; cat: ModuleCategory; m: RankedModule; }; const out: P[] = []; for (const m of modules) { const x = m.gp_fitness; const y = synergyOf(m); if (x == null || !Number.isFinite(x)) continue; if (y == null || !Number.isFinite(y)) continue; out.push({ x, y, cat: categoriseModule(m, winnerSet), m }); } return out; }, [modules, winnerSet]); // Bounded reveal: highlighted groups only (winner + ref-set members). // A few dozen modules × a few genes ≈ ~100-200 ids — fine for a // single batched /reveal. Background dots are deliberately left // opaque; revealing all of them would defeat the airgap. const highlightedGeneIds = useMemo(() => { const ids = new Set(); for (const p of points) { if (p.cat === "other") continue; for (const g of p.m.gene_ids) ids.add(g); } return Array.from(ids); }, [points]); const [highlightSymbols, setHighlightSymbols] = useState>( {}, ); useEffect(() => { if (highlightedGeneIds.length === 0) return; const needed = highlightedGeneIds.filter((g) => !(g in highlightSymbols)); if (needed.length === 0) return; let cancelled = false; postReveal(needed) .then((r) => { if (cancelled) return; const additions: Record = {}; needed.forEach((gid, i) => { additions[gid] = r.symbols[i] ?? ""; }); setHighlightSymbols((prev) => ({ ...prev, ...additions })); }) .catch(() => { // Non-fatal — tooltips fall back to opaque IDs. }); return () => { cancelled = true; }; // Key on the joined opaque-id list so re-renders without a real // change in highlighted modules don't re-fetch. // eslint-disable-next-line react-hooks/exhaustive-deps }, [highlightedGeneIds.join("·")]); const stats = useMemo(() => { if (points.length === 0) { return null; } let xMin = Infinity, xMax = -Infinity; let yMin = Infinity, yMax = -Infinity; const xs: number[] = []; for (const p of points) { if (p.x < xMin) xMin = p.x; if (p.x > xMax) xMax = p.x; if (p.y < yMin) yMin = p.y; if (p.y > yMax) yMax = p.y; xs.push(p.x); } // Pad ranges a bit; force y range to span 0 so the synergy=0 // guide line is always visible. const xPad = Math.max(0.005, (xMax - xMin) * 0.05); const yPad = Math.max(0.005, (yMax - yMin) * 0.08); const x0 = xMin - xPad; const x1 = xMax + xPad; const y0 = Math.min(yMin - yPad, 0); const y1 = Math.max(yMax + yPad, 0); // Median GP fitness for the vertical quadrant guide. const sorted = xs.slice().sort((a, b) => a - b); const mid = sorted.length === 0 ? 0 : sorted.length % 2 === 1 ? sorted[(sorted.length - 1) >> 1] : 0.5 * (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]); return { x0, x1, y0, y1, medianX: mid }; }, [points]); const plotW = Math.max(1, width - PAD.left - PAD.right); const plotH = Math.max(1, HEIGHT - PAD.top - PAD.bottom); function project(x: number, y: number, s: NonNullable) { const px = PAD.left + ((x - s.x0) / (s.x1 - s.x0)) * plotW; const py = PAD.top + plotH - ((y - s.y0) / (s.y1 - s.y0)) * plotH; return [px, py] as const; } // Paint the canvas whenever data / dimensions change. useEffect(() => { const canvas = canvasRef.current; if (!canvas || !stats) return; const s = stats; // narrowed for the closure const dpr = (typeof window !== "undefined" && window.devicePixelRatio) || 1; canvas.width = Math.round(width * dpr); canvas.height = Math.round(HEIGHT * dpr); canvas.style.width = `${width}px`; canvas.style.height = `${HEIGHT}px`; const ctx = canvas.getContext("2d"); if (!ctx) return; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, width, HEIGHT); // Faint background dots first (the "other" group), then the // highlighted ones on top, then the winner last (drawn as a // ring) so nothing occludes it. const layers: Record = { other: [], cell_cycle: [], MMR: [], p16: [], immune: [], winner: [], }; for (const p of points) layers[p.cat].push({ x: p.x, y: p.y, m: p.m }); // Reset the hit-test array on every repaint; the dots' screen // positions move with the canvas size. const hits: Hit[] = []; // Background layer: faint grey dots, no site encoding (the data // doesn't apply — "other" modules aren't reference-tagged anyway). function paintBackground( pts: { x: number; y: number; m: RankedModule }[], fill: string, radius: number, alpha: number, ) { ctx!.save(); ctx!.globalAlpha = alpha; ctx!.fillStyle = fill; for (const pt of pts) { const [px, py] = project(pt.x, pt.y, s); ctx!.beginPath(); ctx!.arc(px, py, radius, 0, Math.PI * 2); ctx!.fill(); } ctx!.restore(); } // Highlighted layer: encode site-survival on the FILL. Solid = // survives the oropharynx check (or n too small to test); hollow // (white fill + coloured stroke) = failed it — signal is at least // partly location. Hide failed dots when the user has flipped the // toggle; they're also removed from the hit-test array. function paintHighlighted( pts: { x: number; y: number; m: RankedModule }[], cat: ModuleCategory, fill: string, stroke: string, radius: number, alpha: number, ) { ctx!.save(); ctx!.globalAlpha = alpha; for (const pt of pts) { const failsSite = pt.m.survives_site === false; if (hideFailedSite && failsSite) continue; const [px, py] = project(pt.x, pt.y, s); ctx!.beginPath(); ctx!.arc(px, py, radius, 0, Math.PI * 2); ctx!.fillStyle = failsSite ? "#FFFFFF" : fill; ctx!.fill(); ctx!.strokeStyle = stroke; ctx!.lineWidth = failsSite ? 1.6 : 1.2; ctx!.stroke(); hits.push({ cx: px, cy: py, cat, m: pt.m }); } ctx!.restore(); } const palP16 = RANK_COLORS.p16; const palImm = RANK_COLORS.immune; const palCC = RANK_COLORS.cell_cycle; const palMMR = RANK_COLORS.MMR; const palWin = RANK_COLORS.winner; paintBackground(layers.other, "#A8AEB5", 1.4, 0.18); paintHighlighted(layers.cell_cycle, "cell_cycle", palCC.fill, palCC.stroke, 3.4, 0.85); paintHighlighted(layers.MMR, "MMR", palMMR.fill, palMMR.stroke, 3.4, 0.85); paintHighlighted(layers.p16, "p16", palP16.fill, palP16.stroke, 4.0, 0.95); paintHighlighted(layers.immune, "immune", palImm.fill, palImm.stroke, 4.0, 0.95); // Winner — a teal ring at radius 7.5 with a coloured centre at // radius 4.5. If the winner's group failed the site check (rare // — the engine usually picks survives_site=true), hollow the // centre too (white fill) so the encoding is consistent. for (const pt of layers.winner) { const failsSite = pt.m.survives_site === false; if (hideFailedSite && failsSite) continue; const [px, py] = project(pt.x, pt.y, s); ctx.save(); ctx.fillStyle = failsSite ? "#FFFFFF" : palWin.fill; ctx.beginPath(); ctx.arc(px, py, 4.5, 0, Math.PI * 2); ctx.fill(); ctx.lineWidth = 2; ctx.strokeStyle = palWin.stroke; ctx.beginPath(); ctx.arc(px, py, 4.5, 0, Math.PI * 2); ctx.stroke(); ctx.beginPath(); ctx.arc(px, py, 7.5, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); hits.push({ cx: px, cy: py, cat: "winner", m: pt.m }); } hitsRef.current = hits; // eslint-disable-next-line react-hooks/exhaustive-deps }, [points, stats, width, plotW, plotH, hideFailedSite]); if (!stats || points.length === 0) { return null; } // Build axis ticks: 4 evenly-spaced ticks on each axis, formatted // to 2 decimals. function ticks(a: number, b: number, n: number): number[] { const out: number[] = []; for (let i = 0; i <= n; i++) out.push(a + (i / n) * (b - a)); return out; } const xTicks = ticks(stats.x0, stats.x1, 4); const yTicks = ticks(stats.y0, stats.y1, 4); const medianXpx = project(stats.medianX, 0, stats)[0]; const zeroYpx = project(0, 0, stats)[1]; // Quadrant captions — placed inside each quadrant near a corner. const captions = [ { x: PAD.left + 6, y: PAD.top + 14, text: "real teamwork", sub: "(unexplored by the engine)", anchor: "start" as const, }, { x: PAD.left + plotW - 6, y: PAD.top + 14, text: "ideal — usually empty", sub: "(engine + teamwork)", anchor: "end" as const, }, { x: PAD.left + 6, y: PAD.top + plotH - 8, text: "weak / junk", sub: "", anchor: "start" as const, }, { x: PAD.left + plotW - 6, y: PAD.top + plotH - 8, text: "engine's lone-gene detectors", sub: "", anchor: "end" as const, }, ]; // Per-dataset legend entries (single-marker first, broader second). const legend: { label: string; pal: RankPalette; ring?: boolean }[] = [ { label: "all explored groups", pal: { stroke: "#7B838C", fill: "#A8AEB5", bg: "#FFFFFF" } }, { label: "winner & picks", pal: RANK_COLORS.winner, ring: true }, ]; if (dataset === "hnsc") { legend.push({ label: "p16 (CDKN2A)", pal: RANK_COLORS.p16 }); legend.push({ label: "cell_cycle", pal: RANK_COLORS.cell_cycle }); } else { legend.push({ label: "immune", pal: RANK_COLORS.immune }); legend.push({ label: "MMR", pal: RANK_COLORS.MMR }); } return (
    Every group the engine explored

    Every group the engine explored. Left–right = how much the engine liked it; up–down = how much the group beats its best single gene. Highlighted dots show where the known HPV genes fall. Solid{" "} dots survive the site check;{" "} hollow{" "} dots fail it (their signal is partly location).

    { const el = wrapperRef.current; if (!el) return; const rect = el.getBoundingClientRect(); const cx = e.clientX - rect.left; const cy = e.clientY - rect.top; let nearest: Hit | null = null; let best = 8 * 8; // pick within ~8px for (const h of hitsRef.current) { const dx = h.cx - cx; const dy = h.cy - cy; const d2 = dx * dx + dy * dy; if (d2 < best) { best = d2; nearest = h; } } if (nearest) { setHover({ cat: nearest.cat, m: nearest.m, cx: nearest.cx, cy: nearest.cy, }); } else if (hover) { setHover(null); } }} onMouseLeave={() => setHover(null)} > {/* Plot frame */} {/* Y gridlines + tick labels */} {yTicks.map((t, i) => { const [, py] = project(stats.x0, t, stats); return ( {t.toFixed(2)} ); })} {/* X gridlines + tick labels */} {xTicks.map((t, i) => { const [px] = project(t, stats.y0, stats); return ( {t.toFixed(2)} ); })} {/* Quadrant guides: median GP fitness (vertical) + synergy=0 (horizontal). */} median genetic-programming fitness synergy = 0 {/* Quadrant captions */} {captions.map((c, i) => ( {c.text} {c.sub && ( {c.sub} )} ))} {/* Axis titles */} Genetic-programming fitness — what the engine preferred → Synergy — teamwork beyond best gene ↑ {hover && ( )}
    {/* Legend */}
    {legend.map((it) => ( {it.ring ? ( ) : ( )} {it.label} ))} hollow = fails site check
    ); } // Hover tooltip for the fitness × synergy scatter. Renders next to // the hovered HIGHLIGHTED dot — never for the faint background dots, // since those aren't revealed. Clamps so it stays inside the chart // rect even when the dot is near the edge. function ScatterTooltip({ hover, symbolByOpaque, plotW, plotH, pad, width, height, }: { hover: { cat: ModuleCategory; m: RankedModule; cx: number; cy: number; }; symbolByOpaque: Record; plotW: number; plotH: number; pad: { top: number; right: number; bottom: number; left: number }; width: number; height: number; }) { const { m, cat, cx, cy } = hover; // Palette per category. "winner" uses the teal accent; ref-set // categories reuse the existing RANK_COLORS map; "other" should // not appear (we don't hit-test it). const pal = cat === "winner" ? RANK_COLORS.winner : cat === "p16" ? RANK_COLORS.p16 : cat === "immune" ? RANK_COLORS.immune : cat === "cell_cycle" ? RANK_COLORS.cell_cycle : RANK_COLORS.MMR; const label = cat === "winner" ? "winner" : (cat as string); // Tooltip dimensions are approximate; we clamp the corner so the // box always stays inside the chart rect. Position 12px to the // right of the dot, biased up; flip if it would overflow. const TT_W = 220; const TT_H = 100; let left = cx + 14; let top = cy - 14 - TT_H; const leftMax = pad.left + plotW - TT_W - 4; if (left > leftMax) left = Math.max(pad.left + 4, cx - TT_W - 14); if (top < pad.top + 4) top = cy + 14; if (top + TT_H > pad.top + plotH + 4) top = Math.max(pad.top + 4, top - TT_H); // Final clamp to the wrapper bounds. left = Math.min(Math.max(left, 4), width - TT_W - 4); top = Math.min(Math.max(top, 4), height - TT_H - 4); const syn = synergyOf(m); const symbols = m.gene_ids.map((g) => { const s = symbolByOpaque[g]; return s && s.length > 0 ? s : g; }); const allRevealed = m.gene_ids.every( (g) => g in symbolByOpaque && symbolByOpaque[g].length > 0, ); return (
    {label} {m.size} gene{m.size === 1 ? "" : "s"}
    {allRevealed ? symbols.join(", ") : "(revealing…)"}
    Genetic-programming fitness
    {fmtFit(m.gp_fitness ?? null, 3)}
    Combined AUROC
    {fmtFit(m.combined_holdout, 3)}
    Coherence
    {fmtFit(m.coherence, 3)}
    Synergy
    {fmtFit(syn, 3)}
    ); } // Ranked-modules panel: shown for coherence-on supervised runs. Each // row is a coordinated gene module the engine explored, scored by // combined held-out AUROC on the SAME train/test split the run used. // Symbols are auto-revealed for the visible page in one batched // /reveal call — bounded by page size, never the whole map. const MODULE_PAGE_SIZE = 25; function ModuleRankingPanel({ runId, dataset, target, winnerGeneIds, }: { runId: string; dataset: DatasetId; target: Target; winnerGeneIds: string[]; }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); type SortKey = "gp_fitness" | "combined" | "coherence" | "synergy"; type SortDir = "asc" | "desc"; // Default sort: the engine's own preference order (what the search // actually picked). The other three are after-the-fact re-score // lenses. const [sortKey, setSortKey] = useState("gp_fitness"); const [sortDir, setSortDir] = useState("desc"); const [page, setPage] = useState(0); // Symbol cache keyed by opaque ID — accumulates across pages so // re-visiting a page is free. const [symbolByOpaque, setSymbolByOpaque] = useState>( {}, ); const [expandedIx, setExpandedIx] = useState(null); useEffect(() => { let cancelled = false; setLoading(true); setError(null); setData(null); setSymbolByOpaque({}); setExpandedIx(null); setPage(0); getRunModules(runId) .then((d) => { if (!cancelled) setData(d); }) .catch((e) => { if (!cancelled) setError(e instanceof Error ? e.message : String(e)); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [runId]); // Synergy / set categorisation are shared with the scatter; lift to // top-level helpers (defined just above this file's module-panel // section) so both views share one source of truth. const sortedModules = useMemo(() => { if (!data) return []; const xs = [...data.modules]; const valueOf = (m: RankedModule): number | null | undefined => { switch (sortKey) { case "coherence": return m.coherence; case "gp_fitness": return m.gp_fitness; case "synergy": return synergyOf(m); default: return m.combined_holdout; } }; xs.sort((a, b) => { const va = valueOf(a), vb = valueOf(b); const fa = va == null || !Number.isFinite(va); const fb = vb == null || !Number.isFinite(vb); if (fa && fb) return 0; if (fa) return 1; if (fb) return -1; return sortDir === "asc" ? (va as number) - (vb as number) : (vb as number) - (va as number); }); return xs; // synergyOf depends only on RankedModule's stable per-gene fields, // so we leave it out of the dep list. // eslint-disable-next-line react-hooks/exhaustive-deps }, [data, sortKey, sortDir]); // Re-sort or re-load resets the page + collapses any open row so an // expanded "rank N" row doesn't strand once the underlying ranking // shifts. useEffect(() => { setPage(0); setExpandedIx(null); }, [sortKey, sortDir, data]); // Winner-set equality: unordered set of opaque IDs equal to the // winner's gene_ids. Used to badge that exact module — connects the // group view back to the Result cards. const winnerSet = useMemo>(() => new Set(winnerGeneIds), [ winnerGeneIds, ]); function isWinnerSet(geneIds: string[]): boolean { if (geneIds.length !== winnerSet.size) return false; for (const g of geneIds) if (!winnerSet.has(g)) return false; return true; } const pageStart = page * MODULE_PAGE_SIZE; const pageEnd = Math.min(pageStart + MODULE_PAGE_SIZE, sortedModules.length); const pageModules = sortedModules.slice(pageStart, pageEnd); const nPages = Math.max( 1, Math.ceil(sortedModules.length / MODULE_PAGE_SIZE), ); // Auto-reveal the visible page's symbols in ONE batched /reveal — // bounded to ≤ MODULE_PAGE_SIZE × max_genes_per_set IDs. Skips // already-cached IDs. useEffect(() => { if (pageModules.length === 0) return; const needed = new Set(); for (const m of pageModules) { for (const g of m.gene_ids) { if (!(g in symbolByOpaque)) needed.add(g); } } if (needed.size === 0) return; const ids = Array.from(needed); let cancelled = false; postReveal(ids) .then((r) => { if (cancelled) return; const additions: Record = {}; ids.forEach((gid, i) => { additions[gid] = r.symbols[i] ?? ""; }); setSymbolByOpaque((prev) => ({ ...prev, ...additions })); }) .catch((e) => { // Non-fatal — symbols just stay "(revealing…)" for this batch. console.error("module page reveal failed", e); }); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [page, sortKey, data]); function toggle(localIx: number) { const globalIx = pageStart + localIx; setExpandedIx(globalIx === expandedIx ? null : globalIx); } return ( {loading && (

    computing module ranks…

    )} {error && (

    {error}

    )} {data && ( <>
    {data.n_modules}{" "} distinct modules · train{" "} {data.n_train}, test{" "} {data.n_test}
    sort: {( [ ["gp_fitness", "Genetic-programming fitness"], ["combined", "Combined AUROC"], ["coherence", "Coherence"], ["synergy", "Synergy"], ] as [SortKey, string][] ).map(([k, label]) => ( ))}
    {(data.subgroups?.site || data.subgroups?.purity) && (
    Survival flags shown for the held-out subgroups — {data.subgroups?.site && ( <> {" site "} ({data.subgroups.site.kind}, n={data.subgroups.site.n}) )} {data.subgroups?.site && data.subgroups?.purity && " · "} {data.subgroups?.purity && ( <> {" purity "} ({data.subgroups.purity.kind}, n={data.subgroups.purity.n}) )} .
    )}
    {(data.subgroups?.site || data.subgroups?.purity) && ( )} {pageModules.map((m, localIx) => { const globalIx = pageStart + localIx; const open = expandedIx === globalIx; // Tint by the most-specific matched set: single-marker // sets (p16 / immune — deep gold) win over broader // proliferation sets (cell_cycle / MMR — muted amber). const primarySet = m.ref_sets.find((s) => s === "p16" || s === "immune") ?? m.ref_sets.find( (s) => s === "cell_cycle" || s === "MMR", ) ?? m.ref_sets[0]; const rowBg = primarySet ? colorForSet(primarySet).bg : undefined; const winner = isWinnerSet(m.gene_ids); return ( toggle(localIx)} aria-expanded={open} > {(data.subgroups?.site || data.subgroups?.purity) && ( )} {open && ( )} ); })}
    # Genetic-programming fitness {data.metric_kind === "auroc" ? "Combined AUROC" : "Combined |ρ|"} Coherence Synergy Size Genes Tags Survives {/* Multi-paragraph copy doesn't fit a hover tooltip — use the rich modal infrastructure so it never clips. */}
    {open ? "▼" : "▶"} {globalIx + 1} {fmtFit(m.gp_fitness ?? null, 3)} {fmtFit(m.combined_holdout, 3)} {fmtFit(m.coherence, 3)} {fmtFit(synergyOf(m), 3)} {m.size}
    {winner && ( winner )} {m.ref_sets.map((s) => { const pal = colorForSet(s); return ( {s} ); })}
    {m.best_program_repr && ( )} {(data.subgroups?.site || data.subgroups?.purity) && ( )}
    )}
    ); } function ModulePager({ page, nPages, pageStart, pageEnd, total, onPage, }: { page: number; nPages: number; pageStart: number; pageEnd: number; total: number; onPage: (p: number) => void; }) { if (nPages <= 1) return null; return (
    {(pageStart + 1).toLocaleString()}–{pageEnd.toLocaleString()} of {total.toLocaleString()}
    page {page + 1} / {nPages}
    ); } function ModuleSymbolList({ geneIds, symbolByOpaque, datasetRefSets, }: { geneIds: string[]; symbolByOpaque: Record; datasetRefSets: Record; }) { const setsBySymbol = useMemo>(() => { const out = new Map(); for (const [name, syms] of Object.entries(datasetRefSets)) { for (const s of syms ?? []) { if (!out.has(s)) out.set(s, name); } } return out; }, [datasetRefSets]); // Truncate long modules inline; expanding the row shows them all. const MAX_INLINE = 8; const head = geneIds.slice(0, MAX_INLINE); const overflow = Math.max(0, geneIds.length - MAX_INLINE); return ( {head.map((gid, i) => { const sym = symbolByOpaque[gid]; const setName = sym ? setsBySymbol.get(sym) : undefined; const pal = setName ? colorForSet(setName) : null; return ( {sym && sym.length > 0 ? sym : sym === "" ? "—" : "(revealing…)"} {i < head.length - 1 ? ", " : null} ); })} {overflow > 0 && ( · +{overflow} )} ); } // Compact ✓ / ✗ / "n too small" chips for a module's site + purity // survival flags. Both are HNSC/HPV-only — for other datasets we don't // render the column at all. function SurvivalChips({ module, subgroups, }: { module: RankedModule; subgroups: ModuleRanking["subgroups"]; }) { // Full-cohort baseline ("pre" in the pre→post pairing). Reused by // every chip so the user sees what each subgroup is being compared to. const pre = module.combined_holdout; function chip( label: string, survives: boolean | null | undefined, post: number | null | undefined, title: string, ) { let bg = "#F4F2EE"; let stroke = "#6E7F8C"; let glyph: string; if (survives === true) { bg = "#E3F2EA"; stroke = "#2F6E4C"; glyph = "✓"; } else if (survives === false) { bg = "#FBE6E6"; stroke = "#A64242"; glyph = "✗"; } else { // n too small / not applicable bg = "#F4F2EE"; stroke = "#9AA0A6"; glyph = "—"; } // pre → post pair. When the subgroup AUROC is null ("n too small") // we drop the arrow + post; the "—" glyph already signals N/A. const preText = pre != null && Number.isFinite(pre) ? fmtFit(pre, 2) : null; const postText = post != null && Number.isFinite(post) ? fmtFit(post, 2) : null; let valueText = ""; if (preText && postText) { const delta = pre! - post!; const deltaText = Number.isFinite(delta) ? ` (${delta >= 0 ? "−" : "+"}${Math.abs(delta).toFixed(2)})` : ""; valueText = ` ${preText} → ${postText}${deltaText}`; } else if (preText && !postText) { valueText = ` ${preText} → —`; } else if (postText && !preText) { valueText = ` ${postText}`; } return ( {label} {glyph}{valueText} ); } return (
    {subgroups?.site && chip( "site", module.survives_site, module.combined_holdout_oropharynx, module.survives_site === null || module.survives_site === undefined ? `Site (oropharynx) subgroup too small to score honestly (n=${module.n_holdout_oropharynx ?? 0}).` : `Module recomputed within the oropharynx held-out subgroup: full-cohort AUROC ${fmtFit(pre, 3)} → subgroup AUROC ${fmtFit(module.combined_holdout_oropharynx, 3)} over n=${module.n_holdout_oropharynx ?? 0} (HPV+ ${module.n_pos_oropharynx ?? 0} / HPV− ${module.n_neg_oropharynx ?? 0}). Survives = within 0.05 of the full-cohort AUROC.`, )} {subgroups?.purity && chip( "purity", module.survives_purity, module.combined_holdout_highpurity, module.survives_purity === null || module.survives_purity === undefined ? `High-purity (bottom-tertile immune proxy) subgroup too small to score honestly (n=${module.n_holdout_highpurity ?? 0}; HPV+ ${module.n_pos_highpurity ?? 0} / HPV− ${module.n_neg_highpurity ?? 0}).` : `Module recomputed within the high-purity (bottom-tertile immune proxy) held-out subgroup: full-cohort AUROC ${fmtFit(pre, 3)} → subgroup AUROC ${fmtFit(module.combined_holdout_highpurity, 3)} over n=${module.n_holdout_highpurity ?? 0} (HPV+ ${module.n_pos_highpurity ?? 0} / HPV− ${module.n_neg_highpurity ?? 0}). Survives = within 0.05 of the full-cohort AUROC.`, )}
    ); } // Expanded-row strip: the stratified AUROCs in numbers next to the // per-gene metrics table, so users can see exactly how far the module // dropped in each subgroup. function StratifiedAUROCStrip({ module, subgroups, metricKind, }: { module: RankedModule; subgroups: ModuleRanking["subgroups"]; metricKind: "auroc" | "spearman"; }) { const label = metricKind === "auroc" ? "AUROC" : "|ρ|"; function row( name: string, value: number | null | undefined, n: number | undefined, n_pos: number | undefined, n_neg: number | undefined, ) { if (n == null && value == null) return null; return (
    {name} {label} {fmtFit(value, 3)} n={n ?? 0} {metricKind === "auroc" && n_pos != null && n_neg != null ? ` (HPV+ ${n_pos} / HPV− ${n_neg})` : ""}
    ); } return (
    Stratified held-out AUROCs (survival check)
    full {label} {fmtFit(module.combined_holdout, 3)}
    {subgroups?.site && row( "oropharynx", module.combined_holdout_oropharynx, module.n_holdout_oropharynx, module.n_pos_oropharynx, module.n_neg_oropharynx, )} {subgroups?.purity && row( "high purity", module.combined_holdout_highpurity, module.n_holdout_highpurity, module.n_pos_highpurity, module.n_neg_highpurity, )}
    ); } // The actual best-fitness program tree for a Groups-table row. // Re-uses the shared renderer (same one the Result // panel uses for the winner) so the visual matches. The module's // genes were already revealed in the table-row expand, so the tree // can show their symbols too — no additional reveal needed. function ModuleBestProgram({ programRepr, outputLabel, symbolByOpaque, }: { programRepr: string; outputLabel: string; symbolByOpaque: Record; }) { // Adapt the flat opaque→symbol map to the {symbol, matched} shape // ProgramGraph expects. `matched` is false here — the reference- // set highlight is for the known-marker recovery diagnostic, not // for a group's own gene tree. const reveal = useMemo< Record >(() => { const out: Record = {}; for (const [opq, sym] of Object.entries(symbolByOpaque)) { if (sym && sym.length > 0) { out[opq] = { symbol: sym, matched: false }; } } return out; }, [symbolByOpaque]); return (
    This group’s best program

    The actual tree of the candidate that earned the Genetic-programming fitness above (argmax over the persisted population for this gene-set).

    program (raw)
    {programRepr}
    ); } function ModuleGeneTable({ module, symbolByOpaque, datasetRefSets, metricKind, }: { module: RankedModule; symbolByOpaque: Record; datasetRefSets: Record; metricKind: "auroc" | "spearman"; }) { // Symbol → ref-set names lookup so each row can be tinted by the // specific set its gene belongs to (p16 deep gold vs cell_cycle // muted amber, etc). const setsBySymbol = useMemo>(() => { const out = new Map(); for (const [name, syms] of Object.entries(datasetRefSets)) { for (const s of syms ?? []) { // First-write-wins; the priority below also normalises this // so p16 (single canonical) outranks cell_cycle for a gene // that's in both (CDKN2A isn't, but a future split could be). if (!out.has(s)) out.set(s, name); } } return out; }, [datasetRefSets]); return (
    {module.per_gene.map((g) => { const sym = symbolByOpaque[g.id]; const setName = sym ? setsBySymbol.get(sym) : undefined; const pal = setName ? colorForSet(setName) : null; return ( ); })}
    Opaque ID Symbol Source {metricKind === "auroc" ? "Single-gene AUROC" : "Single-gene ρ"} Rank / N
    {g.id} {sym && sym.length > 0 ? sym : sym === "" ? "—" : "(revealing…)"} {setName ? ( {setName} ) : ( )} {fmtFit(g.single_gene_metric, 3)} {g.rank != null && g.total != null ? `${g.rank} / ${g.total.toLocaleString()}` : "—"}
    ); } function RankList({ title, subtitle, rows, total, }: { title: string; subtitle: string; rows: GeneRankRow[]; total: number; }) { return (
    {title}

    {subtitle}

      {rows.map((r) => (
    • {r.symbol} {r.present ? ( {r.corr!.toFixed(4)} {" · "} {r.rank} {` / ${total}`} ) : ( not present )}
    • ))}
    ); } function OverlapSummary({ k, n, setName, matchedSymbols, mlh1, totalGenes, }: { k: number; n: number; setName: ReferenceSetKey; matchedSymbols: string[]; mlh1?: GeneRankRow; totalGenes?: number; }) { // For the TMB objective, soften the overlap line: programs rarely // contain MMR genes literally, but the diagnostic shows MLH1 itself // sits near the top of the negative-TMB ranking. const mlh1Tail = mlh1?.present && mlh1.rank != null && totalGenes ? ( Winning programs rarely ARE the MMR genes (many co-silenced genes outrank them) — but MLH1 itself sits in the top{" "} {Math.round((mlh1.rank / totalGenes) * 1000) / 10}%. See the diagnostic below. ) : null; if (k === 0) { return ( No matches — none of the {n} winning gene{n === 1 ? "" : "s"}{" "} {n === 1 ? "is" : "are"} in the{" "} {setName} set. {mlh1Tail} ); } return ( {k} of {n} winning genes{" "} {k === 1 ? "is" : "are"} in the{" "} {setName} set:{" "} {matchedSymbols.join(", ")} {mlh1Tail} ); }