from pathlib import Path
import numpy as np
import pandas as pd
import plotly.colors as pcolors
import plotly.express as px
import plotly.graph_objects as go
import streamlit as st
from mlip_arena.models import REGISTRY as MODELS
DATA_DIR = Path("mlip_arena/tasks/combustion")
st.markdown("""
# Combustion
""")
st.markdown("### Methods")
container = st.container(border=True)
valid_models = [
model
for model, metadata in MODELS.items()
if Path(__file__).stem in metadata.get("gpu-tasks", [])
]
models = container.multiselect(
"MLIPs",
valid_models,
[
"MACE-MP(M)",
"CHGNet",
"M3GNet",
"SevenNet",
"ORB",
"EquiformerV2(OC22)",
"eSCN(OC20)",
],
)
st.markdown("### Settings")
vis = st.container(border=True)
# Get all attributes from pcolors.qualitative
all_attributes = dir(pcolors.qualitative)
color_palettes = {
attr: getattr(pcolors.qualitative, attr)
for attr in all_attributes
if isinstance(getattr(pcolors.qualitative, attr), list)
}
color_palettes.pop("__all__", None)
palette_names = list(color_palettes.keys())
palette_colors = list(color_palettes.values())
palette_name = vis.selectbox("Color sequence", options=palette_names, index=22)
color_sequence = color_palettes[palette_name]
if not models:
st.stop()
@st.cache_data
def get_data(models):
families = [MODELS[str(model)]["family"] for model in models]
dfs = [
pd.read_json(DATA_DIR / family.lower() / "hydrogen.json") for family in families
]
df = pd.concat(dfs, ignore_index=True)
df.drop_duplicates(inplace=True, subset=["formula", "method"])
return df
df = get_data(models)
method_color_mapping = {
method: color_sequence[i % len(color_sequence)]
for i, method in enumerate(df["method"].unique())
}
###
# Number of products
fig = go.Figure()
for method in df["method"].unique():
row = df[df["method"] == method].iloc[0]
fig.add_trace(
go.Scatter(
x=row["timestep"],
y=row["nproducts"],
mode="lines",
name=method,
line=dict(color=method_color_mapping[method]),
showlegend=True,
),
)
fig.update_layout(
title="Hydrogen Combustion (2H2 + O2 -> 2H2O, 64 units)",
xaxis_title="Timestep",
yaxis_title="Number of water molecules",
)
st.plotly_chart(fig)
# tempearture
fig = go.Figure()
for method in df["method"].unique():
row = df[df["method"] == method].iloc[0]
fig.add_trace(
go.Scatter(
x=row["timestep"],
y=row["temperatures"],
mode="markers",
name=method,
line=dict(
color=method_color_mapping[method],
# width=1
),
marker=dict(color=method_color_mapping[method], size=3),
showlegend=True,
),
)
target_steps = df["target_steps"].iloc[0]
fig.add_trace(
go.Line(
x=[0, target_steps / 3, target_steps / 3 * 2, target_steps],
y=[300, 3000, 3000, 300],
mode="lines",
name="Target",
line=dict(dash="dash", color="white"),
showlegend=True,
),
)
fig.update_layout(
title="Hydrogen Combustion (2H2 + O2 -> 2H2O, 64 units)",
xaxis_title="Timestep",
yaxis_title="Temperature (K)",
# yaxis2=dict(
# title="Product Percentage (%)",
# overlaying="y",
# side="right",
# range=[0, 100],
# tickmode="sync",
# ),
# template="plotly_dark",
)
st.plotly_chart(fig)
# Energy
fig = go.Figure()
for method in df["method"].unique():
row = df[df["method"] == method].iloc[0]
fig.add_trace(
go.Scatter(
x=row["timestep"],
y=np.array(row["energies"]) - row["energies"][0],
mode="lines",
name=method,
line=dict(
color=method_color_mapping[method],
# width=1
),
marker=dict(color=method_color_mapping[method], size=3),
showlegend=True,
),
)
fig.update_layout(
# title="Hydrogen Combustion (2H2 + O2 -> 2H2O, 64 units)",
xaxis_title="Timestep",
yaxis_title="Potential Energy 𝚫 (eV)",
# template="plotly_dark",
)
st.plotly_chart(fig)
# Total Energy
# fig = go.Figure()
# for method in df["method"].unique():
# row = df[df["method"] == method].iloc[0]
# fig.add_trace(
# go.Scatter(
# x=row["timestep"],
# y=np.array(row["energies"]) - row["energies"][0] + np.array(row["kinetic_energies"]),
# mode="lines",
# name=method,
# line=dict(
# color=method_color_mapping[method],
# # width=1
# ),
# marker=dict(color=method_color_mapping[method], size=3),
# showlegend=True,
# ),
# )
# fig.update_layout(
# # title="Hydrogen Combustion (2H2 + O2 -> 2H2O, 64 units)",
# xaxis_title="Timestep",
# yaxis_title="Total Energy 𝚫 (eV)",
# # template="plotly_dark",
# )
# st.plotly_chart(fig)
# Reaction energy
fig = go.Figure()
exp_ref = -68.3078 # kcal/mol
df["reaction_energy"] = df["energies"].apply(lambda x: x[-1] - x[0]) / 128 * 23.0609 # kcal/mol
df["reaction_energy_abs_err"] = np.abs(df["reaction_energy"] - exp_ref)
df.sort_values("reaction_energy_abs_err", inplace=True)
fig.add_traces([
go.Bar(
x=df["method"],
y=df["reaction_energy"],
marker=dict(color=[method_color_mapping[method] for method in df["method"]]),
text=[f"{y:.2f}" for y in df["reaction_energy"]],
),
])
fig.add_shape(
go.layout.Shape(
type="line",
x0=-0.5, x1=len(df["method"]) - 0.5, # range covering the bars
y0=exp_ref, y1=exp_ref, # y-values for the horizontal line
line=dict(color="Red", width=2, dash="dash"),
layer="below"
)
)
fig.add_annotation(
go.layout.Annotation(
x=0.5,
xref="paper",
xanchor="center",
y=exp_ref,
yanchor="bottom",
text=f"Experiment: {exp_ref} kcal/mol [1]",
showarrow=False,
font=dict(
color="Red",
),
)
)
fig.update_layout(
# title="Reaction energy 𝚫H (kcal/mol)",
xaxis_title="Method
[1] Lide, D. R. (Ed.). (2004). CRC handbook of chemistry and physics (Vol. 85). CRC press.",
yaxis_title="Reaction energy 𝚫H (kcal/mol)",
# annotations = [
# dict(
# x=0.5, xref="paper", xanchor="center",
# y=-0.5, yref="paper", yanchor="bottom",
# text="Caption",
# )
# ]
)
st.plotly_chart(fig)
# Final reaction rate
fig = go.Figure()
df = df.sort_values("yield", ascending=True)
fig.add_trace(
go.Bar(
x=df["yield"] * 100,
y=df["method"],
opacity=0.75,
orientation="h",
marker=dict(color=[method_color_mapping[method] for method in df["method"]]),
text=[f"{y:.2f} %" for y in df["yield"] * 100],
)
)
fig.update_layout(
title="Reaction yield (2H2 + O2 -> 2H2O, 64 units)",
xaxis_title="Yield (%)",
yaxis_title="Method"
)
st.plotly_chart(fig)
# MD runtime speed
fig = go.Figure()
df = df.sort_values("steps_per_second", ascending=True)
fig.add_trace(
go.Bar(
x=df["steps_per_second"],
y=df["method"],
opacity=0.75,
orientation="h",
marker=dict(color=[method_color_mapping[method] for method in df["method"]]),
text=df["steps_per_second"].round(1),
)
)
fig.update_layout(
title="MD runtime speed (on single A100 GPU)",
xaxis_title="Steps per second",
yaxis_title="Method",
)
st.plotly_chart(fig)
# COM drift
st.markdown("""### Center of mass drift
The center of mass (COM) drift is a measure of the stability of the simulation. A well-behaved simulation should have a COM drift close to zero. The COM drift is calculated as the displacement of the COM of the system from the initial position.
""")
@st.cache_data
def get_com_drifts(df):
df_exploded = df.explode(["timestep", "com_drifts"]).reset_index(drop=True)
# Convert the 'com_drifts' column (which are arrays) into separate columns for x, y, and z components
df_exploded[["com_drift_x", "com_drift_y", "com_drift_z"]] = pd.DataFrame(
df_exploded["com_drifts"].tolist(), index=df_exploded.index
)
# Drop the original 'com_drifts' column
df_flat = df_exploded.drop(columns=["com_drifts"])
df_flat["total_com_drift"] = np.sqrt(
df_flat["com_drift_x"] ** 2
+ df_flat["com_drift_y"] ** 2
+ df_flat["com_drift_z"] ** 2
)
return df_flat
df_exploded = get_com_drifts(df)
if "play" not in st.session_state:
st.session_state.play = False
def toggle_playing():
st.session_state.play = not st.session_state.play
# st.button(
# "Play" if not st.session_state.play else "Pause",
# type="primary" if not st.session_state.play else "secondary",
# on_click=toggle_playing,
# )
increment = df["target_steps"].max() // 200
if "time_range" not in st.session_state:
st.session_state.time_range = (0, increment)
# @st.experimental_fragment(run_every=1e-3 if st.session_state.play else None)
@st.experimental_fragment()
def draw_com_drifts_plot():
if st.session_state.play:
start, end = st.session_state.time_range
end += increment
if end > df["target_steps"].max():
start = 0
end = 0
st.session_state.time_range = (start, end)
start_timestep, end_timestep = st.slider(
"Timestep",
min_value=0,
max_value=df["target_steps"].max(),
value=st.session_state.time_range,
key="time_range",
# on_change=check_range,
)
mask = (df_exploded["timestep"] >= start_timestep) & (
df_exploded["timestep"] <= end_timestep
)
df_filtered = df_exploded[mask]
df_filtered.sort_values(["method", "timestep"], inplace=True)
fig = px.line_3d(
data_frame=df_filtered,
x="com_drift_x",
y="com_drift_y",
z="com_drift_z",
labels={
"com_drift_x": "𝚫x (Å)",
"com_drift_y": "𝚫y (Å)",
"com_drift_z": "𝚫z (Å)",
},
category_orders={"method": df_exploded["method"].unique()},
color_discrete_sequence=[
method_color_mapping[method] for method in df_exploded["method"].unique()
],
color="method",
width=800,
height=800,
)
fig.update_layout(
scene=dict(
aspectmode="cube",
),
legend=dict(
orientation="v",
x=0.95,
xanchor="right",
y=1,
yanchor="top",
bgcolor="rgba(0, 0, 0, 0)",
),
)
fig.add_traces(
[
go.Scatter3d(
x=[0],
y=[0],
z=[0],
mode="markers",
marker=dict(size=3, color="white"),
name="origin",
),
# add last point of each method and annotate the total drift
go.Scatter3d(
# df_filtered.groupby("method")["com_drift_x"].last(),
x=df_filtered.groupby("method")["com_drift_x"].last(),
y=df_filtered.groupby("method")["com_drift_y"].last(),
z=df_filtered.groupby("method")["com_drift_z"].last(),
mode="markers+text",
marker=dict(size=3, color="white", opacity=0.5),
text=df_filtered.groupby("method")["total_com_drift"].last().round(3),
# size=5,
name="total drifts",
textposition="top center",
),
]
)
st.plotly_chart(fig)
draw_com_drifts_plot()