Spaces:
Running
Running
fix sorting
Browse files
app.py
CHANGED
|
@@ -1,61 +1,68 @@
|
|
| 1 |
-
from pathlib import Path
|
| 2 |
import ast
|
| 3 |
import json
|
|
|
|
| 4 |
import traceback
|
| 5 |
-
import pandas as pd
|
| 6 |
-
import numpy as np
|
| 7 |
-
|
| 8 |
-
import gradio as gr
|
| 9 |
-
from datasets import Features, Value, load_dataset
|
| 10 |
from datetime import datetime
|
| 11 |
-
import
|
| 12 |
|
|
|
|
|
|
|
|
|
|
| 13 |
from about import (
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
)
|
|
|
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
}
|
|
|
|
| 59 |
|
| 60 |
|
| 61 |
def get_leaderboard():
|
|
@@ -80,15 +87,46 @@ def get_leaderboard():
|
|
| 80 |
return full_df
|
| 81 |
|
| 82 |
|
| 83 |
-
def
|
| 84 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
if len(df) == 0:
|
| 86 |
-
return df
|
| 87 |
|
| 88 |
selected_cols = ["model_name"]
|
| 89 |
|
| 90 |
if compact_view:
|
| 91 |
from about import COMPACT_VIEW_COLUMNS
|
|
|
|
| 92 |
selected_cols = [col for col in COMPACT_VIEW_COLUMNS if col in df.columns]
|
| 93 |
else:
|
| 94 |
if "training_set" in df.columns:
|
|
@@ -120,7 +158,9 @@ def format_dataframe(df, show_percentage=False, selected_groups=None, compact_vi
|
|
| 120 |
if "paper_link" in df.columns:
|
| 121 |
paper_val = row.get("paper_link", None)
|
| 122 |
if paper_val and isinstance(paper_val, str) and paper_val.strip():
|
| 123 |
-
symbols.append(
|
|
|
|
|
|
|
| 124 |
|
| 125 |
if "relaxed" in df.columns and row.get("relaxed", False):
|
| 126 |
symbols.append("β‘")
|
|
@@ -141,6 +181,7 @@ def format_dataframe(df, show_percentage=False, selected_groups=None, compact_vi
|
|
| 141 |
display_df["model_name"] = df.apply(add_model_symbols, axis=1)
|
| 142 |
|
| 143 |
if "training_set" in display_df.columns:
|
|
|
|
| 144 |
def format_training_set(val):
|
| 145 |
if val is None or (isinstance(val, float) and np.isnan(val)):
|
| 146 |
return ""
|
|
@@ -151,13 +192,20 @@ def format_dataframe(df, show_percentage=False, selected_groups=None, compact_vi
|
|
| 151 |
val = val.replace("'", "").replace('"', "")
|
| 152 |
return val
|
| 153 |
|
| 154 |
-
display_df["training_set"] = display_df["training_set"].apply(
|
|
|
|
|
|
|
| 155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
if show_percentage and "n_structures" in df.columns:
|
| 157 |
n_structures = df["n_structures"]
|
| 158 |
for col in COUNT_BASED_METRICS:
|
| 159 |
if col in display_df.columns:
|
| 160 |
-
display_df[col] = (df[col] / n_structures * 100).round(1)
|
| 161 |
|
| 162 |
for col in display_df.columns:
|
| 163 |
if display_df[col].dtype in ["float64", "float32"]:
|
|
@@ -165,14 +213,26 @@ def format_dataframe(df, show_percentage=False, selected_groups=None, compact_vi
|
|
| 165 |
|
| 166 |
baseline_indices = set()
|
| 167 |
if "notes" in df.columns:
|
| 168 |
-
is_baseline =
|
|
|
|
|
|
|
| 169 |
non_baseline_df = display_df[~is_baseline]
|
| 170 |
baseline_df = display_df[is_baseline]
|
| 171 |
display_df = pd.concat([non_baseline_df, baseline_df]).reset_index(drop=True)
|
| 172 |
baseline_indices = set(range(len(non_baseline_df), len(display_df)))
|
| 173 |
|
| 174 |
-
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
|
| 177 |
|
| 178 |
def apply_color_styling(display_df, original_cols, baseline_indices=None):
|
|
@@ -214,16 +274,31 @@ def parse_training_set(val):
|
|
| 214 |
return []
|
| 215 |
|
| 216 |
|
| 217 |
-
def update_leaderboard(
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
df_to_format = cached_df.copy()
|
| 220 |
|
| 221 |
ALWAYS_SHOW_MODELS = {"AFLOW", "Alexandria", "OQMD"}
|
| 222 |
-
if
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
|
|
|
|
|
|
|
|
|
| 227 |
df_to_format = df_to_format[mask]
|
| 228 |
|
| 229 |
if sort_by and sort_by != "None":
|
|
@@ -232,16 +307,33 @@ def update_leaderboard(show_percentage, selected_groups, compact_view, cached_df
|
|
| 232 |
|
| 233 |
if raw_column_name in df_to_format.columns:
|
| 234 |
ascending = sort_direction == "Ascending"
|
| 235 |
-
df_to_format = df_to_format.sort_values(
|
|
|
|
|
|
|
| 236 |
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
| 238 |
|
| 239 |
|
| 240 |
def show_output_box(message):
|
| 241 |
return gr.update(value=message, visible=True)
|
| 242 |
|
| 243 |
|
| 244 |
-
def submit_cif_files(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
"""Submit structures to the leaderboard."""
|
| 246 |
from huggingface_hub import upload_file
|
| 247 |
|
|
@@ -269,9 +361,13 @@ def submit_cif_files(model_name, problem_type, cif_files, relaxed, relaxation_se
|
|
| 269 |
"model_name": model_name.strip(),
|
| 270 |
"problem_type": problem_type,
|
| 271 |
"relaxed": relaxed,
|
| 272 |
-
"relaxation_settings": relaxation_settings.strip()
|
|
|
|
|
|
|
| 273 |
"training_datasets": training_datasets or [],
|
| 274 |
-
"training_dataset_other": training_dataset_other.strip()
|
|
|
|
|
|
|
| 275 |
"paper_link": paper_link.strip() if paper_link else None,
|
| 276 |
"hf_model_link": hf_model_link.strip() if hf_model_link else None,
|
| 277 |
"email": email.strip(),
|
|
@@ -308,7 +404,11 @@ def submit_cif_files(model_name, problem_type, cif_files, relaxed, relaxation_se
|
|
| 308 |
|
| 309 |
os.unlink(temp_metadata_path)
|
| 310 |
|
| 311 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
|
| 313 |
except Exception as e:
|
| 314 |
return f"Error during submission: {str(e)}", None
|
|
@@ -317,20 +417,36 @@ def submit_cif_files(model_name, problem_type, cif_files, relaxed, relaxation_se
|
|
| 317 |
def generate_metric_legend_html():
|
| 318 |
"""Generate HTML table with color-coded metric group legend."""
|
| 319 |
metric_details = {
|
| 320 |
-
"Validity β": (
|
|
|
|
|
|
|
|
|
|
| 321 |
"Uniqueness & Novelty β": ("Unique, Novel", "β Higher is better"),
|
| 322 |
-
"Energy Metrics β": (
|
|
|
|
|
|
|
|
|
|
| 323 |
"Stability β": ("Stable, Unique in Stable, SUN", "β Higher is better"),
|
| 324 |
-
"Metastability β": (
|
|
|
|
|
|
|
|
|
|
| 325 |
"Distribution β": ("JS Distance, MMD, FID", "β Lower is better"),
|
| 326 |
-
"Diversity β": (
|
|
|
|
|
|
|
|
|
|
| 327 |
"HHI β": ("HHI Production, HHI Reserve", "β Lower is better"),
|
| 328 |
}
|
| 329 |
|
| 330 |
html = '<table style="width: 100%; border-collapse: collapse;">'
|
| 331 |
html += "<thead><tr>"
|
| 332 |
-
html +=
|
| 333 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
html += '<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Metrics</th>'
|
| 335 |
html += '<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Direction</th>'
|
| 336 |
html += "</tr></thead><tbody>"
|
|
@@ -338,9 +454,12 @@ def generate_metric_legend_html():
|
|
| 338 |
for group, color in METRIC_GROUP_COLORS.items():
|
| 339 |
metrics, direction = metric_details.get(group, ("", ""))
|
| 340 |
group_name = group.replace("β", "").replace("β", "").strip()
|
| 341 |
-
|
| 342 |
html += "<tr>"
|
| 343 |
-
html +=
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
html += f'<td style="border: 1px solid #ddd; padding: 8px;"><strong>{group_name}</strong></td>'
|
| 345 |
html += f'<td style="border: 1px solid #ddd; padding: 8px;">{metrics}</td>'
|
| 346 |
html += f'<td style="border: 1px solid #ddd; padding: 8px;">{direction}</td>'
|
|
@@ -352,7 +471,8 @@ def generate_metric_legend_html():
|
|
| 352 |
|
| 353 |
def gradio_interface() -> gr.Blocks:
|
| 354 |
with gr.Blocks() as demo:
|
| 355 |
-
gr.Markdown(
|
|
|
|
| 356 |
# π¬ LeMat-GenBench: A Unified Benchmark for Generative Models of Crystalline Materials
|
| 357 |
|
| 358 |
Generative machine learning models hold great promise for accelerating materials discovery, particularly through the inverse design of inorganic crystals, enabling an unprecedented exploration of chemical space. Yet, the lack of standardized evaluation frameworks makes it difficult to evaluate, compare and further develop these ML models meaningfully.
|
|
@@ -360,7 +480,8 @@ Generative machine learning models hold great promise for accelerating materials
|
|
| 360 |
**LeMat-GenBench** introduces a unified benchmark for generative models of crystalline materials, with standardized evaluation metrics for meaningful model comparison, diverse tasks, and this leaderboard to encourage and track community progress.
|
| 361 |
|
| 362 |
π **Paper**: [arXiv](https://arxiv.org/abs/2512.04562) | π» **Code**: [GitHub](https://github.com/LeMaterial/lemat-genbench) | π§ **Contact**: siddharth.betala [at] entalpic.ai, alexandre.duval [at] entalpic.ai
|
| 363 |
-
"""
|
|
|
|
| 364 |
|
| 365 |
with gr.Tabs(elem_classes="tab-buttons"):
|
| 366 |
with gr.TabItem("π Leaderboard", elem_id="boundary-benchmark-tab-table"):
|
|
@@ -421,11 +542,13 @@ Generative machine learning models hold great promise for accelerating materials
|
|
| 421 |
|
| 422 |
ALWAYS_SHOW_MODELS = {"AFLOW", "Alexandria", "OQMD"}
|
| 423 |
filtered_initial_df = initial_df[
|
| 424 |
-
initial_df["training_set"].apply(
|
|
|
|
|
|
|
| 425 |
| initial_df["model_name"].isin(ALWAYS_SHOW_MODELS)
|
| 426 |
]
|
| 427 |
|
| 428 |
-
formatted_df = format_dataframe(
|
| 429 |
filtered_initial_df,
|
| 430 |
show_percentage=True,
|
| 431 |
selected_groups=list(METRIC_GROUPS.keys()),
|
|
@@ -443,8 +566,11 @@ Generative machine learning models hold great promise for accelerating materials
|
|
| 443 |
value=formatted_df,
|
| 444 |
interactive=False,
|
| 445 |
wrap=True,
|
| 446 |
-
datatype=
|
| 447 |
-
column_widths=["180px"]
|
|
|
|
|
|
|
|
|
|
| 448 |
show_fullscreen_button=True,
|
| 449 |
)
|
| 450 |
|
|
@@ -458,18 +584,33 @@ Generative machine learning models hold great promise for accelerating materials
|
|
| 458 |
training_set_filter,
|
| 459 |
]
|
| 460 |
|
| 461 |
-
show_percentage.change(
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
|
| 468 |
except Exception as e:
|
| 469 |
traceback.print_exc()
|
| 470 |
-
gr.Markdown(
|
|
|
|
|
|
|
| 471 |
|
| 472 |
-
gr.Markdown(
|
|
|
|
| 473 |
**Symbol Legend:**
|
| 474 |
- π Paper available (click to view)
|
| 475 |
- β
Model output verified
|
|
@@ -479,14 +620,17 @@ Generative machine learning models hold great promise for accelerating materials
|
|
| 479 |
|
| 480 |
Verified submissions mean the results came from a model submission rather than a CIF submission.
|
| 481 |
|
| 482 |
-
Models marked as baselines appear below
|
| 483 |
-
"""
|
|
|
|
| 484 |
|
| 485 |
with gr.TabItem("βοΈ Submit", elem_id="boundary-benchmark-tab-table"):
|
| 486 |
-
gr.Markdown(
|
|
|
|
| 487 |
# Materials Submission
|
| 488 |
Upload a ZIP of CIFs with your structures. To ensure eligibility for the leaderboard, please provide exactly 2,500 representative structures.
|
| 489 |
-
"""
|
|
|
|
| 490 |
filename = gr.State(value=None)
|
| 491 |
|
| 492 |
gr.LoginButton()
|
|
@@ -516,7 +660,9 @@ Upload a ZIP of CIFs with your structures. To ensure eligibility for the leaderb
|
|
| 516 |
problem_type = gr.Dropdown(PROBLEM_TYPES, label="Problem Type")
|
| 517 |
|
| 518 |
with gr.Column():
|
| 519 |
-
cif_file = gr.File(
|
|
|
|
|
|
|
| 520 |
relaxed = gr.Checkbox(
|
| 521 |
value=False,
|
| 522 |
label="Structures are pre-relaxed",
|
|
@@ -548,7 +694,9 @@ Upload a ZIP of CIFs with your structures. To ensure eligibility for the leaderb
|
|
| 548 |
)
|
| 549 |
|
| 550 |
training_dataset_input.change(
|
| 551 |
-
fn=lambda x: gr.update(
|
|
|
|
|
|
|
| 552 |
inputs=[training_dataset_input],
|
| 553 |
outputs=[training_dataset_other_input],
|
| 554 |
)
|
|
@@ -585,4 +733,4 @@ Upload a ZIP of CIFs with your structures. To ensure eligibility for the leaderb
|
|
| 585 |
|
| 586 |
|
| 587 |
if __name__ == "__main__":
|
| 588 |
-
gradio_interface().launch(show_error=True)
|
|
|
|
|
|
|
| 1 |
import ast
|
| 2 |
import json
|
| 3 |
+
import os
|
| 4 |
import traceback
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
from datetime import datetime
|
| 6 |
+
from pathlib import Path
|
| 7 |
|
| 8 |
+
import gradio as gr
|
| 9 |
+
import numpy as np
|
| 10 |
+
import pandas as pd
|
| 11 |
from about import (
|
| 12 |
+
COLUMN_DISPLAY_NAMES,
|
| 13 |
+
COLUMN_TO_GROUP,
|
| 14 |
+
COUNT_BASED_METRICS,
|
| 15 |
+
METRIC_GROUP_COLORS,
|
| 16 |
+
METRIC_GROUPS,
|
| 17 |
+
PROBLEM_TYPES,
|
| 18 |
+
TOKEN,
|
| 19 |
+
TRAINING_DATASETS,
|
| 20 |
+
results_repo,
|
| 21 |
+
submissions_repo,
|
| 22 |
)
|
| 23 |
+
from datasets import Features, Value, load_dataset
|
| 24 |
|
| 25 |
+
RESULT_FEATURES = Features(
|
| 26 |
+
{
|
| 27 |
+
"run_name": Value("string"),
|
| 28 |
+
"timestamp": Value("string"),
|
| 29 |
+
"n_structures": Value("int64"),
|
| 30 |
+
"overall_valid_count": Value("int64"),
|
| 31 |
+
"charge_neutral_count": Value("int64"),
|
| 32 |
+
"distance_valid_count": Value("int64"),
|
| 33 |
+
"plausibility_valid_count": Value("int64"),
|
| 34 |
+
"unique_count": Value("int64"),
|
| 35 |
+
"novel_count": Value("int64"),
|
| 36 |
+
"mean_formation_energy": Value("float64"),
|
| 37 |
+
"formation_energy_std": Value("float64"),
|
| 38 |
+
"stability_mean_above_hull": Value("float64"),
|
| 39 |
+
"stability_std_e_above_hull": Value("float64"),
|
| 40 |
+
"stability_mean_ensemble_std": Value("float64"),
|
| 41 |
+
"mean_relaxation_RMSD": Value("float64"),
|
| 42 |
+
"relaxation_RMSE_std": Value("float64"),
|
| 43 |
+
"stable_count": Value("int64"),
|
| 44 |
+
"unique_in_stable_count": Value("int64"),
|
| 45 |
+
"sun_count": Value("int64"),
|
| 46 |
+
"metastable_count": Value("int64"),
|
| 47 |
+
"unique_in_metastable_count": Value("int64"),
|
| 48 |
+
"msun_count": Value("int64"),
|
| 49 |
+
"JSDistance": Value("float64"),
|
| 50 |
+
"MMD": Value("float64"),
|
| 51 |
+
"FrechetDistance": Value("float64"),
|
| 52 |
+
"element_diversity": Value("float64"),
|
| 53 |
+
"space_group_diversity": Value("float64"),
|
| 54 |
+
"site_diversity": Value("float64"),
|
| 55 |
+
"physical_size_diversity": Value("float64"),
|
| 56 |
+
"hhi_production_mean": Value("float64"),
|
| 57 |
+
"hhi_reserve_mean": Value("float64"),
|
| 58 |
+
"hhi_combined_mean": Value("float64"),
|
| 59 |
+
"model_name": Value("string"),
|
| 60 |
+
"relaxed": Value("bool"),
|
| 61 |
+
"training_set": Value("string"),
|
| 62 |
+
"paper_link": Value("string"),
|
| 63 |
+
"notes": Value("string"),
|
| 64 |
+
}
|
| 65 |
+
)
|
| 66 |
|
| 67 |
|
| 68 |
def get_leaderboard():
|
|
|
|
| 87 |
return full_df
|
| 88 |
|
| 89 |
|
| 90 |
+
def get_display_datatypes(display_df, original_cols):
|
| 91 |
+
"""Return an explicit per-column datatype list for gr.Dataframe.
|
| 92 |
+
|
| 93 |
+
Numeric metric columns are marked as "number" so Gradio's header-click
|
| 94 |
+
sorting compares them numerically instead of lexicographically. The model
|
| 95 |
+
column stays "html" (it contains links/symbols) and training_set stays
|
| 96 |
+
"str".
|
| 97 |
+
"""
|
| 98 |
+
datatypes = []
|
| 99 |
+
for i in range(len(display_df.columns)):
|
| 100 |
+
original_col = original_cols[i] if i < len(original_cols) else None
|
| 101 |
+
if original_col == "model_name":
|
| 102 |
+
datatypes.append("html")
|
| 103 |
+
elif original_col == "training_set":
|
| 104 |
+
datatypes.append("str")
|
| 105 |
+
elif original_col == "run_name":
|
| 106 |
+
datatypes.append("str")
|
| 107 |
+
else:
|
| 108 |
+
# Every metric column (counts, percentages, energies, distances,
|
| 109 |
+
# diversity, HHI, ...) is numeric -> numeric sort.
|
| 110 |
+
datatypes.append("number")
|
| 111 |
+
return datatypes
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def format_dataframe(
|
| 115 |
+
df, show_percentage=False, selected_groups=None, compact_view=True
|
| 116 |
+
):
|
| 117 |
+
"""Format the dataframe with proper column names and optional percentages.
|
| 118 |
+
|
| 119 |
+
Returns a tuple of (styler, datatypes) so the caller can pass an explicit
|
| 120 |
+
datatype list to gr.Dataframe for correct numeric sorting.
|
| 121 |
+
"""
|
| 122 |
if len(df) == 0:
|
| 123 |
+
return df, None
|
| 124 |
|
| 125 |
selected_cols = ["model_name"]
|
| 126 |
|
| 127 |
if compact_view:
|
| 128 |
from about import COMPACT_VIEW_COLUMNS
|
| 129 |
+
|
| 130 |
selected_cols = [col for col in COMPACT_VIEW_COLUMNS if col in df.columns]
|
| 131 |
else:
|
| 132 |
if "training_set" in df.columns:
|
|
|
|
| 158 |
if "paper_link" in df.columns:
|
| 159 |
paper_val = row.get("paper_link", None)
|
| 160 |
if paper_val and isinstance(paper_val, str) and paper_val.strip():
|
| 161 |
+
symbols.append(
|
| 162 |
+
f'<a href="{paper_val.strip()}" target="_blank">π</a>'
|
| 163 |
+
)
|
| 164 |
|
| 165 |
if "relaxed" in df.columns and row.get("relaxed", False):
|
| 166 |
symbols.append("β‘")
|
|
|
|
| 181 |
display_df["model_name"] = df.apply(add_model_symbols, axis=1)
|
| 182 |
|
| 183 |
if "training_set" in display_df.columns:
|
| 184 |
+
|
| 185 |
def format_training_set(val):
|
| 186 |
if val is None or (isinstance(val, float) and np.isnan(val)):
|
| 187 |
return ""
|
|
|
|
| 192 |
val = val.replace("'", "").replace('"', "")
|
| 193 |
return val
|
| 194 |
|
| 195 |
+
display_df["training_set"] = display_df["training_set"].apply(
|
| 196 |
+
format_training_set
|
| 197 |
+
)
|
| 198 |
|
| 199 |
+
# --- Percentage handling -------------------------------------------------
|
| 200 |
+
# IMPORTANT: keep count-based metrics NUMERIC (do not append a "%" string).
|
| 201 |
+
# Appending "%" turns the column into strings, which makes Gradio's
|
| 202 |
+
# header-click sorting lexicographic (e.g. 5.0 -> 3.1 -> 22.6). We instead
|
| 203 |
+
# store the numeric percentage and move the "%" indicator to the header.
|
| 204 |
if show_percentage and "n_structures" in df.columns:
|
| 205 |
n_structures = df["n_structures"]
|
| 206 |
for col in COUNT_BASED_METRICS:
|
| 207 |
if col in display_df.columns:
|
| 208 |
+
display_df[col] = (df[col] / n_structures * 100).round(1)
|
| 209 |
|
| 210 |
for col in display_df.columns:
|
| 211 |
if display_df[col].dtype in ["float64", "float32"]:
|
|
|
|
| 213 |
|
| 214 |
baseline_indices = set()
|
| 215 |
if "notes" in df.columns:
|
| 216 |
+
is_baseline = (
|
| 217 |
+
df["notes"].fillna("").str.contains("baseline", case=False, na=False)
|
| 218 |
+
)
|
| 219 |
non_baseline_df = display_df[~is_baseline]
|
| 220 |
baseline_df = display_df[is_baseline]
|
| 221 |
display_df = pd.concat([non_baseline_df, baseline_df]).reset_index(drop=True)
|
| 222 |
baseline_indices = set(range(len(non_baseline_df), len(display_df)))
|
| 223 |
|
| 224 |
+
datatypes = get_display_datatypes(display_df, selected_cols)
|
| 225 |
+
|
| 226 |
+
# Move the "%" indicator into the header so cells stay numeric & sortable.
|
| 227 |
+
rename_map = dict(COLUMN_DISPLAY_NAMES)
|
| 228 |
+
if show_percentage:
|
| 229 |
+
for col in COUNT_BASED_METRICS:
|
| 230 |
+
if col in rename_map:
|
| 231 |
+
rename_map[col] = f"{rename_map[col]} (%)"
|
| 232 |
+
|
| 233 |
+
display_df = display_df.rename(columns=rename_map)
|
| 234 |
+
styler = apply_color_styling(display_df, selected_cols, baseline_indices)
|
| 235 |
+
return styler, datatypes
|
| 236 |
|
| 237 |
|
| 238 |
def apply_color_styling(display_df, original_cols, baseline_indices=None):
|
|
|
|
| 274 |
return []
|
| 275 |
|
| 276 |
|
| 277 |
+
def update_leaderboard(
|
| 278 |
+
show_percentage,
|
| 279 |
+
selected_groups,
|
| 280 |
+
compact_view,
|
| 281 |
+
cached_df,
|
| 282 |
+
sort_by,
|
| 283 |
+
sort_direction,
|
| 284 |
+
training_set_filter,
|
| 285 |
+
):
|
| 286 |
+
"""Update the leaderboard based on user selections.
|
| 287 |
+
|
| 288 |
+
Sorting is performed here on the RAW numeric dataframe (before any string
|
| 289 |
+
formatting), so the "Sort By" dropdown is always numerically correct.
|
| 290 |
+
"""
|
| 291 |
df_to_format = cached_df.copy()
|
| 292 |
|
| 293 |
ALWAYS_SHOW_MODELS = {"AFLOW", "Alexandria", "OQMD"}
|
| 294 |
+
if (
|
| 295 |
+
training_set_filter
|
| 296 |
+
and training_set_filter != "All"
|
| 297 |
+
and "training_set" in df_to_format.columns
|
| 298 |
+
):
|
| 299 |
+
mask = df_to_format["training_set"].apply(
|
| 300 |
+
lambda x: training_set_filter in parse_training_set(x)
|
| 301 |
+
) | df_to_format["model_name"].isin(ALWAYS_SHOW_MODELS)
|
| 302 |
df_to_format = df_to_format[mask]
|
| 303 |
|
| 304 |
if sort_by and sort_by != "None":
|
|
|
|
| 307 |
|
| 308 |
if raw_column_name in df_to_format.columns:
|
| 309 |
ascending = sort_direction == "Ascending"
|
| 310 |
+
df_to_format = df_to_format.sort_values(
|
| 311 |
+
by=raw_column_name, ascending=ascending
|
| 312 |
+
)
|
| 313 |
|
| 314 |
+
styler, _ = format_dataframe(
|
| 315 |
+
df_to_format, show_percentage, selected_groups, compact_view
|
| 316 |
+
)
|
| 317 |
+
return styler
|
| 318 |
|
| 319 |
|
| 320 |
def show_output_box(message):
|
| 321 |
return gr.update(value=message, visible=True)
|
| 322 |
|
| 323 |
|
| 324 |
+
def submit_cif_files(
|
| 325 |
+
model_name,
|
| 326 |
+
problem_type,
|
| 327 |
+
cif_files,
|
| 328 |
+
relaxed,
|
| 329 |
+
relaxation_settings,
|
| 330 |
+
training_datasets,
|
| 331 |
+
training_dataset_other,
|
| 332 |
+
paper_link,
|
| 333 |
+
hf_model_link,
|
| 334 |
+
email,
|
| 335 |
+
profile: gr.OAuthProfile | None,
|
| 336 |
+
):
|
| 337 |
"""Submit structures to the leaderboard."""
|
| 338 |
from huggingface_hub import upload_file
|
| 339 |
|
|
|
|
| 361 |
"model_name": model_name.strip(),
|
| 362 |
"problem_type": problem_type,
|
| 363 |
"relaxed": relaxed,
|
| 364 |
+
"relaxation_settings": relaxation_settings.strip()
|
| 365 |
+
if relaxed and relaxation_settings
|
| 366 |
+
else None,
|
| 367 |
"training_datasets": training_datasets or [],
|
| 368 |
+
"training_dataset_other": training_dataset_other.strip()
|
| 369 |
+
if training_dataset_other
|
| 370 |
+
else None,
|
| 371 |
"paper_link": paper_link.strip() if paper_link else None,
|
| 372 |
"hf_model_link": hf_model_link.strip() if hf_model_link else None,
|
| 373 |
"email": email.strip(),
|
|
|
|
| 404 |
|
| 405 |
os.unlink(temp_metadata_path)
|
| 406 |
|
| 407 |
+
return (
|
| 408 |
+
f"Success! Submitted {model_name} for {problem_type} evaluation. "
|
| 409 |
+
f"Submission ID: {submission_id}",
|
| 410 |
+
submission_id,
|
| 411 |
+
)
|
| 412 |
|
| 413 |
except Exception as e:
|
| 414 |
return f"Error during submission: {str(e)}", None
|
|
|
|
| 417 |
def generate_metric_legend_html():
|
| 418 |
"""Generate HTML table with color-coded metric group legend."""
|
| 419 |
metric_details = {
|
| 420 |
+
"Validity β": (
|
| 421 |
+
"Valid, Charge Neutral, Distance Valid, Plausibility Valid",
|
| 422 |
+
"β Higher is better",
|
| 423 |
+
),
|
| 424 |
"Uniqueness & Novelty β": ("Unique, Novel", "β Higher is better"),
|
| 425 |
+
"Energy Metrics β": (
|
| 426 |
+
"E Above Hull, Formation Energy, Relaxation RMSD (with std)",
|
| 427 |
+
"β Lower is better",
|
| 428 |
+
),
|
| 429 |
"Stability β": ("Stable, Unique in Stable, SUN", "β Higher is better"),
|
| 430 |
+
"Metastability β": (
|
| 431 |
+
"Metastable, Unique in Metastable, MSUN",
|
| 432 |
+
"β Higher is better",
|
| 433 |
+
),
|
| 434 |
"Distribution β": ("JS Distance, MMD, FID", "β Lower is better"),
|
| 435 |
+
"Diversity β": (
|
| 436 |
+
"Element, Space Group, Atomic Site, Crystal Size",
|
| 437 |
+
"β Higher is better",
|
| 438 |
+
),
|
| 439 |
"HHI β": ("HHI Production, HHI Reserve", "β Lower is better"),
|
| 440 |
}
|
| 441 |
|
| 442 |
html = '<table style="width: 100%; border-collapse: collapse;">'
|
| 443 |
html += "<thead><tr>"
|
| 444 |
+
html += (
|
| 445 |
+
'<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Color</th>'
|
| 446 |
+
)
|
| 447 |
+
html += (
|
| 448 |
+
'<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Group</th>'
|
| 449 |
+
)
|
| 450 |
html += '<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Metrics</th>'
|
| 451 |
html += '<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Direction</th>'
|
| 452 |
html += "</tr></thead><tbody>"
|
|
|
|
| 454 |
for group, color in METRIC_GROUP_COLORS.items():
|
| 455 |
metrics, direction = metric_details.get(group, ("", ""))
|
| 456 |
group_name = group.replace("β", "").replace("β", "").strip()
|
|
|
|
| 457 |
html += "<tr>"
|
| 458 |
+
html += (
|
| 459 |
+
f'<td style="border: 1px solid #ddd; padding: 8px;">'
|
| 460 |
+
f'<div style="width: 30px; height: 20px; background-color: {color}; '
|
| 461 |
+
f'border: 1px solid #999;"></div></td>'
|
| 462 |
+
)
|
| 463 |
html += f'<td style="border: 1px solid #ddd; padding: 8px;"><strong>{group_name}</strong></td>'
|
| 464 |
html += f'<td style="border: 1px solid #ddd; padding: 8px;">{metrics}</td>'
|
| 465 |
html += f'<td style="border: 1px solid #ddd; padding: 8px;">{direction}</td>'
|
|
|
|
| 471 |
|
| 472 |
def gradio_interface() -> gr.Blocks:
|
| 473 |
with gr.Blocks() as demo:
|
| 474 |
+
gr.Markdown(
|
| 475 |
+
"""
|
| 476 |
# π¬ LeMat-GenBench: A Unified Benchmark for Generative Models of Crystalline Materials
|
| 477 |
|
| 478 |
Generative machine learning models hold great promise for accelerating materials discovery, particularly through the inverse design of inorganic crystals, enabling an unprecedented exploration of chemical space. Yet, the lack of standardized evaluation frameworks makes it difficult to evaluate, compare and further develop these ML models meaningfully.
|
|
|
|
| 480 |
**LeMat-GenBench** introduces a unified benchmark for generative models of crystalline materials, with standardized evaluation metrics for meaningful model comparison, diverse tasks, and this leaderboard to encourage and track community progress.
|
| 481 |
|
| 482 |
π **Paper**: [arXiv](https://arxiv.org/abs/2512.04562) | π» **Code**: [GitHub](https://github.com/LeMaterial/lemat-genbench) | π§ **Contact**: siddharth.betala [at] entalpic.ai, alexandre.duval [at] entalpic.ai
|
| 483 |
+
"""
|
| 484 |
+
)
|
| 485 |
|
| 486 |
with gr.Tabs(elem_classes="tab-buttons"):
|
| 487 |
with gr.TabItem("π Leaderboard", elem_id="boundary-benchmark-tab-table"):
|
|
|
|
| 542 |
|
| 543 |
ALWAYS_SHOW_MODELS = {"AFLOW", "Alexandria", "OQMD"}
|
| 544 |
filtered_initial_df = initial_df[
|
| 545 |
+
initial_df["training_set"].apply(
|
| 546 |
+
lambda x: "MP-20" in parse_training_set(x)
|
| 547 |
+
)
|
| 548 |
| initial_df["model_name"].isin(ALWAYS_SHOW_MODELS)
|
| 549 |
]
|
| 550 |
|
| 551 |
+
formatted_df, formatted_datatypes = format_dataframe(
|
| 552 |
filtered_initial_df,
|
| 553 |
show_percentage=True,
|
| 554 |
selected_groups=list(METRIC_GROUPS.keys()),
|
|
|
|
| 566 |
value=formatted_df,
|
| 567 |
interactive=False,
|
| 568 |
wrap=True,
|
| 569 |
+
datatype=formatted_datatypes if formatted_datatypes else None,
|
| 570 |
+
column_widths=["180px"]
|
| 571 |
+
+ ["160px"] * (len(formatted_columns) - 1)
|
| 572 |
+
if formatted_columns
|
| 573 |
+
else None,
|
| 574 |
show_fullscreen_button=True,
|
| 575 |
)
|
| 576 |
|
|
|
|
| 584 |
training_set_filter,
|
| 585 |
]
|
| 586 |
|
| 587 |
+
show_percentage.change(
|
| 588 |
+
fn=update_leaderboard, inputs=inputs, outputs=leaderboard_table
|
| 589 |
+
)
|
| 590 |
+
selected_groups.change(
|
| 591 |
+
fn=update_leaderboard, inputs=inputs, outputs=leaderboard_table
|
| 592 |
+
)
|
| 593 |
+
compact_view.change(
|
| 594 |
+
fn=update_leaderboard, inputs=inputs, outputs=leaderboard_table
|
| 595 |
+
)
|
| 596 |
+
sort_by.change(
|
| 597 |
+
fn=update_leaderboard, inputs=inputs, outputs=leaderboard_table
|
| 598 |
+
)
|
| 599 |
+
sort_direction.change(
|
| 600 |
+
fn=update_leaderboard, inputs=inputs, outputs=leaderboard_table
|
| 601 |
+
)
|
| 602 |
+
training_set_filter.change(
|
| 603 |
+
fn=update_leaderboard, inputs=inputs, outputs=leaderboard_table
|
| 604 |
+
)
|
| 605 |
|
| 606 |
except Exception as e:
|
| 607 |
traceback.print_exc()
|
| 608 |
+
gr.Markdown(
|
| 609 |
+
f"Leaderboard is empty or error loading: {type(e).__name__}: {str(e)}"
|
| 610 |
+
)
|
| 611 |
|
| 612 |
+
gr.Markdown(
|
| 613 |
+
"""
|
| 614 |
**Symbol Legend:**
|
| 615 |
- π Paper available (click to view)
|
| 616 |
- β
Model output verified
|
|
|
|
| 620 |
|
| 621 |
Verified submissions mean the results came from a model submission rather than a CIF submission.
|
| 622 |
|
| 623 |
+
Models marked as baselines appear below a separator line at the bottom of the table.
|
| 624 |
+
"""
|
| 625 |
+
)
|
| 626 |
|
| 627 |
with gr.TabItem("βοΈ Submit", elem_id="boundary-benchmark-tab-table"):
|
| 628 |
+
gr.Markdown(
|
| 629 |
+
"""
|
| 630 |
# Materials Submission
|
| 631 |
Upload a ZIP of CIFs with your structures. To ensure eligibility for the leaderboard, please provide exactly 2,500 representative structures.
|
| 632 |
+
"""
|
| 633 |
+
)
|
| 634 |
filename = gr.State(value=None)
|
| 635 |
|
| 636 |
gr.LoginButton()
|
|
|
|
| 660 |
problem_type = gr.Dropdown(PROBLEM_TYPES, label="Problem Type")
|
| 661 |
|
| 662 |
with gr.Column():
|
| 663 |
+
cif_file = gr.File(
|
| 664 |
+
label="Upload a CSV, a pkl, or a ZIP of CIF files."
|
| 665 |
+
)
|
| 666 |
relaxed = gr.Checkbox(
|
| 667 |
value=False,
|
| 668 |
label="Structures are pre-relaxed",
|
|
|
|
| 694 |
)
|
| 695 |
|
| 696 |
training_dataset_input.change(
|
| 697 |
+
fn=lambda x: gr.update(
|
| 698 |
+
visible="Others (must specify)" in (x or [])
|
| 699 |
+
),
|
| 700 |
inputs=[training_dataset_input],
|
| 701 |
outputs=[training_dataset_other_input],
|
| 702 |
)
|
|
|
|
| 733 |
|
| 734 |
|
| 735 |
if __name__ == "__main__":
|
| 736 |
+
gradio_interface().launch(show_error=True)
|