from __future__ import annotations import html import math import tempfile from functools import lru_cache from pathlib import Path import spaces import gradio as gr import pandas as pd import plotly.express as px import plotly.graph_objects as go from biolmnet.artifacts import load_bundle, save_bundle from biolmnet.data import ( PreparedWorkspace, attach_embeddings_and_pathways, build_biological_mask, github_dataset_sources, load_genept_embeddings, read_csv, upstream_example_sources, upstream_interaction_sources, validate_and_align_omics, ) from biolmnet.training import ( Hyperparameters, ModelBundle, pathway_importance, predict, train, ) GENEPT_OPTIONS = { "Auto — bulk or single-cell based on source": "auto", "Bulk · large-3 context": "embedding_original_large_3.parquet", "Bulk · original ada-text context": "embedding_original_ada_text.parquet", "Single-cell · cell type, tissue, drug & pathway": ( "embedding_associations_cell_type_tissue_drug_pathway_openai_large.parquet" ), "Single-cell · age, cell type, drugs & pathways": ( "embedding_associations_age_cell_type_drugs_pathways_openai_large.parquet" ), } CSS = """ :root { --ink: #102824; --muted: #60726e; --line: #d7e2de; --mint: #e8f4ee; --leaf: #167c5a; --leaf-dark: #0e5b43; --amber: #e5a63c; } .gradio-container { max-width: 1260px !important; margin: 0 auto !important; background: radial-gradient(circle at 92% 4%, rgba(209, 235, 224, .7), transparent 26rem), #f8fbf9 !important; color: var(--ink) !important; } .biolm-hero { position: relative; overflow: hidden; padding: 34px 36px 30px; margin: 12px 0 18px; border: 1px solid #cfe0d9; border-radius: 24px; background: linear-gradient(135deg, #0b2f28 0%, #124b3d 70%, #17694f 100%); box-shadow: 0 18px 50px rgba(19, 63, 52, .12); } .biolm-hero::after { content: ""; position: absolute; width: 250px; height: 250px; right: -60px; top: -90px; border: 1px solid rgba(255,255,255,.2); border-radius: 50%; box-shadow: 0 0 0 34px rgba(255,255,255,.035), 0 0 0 68px rgba(255,255,255,.025); } .eyebrow { color: #a9e2ca; font: 700 12px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .14em; text-transform: uppercase; } .biolm-hero h1 { color: white; font-size: clamp(34px, 5vw, 58px); line-height: .98; letter-spacing: -.045em; margin: 12px 0 14px; } .biolm-hero p { max-width: 780px; color: #d6e8e1; font-size: 17px; line-height: 1.55; margin: 0; } .hero-meta { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 22px; } .hero-chip { color: #e9f7f1; border: 1px solid rgba(255,255,255,.23); background: rgba(255,255,255,.07); border-radius: 999px; padding: 7px 11px; font: 600 12px/1 ui-monospace, SFMono-Regular, Menlo, monospace; } .phase-card { border: 1px solid var(--line) !important; border-radius: 18px !important; background: rgba(255,255,255,.86) !important; box-shadow: 0 9px 26px rgba(27, 68, 57, .05) !important; } .phase-intro { border-left: 3px solid var(--leaf); padding: 2px 0 2px 15px; color: var(--muted); } .phase-intro strong { color: var(--ink); } .status-box { border-radius: 15px; padding: 14px 16px; background: var(--mint); border: 1px solid #cbe3d8; color: #254d41; } .error-box { border-radius: 15px; padding: 14px 16px; background: #fff1ee; border: 1px solid #f0cac2; color: #793b31; } .metric-grid { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 10px; } .metric { padding: 13px 14px; border: 1px solid #d4e2dd; border-radius: 14px; background: white; } .metric span { display:block; color: var(--muted); font-size: 11px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; } .metric b { display:block; margin-top: 3px; color: var(--ink); font-size: 22px; } button.primary { background: var(--leaf) !important; border-color: var(--leaf) !important; } button.primary:hover { background: var(--leaf-dark) !important; } .footnote { color: #6d7e79; font-size: 12px; line-height: 1.55; } @media (max-width: 720px) { .biolm-hero { padding: 26px 22px; border-radius: 18px; } .metric-grid { grid-template-columns: repeat(2, minmax(0,1fr)); } } """ def _status(message: str, error: bool = False) -> str: class_name = "error-box" if error else "status-box" return f'
Build a biologically masked network from paired gene expression and DNA methylation, train it with GenePT-guided pathway attention, then carry the exact preprocessing and architecture into prediction.
Assemble the model graph. Select an upstream example, point to a GitHub folder that follows the BioLM-NET file convention, or upload paired omics files.
""" ) with gr.Row(): with gr.Column(scale=7, elem_classes=["phase-card"]): source_mode = gr.Radio( [ "BioLM-NET examples", "GitHub folder", "Upload files", ], value="BioLM-NET examples", label="Dataset source", ) with gr.Column(visible=True) as example_group: example_dataset = gr.Dropdown( ["BRCA", "COAD", "GBM", "scTrioseq2"], value="BRCA", label="Repository dataset", ) with gr.Column(visible=False) as github_group: github_folder = gr.Textbox( label="GitHub dataset folder", placeholder=( "https://github.com/owner/repo/tree/main/Dataset/BRCA" ), info=( "The folder must contain the five standard " "BioLM-NET CSV filenames." ), ) with gr.Column(visible=False) as upload_group: with gr.Row(): uploaded_gene = gr.File( label="Gene expression", file_types=[".csv"], type="filepath", ) uploaded_dna = gr.File( label="DNA methylation", file_types=[".csv"], type="filepath", ) uploaded_labels = gr.File( label="Labels", file_types=[".csv"], type="filepath", ) with gr.Row(): uploaded_gene_pathway = gr.File( label="Gene → pathway mapping", file_types=[".csv"], type="filepath", ) uploaded_dna_pathway = gr.File( label="DNA → pathway mapping", file_types=[".csv"], type="filepath", ) pathway_files_are_significant = gr.Checkbox( value=True, label="Pathway files already contain significant pathways", info=( "Turn off for a full SYMBOL/PathwayID annotation " "catalog; enrichment will use BH-adjusted p < 0.05." ), ) with gr.Column(scale=5, elem_classes=["phase-card"]): embedding_option = gr.Dropdown( list(GENEPT_OPTIONS), value=list(GENEPT_OPTIONS)[0], label="GenePT context", ) with gr.Accordion("Interaction priors", open=False): gr.Markdown( "By default, the app retrieves `PDI.csv` and `PPI.csv` " "from `bozdaglab/BioLM-NET`. Upload both only to override." ) uploaded_pdi = gr.File( label="Custom PDI.csv", file_types=[".csv"], type="filepath", ) uploaded_ppi = gr.File( label="Custom PPI.csv", file_types=[".csv"], type="filepath", ) prepare_button = gr.Button( "Build biological architecture", variant="primary", size="lg", ) preparation_status = gr.HTML( _status("Choose a source, then build the biological architecture.") ) with gr.Row(): architecture_table = gr.Dataframe( label="Sparse architecture audit", interactive=False, wrap=True, ) enrichment_table = gr.Dataframe( label="Retained enriched pathways (first 100)", interactive=False, wrap=True, ) gr.HTML( """Expected orientation: samples in rows and
HGNC gene symbols in columns. PDI requires TF, Target;
PPI requires protein1, protein2, combined_score;
pathways require SYMBOL, PathwayID. PPI is filtered
to score > 0.7 and the retained top decile, following the paper.
Fit and evaluate. The split is stratified; scaling is fit on training samples only; the best validation checkpoint is exported as a safe, self-contained model artifact. A shared GPU is requested only while this training callback is running.
""" ) with gr.Row(): with gr.Column(scale=4, elem_classes=["phase-card"]): epochs = gr.Slider(5, 200, value=50, step=5, label="Epochs") batch_size = gr.Dropdown( [8, 16, 32, 64, 128], value=16, label="Batch size" ) learning_rate = gr.Number( value=0.001, label="Learning rate", minimum=0.000001 ) weight_decay = gr.Number( value=0.01, label="L2 weight decay", minimum=0 ) dropout = gr.Slider( 0, 0.8, value=0.3, step=0.05, label="Dropout" ) with gr.Column(scale=4, elem_classes=["phase-card"]): projection_dim = gr.Dropdown( [16, 32, 64, 128], value=64, label="Branch projection" ) fusion_dim = gr.Dropdown( [8, 12, 16, 32], value=12, label="Fusion layer" ) validation_fraction = gr.Slider( 0.1, 0.4, value=0.2, step=0.05, label="Validation fraction", ) optimizer = gr.Radio( ["Adam", "SGD"], value="Adam", label="Optimizer" ) class_weighting = gr.Checkbox( value=True, label="Balance classes in the loss", info="Uses N / (classes × samples in class), as in the paper.", ) with gr.Column(scale=4, elem_classes=["phase-card"]): gr.Markdown( """ **Paper-faithful defaults** - First layer: trainable `W ⊙ M` - PDI weights: binary - PPI weights: normalized STRING score - Pathway attention: GenePT query attention - Fusion: dual branch → dense → softmax """ ) train_button = gr.Button( "Train BioLM-NET on ZeroGPU", variant="primary", size="lg" ) model_download = gr.File( label="Trained model artifact", interactive=False ) gr.Markdown( "ZeroGPU reserves 30–300 seconds according to dataset " "size and epochs. Visitors use their own daily quota." ) training_status = gr.HTML( _status("Phase 2 unlocks after the architecture is prepared.") ) with gr.Row(): loss_plot = gr.Plot(label="Training history") confusion_plot = gr.Plot(label="Confusion matrix") with gr.Row(): validation_table = gr.Dataframe( label="Validation predictions", interactive=False, wrap=True, ) importance_table = gr.Dataframe( label="Pathway attention audit (first 100)", interactive=False, wrap=True, ) with gr.Tab("3 · Predict", id="predict"): gr.HTML( """Apply a trained model. Continue with the model from this session or upload a previous artifact. Feature names are validated and reordered exactly as they were during training.
""" ) with gr.Row(): with gr.Column(scale=4, elem_classes=["phase-card"]): prediction_artifact = gr.File( label="Optional trained model artifact", file_types=[".zip"], type="filepath", ) prediction_gene = gr.File( label="Prediction gene expression", file_types=[".csv"], type="filepath", ) prediction_dna = gr.File( label="Prediction DNA methylation", file_types=[".csv"], type="filepath", ) predict_button = gr.Button( "Make predictions", variant="primary", size="lg" ) prediction_download = gr.File( label="Prediction CSV", interactive=False ) with gr.Column(scale=8): prediction_status = gr.HTML( _status("Use the current trained model or upload an artifact.") ) prediction_plot = gr.Plot(label="Class distribution") prediction_table = gr.Dataframe( label="Predictions and class probabilities", interactive=False, wrap=True, ) gr.Markdown( """Research use only. This interface reproduces the architecture described by Rifat et al. and uses the upstream BioLM-NET repository and GenePT embeddings. Validate cohorts, preprocessing, and model performance before drawing biological or clinical conclusions.
""" ) source_mode.change( _source_visibility, inputs=[source_mode], outputs=[example_group, github_group, upload_group], ) prepare_button.click( prepare_workspace, inputs=[ source_mode, example_dataset, github_folder, uploaded_gene, uploaded_dna, uploaded_labels, uploaded_gene_pathway, uploaded_dna_pathway, uploaded_pdi, uploaded_ppi, pathway_files_are_significant, embedding_option, ], outputs=[ workspace_state, preparation_status, architecture_table, enrichment_table, ], ) train_button.click( train_workspace, inputs=[ workspace_state, epochs, batch_size, learning_rate, weight_decay, dropout, projection_dim, fusion_dim, validation_fraction, optimizer, class_weighting, ], outputs=[ model_state, training_status, loss_plot, confusion_plot, validation_table, model_download, importance_table, ], ) predict_button.click( run_prediction, inputs=[ model_state, prediction_artifact, prediction_gene, prediction_dna, ], outputs=[ model_state, prediction_status, prediction_table, prediction_plot, prediction_download, ], ) if __name__ == "__main__": demo.queue(default_concurrency_limit=1).launch(theme=THEME, css=CSS)