Spaces:
Sleeping
Sleeping
| "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 ( | |
| <p className="mb-3 last:mb-0 text-sm leading-relaxed text-ink"> | |
| <strong className="text-ink">{label}:</strong>{" "} | |
| <span>{children}</span> | |
| </p> | |
| ); | |
| } | |
| function Caption({ children }: { children: React.ReactNode }) { | |
| return ( | |
| <p className="mb-3 text-xs italic text-muted">{children}</p> | |
| ); | |
| } | |
| function Mono({ children }: { children: React.ReactNode }) { | |
| return ( | |
| <code className="font-mono text-[12.5px] text-ink">{children}</code> | |
| ); | |
| } | |
| function ExampleList({ | |
| rows, | |
| }: { | |
| rows: React.ReactNode[]; | |
| }) { | |
| return ( | |
| <ul className="my-2 ml-5 list-disc text-sm leading-relaxed text-ink marker:text-muted"> | |
| {rows.map((r, i) => ( | |
| <li key={i}>{r}</li> | |
| ))} | |
| </ul> | |
| ); | |
| } | |
| // ---------- the registry ------------------------------------------------- | |
| export const PARAM_HELP: Record<ParamKey, ParamHelpEntry> = { | |
| generations: { | |
| title: "Generations", | |
| short: | |
| "How many rounds of 'keep the best, breed, mutate' the engine runs. More rounds refine further, but take longer.", | |
| detailed: ( | |
| <> | |
| <Item label="What it is"> | |
| 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. | |
| </Item> | |
| <Item label="How it works"> | |
| the engine starts with random programs (mostly poor) and each round | |
| nudges the population toward better ones β like selective breeding. | |
| </Item> | |
| <Item label="More vs fewer"> | |
| more rounds refine the winners further, but with diminishing returns | |
| once it has converged. The harder part is usually <em>discovering</em>{" "} | |
| the right genes in the first place (driven by Population and | |
| mutation), not polishing β so extra generations help less once the | |
| curve flattens. | |
| </Item> | |
| <Item label="Typical">30β60.</Item> | |
| <GenerationsLoopDiagram /> | |
| <Caption> | |
| A 4-step loop: random programs β score each β keep the best β breed | |
| & mutate β (back to score). The loop arrow is labelled | |
| “Γ Generations”. | |
| </Caption> | |
| </> | |
| ), | |
| }, | |
| 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: ( | |
| <> | |
| <Item label="What it is"> | |
| how many candidate programs compete each round. A “program” | |
| is a small pipeline built from the DSL operators β{" "} | |
| <strong>Select</strong> (pick genes) β <strong>Reduce</strong>{" "} | |
| (average them into one score) β <strong>Fit</strong> (use the score | |
| to separate the groups). | |
| </Item> | |
| <Item label="Why it matters"> | |
| a bigger population samples more genes and structures each round, so | |
| it’s the main lever for <em>discovery</em> β finding the right | |
| genes at all. Generations then refine what was found. | |
| </Item> | |
| <p className="mb-1 text-sm font-semibold text-ink"> | |
| Example β three random programs in a population: | |
| </p> | |
| <ExampleList | |
| rows={[ | |
| <Mono key="a">average(g04823, g11201, g00917) β score</Mono>, | |
| <Mono key="b"> | |
| average(g15522, g02013) β average(g08840, g00231) β score | |
| </Mono>, | |
| <Mono key="c">average(g07788, g13002, g05340) β score</Mono>, | |
| ]} | |
| /> | |
| <Item label="More vs fewer"> | |
| more explores more options, but each round is slower. | |
| </Item> | |
| <Item label="Typical">150β300.</Item> | |
| </> | |
| ), | |
| }, | |
| genes_per_set: { | |
| title: "Genes / set", | |
| short: | |
| "The most genes a single score may use. Smaller keeps programs simple and readable.", | |
| detailed: ( | |
| <> | |
| <Item label="What it is"> | |
| a “set” is the group of genes averaged together into a | |
| single score. Genes/set is the maximum size of that group. | |
| </Item> | |
| <Item label="Example"> | |
| <Mono> | |
| Select(g05347, g00048, g06271) β average β one score | |
| </Mono>{" "} | |
| (uses 3; the cap might be 8). | |
| </Item> | |
| <Item label="Smaller vs larger"> | |
| smaller keeps each score simple and readable; larger lets one score | |
| blend more genes. | |
| </Item> | |
| <Item label="Pairs with Max sets"> | |
| Genes/set is the <em>width</em> of each group; Max sets is{" "} | |
| <em>how many</em> groups. | |
| </Item> | |
| </> | |
| ), | |
| }, | |
| 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: ( | |
| <> | |
| <Item label="What it is"> | |
| how many <em>separate</em> gene groups a program may build and then | |
| combine (1 or 2). They use different genes β not the same ones | |
| reused. | |
| </Item> | |
| <p className="mb-1 text-sm font-semibold text-ink">Example (2 sets):</p> | |
| <ExampleList | |
| rows={[ | |
| <> | |
| Set 1:{" "} | |
| <Mono>average(g05347, g00048, g06271) β scoreA</Mono> | |
| </>, | |
| <> | |
| Set 2: <Mono>average(g15522, g02013) β scoreB</Mono> | |
| </>, | |
| <> | |
| Combine: <Mono>scoreA β scoreB β final score</Mono> | |
| </>, | |
| ]} | |
| /> | |
| <Item label="Why two"> | |
| to capture a <em>contrast</em> β 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. | |
| </Item> | |
| <MaxSetsDiagram /> | |
| <Caption> | |
| Two short chains feed a Combine(sub) node and produce a final score. | |
| </Caption> | |
| </> | |
| ), | |
| }, | |
| lambda: { | |
| title: "Ξ» (size penalty)", | |
| short: | |
| "How hard the engine is penalised for using more genes. Higher pushes it toward fewer genes (simpler answers).", | |
| detailed: ( | |
| <> | |
| <Item label="What it is"> | |
| a “price per gene” that discourages bloated programs. | |
| Every program is graded on a single number: | |
| </Item> | |
| <p className="mb-3 ml-5 text-sm"> | |
| <Mono>fitness = separation β Ξ» Γ (number of genes)</Mono> | |
| </p> | |
| <p className="mb-3 text-sm leading-relaxed text-ink"> | |
| so the engine ranks by accuracy <em>minus</em> a size tax. | |
| </p> | |
| <Item label="The key idea"> | |
| Ξ» 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. | |
| </Item> | |
| <Item label="Worked example"> | |
| 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, <em>lower</em>, | |
| so it’s rejected. A 4th gene that lifts it to 0.91 (+0.01) β | |
| net 0.890, <em>higher</em>, so it’s kept. | |
| </Item> | |
| <Item label="Turning the knob"> | |
| Ξ» = 0 β genes free β bloated, overfit programs. Ξ» small (0.005) β | |
| trims useless genes, keeps useful ones. Ξ» large β very lean programs, | |
| but may drop useful genes. | |
| </Item> | |
| <LambdaTradeoffDiagram /> | |
| <Caption> | |
| Raw separation rises and plateaus; net fitness peaks at an | |
| intermediate size, and a larger Ξ» shifts the peak to fewer genes. | |
| </Caption> | |
| </> | |
| ), | |
| }, | |
| 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: ( | |
| <> | |
| <Item label="Why randomness"> | |
| 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. | |
| </Item> | |
| <Item label="What the seed is"> | |
| computers don’t make true randomness; a formula generates a | |
| sequence of numbers, each from the previous one. The seed is the{" "} | |
| <em>starting number</em> fed into that formula β everything random | |
| flows from it. (One common formula:{" "} | |
| <Mono>next = (1664525 Γ current + 1013904223) mod 2Β³Β²</Mono>; with | |
| seed 42 the first value is 1,083,814,273, then mapped onto a gene | |
| position.) | |
| </Item> | |
| <Item label="What it does"> | |
| 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. | |
| </Item> | |
| <Item label="Why vary it"> | |
| 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. | |
| </Item> | |
| <SeedDiceDiagram /> | |
| <Caption> | |
| The seed is one fixed list of dice rolls; early rolls pick initial | |
| genes, later rolls drive mutations. | |
| </Caption> | |
| </> | |
| ), | |
| }, | |
| 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: ( | |
| <> | |
| <Item label="What it checks"> | |
| 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. | |
| </Item> | |
| <Item label="How it works"> | |
| 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. | |
| </Item> | |
| <Item label="Reading it"> | |
| 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. | |
| </Item> | |
| <Item label="The knob"> | |
| with 200 shuffles the smallest p you can report is about 1 / 200 β | |
| 0.005; more permutations give a finer, more trustworthy p. | |
| </Item> | |
| <PermutationsHistogramDiagram /> | |
| <Caption> | |
| 200 shuffled-label runs cluster near 0.58; the real result at 0.89 | |
| sits past every one of them, giving p β 0.005. | |
| </Caption> | |
| </> | |
| ), | |
| }, | |
| 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: ( | |
| <> | |
| <Item label="Default (off)"> | |
| the engine searches all ~20,000 genes, so nothing is pre-excluded β | |
| the most honest setting for a discovery demo. | |
| </Item> | |
| <Item label="Turning it on"> | |
| 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. | |
| </Item> | |
| <Item label="The trade-off"> | |
| speed and focus vs completeness. A univariate shortlist can drop a | |
| gene that only matters <em>in combination</em> (no signal on its | |
| own), and it sets a ceiling β if a gene isn’t in the | |
| shortlist, the engine can never find it. | |
| </Item> | |
| <PrefilterFunnelDiagram /> | |
| <Caption> | |
| ~20,000 genes β keep the N most individually associated β the GP | |
| searches within N. Off by default = no funnel. | |
| </Caption> | |
| </> | |
| ), | |
| }, | |
| // ---------- 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: ( | |
| <> | |
| <ObjectiveIntro /> | |
| <Item label="What it optimises"> | |
| a score that ranks MSI-H patients above MSS β i.e. tells the two | |
| subtypes apart. | |
| </Item> | |
| <Item label="How it’s scored (AUROC)"> | |
| 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. | |
| </Item> | |
| <Item label="Represented as"> | |
| <code className="font-mono text-ink">{`{ target: msi, metric: AUROC }`}</code> | |
| {" "}β the engine sees only the score + the MSI label, never | |
| gene names. | |
| </Item> | |
| <Item label="The catch"> | |
| 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. | |
| </Item> | |
| <ObjectiveFooter /> | |
| </> | |
| ), | |
| }, | |
| 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: ( | |
| <> | |
| <ObjectiveIntro /> | |
| <Item label="What it optimises"> | |
| 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. | |
| </Item> | |
| <Item label="Represented as"> | |
| <code className="font-mono text-ink">{`{ target: tmb, metric: correlation, direction: negative }`}</code> | |
| {" "}β the engine sees only the score + the TMB numbers. | |
| </Item> | |
| <Item label="Why ‘negative’ (not just ‘related’)"> | |
| 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. | |
| </Item> | |
| <Item label="Correlation isn’t causation (honest)"> | |
| 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. | |
| </Item> | |
| <Item label="Toward causal"> | |
| 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. | |
| </Item> | |
| <ObjectiveFooter /> | |
| </> | |
| ), | |
| }, | |
| 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: ( | |
| <> | |
| <p className="mb-4 rounded-md border border-border bg-bg p-3 text-[12.5px] leading-relaxed text-ink"> | |
| <strong className="text-ink">HPV detection.</strong> 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. | |
| </p> | |
| <Item label="How it’s scored (AUROC)"> | |
| 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. | |
| </Item> | |
| <Item label="What the engine sees"> | |
| 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. | |
| </Item> | |
| <Item label="Important"> | |
| this is <em>detecting a known viral fingerprint</em> (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. | |
| </Item> | |
| <ObjectiveFooter /> | |
| </> | |
| ), | |
| }, | |
| 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: ( | |
| <> | |
| <ObjectiveIntro /> | |
| <Item label="No target at all"> | |
| Unlike the other objectives, this one is given NO target β only | |
| the gene numbers. The search never sees MSI, TMB, or any label. | |
| </Item> | |
| <Item label="What it optimises"> | |
| 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. | |
| </Item> | |
| <Item label="Shape vs aim"> | |
| 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. | |
| </Item> | |
| <Item label="How we read it"> | |
| after the run we check what the discovered split lines up with | |
| β MSI? TMB? β the “post-hoc alignment” in the | |
| Result panel. | |
| </Item> | |
| <Item label="The win"> | |
| 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. | |
| </Item> | |
| <Item label="Honest expectation"> | |
| 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. | |
| </Item> | |
| <UnsupObjectiveFooter /> | |
| </> | |
| ), | |
| }, | |
| 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: ( | |
| <> | |
| <Item label="The question"> | |
| whether a group still separates HPV when you take away a possible | |
| <em> confounder</em> β 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. | |
| </Item> | |
| <Item label="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 in the chip read <em>full-cohort β oropharynx-only</em>. | |
| </Item> | |
| <Item label="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 <Mono>β</Mono> here | |
| because those tumours include almost no HPV+ patients, so | |
| there’s nothing to test. | |
| </Item> | |
| <Item label="Reading the chips"> | |
| <span className="font-semibold" style={{ color: "#2F6E4C" }}> | |
| β | |
| </span>{" "} | |
| = held up when the confounder was held constant (likely real | |
| signal).{" "} | |
| <span className="font-semibold" style={{ color: "#A64242" }}> | |
| β | |
| </span>{" "} | |
| = dropped past the tolerance (part of it was the confounder).{" "} | |
| <Mono>β</Mono> = couldn’t test (too few patients per | |
| class in the subgroup). | |
| </Item> | |
| <Caption> | |
| 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. | |
| </Caption> | |
| </> | |
| ), | |
| }, | |
| }; | |
| function ObjectiveIntro() { | |
| return ( | |
| <p className="mb-4 rounded-md border border-border bg-bg p-3 text-[12.5px] leading-relaxed text-ink"> | |
| 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. | |
| </p> | |
| ); | |
| } | |
| function ObjectiveFooter() { | |
| return ( | |
| <p className="mt-4 border-t border-border pt-3 text-[11.5px] italic text-muted"> | |
| 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. | |
| </p> | |
| ); | |
| } | |
| // 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 ( | |
| <p className="mt-4 border-t border-border pt-3 text-[11.5px] italic text-muted"> | |
| 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. | |
| </p> | |
| ); | |
| } | |