"use client"; import React from "react"; import GenerationsLoopDiagram from "./diagrams/GenerationsLoopDiagram"; import LambdaTradeoffDiagram from "./diagrams/LambdaTradeoffDiagram"; import MaxSetsDiagram from "./diagrams/MaxSetsDiagram"; import PermutationsHistogramDiagram from "./diagrams/PermutationsHistogramDiagram"; import PrefilterFunnelDiagram from "./diagrams/PrefilterFunnelDiagram"; import SeedDiceDiagram from "./diagrams/SeedDiceDiagram"; export type ParamKey = | "generations" | "population" | "genes_per_set" | "max_sets" | "lambda" | "seed" | "permutations" | "prefilter_n" // Objective help keys reuse the same modal infrastructure. | "obj_msi" | "obj_tmb" | "obj_hpv" | "obj_unsupervised" // Misc rich helpers that don't fit the hover tooltip envelope // (multi-paragraph, table-cell-anchored). | "module_survival"; export interface ParamHelpEntry { title: string; short: string; detailed: React.ReactNode; } // ---------- shared bits -------------------------------------------------- function Item({ label, children, }: { label: string; children: React.ReactNode; }) { return (

{label}:{" "} {children}

); } function Caption({ children }: { children: React.ReactNode }) { return (

{children}

); } function Mono({ children }: { children: React.ReactNode }) { return ( {children} ); } function ExampleList({ rows, }: { rows: React.ReactNode[]; }) { return ( ); } // ---------- the registry ------------------------------------------------- export const PARAM_HELP: Record = { generations: { title: "Generations", short: "How many rounds of 'keep the best, breed, mutate' the engine runs. More rounds refine further, but take longer.", detailed: ( <> one “generation” is a single round of the engine’s loop — score every program, keep the best, then breed and mutate them into the next batch. Generations is how many rounds it runs. the engine starts with random programs (mostly poor) and each round nudges the population toward better ones — like selective breeding. more rounds refine the winners further, but with diminishing returns once it has converged. The harder part is usually discovering{" "} the right genes in the first place (driven by Population and mutation), not polishing — so extra generations help less once the curve flattens. 30–60. A 4-step loop: random programs → score each → keep the best → breed & mutate → (back to score). The loop arrow is labelled “× Generations”. ), }, population: { title: "Population", short: "How many candidate programs compete in each round. A 'program' is a small pipeline built by composing the DSL operators (Select → Reduce → Fit) over the gene data. More programs explores more options, but is slower.", detailed: ( <> how many candidate programs compete each round. A “program” is a small pipeline built from the DSL operators —{" "} Select (pick genes) → Reduce{" "} (average them into one score) → Fit (use the score to separate the groups). a bigger population samples more genes and structures each round, so it’s the main lever for discovery — finding the right genes at all. Generations then refine what was found.

Example — three random programs in a population:

average(g04823, g11201, g00917) → score, average(g15522, g02013) − average(g08840, g00231) → score , average(g07788, g13002, g05340) → score, ]} /> more explores more options, but each round is slower. 150–300. ), }, genes_per_set: { title: "Genes / set", short: "The most genes a single score may use. Smaller keeps programs simple and readable.", detailed: ( <> a “set” is the group of genes averaged together into a single score. Genes/set is the maximum size of that group. Select(g05347, g00048, g06271) → average → one score {" "} (uses 3; the cap might be 8). smaller keeps each score simple and readable; larger lets one score blend more genes. Genes/set is the width of each group; Max sets is{" "} how many groups. ), }, max_sets: { title: "Max sets", short: "How many separate gene-scores a program may combine (1 or 2). 2 lets it build a small 'score made of scores'.", detailed: ( <> how many separate gene groups a program may build and then combine (1 or 2). They use different genes — not the same ones reused.

Example (2 sets):

Set 1:{" "} average(g05347, g00048, g06271) → scoreA , <> Set 2: average(g15522, g02013) → scoreB , <> Combine: scoreA − scoreB → final score , ]} /> to capture a contrast — e.g. one group that’s low in MSI-H and one that’s high; the gap between them can separate better than either alone (like “repair activity minus immune activity”). One set can’t express that; two can. If one group is enough, the engine can still use 1. Two short chains feed a Combine(sub) node and produce a final score. ), }, lambda: { title: "λ (size penalty)", short: "How hard the engine is penalised for using more genes. Higher pushes it toward fewer genes (simpler answers).", detailed: ( <> a “price per gene” that discourages bloated programs. Every program is graded on a single number:

fitness = separation − λ × (number of genes)

so the engine ranks by accuracy minus a size tax.

λ is the admission price each gene must beat: with λ = 0.005, a gene is only worth keeping if it adds more than 0.005 of separation. a 3-gene program scores 0.90 → net 0.90 − 0.015 = 0.885. Add a 4th gene that lifts it to 0.903 (only +0.003) → net 0.883, lower, so it’s rejected. A 4th gene that lifts it to 0.91 (+0.01) → net 0.890, higher, so it’s kept. λ = 0 → genes free → bloated, overfit programs. λ small (0.005) → trims useless genes, keeps useful ones. λ large → very lean programs, but may drop useful genes. Raw separation rises and plateaus; net fitness peaks at an intermediate size, and a larger λ shifts the peak to fewer genes. ), }, seed: { title: "Seed", short: "The starting point for the engine's randomness. The same seed reproduces the exact same run; change it to see a different run.", detailed: ( <> the space of possible programs is astronomically large (picking even 8 genes out of 20,000 is ~10²⁹ combinations), so the engine can’t try them all — it explores with randomness: random starting programs, random mutations, random breeding. computers don’t make true randomness; a formula generates a sequence of numbers, each from the previous one. The seed is the{" "} starting number fed into that formula — everything random flows from it. (One common formula:{" "} next = (1664525 × current + 1013904223) mod 2³²; with seed 42 the first value is 1,083,814,273, then mapped onto a gene position.) think of the seed as one fixed list of dice rolls used in order across the whole run — early rolls pick the starting genes, later rolls drive mutations. Same seed → same list → identical run. Different seed → a different list → a different run. because the search has luck in it, run a few seeds (42, 7, 123) and see whether the same genes keep appearing — that tells you a finding is real, not a fluke. The seed is one fixed list of dice rolls; early rolls pick initial genes, later rolls drive mutations. ), }, permutations: { title: "Permutations", short: "How many times we re-run on deliberately scrambled labels to check the result isn't luck. The real result must beat these chance runs (that's the 'permutation p').", detailed: ( <> whether the winning score is real signal or could have come up by luck. The engine searches so hard that some programs separate the groups well by pure coincidence, so a high score alone isn’t proof. shuffle the MSI-H / MSS labels across patients at random — this breaks any real gene↔label link, creating a “no-signal world”. Re-score there: any score above 0.5 is pure chance. Repeat many times (this knob = how many) to build a picture of what luck looks like. permutation p = the fraction of shuffled runs that scored ≥ the real winner. p = 0.005 means only 0.5% of chance runs matched it → very unlikely a fluke. Conventionally p < 0.05 is the “unlikely to be luck” line. with 200 shuffles the smallest p you can report is about 1 / 200 ≈ 0.005; more permutations give a finer, more trustworthy p. 200 shuffled-label runs cluster near 0.58; the real result at 0.89 sits past every one of them, giving p ≈ 0.005. ), }, prefilter_n: { title: "Prefilter top-N", short: "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 shows signal in combination.", detailed: ( <> the engine searches all ~20,000 genes, so nothing is pre-excluded — the most honest setting for a discovery demo. first narrows to the N genes most individually associated with the target (computed name-blind, on the training split only), then searches within that shortlist. Faster, because the space is ~10× smaller. speed and focus vs completeness. A univariate shortlist can drop a gene that only matters in combination (no signal on its own), and it sets a ceiling — if a gene isn’t in the shortlist, the engine can never find it. ~20,000 genes → keep the N most individually associated → the GP searches within N. Off by default = no funnel. ), }, // ---------- Objective explainers ---------- obj_msi: { title: "MSI separation", short: "Reward programs whose score separates MSI-H from MSS (measured by AUROC). It rewards ANY separator, so it often finds shortcut genes, not the cause.", detailed: ( <> a score that ranks MSI-H patients above MSS — i.e. tells the two subtypes apart. pick one random MSI-H and one random MSS patient; AUROC is the chance the score puts the MSI-H one higher. 0.5 = coin-flip (no separation), 1.0 = perfect. We use AUROC, not plain accuracy, because MSI-H is only ~15% of patients — “always guess MSS” would look 85% accurate while separating nothing. {`{ target: msi, metric: AUROC }`} {" "}— the engine sees only the score + the MSI label, never gene names. it rewards ANY separator. MSI-H and MSS differ in thousands of genes, so it usually grabs easy “shortcut” genes (consequences or coincidences), not the causal MMR genes. ), }, obj_tmb: { title: "Mutation burden", short: "Reward programs whose score is negatively associated with mutation burden (low score ↔ high TMB) — the broken spell-checker's fingerprint. A sharper proxy for the cause, but still association, not proof.", detailed: ( <> a score that goes DOWN as mutation count goes UP — the fingerprint of a broken repair gene (switch it off → mutations pile up). The repair genes are the MMR set: MLH1, MSH2, MSH6, PMS2. {`{ target: tmb, metric: correlation, direction: negative }`} {" "}— the engine sees only the score + the TMB numbers. we reward the score being NEGATIVELY correlated with TMB. Rewarding any correlation would also pick genes that RISE with mutations — the opposite of the repair signature. this is a mechanism-shaped association — a much better proxy for the cause than predicting the label, but it doesn’t prove causation. Genes silenced alongside the repair genes can mimic the same low-expression↔high-TMB pattern. the program can choose to ADJUST for confounders (age, stage) via the Effect operator — the most causal move the observational data honestly allows. The engine decides whether it helps; we don’t hardcode it. ), }, obj_hpv: { title: "HPV detection", short: "Find a gene-expression pattern that tells HPV+ tumours apart from HPV−. Scored by AUROC (0.5 = coin-flip, 1.0 = perfect).", detailed: ( <>

HPV detection. Head & neck cancers split into two kinds: those caused by the HPV virus (HPV+) and those that aren’t (HPV−). The engine looks for a gene-expression pattern that tells the two apart.

pick one random HPV+ and one random HPV− patient; AUROC is the chance the engine’s score puts the HPV+ one higher. 0.5 is a coin-flip, 1.0 is perfect. Because only ~15% of tumours are HPV+, we use AUROC rather than plain accuracy — “always guess HPV−” would look 85% accurate while separating nothing. only the score its program produces and the HPV+/HPV− label — never gene names. That’s what makes recovering the known biology afterwards a real rediscovery, not a lookup. this is detecting a known viral fingerprint (the virus switches specific genes on), not discovering a new cause. The cause is the virus itself. A high AUROC means the engine recognised the fingerprint blind. ), }, obj_unsupervised: { title: "Unsupervised", short: "No target column at all. The engine searches for a program whose score splits patients into two clean groups; afterwards we check what that split lines up with.", detailed: ( <> Unlike the other objectives, this one is given NO target — only the gene numbers. The search never sees MSI, TMB, or any label. a score that divides patients into two as-cleanly-separated-as- possible groups (measured by cluster separation). It is NOT told what the groups should be. it always produces a two-group split, but it doesn’t aim at MSS/dMMR — or anything. It finds whatever the strongest natural division in the data is. after the run we check what the discovered split lines up with — MSI? TMB? — the “post-hoc alignment” in the Result panel. if the strongest natural split turns out to BE the MSS-vs-dMMR divide, it aligns with MSI at high AUROC → the engine rediscovered the subtype without ever being told it exists. it may instead land on a different dominant axis (e.g. immune hot vs cold) that only partly overlaps MSI. MSI-H is only ~15% of patients, so a clean recovery isn’t guaranteed — the result is HOW MUCH the blind split overlaps MSS/dMMR. ), }, module_survival: { title: "What “Survives” checks", short: "Whether a group still separates the label when you take away a possible confounder — something that travels with the label but isn't its biology.", detailed: ( <> whether a group still separates HPV when you take away a possible confounder — something that travels with HPV but isn’t HPV biology. If the group’s AUROC mostly came from the confounder, it collapses when the confounder is held constant; if the signal is real, it holds. 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 in the chip read full-cohort → oropharynx-only. 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. {" "} = 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 per class in the subgroup). Tolerance is 0.05 AUROC: a subgroup AUROC within 0.05 of the full-cohort AUROC counts as “survives.” HNSC/HPV coherence-on runs only — other (dataset, target) pairs omit this column. ), }, }; function ObjectiveIntro() { return (

An objective is the rule that scores every program — the fitness the engine maximises. It’s computed only from the program’s per-patient output (and, for the supervised objectives, a target column — MSI, TMB, or HPV — never gene names). It sets what “good” means; the engine then composes DSL programs to satisfy it.

); } function ObjectiveFooter() { return (

What the program chooses is how to build the score and how to compare it — a raw association (Associate) or a confounder-adjusted one (Effect), plus the correlation kind. What stays outside the DSL is the compass: the target it’s scored against, in which direction, judged honestly on held-out data. The program can’t pick the target — that would let the answer into the language.

); } // Unsupervised has no target / no Associate / no Effect — its footer // is the deeper invariant: the program can't see a target to optimise // against, which is exactly what makes a downstream label-alignment // meaningful. function UnsupObjectiveFooter() { return (

The only thing outside the DSL here is “find the cleanest split, judged honestly on held-out data” — there is no target, and the program can’t see one. That’s exactly what makes it meaningful when the split turns out to line up with a known label (MSI on the colorectal cohort, HPV on head & neck): the engine wasn’t told to look for it.

); }