matgl-predictor / app.py
Hafnium49's picture
Migrate DGL->PyG: matgl==4.0.1 + M3GNet-PES-MatPES-PBE-2025.2 (old MP-2021 model retired/404; pin matgl to stop unpinned-dep drift)
b3d927b verified
Raw
History Blame Contribute Delete
4.12 kB
import os
import gradio as gr
import torch
import matgl
# 2026-06-05 migration: the previous model "M3GNet-MP-2021.2.8-PES" (DGL backend)
# was RETIRED from MatGL's hosting (404 everywhere) and MatGL dropped the DGL
# backend at 3.0.4 (now PyG-only). This Space now runs MatGL 4.x (PyG default) +
# the current MatPES-trained M3GNet PES model. matgl is PINNED in the Dockerfile
# to prevent the unpinned-dependency drift that broke this Space.
MODEL_NAME = "M3GNet-PES-MatPES-PBE-2025.2"
# Global model cache - load lazily to avoid startup timeout
_model = None
_model_error = None
def get_model():
"""Lazy load the M3GNet model on first use."""
global _model, _model_error
if _model_error:
return None, _model_error
if _model is None:
try:
print(f"Loading {MODEL_NAME} ...")
_model = matgl.load_model(MODEL_NAME)
print("Model loaded successfully.")
except Exception as e:
_model_error = str(e)
print(f"Failed to load model: {e}")
import traceback
traceback.print_exc()
return None, _model_error
return _model, None
def predict_properties(cif_string: str) -> dict:
"""
Predicts formation energy from a CIF string using M3GNet.
Uses ASE Calculator interface for energy calculation.
"""
if not cif_string or not cif_string.strip():
return {"status": "error", "message": "CIF string is empty"}
pot, error = get_model()
if error:
return {"status": "error", "message": f"Model load failed: {error}"}
try:
from pymatgen.core import Structure
from pymatgen.io.ase import AseAtomsAdaptor
from matgl.ext.ase import PESCalculator
# Parse CIF to pymatgen Structure
struct = Structure.from_str(cif_string, fmt="cif")
# Convert to ASE Atoms
adaptor = AseAtomsAdaptor()
atoms = adaptor.get_atoms(struct)
# Create calculator and attach to atoms
calc = PESCalculator(potential=pot)
atoms.calc = calc
# Get potential energy (total energy)
e_total = atoms.get_potential_energy()
e_per_atom = e_total / len(atoms)
return {
"status": "success",
"formation_energy_per_atom": round(e_per_atom, 4),
"unit": "eV/atom",
"formula": struct.composition.reduced_formula,
"num_atoms": len(struct),
"model": MODEL_NAME,
}
except Exception as e:
return {"status": "error", "message": str(e)}
def health_check() -> dict:
"""Health check endpoint."""
pot, error = get_model()
if error:
return {"status": "error", "message": error}
return {"status": "healthy", "model": MODEL_NAME}
# Create Gradio interface
with gr.Blocks() as demo:
gr.Markdown("# MatGL M3GNet Property Predictor")
gr.Markdown("Predict formation energy for crystal structures using M3GNet universal potential.")
gr.Markdown("**Note:** First prediction may take longer as the model loads on-demand.")
with gr.Tab("Predict"):
cif_input = gr.Textbox(
label="CIF Structure",
placeholder="Paste CIF content here...",
lines=10
)
predict_btn = gr.Button("Predict", variant="primary")
output = gr.JSON(label="Result")
predict_btn.click(predict_properties, inputs=cif_input, outputs=output)
gr.Markdown("### Example CIF (NaCl)")
gr.Markdown("""```
data_NaCl
_symmetry_space_group_name_H-M 'F m -3 m'
_cell_length_a 5.64
_cell_length_b 5.64
_cell_length_c 5.64
_cell_angle_alpha 90
_cell_angle_beta 90
_cell_angle_gamma 90
loop_
_atom_site_label
_atom_site_type_symbol
_atom_site_fract_x
_atom_site_fract_y
_atom_site_fract_z
Na1 Na 0.0 0.0 0.0
Cl1 Cl 0.5 0.5 0.5
```""")
with gr.Tab("Health"):
health_btn = gr.Button("Check Health")
health_output = gr.JSON()
health_btn.click(health_check, outputs=health_output)
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860)