Spaces:
Sleeping
Sleeping
Commit Β·
121eaa8
1
Parent(s): 72c0d0b
Fix dataset loading and add intro cards
Browse files- app.py +304 -86
- biolmnet/data.py +8 -2
- biolmnet/ui/components.py +110 -0
- biolmnet/ui/styles.py +196 -1
- tests/test_core.py +62 -0
app.py
CHANGED
|
@@ -106,6 +106,17 @@ def _resolve_embedding_file(option: str, source_mode: str, example_dataset: str)
|
|
| 106 |
return "embedding_original_large_3.parquet"
|
| 107 |
|
| 108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
# ββ rail / chrome state machine (presentation only) βββββββββββββββββββββ
|
| 110 |
|
| 111 |
|
|
@@ -422,22 +433,43 @@ def prepare_workspace(
|
|
| 422 |
f"{len(label_names)} classes Β· {len(gene_branch.pathways) + len(dna_branch.pathways):,} "
|
| 423 |
f"branch-specific pathways Β· GenePT: {html.escape(embedding_file)}{warning_html}"
|
| 424 |
)
|
| 425 |
-
note =
|
| 426 |
-
|
| 427 |
-
+ (
|
| 428 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
)
|
| 430 |
run_meta = {
|
| 431 |
"source_name": source_name,
|
| 432 |
"embedding_file": embedding_file,
|
| 433 |
"unmatched_pathway_symbols": unmatched_symbols,
|
| 434 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
progress(1.0, desc="Ready to train")
|
| 436 |
-
return workspace, summary, architecture, enrichments.head(100), resolved_files, run_meta, note
|
| 437 |
except Exception as exc:
|
| 438 |
-
|
| 439 |
"Choose a source, then build the biological architecture."
|
| 440 |
-
)
|
|
|
|
|
|
|
| 441 |
|
| 442 |
|
| 443 |
def _reveal_symbols(run_meta: dict):
|
|
@@ -611,7 +643,8 @@ def train_workspace(
|
|
| 611 |
|
| 612 |
minutes, seconds = divmod(int(elapsed_seconds), 60)
|
| 613 |
metrics_html = ui.simple_status_html(
|
| 614 |
-
"<strong>Training complete.</strong>
|
|
|
|
| 615 |
"downloadable artifact includes architecture, preprocessing, weights and metrics."
|
| 616 |
)
|
| 617 |
loss_figure = _loss_plot(bundle.history)
|
|
@@ -731,19 +764,26 @@ def _resolve_predict_bundle(model_state, artifact_path, use_upload: bool):
|
|
| 731 |
return model_state
|
| 732 |
|
| 733 |
|
| 734 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
"""Presentation-only pre-flight check: mirrors the two structural checks
|
| 736 |
``validate_prediction_frames`` performs (row-count match, required
|
| 737 |
columns present) purely to render the alignment table live. The actual
|
| 738 |
gating decision on submit still goes through the real, untouched
|
| 739 |
``predict()`` call."""
|
| 740 |
-
align = {"ok": None, "required": None, "matched": None, "source_label": "Session model"}
|
| 741 |
try:
|
| 742 |
bundle = _resolve_predict_bundle(model_state, artifact_path, use_upload)
|
| 743 |
except Exception as exc:
|
| 744 |
rows = [[html.escape("Artifact"), "β", "β", f'<span class="error">{html.escape(str(exc))}</span>']]
|
| 745 |
table = ui.table_html(["Check", "Artifact", "Uploaded", "Result"], rows, aligns=["left", "left", "left", "right"])
|
| 746 |
-
return table, gr.update(visible=False), gr.update(interactive=False), align
|
| 747 |
|
| 748 |
if use_upload:
|
| 749 |
align["source_label"] = "Uploaded artifact" if artifact_path else "Upload artifact"
|
|
@@ -753,14 +793,21 @@ def refresh_alignment(model_state, artifact_path, use_upload, gene_path, dna_pat
|
|
| 753 |
[["Trained model available", "β", "β", '<span class="error">Fail</span>']],
|
| 754 |
aligns=["left", "left", "left", "right"],
|
| 755 |
)
|
| 756 |
-
return table, gr.update(visible=False), gr.update(interactive=False), align
|
| 757 |
|
| 758 |
align["required"] = len(bundle.gene_features) + len(bundle.dna_features)
|
| 759 |
|
| 760 |
rows = []
|
| 761 |
ok = True
|
| 762 |
gene_frame = dna_frame = None
|
| 763 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 764 |
try:
|
| 765 |
gene_frame = read_csv(gene_path)
|
| 766 |
dna_frame = read_csv(dna_path)
|
|
@@ -800,7 +847,7 @@ def refresh_alignment(model_state, artifact_path, use_upload, gene_path, dna_pat
|
|
| 800 |
return table, gr.update(visible=strip_visible, value=(
|
| 801 |
ui.strip_text_html("Blocking Β· features missing", f"{missing_total:,} required column(s) are missing from the uploaded files.")
|
| 802 |
if strip_visible else ""
|
| 803 |
-
)), gr.update(interactive=bool(ok)), align
|
| 804 |
|
| 805 |
|
| 806 |
def _pass_fail(ok: bool) -> str:
|
|
@@ -816,14 +863,28 @@ def _list_missing(align: dict):
|
|
| 816 |
return gr.update(visible=True, value=f'<div class="num" style="word-break:break-word">{html.escape(shown)}{more}</div>')
|
| 817 |
|
| 818 |
|
| 819 |
-
def run_prediction(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 820 |
predict_meta = dict(predict_meta or {})
|
| 821 |
try:
|
| 822 |
active_bundle = load_bundle(uploaded_artifact) if (use_upload and uploaded_artifact) else bundle
|
| 823 |
if active_bundle is None:
|
| 824 |
raise ValueError("Train a model in Phase 2 or upload a BioLM-NET model artifact.")
|
| 825 |
-
|
| 826 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 827 |
output = predict(gene_frame, dna_frame, active_bundle)
|
| 828 |
destination = Path(tempfile.mkdtemp(prefix="biolmnet-prediction-")) / "biolm-net-predictions.csv"
|
| 829 |
output.to_csv(destination, index=False)
|
|
@@ -923,6 +984,8 @@ with gr.Blocks(title="BioLM-NET Workbench", fill_width=True) as demo:
|
|
| 923 |
predict_meta_state = gr.State({})
|
| 924 |
active_page_state = gr.State("data")
|
| 925 |
session_id_state = gr.State("")
|
|
|
|
|
|
|
| 926 |
|
| 927 |
with gr.Row(elem_classes=["app-shell"]):
|
| 928 |
with gr.Column(elem_classes=["rail-col"]):
|
|
@@ -943,7 +1006,9 @@ with gr.Blocks(title="BioLM-NET Workbench", fill_width=True) as demo:
|
|
| 943 |
gr.HTML(ui.footnote_html("Research use only", "ZeroGPU on demand Β· py 3.12"))
|
| 944 |
|
| 945 |
with gr.Column(elem_classes=["workspace-col"]):
|
| 946 |
-
|
|
|
|
|
|
|
| 947 |
|
| 948 |
# ββ Data & Priors ββββββββββββββββββββββββββββββββββββββββ
|
| 949 |
with gr.Column(visible=True) as data_page:
|
|
@@ -1066,16 +1131,7 @@ with gr.Blocks(title="BioLM-NET Workbench", fill_width=True) as demo:
|
|
| 1066 |
)
|
| 1067 |
|
| 1068 |
gr.HTML('<div class="mono" style="margin:24px 0 9px">Graph preview</div>')
|
| 1069 |
-
graph_preview = gr.HTML(
|
| 1070 |
-
ui.blueprint_div(
|
| 1071 |
-
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px 10px">'
|
| 1072 |
-
+ ui.mini_stat_html("β", "Mask density")
|
| 1073 |
-
+ ui.mini_stat_html("β", "Gene nodes")
|
| 1074 |
-
+ ui.mini_stat_html("β", "Pathway units")
|
| 1075 |
-
+ ui.mini_stat_html("β", "GenePT dim")
|
| 1076 |
-
+ "</div>"
|
| 1077 |
-
)
|
| 1078 |
-
)
|
| 1079 |
|
| 1080 |
with gr.Row(visible=False):
|
| 1081 |
architecture_table = gr.Dataframe(interactive=False, wrap=False, value=pd.DataFrame(), headers=[""], column_count=(1, "dynamic"))
|
|
@@ -1230,6 +1286,10 @@ with gr.Blocks(title="BioLM-NET Workbench", fill_width=True) as demo:
|
|
| 1230 |
with gr.Row(elem_classes=["stage-grid"], equal_height=False):
|
| 1231 |
with gr.Column(scale=6, min_width=0):
|
| 1232 |
gr.HTML('<div class="mono sect">Inputs</div>')
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1233 |
with gr.Row(elem_classes=["srow-row", "first"], visible=True) as session_model_row:
|
| 1234 |
with gr.Column(min_width=100):
|
| 1235 |
gr.HTML(ui.esc("Trained artifact"))
|
|
@@ -1240,12 +1300,17 @@ with gr.Blocks(title="BioLM-NET Workbench", fill_width=True) as demo:
|
|
| 1240 |
gr.HTML(ui.esc("Trained artifact"))
|
| 1241 |
with gr.Column():
|
| 1242 |
prediction_artifact = gr.File(label="", show_label=False, file_types=[".zip"], type="filepath", elem_classes=["file-slot"])
|
| 1243 |
-
with gr.Row(elem_classes=["srow-row"]):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1244 |
with gr.Column(min_width=100):
|
| 1245 |
gr.HTML(ui.esc("Gene expression"))
|
| 1246 |
with gr.Column():
|
| 1247 |
prediction_gene = gr.File(label="", show_label=False, file_types=[".csv"], type="filepath", elem_classes=["file-slot"])
|
| 1248 |
-
with gr.Row(elem_classes=["srow-row", "last"]):
|
| 1249 |
with gr.Column(min_width=100):
|
| 1250 |
gr.HTML(ui.esc("DNA methylation"))
|
| 1251 |
with gr.Column():
|
|
@@ -1307,6 +1372,40 @@ with gr.Blocks(title="BioLM-NET Workbench", fill_width=True) as demo:
|
|
| 1307 |
"drawing biological or clinical conclusions.</div>"
|
| 1308 |
)
|
| 1309 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1310 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1311 |
# Wiring
|
| 1312 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -1326,18 +1425,76 @@ with gr.Blocks(title="BioLM-NET Workbench", fill_width=True) as demo:
|
|
| 1326 |
_nav(key), inputs=chrome_inputs_tail, outputs=[active_page_state, *page_columns, *chrome_outputs]
|
| 1327 |
)
|
| 1328 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1329 |
demo.load(
|
| 1330 |
-
|
| 1331 |
).then(
|
| 1332 |
lambda sid, *rest: _refresh_chrome("data", *rest, sid), inputs=[session_id_state, *chrome_inputs_tail[:-1]], outputs=chrome_outputs
|
| 1333 |
)
|
| 1334 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1335 |
source_mode.change(
|
| 1336 |
_source_visibility, inputs=[source_mode], outputs=[example_group, github_group, upload_group]
|
| 1337 |
)
|
| 1338 |
|
| 1339 |
-
|
| 1340 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1341 |
|
| 1342 |
prepare_inputs = [
|
| 1343 |
source_mode, example_dataset, github_folder, uploaded_gene, uploaded_dna, uploaded_labels,
|
|
@@ -1348,35 +1505,93 @@ with gr.Blocks(title="BioLM-NET Workbench", fill_width=True) as demo:
|
|
| 1348 |
def _after_prepare(workspace, run_meta, session_id):
|
| 1349 |
chrome = _refresh_chrome("data", workspace, None, run_meta, {}, {}, session_id)
|
| 1350 |
return (
|
| 1351 |
-
|
| 1352 |
-
|
| 1353 |
*chrome,
|
| 1354 |
)
|
| 1355 |
|
| 1356 |
-
|
| 1357 |
-
|
| 1358 |
-
|
| 1359 |
-
|
| 1360 |
-
|
| 1361 |
-
|
| 1362 |
-
|
| 1363 |
-
|
| 1364 |
-
|
| 1365 |
-
|
| 1366 |
-
|
| 1367 |
-
|
| 1368 |
-
lambda note: f'<div class="actbar-note">{note}</div>', inputs=[data_actionbar_note], outputs=[data_actionbar_note],
|
| 1369 |
-
show_progress="hidden",
|
| 1370 |
-
).then(
|
| 1371 |
-
_after_prepare, inputs=[workspace_state, run_meta_state, session_id_state],
|
| 1372 |
-
outputs=[prepare_button, revalidate_button, *chrome_outputs],
|
| 1373 |
-
show_progress="hidden",
|
| 1374 |
)
|
| 1375 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1376 |
show_symbols_btn.click(_reveal_symbols, inputs=[run_meta_state], outputs=[unmatched_reveal])
|
| 1377 |
pdi_override_btn.click(lambda: gr.update(visible=True), outputs=[uploaded_pdi])
|
| 1378 |
ppi_override_btn.click(lambda: gr.update(visible=True), outputs=[uploaded_ppi])
|
| 1379 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1380 |
# `train_workspace`'s positional signature intentionally mirrors
|
| 1381 |
# `estimate_training_duration` exactly for `@spaces.GPU` β session
|
| 1382 |
# metadata (dataset name, elapsed time, β¦) is threaded through
|
|
@@ -1389,7 +1604,7 @@ with gr.Blocks(title="BioLM-NET Workbench", fill_width=True) as demo:
|
|
| 1389 |
|
| 1390 |
def _after_train(bundle, run_meta, session_id):
|
| 1391 |
chrome = _refresh_chrome("train", bundle is not None and True, bundle, run_meta, {}, {}, session_id)
|
| 1392 |
-
return
|
| 1393 |
|
| 1394 |
validation_predictions_state = gr.State(pd.DataFrame())
|
| 1395 |
importance_state = gr.State(pd.DataFrame())
|
|
@@ -1398,7 +1613,7 @@ with gr.Blocks(title="BioLM-NET Workbench", fill_width=True) as demo:
|
|
| 1398 |
run_meta_update_state = gr.State({})
|
| 1399 |
|
| 1400 |
train_button.click(
|
| 1401 |
-
lambda: _busy("Training on ZeroGPUβ¦"), outputs=[train_button],
|
| 1402 |
show_progress="hidden",
|
| 1403 |
).then(
|
| 1404 |
train_workspace, inputs=train_inputs,
|
|
@@ -1406,7 +1621,7 @@ with gr.Blocks(title="BioLM-NET Workbench", fill_width=True) as demo:
|
|
| 1406 |
model_state, training_status, loss_plot, validation_predictions_state, artifact_path_state,
|
| 1407 |
importance_state, architecture_audit_state, run_meta_update_state, confusion_html_state,
|
| 1408 |
],
|
| 1409 |
-
show_progress="
|
| 1410 |
).then(
|
| 1411 |
lambda old, new: {**(old or {}), **(new or {})},
|
| 1412 |
inputs=[run_meta_state, run_meta_update_state], outputs=[run_meta_state],
|
|
@@ -1415,6 +1630,10 @@ with gr.Blocks(title="BioLM-NET Workbench", fill_width=True) as demo:
|
|
| 1415 |
_after_train, inputs=[model_state, run_meta_state, session_id_state],
|
| 1416 |
outputs=[train_button, *chrome_outputs],
|
| 1417 |
show_progress="hidden",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1418 |
).then(
|
| 1419 |
lambda bundle, artifact_path, run_meta: _export_panel(bundle, artifact_path, run_meta),
|
| 1420 |
inputs=[model_state, artifact_path_state, run_meta_state],
|
|
@@ -1441,30 +1660,22 @@ with gr.Blocks(title="BioLM-NET Workbench", fill_width=True) as demo:
|
|
| 1441 |
).then(
|
| 1442 |
lambda bundle: gr.update(visible=bundle is not None), inputs=[model_state], outputs=[results_tables_bottom],
|
| 1443 |
show_progress="hidden",
|
| 1444 |
-
).then(
|
| 1445 |
-
lambda bundle, run_meta: gr.update(value=_status(
|
| 1446 |
-
f"Training complete Β· validation accuracy {bundle.metrics['accuracy']:.3f} Β· elapsed {run_meta.get('elapsed','β')}"
|
| 1447 |
-
)) if bundle else gr.update(),
|
| 1448 |
-
inputs=[model_state, run_meta_state], outputs=[training_status],
|
| 1449 |
-
show_progress="hidden",
|
| 1450 |
)
|
| 1451 |
-
|
| 1452 |
-
|
| 1453 |
-
|
| 1454 |
-
|
| 1455 |
-
|
| 1456 |
-
|
| 1457 |
-
|
| 1458 |
-
|
| 1459 |
-
|
| 1460 |
-
|
| 1461 |
-
|
| 1462 |
-
|
| 1463 |
-
|
| 1464 |
-
|
| 1465 |
-
|
| 1466 |
-
|
| 1467 |
-
for control in (predict_source_mode, prediction_artifact, prediction_gene, prediction_dna, model_state):
|
| 1468 |
control.change(
|
| 1469 |
_align_wrapper, inputs=align_inputs,
|
| 1470 |
outputs=[alignment_table, alignment_strip, predict_button, align_state],
|
|
@@ -1472,22 +1683,29 @@ with gr.Blocks(title="BioLM-NET Workbench", fill_width=True) as demo:
|
|
| 1472 |
|
| 1473 |
list_missing_btn.click(_list_missing, inputs=[align_state], outputs=[missing_reveal])
|
| 1474 |
|
| 1475 |
-
predict_inputs = [
|
|
|
|
|
|
|
|
|
|
| 1476 |
|
| 1477 |
-
def _predict_wrapper(bundle, artifact_path,
|
| 1478 |
-
return run_prediction(
|
|
|
|
|
|
|
|
|
|
| 1479 |
|
| 1480 |
predict_button.click(
|
| 1481 |
-
lambda: _busy("Running inferenceβ¦"), outputs=[predict_button],
|
| 1482 |
show_progress="hidden",
|
| 1483 |
).then(
|
| 1484 |
_predict_wrapper, inputs=predict_inputs,
|
| 1485 |
outputs=[model_state, prediction_status, prediction_table, distribution_panel, prediction_download, predict_meta_state],
|
| 1486 |
-
show_progress="minimal",
|
| 1487 |
-
).then(
|
| 1488 |
-
lambda: gr.update(value="Run inference", interactive=True), outputs=[predict_button],
|
| 1489 |
show_progress="hidden",
|
| 1490 |
).then(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1491 |
_align_wrapper, inputs=align_inputs, outputs=[alignment_table, alignment_strip, predict_button, align_state],
|
| 1492 |
show_progress="hidden",
|
| 1493 |
).then(
|
|
|
|
| 106 |
return "embedding_original_large_3.parquet"
|
| 107 |
|
| 108 |
|
| 109 |
+
def _graph_preview_html(mask_density: str, gene_nodes: str, pathway_units: str, genept_dim: str) -> str:
|
| 110 |
+
return ui.blueprint_div(
|
| 111 |
+
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px 10px">'
|
| 112 |
+
+ ui.mini_stat_html(mask_density, "Mask density")
|
| 113 |
+
+ ui.mini_stat_html(gene_nodes, "Gene nodes")
|
| 114 |
+
+ ui.mini_stat_html(pathway_units, "Pathway units")
|
| 115 |
+
+ ui.mini_stat_html(genept_dim, "GenePT dim")
|
| 116 |
+
+ "</div>"
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
# ββ rail / chrome state machine (presentation only) βββββββββββββββββββββ
|
| 121 |
|
| 122 |
|
|
|
|
| 433 |
f"{len(label_names)} classes Β· {len(gene_branch.pathways) + len(dna_branch.pathways):,} "
|
| 434 |
f"branch-specific pathways Β· GenePT: {html.escape(embedding_file)}{warning_html}"
|
| 435 |
)
|
| 436 |
+
note = (
|
| 437 |
+
'<div class="actbar-note">'
|
| 438 |
+
+ ui.esc(
|
| 439 |
+
f"{len(resolved_rows)} of {len(resolved_rows)} inputs resolved"
|
| 440 |
+
+ (f" Β· {len(warnings)} warning(s)" if warnings else "")
|
| 441 |
+
+ f" Β· architecture built {datetime.now().strftime('%H:%M:%S')}"
|
| 442 |
+
)
|
| 443 |
+
+ "</div>"
|
| 444 |
)
|
| 445 |
run_meta = {
|
| 446 |
"source_name": source_name,
|
| 447 |
"embedding_file": embedding_file,
|
| 448 |
"unmatched_pathway_symbols": unmatched_symbols,
|
| 449 |
}
|
| 450 |
+
mean_density = float(
|
| 451 |
+
np.mean(
|
| 452 |
+
[
|
| 453 |
+
gene_branch.biological_mask.astype(bool).mean(),
|
| 454 |
+
dna_branch.biological_mask.astype(bool).mean(),
|
| 455 |
+
]
|
| 456 |
+
)
|
| 457 |
+
)
|
| 458 |
+
genept_dim = gene_branch.embeddings.shape[1] if gene_branch.embeddings is not None else 0
|
| 459 |
+
graph_preview_html = _graph_preview_html(
|
| 460 |
+
f"{mean_density * 100:.1f}%",
|
| 461 |
+
f"{len(gene_branch.hidden_genes) + len(dna_branch.hidden_genes):,}",
|
| 462 |
+
f"{len(gene_branch.pathways) + len(dna_branch.pathways):,}",
|
| 463 |
+
f"{genept_dim:,}",
|
| 464 |
+
)
|
| 465 |
progress(1.0, desc="Ready to train")
|
| 466 |
+
return workspace, summary, architecture, enrichments.head(100), resolved_files, run_meta, note, graph_preview_html
|
| 467 |
except Exception as exc:
|
| 468 |
+
fallback_note = '<div class="actbar-note">' + ui.esc(
|
| 469 |
"Choose a source, then build the biological architecture."
|
| 470 |
+
) + "</div>"
|
| 471 |
+
empty_graph_preview = _graph_preview_html("β", "β", "β", "β")
|
| 472 |
+
return None, _status(str(exc), error=True), empty, empty, empty, {}, fallback_note, empty_graph_preview
|
| 473 |
|
| 474 |
|
| 475 |
def _reveal_symbols(run_meta: dict):
|
|
|
|
| 643 |
|
| 644 |
minutes, seconds = divmod(int(elapsed_seconds), 60)
|
| 645 |
metrics_html = ui.simple_status_html(
|
| 646 |
+
f"<strong>Training complete.</strong> Validation accuracy {metrics['accuracy']:.3f} Β· "
|
| 647 |
+
f"elapsed {minutes:02d}:{seconds:02d}. Best validation checkpoint restored; the "
|
| 648 |
"downloadable artifact includes architecture, preprocessing, weights and metrics."
|
| 649 |
)
|
| 650 |
loss_figure = _loss_plot(bundle.history)
|
|
|
|
| 764 |
return model_state
|
| 765 |
|
| 766 |
|
| 767 |
+
def _workspace_prediction_frames(workspace: PreparedWorkspace) -> tuple[pd.DataFrame, pd.DataFrame]:
|
| 768 |
+
return (
|
| 769 |
+
pd.DataFrame(workspace.gene_expression, columns=workspace.gene_branch.input_genes),
|
| 770 |
+
pd.DataFrame(workspace.dna_methylation, columns=workspace.dna_branch.input_genes),
|
| 771 |
+
)
|
| 772 |
+
|
| 773 |
+
|
| 774 |
+
def refresh_alignment(model_state, workspace, artifact_path, use_upload, use_prepared, gene_path, dna_path):
|
| 775 |
"""Presentation-only pre-flight check: mirrors the two structural checks
|
| 776 |
``validate_prediction_frames`` performs (row-count match, required
|
| 777 |
columns present) purely to render the alignment table live. The actual
|
| 778 |
gating decision on submit still goes through the real, untouched
|
| 779 |
``predict()`` call."""
|
| 780 |
+
align = {"ok": None, "required": None, "matched": None, "source_label": "Prepared dataset" if use_prepared else "Session model"}
|
| 781 |
try:
|
| 782 |
bundle = _resolve_predict_bundle(model_state, artifact_path, use_upload)
|
| 783 |
except Exception as exc:
|
| 784 |
rows = [[html.escape("Artifact"), "β", "β", f'<span class="error">{html.escape(str(exc))}</span>']]
|
| 785 |
table = ui.table_html(["Check", "Artifact", "Uploaded", "Result"], rows, aligns=["left", "left", "left", "right"])
|
| 786 |
+
return table, gr.update(visible=False), gr.update(value="Run inference", interactive=False, elem_classes=["actbar-primary-btn"]), align
|
| 787 |
|
| 788 |
if use_upload:
|
| 789 |
align["source_label"] = "Uploaded artifact" if artifact_path else "Upload artifact"
|
|
|
|
| 793 |
[["Trained model available", "β", "β", '<span class="error">Fail</span>']],
|
| 794 |
aligns=["left", "left", "left", "right"],
|
| 795 |
)
|
| 796 |
+
return table, gr.update(visible=False), gr.update(value="Run inference", interactive=False, elem_classes=["actbar-primary-btn"]), align
|
| 797 |
|
| 798 |
align["required"] = len(bundle.gene_features) + len(bundle.dna_features)
|
| 799 |
|
| 800 |
rows = []
|
| 801 |
ok = True
|
| 802 |
gene_frame = dna_frame = None
|
| 803 |
+
if use_prepared:
|
| 804 |
+
if workspace is None:
|
| 805 |
+
rows.append(["Prepared dataset available", "β", "β", '<span class="error">Fail</span>'])
|
| 806 |
+
ok = False
|
| 807 |
+
else:
|
| 808 |
+
gene_frame, dna_frame = _workspace_prediction_frames(workspace)
|
| 809 |
+
rows.append(["Prepared dataset available", "β", f"{len(gene_frame):,} samples", _pass_fail(True)])
|
| 810 |
+
elif gene_path and dna_path:
|
| 811 |
try:
|
| 812 |
gene_frame = read_csv(gene_path)
|
| 813 |
dna_frame = read_csv(dna_path)
|
|
|
|
| 847 |
return table, gr.update(visible=strip_visible, value=(
|
| 848 |
ui.strip_text_html("Blocking Β· features missing", f"{missing_total:,} required column(s) are missing from the uploaded files.")
|
| 849 |
if strip_visible else ""
|
| 850 |
+
)), gr.update(value="Run inference", interactive=bool(ok), elem_classes=["actbar-primary-btn"]), align
|
| 851 |
|
| 852 |
|
| 853 |
def _pass_fail(ok: bool) -> str:
|
|
|
|
| 863 |
return gr.update(visible=True, value=f'<div class="num" style="word-break:break-word">{html.escape(shown)}{more}</div>')
|
| 864 |
|
| 865 |
|
| 866 |
+
def run_prediction(
|
| 867 |
+
bundle: ModelBundle | None,
|
| 868 |
+
workspace: PreparedWorkspace | None,
|
| 869 |
+
uploaded_artifact: str | None,
|
| 870 |
+
use_upload: bool,
|
| 871 |
+
use_prepared: bool,
|
| 872 |
+
gene_file: str | None,
|
| 873 |
+
dna_file: str | None,
|
| 874 |
+
predict_meta: dict,
|
| 875 |
+
):
|
| 876 |
predict_meta = dict(predict_meta or {})
|
| 877 |
try:
|
| 878 |
active_bundle = load_bundle(uploaded_artifact) if (use_upload and uploaded_artifact) else bundle
|
| 879 |
if active_bundle is None:
|
| 880 |
raise ValueError("Train a model in Phase 2 or upload a BioLM-NET model artifact.")
|
| 881 |
+
if use_prepared:
|
| 882 |
+
if workspace is None:
|
| 883 |
+
raise ValueError("Prepare data and priors before scoring the prepared dataset.")
|
| 884 |
+
gene_frame, dna_frame = _workspace_prediction_frames(workspace)
|
| 885 |
+
else:
|
| 886 |
+
gene_frame = _read_required_upload(gene_file, "a prediction gene-expression CSV")
|
| 887 |
+
dna_frame = _read_required_upload(dna_file, "a prediction DNA-methylation CSV")
|
| 888 |
output = predict(gene_frame, dna_frame, active_bundle)
|
| 889 |
destination = Path(tempfile.mkdtemp(prefix="biolmnet-prediction-")) / "biolm-net-predictions.csv"
|
| 890 |
output.to_csv(destination, index=False)
|
|
|
|
| 984 |
predict_meta_state = gr.State({})
|
| 985 |
active_page_state = gr.State("data")
|
| 986 |
session_id_state = gr.State("")
|
| 987 |
+
intro_step_state = gr.State(0)
|
| 988 |
+
intro_dismissed_state = gr.BrowserState(False, storage_key="biolmnet_intro_dismissed")
|
| 989 |
|
| 990 |
with gr.Row(elem_classes=["app-shell"]):
|
| 991 |
with gr.Column(elem_classes=["rail-col"]):
|
|
|
|
| 1006 |
gr.HTML(ui.footnote_html("Research use only", "ZeroGPU on demand Β· py 3.12"))
|
| 1007 |
|
| 1008 |
with gr.Column(elem_classes=["workspace-col"]):
|
| 1009 |
+
with gr.Row(elem_classes=["topbar-host"]):
|
| 1010 |
+
topbar_html_component = gr.HTML(ui.topbar_html(1, STAGE_TOTAL, STAGE_LABELS[0], "ββββββ", "ZeroGPU idle"))
|
| 1011 |
+
intro_open_button = gr.Button("Introduction", size="sm", variant="secondary", elem_classes=["btn-ghost", "intro-open-btn"])
|
| 1012 |
|
| 1013 |
# ββ Data & Priors ββββββββββββββββββββββββββββββββββββββββ
|
| 1014 |
with gr.Column(visible=True) as data_page:
|
|
|
|
| 1131 |
)
|
| 1132 |
|
| 1133 |
gr.HTML('<div class="mono" style="margin:24px 0 9px">Graph preview</div>')
|
| 1134 |
+
graph_preview = gr.HTML(_graph_preview_html("β", "β", "β", "β"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1135 |
|
| 1136 |
with gr.Row(visible=False):
|
| 1137 |
architecture_table = gr.Dataframe(interactive=False, wrap=False, value=pd.DataFrame(), headers=[""], column_count=(1, "dynamic"))
|
|
|
|
| 1286 |
with gr.Row(elem_classes=["stage-grid"], equal_height=False):
|
| 1287 |
with gr.Column(scale=6, min_width=0):
|
| 1288 |
gr.HTML('<div class="mono sect">Inputs</div>')
|
| 1289 |
+
prediction_input_mode = gr.Radio(
|
| 1290 |
+
["Prepared dataset", "Upload files"], value="Prepared dataset", show_label=False,
|
| 1291 |
+
container=False, elem_classes=["seg-radio"],
|
| 1292 |
+
)
|
| 1293 |
with gr.Row(elem_classes=["srow-row", "first"], visible=True) as session_model_row:
|
| 1294 |
with gr.Column(min_width=100):
|
| 1295 |
gr.HTML(ui.esc("Trained artifact"))
|
|
|
|
| 1300 |
gr.HTML(ui.esc("Trained artifact"))
|
| 1301 |
with gr.Column():
|
| 1302 |
prediction_artifact = gr.File(label="", show_label=False, file_types=[".zip"], type="filepath", elem_classes=["file-slot"])
|
| 1303 |
+
with gr.Row(elem_classes=["srow-row"]) as prepared_dataset_row:
|
| 1304 |
+
with gr.Column(min_width=100):
|
| 1305 |
+
gr.HTML(ui.esc("Prediction cohort"))
|
| 1306 |
+
with gr.Column():
|
| 1307 |
+
gr.HTML('<div class="num" style="opacity:.65">Use the dataset prepared in Data & Priors.</div>')
|
| 1308 |
+
with gr.Row(elem_classes=["srow-row"], visible=False) as prediction_gene_row:
|
| 1309 |
with gr.Column(min_width=100):
|
| 1310 |
gr.HTML(ui.esc("Gene expression"))
|
| 1311 |
with gr.Column():
|
| 1312 |
prediction_gene = gr.File(label="", show_label=False, file_types=[".csv"], type="filepath", elem_classes=["file-slot"])
|
| 1313 |
+
with gr.Row(elem_classes=["srow-row", "last"], visible=False) as prediction_dna_row:
|
| 1314 |
with gr.Column(min_width=100):
|
| 1315 |
gr.HTML(ui.esc("DNA methylation"))
|
| 1316 |
with gr.Column():
|
|
|
|
| 1372 |
"drawing biological or clinical conclusions.</div>"
|
| 1373 |
)
|
| 1374 |
|
| 1375 |
+
with gr.Group(elem_id="intro", visible=True) as intro_group:
|
| 1376 |
+
intro_cards = []
|
| 1377 |
+
intro_next_buttons = []
|
| 1378 |
+
intro_back_buttons = []
|
| 1379 |
+
intro_skip_buttons = []
|
| 1380 |
+
intro_start_buttons = []
|
| 1381 |
+
with gr.Column(elem_classes=["intro-card"]):
|
| 1382 |
+
for index, (_, _, _, _, note) in enumerate(ui.INTRO_CARDS):
|
| 1383 |
+
with gr.Column(visible=index == 0, elem_classes=["intro-card-page"]) as intro_card:
|
| 1384 |
+
gr.HTML(ui.intro_card_html(index))
|
| 1385 |
+
with gr.Row(elem_classes=["intro-card-foot"]):
|
| 1386 |
+
if index == 0:
|
| 1387 |
+
intro_dont_show = gr.Checkbox(value=False, label="Don't show again", container=False)
|
| 1388 |
+
else:
|
| 1389 |
+
gr.HTML(f'<span class="mono" style="font-size:9.5px">{ui.esc(note)}</span>')
|
| 1390 |
+
with gr.Row(elem_classes=["intro-card-actions"]):
|
| 1391 |
+
gr.HTML(ui.intro_dots_html(index))
|
| 1392 |
+
if index == 0:
|
| 1393 |
+
skip_btn = gr.Button("Skip", size="sm", variant="secondary", elem_classes=["btn-ghost"])
|
| 1394 |
+
next_btn = gr.Button("Next", size="sm", variant="primary")
|
| 1395 |
+
intro_skip_buttons.append(skip_btn)
|
| 1396 |
+
intro_next_buttons.append(next_btn)
|
| 1397 |
+
elif index < len(ui.INTRO_CARDS) - 1:
|
| 1398 |
+
back_btn = gr.Button("Back", size="sm", variant="secondary", elem_classes=["btn-ghost"])
|
| 1399 |
+
next_btn = gr.Button("Next", size="sm", variant="primary")
|
| 1400 |
+
intro_back_buttons.append(back_btn)
|
| 1401 |
+
intro_next_buttons.append(next_btn)
|
| 1402 |
+
else:
|
| 1403 |
+
back_btn = gr.Button("Back", size="sm", variant="secondary", elem_classes=["btn-ghost"])
|
| 1404 |
+
start_btn = gr.Button("Start with the BRCA example", size="sm", variant="primary")
|
| 1405 |
+
intro_back_buttons.append(back_btn)
|
| 1406 |
+
intro_start_buttons.append(start_btn)
|
| 1407 |
+
intro_cards.append(intro_card)
|
| 1408 |
+
|
| 1409 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1410 |
# Wiring
|
| 1411 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 1425 |
_nav(key), inputs=chrome_inputs_tail, outputs=[active_page_state, *page_columns, *chrome_outputs]
|
| 1426 |
)
|
| 1427 |
|
| 1428 |
+
def _intro_updates(index: int, visible: bool = True):
|
| 1429 |
+
index = max(0, min(len(ui.INTRO_CARDS) - 1, int(index)))
|
| 1430 |
+
cards = [gr.update(visible=i == index) for i in range(len(ui.INTRO_CARDS))]
|
| 1431 |
+
return index, gr.update(visible=visible), *cards
|
| 1432 |
+
|
| 1433 |
+
def _intro_load(dismissed):
|
| 1434 |
+
return secrets.token_hex(3), *_intro_updates(0, visible=not bool(dismissed))
|
| 1435 |
+
|
| 1436 |
+
def _intro_close(dont_show):
|
| 1437 |
+
return 0, gr.update(visible=False), bool(dont_show)
|
| 1438 |
+
|
| 1439 |
+
def _intro_start_brca(dont_show):
|
| 1440 |
+
return (
|
| 1441 |
+
0,
|
| 1442 |
+
gr.update(visible=False),
|
| 1443 |
+
bool(dont_show),
|
| 1444 |
+
"BioLM-NET examples",
|
| 1445 |
+
"BRCA",
|
| 1446 |
+
*_source_visibility("BioLM-NET examples"),
|
| 1447 |
+
)
|
| 1448 |
+
|
| 1449 |
+
intro_outputs = [intro_step_state, intro_group, *intro_cards]
|
| 1450 |
+
|
| 1451 |
demo.load(
|
| 1452 |
+
_intro_load, inputs=[intro_dismissed_state], outputs=[session_id_state, *intro_outputs]
|
| 1453 |
).then(
|
| 1454 |
lambda sid, *rest: _refresh_chrome("data", *rest, sid), inputs=[session_id_state, *chrome_inputs_tail[:-1]], outputs=chrome_outputs
|
| 1455 |
)
|
| 1456 |
|
| 1457 |
+
intro_open_button.click(
|
| 1458 |
+
lambda: _intro_updates(0, True), outputs=intro_outputs, show_progress="hidden"
|
| 1459 |
+
)
|
| 1460 |
+
for index, button in enumerate(intro_next_buttons):
|
| 1461 |
+
button.click(
|
| 1462 |
+
lambda i=index: _intro_updates(i + 1, True), outputs=intro_outputs, show_progress="hidden"
|
| 1463 |
+
)
|
| 1464 |
+
for index, button in enumerate(intro_back_buttons, start=1):
|
| 1465 |
+
button.click(
|
| 1466 |
+
lambda i=index: _intro_updates(i - 1, True), outputs=intro_outputs, show_progress="hidden"
|
| 1467 |
+
)
|
| 1468 |
+
for button in intro_skip_buttons:
|
| 1469 |
+
button.click(
|
| 1470 |
+
_intro_close, inputs=[intro_dont_show],
|
| 1471 |
+
outputs=[intro_step_state, intro_group, intro_dismissed_state],
|
| 1472 |
+
show_progress="hidden",
|
| 1473 |
+
)
|
| 1474 |
+
for button in intro_start_buttons:
|
| 1475 |
+
button.click(
|
| 1476 |
+
_intro_start_brca, inputs=[intro_dont_show],
|
| 1477 |
+
outputs=[
|
| 1478 |
+
intro_step_state, intro_group, intro_dismissed_state,
|
| 1479 |
+
source_mode, example_dataset, example_group, github_group, upload_group,
|
| 1480 |
+
],
|
| 1481 |
+
show_progress="hidden",
|
| 1482 |
+
)
|
| 1483 |
+
|
| 1484 |
source_mode.change(
|
| 1485 |
_source_visibility, inputs=[source_mode], outputs=[example_group, github_group, upload_group]
|
| 1486 |
)
|
| 1487 |
|
| 1488 |
+
# The one deliberate loading indicator (a thin animated bar on the busy
|
| 1489 |
+
# button itself β see `.is-busy` in styles.py) is toggled only through
|
| 1490 |
+
# these two helpers, explicitly, on exactly the button that was clicked.
|
| 1491 |
+
# It is never left to Gradio's own per-component pending state, which is
|
| 1492 |
+
# what caused several of these to appear at once for one click.
|
| 1493 |
+
def _busy(label, base_classes):
|
| 1494 |
+
return gr.update(value=label, interactive=False, elem_classes=[*base_classes, "is-busy"])
|
| 1495 |
+
|
| 1496 |
+
def _idle(label, base_classes, interactive=True):
|
| 1497 |
+
return gr.update(value=label, interactive=interactive, elem_classes=list(base_classes))
|
| 1498 |
|
| 1499 |
prepare_inputs = [
|
| 1500 |
source_mode, example_dataset, github_folder, uploaded_gene, uploaded_dna, uploaded_labels,
|
|
|
|
| 1505 |
def _after_prepare(workspace, run_meta, session_id):
|
| 1506 |
chrome = _refresh_chrome("data", workspace, None, run_meta, {}, {}, session_id)
|
| 1507 |
return (
|
| 1508 |
+
_idle("Build biological architecture", ["actbar-primary-btn"]),
|
| 1509 |
+
_idle("Re-validate inputs", ["actbar-secondary-btn"]),
|
| 1510 |
*chrome,
|
| 1511 |
)
|
| 1512 |
|
| 1513 |
+
# Only the button actually clicked gets the sweep; its sibling just goes
|
| 1514 |
+
# inert (disabled, unchanged label) β two buttons both sweeping for one
|
| 1515 |
+
# action would itself be the "more than one indicator" problem.
|
| 1516 |
+
def _busy_pair(which: str):
|
| 1517 |
+
if which == "primary":
|
| 1518 |
+
return (
|
| 1519 |
+
_busy("Buildingβ¦", ["actbar-primary-btn"]),
|
| 1520 |
+
gr.update(interactive=False),
|
| 1521 |
+
)
|
| 1522 |
+
return (
|
| 1523 |
+
gr.update(interactive=False),
|
| 1524 |
+
_busy("Buildingβ¦", ["actbar-secondary-btn"]),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1525 |
)
|
| 1526 |
|
| 1527 |
+
prepare_button.click(
|
| 1528 |
+
lambda: _busy_pair("primary"), outputs=[prepare_button, revalidate_button],
|
| 1529 |
+
show_progress="hidden",
|
| 1530 |
+
).then(
|
| 1531 |
+
prepare_workspace, inputs=prepare_inputs,
|
| 1532 |
+
outputs=[
|
| 1533 |
+
workspace_state, preparation_status, architecture_table, enrichment_table,
|
| 1534 |
+
resolved_files_table, run_meta_state, data_actionbar_note, graph_preview,
|
| 1535 |
+
],
|
| 1536 |
+
show_progress="hidden",
|
| 1537 |
+
).then(
|
| 1538 |
+
_after_prepare, inputs=[workspace_state, run_meta_state, session_id_state],
|
| 1539 |
+
outputs=[prepare_button, revalidate_button, *chrome_outputs],
|
| 1540 |
+
show_progress="hidden",
|
| 1541 |
+
)
|
| 1542 |
+
|
| 1543 |
+
revalidate_button.click(
|
| 1544 |
+
lambda: _busy_pair("secondary"), outputs=[prepare_button, revalidate_button],
|
| 1545 |
+
show_progress="hidden",
|
| 1546 |
+
).then(
|
| 1547 |
+
prepare_workspace, inputs=prepare_inputs,
|
| 1548 |
+
outputs=[
|
| 1549 |
+
workspace_state, preparation_status, architecture_table, enrichment_table,
|
| 1550 |
+
resolved_files_table, run_meta_state, data_actionbar_note, graph_preview,
|
| 1551 |
+
],
|
| 1552 |
+
show_progress="hidden",
|
| 1553 |
+
).then(
|
| 1554 |
+
_after_prepare, inputs=[workspace_state, run_meta_state, session_id_state],
|
| 1555 |
+
outputs=[prepare_button, revalidate_button, *chrome_outputs],
|
| 1556 |
+
show_progress="hidden",
|
| 1557 |
+
)
|
| 1558 |
+
|
| 1559 |
show_symbols_btn.click(_reveal_symbols, inputs=[run_meta_state], outputs=[unmatched_reveal])
|
| 1560 |
pdi_override_btn.click(lambda: gr.update(visible=True), outputs=[uploaded_pdi])
|
| 1561 |
ppi_override_btn.click(lambda: gr.update(visible=True), outputs=[uploaded_ppi])
|
| 1562 |
|
| 1563 |
+
def _predict_source_toggle(mode):
|
| 1564 |
+
return gr.update(visible=mode == "Session model"), gr.update(visible=mode == "Upload artifact")
|
| 1565 |
+
|
| 1566 |
+
predict_source_mode.change(
|
| 1567 |
+
_predict_source_toggle, inputs=[predict_source_mode], outputs=[session_model_row, artifact_upload_row]
|
| 1568 |
+
)
|
| 1569 |
+
|
| 1570 |
+
def _predict_input_toggle(mode):
|
| 1571 |
+
use_uploads = mode == "Upload files"
|
| 1572 |
+
return (
|
| 1573 |
+
gr.update(visible=not use_uploads),
|
| 1574 |
+
gr.update(visible=use_uploads),
|
| 1575 |
+
gr.update(visible=use_uploads),
|
| 1576 |
+
)
|
| 1577 |
+
|
| 1578 |
+
prediction_input_mode.change(
|
| 1579 |
+
_predict_input_toggle,
|
| 1580 |
+
inputs=[prediction_input_mode],
|
| 1581 |
+
outputs=[prepared_dataset_row, prediction_gene_row, prediction_dna_row],
|
| 1582 |
+
)
|
| 1583 |
+
|
| 1584 |
+
align_inputs = [
|
| 1585 |
+
model_state, workspace_state, prediction_artifact, predict_source_mode,
|
| 1586 |
+
prediction_input_mode, prediction_gene, prediction_dna,
|
| 1587 |
+
]
|
| 1588 |
+
|
| 1589 |
+
def _align_wrapper(bundle, workspace, artifact_path, model_mode, input_mode, gene_path, dna_path):
|
| 1590 |
+
return refresh_alignment(
|
| 1591 |
+
bundle, workspace, artifact_path, model_mode == "Upload artifact",
|
| 1592 |
+
input_mode == "Prepared dataset", gene_path, dna_path,
|
| 1593 |
+
)
|
| 1594 |
+
|
| 1595 |
# `train_workspace`'s positional signature intentionally mirrors
|
| 1596 |
# `estimate_training_duration` exactly for `@spaces.GPU` β session
|
| 1597 |
# metadata (dataset name, elapsed time, β¦) is threaded through
|
|
|
|
| 1604 |
|
| 1605 |
def _after_train(bundle, run_meta, session_id):
|
| 1606 |
chrome = _refresh_chrome("train", bundle is not None and True, bundle, run_meta, {}, {}, session_id)
|
| 1607 |
+
return _idle("Train BioLM-NET on ZeroGPU", ["actbar-primary-btn"]), *chrome
|
| 1608 |
|
| 1609 |
validation_predictions_state = gr.State(pd.DataFrame())
|
| 1610 |
importance_state = gr.State(pd.DataFrame())
|
|
|
|
| 1613 |
run_meta_update_state = gr.State({})
|
| 1614 |
|
| 1615 |
train_button.click(
|
| 1616 |
+
lambda: _busy("Training on ZeroGPUβ¦", ["actbar-primary-btn"]), outputs=[train_button],
|
| 1617 |
show_progress="hidden",
|
| 1618 |
).then(
|
| 1619 |
train_workspace, inputs=train_inputs,
|
|
|
|
| 1621 |
model_state, training_status, loss_plot, validation_predictions_state, artifact_path_state,
|
| 1622 |
importance_state, architecture_audit_state, run_meta_update_state, confusion_html_state,
|
| 1623 |
],
|
| 1624 |
+
show_progress="hidden",
|
| 1625 |
).then(
|
| 1626 |
lambda old, new: {**(old or {}), **(new or {})},
|
| 1627 |
inputs=[run_meta_state, run_meta_update_state], outputs=[run_meta_state],
|
|
|
|
| 1630 |
_after_train, inputs=[model_state, run_meta_state, session_id_state],
|
| 1631 |
outputs=[train_button, *chrome_outputs],
|
| 1632 |
show_progress="hidden",
|
| 1633 |
+
).then(
|
| 1634 |
+
_align_wrapper, inputs=align_inputs,
|
| 1635 |
+
outputs=[alignment_table, alignment_strip, predict_button, align_state],
|
| 1636 |
+
show_progress="hidden",
|
| 1637 |
).then(
|
| 1638 |
lambda bundle, artifact_path, run_meta: _export_panel(bundle, artifact_path, run_meta),
|
| 1639 |
inputs=[model_state, artifact_path_state, run_meta_state],
|
|
|
|
| 1660 |
).then(
|
| 1661 |
lambda bundle: gr.update(visible=bundle is not None), inputs=[model_state], outputs=[results_tables_bottom],
|
| 1662 |
show_progress="hidden",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1663 |
)
|
| 1664 |
+
# training_status is NOT re-set here: train_workspace's own return
|
| 1665 |
+
# already carries the final "Training complete Β· accuracy Β· elapsed"
|
| 1666 |
+
# message (it has the elapsed time on hand already), so writing it
|
| 1667 |
+
# again from run_meta_state afterwards was a second, redundant paint
|
| 1668 |
+
# of the same component for one click.
|
| 1669 |
+
|
| 1670 |
+
# Deliberately NOT including `model_state` here: it only ever changes as
|
| 1671 |
+
# part of the train/predict chains below, and both of those already call
|
| 1672 |
+
# `_align_wrapper` explicitly as their own finalize step. Adding it here
|
| 1673 |
+
# too would fire alignment twice per click (train_workspace/_predict_
|
| 1674 |
+
# wrapper set model_state -> this listener fires -> the chain's own
|
| 1675 |
+
# explicit call also fires) β the exact "loading bar twice" symptom.
|
| 1676 |
+
# `workspace_state` stays: nothing else re-checks alignment when the
|
| 1677 |
+
# user rebuilds the architecture while already on the Predict page.
|
| 1678 |
+
for control in (predict_source_mode, prediction_input_mode, prediction_artifact, prediction_gene, prediction_dna, workspace_state):
|
|
|
|
|
|
|
| 1679 |
control.change(
|
| 1680 |
_align_wrapper, inputs=align_inputs,
|
| 1681 |
outputs=[alignment_table, alignment_strip, predict_button, align_state],
|
|
|
|
| 1683 |
|
| 1684 |
list_missing_btn.click(_list_missing, inputs=[align_state], outputs=[missing_reveal])
|
| 1685 |
|
| 1686 |
+
predict_inputs = [
|
| 1687 |
+
model_state, workspace_state, prediction_artifact, predict_source_mode,
|
| 1688 |
+
prediction_input_mode, prediction_gene, prediction_dna, predict_meta_state,
|
| 1689 |
+
]
|
| 1690 |
|
| 1691 |
+
def _predict_wrapper(bundle, workspace, artifact_path, model_mode, input_mode, gene_path, dna_path, predict_meta):
|
| 1692 |
+
return run_prediction(
|
| 1693 |
+
bundle, workspace, artifact_path, model_mode == "Upload artifact",
|
| 1694 |
+
input_mode == "Prepared dataset", gene_path, dna_path, predict_meta,
|
| 1695 |
+
)
|
| 1696 |
|
| 1697 |
predict_button.click(
|
| 1698 |
+
lambda: _busy("Running inferenceβ¦", ["actbar-primary-btn"]), outputs=[predict_button],
|
| 1699 |
show_progress="hidden",
|
| 1700 |
).then(
|
| 1701 |
_predict_wrapper, inputs=predict_inputs,
|
| 1702 |
outputs=[model_state, prediction_status, prediction_table, distribution_panel, prediction_download, predict_meta_state],
|
|
|
|
|
|
|
|
|
|
| 1703 |
show_progress="hidden",
|
| 1704 |
).then(
|
| 1705 |
+
# This one call both resets the button's label back to "Run
|
| 1706 |
+
# inference" (out of its transient "Running inferenceβ¦" busy state)
|
| 1707 |
+
# and sets the correct interactive flag β no separate blind
|
| 1708 |
+
# re-enable step first, which used to write predict_button twice.
|
| 1709 |
_align_wrapper, inputs=align_inputs, outputs=[alignment_table, alignment_strip, predict_button, align_state],
|
| 1710 |
show_progress="hidden",
|
| 1711 |
).then(
|
biolmnet/data.py
CHANGED
|
@@ -57,6 +57,12 @@ def _normalise_columns(frame: pd.DataFrame) -> pd.DataFrame:
|
|
| 57 |
return result
|
| 58 |
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
def read_csv(source: str | Path | BinaryIO) -> pd.DataFrame:
|
| 61 |
if hasattr(source, "read"):
|
| 62 |
return _normalise_columns(pd.read_csv(source))
|
|
@@ -415,8 +421,7 @@ def load_genept_embeddings(filename: str) -> pd.DataFrame:
|
|
| 415 |
repo_type="model",
|
| 416 |
)
|
| 417 |
frame = pd.read_parquet(path)
|
| 418 |
-
|
| 419 |
-
return frame
|
| 420 |
|
| 421 |
|
| 422 |
def attach_embeddings_and_pathways(
|
|
@@ -426,6 +431,7 @@ def attach_embeddings_and_pathways(
|
|
| 426 |
*,
|
| 427 |
precomputed_significant: bool,
|
| 428 |
) -> pd.DataFrame:
|
|
|
|
| 429 |
embedding_index = set(embedding_frame.index.astype(str))
|
| 430 |
keep = np.array(
|
| 431 |
[gene in embedding_index for gene in branch.hidden_genes], dtype=bool
|
|
|
|
| 57 |
return result
|
| 58 |
|
| 59 |
|
| 60 |
+
def _normalise_embedding_index(frame: pd.DataFrame) -> pd.DataFrame:
|
| 61 |
+
result = frame.copy()
|
| 62 |
+
result.index = result.index.astype(str).str.strip()
|
| 63 |
+
return result[~result.index.duplicated(keep="first")]
|
| 64 |
+
|
| 65 |
+
|
| 66 |
def read_csv(source: str | Path | BinaryIO) -> pd.DataFrame:
|
| 67 |
if hasattr(source, "read"):
|
| 68 |
return _normalise_columns(pd.read_csv(source))
|
|
|
|
| 421 |
repo_type="model",
|
| 422 |
)
|
| 423 |
frame = pd.read_parquet(path)
|
| 424 |
+
return _normalise_embedding_index(frame)
|
|
|
|
| 425 |
|
| 426 |
|
| 427 |
def attach_embeddings_and_pathways(
|
|
|
|
| 431 |
*,
|
| 432 |
precomputed_significant: bool,
|
| 433 |
) -> pd.DataFrame:
|
| 434 |
+
embedding_frame = _normalise_embedding_index(embedding_frame)
|
| 435 |
embedding_index = set(embedding_frame.index.astype(str))
|
| 436 |
keep = np.array(
|
| 437 |
[gene in embedding_index for gene in branch.hidden_genes], dtype=bool
|
biolmnet/ui/components.py
CHANGED
|
@@ -228,6 +228,116 @@ def empty_note_html(message: str) -> str:
|
|
| 228 |
return f'<div class="empty-note">{esc(message)}</div>'
|
| 229 |
|
| 230 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
# ββ misc βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 232 |
|
| 233 |
def human_bytes(n: float) -> str:
|
|
|
|
| 228 |
return f'<div class="empty-note">{esc(message)}</div>'
|
| 229 |
|
| 230 |
|
| 231 |
+
# ββ introduction cards ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 232 |
+
|
| 233 |
+
INTRO_CARDS = [
|
| 234 |
+
(
|
| 235 |
+
"Introduction Β· 01 / 06",
|
| 236 |
+
"A network wired along known biology",
|
| 237 |
+
(
|
| 238 |
+
"BioLM-NET classifies tumour samples from <b>paired gene expression and DNA methylation</b>. "
|
| 239 |
+
"Instead of connecting every gene to every neuron, it only makes the connections biology supports β "
|
| 240 |
+
"KEGG pathways, transcription-factor targets, protein interactions β so trained weights can be read "
|
| 241 |
+
"back as pathways rather than as a black box."
|
| 242 |
+
),
|
| 243 |
+
"This workbench runs the model end to end in five stages. On the bundled BRCA example it takes a few minutes.",
|
| 244 |
+
"",
|
| 245 |
+
),
|
| 246 |
+
(
|
| 247 |
+
"Introduction Β· 02 / 06",
|
| 248 |
+
"Five stages, in order",
|
| 249 |
+
(
|
| 250 |
+
"The rail on the left is the spine of the app. Each stage consumes the previous one's output, so a "
|
| 251 |
+
"stage stays <b>locked</b> until its prerequisite exists β you cannot export a model you have not trained."
|
| 252 |
+
),
|
| 253 |
+
(
|
| 254 |
+
'<div class="intro-stage-list">'
|
| 255 |
+
"<div><em>01</em><span><b>Data & Priors</b> β assemble the masked graph</span></div>"
|
| 256 |
+
"<div><em>02</em><span><b>Train Model</b> β fit it, watch the epochs</span></div>"
|
| 257 |
+
"<div><em>03</em><span><b>Export Artifacts</b> β save a reusable bundle</span></div>"
|
| 258 |
+
"<div><em>04</em><span><b>Predict</b> β score a new cohort</span></div>"
|
| 259 |
+
"<div><em>05</em><span><b>Results</b> β metrics and pathway attention</span></div>"
|
| 260 |
+
"</div>"
|
| 261 |
+
),
|
| 262 |
+
"The plate under the rail always says what state the model is in",
|
| 263 |
+
),
|
| 264 |
+
(
|
| 265 |
+
"Introduction Β· 03 / 06 Β· Stage 01",
|
| 266 |
+
"Data & Priors",
|
| 267 |
+
(
|
| 268 |
+
"Point the workbench at paired omics β a bundled example, a GitHub folder, or your own upload β with "
|
| 269 |
+
"<b>samples in rows and HGNC symbols in columns</b>. The two matrices are aligned on their shared "
|
| 270 |
+
"samples, and every file is reported with the shape it was read at rather than failing silently."
|
| 271 |
+
),
|
| 272 |
+
(
|
| 273 |
+
"The priors on the right are the biology that becomes wiring: pathway annotations, the enrichment "
|
| 274 |
+
"cutoff, the GenePT context and the interaction sources. Defaults follow the paper."
|
| 275 |
+
),
|
| 276 |
+
"Rebuilding clears any trained model",
|
| 277 |
+
),
|
| 278 |
+
(
|
| 279 |
+
"Introduction Β· 04 / 06 Β· Stage 02",
|
| 280 |
+
"Train Model",
|
| 281 |
+
(
|
| 282 |
+
"Hyperparameters sit on the left as a spec sheet. The run panel on the right reports <b>epoch, train "
|
| 283 |
+
"and validation loss, accuracy and ETA</b> as the fit progresses."
|
| 284 |
+
),
|
| 285 |
+
(
|
| 286 |
+
"Controls lock for the duration of a run and the primary button reads <b>Training on ZeroGPU</b> "
|
| 287 |
+
"until it finishes; Cancel stays available. The GPU is allocated on demand, so the first epoch may "
|
| 288 |
+
"wait for its reservation."
|
| 289 |
+
),
|
| 290 |
+
"Paper defaults are one click away",
|
| 291 |
+
),
|
| 292 |
+
(
|
| 293 |
+
"Introduction Β· 05 / 06 Β· Stages 03β04",
|
| 294 |
+
"Export, then predict",
|
| 295 |
+
(
|
| 296 |
+
"Export writes one bundle carrying the weights, the fitted preprocessing, the mask and the config β "
|
| 297 |
+
"enough to score new samples later without rebuilding the graph. Its manifest lists what each entry "
|
| 298 |
+
"reproduces, with a checksum."
|
| 299 |
+
),
|
| 300 |
+
(
|
| 301 |
+
"Predict scores a new cohort with either the session model or an uploaded bundle. Features are "
|
| 302 |
+
"<b>aligned to the artifact first</b>; if a required column is missing, inference is blocked and the "
|
| 303 |
+
"check that failed is named."
|
| 304 |
+
),
|
| 305 |
+
"Bundles are temp files β download before the Space restarts",
|
| 306 |
+
),
|
| 307 |
+
(
|
| 308 |
+
"Introduction Β· 06 / 06 Β· Stage 05",
|
| 309 |
+
"Reading the results",
|
| 310 |
+
(
|
| 311 |
+
"Results collects everything a run produced: validation metrics, the training curve, the confusion "
|
| 312 |
+
"matrix, an audit of how sparse each layer actually is, and the pathway attention weights β which is "
|
| 313 |
+
"the part the architecture exists to give you."
|
| 314 |
+
),
|
| 315 |
+
(
|
| 316 |
+
"Treat it as model output, not as biology. <b>Check cohort composition, preprocessing and class "
|
| 317 |
+
"balance before drawing conclusions</b>, and remember that attention weights rank pathways within "
|
| 318 |
+
"this fit rather than proving mechanism."
|
| 319 |
+
),
|
| 320 |
+
"Research use only Β· not for clinical decisions",
|
| 321 |
+
),
|
| 322 |
+
]
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def intro_card_html(index: int) -> str:
|
| 326 |
+
step, title, body_1, body_2, _ = INTRO_CARDS[index]
|
| 327 |
+
return (
|
| 328 |
+
f'<div class="intro-step">{esc(step)}</div>'
|
| 329 |
+
f"<h4>{esc(title)}</h4>"
|
| 330 |
+
f"<p>{body_1}</p>"
|
| 331 |
+
f"<p>{body_2}</p>"
|
| 332 |
+
)
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
def intro_dots_html(index: int, total: int = 6) -> str:
|
| 336 |
+
return '<div class="intro-dots">' + "".join(
|
| 337 |
+
f'<i class="{"on" if i == index else ""}"></i>' for i in range(total)
|
| 338 |
+
) + "</div>"
|
| 339 |
+
|
| 340 |
+
|
| 341 |
# ββ misc βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 342 |
|
| 343 |
def human_bytes(n: float) -> str:
|
biolmnet/ui/styles.py
CHANGED
|
@@ -274,7 +274,25 @@ body, .gradio-container {
|
|
| 274 |
.kv-plain.last { border-bottom: none; }
|
| 275 |
|
| 276 |
/* ββ topbar / page head / action bar βββββββββββββββββββββββββββββββββββ */
|
| 277 |
-
.topbar
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
.pghd { padding: 26px 30px 0; display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; flex-wrap: wrap; }
|
| 279 |
.pghd h2 { font-size: 34px; line-height: 1; color: var(--color-text); }
|
| 280 |
.pghd .desc { margin-top: 5px; max-width: 640px; color: var(--color-muted); font-size: 13.5px; line-height: 1.5; }
|
|
@@ -510,6 +528,45 @@ body, .gradio-container {
|
|
| 510 |
.gradio-container input[type="range"] { accent-color: var(--color-accent); height: 3px; }
|
| 511 |
.gradio-container :focus-visible { outline: 2px solid var(--color-accent) !important; outline-offset: 2px; }
|
| 512 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
/* dataframes (.tbl-like) */
|
| 514 |
.gradio-container table { color: var(--color-text) !important; font-size: 12.5px !important; }
|
| 515 |
.gradio-container thead th {
|
|
@@ -526,6 +583,144 @@ body, .gradio-container {
|
|
| 526 |
/* accordion */
|
| 527 |
.gradio-container .label-wrap { color: var(--color-text) !important; }
|
| 528 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 529 |
/* ββ responsive βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 530 |
@media (max-width: 1000px) {
|
| 531 |
.app-shell { display: block !important; }
|
|
|
|
| 274 |
.kv-plain.last { border-bottom: none; }
|
| 275 |
|
| 276 |
/* ββ topbar / page head / action bar βββββββββββββββββββββββββββββββββββ */
|
| 277 |
+
.topbar-host {
|
| 278 |
+
position: relative;
|
| 279 |
+
display: block !important;
|
| 280 |
+
padding: 0 !important;
|
| 281 |
+
gap: 0 !important;
|
| 282 |
+
}
|
| 283 |
+
.topbar-host > .block:first-child { width: 100% !important; }
|
| 284 |
+
.topbar { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 13px 132px 13px 30px; border-bottom: 1px solid var(--color-divider); }
|
| 285 |
+
.intro-open-btn {
|
| 286 |
+
position: absolute !important;
|
| 287 |
+
right: 30px;
|
| 288 |
+
top: 8px;
|
| 289 |
+
z-index: 5;
|
| 290 |
+
}
|
| 291 |
+
.intro-open-btn button {
|
| 292 |
+
font-size: 11.5px !important;
|
| 293 |
+
min-height: 28px !important;
|
| 294 |
+
padding: 0 8px !important;
|
| 295 |
+
}
|
| 296 |
.pghd { padding: 26px 30px 0; display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; flex-wrap: wrap; }
|
| 297 |
.pghd h2 { font-size: 34px; line-height: 1; color: var(--color-text); }
|
| 298 |
.pghd .desc { margin-top: 5px; max-width: 640px; color: var(--color-muted); font-size: 13.5px; line-height: 1.5; }
|
|
|
|
| 528 |
.gradio-container input[type="range"] { accent-color: var(--color-accent); height: 3px; }
|
| 529 |
.gradio-container :focus-visible { outline: 2px solid var(--color-accent) !important; outline-offset: 2px; }
|
| 530 |
|
| 531 |
+
/* Gradio pulses a 2px accent border + animation around every component
|
| 532 |
+
whose value is currently pending ("generating"), independent of
|
| 533 |
+
show_progress. One action often updates several visible components at
|
| 534 |
+
once (a table, a note, a stat panel, β¦), so this reads as multiple
|
| 535 |
+
flashing "loading bars" appearing together β we already surface busy
|
| 536 |
+
state through the triggering button's own label (see app.py's `_busy`
|
| 537 |
+
helper), so drop Gradio's border/animation entirely rather than have it
|
| 538 |
+
multiply per output. */
|
| 539 |
+
.gradio-container .generating {
|
| 540 |
+
animation: none !important;
|
| 541 |
+
border-color: transparent !important;
|
| 542 |
+
border-width: 0 !important;
|
| 543 |
+
background: none !important;
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
/* The one deliberate loading indicator: a thin animated bar along the
|
| 547 |
+
bottom edge of whichever action button is currently busy. Toggled
|
| 548 |
+
explicitly by app.py's `_busy`/`_idle` helpers via the `is-busy` class β
|
| 549 |
+
never by Gradio's own per-component pending state β so exactly one of
|
| 550 |
+
these can ever be on screen at a time. */
|
| 551 |
+
/* elem_classes lands directly on the <button> tag for gr.Button (no
|
| 552 |
+
wrapper div) β cover both that and a wrapper div defensively. */
|
| 553 |
+
.is-busy, .is-busy button {
|
| 554 |
+
position: relative;
|
| 555 |
+
overflow: hidden;
|
| 556 |
+
}
|
| 557 |
+
.is-busy::after, .is-busy button::after {
|
| 558 |
+
content: "";
|
| 559 |
+
position: absolute;
|
| 560 |
+
left: 0; bottom: 0; height: 2px; width: 35%;
|
| 561 |
+
background: currentColor;
|
| 562 |
+
opacity: .7;
|
| 563 |
+
animation: workbench-busy-sweep 1.1s ease-in-out infinite;
|
| 564 |
+
}
|
| 565 |
+
@keyframes workbench-busy-sweep {
|
| 566 |
+
0% { transform: translateX(-100%); }
|
| 567 |
+
100% { transform: translateX(285%); }
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
/* dataframes (.tbl-like) */
|
| 571 |
.gradio-container table { color: var(--color-text) !important; font-size: 12.5px !important; }
|
| 572 |
.gradio-container thead th {
|
|
|
|
| 583 |
/* accordion */
|
| 584 |
.gradio-container .label-wrap { color: var(--color-text) !important; }
|
| 585 |
|
| 586 |
+
/* ββ first-open introduction βββββββββββββββββββββββββββββββββββββββββββ */
|
| 587 |
+
#intro {
|
| 588 |
+
position: fixed !important;
|
| 589 |
+
inset: 0 !important;
|
| 590 |
+
z-index: 60 !important;
|
| 591 |
+
min-height: 100vh !important;
|
| 592 |
+
display: grid !important;
|
| 593 |
+
place-items: center !important;
|
| 594 |
+
padding: 24px !important;
|
| 595 |
+
background: rgba(29, 31, 32, .6) !important;
|
| 596 |
+
border: 0 !important;
|
| 597 |
+
}
|
| 598 |
+
#intro[style*="display: none"] {
|
| 599 |
+
display: none !important;
|
| 600 |
+
}
|
| 601 |
+
#intro > .form,
|
| 602 |
+
#intro .form {
|
| 603 |
+
display: contents !important;
|
| 604 |
+
}
|
| 605 |
+
.intro-card {
|
| 606 |
+
position: relative;
|
| 607 |
+
width: min(556px, calc(100vw - 48px)) !important;
|
| 608 |
+
background: var(--color-bg) !important;
|
| 609 |
+
border: 1px solid rgba(29,31,32,.18) !important;
|
| 610 |
+
padding: 26px 28px 20px !important;
|
| 611 |
+
box-shadow: 0 18px 44px rgba(0,0,0,.18) !important;
|
| 612 |
+
}
|
| 613 |
+
.intro-card::before,
|
| 614 |
+
.intro-card::after {
|
| 615 |
+
content: "";
|
| 616 |
+
position: absolute;
|
| 617 |
+
top: -6px;
|
| 618 |
+
bottom: -6px;
|
| 619 |
+
width: 11px;
|
| 620 |
+
pointer-events: none;
|
| 621 |
+
border-color: color-mix(in srgb, var(--color-text) 55%, transparent);
|
| 622 |
+
}
|
| 623 |
+
.intro-card::before {
|
| 624 |
+
left: -6px;
|
| 625 |
+
border-left: 1px solid;
|
| 626 |
+
border-top: 1px solid;
|
| 627 |
+
border-bottom: 1px solid;
|
| 628 |
+
}
|
| 629 |
+
.intro-card::after {
|
| 630 |
+
right: -6px;
|
| 631 |
+
border-right: 1px solid;
|
| 632 |
+
border-top: 1px solid;
|
| 633 |
+
border-bottom: 1px solid;
|
| 634 |
+
}
|
| 635 |
+
.intro-card-page {
|
| 636 |
+
gap: 0 !important;
|
| 637 |
+
}
|
| 638 |
+
.intro-step {
|
| 639 |
+
font: 500 10px/1.3 var(--font-mono);
|
| 640 |
+
letter-spacing: .1em;
|
| 641 |
+
text-transform: uppercase;
|
| 642 |
+
color: var(--color-accent-700);
|
| 643 |
+
}
|
| 644 |
+
.intro-card h4 {
|
| 645 |
+
margin: 10px 0 10px;
|
| 646 |
+
font: 600 27px/1 var(--font-heading);
|
| 647 |
+
color: var(--color-text);
|
| 648 |
+
}
|
| 649 |
+
.intro-card p {
|
| 650 |
+
margin: 0 0 10px;
|
| 651 |
+
font-size: 13.5px;
|
| 652 |
+
line-height: 1.55;
|
| 653 |
+
color: color-mix(in srgb, var(--color-text) 74%, transparent) !important;
|
| 654 |
+
}
|
| 655 |
+
.intro-stage-list {
|
| 656 |
+
display: flex;
|
| 657 |
+
flex-direction: column;
|
| 658 |
+
gap: 8px;
|
| 659 |
+
margin: 8px 0 6px;
|
| 660 |
+
}
|
| 661 |
+
.intro-stage-list div {
|
| 662 |
+
display: grid;
|
| 663 |
+
grid-template-columns: 28px minmax(0, 1fr);
|
| 664 |
+
gap: 8px;
|
| 665 |
+
align-items: baseline;
|
| 666 |
+
font-size: 13px;
|
| 667 |
+
}
|
| 668 |
+
.intro-stage-list em {
|
| 669 |
+
font: 500 10px/1 var(--font-mono);
|
| 670 |
+
color: var(--color-accent-700);
|
| 671 |
+
font-style: normal;
|
| 672 |
+
}
|
| 673 |
+
.intro-card-foot {
|
| 674 |
+
align-items: center !important;
|
| 675 |
+
justify-content: space-between !important;
|
| 676 |
+
gap: 12px 16px !important;
|
| 677 |
+
margin-top: 16px !important;
|
| 678 |
+
padding-top: 14px !important;
|
| 679 |
+
border-top: 1px solid var(--color-divider) !important;
|
| 680 |
+
flex-wrap: wrap !important;
|
| 681 |
+
}
|
| 682 |
+
.intro-card-foot > *:first-child {
|
| 683 |
+
flex: 1 1 auto !important;
|
| 684 |
+
min-width: 0 !important;
|
| 685 |
+
max-width: 100% !important;
|
| 686 |
+
overflow-wrap: anywhere;
|
| 687 |
+
}
|
| 688 |
+
.intro-card-actions {
|
| 689 |
+
align-items: center !important;
|
| 690 |
+
justify-content: flex-end !important;
|
| 691 |
+
flex: 1 0 220px !important;
|
| 692 |
+
gap: 12px !important;
|
| 693 |
+
flex-wrap: nowrap !important;
|
| 694 |
+
width: auto !important;
|
| 695 |
+
margin-left: auto !important;
|
| 696 |
+
}
|
| 697 |
+
.intro-card-actions > * {
|
| 698 |
+
flex: 0 0 auto !important;
|
| 699 |
+
width: auto !important;
|
| 700 |
+
}
|
| 701 |
+
.intro-card-actions button {
|
| 702 |
+
min-height: 30px !important;
|
| 703 |
+
padding: 0 12px !important;
|
| 704 |
+
font-size: 12.5px !important;
|
| 705 |
+
white-space: normal !important;
|
| 706 |
+
line-height: 1.15 !important;
|
| 707 |
+
}
|
| 708 |
+
.intro-dots {
|
| 709 |
+
display: flex;
|
| 710 |
+
align-items: center;
|
| 711 |
+
gap: 5px;
|
| 712 |
+
flex: 0 0 auto;
|
| 713 |
+
}
|
| 714 |
+
.intro-dots i {
|
| 715 |
+
width: 8px;
|
| 716 |
+
height: 8px;
|
| 717 |
+
display: block;
|
| 718 |
+
background: color-mix(in srgb, var(--color-text) 18%, transparent);
|
| 719 |
+
}
|
| 720 |
+
.intro-dots i.on {
|
| 721 |
+
background: var(--color-accent);
|
| 722 |
+
}
|
| 723 |
+
|
| 724 |
/* ββ responsive βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 725 |
@media (max-width: 1000px) {
|
| 726 |
.app-shell { display: block !important; }
|
tests/test_core.py
CHANGED
|
@@ -128,6 +128,35 @@ def test_model_forward_probabilistic_shape() -> None:
|
|
| 128 |
)
|
| 129 |
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
def test_training_artifact_roundtrip(tmp_path) -> None:
|
| 132 |
workspace = _tiny_workspace()
|
| 133 |
result = train(
|
|
@@ -161,6 +190,39 @@ def test_training_artifact_roundtrip(tmp_path) -> None:
|
|
| 161 |
)
|
| 162 |
|
| 163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
def test_zerogpu_duration_estimator_is_bounded_and_scales() -> None:
|
| 165 |
from app import estimate_training_duration
|
| 166 |
|
|
|
|
| 128 |
)
|
| 129 |
|
| 130 |
|
| 131 |
+
def test_duplicate_embedding_rows_do_not_break_pathway_attachment() -> None:
|
| 132 |
+
branch = BranchPriors(
|
| 133 |
+
input_genes=["A", "B"],
|
| 134 |
+
hidden_genes=["A", "B", "C"],
|
| 135 |
+
biological_mask=np.ones((2, 3), dtype=np.float32),
|
| 136 |
+
)
|
| 137 |
+
embeddings = pd.DataFrame(
|
| 138 |
+
np.arange(16, dtype=np.float32).reshape(4, 4),
|
| 139 |
+
index=["A", "B", "B", "C"],
|
| 140 |
+
)
|
| 141 |
+
pathway_mapping = pd.DataFrame(
|
| 142 |
+
{
|
| 143 |
+
"SYMBOL": ["A", "B", "C"],
|
| 144 |
+
"PathwayID": ["hsa1", "hsa1", "hsa2"],
|
| 145 |
+
}
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
attach_embeddings_and_pathways(
|
| 149 |
+
branch,
|
| 150 |
+
embeddings,
|
| 151 |
+
pathway_mapping,
|
| 152 |
+
precomputed_significant=True,
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
assert branch.embeddings.shape[0] == len(branch.hidden_genes)
|
| 156 |
+
assert branch.biological_mask.shape[1] == len(branch.hidden_genes)
|
| 157 |
+
assert branch.pathway_mask.shape[0] == len(branch.hidden_genes)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
def test_training_artifact_roundtrip(tmp_path) -> None:
|
| 161 |
workspace = _tiny_workspace()
|
| 162 |
result = train(
|
|
|
|
| 190 |
)
|
| 191 |
|
| 192 |
|
| 193 |
+
def test_prediction_can_score_prepared_workspace_without_uploads() -> None:
|
| 194 |
+
from app import run_prediction
|
| 195 |
+
|
| 196 |
+
workspace = _tiny_workspace()
|
| 197 |
+
result = train(
|
| 198 |
+
workspace,
|
| 199 |
+
Hyperparameters(
|
| 200 |
+
epochs=3,
|
| 201 |
+
batch_size=8,
|
| 202 |
+
projection_dim=4,
|
| 203 |
+
fusion_dim=3,
|
| 204 |
+
dropout=0.0,
|
| 205 |
+
early_stopping_patience=3,
|
| 206 |
+
),
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
_, status, output, _, download_path, predict_meta = run_prediction(
|
| 210 |
+
result.bundle,
|
| 211 |
+
workspace,
|
| 212 |
+
None,
|
| 213 |
+
False,
|
| 214 |
+
True,
|
| 215 |
+
None,
|
| 216 |
+
None,
|
| 217 |
+
{},
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
assert "Predicted 48 samples" in status
|
| 221 |
+
assert len(output) == len(workspace.labels)
|
| 222 |
+
assert download_path is not None
|
| 223 |
+
assert predict_meta["n_samples"] == len(workspace.labels)
|
| 224 |
+
|
| 225 |
+
|
| 226 |
def test_zerogpu_duration_estimator_is_bounded_and_scales() -> None:
|
| 227 |
from app import estimate_training_duration
|
| 228 |
|