| """C-LipoVAE interactive demo — Hugging Face Space. |
| |
| A leakage-safe conditional variational autoencoder for binary phenotype |
| stratification of high-dimensional omics data. Editorial-quality UI |
| with per-metric interpretive commentary. |
| """ |
| from __future__ import annotations |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| try: |
| import gradio_client.utils as _gcu |
|
|
| _orig_j2p = _gcu._json_schema_to_python_type |
|
|
| def _patched_j2p(schema, defs=None): |
| if isinstance(schema, bool): |
| return "Any" if schema else "None" |
| return _orig_j2p(schema, defs) |
|
|
| _gcu._json_schema_to_python_type = _patched_j2p |
|
|
| _orig_get_type = _gcu.get_type |
|
|
| def _patched_get_type(schema): |
| if isinstance(schema, bool): |
| return "Any" if schema else "None" |
| return _orig_get_type(schema) |
|
|
| _gcu.get_type = _patched_get_type |
| except Exception: |
| pass |
|
|
| from pathlib import Path |
|
|
| import gradio as gr |
| import matplotlib.pyplot as plt |
| import numpy as np |
| import pandas as pd |
| from sklearn.decomposition import PCA |
| from sklearn.preprocessing import StandardScaler |
|
|
| from clipovae import ( |
| active_units, |
| c2st_auc, |
| encode_null, |
| fit_cvae, |
| integrated_gradients, |
| mmd2_rbf, |
| reconstruct, |
| sym_kl_gauss, |
| ) |
|
|
| APP_ROOT = Path(__file__).parent |
| EXAMPLE_MAT = APP_ROOT / "examples" / "wbc_matrix.csv" |
| EXAMPLE_LAB = APP_ROOT / "examples" / "wbc_labels.csv" |
|
|
| MAX_SAMPLES = 2000 |
| MAX_FEATURES = 2000 |
| MAX_CLASSES = 4 |
|
|
|
|
| |
| |
| |
| def auc_verdict(auc: float) -> tuple[str, str, str]: |
| """Return (label, color, sentence) for a C2ST AUC value. |
| |
| Bands follow the interpretation convention used in the C-LipoVAE |
| manuscript, with numeric anchors from real datasets: |
| supervised-comparable (AUC >= 0.90) ← WBC benchmark = 0.965 |
| strongly discriminative (0.80-0.90) ← TCM PD/NPD = 0.877 |
| moderately discriminative (0.70-0.80) |
| weakly discriminative (0.60-0.70) ← PIVUS sex = 0.644 |
| chance-level (< 0.60) |
| """ |
| if auc >= 0.90: |
| return ("Supervised-comparable encoding", "#0f766e", |
| "The null-condition latent carries near-complete " |
| "class-discriminative information (5-fold CV C2ST AUC " |
| f"= {auc:.3f}), of the same order as a direct " |
| "supervised classifier trained on the raw features. " |
| "Consistent with a well-separated, low-noise class " |
| "boundary.") |
| if auc >= 0.80: |
| return ("Strongly discriminative latent", "#16a34a", |
| "The leakage-safe latent geometry retains a " |
| "substantial fraction of the class signal from the " |
| "input space (5-fold CV C2ST AUC = " |
| f"{auc:.3f}); consistent with recoverable, " |
| "reproducible class-conditional structure.") |
| if auc >= 0.70: |
| return ("Moderately discriminative latent", "#ca8a04", |
| "The null-condition latent captures partial " |
| "class-discriminative structure (5-fold CV C2ST AUC " |
| f"= {auc:.3f}). Consistent with a real but incomplete " |
| "phenotype signature; feature attributions should be " |
| "corroborated with an external cohort.") |
| if auc >= 0.60: |
| return ("Weakly discriminative latent", "#d97706", |
| "The latent carries limited class-discriminative " |
| f"signal (5-fold CV C2ST AUC = {auc:.3f}). Consistent " |
| "with a biologically weak effect, small sample size, " |
| "or noisy input features. Interpret integrated-" |
| "gradient attributions with caution.") |
| return ("Chance-level encoding", "#dc2626", |
| f"The null-condition latent (5-fold CV C2ST AUC = {auc:.3f}) " |
| "does not encode the class label beyond chance. Consider a " |
| "higher-capacity model, more training data, or verifying " |
| "that the input features actually carry the phenotype.") |
|
|
|
|
| def au_verdict(n_au: int, d_latent: int) -> tuple[str, str, str]: |
| ratio = n_au / max(d_latent, 1) |
| if ratio >= 0.75: |
| return ("No posterior collapse", "#0f766e", |
| f"All {n_au} of {d_latent} latent dimensions have " |
| "per-dimension KL > 0.01 nats; full latent capacity " |
| "is being used and the prior is not dominating the " |
| "posterior.") |
| if ratio >= 0.5: |
| return ("Partial posterior collapse", "#ca8a04", |
| f"Only {n_au} of {d_latent} dimensions are active " |
| "(per-dim KL > 0.01). The effective latent dimension " |
| f"is {n_au}; the remaining dimensions have collapsed " |
| "onto the prior and contribute no information.") |
| return ("Severe posterior collapse", "#dc2626", |
| f"Only {n_au} of {d_latent} dimensions carry non-trivial " |
| "KL. The β regulariser is over-constraining the encoder. " |
| "Try a lower β (e.g. 0.5) or a larger training set.") |
|
|
|
|
| def r2_verdict(r2: float) -> tuple[str, str, str]: |
| if r2 >= 0.30: |
| return ("High reconstruction fidelity", "#0f766e", |
| "The CVAE recovers a substantial fraction of input " |
| f"variance (R²_rel = {r2:.2f} vs. mean-baseline), so " |
| "the latent code retains most feature-level " |
| "information alongside its discriminative signal.") |
| if r2 >= 0.10: |
| return ("Moderate reconstruction fidelity", "#ca8a04", |
| f"R²_rel = {r2:.2f}: the latent preserves the " |
| "informative signal but sacrifices some raw-feature " |
| "detail. Typical for β = 1.0 CVAEs tuned for " |
| "separability over autoencoding fidelity.") |
| return ("Low reconstruction fidelity", "#d97706", |
| f"R²_rel = {r2:.2f} indicates the bottleneck is " |
| "aggressive relative to the input complexity. Consider " |
| "increasing the latent dimension d or lowering β.") |
|
|
|
|
| def mmd_note(mmd: float) -> str: |
| if mmd >= 0.20: |
| s = ("large distributional shift between the class-conditional " |
| "latent clouds") |
| elif mmd >= 0.05: |
| s = "clear distributional shift" |
| else: |
| s = ("small distributional shift; the two classes overlap " |
| "substantially in the latent") |
| return (f"Squared MMD with a Gaussian-RBF kernel at " |
| f"median-heuristic bandwidth: {s}. Reported alongside " |
| "the C2ST AUC as an independent, classifier-free " |
| "separation check.") |
|
|
|
|
| def kl_note(kl: float) -> str: |
| if kl >= 10.0: |
| s = "well-separated Gaussian fits" |
| elif kl >= 2.0: |
| s = "clearly distinguishable Gaussian fits" |
| else: |
| s = "overlapping Gaussian fits" |
| return (f"Symmetrised KL divergence between multivariate-Gaussian " |
| f"fits to the two class-conditional latent clouds: {s}. " |
| "Larger values mean the class-conditional posteriors are " |
| "further apart in Gaussian-approximation terms.") |
|
|
|
|
| |
| |
| |
| |
| def detect_domain(feature_names: list[str]) -> str: |
| """Rough classifier based on feature-name lexicon.""" |
| joined = " ".join(feature_names[:100]).lower() |
|
|
| lipid_prefixes = ("pc ", "pe ", "ps ", "pi ", "pg ", "lpc ", "lpe ", |
| "sm ", "cer ", "cerp ", "tag ", "tag-", "tg ", |
| "dag ", "dg ", "cholesterol", "ffa ", "coq") |
| if any(f.strip().lower().startswith(lipid_prefixes) |
| for f in feature_names[:40]): |
| return "lipidomics" |
|
|
| wbc_terms = ("radius", "texture", "perimeter", "area", |
| "concavity", "concave points", "smoothness", |
| "compactness", "fractal dimension") |
| if sum(t in joined for t in wbc_terms) >= 4: |
| return "wbc_morphometry" |
|
|
| if any(f.startswith("ENSG") or f.startswith("ENST") |
| for f in feature_names[:100]): |
| return "transcriptomics" |
|
|
| if any(f.upper().startswith(("P0", "P1", "Q9", "Q1")) |
| and len(f) == 6 for f in feature_names[:100]): |
| return "proteomics" |
|
|
| if any("HMDB" in f.upper() or "kegg" in f.lower() |
| or "cid" in f.lower() for f in feature_names[:100]): |
| return "metabolomics" |
|
|
| return "generic" |
|
|
|
|
| def biology_html(domain: str, top_feats: list[str], auc: float) -> str: |
| """Domain-appropriate biological framing of the top drivers.""" |
| top3 = ", ".join(f"<code>{t}</code>" for t in top_feats[:3]) |
|
|
| if domain == "lipidomics": |
| body = ( |
| "The top-ranked features are the strongest nonlinear " |
| "drivers of the class contrast in the leakage-safe latent. " |
| f"Species carrying names such as {top3} identify the " |
| "lipid classes whose abundance shift most strongly " |
| "co-varies with the phenotype. When the leading species " |
| "are glycerophospholipids (<code>PC</code>, <code>PE</code>, " |
| "<code>PS</code>, <code>PI</code>), the signature " |
| "typically points to plasma-membrane remodeling and " |
| "polyunsaturated-fatty-acid trafficking; when " |
| "sphingomyelins (<code>SM</code>) dominate, ceramide-" |
| "signalling and insulin-resistance pathways are the " |
| "conventional biological reading; when triacylglycerols " |
| "(<code>TAG</code>) dominate, adipose storage and hepatic " |
| "lipogenesis are implicated. Corroborate directly against " |
| "the class-conditional log-fold-change and against an " |
| "external cohort before making mechanistic claims." |
| ) |
| header = "Lipidomic interpretation" |
|
|
| elif domain == "wbc_morphometry": |
| body = ( |
| "This is the Wisconsin Breast Cancer benchmark — " |
| "quantitative cell-nucleus morphometry from fine-needle " |
| "aspirates (radius, texture, area, concavity, concave " |
| f"points, and so on). The top-ranked drivers ({top3}) " |
| "are the same nuclear-atypia measurements that " |
| "pathologists use to grade cytological samples: nuclei " |
| "that are larger, more irregular, and more concave " |
| "correspond to the malignant class. C-LipoVAE recovers " |
| "these classical drivers <strong>without being told the " |
| "class label at evaluation time</strong> — a direct " |
| "sanity-check that the leakage-safe protocol still " |
| "extracts biologically meaningful signal, and a " |
| "reproduction of what is arguably the best-studied " |
| "supervised benchmark in medical machine learning." |
| ) |
| header = "Cytological interpretation (WBC benchmark)" |
|
|
| elif domain == "transcriptomics": |
| body = ( |
| "The top-ranked features are Ensembl transcripts / genes " |
| f"({top3}). Their differential representation between " |
| "classes suggests programme-level transcriptional changes " |
| "— consider enrichment against MSigDB Hallmark or KEGG " |
| "gene-sets before drawing pathway conclusions." |
| ) |
| header = "Transcriptomic interpretation" |
|
|
| elif domain == "proteomics": |
| body = ( |
| "The top-ranked features are UniProt-style protein " |
| f"identifiers ({top3}). These are the strongest nonlinear " |
| "drivers of the class separation; annotate them via " |
| "UniProt / Reactome and cross-check with an orthogonal " |
| "proteomic panel." |
| ) |
| header = "Proteomic interpretation" |
|
|
| elif domain == "metabolomics": |
| body = ( |
| "The top-ranked features are metabolomic identifiers " |
| f"({top3}). Map them to HMDB / KEGG to build a " |
| "pathway-level interpretation; check the direction of " |
| "shift against the class-conditional means before making " |
| "biological claims." |
| ) |
| header = "Metabolomic interpretation" |
|
|
| else: |
| body = ( |
| "The top-ranked features are the strongest nonlinear " |
| f"drivers of the class contrast in the latent ({top3}). " |
| "Because the leakage-safe protocol excludes label " |
| "information at evaluation time, these are the features " |
| "that the encoder learned to compress into a " |
| "class-informative geometry — a data-driven candidate " |
| "signature rather than a supervised discriminator. " |
| "Interpret in the context of the actual measurement " |
| "platform, and validate the direction of each feature " |
| "against the class-conditional means before making " |
| "mechanistic claims." |
| ) |
| header = "Signature interpretation" |
|
|
| strength = ("These interpretations are conditional on the AUC " |
| "band shown above: the higher the C2ST AUC, the more " |
| "confidence one can place in the top drivers as a " |
| "reproducible signature. For AUC < 0.70 the " |
| "attributions should be treated as exploratory only.") |
|
|
| return f""" |
| <div class="biology"> |
| <div class="biology-eyebrow">Biological interpretation</div> |
| <div class="biology-title">{header}</div> |
| <p class="biology-body">{body}</p> |
| <div class="biology-caveat">{strength}</div> |
| </div> |
| """ |
|
|
|
|
| |
| |
| |
| def _read_matrix(path: str) -> tuple[np.ndarray, list[str], list[str]]: |
| df = pd.read_csv(path, index_col=0) |
| return (df.values.astype(np.float32), |
| df.index.astype(str).tolist(), |
| df.columns.astype(str).tolist()) |
|
|
|
|
| def _read_labels(path: str) -> pd.Series: |
| df = pd.read_csv(path) |
| if df.shape[1] < 2: |
| raise gr.Error("labels.csv must have >=2 columns: " |
| "sample_id, label.") |
| return df.set_index(df.columns[0])[df.columns[1]] |
|
|
|
|
| def _align(X: np.ndarray, ids: list[str], |
| labels: pd.Series) -> tuple[np.ndarray, np.ndarray]: |
| common = [s for s in ids if s in labels.index] |
| if not common: |
| raise gr.Error("No sample IDs match between matrix and labels.") |
| row = {s: i for i, s in enumerate(ids)} |
| return X[[row[s] for s in common]], labels.loc[common].astype(int).values |
|
|
|
|
| def _r2_rel(X: np.ndarray, Xh: np.ndarray) -> float: |
| mse_r = float(np.mean((X - Xh) ** 2)) |
| mse_b = float(np.mean((X - X.mean(0, keepdims=True)) ** 2)) |
| return 1.0 - mse_r / max(mse_b, 1e-12) |
|
|
|
|
| |
| |
| |
| def _plot_latent(Z: np.ndarray, y: np.ndarray) -> plt.Figure: |
| palette = ["#4338ca", "#ea580c", "#0f766e", "#c026d3"] |
| Z2 = PCA(n_components=2, random_state=0).fit_transform(Z) \ |
| if Z.shape[1] >= 2 else np.hstack([Z, np.zeros_like(Z)]) |
| fig, ax = plt.subplots(figsize=(6.5, 5.0), dpi=110) |
| fig.patch.set_facecolor("#fafafa") |
| ax.set_facecolor("#ffffff") |
| for i, cls in enumerate(sorted(np.unique(y))): |
| m = y == cls |
| ax.scatter(Z2[m, 0], Z2[m, 1], s=42, alpha=0.78, |
| color=palette[i % len(palette)], |
| label=f"Class {cls} (n={int(m.sum())})", |
| edgecolor="white", linewidth=0.7, zorder=3) |
| ax.set_xlabel("PC1 of null-condition latent", |
| fontsize=11, color="#374151") |
| ax.set_ylabel("PC2 of null-condition latent", |
| fontsize=11, color="#374151") |
| ax.set_title("Leakage-safe latent geometry", |
| fontsize=13, fontweight="600", color="#111827", pad=12) |
| ax.legend(frameon=False, fontsize=10, loc="best") |
| for s in ("top", "right"): |
| ax.spines[s].set_visible(False) |
| for s in ("left", "bottom"): |
| ax.spines[s].set_color("#d1d5db") |
| ax.grid(alpha=0.25, linestyle=":", zorder=1) |
| ax.tick_params(colors="#6b7280") |
| fig.tight_layout() |
| return fig |
|
|
|
|
| |
| |
| |
| def run_pipeline(matrix_file, labels_file, |
| beta: float, d_latent: int, epochs: int, |
| log2: bool, seed: int): |
| if not matrix_file or not labels_file: |
| raise gr.Error("Please upload both matrix.csv and labels.csv, " |
| "or click 'Load example (WBC)'.") |
|
|
| try: |
| X_raw, ids, feats = _read_matrix(matrix_file) |
| except Exception as e: |
| raise gr.Error(f"Cannot read matrix CSV: {e}") |
| try: |
| labels = _read_labels(labels_file) |
| except Exception as e: |
| raise gr.Error(f"Cannot read labels CSV: {e}") |
|
|
| X_raw, y = _align(X_raw, ids, labels) |
| n, p = X_raw.shape |
| if n > MAX_SAMPLES: |
| raise gr.Error(f"n={n} exceeds the CPU-space cap of " |
| f"{MAX_SAMPLES}. Install locally for large data.") |
| if p > MAX_FEATURES: |
| raise gr.Error(f"p={p} exceeds the CPU-space cap of " |
| f"{MAX_FEATURES}. Install locally.") |
| n_cls = int(y.max()) + 1 |
| if n_cls > MAX_CLASSES: |
| raise gr.Error(f"{n_cls} classes; the demo supports " |
| f"<= {MAX_CLASSES}.") |
| if np.bincount(y).min() < 3: |
| raise gr.Error("At least 3 samples per class are required.") |
|
|
| if log2: |
| X_raw = np.log2(np.clip(X_raw, 1.0, None)) |
| X = StandardScaler().fit_transform(X_raw).astype(np.float32) |
|
|
| model = fit_cvae(X, y, beta=float(beta), d_latent=int(d_latent), |
| epochs=int(epochs), lr=1e-3, batch_size=16, |
| seed=int(seed), verbose=False) |
| Z = encode_null(model, X) |
| auc_m, auc_s = c2st_auc(Z, y, n_splits=5, seed=int(seed)) |
| mmd, _g = mmd2_rbf(Z[y == 0], Z[y == 1]) |
| kl = sym_kl_gauss(Z[y == 0], Z[y == 1]) |
| n_au, _ = active_units(model, X, y) |
| r2 = _r2_rel(X, reconstruct(model, X, y)) |
|
|
| z_diff = np.abs(Z[y == 1].mean(0) - Z[y == 0].mean(0)) |
| tgt = int(np.argmax(z_diff)) |
| ig = integrated_gradients(model, X, y, target_dim=tgt, steps=50) |
| ig_abs = np.mean(np.abs(ig), axis=0) |
| ord_ = np.argsort(ig_abs)[::-1][:15] |
| top_df = pd.DataFrame({ |
| "Rank": list(range(1, len(ord_) + 1)), |
| "Feature": [feats[i] for i in ord_], |
| "Mean |IG|": [f"{ig_abs[i]:.4f}" for i in ord_], |
| }) |
|
|
| auc_lbl, auc_col, auc_txt = auc_verdict(auc_m) |
| au_lbl, au_col, au_txt = au_verdict(n_au, int(d_latent)) |
| r2_lbl, r2_col, r2_txt = r2_verdict(r2) |
| mmd_txt = mmd_note(mmd) |
| kl_txt = kl_note(kl) |
| domain = detect_domain(feats) |
| top_feats = [feats[i] for i in ord_] |
| biology_block = biology_html(domain, top_feats, auc_m) |
|
|
| |
| verdict_html = f""" |
| <div class="verdict-hero"> |
| <div class="verdict-eyebrow">Latent-space verdict</div> |
| <div class="verdict-label" style="color:{auc_col}">{auc_lbl}</div> |
| <div class="verdict-sub"> |
| 5-fold CV C2ST AUC <strong>{auc_m:.3f} ± {auc_s:.3f}</strong> |
| · n = {n} · p = {p} |
| · classes = {n_cls} |
| </div> |
| <div class="verdict-interp">{auc_txt}</div> |
| <div class="verdict-anchor"> |
| <strong>Reference anchors from the C-LipoVAE manuscript</strong> — |
| Wisconsin Breast Cancer 0.965 (supervised-comparable) · |
| TCM PD/NPD 0.877 (strongly discriminative) · |
| PIVUS sex 0.644 (weakly discriminative). |
| </div> |
| </div> |
| |
| <div class="metric-grid"> |
| <div class="metric"> |
| <div class="metric-name">C2ST AUC</div> |
| <div class="metric-value">{auc_m:.3f} |
| <span class="metric-sd">± {auc_s:.3f}</span></div> |
| <div class="metric-badge" style="background:{auc_col}15;color:{auc_col}">{auc_lbl}</div> |
| <div class="metric-note">{auc_txt}</div> |
| </div> |
| |
| <div class="metric"> |
| <div class="metric-name">MMD² (RBF)</div> |
| <div class="metric-value">{mmd:.3f}</div> |
| <div class="metric-note">{mmd_txt}</div> |
| </div> |
| |
| <div class="metric"> |
| <div class="metric-name">Symmetrised KL</div> |
| <div class="metric-value">{kl:.2f}<span class="metric-unit"> nats</span></div> |
| <div class="metric-note">{kl_txt}</div> |
| </div> |
| |
| <div class="metric"> |
| <div class="metric-name">Active units</div> |
| <div class="metric-value">{n_au}<span class="metric-unit"> / {d_latent}</span></div> |
| <div class="metric-badge" style="background:{au_col}15;color:{au_col}">{au_lbl}</div> |
| <div class="metric-note">{au_txt}</div> |
| </div> |
| |
| <div class="metric"> |
| <div class="metric-name">R²<sub>rel</sub></div> |
| <div class="metric-value">{r2:.3f}</div> |
| <div class="metric-badge" style="background:{r2_col}15;color:{r2_col}">{r2_lbl}</div> |
| <div class="metric-note">{r2_txt}</div> |
| </div> |
| |
| <div class="metric"> |
| <div class="metric-name">Most-discriminative axis</div> |
| <div class="metric-value">z<sub>{tgt}</sub></div> |
| <div class="metric-note">Latent dimension with the largest |μ<sub>1</sub> − μ<sub>0</sub>| |
| under null-condition encoding. Integrated-gradient attribution |
| is computed against this axis, and reported below as the most |
| likely nonlinear discriminative-feature signature.</div> |
| </div> |
| </div> |
| |
| <div class="protocol-note"> |
| <div class="protocol-title">Why these numbers are trustworthy</div> |
| All separability metrics are computed on |
| <code>encode_null(model, X)</code>: the class label is replaced by a |
| zero vector at evaluation time so it cannot leak into the encoder. |
| This <em>null-condition</em> protocol removes the label side-channel |
| that inflates naive CVAE evaluations. |
| </div> |
| |
| {biology_block} |
| """ |
|
|
| return verdict_html, _plot_latent(Z, y), top_df |
|
|
|
|
| def load_example(): |
| return str(EXAMPLE_MAT), str(EXAMPLE_LAB) |
|
|
|
|
| |
| |
| |
| HERO = """ |
| <div class="hero"> |
| <div class="hero-badge">Open-source · MIT · Reproducible</div> |
| <h1 class="hero-title">C-LipoVAE</h1> |
| <p class="hero-tag">A <strong>leakage-safe conditional VAE</strong> |
| for binary phenotype stratification of high-dimensional omics data.</p> |
| <p class="hero-sub"> |
| Upload a feature matrix and binary labels. The model trains a |
| conditional variational autoencoder, evaluates its latent space |
| <span class="hl-yellow">without</span> feeding the label back in, |
| and returns publication-grade separability metrics with |
| interpretation. |
| </p> |
| <div class="hero-links"> |
| <a href="https://github.com/23008613g/C-LipoVAE" target="_blank" |
| rel="noopener">↗ GitHub</a> |
| · |
| <a href="https://github.com/23008613g/C-LipoVAE#quick-start" |
| target="_blank" rel="noopener">↗ Quick start</a> |
| </div> |
| </div> |
| |
| <div class="glance"> |
| <div class="glance-item"><span>1</span> |
| Upload <code>matrix.csv</code> + <code>labels.csv</code> |
| (or load the WBC example)</div> |
| <div class="glance-item"><span>2</span> |
| C-LipoVAE trains a β-VAE (default β = 1.0, d = 8, 150 epochs)</div> |
| <div class="glance-item"><span>3</span> |
| Metrics are computed under a null-condition encoding — no label |
| leakage</div> |
| <div class="glance-item"><span>4</span> |
| Get an interpretive verdict plus integrated-gradient |
| attributions</div> |
| </div> |
| """ |
|
|
|
|
| CSS = """ |
| :root { color-scheme: light; } |
| .gradio-container { max-width: 1160px !important; |
| font-family: 'Inter', 'SF Pro Text', |
| -apple-system, BlinkMacSystemFont, |
| 'Segoe UI', system-ui, sans-serif; |
| background: linear-gradient(180deg,#fafbff 0%, |
| #f4f4f8 100%) !important; } |
| |
| /* HERO */ |
| .hero { padding: 42px 44px 26px 44px; |
| background: linear-gradient(135deg,#0f172a 0%,#1e1b4b 45%, |
| #4c1d95 100%); |
| border-radius: 22px; color: #f8fafc; margin: 8px 0 4px 0; |
| box-shadow: 0 10px 40px -8px rgba(30,27,75,.35); } |
| .hero-badge { display: inline-block; font-size: 11px; letter-spacing: .12em; |
| text-transform: uppercase; color: #c7d2fe; |
| background: rgba(255,255,255,.08); padding: 5px 12px; |
| border-radius: 999px; margin-bottom: 20px; } |
| .hero-title { font-size: 44px; margin: 0 0 8px 0; font-weight: 800; |
| letter-spacing: -0.03em; |
| background: linear-gradient(90deg,#fff 0%,#c7d2fe 100%); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; } |
| .hero-tag { font-size: 19px; margin: 4px 0 8px 0; color: #e0e7ff; |
| font-weight: 300; } |
| .hero-tag strong { font-weight: 600; color: #fff; } |
| .hero-sub { font-size: 14.5px; max-width: 780px; margin: 6px 0 14px 0; |
| color: #cbd5e1; line-height: 1.6; } |
| .hero-sub .hl-yellow { color: #fde68a; font-weight: 600; } |
| .hero-links { font-size: 13.5px; color: #a5b4fc; } |
| .hero-links a { color: #ddd6fe; text-decoration: none; } |
| .hero-links a:hover { color: #fff; text-decoration: underline; } |
| |
| /* AT-A-GLANCE STRIP */ |
| .glance { display: grid; grid-template-columns: repeat(4, 1fr); |
| gap: 12px; margin: 22px 0 10px 0; } |
| .glance-item { background: #ffffff; padding: 14px 16px 14px 46px; |
| border-radius: 12px; position: relative; font-size: 13px; |
| color: #334155; line-height: 1.5; |
| box-shadow: 0 1px 2px rgba(15,23,42,.05), |
| 0 4px 12px -6px rgba(15,23,42,.08); |
| border: 1px solid #e5e7eb; } |
| .glance-item span { position: absolute; left: 12px; top: 14px; |
| width: 26px; height: 26px; border-radius: 8px; |
| background: linear-gradient(135deg,#4f46e5,#7c3aed); |
| color: #fff; font-weight: 600; font-size: 12.5px; |
| display: flex; align-items: center; |
| justify-content: center; } |
| .glance-item code { background: #f1f5f9; padding: 1px 5px; |
| border-radius: 4px; font-size: 12px; } |
| |
| /* SECTION HEADERS */ |
| .section-title { font-size: 12px; font-weight: 700; |
| letter-spacing: .16em; text-transform: uppercase; |
| color: #6366f1; margin: 24px 0 10px 4px; } |
| |
| /* INPUTS PANEL */ |
| .panel { background: #ffffff; padding: 18px; border-radius: 14px; |
| border: 1px solid #e5e7eb; |
| box-shadow: 0 2px 8px rgba(15,23,42,.04); } |
| |
| /* VERDICT HERO */ |
| .verdict-hero { background: #ffffff; border-radius: 16px; |
| padding: 26px 30px; margin-bottom: 18px; |
| border: 1px solid #e5e7eb; |
| box-shadow: 0 4px 24px -8px rgba(15,23,42,.10); } |
| .verdict-eyebrow { font-size: 11px; letter-spacing: .18em; |
| text-transform: uppercase; color: #6b7280; |
| font-weight: 700; } |
| .verdict-label { font-size: 34px; font-weight: 800; |
| letter-spacing: -0.02em; margin: 4px 0 6px 0; |
| line-height: 1.1; } |
| .verdict-sub { color: #4b5563; font-size: 14px; |
| font-family: 'JetBrains Mono', 'SF Mono', |
| 'Menlo', monospace; } |
| .verdict-sub strong { color: #111827; } |
| .verdict-interp { margin-top: 14px; color: #1f2937; font-size: 14.5px; |
| line-height: 1.65; max-width: 780px; } |
| .verdict-anchor { margin-top: 14px; padding: 10px 14px; |
| background: #f3f4f6; border-radius: 8px; |
| color: #4b5563; font-size: 12.5px; line-height: 1.55; |
| border-left: 3px solid #6366f1; } |
| .verdict-anchor strong { color: #111827; } |
| |
| /* METRIC GRID */ |
| .metric-grid { display: grid; grid-template-columns: repeat(3, 1fr); |
| gap: 14px; margin-bottom: 20px; } |
| @media (max-width: 900px) { |
| .metric-grid { grid-template-columns: repeat(2, 1fr); } |
| .glance { grid-template-columns: repeat(2, 1fr); } |
| } |
| .metric { background: #ffffff; border-radius: 14px; padding: 18px 20px; |
| border: 1px solid #e5e7eb; |
| box-shadow: 0 1px 2px rgba(15,23,42,.04), |
| 0 4px 20px -10px rgba(15,23,42,.10); |
| display: flex; flex-direction: column; } |
| .metric-name { font-size: 11px; letter-spacing: .12em; |
| text-transform: uppercase; color: #6b7280; |
| font-weight: 700; } |
| .metric-value { font-size: 32px; font-weight: 800; margin: 4px 0 0 0; |
| color: #111827; line-height: 1.05; letter-spacing: -0.02em; |
| font-family: 'JetBrains Mono', 'SF Mono', |
| 'Menlo', monospace; } |
| .metric-sd, .metric-unit { font-size: 15px; font-weight: 500; |
| color: #9ca3af; margin-left: 4px; } |
| .metric-badge { display: inline-block; margin: 8px 0 6px 0; |
| padding: 3px 10px; border-radius: 999px; |
| font-size: 11.5px; font-weight: 700; |
| letter-spacing: .04em; text-transform: uppercase; |
| align-self: flex-start; } |
| .metric-note { color: #4b5563; font-size: 12.5px; line-height: 1.55; |
| margin-top: 4px; } |
| |
| /* BIOLOGY BLOCK */ |
| .biology { background: linear-gradient(180deg,#ffffff 0%,#fdfaff 100%); |
| border-radius: 16px; padding: 22px 26px 20px 26px; |
| border: 1px solid #e5e7eb; |
| box-shadow: 0 4px 24px -12px rgba(15,23,42,.12); |
| margin: 4px 0 18px 0; } |
| .biology-eyebrow { font-size: 11px; letter-spacing: .18em; |
| text-transform: uppercase; color: #7c3aed; |
| font-weight: 700; } |
| .biology-title { font-size: 20px; font-weight: 700; color: #111827; |
| margin: 4px 0 10px 0; letter-spacing: -0.01em; } |
| .biology-body { color: #1f2937; font-size: 14.5px; line-height: 1.7; |
| margin: 0 0 12px 0; max-width: 820px; } |
| .biology-body code { background: #f3f4f6; padding: 1px 6px; |
| border-radius: 4px; font-size: 12.8px; |
| color: #4338ca; |
| font-family: 'JetBrains Mono','SF Mono',monospace; } |
| .biology-body strong { color: #111827; font-weight: 700; } |
| .biology-caveat { color: #6b7280; font-size: 12.5px; line-height: 1.55; |
| padding: 8px 12px; background: #f9fafb; |
| border-radius: 6px; border-left: 2px solid #d1d5db; } |
| |
| /* PROTOCOL NOTE */ |
| .protocol-note { background: #f5f3ff; border-left: 3px solid #7c3aed; |
| border-radius: 8px; padding: 14px 18px; |
| margin: 6px 0 18px 0; font-size: 13px; color: #4c1d95; |
| line-height: 1.6; } |
| .protocol-note code { background: #ede9fe; padding: 1px 6px; |
| border-radius: 4px; font-size: 12.5px; |
| color: #5b21b6; } |
| .protocol-title { font-weight: 700; font-size: 12px; |
| letter-spacing: .12em; text-transform: uppercase; |
| color: #6d28d9; margin-bottom: 4px; } |
| |
| /* BUTTONS */ |
| button.primary { background: linear-gradient(135deg,#4f46e5,#7c3aed) |
| !important; color: #fff !important; |
| border: 0 !important; font-weight: 700 !important; |
| letter-spacing: .02em !important; |
| box-shadow: 0 4px 12px -3px rgba(79,70,229,.5) !important; } |
| button.primary:hover { filter: brightness(1.08); } |
| |
| /* FOOTER */ |
| .footer { text-align: center; color: #6b7280; font-size: 12px; |
| margin: 22px 0 10px 0; letter-spacing: .04em; } |
| |
| /* Hide Gradio's built-in footer ("Built with Gradio" / "Settings"), |
| which is translated by browser locale. Our own English footer |
| replaces it above. */ |
| footer, |
| .gradio-container footer, |
| div[class*="svelte-"] > footer, |
| button[title*="Setting"], button[title*="设置"], button[title*="設定"] { |
| display: none !important; |
| } |
| """ |
|
|
|
|
| FORCE_EN_JS = """ |
| <script> |
| (function () { |
| // Force English UI text regardless of the browser locale that |
| // Gradio's i18n picks. Maps common Traditional / Simplified Chinese |
| // strings shown by Gradio 4.x components to their English equivalents. |
| const T = { |
| '拖放檔案至此處': 'Drop CSV file here', |
| '拖放檔案至此': 'Drop CSV file here', |
| '拖放文件至此处': 'Drop CSV file here', |
| '拖放文件至此': 'Drop CSV file here', |
| '點擊上傳': 'or click to upload', |
| '点击上传': 'or click to upload', |
| '- 或 -': '— or —', |
| '或': 'or', |
| '上傳檔案': 'Upload file', |
| '上传文件': 'Upload file', |
| '錯誤': 'Error', |
| '错误': 'Error', |
| '提交': 'Submit', |
| '送出': 'Submit', |
| '清除': 'Clear', |
| '刪除': 'Delete', |
| '删除': 'Delete', |
| '執行': 'Run', |
| '执行': 'Run', |
| '下載': 'Download', |
| '下载': 'Download', |
| '已完成': 'Done', |
| '完成': 'Done', |
| '處理中': 'Processing', |
| '处理中': 'Processing', |
| '請等待': 'Please wait', |
| '请等待': 'Please wait', |
| '使用Gradio建構': 'Built with Gradio', |
| '使用Gradio构建': 'Built with Gradio', |
| '設定': 'Settings', |
| '设置': 'Settings', |
| }; |
| const swap = () => { |
| // Walk text nodes and translate any exact matches. |
| const walker = document.createTreeWalker( |
| document.body, NodeFilter.SHOW_TEXT, null, false); |
| let n; |
| while ((n = walker.nextNode())) { |
| const s = n.nodeValue.trim(); |
| if (T[s] !== undefined && n.nodeValue !== T[s]) { |
| n.nodeValue = n.nodeValue.replace(s, T[s]); |
| } |
| } |
| // Also swap common aria-labels / titles. |
| document.querySelectorAll('[aria-label],[title]').forEach(el => { |
| const a = el.getAttribute('aria-label'); |
| if (a && T[a.trim()]) el.setAttribute('aria-label', T[a.trim()]); |
| const t = el.getAttribute('title'); |
| if (t && T[t.trim()]) el.setAttribute('title', T[t.trim()]); |
| }); |
| }; |
| const start = () => { |
| swap(); |
| new MutationObserver(swap).observe(document.body, { |
| childList: true, subtree: true, characterData: true |
| }); |
| }; |
| if (document.readyState === 'loading') { |
| document.addEventListener('DOMContentLoaded', start); |
| } else { start(); } |
| })(); |
| </script> |
| """ |
|
|
|
|
| def build_demo() -> gr.Blocks: |
| with gr.Blocks(title="C-LipoVAE — leakage-safe conditional VAE", |
| css=CSS, head=FORCE_EN_JS, |
| theme=gr.themes.Base( |
| primary_hue="indigo", |
| neutral_hue="slate")) as demo: |
| gr.HTML(HERO) |
|
|
| gr.HTML('<div class="section-title">Inputs</div>') |
| with gr.Group(elem_classes=["panel"]): |
| with gr.Row(): |
| matrix_in = gr.File(label="Feature matrix (CSV)", |
| file_types=[".csv"]) |
| labels_in = gr.File(label="Labels (CSV)", |
| file_types=[".csv"]) |
| with gr.Row(): |
| example_btn = gr.Button("Load example (Wisconsin " |
| "Breast Cancer)", |
| variant="secondary") |
| run_btn = gr.Button("Run C-LipoVAE", |
| variant="primary", |
| elem_classes=["primary"]) |
| with gr.Accordion("Hyperparameters (advanced)", open=False): |
| with gr.Row(): |
| beta = gr.Slider(0.1, 4.0, value=1.0, step=0.1, |
| label="β (KL weight)") |
| d_latent = gr.Slider(2, 16, value=8, step=1, |
| label="Latent dim (d)") |
| epochs = gr.Slider(20, 300, value=150, step=10, |
| label="Epochs") |
| with gr.Row(): |
| seed = gr.Number(value=42, label="Seed", |
| precision=0) |
| log2 = gr.Checkbox( |
| label="Apply log₂ transform " |
| "(for raw LC-MS intensities)") |
|
|
| gr.HTML('<div class="section-title">Results</div>') |
| result_html = gr.HTML(value=( |
| '<div class="verdict-hero">' |
| '<div class="verdict-eyebrow">Awaiting run</div>' |
| '<div class="verdict-label" style="color:#6b7280">' |
| '— · —</div>' |
| '<div class="verdict-sub">' |
| 'Click <strong>Load example</strong> then ' |
| '<strong>Run C-LipoVAE</strong> to see the interpretive ' |
| 'verdict and metric breakdown.</div></div>')) |
|
|
| with gr.Row(): |
| plot_out = gr.Plot(label="Leakage-safe latent (PCA-2)", |
| show_label=False) |
| table_out = gr.Dataframe( |
| headers=["Rank", "Feature", "Mean |IG|"], |
| label="Top-15 features by integrated-gradient " |
| "attribution", |
| interactive=False) |
|
|
| gr.HTML( |
| '<div class="footer">' |
| 'Runs on Hugging Face free-tier CPU · uploaded files ' |
| 'are used only for the current inference call and are not ' |
| 'stored · MIT license · ' |
| '<a href="https://github.com/23008613g/C-LipoVAE" ' |
| 'style="color:#6366f1;text-decoration:none">' |
| 'github.com/23008613g/C-LipoVAE</a></div>') |
|
|
| example_btn.click(load_example, None, [matrix_in, labels_in]) |
| run_btn.click(run_pipeline, |
| [matrix_in, labels_in, |
| beta, d_latent, epochs, log2, seed], |
| [result_html, plot_out, table_out]) |
| return demo |
|
|
|
|
| if __name__ == "__main__": |
| build_demo().launch(server_name="0.0.0.0", server_port=7860) |
|
|