""" Forge MIP Embedding Explorer — HuggingFace Spaces app. Single MIP: upload a .mps/.lp file → learned instance embedding + stats. Multi-MIP: upload several files → interactive 2-D scatter plot (PCA / t-SNE). Embedding type: Forge pre-trained model (mip_to_embeddings). """ import os import tempfile import traceback from pathlib import Path import gradio as gr import numpy as np import pandas as pd import plotly.express as px import plotly.graph_objects as go import spaces from sklearn.decomposition import PCA # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- _ACCEPTED_EXTS = (".mps", ".lp", ".mps.gz", ".lp.gz") SAMPLE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "samples") MODEL_PKL = os.path.join(os.path.dirname(os.path.abspath(__file__)), "models", "forge_pretrain_trained.pkl") TRAIN_CONFIG_YAML = os.path.join(os.path.dirname(os.path.abspath(__file__)), "configs", "train_config.yaml") def _extract_label(filename: str) -> str: """Best-effort: pull the problem-type token from DMIPLIB filenames.""" stem = Path(filename).stem if stem.endswith(".mps") or stem.endswith(".lp"): stem = Path(stem).stem parts = stem.split("_") return parts[1] if len(parts) > 1 else stem def _clean_embed_matrix(mat: np.ndarray) -> np.ndarray: mat = np.where(np.isfinite(mat), mat, 0.0) col_sum = mat.sum(axis=0, keepdims=True) return mat / (col_sum + 1e-10) def _build_embedder(): from forge.embeddings import Forge return Forge(train_config_yaml=TRAIN_CONFIG_YAML) def _mip_stats(path: str) -> dict: """Read basic MIP stats directly from the Gurobi model attributes.""" import gurobipy as gp model = gp.read(path) type_map = { (False, False, False): "LP", (True, False, False): "MILP", (False, True, False): "QP", (True, True, False): "MIQP", (False, False, True): "QCP", (True, False, True): "MIQCP", } prob_type = type_map.get((bool(model.IsMIP), bool(model.IsQP), bool(model.IsQCP)), "MILP") return { "probtype": prob_type, "n_vars": int(model.NumVars), "n_constr": int(model.NumConstrs), "n_nzcnt": int(model.NumNZs), "num_b_variables": int(model.NumBinVars), "num_i_variables": int(model.NumIntVars) - int(model.NumBinVars), "num_c_variables": int(model.NumVars) - int(model.NumIntVars), } def _embed_file(path: str, forge): from forge.pipeline import mip_to_embeddings from forge.utils import Constants with tempfile.NamedTemporaryFile(suffix=".pkl", delete=False) as tmp: output_pkl = tmp.name try: result = mip_to_embeddings( forge=forge, input_forge_pkl=MODEL_PKL, model_type=Constants.FORGE_PRE_TRAIN, input_mips=path, input_mip_instances_file=None, output_mip_to_embeddings_pkl=output_pkl, instance_embedding_only=True, ) finally: if os.path.exists(output_pkl): os.remove(output_pkl) if not result: return None key = next(iter(result)) return result[key] # --------------------------------------------------------------------------- # Single-MIP # --------------------------------------------------------------------------- @spaces.GPU(duration=60) def embed_single(mip_file): if mip_file is None: return None, None, None, "⚠ Upload a MIP instance first." path = mip_file if isinstance(mip_file, str) else mip_file.name try: forge = _build_embedder() emb = _embed_file(path, forge) if emb is None: return None, None, None, "⚠ Could not process the file." stats = _mip_stats(path) stats_df = pd.DataFrame({ "Metric": [ "Problem type", "Variables", "Constraints", "Non-zeros", "Binary vars", "Integer vars", "Continuous vars", ], "Value": [ stats["probtype"], stats["n_vars"], stats["n_constr"], stats["n_nzcnt"], stats["num_b_variables"], stats["num_i_variables"], stats["num_c_variables"], ], }) vals = np.where(np.isfinite(emb.instance_embedding), emb.instance_embedding, 0.0) names = [f"dim_{i}" for i in range(len(vals))] top_n = min(40, len(names)) top_idx = np.argsort(np.abs(vals))[-top_n:][::-1] top_names = [names[i] for i in top_idx] top_vals = [float(vals[i]) for i in top_idx] fig = go.Figure( go.Bar( x=top_names, y=top_vals, marker=dict( color=top_vals, colorscale="RdBu", cmid=0, showscale=True, ), ) ) fig.update_layout( title=f"Top-{top_n} Instance Embedding Dimensions — {Path(path).name}", xaxis_title="Embedding dimension", yaxis_title="Value", xaxis_tickangle=-55, height=480, margin=dict(b=180), ) csv_df = pd.DataFrame({"dimension": names, "value": emb.instance_embedding}) return ( stats_df, fig, csv_df, f"✓ Processed {Path(path).name} " f"({stats['n_vars']} vars, {stats['n_constr']} constraints)", ) except Exception as exc: tb = traceback.format_exc() msg = ( f"❌ {exc}\n\n" "Make sure gurobipy is installed and a valid Gurobi licence is available.\n" "For small instances the size-limited free licence works out of the box.\n" "For larger instances set GRB_WLSACCESSID / GRB_WLSSECRET / GRB_LICENSEID " "as Space secrets.\n\n" f"Traceback:\n{tb}" ) return None, None, None, msg # --------------------------------------------------------------------------- # Multi-MIP # --------------------------------------------------------------------------- @spaces.GPU(duration=120) def embed_multi(mip_files, method, label_mode, max_files=10): if not mip_files: return None, None, "⚠ Upload at least 2 MIP files." paths = [f if isinstance(f, str) else f.name for f in mip_files] if len(paths) > max_files: paths = paths[:max_files] try: embedder = _build_embedder() embeddings, names, labels, errors = [], [], [], [] for p in paths: try: emb = _embed_file(p, embedder) if emb is None: errors.append(f"{Path(p).name}: no embedding returned") continue embeddings.append(emb.instance_embedding) name = Path(p).name names.append(name) labels.append(_extract_label(name) if label_mode == "problem type" else name) except Exception as exc: errors.append(f"{Path(p).name}: {exc}") if len(embeddings) < 2: return None, None, "⚠ Need ≥ 2 valid embeddings.\n" + "\n".join(errors) mat = _clean_embed_matrix(np.array(embeddings)) dim_label = "" if method == "PCA": reducer = PCA(n_components=2, random_state=42) coords = reducer.fit_transform(mat) ev = reducer.explained_variance_ratio_ dim_label = f"PCA (PC1 {ev[0]:.1%}, PC2 {ev[1]:.1%})" elif method == "t-SNE": from sklearn.manifold import TSNE perp = max(5, min(30, len(embeddings) - 1)) coords = TSNE(n_components=2, perplexity=perp, random_state=42, max_iter=1000).fit_transform(mat) dim_label = f"t-SNE (perplexity={perp})" else: # UMAP try: import umap coords = umap.UMAP(n_components=2, random_state=42).fit_transform(mat) dim_label = "UMAP" except ImportError: reducer = PCA(n_components=2, random_state=42) coords = reducer.fit_transform(mat) ev = reducer.explained_variance_ratio_ dim_label = f"PCA (UMAP unavailable) PC1 {ev[0]:.1%} PC2 {ev[1]:.1%}" df_plot = pd.DataFrame({ "Dim 1": coords[:, 0], "Dim 2": coords[:, 1], "label": labels, "file": names, }) unique_labels = df_plot["label"].unique() palette = px.colors.qualitative.Plotly + px.colors.qualitative.Set2 color_map = {lbl: palette[i % len(palette)] for i, lbl in enumerate(sorted(unique_labels))} fig = go.Figure() for lbl in sorted(unique_labels): sub = df_plot[df_plot["label"] == lbl] fig.add_trace(go.Scatter( x=sub["Dim 1"], y=sub["Dim 2"], mode="markers+text", text=sub["file"] if len(paths) <= 15 else None, textposition="top center", marker=dict(size=10, color=color_map[lbl]), name=lbl, hovertemplate="%{customdata}
Dim1=%{x:.3f} Dim2=%{y:.3f}", customdata=sub["file"], )) fig.update_layout( title=f"MIP Embedding Space — {dim_label}", xaxis_title="Dim 1", yaxis_title="Dim 2", legend_title="Label", height=560, ) summary = pd.DataFrame({ "File": names, "Label": labels, "Dim 1": coords[:, 0].round(4), "Dim 2": coords[:, 1].round(4), }) status = f"✓ Embedded {len(embeddings)} MIPs | {dim_label}" if errors: status += f"\n⚠ {len(errors)} failed:\n" + "\n".join(errors) return fig, summary, status except Exception as exc: tb = traceback.format_exc() return None, None, f"❌ {exc}\n\n{tb}" # --------------------------------------------------------------------------- # Sample-data helper # --------------------------------------------------------------------------- def load_sample_files(): """Return all files from the bundled samples directory.""" if not os.path.isdir(SAMPLE_DIR): return [], "⚠ Sample data directory not found." from collections import defaultdict by_type = defaultdict(list) for f in os.listdir(SAMPLE_DIR): if any(f.lower().endswith(e) for e in _ACCEPTED_EXTS): lbl = _extract_label(f) by_type[lbl].append(os.path.join(SAMPLE_DIR, f)) selected = [] for lbl in sorted(by_type): selected.extend(sorted(by_type[lbl])) if not selected: return [], "⚠ No MIP files found in samples directory." selected = selected[:10] return selected, f"Loaded {len(selected)} sample files (capped at 10 for free-tier GPU)." def use_sample_data(method, label_mode): # Not decorated with @spaces.GPU itself — it delegates to embed_multi, # which is already decorated and will request the GPU when called. # # NOTE: ZeroGPU runs the decorated call in a separate worker process and # pickles the arguments to send them over. embed_multi already accepts # plain path strings (`f if isinstance(f, str) else f.name`), so pass # strings directly rather than wrapping them in a locally-defined class # (local/inner classes can't be pickled, which is what caused the # PicklingError). paths, msg = load_sample_files() if not paths: return None, None, msg fig, table, status = embed_multi(paths, method, label_mode) return fig, table, msg + "\n" + (status or "") # --------------------------------------------------------------------------- # Gradio UI # --------------------------------------------------------------------------- _DESCRIPTION = """ # For more details, visit [Forge: Foundational Optimization Representation from Graph Embeddings](https://skadio.github.io/forge/) """ with gr.Blocks(title="Forge MIP Embeddings", theme=gr.themes.Soft()) as demo: gr.Markdown(_DESCRIPTION) with gr.Tab("Single MIP"): gr.Markdown("Upload a MIP instace to visualize its Forge embedding.") with gr.Row(): single_file = gr.File( label="MIP instance (.mps / .lp / .mps.gz / .lp.gz)", file_types=[".mps", ".lp", ".gz"], ) single_btn = gr.Button("Generate Forge Embedding", variant="primary", scale=0) single_status = gr.Textbox(label="Status", lines=3, interactive=False) with gr.Row(): stats_df = gr.DataFrame(label="MIP Statistics", interactive=False) single_plot = gr.Plot(label="Top Instance Features") full_emb_df = gr.DataFrame(label="Full Embedding Vector", interactive=False, wrap=True) single_btn.click( fn=embed_single, inputs=single_file, outputs=[stats_df, single_plot, full_emb_df, single_status], ) with gr.Tab("Multiple MIPs"): gr.Markdown( "Upload MIP instances to visualize their Forge embeddings. " "Max 10 instances on this free-tier CPU. Alternatively, download this app and run on your GPU." ) with gr.Row(): multi_files = gr.File( label="MIP files (multiple)", file_count="multiple", file_types=[".mps", ".lp", ".gz"], ) with gr.Row(): method_radio = gr.Radio( ["PCA", "t-SNE", "UMAP"], value="PCA", label="Dimensionality reduction", ) label_mode = gr.Radio( ["problem type", "filename"], value="problem type", label="Colour by", ) multi_btn = gr.Button("Embed & Visualise", variant="primary", scale=0) multi_status = gr.Textbox(label="Status", lines=3, interactive=False) scatter_plot = gr.Plot(label="2D Forge Embedding Space") coords_df = gr.DataFrame(label="Embedding Coordinates", interactive=False) multi_btn.click( fn=embed_multi, inputs=[multi_files, method_radio, label_mode], outputs=[scatter_plot, coords_df, multi_status], ) gr.Markdown("---\n### Demo with bundled MIP instances") gr.Markdown( "No files to upload? Run on MIP instances bundled within this space." ) with gr.Row(): sample_method = gr.Radio( ["PCA", "t-SNE", "UMAP"], value="PCA", label="Reduction method", ) sample_label = gr.Radio( ["problem type", "filename"], value="problem type", label="Colour by", ) sample_btn = gr.Button("Demo on Sample Data", variant="primary", scale=0) sample_status = gr.Textbox(label="Status", lines=2, interactive=False) sample_plot = gr.Plot(label="Sample Embedding Space") sample_df = gr.DataFrame(label="Sample Coordinates", interactive=False) sample_btn.click( fn=use_sample_data, inputs=[sample_method, sample_label], outputs=[sample_plot, sample_df, sample_status], ) gr.Markdown( "---\n" "[Forge Homepage](https://skadio.github.io/forge/)" ) if __name__ == "__main__": demo.launch()