# Project export: Multiple Sclerosis Blood Test using TCR ML Algorithm

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: We used an ML algorithm trained on the T Cell Receptor (TCR) sequences of T cells in multiple sclerosis (MS) patients to predict whether a patient has MS or not using blood samples.
- Devpost: https://devpost.com/software/multiple-sclerosis-blood-test-using-tcr-ml-algorithm
- GitHub: https://github.com/gabrjos6/Model-GUI
- Demo: https://github.com/gabrjos6/Preprocessing_pipeline
- Video: https://www.youtube.com/embed/6VWR_GWJl7g?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 0 GitHub contributor(s) — 

## Devpost submission (written by the team)

### Inspiration

Multiple Sclerosis (MS) is an autoimmune disease that affects nearly 1 million people in the US alone each year. In MS, autoreactive t-cells trigger an immune response against myelin, the protein that protects the nerves. As a result, MS causes progressive disability in the central nervous system and brain, and no cure currently exists. Diagnosis for MS is long, difficult, and expensive. It requires a combination of symptoms, as well as the identification of active lesions or scar tissue in the brain through MRI. This means that by the time MS patients are diagnosed and placed on disease modifying therapies, there is permanent damage already done. Our vision is of a world where a simple blood test can accurately diagnose patients with MS, allowing for earlier, cheaper, and more convenient detection. We believe that the rise of scRNA-seq data can make this idea into a reality.

### What it does

The algorithm takes scRNA-seq data from a patient blood sample, and predicts whether the patient has multiple sclerosis (MS).

### How we built it

The first step of the pipeline we built is preprocessing. This takes the raw fastq files (generated by sequencing of the Peripheral Blood Mononuclear Cells (PBMC) and aligns them to a reference genome. Then, the reads are analyzed with TRUST4, a TCR assembly algorithm designed for speed. This algorithm reconstructs the t-cell receptor (TCR) region for each t cell. The TCR region is responsible for the recognition of foreign epitopes by the adaptive immune system. In autoimmune diseases or during infection, the relative abundance of t-cells with a given TCR region shifts in a process called clonal expansion. After the TCR data is generated for each sample, we normalize to generate relative abundance. This is the data that is sent to the ML algorithm. This processing was done using a Runpod Serverless GPU endpoint. Without the high compute and easy compatibility, the preprocessing would be exponentially more difficult. First, we tried to use a transformer architecture based on BERT-TCR (https://github.com/zhangbeibei-min/BertTCR/), which uses TCR regions to diagnose disease (cancer). The code required a significant amount of modifications and some architecture changes to run. BERT-TCR is built upon a pretrained protein model, which is useful due to the limited nature of data and high computational power needed for processing. This model encodes the cdr3 region (which includes the most variable, and thus important parts of the TCR region) to give it vectorized meaning. Ensemble learning is used on MS prediction scores gathered from each cdr3 sequence, resulting in one final score for a given patient. Then, training is conducted with the relative abundance of MS vs non-MS TCR regions. However, this model was unable to converge due to our limited data, and we pivoted to a Random Forest Classifier, a much simpler model that required less data. This new algorithm was able to achieve an AUC of 0.960+-.053 and recall of 0.9+-.17 after a short training period.

### Challenges we ran into

The first challenge was data availability. In total, we were only able to identify about 150 samples of scRNA-seq data for the blood of MS patients. This is not a very large amount of data to train an algorithm on. The second, and largest challenge, was that the preprocessing of the data was extensively difficult, time consuming, and required large amounts of computation. In the process of finding TCR sequences, there has to be alignment of the raw reads to a reference genome. It turns out that using cellranger, it takes 2-4 hours for this pipeline to run per sample, making the analysis of 100+ samples infeasible in the time frame. We pivoted to using a much faster algorithm at the cost of TCR sensitivity. Our new analysis with TRUST4 yielded workable data, but still in small quantities (about 25 samples positive, 50s of control). The final challenge was the implementation with the transformer model. It seemed like in the end, the lack of data severely bottlenecked the model. Even with multiple hyperparameter sweeps, the model would not converge given the data and simply overfit to the training data. Thus, we also pivoted to using a much faster algorithm–although this time, a much simpler one. Finally, we used flask to generate a simple GUI for demos.

### Accomplishments we're proud of

We are proud of being able to process terabytes of data in a short period of time, thus validating the methodology. Our current results suggest that this method has potential to break a barrier in MS diagnosis and treatment. We think that this project can go even further in a research setting, given more time to process data and experiment with training more architectures.

### What we learned

We learned that some architectures need more time to accomplish than 36 hours. We learned how to go about processing complex datasets, and the value of efficiency in data processing. Further, sometimes simple models work better than complex ones, especially with limited time and resources.

### What's next

Given more time to process the datasets we gathered, we expect to see an improved model. We can also perform testing on a larger dataset to validate our results. We want to try out different pretrained model architectures. We had plans to use the ESM-2 protein model by Meta, but did not have the time to fully implement this. We also want to use models pretrained on specifically TCR data rather than general proteins (or, given our extensive dataset sweep, pretrain our own) to further account for our lack of data. Further, we can expand on the Random Forest architecture and combine it with our deep learning approach.

## README (from the GitHub repository)

# MS Blood-Based Diagnosis (TCR Abundance Classifier)

Local web app and training pipeline for MS vs healthy classification using TCR CDR3 relative-abundance TSV/CSV input.

## What is included

- Web app: `/Users/falconglyph/Documents/tcr-preprocessing/app.py`
- Frontend: `/Users/falconglyph/Documents/tcr-preprocessing/templates/index.html`, `/Users/falconglyph/Documents/tcr-preprocessing/static/`
- Training script: `/Users/falconglyph/Documents/tcr-preprocessing/train_rf_ms_classifier.py`
- Current deployed model family: `/Users/falconglyph/Documents/tcr-preprocessing/rf_ms_model_output_training_tsvs_augmented_v4_split_80_20_20seeds/`
- TCR preprocessing pipeline (reference): `/Users/falconglyph/Documents/tcr-preprocessing/try2/`

## Quick start

1. Create env and install dependencies:

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements-gui.txt
```

2. Run the web app:

```bash
python3 app.py
```

3. Open:

```text
http://127.0.0.1:5000
```

## Input format expected by web app

Upload a `.tsv`, `.csv`, or `.txt` file with one of these header pairs:

- `TCR,Abundance`
- `cdr3,relative_abundance`
- `cdr3_aa,relative_abundance`

Optional: provide `selected_sample` when the uploaded table contains multiple samples.

## Retraining (example)

```bash
python3 train_rf_ms_classifier.py \
  --tsv_dir Training_tsvs_augmented_v4 \
  --outdir rf_ms_model_output_training_tsvs_augmented_v4_split_80_20_20seeds/seed_02
```

Then point the app to the new model:

```bash
MODEL_PATH=/absolute/path/to/random_forest_ms_vs_hc.joblib python3 app.py
```

## Repo hygiene

- Runtime outputs are written under `web_runs/` and ignored by git.
- Python caches and macOS artifacts are ignored by `.gitignore`.


## Detected evidence (automated analysis)

Indexed codebase: 35 recognized source files, 207 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- Flask (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (120 of 569)

```
.gitignore
app.py
bulk_chain_abundance4.csv
bulk-chain-3-6.csv
data/vdjdb-db-master/.gitattributes
data/vdjdb-db-master/.gitignore
data/vdjdb-db-master/.gitlab-ci.yml
data/vdjdb-db-master/.travis.yml
data/vdjdb-db-master/chunks_negative/PMID_40713946.txt
data/vdjdb-db-master/chunks_negative/PMID_41315082_negative.txt
data/vdjdb-db-master/chunks_negative/Visani_et_al_Hermes_negative.txt
data/vdjdb-db-master/chunks_unformatted/Mice_TCRs_specific_to_B16_melanoma_neoantigens_(Shagina_et_al_2024).txt
data/vdjdb-db-master/chunks_with_unconventional_aa/PMID_39043656.txt
data/vdjdb-db-master/chunks/10xgenomics-2019-07-09.txt
data/vdjdb-db-master/chunks/akitsu-etal-2025-11.txt
data/vdjdb-db-master/chunks/bunkofske-etal-2024.txt
data/vdjdb-db-master/chunks/covid19-2020-05-19.txt
data/vdjdb-db-master/chunks/covid19-heathlab-26-11-2021.txt
data/vdjdb-db-master/chunks/covid19-sewell-13-05-2021.txt
data/vdjdb-db-master/chunks/drlcook-etal-2020-02-01.txt
data/vdjdb-db-master/chunks/efimovlab-2021-09-15_LTI_VDJdb.txt
data/vdjdb-db-master/chunks/evgeny-etal-2018-04.txt
data/vdjdb-db-master/chunks/goncharov-gluten-2023-05-06.txt
data/vdjdb-db-master/chunks/goncharov-sarscov2-mhcii-2021-08-12.txt
data/vdjdb-db-master/chunks/goncharov-taa-2020-10-12.txt
data/vdjdb-db-master/chunks/goncharov-taa-2020-11-02.txt
data/vdjdb-db-master/chunks/goncharov-taa-2020-11-24.txt
data/vdjdb-db-master/chunks/goncharov-taa-2021-02-01.txt
data/vdjdb-db-master/chunks/goncharov-taa-2021-08-12.txt
data/vdjdb-db-master/chunks/goncharov-taa-2022-01-27.txt
data/vdjdb-db-master/chunks/goncharov-various-2023-04-18.txt
data/vdjdb-db-master/chunks/goncharov-various-2023-05-06.txt
data/vdjdb-db-master/chunks/greenshields_cole_2019.txt
data/vdjdb-db-master/chunks/hayleymc-2023-08-13.txt
data/vdjdb-db-master/chunks/huang-etal-2020-01-09.txt
data/vdjdb-db-master/chunks/kedzierska-etal-2021-06-18.txt
data/vdjdb-db-master/chunks/luciani-samir-etal-hcv-14-09-2021.txt
data/vdjdb-db-master/chunks/menon_etal_2024.txt
data/vdjdb-db-master/chunks/Mice_TCRs_specific_to_B16_melanoma_neoantigens_(Shagina_et_al_2024).txt
data/vdjdb-db-master/chunks/nguyen-etal-2023.txt
data/vdjdb-db-master/chunks/paley-etal-2024-06-18.txt
data/vdjdb-db-master/chunks/PDB_Database.txt
data/vdjdb-db-master/chunks/PMID_10756006.txt
data/vdjdb-db-master/chunks/PMID_10925283.txt
data/vdjdb-db-master/chunks/PMID_11046006.txt
data/vdjdb-db-master/chunks/PMID_11756174.txt
data/vdjdb-db-master/chunks/PMID_11930311.txt
data/vdjdb-db-master/chunks/PMID_12165524.txt
data/vdjdb-db-master/chunks/PMID_12466894.txt
data/vdjdb-db-master/chunks/PMID_12504586.txt
data/vdjdb-db-master/chunks/PMID_12555663.txt
data/vdjdb-db-master/chunks/PMID_15589168.txt
data/vdjdb-db-master/chunks/PMID_15596521.txt
data/vdjdb-db-master/chunks/PMID_15753288.txt
data/vdjdb-db-master/chunks/PMID_15849183.txt
data/vdjdb-db-master/chunks/PMID_16148129.txt
data/vdjdb-db-master/chunks/PMID_16237109.txt
data/vdjdb-db-master/chunks/PMID_16287711.txt
data/vdjdb-db-master/chunks/PMID_16326979.txt
data/vdjdb-db-master/chunks/PMID_16772368.txt
data/vdjdb-db-master/chunks/PMID_16982909.txt
data/vdjdb-db-master/chunks/PMID_17082594.txt
data/vdjdb-db-master/chunks/PMID_17121793.txt
data/vdjdb-db-master/chunks/PMID_17287271.txt
data/vdjdb-db-master/chunks/PMID_17459926.txt
data/vdjdb-db-master/chunks/PMID_17709536.txt
data/vdjdb-db-master/chunks/PMID_17893201.txt
data/vdjdb-db-master/chunks/PMID_18270323.txt
data/vdjdb-db-master/chunks/PMID_18802118.txt
data/vdjdb-db-master/chunks/PMID_19014475.txt
data/vdjdb-db-master/chunks/PMID_19017975.txt
data/vdjdb-db-master/chunks/PMID_19349463.txt
data/vdjdb-db-master/chunks/PMID_19776383.txt
data/vdjdb-db-master/chunks/PMID_19864595.txt
data/vdjdb-db-master/chunks/PMID_20139278.txt
data/vdjdb-db-master/chunks/PMID_20432235.txt
data/vdjdb-db-master/chunks/PMID_21118816.txt
data/vdjdb-db-master/chunks/PMID_21135165.txt
data/vdjdb-db-master/chunks/PMID_21160049.txt
data/vdjdb-db-master/chunks/PMID_21555537.txt
data/vdjdb-db-master/chunks/PMID_21562156.txt
data/vdjdb-db-master/chunks/PMID_21752903.txt
data/vdjdb-db-master/chunks/PMID_22044339.txt
data/vdjdb-db-master/chunks/PMID_22210916.txt
data/vdjdb-db-master/chunks/PMID_22278241.txt
data/vdjdb-db-master/chunks/PMID_22314361.txt
data/vdjdb-db-master/chunks/PMID_22323539.txt
data/vdjdb-db-master/chunks/PMID_23028307.txt
data/vdjdb-db-master/chunks/PMID_23267020.txt
data/vdjdb-db-master/chunks/PMID_23521884.txt
data/vdjdb-db-master/chunks/PMID_23637823.txt
data/vdjdb-db-master/chunks/PMID_23933763.txt
data/vdjdb-db-master/chunks/PMID_24069285.txt
data/vdjdb-db-master/chunks/PMID_24512815.txt
data/vdjdb-db-master/chunks/PMID_24600035.txt
data/vdjdb-db-master/chunks/PMID_24711416.txt
data/vdjdb-db-master/chunks/PMID_24906112.txt
data/vdjdb-db-master/chunks/PMID_25157096.txt
data/vdjdb-db-master/chunks/PMID_25320304.txt
data/vdjdb-db-master/chunks/PMID_25339770.txt
data/vdjdb-db-master/chunks/PMID_25609818.txt
data/vdjdb-db-master/chunks/PMID_25801351.txt
data/vdjdb-db-master/chunks/PMID_25837513.txt
data/vdjdb-db-master/chunks/PMID_25911754.txt
data/vdjdb-db-master/chunks/PMID_25925682.txt
data/vdjdb-db-master/chunks/PMID_26860370.txt
data/vdjdb-db-master/chunks/PMID_27111229.txt
data/vdjdb-db-master/chunks/PMID_27252176.txt
data/vdjdb-db-master/chunks/PMID_27645996.txt
data/vdjdb-db-master/chunks/PMID_27760342.txt
data/vdjdb-db-master/chunks/PMID_28103239.txt
data/vdjdb-db-master/chunks/PMID_28146579.txt
data/vdjdb-db-master/chunks/PMID_28250417.txt
data/vdjdb-db-master/chunks/PMID_28423320.txt
data/vdjdb-db-master/chunks/PMID_28623251.txt
data/vdjdb-db-master/chunks/PMID_2862975.txt
data/vdjdb-db-master/chunks/PMID_28636589.txt
data/vdjdb-db-master/chunks/PMID_28636592.txt
data/vdjdb-db-master/chunks/PMID_28724766.txt
data/vdjdb-db-master/chunks/PMID_28934479.txt
[449 more files omitted for size]
```

### Dependencies

- try2/requirements.txt: anndata@>=0.10, harmonypy@>=0.0.10, numpy@>=1.24, pandas@>=2.1, scanpy@>=1.10

### Recent commits (newest first)

- Clean repo for publish: app, model assets, docs, and hygiene updates
- Initial commit

## Key source files (fetched from GitHub, selected and truncated for size)

### README_LOCAL_GUI.md

```markdown
# NeuroTCR Local GUI

This app provides a local web interface to:

1. Upload a TCR relative-abundance file
2. Run MS vs HC inference with the trained random forest model
3. Return predicted class, raw + threshold-adjusted probabilities, calibration metrics (Brier/ECE), and top contributing TCR features
4. Optionally click **Generate Possible Antigens** to run antigen lookup on-demand

## Run

```bash
cd /Users/falconglyph/Documents/tcr-preprocessing
python3 app.py
```

Open: [http://127.0.0.1:8080](http://127.0.0.1:8080)

## Expected Input File

Upload a `.tsv`, `.csv`, or `.txt` table with one of the following schemas:

- `TCR`, `Abundance`
- `cdr3`, `relative_abundance`
- `cdr3_aa`, `relative_abundance`

Optional sample columns supported:

- `sample`, `sample_id`, or `sample_gex`

If your file contains multiple samples, provide `Sample Column Filter` in the UI.

## Environment Variables (Optional)

- `MODEL_PATH`: path to `.joblib` model artifact
- `LOCAL_ANTIGEN_DB_DIR`: local dataset folder for antigen lookup (default `/Users/falconglyph/Documents/tcr-preprocessing/data/vdjdb-db-master/chunks`)
- `ANTIGEN_LOOKUP_MODE`: `local_only` (default), `local_then_api`, or `api_only`
- `VDJDB_API_URL`: antigen lookup endpoint (default `https://vdjdb.cdr3.net/api/database/search`)
- `VDJDB_META_URL`: VDJdb metadata endpoint (default `https://vdjdb.cdr3.net/api/database/meta`)
- `VDJDB_LEGACY_API_URL`: legacy fallback endpoint (default `https://vdjdb.cdr3.net/search`)
- `ANTIGEN_LOOKUP_TIMEOUT_SEC`: lookup timeout in seconds (default `8`)
- `HOST`: default `127.0.0.1`
- `PORT`: default `8080`
- `DEBUG`: `1` to enable Flask debug mode
- `MAX_UPLOAD_BYTES`: max upload size (default `536870912`, i.e. 512 MB)

The app normalizes abundances to sum to 1 before inference.

## Antigen Lookup

The app performs exact CDR3 lookup for each top-contributing CDR3.
By default it uses a local VDJdb dataset bundled in this project:

- `/Users/falconglyph/Documents/tcr-preprocessing/data/vdjdb-db-master/chunks`

Optional fallback modes:
1. `local_then_api`: try local first, then VDJdb API endpoints.
2. `api_only`: skip local and use API only.

Compatibility handling:
- CDR3 strings are normalized to uppercase letters.
- Multiple CDR3 query variants are tested (canonical, trimmed, and padded forms) to improve matching across formatting conventions.

```

### try2/requirements.txt

```
anndata>=0.10
numpy>=1.24
pandas>=2.1
scanpy>=1.10
harmonypy>=0.0.10

```

### data/vdjdb-db-master/Dockerfile

```
FROM ubuntu:18.04

ENV DEBIAN_FRONTEND=noninteractive


# Fix certificate issues
RUN apt-get update && \
    apt-get install -y ca-certificates-java && \
    apt-get clean && \
    update-ca-certificates -f;

# `openjdk-8`
RUN apt-get update
RUN apt-get install -y --no-install-recommends software-properties-common
RUN add-apt-repository -y ppa:openjdk-r/ppa
RUN apt-get update

RUN apt-get update && \
    apt-get install -y \
    build-essential \
    libssl-dev \
    zlib1g-dev \
    libncurses5-dev \
    libgdbm-dev \
    liblzma-dev \
    libsqlite3-dev \
    libbz2-dev \
    libffi-dev \
    wget \
    liblzma-dev \
    libreadline-dev \
    libtk8.6 \
    libgdbm-compat-dev \
    libncursesw5-dev

WORKDIR /usr/src

RUN wget https://www.python.org/ftp/python/3.10.10/Python-3.10.10.tgz && \
    tar xzf Python-3.10.10.tgz && \
    cd Python-3.10.10 && \
    ./configure --enable-optimizations && \
    make altinstall

# Ensure pip is installed
RUN /usr/local/bin/python3.10 -m ensurepip

# Update alternatives to make Python 3.10 the default
RUN update-alternatives --install /usr/bin/python3 python3 /usr/local/bin/python3.10 1 && \
    update-alternatives --install /usr/bin/pip pip /usr/local/bin/pip3.10 1

# Verify Python and pip versions
RUN python3 --version && pip --version

# Optionally install any Python packages you need
# RUN pip install <your-package>
RUN python3 -m pip install 'pandas==2.2.2' 'numpy==2.0.0'
RUN python3 -m pip install 'colorama==0.4.6'

RUN apt-get install -y openjdk-8-jre openjdk-8-jdk openjdk-8-jdk-headless openjdk-8-jre-headless
RUN update-alternatives --config java
RUN update-alternatives --config javac

# Setup JAVA_HOME -- useful for docker commandline
ENV JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64/
RUN export JAVA_HOME

RUN apt-get install -y wget
RUN apt-get install -y unzip
RUN apt-get install -y git
RUN apt-get install -y curl
RUN apt-get install -y zip
RUN apt-get install -y pandoc

SHELL ["/bin/bash", "-c"] 

RUN curl -s "https://get.sdkman.io" | bash
RUN source "$HOME/.sdkman/bin/sdkman-init.sh" && \
    sdk install groovy 3.0.9

# needed for R 
ENV DEBIAN_FRONTEND noninteractive

RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys E298A3A825C0D65DFD57CBB651716619E084DAB9
RUN add-apt-repository -y 'deb https://cloud.r-project.org/bin/linux/ubuntu bionic-cran40/'
RUN apt-get update

RUN apt-get update \
        && apt-get install -y --no-install-recommends \
         r-base \
         r-base-dev \
         r-recommended

# for R deps
RUN apt-get install -y libnlopt-dev
RUN apt-get install -y libfontconfig1-dev
RUN apt-get install -y libcurl4-openssl-dev
RUN apt-get install -y libssl-dev
RUN apt-get install -y libxml2 libxml2-dev
RUN apt-get install -y libcairo2-dev libxt-dev libx11-dev
RUN apt-get install -y libmagick++-dev
RUN apt-get install -y libharfbuzz-dev libfribidi-dev

# 'knitr', 'htmltools', 'jquerylib', 'stringr' are not available for package 'rmarkdown'
RUN Rscript -e 'install.packages(c("knitr", "ggplot2", "RColorBrewer", "data.table", "forcats", "ggh4x", "ggalluvial", "circlize", "ggrepel", "tidyverse", "httr", "xml2", "stringr", "gridExtra", "maps", "scatterpie", "dplyr", "stringr", "stringdist", "reshape2", "igraph", "ggseqlogo", "parallel", "cowplot"))'

RUN apt-get install -y texlive-latex-base texlive-fonts-recommended texlive-fonts-extra texlive-latex-extra
RUN apt-get install -y build-essential procps curl file git

RUN wget https://github.com/mikessh/vdjtools/releases/download/1.2.1/vdjtools-1.2.1.zip
RUN mkdir -p /software/bin/
RUN unzip vdjtools-1.2.1.zip
RUN cp -r vdjtools-1.2.1/* /software/bin/
RUN chmod +x /software/bin/vdjtools

ENV PATH="/usr/lib/jvm/java-8-openjdk-amd64/bin:${PATH}"
ENV PATH="/software/bin:${PATH}"

RUN touch docker.sh
RUN echo '# /bin/sh' >> docker.sh
RUN echo '[[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh"' >> docker.sh
RUN echo 'mkdir -p vcs' >> docker.sh
RUN echo 'cd vcs' >> docker.sh
RUN echo 'echo $(pwd)' >> docker.sh
RUN echo 'git clone https://github.com/antigenomics/vdjdb-db vdjdb-db' >> docker.sh
RUN echo 'git clone https://github.com/antigenomics/vdjdb-motifs vdjdb-motifs' >> docker.sh
RUN echo 'cd vdjdb-db' >> docker.sh
RUN echo 'echo $(pwd)' >> docker.sh
RUN echo 'mkdir -p /root/output' >> docker.sh
RUN echo 'bash release.sh 2>&1 | tee /root/output/buildlog' >> docker.sh
RUN echo 'cp -r database/*zip /root/output/' >> docker.sh

CMD [ "bash", "docker.sh" ]
```

### static/app.js

```javascript
const form = document.getElementById("predict-form");
const submitBtn = document.getElementById("submit-btn");
const statusPanel = document.getElementById("status-panel");
const statusText = document.getElementById("status-text");
const resultPanel = document.getElementById("result-panel");
const pipelineMeta = document.getElementById("pipeline-meta");
const hitsTable = document.getElementById("hits-table");
const hitsHeaderRow = document.querySelector("#hits-table thead tr");
const hitsTableBody = document.querySelector("#hits-table tbody");
const annotateBtn = document.getElementById("annotate-antigens-btn");
const antigenLoading = document.getElementById("antigen-loading");
let currentTopHits = [];
let currentInputMeta = null;
let currentLookupMeta = null;
let currentStability = null;
let antigenColumnsVisible = false;

function showStatus(message, isError = false) {
  statusPanel.classList.remove("hidden");
  statusText.textContent = message;
  statusText.style.color = isError ? "#b0431d" : "#12486b";
}

function clearResults() {
  resultPanel.classList.add("hidden");
  hitsTableBody.innerHTML = "";
  pipelineMeta.textContent = "";
  currentTopHits = [];
  currentInputMeta = null;
  currentLookupMeta = null;
  currentStability = null;
  antigenColumnsVisible = false;
  renderTableHeader();
  setAntigenLoading(false);
  if (hitsTable) {
    hitsTable.classList.remove("antigen-columns-enter");
    hitsTable.classList.remove("antigen-columns-enter-active");
  }
  if (annotateBtn) annotateBtn.disabled = true;
}

function setResultValue(id, value) {
  const el = document.getElementById(id);
  if (el) {
    el.textContent = value;
  }
}

function formatMaybeNumber(value, decimals = 4) {
  if (value === null || value === undefined || Number.isNaN(Number(value))) {
    return "N/A";
  }
  return Number(value).toFixed(decimals);
}

function formatMaybePercent(value, decimals = 1) {
  if (value === null || value === undefined || Number.isNaN(Number(value))) {
    return "N/A";
  }
  return `${(Number(value) * 100).toFixed(decimals)}%`;
}

function renderTableHeader() {
  if (!hitsHeaderRow) return;
  if (antigenColumnsVisible) {
    hitsHeaderRow.innerHTML = `
      <th>CDR3</th>
      <th>Abundance</th>
      <th>Importance</th>
      <th>Impact</th>
      <th class="antigen-col">Antigen Match</th>
      <th class="antigen-col">Match Type</th>
    `;
    return;
  }
  hitsHeaderRow.innerHTML = `
    <th>CDR3</th>
    <th>Abundance</th>
    <th>Importance</th>
    <th>Impact</th>
  `;
}

function setAntigenLoading(active) {
  if (!antigenLoading) return;
  if (active) {
    antigenLoading.classList.remove("hidden");
    requestAnimationFrame(() => antigenLoading.classList.add("active"));
    return;
  }
  antigenLoading.classList.remove("active");
  window.setTimeout(() => antigenLoading.classList.add("hidden"), 170);
}

function revealAntigenColumns() {
  if (!hitsTable) return;
  if (!antigenColumnsVisible) {
    antigenColumnsVisible = true;
    renderTableHeader();
    hitsTable.classList.add("antigen-columns-enter");
    renderHits(currentTopHits);
    requestAnimationFrame(() => {
      hitsTable.classList.add("antigen-columns-enter-active");
    });
    window.setTimeout(() => {
      hitsTable.classList.remove("antigen-columns-enter");
      hitsTable.classList.remove("antigen-columns-enter-active");
    }, 320);
    return;
  }
  renderHits(currentTopHits);
}

function renderHits(rows) {
  hitsTableBody.innerHTML = "";
  for (const row of rows || []) {
    const tr = document.createElement("tr");
    if (antigenColumnsVisible) {
      tr.innerHTML = `
        <td>${row.cdr3_aa}</td>
        <td>${Number(row.abundance).toFixed(6)}</td>
        <td>${Number(row.importance).toExponential(3)}</td>
        <td>${Number(row.impact_score).toExponential(3)}</td>
        <td class="antigen-col">${row.antigen || "unknown"}</td>
        <td class="antigen-col">${row.match_type || "unknown"}</td>
      `;
    } else {
      tr.innerHTML = `
        <td>${row.cdr3_aa}</td>
        <td>${Number(row.abundance).toFixed(6)}</td>
        <td>${Number(row.importance).toExponential(3)}</td>
        <td>${Number(row.impact_score).toExponential(3)}</td>
      `;
    }
    hitsTableBody.appendChild(tr);
  }
  if (!rows || !rows.length) {
    const colspan = antigenColumnsVisible ? 6 : 4;
    const tr = document.createElement("tr");
    tr.innerHTML = `<td colspan="${colspan}">No overlapping model features found in input file.</td>`;
    hitsTableBody.appendChild(tr);
  }
}

function updateMetaPanel() {
  pipelineMeta.textContent = JSON.stringify(
    {
      input: currentInputMeta,
      stability: currentStability,
      antigen_lookup: currentLookupMeta,
    },
    null,
    2
  );
}

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  clearResults();
  showStatus("Uploading abundance file and running prediction...");
  submitBtn.disabled = true;

  const data = new FormData(form);

  try {
    const response = await fetch("/api/predict", {
      method: "POST",
      body: data,
    });
    const payload = await response.json();

    if (!response.ok || !payload.ok) {
      throw new Error(payload.error || "Unknown error");
    }

    showStatus(`Prediction run ${payload.run_id} completed successfully.`);

    const labelDisplay =
      String(payload.prediction.label).toUpperCase() === "HC"
        ? "Healthy"
        : payload.prediction.label;
    setResultValue("pred-label", labelDisplay);
    setResultValue("pred-ms", formatMaybeNumber(payload.prediction.probability_ms, 4));
    setResultValue("pred-hc", formatMaybeNumber(payload.prediction.probability_hc, 4));
    setResultValue(
      "pred-ms-adjusted",
      formatMaybeNumber(payload.prediction.adjusted_probability_ms, 4)
    );
    setResultValue(
      "pred-confidence",
      `${payload.prediction.confidence_level} (${formatMaybeNumber(payload.prediction.confidence_score, 2)})`
    );
    setResultValue("pred-thres
[truncated — 3174 more characters]
```

### app.py

```python
#!/usr/bin/env python3
import csv
import json
import os
import re
import time
import uuid
from pathlib import Path
from urllib.parse import urlparse
from urllib import request as urlrequest

import joblib
import numpy as np
import pandas as pd
from flask import Flask, jsonify, render_template, request


BASE_DIR = Path(__file__).resolve().parent
RUNS_DIR = BASE_DIR / "web_runs"
RUNS_DIR.mkdir(exist_ok=True)

DEFAULT_MODEL_PATH = (
    BASE_DIR
    / "rf_ms_model_output_training_tsvs_augmented_v4_split_80_20_20seeds"
    / "seed_02"
    / "random_forest_ms_vs_hc.joblib"
)

VALID_LOOKUP_MODES = {"local_only", "local_then_api", "api_only"}
LOCAL_LOOKUP_CACHE: dict[str, dict] = {}


def create_app() -> Flask:
    app = Flask(__name__)
    app.config["MAX_CONTENT_LENGTH"] = int(
        os.getenv("MAX_UPLOAD_BYTES", str(512 * 1024 * 1024))
    )
    app.config["MODEL_PATH"] = Path(os.getenv("MODEL_PATH", str(DEFAULT_MODEL_PATH)))
    app.config["MODEL_CALIBRATION"] = load_model_calibration_metrics(app.config["MODEL_PATH"])
    app.config["VDJDB_API_URL"] = os.getenv(
        "VDJDB_API_URL", "https://vdjdb.cdr3.net/api/database/search"
    ).strip()
    app.config["VDJDB_META_URL"] = os.getenv(
        "VDJDB_META_URL", "https://vdjdb.cdr3.net/api/database/meta"
    ).strip()
    app.config["VDJDB_LEGACY_API_URL"] = os.getenv(
        "VDJDB_LEGACY_API_URL", "https://vdjdb.cdr3.net/search"
    ).strip()
    app.config["LOCAL_ANTIGEN_DB_DIR"] = Path(
        os.getenv(
            "LOCAL_ANTIGEN_DB_DIR",
            str(BASE_DIR / "data" / "vdjdb-db-master" / "chunks"),
        )
    )
    app.config["ANTIGEN_LOOKUP_MODE"] = normalize_lookup_mode(
        os.getenv("ANTIGEN_LOOKUP_MODE", "local_only")
    )
    app.config["ANTIGEN_LOOKUP_TIMEOUT_SEC"] = float(os.getenv("ANTIGEN_LOOKUP_TIMEOUT_SEC", "8"))
    app.config["STABILITY_N_RUNS"] = max(20, int(os.getenv("STABILITY_N_RUNS", "100")))
    app.config["STABILITY_NOISE_SIGMA"] = max(
        0.01, float(os.getenv("STABILITY_NOISE_SIGMA", "0.20"))
    )
    app.config["STABILITY_SEED"] = int(os.getenv("STABILITY_SEED", "42"))
    app.config["ANTIGEN_LOOKUP_META"] = make_antigen_lookup_meta(app.config)

    @app.get("/")
    def index():
        return render_template(
            "index.html",
            model_path=str(app.config["MODEL_PATH"]),
            antigen_lookup_meta=app.config["ANTIGEN_LOOKUP_META"],
        )

    @app.post("/api/predict")
    def predict():
        abundance_file = request.files.get("abundance_file")
        sample_name = request.form.get("sample_name", "").strip()
        selected_sample = request.form.get("selected_sample", "").strip()

        if not abundance_file:
            return jsonify({"ok": False, "error": "Upload a TCR relative-abundance file."}), 400

        if not is_table_filename(abundance_file.filename):
            return (
                jsonify(
                    {
                        "ok": False,
                        "error": "Invalid file extension. Expected .tsv/.csv/.txt.",
                    }
                ),
                400,
            )

        run_id = uuid.uuid4().hex[:10]
        run_dir = RUNS_DIR / run_id
        run_dir.mkdir(parents=True, exist_ok=True)

        input_path = run_dir / sanitize_filename(abundance_file.filename)
        abundance_file.save(input_path)

        model_artifact = load_model_artifact(app.config["MODEL_PATH"])

        try:
            (
                proba_ms,
                predicted_label,
                used_threshold,
                top_hits,
                parsed_meta,
                feature_vector,
            ) = predict_from_file(
                model_artifact=model_artifact,
                table_path=input_path,
                selected_sample=selected_sample,
            )
        except Exception as exc:
            return jsonify({"ok": False, "error": f"Prediction failed: {exc}"}), 500

        top_hits = initialize_top_hits_unknown(top_hits)
        if not sample_name:
            sample_name = parsed_meta.get("resolved_sample") or input_path.stem

        stability = compute_prediction_stability(
            model=model_artifact["model"],
            feature_names=list(model_artifact["feature_names"]),
            base_vector=feature_vector,
            base_probability_ms=proba_ms,
            decision_threshold=used_threshold,
            n_runs=app.config["STABILITY_N_RUNS"],
            noise_sigma=app.config["STABILITY_NOISE_SIGMA"],
            seed=app.config["STABILITY_SEED"],
        )
        adjusted_probability_ms = threshold_adjusted_probability(
            probability_ms=proba_ms, decision_threshold=used_threshold
        )
        confidence_score, confidence_level = decision_confidence(
            probability_ms=proba_ms, decision_threshold=used_threshold
        )

        return jsonify(
            {
                "ok": True,
                "run_id": run_id,
                "sample_name": sample_name,
                "prediction": {
                    "label": predicted_label,
                    "probability_ms": round(proba_ms, 6),
                    "probability_hc": round(1.0 - proba_ms, 6),
                    "threshold": round(used_threshold, 6),
                    "adjusted_probability_ms": round(adjusted_probability_ms, 6),
                    "confidence_score": round(confidence_score, 6),
                    "confidence_level": confidence_level,
                },
                "model_path": str(app.config["MODEL_PATH"]),
                "input": {
                    "path": str(input_path),
                    "selected_sample": selected_sample or None,
                    "resolved_sample": parsed_meta.get("resolved_sample"),
                    "n_unique_tcr": parsed_meta.get("n_unique_tcr"),
                },
                "calibration": app.config["MODEL_CALIBRATION"],
                "stability": stability,
                "antigen_lookup": {
             
[truncated — 37936 more characters]
```

### train_rf_ms_classifier.py

```python
#!/usr/bin/env python3
"""
Train a Random Forest classifier to predict MS vs HC from CDR3 relative-abundance data.

Input format (CSV):
  - sample_gex: sample identifier (treated as patient/sample unit)
  - group: class label ("MS" or "HC")
  - cdr3_aa: CDR3 amino-acid sequence
  - relative_abundance: relative abundance of that CDR3 in the sample

Alternative input format (directory of TSV files):
  - One file per sample with columns: TCR, Abundance
  - Filename prefix encodes class:
      - Health*  => HC
      - Patient* => MS

Example:
  python3 train_rf_ms_classifier.py \
    --input pbmc_ms_hc_tcell_cdr3_relative_abundance.csv \
    --outdir rf_ms_model_output

  python3 train_rf_ms_classifier.py \
    --input-dir Training_tsvs \
    --outdir rf_ms_model_output_from_tsvs
"""

from __future__ import annotations

import argparse
import json
from pathlib import Path

import joblib
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    accuracy_score,
    balanced_accuracy_score,
    confusion_matrix,
    f1_score,
    precision_score,
    recall_score,
    roc_auc_score,
)
from sklearn.model_selection import StratifiedKFold, cross_val_predict, cross_validate
from sklearn.metrics import make_scorer
from sklearn.model_selection import train_test_split


REQUIRED_COLUMNS = {"sample_gex", "group", "cdr3_aa", "relative_abundance"}
TSV_COLUMNS = {"TCR", "Abundance"}


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Train MS vs HC Random Forest classifier.")
    parser.add_argument(
        "--input",
        type=Path,
        default=None,
        help="Input CSV path (sample_gex/group/cdr3_aa/relative_abundance format).",
    )
    parser.add_argument(
        "--input-dir",
        type=Path,
        default=None,
        help="Directory containing one TSV per sample with TCR/Abundance columns.",
    )
    parser.add_argument(
        "--outdir",
        type=Path,
        default=Path("rf_ms_model_output"),
        help="Directory for model and reports.",
    )
    parser.add_argument(
        "--n-estimators",
        type=int,
        default=1000,
        help="Number of trees in the random forest.",
    )
    parser.add_argument(
        "--max-features",
        default="sqrt",
        help='max_features for RandomForestClassifier (e.g., "sqrt", "log2", or float).',
    )
    parser.add_argument(
        "--random-state",
        type=int,
        default=42,
        help="Random seed.",
    )
    parser.add_argument(
        "--n-jobs",
        type=int,
        default=1,
        help="Parallel workers (use 1 in restricted environments).",
    )
    parser.add_argument(
        "--top-features",
        type=int,
        default=50,
        help="Number of top CDR3 features to export by importance.",
    )
    parser.add_argument(
        "--evaluation-mode",
        choices=["cv", "split"],
        default="cv",
        help="Evaluation mode: cross-validation ('cv') or stratified train/test split ('split').",
    )
    parser.add_argument(
        "--test-size",
        type=float,
        default=0.2,
        help="Fraction of samples in test split when --evaluation-mode split is used.",
    )
    return parser.parse_args()


def validate_input(df: pd.DataFrame) -> None:
    missing = REQUIRED_COLUMNS - set(df.columns)
    if missing:
        raise ValueError(f"Missing required columns: {sorted(missing)}")


def infer_group_from_name(name: str) -> str:
    lower = name.lower()
    if lower.startswith("health"):
        return "HC"
    if lower.startswith("patient"):
        return "MS"
    raise ValueError(
        f"Could not infer class from filename '{name}'. Expected prefix Health* or Patient*."
    )


def load_tsv_directory(input_dir: Path) -> pd.DataFrame:
    if not input_dir.exists() or not input_dir.is_dir():
        raise ValueError(f"--input-dir does not exist or is not a directory: {input_dir}")

    tsv_files = sorted(input_dir.glob("*.tsv"))
    if not tsv_files:
        raise ValueError(f"No .tsv files found in directory: {input_dir}")

    rows = []
    for path in tsv_files:
        sample_name = path.stem
        group = infer_group_from_name(sample_name)
        sample_df = pd.read_csv(path, sep="\t")
        missing = TSV_COLUMNS - set(sample_df.columns)
        if missing:
            raise ValueError(f"{path} missing required columns: {sorted(missing)}")

        sample_df = sample_df[list(TSV_COLUMNS)].copy()
        sample_df.rename(columns={"TCR": "cdr3_aa", "Abundance": "relative_abundance"}, inplace=True)
        sample_df["sample_gex"] = sample_name
        sample_df["group"] = group
        rows.append(sample_df)

    df = pd.concat(rows, ignore_index=True)
    df["cdr3_aa"] = df["cdr3_aa"].astype(str).str.strip()
    df["relative_abundance"] = pd.to_numeric(df["relative_abundance"], errors="coerce")
    df = df[df["cdr3_aa"].ne("") & df["relative_abundance"].notna() & (df["relative_abundance"] > 0)].copy()

    # Normalize each sample to exactly sum to 1.0.
    per_sample_sum = df.groupby("sample_gex")["relative_abundance"].transform("sum")
    df["relative_abundance"] = df["relative_abundance"] / per_sample_sum

    return df[["sample_gex", "group", "cdr3_aa", "relative_abundance"]]


def build_sample_matrix(df: pd.DataFrame) -> tuple[pd.DataFrame, pd.Series]:
    filtered = df[df["group"].isin(["MS", "HC"])].copy()
    if filtered.empty:
        raise ValueError("No MS/HC rows found after filtering.")

    # Ensure one label per sample.
    sample_labels = (
        filtered[["sample_gex", "group"]]
        .drop_duplicates()
        .set_index("sample_gex")["group"]
        .sort_index()
    )
    if sample_labels.index.duplicated().any():
        raise ValueError("Duplicate sample IDs detected with inconsistent labels.")

    # Build sample x CDR3 relative-abundance feature matrix.
    x = (
        filtered.pivot_table(
            i
[truncated — 11834 more characters]
```

### templates/index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>MS Blood-Based Diagnosis</title>
    <link rel="stylesheet" href="/static/styles.css" />
  </head>
  <body>
    <div class="bg-grid"></div>
    <main class="shell">
      <header class="hero reveal">
        <p class="eyebrow">Clinical Decision Support</p>
        <h1>MS Blood-Based Diagnosis</h1>
        <p class="hero-copy">
          Quick and accurate MS diagnosis using scRNA sequencing of blood.
        </p>
      </header>

      <section class="panel reveal delay-1">
        <h2>Run Diagnostic Analysis</h2>
        <form id="predict-form">
          <div class="grid">
            <label>
              <span>Sample Identifier (optional)</span>
              <input type="text" name="sample_name" placeholder="Sample_001" />
            </label>

            <label>
              <span>TCR Relative Abundance File</span>
              <input type="file" name="abundance_file" accept=".tsv,.csv,.txt" required />
            </label>

            <label>
              <span>Sample Column Filter (optional)</span>
              <input
                type="text"
                name="selected_sample"
                placeholder="Use when file contains multiple samples"
              />
            </label>
          </div>

          <button id="submit-btn" type="submit">Run Prediction</button>
        </form>
      </section>

      <section class="panel panel-config reveal delay-2">
        <h2>System Configuration</h2>
        <ul>
          <li><strong>Model:</strong> <code>{{ model_path }}</code></li>
          <li><strong>Accepted formats:</strong> <code>TCR,Abundance</code> or <code>cdr3,relative_abundance</code> or <code>cdr3_aa,relative_abundance</code></li>
          <li><strong>Input type:</strong> <code>.tsv</code>, <code>.csv</code>, <code>.txt</code></li>
          <li><strong>Antigen lookup:</strong> <code>{{ antigen_lookup_meta.provider }}</code></li>
          <li><strong>Lookup mode:</strong> <code>{{ antigen_lookup_meta.mode }}</code></li>
          <li><strong>Lookup target:</strong> <code>{{ antigen_lookup_meta.endpoint }}</code></li>
        </ul>
      </section>

      <section id="status-panel" class="panel status hidden">
        <h2>Status</h2>
        <p id="status-text"></p>
      </section>

      <section id="result-panel" class="panel result hidden">
        <h2>Diagnostic Result</h2>
        <div class="result-grid">
          <div class="tile">
            <p class="label">Predicted Outcome</p>
            <p id="pred-label" class="value"></p>
          </div>
          <div class="tile">
            <p class="label">Adjusted MS Probability</p>
            <p id="pred-ms-adjusted" class="value"></p>
          </div>
          <div class="tile">
            <p class="label">Decision Confidence</p>
            <p id="pred-confidence" class="value"></p>
          </div>
          <div class="tile">
            <p class="label">Prediction Stability</p>
            <p id="pred-stability" class="value"></p>
            <p id="pred-stability-detail" class="subvalue"></p>
          </div>
        </div>

        <details class="advanced-stats">
          <summary>Advanced Statistics</summary>
          <div class="advanced-grid">
            <div class="stat-row">
              <span>Raw MS Probability</span>
              <strong id="pred-ms"></strong>
            </div>
            <div class="stat-row">
              <span>Raw Healthy Probability</span>
              <strong id="pred-hc"></strong>
            </div>
            <div class="stat-row">
              <span>Decision Threshold</span>
              <strong id="pred-threshold"></strong>
            </div>
            <div class="stat-row">
              <span>Brier Score (lower is better)</span>
              <strong id="cal-brier"></strong>
            </div>
            <div class="stat-row">
              <span>ECE (10 bins, lower is better)</span>
              <strong id="cal-ece"></strong>
            </div>
            <div class="stat-row">
              <span>Calibration Evaluation N</span>
              <strong id="cal-n"></strong>
            </div>
            <div class="stat-row">
              <span>Stability Runs</span>
              <strong id="stab-runs"></strong>
            </div>
            <div class="stat-row">
              <span>MS Vote Rate</span>
              <strong id="stab-ms-vote"></strong>
            </div>
            <div class="stat-row">
              <span>Label Flip Rate</span>
              <strong id="stab-flip-rate"></strong>
            </div>
            <div class="stat-row">
              <span>MS Probability 5-95% Range</span>
              <strong id="stab-range"></strong>
            </div>
          </div>
        </details>

        <h3>Top Contributing TCR Features</h3>
        <div class="actions-row">
          <button id="annotate-antigens-btn" type="button" class="secondary-btn" disabled>
            Generate Possible Antigens
          </button>
        </div>
        <div id="antigen-loading" class="lookup-progress hidden" aria-live="polite">
          <div class="lookup-progress-track">
            <div class="lookup-progress-bar"></div>
          </div>
        </div>
        <p class="hint">Antigen lookup is optional and runs only when you click this button.</p>
        <table id="hits-table">
          <thead>
            <tr>
              <th>CDR3</th>
              <th>Abundance</th>
              <th>Importance</th>
              <th>Impact</th>
            </tr>
          </thead>
          <tbody></tbody>
        </table>

        <details>
          <summary>Input Details</summary>
          <pre id="pipeline-meta"></pre>
        </details>
      </section>
    </main>

    <script src="/static/app.js"></script>
  </body>
</html>

```

### data/vdjdb-db-master/test.sh

```shell
cd src/
groovy -cp . BuildDatabase.groovy --no2fix
```

### data/vdjdb-db-master/release_docker.sh

```shell
docker build --no-cache -t vdjdbdb . 2>&1 | tee database/docker_build.log
docker run -v `pwd`/database:/root/output vdjdbdb 2>&1 | tee database/docker_run.log
```

### data/vdjdb-db-master/.travis.yml

```yaml
dist: xenial

language:
  - groovy
  - python

python:
  - 2.7

jdk:
  - openjdk8

before_install:
  - sudo pip install pandas
  - sudo pip install 'biopython==1.76'
  
script:
  - cd src/ 
  - travis_wait groovy -cp . BuildDatabase.groovy --no2fix

```

[22 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]