Spaces:
Running
Running
Add interactive ABI5 genome browser with measured variant scores
Browse files- README.md +15 -2
- assets/causal-browser.js +196 -0
- assets/causal.css +45 -0
- assets/site.css +10 -0
- causal-variants.html +22 -3
- corpus.html +1 -1
- data/abi5-locus.json +0 -0
- factory.html +1 -1
- index.html +1 -1
- leaderboard.html +1 -1
README.md
CHANGED
|
@@ -42,6 +42,10 @@ Static landing page for the Botanic1 models (Living Models), linking to the mode
|
|
| 42 |
- `leaderboard.html`: the S_bal^test leaderboard, the nine-family scorecard and the
|
| 43 |
size and training-token views, filterable and sortable.
|
| 44 |
- `causal-variants.html`: the 545-study causal-variant prioritisation benchmark, with
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
the recall curve recomputed client-side from the full cohort's per-study ranks and
|
| 46 |
a fixed preview of 20 studies selected across species. Sorting and searching stay
|
| 47 |
within those 20 examples; the page notes that the full study set will be published later.
|
|
@@ -68,8 +72,8 @@ adding a page.
|
|
| 68 |
|
| 69 |
## Data
|
| 70 |
|
| 71 |
-
|
| 72 |
-
tables by `scripts/build_data.py`, which imports the manuscript's own
|
| 73 |
`figures/scripts/gwas_causal_common.py` so the cohort and the tie-aware recall cannot
|
| 74 |
diverge from the article. Rerun it from the report repository root after any change
|
| 75 |
to `figures/data/`:
|
|
@@ -81,6 +85,15 @@ python3 hf_space/scripts/build_data.py
|
|
| 81 |
The script prints the checks it runs (the S_bal of the four sizes, the cohort size and
|
| 82 |
the headline recalls) so a mismatch with the manuscript is visible at build time.
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
Figure PNGs under `assets/figures/` are the report's committed renders.
|
| 85 |
`data_pipeline.svg` is a `pdftocairo -svg` conversion of the report's data-pipeline PDF.
|
| 86 |
`agentic_orchestration.svg` retains Figure S1's layout, nodes, queues, connections and
|
|
|
|
| 42 |
- `leaderboard.html`: the S_bal^test leaderboard, the nine-family scorecard and the
|
| 43 |
size and training-token views, filterable and sortable.
|
| 44 |
- `causal-variants.html`: the 545-study causal-variant prioritisation benchmark, with
|
| 45 |
+
a JavaScript/SVG genome browser for the ABI5 locus. Gene models, SNP positions,
|
| 46 |
+
and measured Botanic1-XL absolute LLR scores share a coordinate axis. Hover, tap,
|
| 47 |
+
keyboard selection, zoom, and pan are handled by `assets/causal-browser.js`;
|
| 48 |
+
`assets/causal.css` styles the browser.
|
| 49 |
the recall curve recomputed client-side from the full cohort's per-study ranks and
|
| 50 |
a fixed preview of 20 studies selected across species. Sorting and searching stay
|
| 51 |
within those 20 examples; the page notes that the full study set will be published later.
|
|
|
|
| 72 |
|
| 73 |
## Data
|
| 74 |
|
| 75 |
+
The benchmark summaries under `data/` are generated from the report repository's
|
| 76 |
+
committed tables by `scripts/build_data.py`, which imports the manuscript's own
|
| 77 |
`figures/scripts/gwas_causal_common.py` so the cohort and the tie-aware recall cannot
|
| 78 |
diverge from the article. Rerun it from the report repository root after any change
|
| 79 |
to `figures/data/`:
|
|
|
|
| 85 |
The script prints the checks it runs (the S_bal of the four sizes, the cohort size and
|
| 86 |
the headline recalls) so a mismatch with the manuscript is visible at build time.
|
| 87 |
|
| 88 |
+
`data/abi5-locus.json` is a separate extract of the original Botanic1-XL 4,096 bp
|
| 89 |
+
scoring output for ABI5: 5,948 SNPs in a 100 kbp locus. Scores are the absolute
|
| 90 |
+
values of the source `score` column. The validated SNP at TAIR10 Chr2:15,205,931
|
| 91 |
+
G>C has |LLR| 7.703125 and rank 23, matching `causal.json`. Ranks and ties always
|
| 92 |
+
refer to the full locus, including when the view is zoomed. The file also contains
|
| 93 |
+
38 Araport11 canonical gene models obtained from Ensembl REST, together with the
|
| 94 |
+
annotation URL, study reference, and source scoring-file SHA-256. Gene annotations
|
| 95 |
+
are displayed for biological context and are not inputs to the model score.
|
| 96 |
+
|
| 97 |
Figure PNGs under `assets/figures/` are the report's committed renders.
|
| 98 |
`data_pipeline.svg` is a `pdftocairo -svg` conversion of the report's data-pipeline PDF.
|
| 99 |
`agentic_orchestration.svg` retains Figure S1's layout, nodes, queues, connections and
|
assets/causal-browser.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* A single measured locus, drawn in genomic coordinates. No raster assets.
|
| 2 |
+
Ranks use descending |LLR| over the full 100 kbp locus, even while zoomed. */
|
| 3 |
+
(async function () {
|
| 4 |
+
const host = document.getElementById('locus-plot');
|
| 5 |
+
if (!host) return;
|
| 6 |
+
const readout = document.getElementById('locus-readout');
|
| 7 |
+
const region = document.getElementById('locus-region');
|
| 8 |
+
const buttons = ['locus-in', 'locus-out', 'locus-full', 'locus-causal'].map(id => document.getElementById(id));
|
| 9 |
+
buttons.forEach(b => { b.disabled = true; });
|
| 10 |
+
const esc = s => String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
| 11 |
+
const fmt = n => Math.round(n).toLocaleString('en-US');
|
| 12 |
+
try {
|
| 13 |
+
const response = await fetch('data/abi5-locus.json');
|
| 14 |
+
if (!response.ok) throw new Error('Locus data unavailable');
|
| 15 |
+
const data = await response.json();
|
| 16 |
+
const variants = data.variants;
|
| 17 |
+
const causalIndex = variants.findIndex(v => v.causal);
|
| 18 |
+
const causal = variants[causalIndex];
|
| 19 |
+
const sorted = variants.map(v => v.abs_llr).sort((a, b) => b - a);
|
| 20 |
+
const cutoff = sorted[Math.floor(variants.length * .01) - 1];
|
| 21 |
+
const ymax = Math.ceil(sorted[0] / 2) * 2;
|
| 22 |
+
let start = causal.pos - 10000, end = causal.pos + 10000;
|
| 23 |
+
let selected = causalIndex, inspected = causalIndex, shown = [], W = 960;
|
| 24 |
+
let L = 96, R = 22;
|
| 25 |
+
const H = 365, scoreTop = 207, scoreBottom = 318, snpY = 171;
|
| 26 |
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
| 27 |
+
svg.setAttribute('role', 'group'); svg.setAttribute('tabindex', '0');
|
| 28 |
+
svg.setAttribute('aria-label', 'ABI5 genome browser. Gene models, SNP positions and absolute LLR scores.');
|
| 29 |
+
svg.setAttribute('aria-describedby', 'locus-help');
|
| 30 |
+
host.append(svg);
|
| 31 |
+
const x = pos => L + (pos - start) / (end - start) * (W - L - R);
|
| 32 |
+
const y = score => scoreBottom - score / ymax * (scoreBottom - scoreTop);
|
| 33 |
+
const path = (d, cls) => `<path class="${cls}" d="${d}"/>`;
|
| 34 |
+
const text = (px, py, s, cls = '', anchor = 'start') => `<text x="${px}" y="${py}" class="${cls}" text-anchor="${anchor}">${esc(s)}</text>`;
|
| 35 |
+
const diamond = (px, py, r = 5) => `<path class="causal-dot" d="M${px} ${py-r}l${r} ${r}l-${r} ${r}l-${r} -${r}Z"/>`;
|
| 36 |
+
function setView(a, b) {
|
| 37 |
+
const width = Math.max(1000, Math.min(data.end - data.start, b - a));
|
| 38 |
+
start = Math.max(data.start, Math.min(data.end - width, a)); end = start + width;
|
| 39 |
+
render();
|
| 40 |
+
}
|
| 41 |
+
function zoom(factor) {
|
| 42 |
+
const centre = variants[selected].pos >= start && variants[selected].pos <= end ? variants[selected].pos : (start + end) / 2;
|
| 43 |
+
const width = Math.max(1000, Math.min(data.end - data.start, (end - start) * factor));
|
| 44 |
+
setView(centre - width / 2, centre + width / 2);
|
| 45 |
+
}
|
| 46 |
+
function show(index, announce = false) {
|
| 47 |
+
inspected = index;
|
| 48 |
+
const v = variants[index];
|
| 49 |
+
const group = svg.querySelector('#locus-focus');
|
| 50 |
+
const shade = svg.querySelector('#locus-context');
|
| 51 |
+
if (!group || !shade) return;
|
| 52 |
+
const left = Math.max(L, x(v.pos - data.context_bp / 2));
|
| 53 |
+
const right = Math.min(W - R, x(v.pos + data.context_bp / 2));
|
| 54 |
+
shade.setAttribute('x', left); shade.setAttribute('width', Math.max(0, right - left));
|
| 55 |
+
group.innerHTML = v.pos >= start && v.pos <= end ?
|
| 56 |
+
path(`M${x(v.pos)} 42V325`, 'focus-line') +
|
| 57 |
+
`<circle class="${v.causal ? 'causal-dot' : 'focus-dot'}" cx="${x(v.pos)}" cy="${y(v.abs_llr)}" r="4"/>` +
|
| 58 |
+
(v.causal ? diamond(x(v.pos), snpY, 5) : `<circle class="focus-dot" cx="${x(v.pos)}" cy="${snpY}" r="3.5"/>`) : '';
|
| 59 |
+
readout.setAttribute('aria-live', announce ? 'polite' : 'off');
|
| 60 |
+
readout.innerHTML = `<span class="${v.causal ? 'causal-tag' : 'variant-tag'}">${v.causal ? 'Validated causal SNP' : 'Population SNP'}</span>` +
|
| 61 |
+
`<strong>Chr${esc(data.chrom)}:${fmt(v.pos)} ${esc(v.ref)} → ${esc(v.alt)}</strong>` +
|
| 62 |
+
`<span>|LLR| <strong>${v.abs_llr.toFixed(3)}</strong></span>` +
|
| 63 |
+
`<span>Rank <strong>${fmt(v.rank)}${v.ties > 1 ? '–' + fmt(v.rank + v.ties - 1) : ''} / ${fmt(variants.length)}</strong>${v.ties > 1 ? ' (tied)' : ''}</span>`;
|
| 64 |
+
}
|
| 65 |
+
function render() {
|
| 66 |
+
W = host.clientWidth;
|
| 67 |
+
L = W < 500 ? 72 : 96; R = W < 500 ? 16 : 22;
|
| 68 |
+
svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
|
| 69 |
+
svg.setAttribute('data-start', Math.round(start)); svg.setAttribute('data-end', Math.round(end));
|
| 70 |
+
shown = variants.map((v, index) => ({v, index})).filter(({v}) => v.pos >= start && v.pos <= end);
|
| 71 |
+
region.textContent = `Chr${data.chrom}:${fmt(start)}–${fmt(end)} · ${((end-start)/1000).toLocaleString('en-US',{maximumFractionDigits:1})} kbp · ${fmt(shown.length)} SNPs in view`;
|
| 72 |
+
buttons[0].disabled = end - start <= 1000;
|
| 73 |
+
buttons[1].disabled = end - start >= data.end - data.start;
|
| 74 |
+
buttons[2].disabled = end - start >= data.end - data.start;
|
| 75 |
+
buttons[3].disabled = false;
|
| 76 |
+
let out = `<title>ABI5 locus: ${data.model}, ${fmt(variants.length)} measured SNP scores</title>`;
|
| 77 |
+
out += `<defs><clipPath id="locus-clip"><rect x="${L}" y="40" width="${W-L-R}" height="286"/></clipPath></defs>`;
|
| 78 |
+
out += text(12, 32, 'Chr ' + data.chrom, 'track-name');
|
| 79 |
+
out += text(12, 80, 'Genes', 'track-name') + text(12, 97, 'Araport11');
|
| 80 |
+
out += text(12, snpY + 4, 'SNPs', 'track-name');
|
| 81 |
+
out += text(12, 226, '|LLR|', 'track-name');
|
| 82 |
+
out += `<g clip-path="url(#locus-clip)"><rect id="locus-context" class="context-window" y="42" height="283"/></g>`;
|
| 83 |
+
const targetStep = (end-start) / Math.max(1, Math.floor((W-L-R)/120));
|
| 84 |
+
const step = [250,500,1000,2500,5000,10000,20000,50000,100000].find(s => s >= targetStep);
|
| 85 |
+
for (let pos = Math.ceil(start / step) * step; pos <= end; pos += step) {
|
| 86 |
+
const anchor = x(pos)<L+30 ? 'start' : x(pos)>W-R-30 ? 'end' : 'middle';
|
| 87 |
+
out += path(`M${x(pos)} 37V325`, 'grid') + text(x(pos), 30, fmt(pos), '', anchor);
|
| 88 |
+
}
|
| 89 |
+
// Pack canonical transcripts in three lanes; labels are shown only when they fit.
|
| 90 |
+
const lanes = [L-1, L-1, L-1];
|
| 91 |
+
const genes = data.genes.filter(g => g.end >= start && g.start <= end);
|
| 92 |
+
let geneShapes = '';
|
| 93 |
+
for (const g of genes) {
|
| 94 |
+
const gx = Math.max(L, x(g.start)), ge = Math.min(W-R, x(g.end));
|
| 95 |
+
const lane = lanes.indexOf(Math.min(...lanes)); const gy = 75 + lane * 26;
|
| 96 |
+
const isCausal = g.name === 'ABI5';
|
| 97 |
+
lanes[lane] = Math.max(ge + 9, gx + g.name.length * 6 + 8);
|
| 98 |
+
geneShapes += `<g class="gene${isCausal ? ' causal-gene' : ''}"><title>${esc(g.name)} · ${esc(g.transcript)} · ${g.strand > 0 ? '+' : '−'} strand</title>`;
|
| 99 |
+
geneShapes += path(`M${gx} ${gy}H${ge}`, 'gene-line');
|
| 100 |
+
for (let px = gx+12; px < ge-4; px += 18) {
|
| 101 |
+
const direction = g.strand > 0 ? 3 : -3;
|
| 102 |
+
geneShapes += path(`M${px-direction} ${gy-2.5}l${direction} 2.5l${-direction} 2.5`, 'gene-line');
|
| 103 |
+
}
|
| 104 |
+
for (const [a,b] of g.exons) if (b >= start && a <= end) {
|
| 105 |
+
const ex = Math.max(L,x(a)), ew = Math.max(1,Math.min(W-R,x(b))-ex);
|
| 106 |
+
geneShapes += `<rect class="exon" x="${ex}" y="${gy-4}" width="${ew}" height="8"/>`;
|
| 107 |
+
}
|
| 108 |
+
// Reserve the causal gene's label even in the full-locus overview.
|
| 109 |
+
const labelWidth = g.name.length*6;
|
| 110 |
+
const next = genes.find(n => n.start > g.start && x(n.start) < gx + labelWidth + 8);
|
| 111 |
+
if (isCausal || (!next && gx + labelWidth < W-R)) geneShapes += text(gx,gy-9,g.name,'gene-label' + (isCausal ? ' causal-gene' : ''));
|
| 112 |
+
geneShapes += '</g>';
|
| 113 |
+
}
|
| 114 |
+
out += `<g clip-path="url(#locus-clip)">${geneShapes}</g>`;
|
| 115 |
+
for (let score=0; score<=ymax; score+=4) out += path(`M${L} ${y(score)}H${W-R}`,'grid')+text(L-8,y(score)+3,score,'','end');
|
| 116 |
+
out += text(W-R,195,'◆ validated causal SNP','','end');
|
| 117 |
+
out += `<g clip-path="url(#locus-clip)">`;
|
| 118 |
+
out += path(shown.map(({v})=>`M${x(v.pos).toFixed(2)} ${snpY-5}v10`).join(''),'snp-rug');
|
| 119 |
+
out += path(shown.map(({v})=>`M${x(v.pos).toFixed(2)} ${scoreBottom}V${y(v.abs_llr).toFixed(2)}`).join(''),'score-stems');
|
| 120 |
+
out += path(`M${L} ${y(cutoff)}H${W-R}`,'threshold');
|
| 121 |
+
out += text(W-R-4,y(cutoff)-5,'top 1% cutoff','','end');
|
| 122 |
+
if (causal.pos >= start && causal.pos <= end) {
|
| 123 |
+
out += path(`M${x(causal.pos)} ${scoreBottom}V${y(causal.abs_llr)}`,'causal-stem');
|
| 124 |
+
out += diamond(x(causal.pos),y(causal.abs_llr),5) + diamond(x(causal.pos),snpY,5);
|
| 125 |
+
}
|
| 126 |
+
out += '<g id="locus-focus"></g></g>';
|
| 127 |
+
out += `<rect x="${L}" y="342" width="12" height="9" fill="var(--sage)" stroke="var(--rule-2)"/>`;
|
| 128 |
+
out += text(L+19,350,W<500 ? '4,096 bp context' : 'shaded: 4,096 bp scoring window');
|
| 129 |
+
svg.innerHTML = out;
|
| 130 |
+
show(inspected);
|
| 131 |
+
}
|
| 132 |
+
function eventPoint(ev) {
|
| 133 |
+
const point = svg.createSVGPoint(); point.x = ev.clientX; point.y = ev.clientY;
|
| 134 |
+
return point.matrixTransform(svg.getScreenCTM().inverse());
|
| 135 |
+
}
|
| 136 |
+
function nearest(ev) {
|
| 137 |
+
const point = eventPoint(ev);
|
| 138 |
+
if (point.x < L || point.x > W-R || point.y < 147 || point.y > 325) return null;
|
| 139 |
+
let best = null, distance = Infinity;
|
| 140 |
+
for (const {v,index} of shown) {
|
| 141 |
+
const dx = x(v.pos)-point.x;
|
| 142 |
+
const dy = point.y > 195 ? (y(v.abs_llr)-point.y) * .4 : 0;
|
| 143 |
+
const d = dx*dx+dy*dy;
|
| 144 |
+
if (d < distance) { distance = d; best = index; }
|
| 145 |
+
}
|
| 146 |
+
return best;
|
| 147 |
+
}
|
| 148 |
+
let drag = null;
|
| 149 |
+
svg.addEventListener('pointerdown', ev => {
|
| 150 |
+
if (ev.button !== 0) return;
|
| 151 |
+
const point = eventPoint(ev);
|
| 152 |
+
if (point.x < L || point.x > W-R) return;
|
| 153 |
+
drag = {px:ev.clientX,start,end,moved:false};
|
| 154 |
+
});
|
| 155 |
+
svg.addEventListener('pointermove', ev => {
|
| 156 |
+
if (drag) {
|
| 157 |
+
const delta = ev.clientX-drag.px;
|
| 158 |
+
if (Math.abs(delta)>5) drag.moved = true;
|
| 159 |
+
if (drag.moved) {
|
| 160 |
+
svg.setPointerCapture(ev.pointerId);
|
| 161 |
+
const shift = -delta / svg.getBoundingClientRect().width * W / (W-L-R) * (drag.end-drag.start);
|
| 162 |
+
setView(drag.start+shift,drag.end+shift); return;
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
const index = nearest(ev); if (index !== null && index !== inspected) show(index);
|
| 166 |
+
});
|
| 167 |
+
svg.addEventListener('pointerup', ev => {
|
| 168 |
+
if (drag && !drag.moved) { const index=nearest(ev); if(index!==null) { selected=index;show(index,true); } }
|
| 169 |
+
drag=null; if(svg.hasPointerCapture(ev.pointerId)) svg.releasePointerCapture(ev.pointerId);
|
| 170 |
+
});
|
| 171 |
+
svg.addEventListener('pointercancel', () => {drag=null;show(selected);});
|
| 172 |
+
svg.addEventListener('pointerleave', () => { if (!drag) show(selected); });
|
| 173 |
+
svg.addEventListener('keydown', ev => {
|
| 174 |
+
if (ev.key === '+' || ev.key === '=') { ev.preventDefault();zoom(.5);return; }
|
| 175 |
+
if (ev.key === '-') { ev.preventDefault();zoom(2);return; }
|
| 176 |
+
if (ev.key === 'Home') { ev.preventDefault();selected=causalIndex;inspected=selected;setView(causal.pos-10000,causal.pos+10000);show(selected,true);return; }
|
| 177 |
+
if (!['ArrowLeft','ArrowRight'].includes(ev.key)) return;
|
| 178 |
+
ev.preventDefault();selected=Math.max(0,Math.min(variants.length-1,selected+(ev.key==='ArrowLeft'?-1:1)));
|
| 179 |
+
inspected=selected;
|
| 180 |
+
const pos=variants[selected].pos, width=end-start;
|
| 181 |
+
if (pos < start || pos > end) setView(pos-width/2,pos+width/2);
|
| 182 |
+
show(selected,true);
|
| 183 |
+
});
|
| 184 |
+
buttons[0].addEventListener('click',()=>zoom(.5));
|
| 185 |
+
buttons[1].addEventListener('click',()=>zoom(2));
|
| 186 |
+
buttons[2].addEventListener('click',()=>setView(data.start,data.end));
|
| 187 |
+
buttons[3].addEventListener('click',()=>{selected=causalIndex;inspected=selected;setView(causal.pos-10000,causal.pos+10000);show(selected,true);});
|
| 188 |
+
let resize;
|
| 189 |
+
new ResizeObserver(()=>{cancelAnimationFrame(resize);resize=requestAnimationFrame(render);}).observe(host);
|
| 190 |
+
render();
|
| 191 |
+
} catch (error) {
|
| 192 |
+
region.textContent = 'ABI5 locus';
|
| 193 |
+
readout.textContent = 'The locus could not be loaded. Reload the page to try again.';
|
| 194 |
+
host.innerHTML = '<p class="locus-fallback">The recall curves below summarise the full benchmark.</p>';
|
| 195 |
+
}
|
| 196 |
+
})();
|
assets/causal.css
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Live locus tracks. Coordinates and scores come from data/abi5-locus.json. */
|
| 2 |
+
.locus-browser { margin: 30px 0; border: 1px solid var(--rule-2); border-radius: 10px; background: var(--card); overflow: hidden; font-family: var(--sans); }
|
| 3 |
+
.locus-heading { display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap; padding: 18px 22px; border-bottom: 1px solid var(--rule); background: var(--sage); }
|
| 4 |
+
.locus-browser h2 { margin: 0 0 5px; font-size: 19px; }
|
| 5 |
+
.locus-heading p { margin: 0; color: var(--ink-2); font-size: 12px; }
|
| 6 |
+
.locus-tools { display: flex; gap: 6px; flex-wrap: wrap; }
|
| 7 |
+
.locus-tools button { font: 600 12px var(--sans); color: var(--ink-2); background: var(--card); border: 1px solid var(--rule-2); border-radius: 5px; padding: 7px 10px; cursor: pointer; min-height: 34px; }
|
| 8 |
+
.locus-tools button:first-child, .locus-tools button:nth-child(2) { font-size: 17px; min-width: 34px; padding: 3px 9px; }
|
| 9 |
+
.locus-tools button:hover { background: var(--lime-2); color: var(--ink); }
|
| 10 |
+
.locus-tools button:disabled { opacity: .4; cursor: default; }
|
| 11 |
+
.locus-browser .locus-region { margin: 0; padding: 12px 22px 0; font: 11px var(--mono); color: var(--ink-2); max-width: none; }
|
| 12 |
+
.locus-scroll { overflow-x: auto; padding: 0 12px; }
|
| 13 |
+
#locus-plot { min-width: 0; }
|
| 14 |
+
#locus-plot svg { display: block; width: 100%; height: auto; touch-action: pan-y; cursor: crosshair; }
|
| 15 |
+
#locus-plot text { fill: var(--ink-2); font: 10.5px var(--mono); }
|
| 16 |
+
#locus-plot .track-name { fill: var(--ink); font: 600 11px var(--sans); }
|
| 17 |
+
#locus-plot .gene-label { font: italic 10px var(--sans); }
|
| 18 |
+
#locus-plot .gene-label.causal-gene { font-weight: 800; fill: var(--ink); }
|
| 19 |
+
#locus-plot .grid { stroke: var(--rule); stroke-width: .7; }
|
| 20 |
+
#locus-plot .gene-line { stroke: var(--tree-2); stroke-width: 1; fill: none; }
|
| 21 |
+
#locus-plot .exon { fill: var(--tree-2); }
|
| 22 |
+
#locus-plot .gene.causal-gene .exon { fill: var(--accent); }
|
| 23 |
+
#locus-plot .gene.causal-gene .gene-line { stroke: var(--accent); }
|
| 24 |
+
#locus-plot .score-stems { fill: none; stroke: var(--tree-2); stroke-width: 1; opacity: .85; }
|
| 25 |
+
#locus-plot .snp-rug { fill: none; stroke: var(--tree-2); stroke-width: 1; opacity: .6; }
|
| 26 |
+
#locus-plot .causal-stem { stroke: var(--ink); stroke-width: 2; }
|
| 27 |
+
#locus-plot .causal-dot { fill: var(--lime); stroke: var(--ink); stroke-width: 1.3; }
|
| 28 |
+
#locus-plot .context-window { fill: color-mix(in srgb, var(--ours-light) 13%, var(--card)); }
|
| 29 |
+
#locus-plot .focus-line { stroke: var(--ink-3); stroke-dasharray: 3 3; stroke-width: 1; }
|
| 30 |
+
#locus-plot .focus-dot { fill: var(--card); stroke: var(--ink); stroke-width: 1.8; }
|
| 31 |
+
#locus-plot .threshold { fill: none; stroke: var(--ink-3); stroke-dasharray: 4 4; stroke-width: 1; }
|
| 32 |
+
.locus-readout { display: flex; align-items: center; gap: 7px 20px; flex-wrap: wrap; min-height: 64px; padding: 13px 22px; border-top: 1px solid var(--rule); background: var(--paper-2); font: 12px var(--mono); color: var(--ink-2); }
|
| 33 |
+
.locus-readout strong { color: var(--ink); font-weight: 500; }
|
| 34 |
+
.locus-readout .causal-tag { display: inline-flex; align-items: center; gap: 7px; font: 600 12px var(--sans); color: var(--ink); }
|
| 35 |
+
.locus-readout .causal-tag::before { content: ''; width: 8px; height: 8px; transform: rotate(45deg); background: var(--lime); border: 1px solid var(--ink); }
|
| 36 |
+
.locus-readout .variant-tag { font: 12px var(--sans); }
|
| 37 |
+
.locus-browser figcaption { margin: 0; padding: 11px 22px; max-width: none; display: flex; flex-wrap: wrap; justify-content: space-between; gap: 7px 24px; font-size: 11px; border-top: 1px solid var(--rule); }
|
| 38 |
+
.locus-browser figcaption a { color: var(--ink-2); }
|
| 39 |
+
.locus-browser .locus-fallback { margin: 15px 22px; font-size: 14px; }
|
| 40 |
+
@media (max-width: 640px) {
|
| 41 |
+
.locus-heading { padding: 16px; gap: 12px; }
|
| 42 |
+
.locus-browser .locus-region { padding: 12px 16px 0; font-size: 10px; }
|
| 43 |
+
.locus-readout, .locus-browser figcaption { padding-left: 16px; padding-right: 16px; }
|
| 44 |
+
.locus-tools button { min-height: 40px; }
|
| 45 |
+
}
|
assets/site.css
CHANGED
|
@@ -100,6 +100,16 @@ em, i { font-style: italic; }
|
|
| 100 |
.topbar nav a:hover { color: var(--ink); }
|
| 101 |
.topbar nav a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--lime); }
|
| 102 |
.topbar nav a.ext::after { content: "↗"; font-size: 0.8em; margin-left: 4px; color: var(--ink-3); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
/* ---- token cells ---- */
|
| 105 |
.wordmark { --cell: 64px; display: inline-flex; align-items: flex-end; gap: calc(var(--cell) * 0.09); user-select: none; }
|
|
|
|
| 100 |
.topbar nav a:hover { color: var(--ink); }
|
| 101 |
.topbar nav a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--lime); }
|
| 102 |
.topbar nav a.ext::after { content: "↗"; font-size: 0.8em; margin-left: 4px; color: var(--ink-3); }
|
| 103 |
+
@media (max-width: 1100px) {
|
| 104 |
+
.topbar .inner { flex-wrap: wrap; gap: 10px; }
|
| 105 |
+
.topbar nav { gap: 8px 20px; }
|
| 106 |
+
}
|
| 107 |
+
@media (max-width: 640px) {
|
| 108 |
+
.topbar .inner { padding: 10px 18px; }
|
| 109 |
+
.topbar .lockup .wordmark { --cell: 18px; }
|
| 110 |
+
.topbar .lockup .org { font-size: 12px; gap: 5px; }
|
| 111 |
+
.topbar nav { font-size: 12px; gap: 4px 16px; }
|
| 112 |
+
}
|
| 113 |
|
| 114 |
/* ---- token cells ---- */
|
| 115 |
.wordmark { --cell: 64px; display: inline-flex; align-items: flex-end; gap: calc(var(--cell) * 0.09); user-select: none; }
|
causal-variants.html
CHANGED
|
@@ -8,7 +8,8 @@
|
|
| 8 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700;800&family=Spectral:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Mono:wght@400;500;600&display=swap">
|
| 11 |
-
<link rel="stylesheet" href="assets/site.css?v=20260908-
|
|
|
|
| 12 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
| 13 |
</head>
|
| 14 |
<body>
|
|
@@ -17,7 +18,24 @@
|
|
| 17 |
|
| 18 |
<main class="prose">
|
| 19 |
<h1>Zero-shot causal variant prioritisation</h1>
|
| 20 |
-
<p class="lede">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
<p>Our newly introduced causal-variant benchmark uses experimentally validated functional variants as a source of ground truth, instead of relying on indirect proxies such as allele frequency or variant class. Every language model is scored at min(4,096, trained context) (PlantCaduceus-l32 and GPN are fixed at 512 bp) for each LLR, putting the SNV in the middle of the window. All gLMs are evaluated on the full dataset from the reference genome sequences only, while bioinformatics baselines require either precise annotations for Ensembl VEP or MSA support for conservation scores. As a result, PhyloP and PhastCons, which are built on PlantRegMap, are only scored on a subset of the studies.</p>
|
| 23 |
<p>Although it should not necessarily be ranked first, we expect a validated causal variant to rank near the top of its locus, within the top 1% (about 60 variants) or even 0.1% (about 6 variants), which is why we choose to assess the performance on this task using the recall at these thresholds. The recalls are tie-aware: if the candidate is within a tie block in a study and the considered top fraction has to cut within the block, then the contribution of that study to the recall is neither 0 nor 1 but the proportion of the tie block that can fit in the shortlist.</p>
|
|
@@ -46,7 +64,7 @@
|
|
| 46 |
<div class="tablewrap"><table id="sweep"></table></div>
|
| 47 |
|
| 48 |
<h2 id="limits">Simplifications</h2>
|
| 49 |
-
<p>This
|
| 50 |
|
| 51 |
<h2 id="sec-studies">The 545 studies</h2>
|
| 52 |
<p>Below are 20 example studies from the benchmark. The full set of 545 studies, candidate variants, and curation audit will be published later.</p>
|
|
@@ -66,6 +84,7 @@
|
|
| 66 |
</div>
|
| 67 |
<div class="tip" id="tip" role="tooltip"></div>
|
| 68 |
<script src="assets/hero.js"></script>
|
|
|
|
| 69 |
|
| 70 |
<script>
|
| 71 |
(async function () {
|
|
|
|
| 8 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700;800&family=Spectral:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Mono:wght@400;500;600&display=swap">
|
| 11 |
+
<link rel="stylesheet" href="assets/site.css?v=20260908-browser">
|
| 12 |
+
<link rel="stylesheet" href="assets/causal.css?v=20260908-browser">
|
| 13 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
| 14 |
</head>
|
| 15 |
<body>
|
|
|
|
| 18 |
|
| 19 |
<main class="prose">
|
| 20 |
<h1>Zero-shot causal variant prioritisation</h1>
|
| 21 |
+
<p class="lede">545 loci across 14 species, each with an experimentally validated causal SNP. The task: recover that SNP among thousands of neighbouring population variants, using sequence alone.</p>
|
| 22 |
+
|
| 23 |
+
<figure class="locus-browser" aria-labelledby="locus-title">
|
| 24 |
+
<div class="locus-heading">
|
| 25 |
+
<div><h2 id="locus-title">ABI5 locus</h2><p><i>Arabidopsis thaliana</i> · TAIR10 · Botanic1-XL</p></div>
|
| 26 |
+
<div class="locus-tools" role="group" aria-label="Genome browser navigation">
|
| 27 |
+
<button type="button" id="locus-out" aria-label="Zoom out">−</button>
|
| 28 |
+
<button type="button" id="locus-in" aria-label="Zoom in">+</button>
|
| 29 |
+
<button type="button" id="locus-full">100 kbp locus</button>
|
| 30 |
+
<button type="button" id="locus-causal">Causal SNP</button>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
<p class="locus-region" id="locus-region">Loading locus…</p>
|
| 34 |
+
<div class="locus-scroll"><div id="locus-plot"></div></div>
|
| 35 |
+
<div class="locus-readout" id="locus-readout" aria-live="polite" aria-atomic="true"></div>
|
| 36 |
+
<figcaption><span id="locus-help">Hover or tap a SNP to inspect. Drag to pan; + / − to zoom. Arrow keys select SNPs.</span><span>Measured SNP scores · 4,096 bp context · <a href="https://plants.ensembl.org/Arabidopsis_thaliana/Gene/Summary?g=AT2G36270" target="_blank" rel="noopener">Araport11 gene models ↗</a></span></figcaption>
|
| 37 |
+
<noscript><p class="locus-fallback">Enable JavaScript to explore the ABI5 locus. <a href="https://www.biorxiv.org/content/10.64898/2026.09.04.749355v1">Read the benchmark in the report ↗</a></p></noscript>
|
| 38 |
+
</figure>
|
| 39 |
|
| 40 |
<p>Our newly introduced causal-variant benchmark uses experimentally validated functional variants as a source of ground truth, instead of relying on indirect proxies such as allele frequency or variant class. Every language model is scored at min(4,096, trained context) (PlantCaduceus-l32 and GPN are fixed at 512 bp) for each LLR, putting the SNV in the middle of the window. All gLMs are evaluated on the full dataset from the reference genome sequences only, while bioinformatics baselines require either precise annotations for Ensembl VEP or MSA support for conservation scores. As a result, PhyloP and PhastCons, which are built on PlantRegMap, are only scored on a subset of the studies.</p>
|
| 41 |
<p>Although it should not necessarily be ranked first, we expect a validated causal variant to rank near the top of its locus, within the top 1% (about 60 variants) or even 0.1% (about 6 variants), which is why we choose to assess the performance on this task using the recall at these thresholds. The recalls are tie-aware: if the candidate is within a tie block in a study and the considered top fraction has to cut within the block, then the contribution of that study to the recall is neither 0 nor 1 but the proportion of the tie block that can fit in the shortlist.</p>
|
|
|
|
| 64 |
<div class="tablewrap"><table id="sweep"></table></div>
|
| 65 |
|
| 66 |
<h2 id="limits">Simplifications</h2>
|
| 67 |
+
<p>This benchmark restricts the search to SNPs in a 100 kbp locus. It asks whether the documented causal SNP ranks near the top by absolute LLR. Surrounding unlabelled variants may also be functional or phenotypically relevant, so the benchmark measures recovery of the documented causal SNP rather than identifying every functional variant in the locus.</p>
|
| 68 |
|
| 69 |
<h2 id="sec-studies">The 545 studies</h2>
|
| 70 |
<p>Below are 20 example studies from the benchmark. The full set of 545 studies, candidate variants, and curation audit will be published later.</p>
|
|
|
|
| 84 |
</div>
|
| 85 |
<div class="tip" id="tip" role="tooltip"></div>
|
| 86 |
<script src="assets/hero.js"></script>
|
| 87 |
+
<script src="assets/causal-browser.js?v=20260908-browser" defer></script>
|
| 88 |
|
| 89 |
<script>
|
| 90 |
(async function () {
|
corpus.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
| 8 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700;800&family=Spectral:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Mono:wght@400;500;600&display=swap">
|
| 11 |
-
<link rel="stylesheet" href="assets/site.css?v=20260908-
|
| 12 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
| 13 |
</head>
|
| 14 |
<body>
|
|
|
|
| 8 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700;800&family=Spectral:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Mono:wght@400;500;600&display=swap">
|
| 11 |
+
<link rel="stylesheet" href="assets/site.css?v=20260908-browser">
|
| 12 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
| 13 |
</head>
|
| 14 |
<body>
|
data/abi5-locus.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
factory.html
CHANGED
|
@@ -9,7 +9,7 @@
|
|
| 9 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 11 |
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700;800&family=Spectral:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Mono:wght@400;500;600&display=swap">
|
| 12 |
-
<link rel="stylesheet" href="assets/site.css?v=20260908-
|
| 13 |
<link rel="stylesheet" href="assets/factory.css">
|
| 14 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
| 15 |
</head>
|
|
|
|
| 9 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 11 |
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700;800&family=Spectral:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Mono:wght@400;500;600&display=swap">
|
| 12 |
+
<link rel="stylesheet" href="assets/site.css?v=20260908-browser">
|
| 13 |
<link rel="stylesheet" href="assets/factory.css">
|
| 14 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
| 15 |
</head>
|
index.html
CHANGED
|
@@ -9,7 +9,7 @@
|
|
| 9 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 11 |
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700;800&family=Spectral:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Mono:wght@400;500;600&display=swap">
|
| 12 |
-
<link rel="stylesheet" href="assets/site.css?v=20260908-
|
| 13 |
<link rel="stylesheet" href="assets/landing.css?v=20260908-inline-logo">
|
| 14 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js" defer></script>
|
| 15 |
<script src="assets/hero.js" defer></script>
|
|
|
|
| 9 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 11 |
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700;800&family=Spectral:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Mono:wght@400;500;600&display=swap">
|
| 12 |
+
<link rel="stylesheet" href="assets/site.css?v=20260908-browser">
|
| 13 |
<link rel="stylesheet" href="assets/landing.css?v=20260908-inline-logo">
|
| 14 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js" defer></script>
|
| 15 |
<script src="assets/hero.js" defer></script>
|
leaderboard.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
| 8 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700;800&family=Spectral:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Mono:wght@400;500;600&display=swap">
|
| 11 |
-
<link rel="stylesheet" href="assets/site.css?v=20260908-
|
| 12 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
| 13 |
</head>
|
| 14 |
<body>
|
|
|
|
| 8 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700;800&family=Spectral:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Mono:wght@400;500;600&display=swap">
|
| 11 |
+
<link rel="stylesheet" href="assets/site.css?v=20260908-browser">
|
| 12 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
| 13 |
</head>
|
| 14 |
<body>
|