Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
verena commited on
Commit Β·
37e25dc
0
Parent(s):
Release hackathon gradio app
Browse files- .gitattributes +37 -0
- .gitignore +224 -0
- README.md +34 -0
- app.py +122 -0
- config.py +45 -0
- evaluation.py +196 -0
- groundtruth.py +69 -0
- requirements.txt +3 -0
- static/child-drawing.png +3 -0
- static/logos/BEACON_dark.png +3 -0
- static/logos/anthropic-logo.svg +7 -0
- static/logos/aws-logo.svg.webp +0 -0
- static/logos/beacon-logo.webp +0 -0
- static/logos/hf-logo.svg +8 -0
- static/logos/mva-society-logo.jpg +0 -0
- static/logos/powered-by-aws.svg +1 -0
- static/logos/sage-logo.svg +13 -0
- static/styles.css +610 -0
- static/templates/methods_description_form.xlsx +0 -0
- static/templates/track1_submission_template.csv +3 -0
- tabs/about.py +380 -0
- tabs/faq.py +85 -0
- tabs/leaderboard.py +32 -0
- tabs/rules.py +160 -0
- tabs/submit_track1.py +269 -0
- tabs/submit_track2.py +227 -0
- utils.py +207 -0
.gitattributes
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
static/child-drawing.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
static/logos/BEACON_dark.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
.idea/
|
| 3 |
+
|
| 4 |
+
# Local dev test output and inputs (written when HF_TOKEN is not set)
|
| 5 |
+
local_dev/
|
| 6 |
+
|
| 7 |
+
# Byte-compiled / optimized / DLL files
|
| 8 |
+
**/__pycache__/
|
| 9 |
+
*.py[codz]
|
| 10 |
+
*$py.class
|
| 11 |
+
|
| 12 |
+
# C extensions
|
| 13 |
+
*.so
|
| 14 |
+
|
| 15 |
+
# Distribution / packaging
|
| 16 |
+
.Python
|
| 17 |
+
build/
|
| 18 |
+
develop-eggs/
|
| 19 |
+
dist/
|
| 20 |
+
downloads/
|
| 21 |
+
eggs/
|
| 22 |
+
.eggs/
|
| 23 |
+
lib/
|
| 24 |
+
lib64/
|
| 25 |
+
parts/
|
| 26 |
+
sdist/
|
| 27 |
+
var/
|
| 28 |
+
wheels/
|
| 29 |
+
share/python-wheels/
|
| 30 |
+
*.egg-info/
|
| 31 |
+
.installed.cfg
|
| 32 |
+
*.egg
|
| 33 |
+
MANIFEST
|
| 34 |
+
|
| 35 |
+
# PyInstaller
|
| 36 |
+
# Usually these files are written by a python script from a template
|
| 37 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 38 |
+
*.manifest
|
| 39 |
+
*.spec
|
| 40 |
+
|
| 41 |
+
# Installer logs
|
| 42 |
+
pip-log.txt
|
| 43 |
+
pip-delete-this-directory.txt
|
| 44 |
+
|
| 45 |
+
# Unit test / coverage reports
|
| 46 |
+
htmlcov/
|
| 47 |
+
.tox/
|
| 48 |
+
.nox/
|
| 49 |
+
.coverage
|
| 50 |
+
.coverage.*
|
| 51 |
+
.cache
|
| 52 |
+
nosetests.xml
|
| 53 |
+
coverage.xml
|
| 54 |
+
*.cover
|
| 55 |
+
*.py.cover
|
| 56 |
+
.hypothesis/
|
| 57 |
+
.pytest_cache/
|
| 58 |
+
cover/
|
| 59 |
+
|
| 60 |
+
# Translations
|
| 61 |
+
*.mo
|
| 62 |
+
*.pot
|
| 63 |
+
|
| 64 |
+
# Django stuff:
|
| 65 |
+
*.log
|
| 66 |
+
local_settings.py
|
| 67 |
+
db.sqlite3
|
| 68 |
+
db.sqlite3-journal
|
| 69 |
+
|
| 70 |
+
# Flask stuff:
|
| 71 |
+
instance/
|
| 72 |
+
.webassets-cache
|
| 73 |
+
|
| 74 |
+
# Scrapy stuff:
|
| 75 |
+
.scrapy
|
| 76 |
+
|
| 77 |
+
# Sphinx documentation
|
| 78 |
+
docs/_build/
|
| 79 |
+
|
| 80 |
+
# PyBuilder
|
| 81 |
+
.pybuilder/
|
| 82 |
+
target/
|
| 83 |
+
|
| 84 |
+
# Jupyter Notebook
|
| 85 |
+
.ipynb_checkpoints
|
| 86 |
+
|
| 87 |
+
# IPython
|
| 88 |
+
profile_default/
|
| 89 |
+
ipython_config.py
|
| 90 |
+
|
| 91 |
+
# pyenv
|
| 92 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 93 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 94 |
+
# .python-version
|
| 95 |
+
|
| 96 |
+
# pipenv
|
| 97 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 98 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 99 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 100 |
+
# install all needed dependencies.
|
| 101 |
+
# Pipfile.lock
|
| 102 |
+
|
| 103 |
+
# UV
|
| 104 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
| 105 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 106 |
+
# commonly ignored for libraries.
|
| 107 |
+
# uv.lock
|
| 108 |
+
|
| 109 |
+
# poetry
|
| 110 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 111 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 112 |
+
# commonly ignored for libraries.
|
| 113 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 114 |
+
# poetry.lock
|
| 115 |
+
# poetry.toml
|
| 116 |
+
|
| 117 |
+
# pdm
|
| 118 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 119 |
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
| 120 |
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
| 121 |
+
# pdm.lock
|
| 122 |
+
# pdm.toml
|
| 123 |
+
.pdm-python
|
| 124 |
+
.pdm-build/
|
| 125 |
+
|
| 126 |
+
# pixi
|
| 127 |
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
| 128 |
+
# pixi.lock
|
| 129 |
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
| 130 |
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
| 131 |
+
.pixi
|
| 132 |
+
|
| 133 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 134 |
+
__pypackages__/
|
| 135 |
+
|
| 136 |
+
# Celery stuff
|
| 137 |
+
celerybeat-schedule
|
| 138 |
+
celerybeat.pid
|
| 139 |
+
|
| 140 |
+
# Redis
|
| 141 |
+
*.rdb
|
| 142 |
+
*.aof
|
| 143 |
+
*.pid
|
| 144 |
+
|
| 145 |
+
# RabbitMQ
|
| 146 |
+
mnesia/
|
| 147 |
+
rabbitmq/
|
| 148 |
+
rabbitmq-data/
|
| 149 |
+
|
| 150 |
+
# ActiveMQ
|
| 151 |
+
activemq-data/
|
| 152 |
+
|
| 153 |
+
# SageMath parsed files
|
| 154 |
+
*.sage.py
|
| 155 |
+
|
| 156 |
+
# Environments
|
| 157 |
+
.env
|
| 158 |
+
.envrc
|
| 159 |
+
.venv
|
| 160 |
+
env/
|
| 161 |
+
venv/
|
| 162 |
+
ENV/
|
| 163 |
+
env.bak/
|
| 164 |
+
venv.bak/
|
| 165 |
+
|
| 166 |
+
# Spyder project settings
|
| 167 |
+
.spyderproject
|
| 168 |
+
.spyproject
|
| 169 |
+
|
| 170 |
+
# Rope project settings
|
| 171 |
+
.ropeproject
|
| 172 |
+
|
| 173 |
+
# mkdocs documentation
|
| 174 |
+
/site
|
| 175 |
+
|
| 176 |
+
# mypy
|
| 177 |
+
.mypy_cache/
|
| 178 |
+
.dmypy.json
|
| 179 |
+
dmypy.json
|
| 180 |
+
|
| 181 |
+
# Pyre type checker
|
| 182 |
+
.pyre/
|
| 183 |
+
|
| 184 |
+
# pytype static type analyzer
|
| 185 |
+
.pytype/
|
| 186 |
+
|
| 187 |
+
# Cython debug symbols
|
| 188 |
+
cython_debug/
|
| 189 |
+
|
| 190 |
+
# PyCharm
|
| 191 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 192 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 193 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 194 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 195 |
+
# .idea/
|
| 196 |
+
|
| 197 |
+
# Abstra
|
| 198 |
+
# Abstra is an AI-powered process automation framework.
|
| 199 |
+
# Ignore directories containing user credentials, local state, and settings.
|
| 200 |
+
# Learn more at https://abstra.io/docs
|
| 201 |
+
.abstra/
|
| 202 |
+
|
| 203 |
+
# Visual Studio Code
|
| 204 |
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
| 205 |
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
| 206 |
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
| 207 |
+
# you could uncomment the following to ignore the entire vscode folder
|
| 208 |
+
# .vscode/
|
| 209 |
+
# Temporary file for partial code execution
|
| 210 |
+
tempCodeRunnerFile.py
|
| 211 |
+
|
| 212 |
+
# Ruff stuff:
|
| 213 |
+
.ruff_cache/
|
| 214 |
+
|
| 215 |
+
# PyPI configuration file
|
| 216 |
+
.pypirc
|
| 217 |
+
|
| 218 |
+
# Marimo
|
| 219 |
+
marimo/_static/
|
| 220 |
+
marimo/_lsp/
|
| 221 |
+
__marimo__/
|
| 222 |
+
|
| 223 |
+
# Streamlit
|
| 224 |
+
.streamlit/secrets.toml
|
README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: 'Rare Disease, Real Kid: MVA Hackathon 2026'
|
| 3 |
+
emoji: π§¬
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 6.24.0
|
| 8 |
+
python_version: '3.13'
|
| 9 |
+
app_file: app.py
|
| 10 |
+
hf_oauth: true
|
| 11 |
+
pinned: true
|
| 12 |
+
license: cc-by-4.0
|
| 13 |
+
short_description: Rare disease hackathon 2026
|
| 14 |
+
datasets:
|
| 15 |
+
- SageBio/mva-hackathon-2026-data
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
<h1 align="center">
|
| 19 |
+
|
| 20 |
+
𧬠Rare Disease, Real Kid: The MVA Hackathon 2026
|
| 21 |
+
|
| 22 |
+
</h1>
|
| 23 |
+
|
| 24 |
+
The genome and clinical story here belong to a real child living with Mosaic Variegated
|
| 25 |
+
Aneuploidy (MVA), an ultra-rare genetic condition affecting fewer than 50 people worldwide.
|
| 26 |
+
There is currently no established treatment, and care today means managing symptoms.
|
| 27 |
+
|
| 28 |
+
The family has opened the case to the research community, hoping someone can find an answer.
|
| 29 |
+
|
| 30 |
+
<h3 align="center">Help us understand MVA better!</h3>
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
*The MVA Hackathon is not intended to provide general medical care, diagnosis, or professional medical advice.*
|
app.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Rare Disease, Real Kid: The MVA Hackathon 2026
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
import base64
|
| 8 |
+
import mimetypes
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
import gradio as gr
|
| 12 |
+
|
| 13 |
+
from config import CHALLENGE_ACTIVE, SPONSOR_LOGOS
|
| 14 |
+
from tabs import (
|
| 15 |
+
about,
|
| 16 |
+
faq,
|
| 17 |
+
leaderboard,
|
| 18 |
+
rules,
|
| 19 |
+
submit_track1,
|
| 20 |
+
submit_track2,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _data_uri(path: str) -> str:
|
| 25 |
+
"""
|
| 26 |
+
Read a static asset and return a base64 data URI - works locally and on HF Spaces.
|
| 27 |
+
Returns an empty string if the file doesn't exist (e.g. before web UI upload).
|
| 28 |
+
"""
|
| 29 |
+
p = Path(path)
|
| 30 |
+
if not p.exists():
|
| 31 |
+
return ""
|
| 32 |
+
mime = mimetypes.guess_type(p.name)[0] or "application/octet-stream"
|
| 33 |
+
encoded = base64.b64encode(p.read_bytes()).decode()
|
| 34 |
+
return f"data:{mime};base64,{encoded}"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
IMG_CHILD = _data_uri("static/child-drawing.png")
|
| 38 |
+
_sponsor_imgs = [
|
| 39 |
+
(_data_uri(path), alt, cls) for path, alt, cls in SPONSOR_LOGOS
|
| 40 |
+
]
|
| 41 |
+
_sponsor_html = "\n".join(
|
| 42 |
+
f'<img src="{src}" alt="{alt}"' + (f' class="{cls}"' if cls else '') + '>'
|
| 43 |
+
for src, alt, cls in _sponsor_imgs
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
# Load CSS from file and wrap it for injection.
|
| 47 |
+
_css = Path("static/styles.css").read_text()
|
| 48 |
+
GLOBAL_STYLES = f"""<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 49 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 50 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;0,9..40,800;1,9..40,400&display=swap" rel="stylesheet">
|
| 51 |
+
<style>
|
| 52 |
+
{_css}
|
| 53 |
+
</style>
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
HERO_HTML = f"""
|
| 59 |
+
{GLOBAL_STYLES}
|
| 60 |
+
<div id="page-top"></div>
|
| 61 |
+
<div class="challenge-hero">
|
| 62 |
+
<div class="badge">𧬠Rare Disease Hackathon</div>
|
| 63 |
+
<h1>Rare Disease, Real Kid: The MVA Hackathon 2026</h1>
|
| 64 |
+
<p>
|
| 65 |
+
The genome and clinical story on this page belong to a real child living with
|
| 66 |
+
Mosaic Variegated Aneuploidy (MVA), an ultra-rare genetic condition affecting fewer than
|
| 67 |
+
50 people worldwide. There is currently no established treatment, and care today means
|
| 68 |
+
managing symptoms.
|
| 69 |
+
</p>
|
| 70 |
+
<p class="hero-lede">
|
| 71 |
+
The family has opened the case to the research community, hoping someone can find an answer.
|
| 72 |
+
</p>
|
| 73 |
+
<h3>Help us understand MVA better!</h3>
|
| 74 |
+
<div class="sponsor-row">
|
| 75 |
+
<span class="sponsor-row-label">Made possible by</span>
|
| 76 |
+
<div class="sponsor-row-logos">
|
| 77 |
+
{_sponsor_html}
|
| 78 |
+
</div>
|
| 79 |
+
<br/>
|
| 80 |
+
<span class="hero-disclaimer">
|
| 81 |
+
The MVA Hackathon is not intended to provide general medical care, diagnosis, or professional
|
| 82 |
+
medical advice.
|
| 83 |
+
</span>
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
"""
|
| 87 |
+
|
| 88 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 89 |
+
# Main app
|
| 90 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 91 |
+
with gr.Blocks(title="Rare Disease, Real Kid: The MVA Hackathon 2026") as app:
|
| 92 |
+
gr.HTML(HERO_HTML)
|
| 93 |
+
if CHALLENGE_ACTIVE:
|
| 94 |
+
gr.Markdown("**Register now - sign in to participate!**", elem_classes=["login-cta"])
|
| 95 |
+
gr.LoginButton(elem_id="login-btn")
|
| 96 |
+
else:
|
| 97 |
+
gr.Markdown(
|
| 98 |
+
"_Registration coming soon!_",
|
| 99 |
+
elem_classes=["login-cta"],
|
| 100 |
+
)
|
| 101 |
+
with gr.Tabs():
|
| 102 |
+
about.render(img_child=IMG_CHILD)
|
| 103 |
+
if CHALLENGE_ACTIVE:
|
| 104 |
+
leaderboard.render()
|
| 105 |
+
quota_t1 = submit_track1.render()
|
| 106 |
+
status_t2 = submit_track2.render()
|
| 107 |
+
app.load(fn=submit_track1._quota_status, outputs=quota_t1)
|
| 108 |
+
app.load(fn=submit_track2._submission_status, outputs=status_t2)
|
| 109 |
+
faq.render()
|
| 110 |
+
rules.render()
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
if __name__ == "__main__":
|
| 115 |
+
app.launch(
|
| 116 |
+
theme=gr.themes.Soft(
|
| 117 |
+
primary_hue=gr.themes.colors.cyan,
|
| 118 |
+
secondary_hue=gr.themes.colors.blue,
|
| 119 |
+
),
|
| 120 |
+
css="footer.svelte-1ax1toq { display: none !important; }",
|
| 121 |
+
allowed_paths=["static/", "templates/"],
|
| 122 |
+
)
|
config.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration for the hackathon app.
|
| 2 |
+
|
| 3 |
+
In particular, the following constants are
|
| 4 |
+
GROUNDTRUTH_FILENAME - filename of the answer key in the private GT dataset
|
| 5 |
+
LEADERBOARD_HEADERS - columns displayed on the leaderboard
|
| 6 |
+
CHALLENGE_ACTIVE - set to False to hide leaderboard submission tabs
|
| 7 |
+
MAX_TRACK1_SUBMISSIONS - how many Track 1 uploads each participant (a HF user) is allowed
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from dotenv import load_dotenv
|
| 11 |
+
|
| 12 |
+
load_dotenv()
|
| 13 |
+
|
| 14 |
+
# URLs
|
| 15 |
+
DATASET_URL = "https://huggingface.co/datasets/SageBio/mva-hackathon-2026-data"
|
| 16 |
+
DISCUSSIONS_URL = "https://huggingface.co/spaces/SageBio/rare-disease-real-kid-mva-hackathon-2026/discussions"
|
| 17 |
+
|
| 18 |
+
# Sponsor logos
|
| 19 |
+
# Each entry: (path_to_static_file, alt_text, optional_extra_css_class)
|
| 20 |
+
# These logos will be loaded in app.py.
|
| 21 |
+
SPONSOR_LOGOS = [
|
| 22 |
+
("static/logos/mva-society-logo.jpg", "MVA Society", "sponsor-logo-rounded"),
|
| 23 |
+
("static/logos/sage-logo.svg", "Sage Bionetworks", "sponsor-logo-rounded"),
|
| 24 |
+
("static/logos/hf-logo.svg", "Hugging Face", "sponsor-logo-rounded"),
|
| 25 |
+
("static/logos/powered-by-aws.svg", "Powered by AWS", "sponsor-logo-wide"),
|
| 26 |
+
("static/logos/anthropic-logo.svg", "Anthropic", "sponsor-logo-rounded"),
|
| 27 |
+
("static/logos/BEACON_dark.png", "BEACON", ""),
|
| 28 |
+
]
|
| 29 |
+
|
| 30 |
+
# HF private datasets
|
| 31 |
+
LEADERBOARD_DATASET = "SageBio/mva-hackathon-2026-leaderboard"
|
| 32 |
+
GROUNDTRUTH_DATASET = "SageBio/mva-hackathon-2026-gt"
|
| 33 |
+
GROUNDTRUTH_FILENAME = "gold_standard_track1.json"
|
| 34 |
+
|
| 35 |
+
# HF submission folders
|
| 36 |
+
# Folder paths inside LEADERBOARD_DATASET; one JSON file is created per submission.
|
| 37 |
+
TRACK1_SUBMISSIONS_FOLDER = "track1/"
|
| 38 |
+
TRACK2_SUBMISSIONS_FOLDER = "track2/"
|
| 39 |
+
|
| 40 |
+
# Leaderboard display columns
|
| 41 |
+
LEADERBOARD_HEADERS = ["#", "Participant", "Rank Points", "F-max", "Model", "Submitted"]
|
| 42 |
+
|
| 43 |
+
# Challenge control
|
| 44 |
+
CHALLENGE_ACTIVE = True
|
| 45 |
+
MAX_TRACK1_SUBMISSIONS = 6
|
evaluation.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Track 1 (Variant Prediction) automated scoring.
|
| 3 |
+
|
| 4 |
+
Adapted from the CAGI6 Rare Genomes Project Challenge assessment methodology
|
| 5 |
+
(Stenton et al., 2024), simplified for a single proband ("N-of-1") with a
|
| 6 |
+
clinically validated compound-heterozygous answer key.
|
| 7 |
+
|
| 8 |
+
Submission format (CSV):
|
| 9 |
+
proband_id,chrom_1,pos_1,ref_1,alt_1,chrom_2,pos_2,ref_2,alt_2,epcr
|
| 10 |
+
|
| 11 |
+
- One row per proposed causal variant (or compound-het variant pair).
|
| 12 |
+
- chrom_2/pos_2/ref_2/alt_2 are left blank for single-variant proposals.
|
| 13 |
+
- epcr = Estimated Probability of Causal Relationship, in (0, 1].
|
| 14 |
+
- Up to 10 rows per proband. Rows do NOT need to be pre-sorted by the
|
| 15 |
+
submitter; this script sorts by epcr descending before scoring
|
| 16 |
+
(but will warn if the file wasn't already sorted, since real CAGI
|
| 17 |
+
submissions are expected to be pre-ranked).
|
| 18 |
+
|
| 19 |
+
Two metrics are computed, matching CAGI's two numeric metrics:
|
| 20 |
+
1. Rank points - weighted score based on the rank position of the
|
| 21 |
+
true causal variant(s) in the submission.
|
| 22 |
+
2. F-max - the maximum F-measure (harmonic mean of precision
|
| 23 |
+
and recall) for causal-variant identification, swept across all
|
| 24 |
+
unique EPCR thresholds present in the submission.
|
| 25 |
+
|
| 26 |
+
Because there is only one proband, "mean rank points across probands"
|
| 27 |
+
collapses to a single score, and F-max is computed by treating each
|
| 28 |
+
individual variant (not each row) as a classifiable unit - this keeps
|
| 29 |
+
the metric meaningful even at N=1, since a compound-het true answer
|
| 30 |
+
still has two independent variants to be recovered.
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
from __future__ import annotations
|
| 34 |
+
|
| 35 |
+
import csv
|
| 36 |
+
from dataclasses import dataclass
|
| 37 |
+
|
| 38 |
+
Variant = tuple[str, int, str, str] # (chrom, pos, ref, alt)
|
| 39 |
+
|
| 40 |
+
# Rank-point scale, compressed from CAGI's RGP weighting to fit a single
|
| 41 |
+
# proband with a max of 10 submitted candidates (rather than CAGI's
|
| 42 |
+
# multi-family, up-to-100-candidate benchmark).
|
| 43 |
+
RANK_POINT_TIERS = [
|
| 44 |
+
(1, 100),
|
| 45 |
+
(3, 50),
|
| 46 |
+
(5, 25),
|
| 47 |
+
(10, 10),
|
| 48 |
+
]
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@dataclass
|
| 52 |
+
class SubmissionRow:
|
| 53 |
+
variants: frozenset[Variant] # 1 element = single variant, 2 = compound het
|
| 54 |
+
epcr: float
|
| 55 |
+
rank: int # 1-indexed position after sorting by epcr descending
|
| 56 |
+
finding_type: str = "primary" # "primary" or "secondary" - informational only,
|
| 57 |
+
# does not affect the automated score
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@dataclass
|
| 61 |
+
class ScoreResult:
|
| 62 |
+
proband_id: str
|
| 63 |
+
full_match_rank: int | None
|
| 64 |
+
partial_match_rank: int | None
|
| 65 |
+
rank_points: float
|
| 66 |
+
f_max: float
|
| 67 |
+
f_max_threshold: float | None
|
| 68 |
+
n_predictions_at_f_max: int
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _parse_variant(chrom: str, pos: str, ref: str, alt: str) -> Variant | None:
|
| 72 |
+
if not chrom or not pos:
|
| 73 |
+
return None
|
| 74 |
+
return (chrom.strip(), int(pos), ref.strip().upper(), alt.strip().upper())
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def load_submission(csv_path: str) -> dict[str, list[SubmissionRow]]:
|
| 78 |
+
"""Load a Track 1 submission CSV and group rows by proband, sorted by EPCR desc."""
|
| 79 |
+
by_proband: dict[str, list[tuple[frozenset[Variant], float, str]]] = {}
|
| 80 |
+
|
| 81 |
+
with open(csv_path, newline="") as f:
|
| 82 |
+
reader = csv.DictReader(f)
|
| 83 |
+
for row in reader:
|
| 84 |
+
pid = row["proband_id"].strip()
|
| 85 |
+
v1 = _parse_variant(row["chrom_1"], row["pos_1"], row["ref_1"], row["alt_1"])
|
| 86 |
+
v2 = _parse_variant(row.get("chrom_2", ""), row.get("pos_2", ""),
|
| 87 |
+
row.get("ref_2", ""), row.get("alt_2", ""))
|
| 88 |
+
if v1 is None:
|
| 89 |
+
raise ValueError(f"Row missing primary variant for proband {pid}")
|
| 90 |
+
variants = frozenset([v1, v2]) if v2 else frozenset([v1])
|
| 91 |
+
epcr = float(row["epcr"])
|
| 92 |
+
if not (0 < epcr <= 1):
|
| 93 |
+
raise ValueError(f"EPCR {epcr} out of range (0,1] for proband {pid}")
|
| 94 |
+
finding_type = (row.get("finding_type") or "primary").strip().lower()
|
| 95 |
+
if finding_type not in ("primary", "secondary"):
|
| 96 |
+
raise ValueError(
|
| 97 |
+
f"finding_type must be 'primary' or 'secondary' for proband {pid}, "
|
| 98 |
+
f"got '{finding_type}'"
|
| 99 |
+
)
|
| 100 |
+
by_proband.setdefault(pid, []).append((variants, epcr, finding_type))
|
| 101 |
+
|
| 102 |
+
result: dict[str, list[SubmissionRow]] = {}
|
| 103 |
+
for pid, rows in by_proband.items():
|
| 104 |
+
if len(rows) > 10:
|
| 105 |
+
raise ValueError(f"Proband {pid} has {len(rows)} rows; max is 10")
|
| 106 |
+
# Sort descending by EPCR (ties broken by original order)
|
| 107 |
+
rows_sorted = sorted(enumerate(rows), key=lambda x: (-x[1][1], x[0]))
|
| 108 |
+
result[pid] = [
|
| 109 |
+
SubmissionRow(variants=v, epcr=e, rank=i + 1, finding_type=ft)
|
| 110 |
+
for i, (_, (v, e, ft)) in enumerate(rows_sorted)
|
| 111 |
+
]
|
| 112 |
+
return result
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def _rank_to_points(rank: int) -> int:
|
| 116 |
+
for max_rank, points in RANK_POINT_TIERS:
|
| 117 |
+
if rank <= max_rank:
|
| 118 |
+
return points
|
| 119 |
+
return 0
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def score_proband(
|
| 123 |
+
proband_id: str,
|
| 124 |
+
submission_rows: list[SubmissionRow],
|
| 125 |
+
true_variants: frozenset[Variant],
|
| 126 |
+
) -> ScoreResult:
|
| 127 |
+
"""
|
| 128 |
+
Score a single proband's submission against its known causal variant(s).
|
| 129 |
+
|
| 130 |
+
true_variants: frozenset of 1 variant (simple dominant/de novo case) or
|
| 131 |
+
2 variants (compound heterozygous recessive case).
|
| 132 |
+
"""
|
| 133 |
+
is_compound_het = len(true_variants) == 2
|
| 134 |
+
|
| 135 |
+
full_match_rank = None
|
| 136 |
+
partial_match_rank = None
|
| 137 |
+
|
| 138 |
+
for row in submission_rows:
|
| 139 |
+
if row.variants == true_variants:
|
| 140 |
+
full_match_rank = row.rank
|
| 141 |
+
break # first (best) full match is what counts
|
| 142 |
+
|
| 143 |
+
if is_compound_het and full_match_rank is None:
|
| 144 |
+
# Look for a row containing exactly one of the two true variants -
|
| 145 |
+
# either proposed alone, or paired with an incorrect second variant.
|
| 146 |
+
for row in submission_rows:
|
| 147 |
+
if row.variants & true_variants:
|
| 148 |
+
partial_match_rank = row.rank
|
| 149 |
+
break
|
| 150 |
+
|
| 151 |
+
# --- Rank points (metric 1) ---
|
| 152 |
+
if full_match_rank is not None:
|
| 153 |
+
rank_points = float(_rank_to_points(full_match_rank))
|
| 154 |
+
elif partial_match_rank is not None:
|
| 155 |
+
# CAGI-style partial credit: half weight for recovering only one
|
| 156 |
+
# of two causal variants in a compound-het pair.
|
| 157 |
+
rank_points = 0.5 * _rank_to_points(partial_match_rank)
|
| 158 |
+
else:
|
| 159 |
+
rank_points = 0.0
|
| 160 |
+
|
| 161 |
+
# --- F-max (metric 2), computed at the individual-variant level ---
|
| 162 |
+
thresholds = sorted({row.epcr for row in submission_rows}, reverse=True)
|
| 163 |
+
best_f = 0.0
|
| 164 |
+
best_threshold = None
|
| 165 |
+
best_n = 0
|
| 166 |
+
|
| 167 |
+
for t in thresholds:
|
| 168 |
+
predicted_variants: set[Variant] = set()
|
| 169 |
+
n_rows_at_threshold = 0
|
| 170 |
+
for row in submission_rows:
|
| 171 |
+
if row.epcr >= t:
|
| 172 |
+
predicted_variants |= row.variants
|
| 173 |
+
n_rows_at_threshold += 1
|
| 174 |
+
|
| 175 |
+
tp = len(predicted_variants & true_variants)
|
| 176 |
+
fp = len(predicted_variants - true_variants)
|
| 177 |
+
fn = len(true_variants - predicted_variants)
|
| 178 |
+
|
| 179 |
+
precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
|
| 180 |
+
recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0
|
| 181 |
+
f = (2 * precision * recall / (precision + recall)) if (precision + recall) > 0 else 0.0
|
| 182 |
+
|
| 183 |
+
if f > best_f:
|
| 184 |
+
best_f = f
|
| 185 |
+
best_threshold = t
|
| 186 |
+
best_n = n_rows_at_threshold
|
| 187 |
+
|
| 188 |
+
return ScoreResult(
|
| 189 |
+
proband_id=proband_id,
|
| 190 |
+
full_match_rank=full_match_rank,
|
| 191 |
+
partial_match_rank=partial_match_rank,
|
| 192 |
+
rank_points=rank_points,
|
| 193 |
+
f_max=best_f,
|
| 194 |
+
f_max_threshold=best_threshold,
|
| 195 |
+
n_predictions_at_f_max=best_n,
|
| 196 |
+
)
|
groundtruth.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Groundtruth loader for Track 1 evaluation.
|
| 3 |
+
|
| 4 |
+
* In production: load data from private HF dataset
|
| 5 |
+
* In local dev: use small hardcoded dataset (_LOCAL_FALLBACK)
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import json
|
| 11 |
+
import os
|
| 12 |
+
import warnings
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
from config import GROUNDTRUTH_DATASET, GROUNDTRUTH_FILENAME
|
| 16 |
+
|
| 17 |
+
# Fallback for local development only.
|
| 18 |
+
_LOCAL_FALLBACK: dict[str, frozenset] = {
|
| 19 |
+
"PROBAND01": frozenset([
|
| 20 |
+
("chr2", 12345678, "T", "G"),
|
| 21 |
+
("chr15", 12345678, "T", "G"),
|
| 22 |
+
]),
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def load_groundtruth() -> dict[str, frozenset]:
|
| 27 |
+
"""
|
| 28 |
+
Load the ground truth from a private HF dataset if HF_TOKEN is set,
|
| 29 |
+
otherwise warn and return the local fallback.
|
| 30 |
+
"""
|
| 31 |
+
token = os.environ.get("HF_TOKEN")
|
| 32 |
+
|
| 33 |
+
if not token:
|
| 34 |
+
warnings.warn(
|
| 35 |
+
"HF_TOKEN not set - using local fallback ground truth. "
|
| 36 |
+
"Set HF_TOKEN as a Space secret in production.",
|
| 37 |
+
stacklevel=2,
|
| 38 |
+
)
|
| 39 |
+
return _LOCAL_FALLBACK
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
from huggingface_hub import hf_hub_download
|
| 43 |
+
|
| 44 |
+
path = hf_hub_download(
|
| 45 |
+
repo_id=GROUNDTRUTH_DATASET,
|
| 46 |
+
filename=GROUNDTRUTH_FILENAME,
|
| 47 |
+
repo_type="dataset",
|
| 48 |
+
token=token,
|
| 49 |
+
)
|
| 50 |
+
raw: dict = json.loads(Path(path).read_text())
|
| 51 |
+
result = {}
|
| 52 |
+
for proband_id, entry in raw.items():
|
| 53 |
+
if proband_id.startswith("_"):
|
| 54 |
+
continue
|
| 55 |
+
if isinstance(entry, list):
|
| 56 |
+
result[proband_id] = frozenset(tuple(v) for v in entry)
|
| 57 |
+
else:
|
| 58 |
+
variants = entry.get("primary_variants", [])
|
| 59 |
+
result[proband_id] = frozenset(
|
| 60 |
+
(v["chrom"], v["pos"], v["ref"], v["alt"]) for v in variants
|
| 61 |
+
)
|
| 62 |
+
return result
|
| 63 |
+
except Exception as e:
|
| 64 |
+
warnings.warn(
|
| 65 |
+
f"Failed to load ground truth from {GROUNDTRUTH_DATASET}: {e}. "
|
| 66 |
+
"Falling back to local data.",
|
| 67 |
+
stacklevel=2,
|
| 68 |
+
)
|
| 69 |
+
return _LOCAL_FALLBACK
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.0.0
|
| 2 |
+
huggingface_hub>=0.22.0
|
| 3 |
+
python-dotenv
|
static/child-drawing.png
ADDED
|
Git LFS Details
|
static/logos/BEACON_dark.png
ADDED
|
|
Git LFS Details
|
static/logos/anthropic-logo.svg
ADDED
|
|
static/logos/aws-logo.svg.webp
ADDED
|
static/logos/beacon-logo.webp
ADDED
|
|
static/logos/hf-logo.svg
ADDED
|
|
static/logos/mva-society-logo.jpg
ADDED
|
static/logos/powered-by-aws.svg
ADDED
|
|
static/logos/sage-logo.svg
ADDED
|
|
static/styles.css
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ββ Color and themes ββ */
|
| 2 |
+
:root {
|
| 3 |
+
font-size: 17px;
|
| 4 |
+
--c-heading: #0a4f6e;
|
| 5 |
+
--c-text: #3a4f60;
|
| 6 |
+
--c-text-soft: #4a6070;
|
| 7 |
+
--c-text-muted: #3d6070;
|
| 8 |
+
--c-caption: #4a6272;
|
| 9 |
+
--c-link: #0d7fa5;
|
| 10 |
+
--c-bg-card: #f0f6fa;
|
| 11 |
+
--c-bg-alt: #ffffff;
|
| 12 |
+
--c-border: #c8dfe8;
|
| 13 |
+
--c-border-soft: #d0e8f0;
|
| 14 |
+
--c-fmt-bg: #e4f0f6;
|
| 15 |
+
--c-fmt-text: #3a5060;
|
| 16 |
+
--c-tip-bg: #1a2b3c;
|
| 17 |
+
--c-tip-fg: #ffffff;
|
| 18 |
+
/* prize cards */
|
| 19 |
+
--c-gold-bg: #fef9e7; --c-gold-br: #f5c842; --c-gold-tx: #7a5a00;
|
| 20 |
+
--c-silver-bg: #f4f6f7; --c-silver-br: #b0bec5; --c-silver-tx: #37474f;
|
| 21 |
+
--c-bronze-bg: #fdf0e8; --c-bronze-br: #e0926a; --c-bronze-tx: #7a3a10;
|
| 22 |
+
--c-special-bg: #edf6f0; --c-special-br: #5cb88a; --c-special-tx: #1a5c38;
|
| 23 |
+
}
|
| 24 |
+
.dark {
|
| 25 |
+
--c-heading: #7cc8e0;
|
| 26 |
+
--c-text: #b0cede;
|
| 27 |
+
--c-text-soft: #8aaabb;
|
| 28 |
+
--c-text-muted: #6a8898;
|
| 29 |
+
--c-caption: #4a6878;
|
| 30 |
+
--c-link: #4ab8d4;
|
| 31 |
+
--c-bg-card: #1a2d3e;
|
| 32 |
+
--c-bg-alt: #162030;
|
| 33 |
+
--c-border: #2a4558;
|
| 34 |
+
--c-border-soft: #223548;
|
| 35 |
+
--c-fmt-bg: #1a3448;
|
| 36 |
+
--c-fmt-text: #7a9aaa;
|
| 37 |
+
--c-tip-bg: #e8f0f6;
|
| 38 |
+
--c-tip-fg: #1a2b3c;
|
| 39 |
+
--c-gold-bg: #2a2010; --c-gold-br: #906020; --c-gold-tx: #d0a840;
|
| 40 |
+
--c-silver-bg: #1e2830; --c-silver-br: #506070; --c-silver-tx: #90aaba;
|
| 41 |
+
--c-bronze-bg: #2a1a10; --c-bronze-br: #905040; --c-bronze-tx: #b08060;
|
| 42 |
+
--c-special-bg: #10201a; --c-special-br: #306850; --c-special-tx: #60b880;
|
| 43 |
+
}
|
| 44 |
+
body, .gradio-container, button, input, select, textarea, table, th, td, p, h1, h2, h3, h4, li {
|
| 45 |
+
font-family: 'DM Sans', sans-serif !important;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/* ββ Hero ββ */
|
| 49 |
+
.challenge-hero {
|
| 50 |
+
background: linear-gradient(135deg, #0a4f6e 0%, #0d7fa5 60%, #12a89d 100%);
|
| 51 |
+
border-radius: 12px;
|
| 52 |
+
color: #fff !important;
|
| 53 |
+
padding: 2.5rem 2rem 2rem;
|
| 54 |
+
margin-bottom: 1rem;
|
| 55 |
+
text-align: center;
|
| 56 |
+
}
|
| 57 |
+
.challenge-hero h1 {
|
| 58 |
+
font-size: 1.9rem;
|
| 59 |
+
font-weight: 800;
|
| 60 |
+
margin: 0.5rem 0 0.75rem;
|
| 61 |
+
font-family: 'DM Sans', sans-serif !important;
|
| 62 |
+
color: #fff !important;
|
| 63 |
+
}
|
| 64 |
+
.challenge-hero p {
|
| 65 |
+
color: rgba(255,255,255,0.88) !important;
|
| 66 |
+
font-size: 1rem;
|
| 67 |
+
max-width: 700px;
|
| 68 |
+
margin: 0 auto 1.5rem;
|
| 69 |
+
}
|
| 70 |
+
.challenge-hero h3 { color: #fff !important; }
|
| 71 |
+
.challenge-hero .badge {
|
| 72 |
+
display: inline-block;
|
| 73 |
+
background: rgba(255,255,255,0.18);
|
| 74 |
+
border: 1px solid rgba(255,255,255,0.35);
|
| 75 |
+
border-radius: 999px;
|
| 76 |
+
padding: 0.25rem 0.9rem;
|
| 77 |
+
font-size: 0.82rem;
|
| 78 |
+
margin-bottom: 0.75rem;
|
| 79 |
+
letter-spacing: 0.04em;
|
| 80 |
+
}
|
| 81 |
+
/* ββ Hero sponsor row ββ */
|
| 82 |
+
.sponsor-row {
|
| 83 |
+
margin-top: 1.5rem;
|
| 84 |
+
display: flex;
|
| 85 |
+
flex-direction: column;
|
| 86 |
+
align-items: center;
|
| 87 |
+
gap: 0.6rem;
|
| 88 |
+
}
|
| 89 |
+
.sponsor-row-label {
|
| 90 |
+
font-size: 0.75rem;
|
| 91 |
+
color: rgba(255,255,255,0.55) !important;
|
| 92 |
+
text-transform: uppercase;
|
| 93 |
+
letter-spacing: 0.08em;
|
| 94 |
+
}
|
| 95 |
+
.sponsor-row-logos {
|
| 96 |
+
display: flex;
|
| 97 |
+
align-items: center;
|
| 98 |
+
justify-content: center;
|
| 99 |
+
gap: 2rem;
|
| 100 |
+
flex-wrap: wrap;
|
| 101 |
+
padding: 1rem 2rem;
|
| 102 |
+
background: rgba(255,255,255,0.92);
|
| 103 |
+
border-radius: 10px;
|
| 104 |
+
}
|
| 105 |
+
.sponsor-row-logos img {
|
| 106 |
+
height: 42px;
|
| 107 |
+
width: auto;
|
| 108 |
+
max-width: 150px;
|
| 109 |
+
object-fit: contain;
|
| 110 |
+
}
|
| 111 |
+
.sponsor-logo-rounded { border-radius: 4px; }
|
| 112 |
+
|
| 113 |
+
/* Login CTA */
|
| 114 |
+
.login-cta { text-align: center; margin: 0 !important; padding: 0 !important; }
|
| 115 |
+
#login-btn { margin-bottom: 1.5rem !important; }
|
| 116 |
+
|
| 117 |
+
/* ββ Tab navigation - centered ββ */
|
| 118 |
+
.tab-container {
|
| 119 |
+
justify-content: center !important;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/* ββ Mark / highlight ββ */
|
| 123 |
+
mark {
|
| 124 |
+
background: #fff3b0;
|
| 125 |
+
color: #3a3000;
|
| 126 |
+
padding: 0.1em 0.25em;
|
| 127 |
+
border-radius: 3px;
|
| 128 |
+
font-style: normal;
|
| 129 |
+
}
|
| 130 |
+
.dark mark {
|
| 131 |
+
background: #4a3a00;
|
| 132 |
+
color: #ffe880;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
/* ββ Content layout ββ */
|
| 136 |
+
.container {
|
| 137 |
+
max-width: 1040px;
|
| 138 |
+
margin: 0 auto;
|
| 139 |
+
padding: 0 1.5rem;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
/* ββ Sections ββ */
|
| 143 |
+
.section {
|
| 144 |
+
padding: 2.5rem 0;
|
| 145 |
+
}
|
| 146 |
+
main > .section:first-child {
|
| 147 |
+
padding-top: 1rem;
|
| 148 |
+
}
|
| 149 |
+
.section-alt {
|
| 150 |
+
background: var(--c-bg-alt);
|
| 151 |
+
border-radius: 8px;
|
| 152 |
+
}
|
| 153 |
+
.section-alt .container {
|
| 154 |
+
padding-left: 3rem;
|
| 155 |
+
padding-right: 3rem;
|
| 156 |
+
}
|
| 157 |
+
.section h2 {
|
| 158 |
+
font-size: 1.5rem;
|
| 159 |
+
font-weight: 700;
|
| 160 |
+
color: var(--c-heading);
|
| 161 |
+
margin-bottom: 1rem;
|
| 162 |
+
padding-bottom: 0.5rem;
|
| 163 |
+
border-bottom: 3px solid #0d7fa5;
|
| 164 |
+
display: inline-block;
|
| 165 |
+
}
|
| 166 |
+
.section p, .section li {
|
| 167 |
+
color: var(--c-text);
|
| 168 |
+
font-size: 1rem;
|
| 169 |
+
margin-top: 0.6rem;
|
| 170 |
+
}
|
| 171 |
+
.section ul, .section ol {
|
| 172 |
+
padding-left: 1.4rem;
|
| 173 |
+
margin-top: 0.8rem;
|
| 174 |
+
}
|
| 175 |
+
.section li { margin-bottom: 0.4rem; }
|
| 176 |
+
.section a { color: var(--c-link); text-decoration: none; }
|
| 177 |
+
.section a:hover { text-decoration: underline; }
|
| 178 |
+
|
| 179 |
+
/* ββ Small note / disclaimer text ββ */
|
| 180 |
+
.note-small, .section .note-small {
|
| 181 |
+
font-size: 0.88rem;
|
| 182 |
+
color: var(--c-text-muted);
|
| 183 |
+
margin-top: 0.6rem;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
/* ββ Card grid ββ */
|
| 187 |
+
.card-grid {
|
| 188 |
+
display: grid;
|
| 189 |
+
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
|
| 190 |
+
gap: 1.25rem;
|
| 191 |
+
margin-top: 1.5rem;
|
| 192 |
+
}
|
| 193 |
+
.info-card {
|
| 194 |
+
background: var(--c-bg-card);
|
| 195 |
+
border: 1px solid var(--c-border);
|
| 196 |
+
border-radius: 12px;
|
| 197 |
+
padding: 1.5rem 1.75rem;
|
| 198 |
+
}
|
| 199 |
+
.quota-card {
|
| 200 |
+
padding: 0.6rem 1rem;
|
| 201 |
+
border-radius: 8px;
|
| 202 |
+
font-size: 0.9rem;
|
| 203 |
+
display: inline-block;
|
| 204 |
+
margin-bottom: 0.5rem;
|
| 205 |
+
}
|
| 206 |
+
.info-card h3 {
|
| 207 |
+
font-size: 1rem;
|
| 208 |
+
font-weight: 700;
|
| 209 |
+
color: var(--c-heading);
|
| 210 |
+
margin-bottom: 0.3rem;
|
| 211 |
+
margin-top: 0;
|
| 212 |
+
}
|
| 213 |
+
.info-card p {
|
| 214 |
+
font-size: 0.9rem;
|
| 215 |
+
color: var(--c-text-soft);
|
| 216 |
+
margin: 0.3rem 0 0;
|
| 217 |
+
}
|
| 218 |
+
/* ββ Track cards ββ */
|
| 219 |
+
.track-card { display: flex; flex-direction: column; gap: 0.5rem; }
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
/* ββ Story section ββ */
|
| 223 |
+
.story-container {
|
| 224 |
+
display: flex;
|
| 225 |
+
align-items: flex-start;
|
| 226 |
+
gap: 2.5rem;
|
| 227 |
+
flex-wrap: wrap;
|
| 228 |
+
}
|
| 229 |
+
.story-text {
|
| 230 |
+
flex: 1 1 480px;
|
| 231 |
+
min-width: 280px;
|
| 232 |
+
}
|
| 233 |
+
.story-figure {
|
| 234 |
+
flex: 0 0 260px;
|
| 235 |
+
margin: 0;
|
| 236 |
+
text-align: center;
|
| 237 |
+
}
|
| 238 |
+
.story-drawing {
|
| 239 |
+
width: 100%;
|
| 240 |
+
max-width: 260px;
|
| 241 |
+
height: auto;
|
| 242 |
+
border-radius: 12px;
|
| 243 |
+
border: 1px solid var(--c-border);
|
| 244 |
+
box-shadow: 0 2px 12px rgba(10, 79, 110, 0.1);
|
| 245 |
+
}
|
| 246 |
+
.story-figure figcaption {
|
| 247 |
+
margin-top: 0.6rem;
|
| 248 |
+
font-size: 0.8rem;
|
| 249 |
+
color: var(--c-caption);
|
| 250 |
+
}
|
| 251 |
+
@media (max-width: 720px) {
|
| 252 |
+
.story-container { flex-direction: column; }
|
| 253 |
+
.story-figure { order: -1; flex: 0 0 auto; max-width: 220px; margin: 0 auto; }
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
/* ββ Prize grid ββ */
|
| 257 |
+
.prize-grid {
|
| 258 |
+
display: grid;
|
| 259 |
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
| 260 |
+
gap: 1rem;
|
| 261 |
+
margin-top: 1.25rem;
|
| 262 |
+
}
|
| 263 |
+
.prize-card {
|
| 264 |
+
border-radius: 12px;
|
| 265 |
+
padding: 1.25rem 1rem;
|
| 266 |
+
text-align: center;
|
| 267 |
+
border: 1px solid transparent;
|
| 268 |
+
}
|
| 269 |
+
.prize-place {
|
| 270 |
+
font-size: 0.85rem;
|
| 271 |
+
font-weight: 600;
|
| 272 |
+
text-transform: uppercase;
|
| 273 |
+
letter-spacing: 0.05em;
|
| 274 |
+
margin-bottom: 0.4rem;
|
| 275 |
+
}
|
| 276 |
+
.prize-amount {
|
| 277 |
+
font-size: 0.9rem;
|
| 278 |
+
font-weight: 800;
|
| 279 |
+
}
|
| 280 |
+
.prize-card.gold { background: var(--c-gold-bg); border-color: var(--c-gold-br); color: var(--c-gold-tx); }
|
| 281 |
+
.prize-card.silver { background: var(--c-silver-bg); border-color: var(--c-silver-br); color: var(--c-silver-tx); }
|
| 282 |
+
.prize-card.bronze { background: var(--c-bronze-bg); border-color: var(--c-bronze-br); color: var(--c-bronze-tx); }
|
| 283 |
+
.prize-card.special { background: var(--c-special-bg); border-color: var(--c-special-br); color: var(--c-special-tx); }
|
| 284 |
+
|
| 285 |
+
/* ββ Steps list ββ */
|
| 286 |
+
.steps {
|
| 287 |
+
list-style: none;
|
| 288 |
+
padding: 0;
|
| 289 |
+
margin-top: 1.25rem;
|
| 290 |
+
display: flex;
|
| 291 |
+
flex-direction: column;
|
| 292 |
+
gap: 1.25rem;
|
| 293 |
+
}
|
| 294 |
+
.steps li {
|
| 295 |
+
display: flex;
|
| 296 |
+
align-items: flex-start;
|
| 297 |
+
gap: 1rem;
|
| 298 |
+
margin: 0;
|
| 299 |
+
}
|
| 300 |
+
.step-num {
|
| 301 |
+
min-width: 2.2rem;
|
| 302 |
+
height: 2.2rem;
|
| 303 |
+
display: flex;
|
| 304 |
+
align-items: center;
|
| 305 |
+
justify-content: center;
|
| 306 |
+
background: #0d7fa5;
|
| 307 |
+
color: #fff;
|
| 308 |
+
border-radius: 50%;
|
| 309 |
+
font-weight: 700;
|
| 310 |
+
font-size: 1rem;
|
| 311 |
+
flex-shrink: 0;
|
| 312 |
+
}
|
| 313 |
+
.steps strong {
|
| 314 |
+
display: block;
|
| 315 |
+
color: var(--c-heading);
|
| 316 |
+
font-size: 1rem;
|
| 317 |
+
margin-bottom: 0.2rem;
|
| 318 |
+
}
|
| 319 |
+
.steps p {
|
| 320 |
+
margin: 0;
|
| 321 |
+
color: var(--c-text-soft);
|
| 322 |
+
font-size: 0.95rem;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
/* ββ Sub-headings inside cards ββ */
|
| 326 |
+
.sub-heading {
|
| 327 |
+
font-size: 1rem;
|
| 328 |
+
font-weight: 700;
|
| 329 |
+
color: var(--c-heading);
|
| 330 |
+
margin: 1rem 0 0.5rem;
|
| 331 |
+
padding-bottom: 0.25rem;
|
| 332 |
+
border-bottom: 2px solid var(--c-border);
|
| 333 |
+
display: block;
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
/* ββ Judging criteria ββ */
|
| 337 |
+
.criteria-list {
|
| 338 |
+
display: flex;
|
| 339 |
+
flex-direction: column;
|
| 340 |
+
gap: 0.6rem;
|
| 341 |
+
margin-top: 0.75rem;
|
| 342 |
+
}
|
| 343 |
+
.criterion {
|
| 344 |
+
display: flex;
|
| 345 |
+
flex-direction: column;
|
| 346 |
+
gap: 0.2rem;
|
| 347 |
+
padding-bottom: 0.75rem;
|
| 348 |
+
border-bottom: 1px solid var(--c-border-soft);
|
| 349 |
+
}
|
| 350 |
+
.criterion:last-child {
|
| 351 |
+
border-bottom: none;
|
| 352 |
+
padding-bottom: 0;
|
| 353 |
+
}
|
| 354 |
+
.criteria-stacked-bar {
|
| 355 |
+
display: flex;
|
| 356 |
+
height: 14px;
|
| 357 |
+
border-radius: 999px;
|
| 358 |
+
overflow: visible;
|
| 359 |
+
margin: 0.75rem 0 1rem;
|
| 360 |
+
gap: 2px;
|
| 361 |
+
}
|
| 362 |
+
.csb-seg {
|
| 363 |
+
flex: 0 0 var(--w);
|
| 364 |
+
background: var(--c);
|
| 365 |
+
position: relative;
|
| 366 |
+
cursor: default;
|
| 367 |
+
transition: filter 0.15s ease;
|
| 368 |
+
}
|
| 369 |
+
.criteria-stacked-bar > .csb-seg:first-child { border-radius: 999px 0 0 999px; }
|
| 370 |
+
.criteria-stacked-bar > .csb-seg:last-child { border-radius: 0 999px 999px 0; }
|
| 371 |
+
.csb-seg:hover { filter: brightness(1.15); z-index: 1; }
|
| 372 |
+
.csb-seg::after {
|
| 373 |
+
content: attr(data-tip);
|
| 374 |
+
position: absolute;
|
| 375 |
+
bottom: calc(100% + 8px);
|
| 376 |
+
left: 50%;
|
| 377 |
+
transform: translateX(-50%);
|
| 378 |
+
background: var(--c-tip-bg);
|
| 379 |
+
color: var(--c-tip-fg);
|
| 380 |
+
font-size: 0.78rem;
|
| 381 |
+
white-space: nowrap;
|
| 382 |
+
padding: 0.3rem 0.6rem;
|
| 383 |
+
border-radius: 5px;
|
| 384 |
+
pointer-events: none;
|
| 385 |
+
opacity: 0;
|
| 386 |
+
transition: opacity 0.15s ease;
|
| 387 |
+
z-index: 10;
|
| 388 |
+
}
|
| 389 |
+
.csb-seg:hover::after { opacity: 1; }
|
| 390 |
+
.criterion-swatch {
|
| 391 |
+
display: inline-block;
|
| 392 |
+
width: 10px;
|
| 393 |
+
height: 10px;
|
| 394 |
+
border-radius: 2px;
|
| 395 |
+
background: var(--c);
|
| 396 |
+
margin-right: 0.45rem;
|
| 397 |
+
vertical-align: middle;
|
| 398 |
+
flex-shrink: 0;
|
| 399 |
+
}
|
| 400 |
+
.criterion-label {
|
| 401 |
+
display: flex;
|
| 402 |
+
justify-content: space-between;
|
| 403 |
+
align-items: center;
|
| 404 |
+
font-size: 0.9rem;
|
| 405 |
+
color: var(--c-text);
|
| 406 |
+
}
|
| 407 |
+
.criterion-label strong { color: var(--c-heading); }
|
| 408 |
+
.criterion-desc {
|
| 409 |
+
font-size: 0.83rem;
|
| 410 |
+
color: var(--c-text-muted);
|
| 411 |
+
margin: 0.1rem 0 0;
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
/* ββ Track goal accent ββ */
|
| 415 |
+
.track-goal {
|
| 416 |
+
border-left: 3px solid #0d7fa5;
|
| 417 |
+
border-radius: 0 6px 6px 0;
|
| 418 |
+
background: var(--c-fmt-bg);
|
| 419 |
+
color: var(--c-text);
|
| 420 |
+
padding: 0.65rem 1rem;
|
| 421 |
+
margin: 0.5rem 0 0.9rem;
|
| 422 |
+
font-size: 0.95rem;
|
| 423 |
+
}
|
| 424 |
+
.track-goal strong { color: var(--c-heading); }
|
| 425 |
+
|
| 426 |
+
/* ββ Timeline table ββ */
|
| 427 |
+
.timeline-table {
|
| 428 |
+
width: 100%;
|
| 429 |
+
border-collapse: collapse;
|
| 430 |
+
margin-top: 1.25rem;
|
| 431 |
+
font-size: 0.95rem;
|
| 432 |
+
color: var(--c-text);
|
| 433 |
+
}
|
| 434 |
+
.timeline-table th {
|
| 435 |
+
text-align: left;
|
| 436 |
+
padding: 0.6rem 1rem;
|
| 437 |
+
font-weight: 700;
|
| 438 |
+
color: var(--c-heading);
|
| 439 |
+
border-bottom: 2px solid #0d7fa5;
|
| 440 |
+
background: transparent;
|
| 441 |
+
}
|
| 442 |
+
.timeline-table td {
|
| 443 |
+
padding: 0.65rem 1rem;
|
| 444 |
+
border-bottom: 1px solid var(--c-border-soft);
|
| 445 |
+
vertical-align: top;
|
| 446 |
+
}
|
| 447 |
+
.timeline-table tbody tr:last-child td {
|
| 448 |
+
border-bottom: none;
|
| 449 |
+
}
|
| 450 |
+
.timeline-table tbody tr:hover td {
|
| 451 |
+
background: var(--c-bg-card);
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
/* ββ Back to top ββ */
|
| 455 |
+
.back-to-top-row { text-align: center; padding: 2.5rem 0 1rem; }
|
| 456 |
+
.back-to-top {
|
| 457 |
+
display: inline-block !important;
|
| 458 |
+
padding: 0.55rem 1.6rem !important;
|
| 459 |
+
border-radius: 8px !important;
|
| 460 |
+
font-weight: 600 !important;
|
| 461 |
+
font-size: 0.92rem !important;
|
| 462 |
+
font-family: 'DM Sans', sans-serif !important;
|
| 463 |
+
text-decoration: none !important;
|
| 464 |
+
background: transparent !important;
|
| 465 |
+
color: #0a4f6e !important;
|
| 466 |
+
border: 2px solid #0a4f6e !important;
|
| 467 |
+
}
|
| 468 |
+
.back-to-top:hover { background: #0a4f6e !important; color: #fff !important; }
|
| 469 |
+
.back-to-top:visited { color: #0a4f6e !important; }
|
| 470 |
+
|
| 471 |
+
/* ββ FAQ Accordions ββ */
|
| 472 |
+
.faq-accordion {
|
| 473 |
+
background: var(--c-bg-card) !important;
|
| 474 |
+
border: 1px solid var(--c-border) !important;
|
| 475 |
+
border-left: 3px solid var(--c-link) !important;
|
| 476 |
+
border-radius: 10px !important;
|
| 477 |
+
margin-bottom: 0.55rem !important;
|
| 478 |
+
overflow: hidden;
|
| 479 |
+
box-shadow: none !important;
|
| 480 |
+
}
|
| 481 |
+
.faq-accordion .label-wrap {
|
| 482 |
+
padding: 0.2rem 0.4rem !important;
|
| 483 |
+
}
|
| 484 |
+
.faq-accordion button,
|
| 485 |
+
.faq-accordion button span,
|
| 486 |
+
.faq-accordion button p,
|
| 487 |
+
.faq-accordion button div {
|
| 488 |
+
font-size: 1rem !important;
|
| 489 |
+
font-weight: 700 !important;
|
| 490 |
+
color: var(--c-heading) !important;
|
| 491 |
+
font-family: 'DM Sans', sans-serif !important;
|
| 492 |
+
}
|
| 493 |
+
.faq-accordion .gap {
|
| 494 |
+
border-top: 1px solid var(--c-border-soft) !important;
|
| 495 |
+
padding: 0.85rem 1.1rem !important;
|
| 496 |
+
}
|
| 497 |
+
.faq-accordion .gap p,
|
| 498 |
+
.faq-accordion .gap li {
|
| 499 |
+
color: var(--c-text) !important;
|
| 500 |
+
font-size: 0.95rem !important;
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
/* ββ Overrides for markdown ββ */
|
| 504 |
+
.prose p, .prose li {
|
| 505 |
+
font-size: 1rem !important;
|
| 506 |
+
color: var(--c-text);
|
| 507 |
+
}
|
| 508 |
+
.prose h2 { font-size: 1.4rem !important; color: var(--c-heading); }
|
| 509 |
+
.prose h3 { font-size: 1.1rem !important; color: var(--c-heading); }
|
| 510 |
+
.prose h4 { font-size: 1rem !important; color: var(--c-heading); }
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
/* ββ Layout and legibility fixes ββ */
|
| 514 |
+
|
| 515 |
+
/* Hero was full-bleed while all content sits in a 1040px column. */
|
| 516 |
+
.challenge-hero {
|
| 517 |
+
max-width: 1040px;
|
| 518 |
+
margin-left: auto;
|
| 519 |
+
margin-right: auto;
|
| 520 |
+
padding: 3rem 2rem 2.25rem;
|
| 521 |
+
border-radius: 16px;
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
/* Login button was rendering full viewport width (1856px on a 1920px screen). */
|
| 525 |
+
#login-btn {
|
| 526 |
+
max-width: 320px !important;
|
| 527 |
+
width: 100% !important;
|
| 528 |
+
margin: 0.35rem auto 1.75rem !important;
|
| 529 |
+
display: flex !important;
|
| 530 |
+
align-items: center !important;
|
| 531 |
+
justify-content: center !important;
|
| 532 |
+
gap: 0.5rem !important;
|
| 533 |
+
white-space: nowrap !important;
|
| 534 |
+
padding: 0.7rem 1.25rem !important;
|
| 535 |
+
font-size: 1rem !important;
|
| 536 |
+
line-height: 1.2 !important;
|
| 537 |
+
}
|
| 538 |
+
#login-btn * {
|
| 539 |
+
display: inline !important;
|
| 540 |
+
white-space: nowrap !important;
|
| 541 |
+
}
|
| 542 |
+
.login-cta p { font-size: 1.02rem !important; }
|
| 543 |
+
|
| 544 |
+
/* The medical disclaimer previously reused .sponsor-row-label and inherited its
|
| 545 |
+
55% opacity. Uppercase and tracking are kept; contrast and leading are raised
|
| 546 |
+
so the line is actually readable. Own class so it no longer changes whenever
|
| 547 |
+
the sponsor label is restyled. */
|
| 548 |
+
.hero-disclaimer {
|
| 549 |
+
display: block;
|
| 550 |
+
max-width: 700px;
|
| 551 |
+
margin: 1.35rem auto 0;
|
| 552 |
+
font-size: 0.75rem;
|
| 553 |
+
line-height: 1.7;
|
| 554 |
+
text-transform: uppercase;
|
| 555 |
+
letter-spacing: 0.08em;
|
| 556 |
+
color: rgba(255, 255, 255, 0.72) !important;
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
/* Lift the sponsor panel off the gradient. */
|
| 560 |
+
.sponsor-row-logos {
|
| 561 |
+
background: rgba(255, 255, 255, 0.96);
|
| 562 |
+
padding: 0.9rem 1.75rem;
|
| 563 |
+
gap: 1.75rem;
|
| 564 |
+
box-shadow: 0 2px 14px rgba(0, 0, 0, 0.18);
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
/* Gradio applies padding: 2px 8px to every anchor, which opens a visible gap
|
| 568 |
+
before the following comma or period. Reset it for body copy links, but keep
|
| 569 |
+
the back-to-top control, which is styled as a button. */
|
| 570 |
+
.section a:not(.back-to-top),
|
| 571 |
+
.prose a:not(.back-to-top) {
|
| 572 |
+
padding: 0 !important;
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
/* The family's decision reads as its own statement rather than a clause
|
| 576 |
+
buried at the end of the description. */
|
| 577 |
+
.challenge-hero .hero-lede {
|
| 578 |
+
margin-top: 0.9rem !important;
|
| 579 |
+
margin-bottom: 1.25rem !important;
|
| 580 |
+
font-size: 1.02rem !important;
|
| 581 |
+
color: rgba(255, 255, 255, 0.96) !important;
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
/* Centered text stops working past about three lines: every line starts at a
|
| 585 |
+
different x-position, so the eye has to hunt for each one. The headline,
|
| 586 |
+
badge and status chip stay centered; the body copy does not. Desktop is
|
| 587 |
+
unaffected, where the paragraph caps at 700px and lands at three lines. */
|
| 588 |
+
@media (max-width: 640px) {
|
| 589 |
+
.challenge-hero p,
|
| 590 |
+
.challenge-hero .hero-disclaimer {
|
| 591 |
+
text-align: left;
|
| 592 |
+
}
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
/* The Powered by AWS lockup is a 5.7:1 horizontal mark. The default 150px cap would
|
| 596 |
+
render it at about 26px tall beside 42px logos, so it gets a wider ceiling. */
|
| 597 |
+
.sponsor-row-logos img.sponsor-logo-wide {
|
| 598 |
+
max-width: 250px;
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
/* Sponsor marks range from 1:1 to 6.15:1. With object-fit: contain, a tight max-width
|
| 602 |
+
makes wide wordmarks render shorter than square ones: Sage was showing 24px tall beside
|
| 603 |
+
42px logos. Normalise on a height at which every logo renders uncapped on one row. */
|
| 604 |
+
.sponsor-row-logos {
|
| 605 |
+
gap: 1.5rem;
|
| 606 |
+
}
|
| 607 |
+
.sponsor-row-logos img {
|
| 608 |
+
height: 36px;
|
| 609 |
+
max-width: 280px;
|
| 610 |
+
}
|
static/templates/methods_description_form.xlsx
ADDED
|
Binary file (14.3 kB). View file
|
|
|
static/templates/track1_submission_template.csv
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
proband_id,chrom_1,pos_1,ref_1,alt_1,chrom_2,pos_2,ref_2,alt_2,epcr,finding_type,notes
|
| 2 |
+
PROBAND01,chr1,100000,A,T,chr1,200000,G,C,0.90,primary,
|
| 3 |
+
PROBAND01,chr7,300000,C,T,,,,,0.30,secondary,Incidental finding unrelated to primary phenotype
|
tabs/about.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Overview/About tab.
|
| 2 |
+
|
| 3 |
+
Due to the logos and need for layout control, this tab is written in raw HTML
|
| 4 |
+
rather than markdown. To update text content, edit _build_html() below.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import gradio as gr
|
| 10 |
+
|
| 11 |
+
from config import DATASET_URL, DISCUSSIONS_URL
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _build_html(img_child: str) -> str:
|
| 15 |
+
story_figure = f"""
|
| 16 |
+
<figure class="story-figure">
|
| 17 |
+
<img
|
| 18 |
+
src="{img_child}"
|
| 19 |
+
alt="A child's drawing of himself smiling in a hospital bed"
|
| 20 |
+
class="story-drawing">
|
| 21 |
+
<figcaption>Illustration courtesy of the
|
| 22 |
+
<a href="https://mvasociety.org" target="_blank" rel="noopener">MVA Society</a>
|
| 23 |
+
</figcaption>
|
| 24 |
+
</figure>""" if img_child else ""
|
| 25 |
+
|
| 26 |
+
return f"""
|
| 27 |
+
<main>
|
| 28 |
+
|
| 29 |
+
<section id="about" class="section">
|
| 30 |
+
<div class="container">
|
| 31 |
+
<p>
|
| 32 |
+
Every person's DNA contains millions of tiny variations, and almost all of them are harmless. But
|
| 33 |
+
every so often, one of those variations disrupts something important, and the result is a disease.
|
| 34 |
+
For many rare diseases, doctors and researchers recognize the symptoms but don't know the exact
|
| 35 |
+
genetic variations responsible for them.
|
| 36 |
+
</p>
|
| 37 |
+
<p>
|
| 38 |
+
This MVA Hackathon releases real genetic and clinical data, shared with full, explicit
|
| 39 |
+
permission from the family it belongs to, and asks the global research community to help make sense
|
| 40 |
+
of it.
|
| 41 |
+
</p>
|
| 42 |
+
<p>
|
| 43 |
+
Access to the data is controlled. The research outputs are open: participant
|
| 44 |
+
submissions, code, and reports are released under CC BY 4.0, so methods developed for this
|
| 45 |
+
case can be reused for other undiagnosed individuals.
|
| 46 |
+
</p>
|
| 47 |
+
</div>
|
| 48 |
+
</section>
|
| 49 |
+
|
| 50 |
+
<section id="story" class="section section-alt">
|
| 51 |
+
<div class="container story-container">
|
| 52 |
+
<div class="story-text">
|
| 53 |
+
<h2>Why This Hackathon Exists</h2>
|
| 54 |
+
<p>
|
| 55 |
+
Behind this project is a family who made a deeply personal decision: to share their child's most
|
| 56 |
+
private information - their genome - with strangers around the world, in the hope that someone,
|
| 57 |
+
somewhere, might help understand their child's rare condition.
|
| 58 |
+
</p>
|
| 59 |
+
<p>
|
| 60 |
+
Their child lives with <strong>Mosaic Variegated Aneuploidy (MVA)</strong>, a condition few
|
| 61 |
+
clinicians will ever encounter in their careers. There is currently no established treatment, but
|
| 62 |
+
the MVA Society is funding focused research to bring treatments closer. Care today means managing
|
| 63 |
+
symptoms and fighting cancer cells.
|
| 64 |
+
</p>
|
| 65 |
+
</div>
|
| 66 |
+
{story_figure}
|
| 67 |
+
</div>
|
| 68 |
+
</section>
|
| 69 |
+
|
| 70 |
+
<section id="tracks" class="section">
|
| 71 |
+
<div class="container">
|
| 72 |
+
<h2>Two Tracks</h2>
|
| 73 |
+
<div class="card-grid">
|
| 74 |
+
|
| 75 |
+
<div class="info-card track-card">
|
| 76 |
+
<h3>𧬠Track 1 - Variant Prediction</h3>
|
| 77 |
+
<div class="track-goal">
|
| 78 |
+
<strong>The Goal:</strong> Predict the specific genetic variant(s) that may be driving the child's
|
| 79 |
+
condition using their clinical symptoms and genomic data.
|
| 80 |
+
</div>
|
| 81 |
+
<p>
|
| 82 |
+
You'll be given the child's genomic data (FASTQ + VCF) and a description of their symptoms, and asked to
|
| 83 |
+
predict which specific variant(s) may be causing the condition. Secondary or incidental findings are
|
| 84 |
+
welcome too.
|
| 85 |
+
</p>
|
| 86 |
+
<p>
|
| 87 |
+
Your ranked predictions are automatically checked against the clinically confirmed answer
|
| 88 |
+
and your score updates live on the public leaderboard.
|
| 89 |
+
</p>
|
| 90 |
+
<ul class="note-small">
|
| 91 |
+
<li>What you submit: A ranked list of variant predictions, a Github link, and a methods writeup.</li>
|
| 92 |
+
<li>Submission limit: 6</li>
|
| 93 |
+
</ul>
|
| 94 |
+
<span class="sub-heading">Scoring</span>
|
| 95 |
+
<p>
|
| 96 |
+
Track 1 is scored automatically on two primary metrics:
|
| 97 |
+
</p>
|
| 98 |
+
<ul>
|
| 99 |
+
<li><strong>Rank points</strong> - How high did you rank the true variant(s). If your top-ranked
|
| 100 |
+
variant(s) is correct, you'll get 100pts. If you get the correct variant(s) within your top 10
|
| 101 |
+
ranked ones, you'll get partial credit. If you correctly identify one of the two variants in a
|
| 102 |
+
heterozygous pair, you'll get half credit.</li>
|
| 103 |
+
<li><strong>F-max</strong> - the best balance of precision and recall your submission achieves at
|
| 104 |
+
any confidence threshold, rewarding submissions that pinpoint the right answer without burying it
|
| 105 |
+
in noise. Secondary or incidental findings won't hurt your automated score.</li>
|
| 106 |
+
</ul>
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
+
<div class="info-card track-card">
|
| 110 |
+
<h3>π Track 2 - Drug Repurposing</h3>
|
| 111 |
+
<div class="track-goal">
|
| 112 |
+
<strong>The Goal:</strong> Identify existing, approved medications that are plausible
|
| 113 |
+
candidates for further investigation, based on the biological mechanisms disrupted in this
|
| 114 |
+
case. Submissions are hypotheses for follow-up, not evidence that a medicine works.
|
| 115 |
+
</div>
|
| 116 |
+
<p>
|
| 117 |
+
Once you know (or have a strong guess at) what biological mechanisms are involved, the next question
|
| 118 |
+
is: Does an existing medication work on this biological problem?
|
| 119 |
+
</p>
|
| 120 |
+
<p>
|
| 121 |
+
For this track, you'll characterize the mechanism behind the variant(s) and write up your reasoning
|
| 122 |
+
and proposed candidate medication(s). An independent panel of expert judges evaluates submissions on
|
| 123 |
+
scientific merit and real-world potential.
|
| 124 |
+
</p>
|
| 125 |
+
<ul class="note-small">
|
| 126 |
+
<li>What you submit: A detailed report, a Github link, and a 3-minute pitch video</li>
|
| 127 |
+
<li>Submission limit: 1</li>
|
| 128 |
+
</ul>
|
| 129 |
+
<span class="sub-heading">Scoring</span>
|
| 130 |
+
<p>
|
| 131 |
+
Track 2 is scored by our expert panel across four weighted criteria:
|
| 132 |
+
</p>
|
| 133 |
+
<!-- Hiding for now, I dont think it adds much value
|
| 134 |
+
<div class="criteria-stacked-bar" aria-hidden="true">
|
| 135 |
+
<div class="csb-seg" style="--w:35%; --c:#1f9bea" data-tip="Scientific Rigor - 35%"></div>
|
| 136 |
+
<div class="csb-seg" style="--w:25%; --c:#59c959" data-tip="Potential Impact - 25%"></div>
|
| 137 |
+
<div class="csb-seg" style="--w:25%; --c:#7b5ea7" data-tip="Innovation - 25%"></div>
|
| 138 |
+
<div class="csb-seg" style="--w:15%; --c:#e0509a" data-tip="Scalability - 15%"></div>
|
| 139 |
+
</div>
|
| 140 |
+
-->
|
| 141 |
+
<div class="criteria-list">
|
| 142 |
+
<div class="criterion">
|
| 143 |
+
<div class="criterion-label">
|
| 144 |
+
<span>
|
| 145 |
+
<span class="criterion-swatch" style="--c:#1f9bea"></span>
|
| 146 |
+
<strong>Scientific Rigor</strong>
|
| 147 |
+
</span>
|
| 148 |
+
<strong>35%</strong>
|
| 149 |
+
</div>
|
| 150 |
+
<p class="criterion-desc">
|
| 151 |
+
Is the variant mechanism characterization sound, and is the proposed drug repurposing
|
| 152 |
+
candidate well-supported by that mechanism?
|
| 153 |
+
</p>
|
| 154 |
+
</div>
|
| 155 |
+
<div class="criterion">
|
| 156 |
+
<div class="criterion-label">
|
| 157 |
+
<span>
|
| 158 |
+
<span class="criterion-swatch" style="--c:#59c959"></span>
|
| 159 |
+
<strong>Potential Impact</strong>
|
| 160 |
+
</span>
|
| 161 |
+
<strong>25%</strong>
|
| 162 |
+
</div>
|
| 163 |
+
<p class="criterion-desc">
|
| 164 |
+
If independently validated, how much could this contribute to understanding or
|
| 165 |
+
diagnosing MVA, for this child or for others living with it?
|
| 166 |
+
</p>
|
| 167 |
+
</div>
|
| 168 |
+
<div class="criterion">
|
| 169 |
+
<div class="criterion-label">
|
| 170 |
+
<span>
|
| 171 |
+
<span class="criterion-swatch" style="--c:#7b5ea7"></span>
|
| 172 |
+
<strong>Innovation</strong>
|
| 173 |
+
</span>
|
| 174 |
+
<strong>25%</strong>
|
| 175 |
+
</div>
|
| 176 |
+
<p class="criterion-desc">
|
| 177 |
+
Did you bring a genuinely creative angle, method, or tool to the problem?
|
| 178 |
+
</p>
|
| 179 |
+
</div>
|
| 180 |
+
<div class="criterion">
|
| 181 |
+
<div class="criterion-label">
|
| 182 |
+
<span>
|
| 183 |
+
<span class="criterion-swatch" style="--c:#e0509a"></span>
|
| 184 |
+
<strong>Scalability</strong>
|
| 185 |
+
</span>
|
| 186 |
+
<strong>15%</strong>
|
| 187 |
+
</div>
|
| 188 |
+
<p class="criterion-desc">
|
| 189 |
+
Could your approach realistically be applied to other individuals, other diseases, or larger
|
| 190 |
+
datasets - not just this one case?
|
| 191 |
+
</p>
|
| 192 |
+
</div>
|
| 193 |
+
</div>
|
| 194 |
+
</div>
|
| 195 |
+
|
| 196 |
+
</div>
|
| 197 |
+
</div>
|
| 198 |
+
</section>
|
| 199 |
+
|
| 200 |
+
<section id="prizes" class="section">
|
| 201 |
+
<div class="container">
|
| 202 |
+
<h2>Prizes</h2>
|
| 203 |
+
<p>
|
| 204 |
+
<strong>$50,000 total</strong> - $25,000 in cash from the AWS Imagine Grant program + $25,000 in Claude credits from Anthropic*.
|
| 205 |
+
</p>
|
| 206 |
+
<div class="prize-grid">
|
| 207 |
+
<div class="prize-card gold">
|
| 208 |
+
<div class="prize-place">π₯ 1st Place</div>
|
| 209 |
+
<div class="prize-amount">1x $12,000 cash</div>
|
| 210 |
+
<div class="prize-amount">1x $12,000 credits</div>
|
| 211 |
+
</div>
|
| 212 |
+
<div class="prize-card silver">
|
| 213 |
+
<div class="prize-place">π₯ 2nd Place</div>
|
| 214 |
+
<div class="prize-amount">1x $7,000 cash</div>
|
| 215 |
+
<div class="prize-amount">1x $7,000 credits</div>
|
| 216 |
+
</div>
|
| 217 |
+
<div class="prize-card bronze">
|
| 218 |
+
<div class="prize-place">π₯ 3rd Place</div>
|
| 219 |
+
<div class="prize-amount">1x $4,000 cash</div>
|
| 220 |
+
<div class="prize-amount">1x $4,000 credits</div>
|
| 221 |
+
</div>
|
| 222 |
+
<div class="prize-card special">
|
| 223 |
+
<div class="prize-place">π Innovation</div>
|
| 224 |
+
<div class="prize-amount">1x $2,000 cash</div>
|
| 225 |
+
<div class="prize-amount">1x $2,000 credits</div>
|
| 226 |
+
</div>
|
| 227 |
+
</div>
|
| 228 |
+
<p class="note-small"><em>
|
| 229 |
+
*Anthropic credits are provided for use with Claude Science, intended to help winning teams deepen
|
| 230 |
+
their computational analysis and extend their findings on MVA beyond the Hackathon itself, not as a
|
| 231 |
+
cash-equivalent award.
|
| 232 |
+
</em></p>
|
| 233 |
+
</div>
|
| 234 |
+
</section>
|
| 235 |
+
|
| 236 |
+
<section id="how-to-participate" class="section section-alt">
|
| 237 |
+
<div class="container">
|
| 238 |
+
<h2>How to Participate</h2>
|
| 239 |
+
<p>
|
| 240 |
+
Anyone can join, and no previous hackathon experience is needed. The tracks are built for a
|
| 241 |
+
mix of backgrounds: ML engineers and computational biologists, clinicians and geneticists,
|
| 242 |
+
students and citizen scientists.
|
| 243 |
+
</p>
|
| 244 |
+
<ol class="steps">
|
| 245 |
+
<li>
|
| 246 |
+
<span class="step-num">1</span>
|
| 247 |
+
<div>
|
| 248 |
+
<strong>Sign in</strong>
|
| 249 |
+
<p>
|
| 250 |
+
Log in with your Hugging Face account. Don't have one? <a href="https://huggingface.co/join" target="_blank" rel="noopener">Create one for free</a>.
|
| 251 |
+
</p>
|
| 252 |
+
</div>
|
| 253 |
+
</li>
|
| 254 |
+
<li>
|
| 255 |
+
<span class="step-num">2</span>
|
| 256 |
+
<div>
|
| 257 |
+
<strong>Get the data</strong>
|
| 258 |
+
<p>
|
| 259 |
+
Because the data comes from a real person, you must first request access. Fill out the
|
| 260 |
+
short form on the <a href="{DATASET_URL}" target="_blank" rel="noopener">dataset page</a>
|
| 261 |
+
confirming you understand and agree to the data usage rules. The data is shared under a
|
| 262 |
+
protocol approved by WCG IRB (protocol #20252010).
|
| 263 |
+
</p>
|
| 264 |
+
</div>
|
| 265 |
+
</li>
|
| 266 |
+
<li>
|
| 267 |
+
<span class="step-num">3</span>
|
| 268 |
+
<div>
|
| 269 |
+
<strong>Choose your track</strong>
|
| 270 |
+
<p>
|
| 271 |
+
Decide whether you want to try identifying the causal variant (Track 1), propose a
|
| 272 |
+
repurposed medication (Track 2), or take on both - many teams do. You don't need to
|
| 273 |
+
finish one to start the other.
|
| 274 |
+
</p>
|
| 275 |
+
</div>
|
| 276 |
+
</li>
|
| 277 |
+
<li>
|
| 278 |
+
<span class="step-num">4</span>
|
| 279 |
+
<div>
|
| 280 |
+
<strong>Build and submit</strong>
|
| 281 |
+
<p>
|
| 282 |
+
Work through the data at your own pace using whatever tools, software, or methods you prefer.
|
| 283 |
+
When ready, navigate to the "Submit - Track 1" or "Submit - Track 2" tabs for instructions and
|
| 284 |
+
submission guidelines.
|
| 285 |
+
</p>
|
| 286 |
+
</div>
|
| 287 |
+
</li>
|
| 288 |
+
<li>
|
| 289 |
+
<span class="step-num">5</span>
|
| 290 |
+
<div>
|
| 291 |
+
<strong>Join the community</strong>
|
| 292 |
+
<p>
|
| 293 |
+
Have questions or want to connect with other participants? Join the conversation on the
|
| 294 |
+
<a href="{DISCUSSIONS_URL}" target="_blank" rel="noopener">Community page</a>.
|
| 295 |
+
</p>
|
| 296 |
+
</div>
|
| 297 |
+
</li>
|
| 298 |
+
</ol>
|
| 299 |
+
</div>
|
| 300 |
+
</section>
|
| 301 |
+
|
| 302 |
+
<section id="timeline" class="section">
|
| 303 |
+
<div class="container">
|
| 304 |
+
<h2>Timeline</h2>
|
| 305 |
+
<p>
|
| 306 |
+
Dates are subject to change. Any updates to key milestones will be reflected in this table
|
| 307 |
+
posted to the <a href="{DISCUSSIONS_URL}" target="_blank" rel="noopener">Community page</a>.
|
| 308 |
+
</p>
|
| 309 |
+
<table class="timeline-table">
|
| 310 |
+
<thead>
|
| 311 |
+
<tr>
|
| 312 |
+
<th>Date</th>
|
| 313 |
+
<th>Milestone</th>
|
| 314 |
+
</tr>
|
| 315 |
+
</thead>
|
| 316 |
+
<tbody>
|
| 317 |
+
<tr>
|
| 318 |
+
<td>August 24</td>
|
| 319 |
+
<td>Hackathon launch; dataset available for request and download</td>
|
| 320 |
+
</tr>
|
| 321 |
+
<tr>
|
| 322 |
+
<td>August 25</td>
|
| 323 |
+
<td>Submissions open</td>
|
| 324 |
+
</tr>
|
| 325 |
+
<tr>
|
| 326 |
+
<td>October 24 (23:59 UTC)</td>
|
| 327 |
+
<td>Submissions close; Track 1 leaderboard frozen</td>
|
| 328 |
+
</tr>
|
| 329 |
+
<tr>
|
| 330 |
+
<td>October 24 - November 24</td>
|
| 331 |
+
<td>Track 1 qualitative evaluation; Track 2 expert panel judging</td>
|
| 332 |
+
</tr>
|
| 333 |
+
<tr>
|
| 334 |
+
<td>November 25</td>
|
| 335 |
+
<td>Winners announced; prizes awarded</td>
|
| 336 |
+
</tr>
|
| 337 |
+
</tbody>
|
| 338 |
+
</table>
|
| 339 |
+
</div>
|
| 340 |
+
</section>
|
| 341 |
+
|
| 342 |
+
<section id="acknowledgements" class="section section-alt">
|
| 343 |
+
<div class="container">
|
| 344 |
+
<h2>Acknowledgements</h2>
|
| 345 |
+
<p>
|
| 346 |
+
We are deeply grateful to the child and their family who chose to share their genomic data openly
|
| 347 |
+
with the research community. This hackathon would not exist without their generosity and trust.
|
| 348 |
+
</p>
|
| 349 |
+
<p>
|
| 350 |
+
This hackathon was organized in partnership with
|
| 351 |
+
<a href="https://sagebionetworks.org" target="_blank" rel="noopener">Sage Bionetworks</a>,
|
| 352 |
+
the <a href="https://mvasociety.org" target="_blank" rel="noopener">MVA Society</a>,
|
| 353 |
+
<a href="https://huggingface.co" target="_blank" rel="noopener">Hugging Face</a>,
|
| 354 |
+
and <a href="https://conscience.ca/beacon/" target="_blank" rel="noopener">BEACON</a>
|
| 355 |
+
(The Benchmarking, Evaluation, and Assessment Consortium for Science),
|
| 356 |
+
with prize sponsorship from
|
| 357 |
+
<a href="https://aws.amazon.com" target="_blank" rel="noopener">AWS</a>
|
| 358 |
+
and <a href="https://www.anthropic.com" target="_blank" rel="noopener">Anthropic</a>.
|
| 359 |
+
</p>
|
| 360 |
+
<p>
|
| 361 |
+
We thank the <a href="https://wilhelmfoundation.org" target="_blank" rel="noopener">Wilhelm Foundation</a>
|
| 362 |
+
for their guidance and insight on this project.
|
| 363 |
+
</p>
|
| 364 |
+
</div>
|
| 365 |
+
</section>
|
| 366 |
+
|
| 367 |
+
<div class="back-to-top-row">
|
| 368 |
+
<a class="back-to-top" href="javascript:void(0)" onclick="document.getElementById('page-top').scrollIntoView({{behavior:'smooth'}})">β Back to top</a>
|
| 369 |
+
</div>
|
| 370 |
+
|
| 371 |
+
</main>
|
| 372 |
+
"""
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
def render(img_child: str = "") -> None:
|
| 376 |
+
with gr.Tab("Overview"):
|
| 377 |
+
gr.HTML(_build_html(img_child))
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
|
tabs/faq.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FAQ tab."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import gradio as gr
|
| 6 |
+
|
| 7 |
+
from config import DATASET_URL, DISCUSSIONS_URL
|
| 8 |
+
|
| 9 |
+
FAQ_ITEMS = [
|
| 10 |
+
(
|
| 11 |
+
"Who can participate?",
|
| 12 |
+
"Anyone with a Hugging Face account can participate - data scientists, ML engineers, clinicians, "
|
| 13 |
+
"students, and citizen scientists alike. You must be at least 18 years old. See the **Official Rules** "
|
| 14 |
+
"tab for details.",
|
| 15 |
+
),
|
| 16 |
+
(
|
| 17 |
+
"Can I participate in both tracks?",
|
| 18 |
+
"Yes. Track 1 and Track 2 are scored independently. You can submit to one or both.",
|
| 19 |
+
),
|
| 20 |
+
(
|
| 21 |
+
"Can I participate as a team?",
|
| 22 |
+
"Yes.\n\nEach team member must register individually with their own Hugging Face account. For Track 1, each "
|
| 23 |
+
"team member may submit independently and your best score counts. For Track 2, designate one person to submit "
|
| 24 |
+
"on the team's behalf - only one Track 2 submission per team is accepted, and additional submissions from "
|
| 25 |
+
"other team members will be ignored.",
|
| 26 |
+
),
|
| 27 |
+
(
|
| 28 |
+
"How is Track 1 scored?",
|
| 29 |
+
"Submissions are scored the way large-scale rare disease benchmarks (like Stenton et al., 2024) score solved "
|
| 30 |
+
"cases applied to this single, clinically confirmed answer.\n\nTwo metrics are computed automatically: **rank points** "
|
| 31 |
+
"(based on how high the true variant(s) land in your ranked list, with partial credit if you recover only one of "
|
| 32 |
+
"two compound-heterozygous variants) and **F-max** (the best precision/recall balance across your submitted"
|
| 33 |
+
"confidence thresholds).",
|
| 34 |
+
),
|
| 35 |
+
(
|
| 36 |
+
"Are secondary or incidental findings scored?",
|
| 37 |
+
"They won't hurt your automated Track 1 score β include them as additional rows if you'd like. They're set aside "
|
| 38 |
+
"for qualitative review by the judging panel rather than folded into the automated rank points/F-max calculation, "
|
| 39 |
+
"since there's no fixed \"correct\" answer for secondary findings the way there is for the primary causal variant.\n\n"
|
| 40 |
+
"Use the optional `notes` column to briefly explain why you're flagging it (e.g. \"well-established pathogenic "
|
| 41 |
+
"variant, unrelated to primary phenotype, recommend clinical follow-up\")",
|
| 42 |
+
|
| 43 |
+
),
|
| 44 |
+
(
|
| 45 |
+
"How is Track 2 scored?",
|
| 46 |
+
"Reports are reviewed by an independent panel, and will be judged on scientific rigor, potential impact, innovation, "
|
| 47 |
+
"and scalability.",
|
| 48 |
+
),
|
| 49 |
+
(
|
| 50 |
+
"How many submissions can I make?",
|
| 51 |
+
"Track 1 allows up to **6 submissions** per participant; only your highest-scoring submission appears on the "
|
| 52 |
+
"leaderboard. Track 2 accepts **one final submission** per participant. No re-submissions allowed, so make "
|
| 53 |
+
"them count.",
|
| 54 |
+
),
|
| 55 |
+
(
|
| 56 |
+
"What compute resources are available?",
|
| 57 |
+
"This Space runs on CPU-basic hardware. You are welcome to use your own compute for training, as only "
|
| 58 |
+
"the final submission files need to be uploaded here.",
|
| 59 |
+
),
|
| 60 |
+
(
|
| 61 |
+
"What is the data license?",
|
| 62 |
+
"The underlying dataset is **gated** and subject to the terms of the Hackathon Rules and Data Transfer Agreement "
|
| 63 |
+
"- access requires approval, redistribution is prohibited, and all data must be deleted upon conclusion of the "
|
| 64 |
+
"Hackathon.\n\nParticipant submissions and results (predictions, code, reports) are released under **CC BY 4.0** and "
|
| 65 |
+
"may be reused with attribution.",
|
| 66 |
+
),
|
| 67 |
+
(
|
| 68 |
+
"When will results be announced?",
|
| 69 |
+
"Final rankings will be announced after the ~2-3 month judging window closes. The exact date will be posted in the "
|
| 70 |
+
f"[Community tab]({DISCUSSIONS_URL}).",
|
| 71 |
+
),
|
| 72 |
+
(
|
| 73 |
+
"I have a question not answered here.",
|
| 74 |
+
f"Post in the [Discussions tab]({DISCUSSIONS_URL}) and the community or organizers will respond!",
|
| 75 |
+
),
|
| 76 |
+
]
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def render() -> None:
|
| 80 |
+
with gr.Tab("FAQ"):
|
| 81 |
+
gr.Markdown("## Frequently Asked Questions")
|
| 82 |
+
for question, answer in FAQ_ITEMS:
|
| 83 |
+
with gr.Accordion(question, open=False, elem_classes=["faq-accordion"]):
|
| 84 |
+
gr.Markdown(answer)
|
| 85 |
+
gr.HTML('<div class="back-to-top-row"><a class="back-to-top" href="javascript:void(0)" onclick="document.getElementById(\'page-top\').scrollIntoView({behavior:\'smooth\'})">β Back to top</a></div>')
|
tabs/leaderboard.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Leaderboard tab for Track 1."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import gradio as gr
|
| 6 |
+
|
| 7 |
+
from config import LEADERBOARD_HEADERS, CHALLENGE_ACTIVE
|
| 8 |
+
from utils import leaderboard_df
|
| 9 |
+
|
| 10 |
+
LEADERBOARD_MD = """
|
| 11 |
+
## Track 1 Leaderboard
|
| 12 |
+
|
| 13 |
+
Rankings update automatically as valid Track 1 submissions are scored. Only your team's highest-scoring
|
| 14 |
+
submission is displayed.
|
| 15 |
+
|
| 16 |
+
**Metrics:** Rank Points & F-max (adapted from _Stenton et al., 2024_)
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def render() -> None:
|
| 21 |
+
with gr.Tab("Leaderboard"):
|
| 22 |
+
gr.Markdown(LEADERBOARD_MD)
|
| 23 |
+
lb_table = gr.Dataframe(
|
| 24 |
+
value=leaderboard_df,
|
| 25 |
+
headers=LEADERBOARD_HEADERS,
|
| 26 |
+
datatype=["str"] * 6,
|
| 27 |
+
interactive=False,
|
| 28 |
+
wrap=True,
|
| 29 |
+
every=60 if CHALLENGE_ACTIVE else None,
|
| 30 |
+
)
|
| 31 |
+
refresh_btn = gr.Button("β» Refresh leaderboard", variant="secondary", size="sm")
|
| 32 |
+
refresh_btn.click(fn=leaderboard_df, inputs=[], outputs=lb_table)
|
tabs/rules.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rules tab."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import gradio as gr
|
| 6 |
+
|
| 7 |
+
RULES_MD = """
|
| 8 |
+
## Official Rules
|
| 9 |
+
|
| 10 |
+
To participate in this Hackathon, you must review and accept the following terms. Please read carefully and proceed
|
| 11 |
+
only if you agree to comply with the conditions below.
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
### Terms of Service & Eligibility
|
| 16 |
+
- You must abide by the [Hugging Face Terms of Service](https://huggingface.co/terms-of-service) and by these Hackathon
|
| 17 |
+
Rules.
|
| 18 |
+
- You must be 18 years of age or older to participate.
|
| 19 |
+
- Each member of your Hackathon team must individually register as a participant and agree to these Hackathon Rules.
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
### Data Privacy & Recontact Restrictions
|
| 24 |
+
- You agree that you will not attempt to recontact the data subject, data subject family members, or any points of
|
| 25 |
+
contact at the MVA Society.
|
| 26 |
+
- Your use of the data will comply with all applicable laws, rules, regulations, and professional standards.
|
| 27 |
+
- If any unauthorized disclosure of the data occurs or is suspected, you agree to contact Sage Bionetworks' Privacy and
|
| 28 |
+
Compliance Office via Sage's Help Center.
|
| 29 |
+
- You will not release or otherwise grant data access to anyone, and you will establish appropriate safeguards to
|
| 30 |
+
prevent unauthorized data use.
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
### Hackathon Flow:
|
| 35 |
+
|
| 36 |
+
1. <u>Registration:</u> Participants must register with Hugging Face and request access to the data, including a valid
|
| 37 |
+
email address and attestation to accepting these Hackathon Rules.
|
| 38 |
+
|
| 39 |
+
2. <u>Hackathon Tasks:</u> The Hackathon consists of two tasks:
|
| 40 |
+
- Track 1 - Variant Identification: identify the disease-causing variant(s) using genetic variant information and
|
| 41 |
+
phenotypic presentations.
|
| 42 |
+
- Track 2 - Drug Repurposing: leveraging the genetic information and variant prediction from Task 1, identify
|
| 43 |
+
existing market-approved medications whose mechanism of action may target the disease-causing pathway(s).
|
| 44 |
+
|
| 45 |
+
3. <u>Team Formation:</u> Teams are not required. Participants may form teams based on skills and interests.
|
| 46 |
+
|
| 47 |
+
4. <u>Data and Resource Provision:</u>
|
| 48 |
+
- File types: genomic data in VCF format; raw sequencing data optionally in BAM/CRAM. Phenotypic data as
|
| 49 |
+
standardized HPO terms.
|
| 50 |
+
- Data size: ~85 GB (single-subject dataset).
|
| 51 |
+
- Access: participants request access via Hugging Face's gated-dataset mechanism. No data may be reshared through
|
| 52 |
+
any channel.
|
| 53 |
+
- Compute: participants are responsible for their own compute environment and any associated costs.
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
5. <u>Development and Prototyping:</u>
|
| 57 |
+
* Teams work intensively to develop a prototype or solution that addresses the Hackathon.
|
| 58 |
+
* This involves brainstorming, coding, data analysis, and design.
|
| 59 |
+
|
| 60 |
+
6. <u>Data Deletion:</u>
|
| 61 |
+
- Data retention after the Hackathon is not permitted for any purpose.
|
| 62 |
+
- All data must be deleted within 30 days of Hackathon close from all environments (local machines, cloud instances,
|
| 63 |
+
notebooks, private repos, and any intermediate or derived datasets).
|
| 64 |
+
- <mark>All participants must email RarediseaserealkidMVAhackathon2026@synapse.org to confirm data has been deleted.
|
| 65 |
+
**If you do not contact us, we may contact you directly in 30 days**</mark>
|
| 66 |
+
|
| 67 |
+
7. <u>Presentations & Judging:</u>
|
| 68 |
+
- Each team's submission includes a written report, a GitHub repository, and a 3-minute recorded pitch video.
|
| 69 |
+
- A panel of expert judges - researchers, clinicians, and patient advocates - will review all Track 2 submissions.
|
| 70 |
+
- Judging takes place over approximately 2-3 months following submission close.
|
| 71 |
+
- Presentations are pre-recorded; there is no live Q&A round.
|
| 72 |
+
|
| 73 |
+
8. <u>Evaluation and Outcome:</u>
|
| 74 |
+
- Track 1 - Variant Prediction (automated): scored automatically against the NHS-validated causal variant(s) using
|
| 75 |
+
rank points and F-max.
|
| 76 |
+
- Track 2 - Drug Repurposing (panel-judged):
|
| 77 |
+
* Scientific Rigor (35%)
|
| 78 |
+
* Potential Impact (25%)
|
| 79 |
+
* Innovation (25%)
|
| 80 |
+
* Scalability (15%)
|
| 81 |
+
|
| 82 |
+
9. <u>Prizes:</u>
|
| 83 |
+
The total prize pool is $50,000, sponsored by the AWS Imagine Grant program and Anthropic:
|
| 84 |
+
* π₯ 1st Place: $12,000 cash + $12,000 Claude credits
|
| 85 |
+
* π₯ 2nd Place: $7,000 cash + $7,000 Claude credits
|
| 86 |
+
* π₯ 3rd Place: $4,000 cash + $4,000 Claude credits
|
| 87 |
+
* π Innovation/Community Award: $2,000 cash + $2,000 Claude credits
|
| 88 |
+
|
| 89 |
+
The Innovation/Community Award will be awarded at the judges' discretion to a submission demonstrating
|
| 90 |
+
exceptional creativity, community impact, or patient-centered design, independent of overall placement
|
| 91 |
+
|
| 92 |
+
10. <u>Research Progress & Post-Hackathon Data Handling:</u>
|
| 93 |
+
- Participants may publish and present on Hackathon results, subject to the embargo policy below.
|
| 94 |
+
- All data must be deleted from participants' systems within 30 days of Hackathon close.
|
| 95 |
+
- Anonymized results remain available to the research community via Synapse and/or Hugging Face after the Hackathon
|
| 96 |
+
ends.
|
| 97 |
+
- Submissions are released under a CC-BY license.
|
| 98 |
+
|
| 99 |
+
11. <u>Embargo Policy:</u>
|
| 100 |
+
- The embargo period begins at Hackathon close and ends upon public posting of the hackathon summary report or
|
| 101 |
+
preprint by the organizing team.
|
| 102 |
+
- During the embargo period, participants may not submit manuscripts for peer-reviewed publication that use the
|
| 103 |
+
hackathon dataset.
|
| 104 |
+
- Participants are free to publicly share their code, models, and derived outputs at any time.
|
| 105 |
+
- Participants may present preliminary findings in conference abstracts or posters with prior written approval from
|
| 106 |
+
the organizers.
|
| 107 |
+
|
| 108 |
+
---
|
| 109 |
+
|
| 110 |
+
### Submission Reuse:
|
| 111 |
+
|
| 112 |
+
By registering, participants acknowledge that submissions may be rerun by the Hackathon organizers. Submissions are
|
| 113 |
+
subject to a CC-BY license, and your name will be shared as part of the open-access attribution.
|
| 114 |
+
|
| 115 |
+
---
|
| 116 |
+
|
| 117 |
+
### Recontact:
|
| 118 |
+
|
| 119 |
+
By providing your email address and participating, you consent to being recontacted by the Hackathon organizers for
|
| 120 |
+
purposes related to the Hackathon.
|
| 121 |
+
|
| 122 |
+
---
|
| 123 |
+
|
| 124 |
+
### Post-Hackathon Information Sharing:
|
| 125 |
+
|
| 126 |
+
All submitted results will be accessible to the broader research community to foster collaboration and innovacation.
|
| 127 |
+
Leading submissions may be prominently featured on the Hugging Face, Sage Bionetworks, or various Synapse-based websites,
|
| 128 |
+
ensuring visibility and recognition for impactful contributions. Additionally, follow-up activities such as blog posts,
|
| 129 |
+
video recaps, and social media highlights will showcase key submissions and behind-the-scenes moments. This open-access
|
| 130 |
+
approach ensures that the knowledge generated benefits the broader research community while inspiring future advancements
|
| 131 |
+
in rare disease research.
|
| 132 |
+
|
| 133 |
+
---
|
| 134 |
+
|
| 135 |
+
## Citation and Acknowledgement:
|
| 136 |
+
|
| 137 |
+
Any publication, preprint, conference abstract, or public communication arising from participation in this Hackathon
|
| 138 |
+
must include the following acknowledgement:
|
| 139 |
+
|
| 140 |
+
> _"This work was made possible through the Hackathon, organized by Sage Bionetworks in partnership with the MVA_
|
| 141 |
+
> _Society, Hugging Face, and BEACON (The Benchmarking, Evaluation, and Assessment Consortium for Science), with prize_
|
| 142 |
+
> _sponsorship from AWS and Anthropic. We are deeply grateful to the child and their family who generously contributed_
|
| 143 |
+
> _their data and their story to advance research into this rare disease. We acknowledge their trust in making this_
|
| 144 |
+
> _Hackathon possible."_
|
| 145 |
+
|
| 146 |
+
In addition, participants must observe the following requirements:
|
| 147 |
+
|
| 148 |
+
* **Data subject privacy** - Any publication referencing the underlying individual-level data must not include any information
|
| 149 |
+
that could re-identify the data subject or their family, beyond what is already publicly available through the
|
| 150 |
+
family's own blog posts and public communications.
|
| 151 |
+
* **Dataset citation** - The Hackathon dataset must be cited using the reference provided on the Hackathon Synapse page
|
| 152 |
+
at the time of publication.
|
| 153 |
+
"""
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def render() -> None:
|
| 157 |
+
with gr.Tab("Official Rules"):
|
| 158 |
+
gr.Markdown(RULES_MD)
|
| 159 |
+
gr.HTML('<div class="back-to-top-row"><a class="back-to-top" href="javascript:void(0)" onclick="document.getElementById(\'page-top\').scrollIntoView({behavior:\'smooth\'})">β Back to top</a></div>')
|
| 160 |
+
|
tabs/submit_track1.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Submit tab for Track 1."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import tempfile
|
| 7 |
+
import traceback
|
| 8 |
+
from datetime import datetime, timezone
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
import gradio as gr
|
| 12 |
+
|
| 13 |
+
from config import MAX_TRACK1_SUBMISSIONS
|
| 14 |
+
from evaluation import load_submission, score_proband
|
| 15 |
+
from groundtruth import load_groundtruth
|
| 16 |
+
from utils import (
|
| 17 |
+
append_submission,
|
| 18 |
+
get_hf_username,
|
| 19 |
+
hf_username_to_display_slug,
|
| 20 |
+
submissions_by_user,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
# Loaded once at startup (from private HF dataset if HF_TOKEN is set, else local fallback)
|
| 24 |
+
_GT = load_groundtruth()
|
| 25 |
+
|
| 26 |
+
INTRO_MD = f"""
|
| 27 |
+
Upload your Track 1 predictions file below to receive instant evaluation against the confirmed clinical groundtruth.
|
| 28 |
+
|
| 29 |
+
You are allowed up to <u>{MAX_TRACK1_SUBMISSIONS} submissions</u>; only your highest-scoring submission will be
|
| 30 |
+
featured on the leaderboard.
|
| 31 |
+
|
| 32 |
+
**Team Participation:** Each member receives their own submission quota; coordinate with your teammates to avoid
|
| 33 |
+
using submissions unnecessarily, and ensure everyone uses the exact same team name so your entries group
|
| 34 |
+
together on the leaderboard.
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
INSTRUCTIONS_MD = """
|
| 38 |
+
### 1. Create your predictions file.
|
| 39 |
+
|
| 40 |
+
One row per proposed causal variant (or compound-het pair). Rank your predictions by your own estimated probability
|
| 41 |
+
of causal relationship (EPCR), where 1 is most likely causal and 0 is least likely. There is no fixed "confident enough"
|
| 42 |
+
cutoff, as only your ranking relative to your own guesses matters for scoring. Coordinates must use GRCh38.
|
| 43 |
+
|
| 44 |
+
**Up to 10 candidate rows accepted**. This is one case, not a cohort, so we're asking for your best-ranked guesses, not
|
| 45 |
+
an exhaustive list. Flag whether each row is your primary candidate or a secondary/incidental finding using the `finding_type`
|
| 46 |
+
column. Secondary or incidental findings won't hurt your automated score - include them if you want them considered.
|
| 47 |
+
|
| 48 |
+
| Field | Type | Description |
|
| 49 |
+
|---|---|---|
|
| 50 |
+
| `proband_id` | string | Proband identifier (provided in the dataset) |
|
| 51 |
+
| `chrom_1` | string | Chromosome of the first (or only) variant (e.g. `chr15`) |
|
| 52 |
+
| `pos_1` | integer | GRCh38 base-pair position of the first variant |
|
| 53 |
+
| `ref_1` | string | Reference allele of the first variant |
|
| 54 |
+
| `alt_1` | string | Alternate allele of the first variant |
|
| 55 |
+
| `chrom_2` | string | Chromosome of the second variant (if any); compound-het pairs only, blank otherwise |
|
| 56 |
+
| `pos_2` | integer | GRCh38 base-pair position of the second variant (blank if not applicable) |
|
| 57 |
+
| `ref_2` | string | Reference allele of the second variant (blank if not applicable) |
|
| 58 |
+
| `alt_2` | string | Alternate allele of the second variant (blank if not applicable) |
|
| 59 |
+
| `epcr` | float | Estimated probability of causal relationship, (0, 1] |
|
| 60 |
+
| `finding_type` | string | `primary` or `secondary` |
|
| 61 |
+
| `notes` | string | Optional - brief rationale, especially for secondary findings. For example, findings unrelated to the primary condition, but still worth surfacing |
|
| 62 |
+
|
| 63 |
+
When saving your predictions file, please include your username (or team name) in the filename, as well as a
|
| 64 |
+
short descriptive name for your model or approach. For example, if your HF username is `jane-doe`, you might
|
| 65 |
+
name your predictions file:
|
| 66 |
+
|
| 67 |
+
* `jane-doe_gatk-haplotype.csv`
|
| 68 |
+
* `jane-doe_deepvariant-v2.csv`
|
| 69 |
+
|
| 70 |
+
### 2. Write your report.
|
| 71 |
+
|
| 72 |
+
Each model must include a brief report (PDF or Markdown) describing your approach, rationale, and any supporting evidence.
|
| 73 |
+
This is required for judging, but is not scored automatically.
|
| 74 |
+
|
| 75 |
+
Not sure what details to include? Please use the provided methods description template to organize your analysis, then
|
| 76 |
+
export your report as a PDF or Markdown file for submission.
|
| 77 |
+
|
| 78 |
+
When saving your report, please include your username (or team name) in the filename. For example, if your HF username
|
| 79 |
+
is `jane-doe`, you might name your report file:
|
| 80 |
+
|
| 81 |
+
* `jane-doe_track1_report.pdf` (`.md` also accepted)
|
| 82 |
+
|
| 83 |
+
### 3. Prepare other supporting materials.
|
| 84 |
+
|
| 85 |
+
* Push documented, reproducible code to a public GitHub repository.
|
| 86 |
+
|
| 87 |
+
### 4. After submission:
|
| 88 |
+
|
| 89 |
+
Your CSV submission is evaluated automatically using rank points and F-max against the clinically confirmed
|
| 90 |
+
answer on the leaderboard.
|
| 91 |
+
|
| 92 |
+
Your method(s) will be reviewed separately as part of the judging criteria (e.g. innovation and scalability).
|
| 93 |
+
"""
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _score_csv_bytes(csv_bytes: bytes) -> tuple:
|
| 97 |
+
"""Write bytes to a temp file, run the scorer, return (ScoreResult, proband_id)."""
|
| 98 |
+
with tempfile.NamedTemporaryFile(suffix=".csv", delete=False, mode="wb") as tmp:
|
| 99 |
+
tmp.write(csv_bytes)
|
| 100 |
+
tmp_path = tmp.name
|
| 101 |
+
try:
|
| 102 |
+
submissions = load_submission(tmp_path)
|
| 103 |
+
if not submissions:
|
| 104 |
+
raise ValueError("No valid rows found in the CSV.")
|
| 105 |
+
proband_id = next(iter(submissions))
|
| 106 |
+
true_variants = _GT.get(proband_id)
|
| 107 |
+
if true_variants is None:
|
| 108 |
+
raise ValueError(
|
| 109 |
+
f"Unknown proband_id '{proband_id}'. "
|
| 110 |
+
"Only PROBAND01 is accepted in this challenge."
|
| 111 |
+
)
|
| 112 |
+
result = score_proband(proband_id, submissions[proband_id], true_variants)
|
| 113 |
+
return result, proband_id
|
| 114 |
+
finally:
|
| 115 |
+
os.unlink(tmp_path)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def _handle_submit(display_name_input: str, github_url: str, report_file, csv_file, request: gr.Request, oauth_profile: gr.OAuthProfile | None = None) -> tuple:
|
| 119 |
+
"""Gradio callback - returns (feedback_md, quota_md)."""
|
| 120 |
+
hf_username = get_hf_username(request, oauth_profile)
|
| 121 |
+
if not hf_username:
|
| 122 |
+
return "β οΈ Please sign in with your Hugging Face account to submit.", ""
|
| 123 |
+
|
| 124 |
+
display_name = (display_name_input or "").strip() or hf_username_to_display_slug(hf_username)
|
| 125 |
+
|
| 126 |
+
github_url = (github_url or "").strip()
|
| 127 |
+
if not github_url.startswith("https://github.com/"):
|
| 128 |
+
return "β οΈ GitHub URL must start with `https://github.com/`.", _quota_status(request)
|
| 129 |
+
|
| 130 |
+
if report_file is None:
|
| 131 |
+
return "β οΈ Please upload your report (PDF or Markdown).", _quota_status(request)
|
| 132 |
+
report_path = report_file if isinstance(report_file, str) else report_file.name
|
| 133 |
+
if Path(report_path).suffix.lower() not in {".pdf", ".md"}:
|
| 134 |
+
return "β οΈ Report must be a PDF (.pdf) or Markdown (.md) file.", _quota_status(request)
|
| 135 |
+
report_filename = Path(report_path).name
|
| 136 |
+
|
| 137 |
+
if csv_file is None:
|
| 138 |
+
return "β οΈ Please upload a CSV file.", _quota_status(request)
|
| 139 |
+
|
| 140 |
+
csv_path = csv_file if isinstance(csv_file, str) else csv_file.name
|
| 141 |
+
csv_filename = Path(csv_path).name
|
| 142 |
+
if Path(csv_path).suffix.lower() != ".csv":
|
| 143 |
+
return "β οΈ File must be a CSV (.csv).", _quota_status(request)
|
| 144 |
+
|
| 145 |
+
count = submissions_by_user(hf_username)
|
| 146 |
+
if count >= MAX_TRACK1_SUBMISSIONS:
|
| 147 |
+
return (
|
| 148 |
+
f"β οΈ You have already used all {MAX_TRACK1_SUBMISSIONS} submissions. "
|
| 149 |
+
"Only your best-scoring submission counts toward final ranking.",
|
| 150 |
+
_quota_status(request),
|
| 151 |
+
)
|
| 152 |
+
model_n = count + 1
|
| 153 |
+
|
| 154 |
+
try:
|
| 155 |
+
if isinstance(csv_file, str):
|
| 156 |
+
csv_bytes = Path(csv_file).read_bytes()
|
| 157 |
+
else:
|
| 158 |
+
csv_bytes = csv_file.read() if hasattr(csv_file, "read") else Path(csv_file.name).read_bytes()
|
| 159 |
+
except Exception as e:
|
| 160 |
+
return f"β οΈ Could not read uploaded file: {e}", _quota_status(request)
|
| 161 |
+
|
| 162 |
+
try:
|
| 163 |
+
result, proband_id = _score_csv_bytes(csv_bytes)
|
| 164 |
+
except Exception as e:
|
| 165 |
+
tb = traceback.format_exc()
|
| 166 |
+
return (
|
| 167 |
+
f"β **Scoring error:** {e}\n\n"
|
| 168 |
+
f"<details><summary>Technical details</summary>\n\n```\n{tb}\n```\n</details>",
|
| 169 |
+
_quota_status(request),
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
entry = {
|
| 173 |
+
"hf_username": hf_username,
|
| 174 |
+
"display_name": display_name,
|
| 175 |
+
"filename": f"model{model_n}_{csv_filename}",
|
| 176 |
+
"github_url": github_url,
|
| 177 |
+
"report_filename": report_filename,
|
| 178 |
+
"proband_id": proband_id,
|
| 179 |
+
"rank_points": result.rank_points,
|
| 180 |
+
"f_max": result.f_max,
|
| 181 |
+
"full_match_rank": result.full_match_rank,
|
| 182 |
+
"partial_match_rank": result.partial_match_rank,
|
| 183 |
+
"submitted_at": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"),
|
| 184 |
+
}
|
| 185 |
+
append_submission(entry, report_path)
|
| 186 |
+
|
| 187 |
+
match_desc = (
|
| 188 |
+
f"β
**Full match** at rank {result.full_match_rank}"
|
| 189 |
+
if result.full_match_rank is not None
|
| 190 |
+
else (
|
| 191 |
+
f"β οΈ **Partial match** (one of two compound-het variants) at rank {result.partial_match_rank}"
|
| 192 |
+
if result.partial_match_rank is not None
|
| 193 |
+
else "β **No match** - true variant(s) not found in submission."
|
| 194 |
+
)
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
feedback = f"""
|
| 198 |
+
## Submission received β
|
| 199 |
+
|
| 200 |
+
**{display_name}** (`{hf_username}`) | **Submission:** {model_n} | **Proband:** {proband_id}
|
| 201 |
+
|
| 202 |
+
---
|
| 203 |
+
|
| 204 |
+
### Your scores
|
| 205 |
+
|
| 206 |
+
| Metric | Value |
|
| 207 |
+
|---|---|
|
| 208 |
+
| Rank points | **{result.rank_points:.1f}** / 100 |
|
| 209 |
+
| F-max | **{result.f_max:.3f}** |
|
| 210 |
+
| F-max EPCR threshold | {result.f_max_threshold if result.f_max_threshold is not None else "-"} |
|
| 211 |
+
|
| 212 |
+
{match_desc}
|
| 213 |
+
"""
|
| 214 |
+
return feedback.strip(), _quota_status(request)
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def _quota_status(request: gr.Request, oauth_profile: gr.OAuthProfile | None = None) -> str:
|
| 218 |
+
hf_username = get_hf_username(request, oauth_profile)
|
| 219 |
+
if not hf_username:
|
| 220 |
+
return ""
|
| 221 |
+
count = submissions_by_user(hf_username)
|
| 222 |
+
remaining = MAX_TRACK1_SUBMISSIONS - count
|
| 223 |
+
return (
|
| 224 |
+
f'<div class="info-card quota-card">'
|
| 225 |
+
f'<strong>{count} / {MAX_TRACK1_SUBMISSIONS}</strong> submissions used Β· '
|
| 226 |
+
f'<strong>{remaining}</strong> remaining'
|
| 227 |
+
f'</div>'
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def render() -> None:
|
| 232 |
+
with gr.Tab("Submit - Track 1") as tab:
|
| 233 |
+
gr.Markdown(INTRO_MD)
|
| 234 |
+
quota_md = gr.HTML()
|
| 235 |
+
with gr.Accordion("π Submission Format & Instructions", open=True, elem_classes=["faq-accordion"]):
|
| 236 |
+
gr.Markdown(
|
| 237 |
+
"Templates:\n"
|
| 238 |
+
"- [Submission template (CSV)](/file=static/templates/track1_submission_template.csv)\n"
|
| 239 |
+
"- [Methods description template (Excel)](/file=static/templates/methods_description_form.xlsx)"
|
| 240 |
+
)
|
| 241 |
+
gr.Markdown(INSTRUCTIONS_MD)
|
| 242 |
+
gr.Markdown("---")
|
| 243 |
+
with gr.Row():
|
| 244 |
+
with gr.Column():
|
| 245 |
+
team_name_box = gr.Textbox(
|
| 246 |
+
label="Team / Display Name (optional)",
|
| 247 |
+
placeholder="e.g. helix-squad",
|
| 248 |
+
info="This will be displayed on the public leaderboard. Leave blank to use your "
|
| 249 |
+
"HF username if you are participating individually.",
|
| 250 |
+
)
|
| 251 |
+
github_box = gr.Textbox(
|
| 252 |
+
label="GitHub repo URL *",
|
| 253 |
+
placeholder="https://github.com/your-org/your-repo",
|
| 254 |
+
)
|
| 255 |
+
with gr.Column():
|
| 256 |
+
csv_file = gr.File(label="Predictions file (CSV) *", file_types=[".csv"])
|
| 257 |
+
report_file = gr.File(
|
| 258 |
+
label="Report file (PDF or Markdown) *",
|
| 259 |
+
file_types=[".pdf", ".md"],
|
| 260 |
+
)
|
| 261 |
+
submit_btn = gr.Button("Submit & Score", variant="primary")
|
| 262 |
+
result_md = gr.Markdown(label="Result")
|
| 263 |
+
submit_btn.click(
|
| 264 |
+
fn=_handle_submit,
|
| 265 |
+
inputs=[team_name_box, github_box, report_file, csv_file],
|
| 266 |
+
outputs=[result_md, quota_md],
|
| 267 |
+
)
|
| 268 |
+
tab.select(fn=_quota_status, inputs=[], outputs=quota_md)
|
| 269 |
+
return quota_md
|
tabs/submit_track2.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Submit tab for Track 2."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import io
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
from datetime import datetime, timezone
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
import gradio as gr
|
| 12 |
+
|
| 13 |
+
from config import LEADERBOARD_DATASET, TRACK2_SUBMISSIONS_FOLDER
|
| 14 |
+
from utils import _LOCAL_DEV_DIR, _sanitize, get_hf_username, hf_username_to_display_slug
|
| 15 |
+
|
| 16 |
+
INTRO_MD = """
|
| 17 |
+
Upload your Track 2 proposal here for review by our independent expert judging panel. Unlike Track 1,
|
| 18 |
+
this track uses qualitative evaluation rather than automated scoring.
|
| 19 |
+
|
| 20 |
+
Only <u>one submission</u> is accepted for this track.
|
| 21 |
+
|
| 22 |
+
**Team Participation:** Please designate a single team member to submit on the team's behalf. Additional
|
| 23 |
+
or duplicate submissions from other team members will not be reviewed.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
INSTRUCTIONS_MD = """
|
| 27 |
+
### 1. Write your report.
|
| 28 |
+
|
| 29 |
+
Prepare a written report (PDF or Markdown) proposing repositioned drug candidates supported by your
|
| 30 |
+
analysis. Include a characterization of the variant's mechanism (loss-of-function / gain-of-function,
|
| 31 |
+
pathway disrupted, downstream biological consequence) as the basis for your repurposing rationale.
|
| 32 |
+
|
| 33 |
+
Not sure what details to include? Please use the provided methods description template to organize your
|
| 34 |
+
analysis and supporting materials, then export your report as a PDF or Markdown file for submission.
|
| 35 |
+
|
| 36 |
+
When saving your report, please include your username (or team name) in the filename. For example, if your
|
| 37 |
+
HF username is `jane-doe`, you might name your report file:
|
| 38 |
+
|
| 39 |
+
* `jane-doe_track2_report.pdf` (`.md` also accepted)
|
| 40 |
+
|
| 41 |
+
### 2. Prepare other supporting materials.
|
| 42 |
+
|
| 43 |
+
* Push documented, reproducible code to a public GitHub repository.
|
| 44 |
+
* Record a 3-minute pitch video walking through your reasoning and proposed candidates and upload it to
|
| 45 |
+
YouTube or Vimeo.
|
| 46 |
+
|
| 47 |
+
### 3. After submission:
|
| 48 |
+
|
| 49 |
+
The independent panel will review all entries over a ~2-3 month window. Results will be announced at a
|
| 50 |
+
later date.
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
def _save_track2_submission(entry: dict, report_path: str) -> None:
|
| 54 |
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S_%f")
|
| 55 |
+
safe_user = _sanitize(entry.get("hf_username", "unknown"))
|
| 56 |
+
subfolder = f"{TRACK2_SUBMISSIONS_FOLDER}{safe_user}/"
|
| 57 |
+
filename = f"sub_{timestamp}.json"
|
| 58 |
+
payload = json.dumps(entry, indent=2).encode()
|
| 59 |
+
token = os.environ.get("HF_TOKEN")
|
| 60 |
+
if token:
|
| 61 |
+
from huggingface_hub import upload_file
|
| 62 |
+
upload_file(
|
| 63 |
+
path_or_fileobj=io.BytesIO(payload),
|
| 64 |
+
path_in_repo=f"{subfolder}{filename}",
|
| 65 |
+
repo_id=LEADERBOARD_DATASET,
|
| 66 |
+
repo_type="dataset",
|
| 67 |
+
token=token,
|
| 68 |
+
commit_message=f"track2: {entry.get('display_name', entry.get('hf_username', '?'))}",
|
| 69 |
+
)
|
| 70 |
+
upload_file(
|
| 71 |
+
path_or_fileobj=report_path,
|
| 72 |
+
path_in_repo=f"{subfolder}{entry['report_filename']}",
|
| 73 |
+
repo_id=LEADERBOARD_DATASET,
|
| 74 |
+
repo_type="dataset",
|
| 75 |
+
token=token,
|
| 76 |
+
commit_message=f"track2 report: {entry.get('display_name', entry.get('hf_username', '?'))}",
|
| 77 |
+
)
|
| 78 |
+
else:
|
| 79 |
+
local_dir = _LOCAL_DEV_DIR / "track2_submissions" / safe_user
|
| 80 |
+
local_dir.mkdir(parents=True, exist_ok=True)
|
| 81 |
+
(local_dir / filename).write_bytes(payload)
|
| 82 |
+
import shutil
|
| 83 |
+
shutil.copy(report_path, local_dir / entry['report_filename'])
|
| 84 |
+
|
| 85 |
+
def _track2_submissions_by_user(hf_username: str) -> int:
|
| 86 |
+
"""Count Track 2 submissions for a given HF user."""
|
| 87 |
+
safe_user = _sanitize(hf_username)
|
| 88 |
+
token = os.environ.get("HF_TOKEN")
|
| 89 |
+
if token:
|
| 90 |
+
from huggingface_hub import HfApi
|
| 91 |
+
api = HfApi()
|
| 92 |
+
try:
|
| 93 |
+
return sum(
|
| 94 |
+
1 for f in api.list_repo_files(
|
| 95 |
+
LEADERBOARD_DATASET, repo_type="dataset", token=token
|
| 96 |
+
)
|
| 97 |
+
if f.startswith(f"{TRACK2_SUBMISSIONS_FOLDER}{safe_user}/sub_") and f.endswith(".json")
|
| 98 |
+
)
|
| 99 |
+
except Exception:
|
| 100 |
+
return 0
|
| 101 |
+
# Local dev fallback
|
| 102 |
+
local_dir = _LOCAL_DEV_DIR / "track2_submissions" / safe_user
|
| 103 |
+
if not local_dir.exists():
|
| 104 |
+
return 0
|
| 105 |
+
return sum(1 for _ in local_dir.glob("sub_*.json"))
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def _submission_status(request: gr.Request, oauth_profile: gr.OAuthProfile | None = None) -> str:
|
| 109 |
+
hf_username = get_hf_username(request, oauth_profile)
|
| 110 |
+
if not hf_username:
|
| 111 |
+
return ""
|
| 112 |
+
count = _track2_submissions_by_user(hf_username)
|
| 113 |
+
if count >= 1:
|
| 114 |
+
return '<div class="info-card quota-card">β
<strong>Track 2 submission received.</strong> Only one submission per team is accepted.</div>'
|
| 115 |
+
return '<div class="info-card quota-card"><strong>0 / 1</strong> submissions used Β· <strong>1</strong> remaining</div>'
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def _handle_submit(
|
| 119 |
+
display_name_input: str,
|
| 120 |
+
github_url: str,
|
| 121 |
+
video_url: str,
|
| 122 |
+
report_file,
|
| 123 |
+
notes: str,
|
| 124 |
+
request: gr.Request,
|
| 125 |
+
oauth_profile: gr.OAuthProfile | None = None,
|
| 126 |
+
) -> str:
|
| 127 |
+
hf_username = get_hf_username(request, oauth_profile)
|
| 128 |
+
if not hf_username:
|
| 129 |
+
return "β οΈ Please sign in with your Hugging Face account to submit."
|
| 130 |
+
|
| 131 |
+
display_name = (display_name_input or "").strip() or hf_username_to_display_slug(hf_username)
|
| 132 |
+
|
| 133 |
+
count = _track2_submissions_by_user(hf_username)
|
| 134 |
+
if count >= 1:
|
| 135 |
+
return "β οΈ You have already submitted a Track 2 entry. Only one submission per participant is accepted."
|
| 136 |
+
|
| 137 |
+
github_url = (github_url or "").strip()
|
| 138 |
+
if not github_url.startswith("https://github.com/"):
|
| 139 |
+
return "β οΈ GitHub URL must start with `https://github.com/`."
|
| 140 |
+
video_url = (video_url or "").strip()
|
| 141 |
+
if not video_url:
|
| 142 |
+
return "β οΈ A pitch video URL is required. Please upload your 3-minute video to YouTube or Vimeo and paste the link."
|
| 143 |
+
if report_file is None:
|
| 144 |
+
return "β οΈ Please upload your report (PDF or Markdown)."
|
| 145 |
+
|
| 146 |
+
report_path = report_file if isinstance(report_file, str) else report_file.name
|
| 147 |
+
if Path(report_path).suffix.lower() not in {".pdf", ".md"}:
|
| 148 |
+
return "β οΈ Report must be a PDF (.pdf) or Markdown (.md) file."
|
| 149 |
+
|
| 150 |
+
report_name = Path(
|
| 151 |
+
report_file if isinstance(report_file, str) else report_file.name
|
| 152 |
+
).name
|
| 153 |
+
|
| 154 |
+
entry = {
|
| 155 |
+
"hf_username": hf_username,
|
| 156 |
+
"display_name": display_name,
|
| 157 |
+
"github_url": github_url,
|
| 158 |
+
"video_url": (video_url or "").strip(),
|
| 159 |
+
"report_filename": report_name,
|
| 160 |
+
"notes": (notes or "").strip(),
|
| 161 |
+
"submitted_at": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"),
|
| 162 |
+
}
|
| 163 |
+
_save_track2_submission(entry, report_path)
|
| 164 |
+
|
| 165 |
+
video_line = f"[{entry['video_url']}]({entry['video_url']})" if entry["video_url"] else "_(not provided)_"
|
| 166 |
+
|
| 167 |
+
return f"""
|
| 168 |
+
## Track 2 submission received β
|
| 169 |
+
|
| 170 |
+
**{display_name}** (`{hf_username}`)
|
| 171 |
+
|
| 172 |
+
Your submission has been logged and will be reviewed by the expert judging panel over the
|
| 173 |
+
~2-3 month judging window. Results will be announced after the judging period closes.
|
| 174 |
+
|
| 175 |
+
**What you submitted:**
|
| 176 |
+
- GitHub repository: [{github_url}]({github_url})
|
| 177 |
+
- Video: {video_line}
|
| 178 |
+
- Report file: `{report_name}`
|
| 179 |
+
""".strip()
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def render() -> None:
|
| 183 |
+
with gr.Tab("Submit - Track 2") as tab:
|
| 184 |
+
gr.Markdown(INTRO_MD)
|
| 185 |
+
status_md = gr.HTML()
|
| 186 |
+
with gr.Accordion("π Submission Format & Instructions", open=True, elem_classes=["faq-accordion"]):
|
| 187 |
+
gr.Markdown(
|
| 188 |
+
"Templates:\n"
|
| 189 |
+
"- [Methods description template (Excel)](/file=static/templates/methods_description_form.xlsx)"
|
| 190 |
+
)
|
| 191 |
+
gr.Markdown(INSTRUCTIONS_MD)
|
| 192 |
+
gr.Markdown("---")
|
| 193 |
+
with gr.Row():
|
| 194 |
+
with gr.Column():
|
| 195 |
+
team_name_box = gr.Textbox(
|
| 196 |
+
label="Team / Display Name (optional)",
|
| 197 |
+
placeholder="e.g. helix-squad",
|
| 198 |
+
info="This will be used for the public announcement. Leave blank to use your "
|
| 199 |
+
"HF username if you are participating individually.",
|
| 200 |
+
)
|
| 201 |
+
github_box = gr.Textbox(
|
| 202 |
+
label="GitHub repo URL *",
|
| 203 |
+
placeholder="https://github.com/your-org/your-repo",
|
| 204 |
+
)
|
| 205 |
+
video_box = gr.Textbox(
|
| 206 |
+
label="Pitch video URL *",
|
| 207 |
+
placeholder="https://youtu.be/...",
|
| 208 |
+
)
|
| 209 |
+
notes_box = gr.Textbox(
|
| 210 |
+
label="Notes for judges (optional)",
|
| 211 |
+
lines=3,
|
| 212 |
+
placeholder="Anything you want judges to know before they open your submissionβ¦",
|
| 213 |
+
)
|
| 214 |
+
with gr.Column():
|
| 215 |
+
report_file = gr.File(
|
| 216 |
+
label="Report file (PDF or Markdown) *",
|
| 217 |
+
file_types=[".pdf", ".md"],
|
| 218 |
+
)
|
| 219 |
+
submit_btn = gr.Button("Submit", variant="primary")
|
| 220 |
+
result_md = gr.Markdown(label="Confirmation")
|
| 221 |
+
submit_btn.click(
|
| 222 |
+
fn=_handle_submit,
|
| 223 |
+
inputs=[team_name_box, github_box, video_box, report_file, notes_box],
|
| 224 |
+
outputs=result_md,
|
| 225 |
+
)
|
| 226 |
+
tab.select(fn=_submission_status, inputs=[], outputs=status_md)
|
| 227 |
+
return status_md
|
utils.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Helpers to load from and push submissions.
|
| 2 |
+
|
| 3 |
+
* In production: submissions are written to a private HuggingFace dataset
|
| 4 |
+
* In local dev: submissions are written to a local folder
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import io
|
| 10 |
+
import json
|
| 11 |
+
import os
|
| 12 |
+
import re
|
| 13 |
+
from datetime import datetime, timezone
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
from config import (
|
| 17 |
+
LEADERBOARD_DATASET,
|
| 18 |
+
TRACK1_SUBMISSIONS_FOLDER,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
_LOCAL_DEV_DIR = Path("local_dev")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _sanitize(name: str) -> str:
|
| 25 |
+
"""Return a filename-safe version of a name."""
|
| 26 |
+
return re.sub(r"[^A-Za-z0-9._-]+", "_", name).strip("_") or "unknown"
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# ββ Identity helpers ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 30 |
+
|
| 31 |
+
def get_hf_username(request=None, oauth_profile=None) -> str | None:
|
| 32 |
+
"""Extract HF username from a Gradio OAuthProfile or request, with a local dev override.
|
| 33 |
+
|
| 34 |
+
Set HF_USERNAME_OVERRIDE in your environment to simulate a logged-in user
|
| 35 |
+
during local development (where OAuth is not available).
|
| 36 |
+
"""
|
| 37 |
+
override = os.getenv("HF_USERNAME_OVERRIDE")
|
| 38 |
+
if override:
|
| 39 |
+
return override
|
| 40 |
+
if oauth_profile is not None:
|
| 41 |
+
return getattr(oauth_profile, "username", None)
|
| 42 |
+
return getattr(request, "username", None) if request else None
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def hf_username_to_display_slug(username: str) -> str:
|
| 46 |
+
"""Convert an HF username to a display-safe slug (lowercase, hyphens)."""
|
| 47 |
+
slug = re.sub(r"[^a-z0-9]+", "-", username.lower()).strip("-")
|
| 48 |
+
return slug or "participant"
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# ββ Submissions ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 52 |
+
|
| 53 |
+
def _local_submissions_dir() -> Path:
|
| 54 |
+
return _LOCAL_DEV_DIR / "track1_submissions"
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def append_submission(entry: dict, report_path: str | None = None) -> None:
|
| 58 |
+
"""
|
| 59 |
+
Store a single submission as its own JSON file in the HF dataset (or locally in dev).
|
| 60 |
+
Optionally also saves a report file alongside the JSON.
|
| 61 |
+
"""
|
| 62 |
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S_%f")
|
| 63 |
+
safe_user = _sanitize(entry.get("hf_username", "unknown"))
|
| 64 |
+
subfolder = f"{TRACK1_SUBMISSIONS_FOLDER}{safe_user}/"
|
| 65 |
+
filename = f"sub_{timestamp}.json"
|
| 66 |
+
|
| 67 |
+
# Stamp the report filename with the same timestamp as the metadata.
|
| 68 |
+
if report_path and entry.get("report_filename"):
|
| 69 |
+
ext = Path(entry["report_filename"]).suffix
|
| 70 |
+
entry["report_filename"] = f"report_{timestamp}{ext}"
|
| 71 |
+
|
| 72 |
+
payload = json.dumps(entry, indent=2).encode()
|
| 73 |
+
|
| 74 |
+
token = os.environ.get("HF_TOKEN")
|
| 75 |
+
if token:
|
| 76 |
+
from huggingface_hub import upload_file
|
| 77 |
+
|
| 78 |
+
upload_file(
|
| 79 |
+
path_or_fileobj=io.BytesIO(payload),
|
| 80 |
+
path_in_repo=f"{subfolder}{filename}",
|
| 81 |
+
repo_id=LEADERBOARD_DATASET,
|
| 82 |
+
repo_type="dataset",
|
| 83 |
+
token=token,
|
| 84 |
+
commit_message=f"track1: {entry.get('display_name', entry.get('hf_username', '?'))}",
|
| 85 |
+
)
|
| 86 |
+
if report_path and entry.get("report_filename"):
|
| 87 |
+
upload_file(
|
| 88 |
+
path_or_fileobj=report_path,
|
| 89 |
+
path_in_repo=f"{subfolder}{entry['report_filename']}",
|
| 90 |
+
repo_id=LEADERBOARD_DATASET,
|
| 91 |
+
repo_type="dataset",
|
| 92 |
+
token=token,
|
| 93 |
+
commit_message=f"track1 report: {entry.get('display_name', entry.get('hf_username', '?'))}",
|
| 94 |
+
)
|
| 95 |
+
else:
|
| 96 |
+
import shutil
|
| 97 |
+
local_dir = _local_submissions_dir() / safe_user
|
| 98 |
+
local_dir.mkdir(parents=True, exist_ok=True)
|
| 99 |
+
(local_dir / filename).write_bytes(payload)
|
| 100 |
+
if report_path and entry.get("report_filename"):
|
| 101 |
+
shutil.copy(report_path, local_dir / entry["report_filename"])
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def load_leaderboard() -> list[dict]:
|
| 105 |
+
"""
|
| 106 |
+
Download all Track 1 submissions from the HF dataset and return them as a list.
|
| 107 |
+
Falls back to local_dev/track1_submissions/ when HF_TOKEN is not set.
|
| 108 |
+
"""
|
| 109 |
+
token = os.environ.get("HF_TOKEN")
|
| 110 |
+
if token:
|
| 111 |
+
from huggingface_hub import HfApi, hf_hub_download
|
| 112 |
+
|
| 113 |
+
api = HfApi()
|
| 114 |
+
try:
|
| 115 |
+
files = [
|
| 116 |
+
f
|
| 117 |
+
for f in api.list_repo_files(
|
| 118 |
+
LEADERBOARD_DATASET, repo_type="dataset", token=token
|
| 119 |
+
)
|
| 120 |
+
if f.startswith(TRACK1_SUBMISSIONS_FOLDER) and f.endswith(".json")
|
| 121 |
+
]
|
| 122 |
+
except Exception:
|
| 123 |
+
return []
|
| 124 |
+
rows = []
|
| 125 |
+
for filename in files:
|
| 126 |
+
try:
|
| 127 |
+
path = hf_hub_download(
|
| 128 |
+
repo_id=LEADERBOARD_DATASET,
|
| 129 |
+
filename=filename,
|
| 130 |
+
repo_type="dataset",
|
| 131 |
+
token=token,
|
| 132 |
+
force_download=True,
|
| 133 |
+
)
|
| 134 |
+
rows.append(json.loads(Path(path).read_text()))
|
| 135 |
+
except Exception:
|
| 136 |
+
continue
|
| 137 |
+
return rows
|
| 138 |
+
# Local dev fallback
|
| 139 |
+
rows = []
|
| 140 |
+
for f in sorted(_local_submissions_dir().glob("**/sub_*.json")):
|
| 141 |
+
try:
|
| 142 |
+
rows.append(json.loads(f.read_text()))
|
| 143 |
+
except Exception:
|
| 144 |
+
continue
|
| 145 |
+
return rows
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def best_per_team(rows: list[dict]) -> list[dict]:
|
| 149 |
+
"""Return the best-scoring submission per team (display_name), sorted by score."""
|
| 150 |
+
best: dict[str, dict] = {}
|
| 151 |
+
for row in rows:
|
| 152 |
+
team = row.get("display_name") or row.get("hf_username") or row.get("team", "unknown")
|
| 153 |
+
if team not in best:
|
| 154 |
+
best[team] = row
|
| 155 |
+
else:
|
| 156 |
+
prev = best[team]
|
| 157 |
+
if (row["rank_points"], row["f_max"]) > (prev["rank_points"], prev["f_max"]):
|
| 158 |
+
best[team] = row
|
| 159 |
+
sorted_rows = sorted(best.values(), key=lambda r: (-r["rank_points"], -r["f_max"]))
|
| 160 |
+
return [{**r, "place": i} for i, r in enumerate(sorted_rows, 1)]
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def _list_user_files(folder: str, hf_username: str) -> list[str]:
|
| 164 |
+
"""Return sorted list of JSON submission paths for a user in the given folder."""
|
| 165 |
+
safe_user = _sanitize(hf_username)
|
| 166 |
+
prefix = f"{folder}{safe_user}/sub_"
|
| 167 |
+
token = os.environ.get("HF_TOKEN")
|
| 168 |
+
if token:
|
| 169 |
+
from huggingface_hub import HfApi
|
| 170 |
+
try:
|
| 171 |
+
return sorted(
|
| 172 |
+
f for f in HfApi().list_repo_files(LEADERBOARD_DATASET, repo_type="dataset", token=token)
|
| 173 |
+
if f.startswith(prefix) and f.endswith(".json")
|
| 174 |
+
)
|
| 175 |
+
except Exception:
|
| 176 |
+
return []
|
| 177 |
+
user_dir = _local_submissions_dir() / safe_user
|
| 178 |
+
if not user_dir.exists():
|
| 179 |
+
return []
|
| 180 |
+
return sorted(str(f) for f in user_dir.glob("sub_*.json"))
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def submissions_by_user(hf_username: str) -> int:
|
| 184 |
+
"""Return the number of Track 1 submissions made by a given HF user."""
|
| 185 |
+
return len(_list_user_files(TRACK1_SUBMISSIONS_FOLDER, hf_username))
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def leaderboard_df() -> list[list]:
|
| 189 |
+
"""Return rows formatted for gr.Dataframe."""
|
| 190 |
+
rows = best_per_team(load_leaderboard())
|
| 191 |
+
if not rows:
|
| 192 |
+
return []
|
| 193 |
+
out = []
|
| 194 |
+
for r in rows:
|
| 195 |
+
medal = {1: "π₯", 2: "π₯", 3: "π₯"}.get(r["place"], str(r["place"]))
|
| 196 |
+
display = r.get("display_name") or r.get("hf_username") or r.get("team", "?")
|
| 197 |
+
hf_user = r.get("hf_username") or r.get("team", "?")
|
| 198 |
+
participant = f"{display} (@{hf_user})" if display != hf_user else f"@{hf_user}"
|
| 199 |
+
out.append([
|
| 200 |
+
medal,
|
| 201 |
+
participant,
|
| 202 |
+
f'{r["rank_points"]:.1f}',
|
| 203 |
+
f'{r["f_max"]:.3f}',
|
| 204 |
+
r.get("filename") or r.get("model", ""),
|
| 205 |
+
r["submitted_at"],
|
| 206 |
+
])
|
| 207 |
+
return out
|