import warnings
warnings.filterwarnings("ignore")
import streamlit as st
import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from pathlib import Path
from utils.data_generator import (
generate_bond_order_data,
generate_master_dataset,
get_ferrocene_atoms,
fecp_bond_order,
cc_bond_order,
ch_bond_order,
)
from utils.models import (
reactor_metrics,
predict_cnt_properties,
train_pipeline_models,
bayesian_optimization_top_recipes,
simulate_reaxff_optimization,
predict_nucleation_probability,
)
# ── Page Config ──────────────────────────────────────────────────────────────
st.set_page_config(
page_title="Fe(Cp)₂ → CNT · AI Platform",
page_icon="⚗️",
layout="wide",
initial_sidebar_state="expanded",
)
# ── Colour palette matching demo.html ────────────────────────────────────────
COLORS = {
"bg": "#07101f",
"bg2": "#0f1a2e",
"ac": "#38bdf8",
"fe": "#f97316",
"gd": "#34d399",
"rd": "#f87171",
"yw": "#fbbf24",
"pu": "#a78bfa",
"mt": "#64748b",
}
PLOTLY_TEMPLATE = "plotly_dark"
PLOTLY_BG = "#0f1a2e"
PLOTLY_PAPER = "#07101f"
PLOTLY_GRID = "rgba(26,48,80,0.6)"
def dark_layout(title: str = "", height: int = 340) -> dict:
# Use magic-underscore keys so callers can freely pass xaxis=dict(...) or yaxis=dict(...)
# without triggering a "multiple values for keyword argument" Python error.
return dict(
title_text=title,
title_font_color=COLORS["ac"],
paper_bgcolor=PLOTLY_PAPER,
plot_bgcolor=PLOTLY_BG,
font=dict(color="#c9d6f0", size=11),
margin=dict(l=45, r=20, t=40 if title else 20, b=45),
height=height,
xaxis_gridcolor=PLOTLY_GRID,
xaxis_zerolinecolor=PLOTLY_GRID,
yaxis_gridcolor=PLOTLY_GRID,
yaxis_zerolinecolor=PLOTLY_GRID,
)
# ── Cached data ───────────────────────────────────────────────────────────────
@st.cache_data
def load_bond_data() -> pd.DataFrame:
return generate_bond_order_data()
@st.cache_data
def load_master_dataset() -> pd.DataFrame:
cache_path = Path("data/master_dataset.csv")
if cache_path.exists():
return pd.read_csv(cache_path)
df = generate_master_dataset()
cache_path.parent.mkdir(parents=True, exist_ok=True)
df.to_csv(cache_path, index=False)
return df
@st.cache_data
def load_model_results(df_hash: int) -> dict:
return train_pipeline_models(st.session_state.get("master_df", generate_master_dataset()))
# ── Custom CSS ────────────────────────────────────────────────────────────────
st.markdown("""
""", unsafe_allow_html=True)
# ── Sidebar ───────────────────────────────────────────────────────────────────
with st.sidebar:
st.markdown('
⚗️ Fe(Cp)₂ → CNT Platform
', unsafe_allow_html=True)
st.markdown('AI-Driven CNT Manufacturing Simulator
', unsafe_allow_html=True)
st.divider()
st.markdown('Global Controls
', unsafe_allow_html=True)
# CNT Product Type Selector
cnt_product_type = st.selectbox(
"Target CNT Product",
["SWCNT", "DWCNT", "MWCNT", "Ultra-Long CNT", "CNT Fiber", "Conductive Network", "High-Purity CNT"],
help="Select the desired CNT product type for optimization"
)
# Catalyst Type Selector
catalyst_type_sel = st.selectbox(
"Catalyst System",
["Fe", "Fe-C", "Fe-S", "Fe-Mo-C", "Fe-Co-C", "Fe-Ni-C"],
index=2,
help="Select catalyst composition for simulation"
)
global_temp = st.slider("Reactor Temperature (K)", 200, 2000, 500, 20, key="global_temp")
st.divider()
st.markdown('System Status
', unsafe_allow_html=True)
m = reactor_metrics(global_temp)
st.markdown(f"**Temp:** `{m['temperature_K']} K`")
st.markdown(f"**Pressure:** `{m['pressure_atm']} atm`")
st.markdown(f"**Fe–Cp Bonds:** `{m['fecp_bonds']}`")
st.markdown(f"**Free Fe Atoms:** `{m['free_fe_atoms']}`")
cnt_color = "#34d399" if m["cnt_potential_score"] == "High" else "#fbbf24" if m["cnt_potential_score"] == "Medium" else "#38bdf8" if m["cnt_potential_score"] == "Low-Medium" else "#f87171"
st.markdown(f'**CNT Potential:** {m["cnt_potential_score"]}', unsafe_allow_html=True)
st.divider()
st.markdown('ReaxFF MD · 91 Temperature Points · 13.6M Timesteps · GPU Accelerated
', unsafe_allow_html=True)
# ── Tabs ──────────────────────────────────────────────────────────────────────
tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs([
"⚡ Digital Twin Reactor",
"🎬 Decomposition Analysis",
"🔵 Catalyst & CNT Predictor",
"🌳 Pathways & Summary",
"🤖 AI Pipeline",
"⚙️ ReaxFF Optimization",
])
# ═══════════════════════════════════════════════════════════════════════════════
# TAB 1 — DIGITAL TWIN REACTOR
# ═══════════════════════════════════════════════════════════════════════════════
with tab1:
st.markdown("""
Ferrocene Decomposition — Digital Twin Reactor: 3D simulation of two ferrocene molecules
(Fe, C, H atoms) under reactive MD conditions. ReaxFF molecular dynamics scans 200 K → 2000 K.
Fe atoms are orange, C dark grey,
H white. Drag to rotate.
""", unsafe_allow_html=True)
T_twin = st.slider("Temperature (K)", 200, 2000, global_temp, 20, key="twin_temp")
m_twin = reactor_metrics(T_twin)
col_3d, col_metrics = st.columns([3, 1])
with col_3d:
atoms = get_ferrocene_atoms(T_twin)
atom_df = pd.DataFrame(atoms)
color_map = {"Fe": COLORS["fe"], "C": "#475569", "H": "#e2e8f0"}
size_map = {"Fe": 14, "C": 9, "H": 5}
fig3d = go.Figure()
for atype, color in color_map.items():
sub = atom_df[atom_df["type"] == atype]
fig3d.add_trace(go.Scatter3d(
x=sub["x"], y=sub["y"], z=sub["z"],
mode="markers",
marker=dict(size=size_map[atype], color=color, opacity=0.9,
line=dict(color="white", width=0.3)),
name=atype,
hovertemplate=f"{atype}
x: %{{x:.2f}} Å
y: %{{y:.2f}} Å
z: %{{z:.2f}} Å",
))
# Draw Fe–C bonds
bond_xs, bond_ys, bond_zs = [], [], []
fe_atoms = atom_df[atom_df["type"] == "Fe"]
c_atoms = atom_df[atom_df["type"] == "C"]
fecp_bo = fecp_bond_order(T_twin)
for _, fa in fe_atoms.iterrows():
for _, ca in c_atoms.iterrows():
d = np.sqrt((fa.x - ca.x)**2 + (fa.y - ca.y)**2 + (fa.z - ca.z)**2)
if d < 3.5 and fecp_bo > 0.05:
bond_xs += [fa.x, ca.x, None]
bond_ys += [fa.y, ca.y, None]
bond_zs += [fa.z, ca.z, None]
# C-C bonds within rings
cc_xs, cc_ys, cc_zs = [], [], []
for _, ca in c_atoms.iterrows():
for _, cb in c_atoms.iterrows():
d = np.sqrt((ca.x - cb.x)**2 + (ca.y - cb.y)**2 + (ca.z - cb.z)**2)
if 0.1 < d < 1.7:
cc_xs += [ca.x, cb.x, None]
cc_ys += [ca.y, cb.y, None]
cc_zs += [ca.z, cb.z, None]
if bond_xs:
fig3d.add_trace(go.Scatter3d(
x=bond_xs, y=bond_ys, z=bond_zs, mode="lines",
line=dict(color=f"rgba(249,115,22,{max(0.05, fecp_bo * 0.8):.2f})", width=3),
name="Fe–C bond", showlegend=False,
))
if cc_xs:
fig3d.add_trace(go.Scatter3d(
x=cc_xs, y=cc_ys, z=cc_zs, mode="lines",
line=dict(color="rgba(71,85,105,0.5)", width=1.5),
name="C–C bond", showlegend=False,
))
fig3d.update_layout(
paper_bgcolor=PLOTLY_PAPER, plot_bgcolor=PLOTLY_BG,
scene=dict(
bgcolor=PLOTLY_BG,
xaxis=dict(backgroundcolor=PLOTLY_BG, gridcolor=PLOTLY_GRID, showticklabels=False, title=""),
yaxis=dict(backgroundcolor=PLOTLY_BG, gridcolor=PLOTLY_GRID, showticklabels=False, title=""),
zaxis=dict(backgroundcolor=PLOTLY_BG, gridcolor=PLOTLY_GRID, showticklabels=False, title=""),
camera=dict(eye=dict(x=1.5, y=1.5, z=1.2)),
),
margin=dict(l=0, r=0, t=0, b=0),
height=420,
legend=dict(bgcolor="rgba(0,0,0,0)", font=dict(color="#c9d6f0", size=10)),
)
st.plotly_chart(fig3d, use_container_width=True)
# Observation text
f = max(0, min(1, (T_twin - 200) / 1800))
if T_twin < 500:
obs = f"At {T_twin} K: Molecules are thermally stable. All Fe–Cp bonds intact. Atoms vibrate around equilibrium. Ferrocene retains sandwich geometry."
elif T_twin < 900:
obs = f"At {T_twin} K: Thermal vibration amplitude increasing. Fe–Cp bond order beginning to decrease ({fecp_bond_order(T_twin):.3f}). Cp rings show slight distortion. System approaching decomposition threshold."
elif T_twin < 1200:
obs = f"At {T_twin} K: Fe–Cp bonds weakening significantly (bond order: {fecp_bond_order(T_twin):.3f}). Cp ring tilt observable. Fe atom shows increased displacement from equilibrium. Decomposition onset in this range."
elif T_twin < 1500:
obs = f"At {T_twin} K: Fe atom released from Cp sandwich. Free Fe atoms visible diffusing. C–C bonds in Cp radicals remain intact (bond order: {cc_bond_order(T_twin):.3f}). Fe clustering begins — catalyst nanoparticle forming."
else:
obs = f"At {T_twin} K: Complete ferrocene decomposition. Fe atoms aggregating into catalyst nanoparticle. CNT nucleation potential is HIGH. Cp fragments may further pyrolyze."
st.markdown(f'{obs}
', unsafe_allow_html=True)
with col_metrics:
st.markdown('Live Reactor Metrics
', unsafe_allow_html=True)
metric_rows = [
("Temperature", f"{m_twin['temperature_K']} K", COLORS["ac"]),
("Pressure", f"{m_twin['pressure_atm']} atm", "#c9d6f0"),
("Pot. Energy", f"{m_twin['potential_energy_kcal']} kcal/mol", "#c9d6f0"),
("Kin. Energy", f"{m_twin['kinetic_energy_kcal']} kcal/mol", "#c9d6f0"),
]
for label, val, color in metric_rows:
st.markdown(f"{label}{val}
", unsafe_allow_html=True)
st.markdown('Bond Statistics
', unsafe_allow_html=True)
bond_rows = [
("Fe–Cp Bonds", m_twin["fecp_bonds"], COLORS["fe"]),
("C–C Bonds", m_twin["cc_bonds"], COLORS["ac"]),
("C–H Bonds", m_twin["ch_bonds"], "#c9d6f0"),
]
for label, val, color in bond_rows:
st.markdown(f"{label}{val}
", unsafe_allow_html=True)
st.markdown('Cluster Analysis
', unsafe_allow_html=True)
cluster_rows = [
("Free Fe Atoms", m_twin["free_fe_atoms"], COLORS["yw"]),
("Largest Fe Cluster", m_twin["largest_fe_cluster"], COLORS["gd"]),
("CNT Potential", m_twin["cnt_potential_score"], cnt_color),
]
for label, val, color in cluster_rows:
st.markdown(f"{label}{val}
", unsafe_allow_html=True)
st.markdown('System Info
', unsafe_allow_html=True)
sys_rows = [
("Total Atoms", "125"), ("Box Size", "38.4³ Å"),
("Timestep", "0.25 fs"), ("Total Steps", "13.6 M"),
("Force Field", "ReaxFF"), ("GPU Accel.", "Active"),
]
for label, val in sys_rows:
color = COLORS["ac"] if label == "Force Field" else COLORS["gd"] if label == "GPU Accel." else "#c9d6f0"
st.markdown(f"{label}{val}
", unsafe_allow_html=True)
# ═══════════════════════════════════════════════════════════════════════════════
# TAB 2 — DECOMPOSITION ANALYSIS
# ═══════════════════════════════════════════════════════════════════════════════
with tab2:
FRAMES = [
{"name": "Intact Ferrocene", "T": "200 K", "color": COLORS["ac"],
"desc": "Ferrocene molecule in equilibrium geometry. Fe atom sits in centre of two Cp (cyclopentadienyl) rings in η⁵ coordination. All 10 Fe–Cp bonds intact, average bond order 0.589. Ring–ring distance: 3.32 Å. System is chemically inert at this temperature."},
{"name": "Bond Weakening", "T": "850 K", "color": COLORS["yw"],
"desc": "Fe–Cp π-bond order drops to ~0.42. Thermal energy approaching activation barrier E_a ≈ 2.1 eV for Fe–Cp dissociation. Cp rings begin asymmetric tilt — one ring tilts +12° relative to equilibrium. Fe atom displaced 0.3 Å from sandwich centre."},
{"name": "Cp Ring Distortion", "T": "1050 K", "color": COLORS["pu"],
"desc": "One Cp ring has rotated 35° and tilted 20°. Fe–Cp bond order on distorted ring falls to ~0.18 — near breaking threshold. Asymmetric distortion is the precursor to full Fe release. CpFe• radical intermediate forming."},
{"name": "Fe Released", "T": "1200 K", "color": COLORS["gd"],
"desc": "Fe atom fully dissociated from both Cp rings. Fe–Cp bond order = 0 for departed ring. Fe atom is now a free radical in the gas phase. The two Cp rings (C₅H₅•) are free cyclopentadienyl radicals — the key Fe source for catalyst formation."},
{"name": "Fe Aggregation", "T": "1400 K", "color": COLORS["fe"],
"desc": "Multiple Fe atoms from decomposed molecules diffuse and aggregate. Fe–Fe interaction energy (~0.8 eV) drives cluster formation. A 2-atom Fe dimer (Fe₂) forms as the initial catalyst nucleus. Diffusion coefficient D_Fe ≈ 2.1×10⁻⁴ cm²/s at 1400 K."},
{"name": "Catalyst Nanoparticle", "T": "1600 K", "color": COLORS["rd"],
"desc": "Fe₅ nanoparticle formed — five Fe atoms in close-packed configuration, average Fe–Fe distance 2.48 Å. Cluster radius ≈ 0.75 nm. Optimal catalyst size for SWCNT nucleation via vapour-liquid-solid (VLS) mechanism. CNT nucleation probability: HIGH. Expected CNT diameter ≈ 1.5 nm."},
]
# ── Section A: Molecular Movie ──────────────────────────────────────────
st.markdown('Molecular Decomposition Movie — Frame-by-Frame Pathway
', unsafe_allow_html=True)
frame_idx = st.select_slider(
"Select decomposition frame",
options=list(range(len(FRAMES))),
format_func=lambda i: f"{i+1}. {FRAMES[i]['name']} ({FRAMES[i]['T']})",
key="movie_frame",
)
col_movie, col_framedesc = st.columns([1, 1])
FRAME_TEMPS_K = [200, 850, 1050, 1200, 1400, 1600]
with col_movie:
T_frame = FRAME_TEMPS_K[frame_idx]
f_atoms = get_ferrocene_atoms(T_frame)
fa_df = pd.DataFrame(f_atoms)
color_map = {"Fe": COLORS["fe"], "C": "#475569", "H": "#e2e8f0"}
size_map = {"Fe": 12, "C": 8, "H": 5}
fig_movie = go.Figure()
for atype, color in color_map.items():
sub = fa_df[fa_df["type"] == atype]
fig_movie.add_trace(go.Scatter3d(
x=sub["x"], y=sub["y"], z=sub["z"], mode="markers",
marker=dict(size=size_map[atype], color=color, opacity=0.9,
line=dict(color="white", width=0.3)),
name=atype,
))
fecp_bo = fecp_bond_order(T_frame)
fe_a = fa_df[fa_df["type"] == "Fe"]
c_a = fa_df[fa_df["type"] == "C"]
bx, by, bz = [], [], []
for _, fa in fe_a.iterrows():
for _, ca in c_a.iterrows():
d = np.sqrt((fa.x - ca.x)**2 + (fa.y - ca.y)**2 + (fa.z - ca.z)**2)
if d < 3.5 and fecp_bo > 0.05:
bx += [fa.x, ca.x, None]; by += [fa.y, ca.y, None]; bz += [fa.z, ca.z, None]
if bx:
fig_movie.add_trace(go.Scatter3d(x=bx, y=by, z=bz, mode="lines",
line=dict(color=f"rgba(249,115,22,{max(0.05,fecp_bo*0.8):.2f})", width=3),
showlegend=False))
fig_movie.update_layout(
paper_bgcolor=PLOTLY_PAPER, plot_bgcolor=PLOTLY_BG,
scene=dict(bgcolor=PLOTLY_BG,
xaxis=dict(backgroundcolor=PLOTLY_BG, gridcolor=PLOTLY_GRID, showticklabels=False, title=""),
yaxis=dict(backgroundcolor=PLOTLY_BG, gridcolor=PLOTLY_GRID, showticklabels=False, title=""),
zaxis=dict(backgroundcolor=PLOTLY_BG, gridcolor=PLOTLY_GRID, showticklabels=False, title=""),
camera=dict(eye=dict(x=1.5, y=1.2, z=1.0)),
),
margin=dict(l=0, r=0, t=0, b=0), height=320,
legend=dict(bgcolor="rgba(0,0,0,0)", font=dict(color="#c9d6f0", size=10)),
)
st.plotly_chart(fig_movie, use_container_width=True)
pathway_html = """
T ≈ 900–1100 K — Fe–Cp π-bond weakens
T ≈ 1100–1300 K — Cp ring distortion & separation
T > 1300 K — Fe atom released to gas phase
T > 1400 K — Fe aggregation → nanoparticle
T > 1500 K — Catalyst nanoparticle (CNT nucleation site)
"""
st.markdown(pathway_html, unsafe_allow_html=True)
with col_framedesc:
frame = FRAMES[frame_idx]
st.markdown(f'Frame {frame_idx+1}/6 — {frame["name"]} @ {frame["T"]}
', unsafe_allow_html=True)
st.markdown(f'{frame["desc"]}
', unsafe_allow_html=True)
# Frame progress bars
st.markdown('Decomposition State
', unsafe_allow_html=True)
state_vals = {
"Fe–Cp Bond Integrity": max(0, 100 - frame_idx * 20),
"Cp Ring Stability": max(0, 100 - frame_idx * 15),
"Fe Liberation": min(100, frame_idx * 22),
"Cluster Formation": min(100, max(0, (frame_idx - 3) * 35)),
}
bar_colors = [COLORS["fe"], COLORS["ac"], COLORS["gd"], COLORS["yw"]]
for (label, val), color in zip(state_vals.items(), bar_colors):
st.markdown(f"""
""", unsafe_allow_html=True)
# ── Section B: Bond Order vs Temperature ───────────────────────────────
st.divider()
st.markdown('Bond Order vs Temperature — Animated Temperature Sweep
', unsafe_allow_html=True)
bond_df = load_bond_data()
show_range = st.slider("Display temperature range (K)", 200, 2000, (200, 2000), 50, key="bond_range")
mask = (bond_df["temperature_K"] >= show_range[0]) & (bond_df["temperature_K"] <= show_range[1])
bd = bond_df[mask]
fig_bond = go.Figure()
fig_bond.add_vline(x=1000, line_dash="dash", line_color="rgba(248,113,113,0.6)", line_width=1.5,
annotation_text="T_decomp onset", annotation_font_color="#f87171",
annotation_font_size=10, annotation_position="top right")
fig_bond.add_trace(go.Scatter(x=bd["temperature_K"], y=bd["fecp_bond_order"],
mode="lines", name="Fe–Cp Bond Order", line=dict(color=COLORS["fe"], width=2.5),
fill="tozeroy", fillcolor="rgba(249,115,22,0.08)"))
fig_bond.add_trace(go.Scatter(x=bd["temperature_K"], y=bd["cc_bond_order"],
mode="lines", name="C–C Bond Order", line=dict(color=COLORS["ac"], width=2.5)))
fig_bond.add_trace(go.Scatter(x=bd["temperature_K"], y=bd["ch_bond_order"],
mode="lines", name="C–H Bond Order", line=dict(color=COLORS["yw"], width=2.5)))
fig_bond.update_layout(**dark_layout(height=300),
xaxis_title="Temperature (K)", yaxis_title="Bond Order",
yaxis=dict(range=[0, 1.4], gridcolor=PLOTLY_GRID),
legend=dict(bgcolor="rgba(0,0,0,0)", font=dict(color="#c9d6f0", size=10)),
)
st.plotly_chart(fig_bond, use_container_width=True)
st.markdown("""
● Fe–Cp (orange) weakens first — thermally labile organometallic π-bond.
● C–C (cyan) most covalently robust.
● C–H (yellow) moderately stable.
The intersection of Fe–Cp with the dashed threshold gives Tdecomp.
""", unsafe_allow_html=True)
# ── Section C: Bond Breaking Network ──────────────────────────────────
st.divider()
st.markdown('Bond Survival Landscape — Temperature vs Bond Strength
', unsafe_allow_html=True)
fig_surv = go.Figure()
fig_surv.add_trace(go.Scatter(x=bond_df["temperature_K"], y=bond_df["fecp_survival_pct"],
mode="lines", name="Fe–Cp survival %", line=dict(color=COLORS["fe"], width=2.5),
fill="tozeroy", fillcolor="rgba(249,115,22,0.08)"))
fig_surv.add_trace(go.Scatter(x=bond_df["temperature_K"], y=bond_df["cc_survival_pct"],
mode="lines", name="C–C survival %", line=dict(color=COLORS["ac"], width=2.5)))
fig_surv.add_trace(go.Scatter(x=bond_df["temperature_K"], y=bond_df["ch_survival_pct"],
mode="lines", name="C–H survival %", line=dict(color=COLORS["yw"], width=2.5)))
fig_surv.update_layout(**dark_layout(height=280),
xaxis_title="Temperature (K)", yaxis_title="Bond Survival (%)",
yaxis=dict(range=[0, 105], gridcolor=PLOTLY_GRID),
legend=dict(bgcolor="rgba(0,0,0,0)", font=dict(color="#c9d6f0", size=10)),
)
st.plotly_chart(fig_surv, use_container_width=True)
# Current T marker
T_marker = st.slider("Mark temperature on landscape", 200, 2000, global_temp, 50, key="landscape_T")
row = bond_df[bond_df["temperature_K"] == T_marker].iloc[0] if T_marker in bond_df["temperature_K"].values else None
if row is not None:
c1, c2, c3 = st.columns(3)
c1.metric("Fe–Cp Survival", f"{row['fecp_survival_pct']:.1f}%")
c2.metric("C–C Survival", f"{row['cc_survival_pct']:.1f}%")
c3.metric("C–H Survival", f"{row['ch_survival_pct']:.1f}%")
# ═══════════════════════════════════════════════════════════════════════════════
# TAB 3 — CATALYST & CNT PREDICTOR
# ═══════════════════════════════════════════════════════════════════════════════
with tab3:
# ── Section A: Cluster Formation Simulator ──────────────────────────────
st.markdown('Fe Nanoparticle Formation — Catalyst Evolution Simulator
', unsafe_allow_html=True)
st.markdown("""
After ferrocene decomposes, released Fe atoms aggregate due to Fe–Fe attractive interactions.
Fe cluster size 1–5 nm is the optimal range for floating-catalyst CVD CNT growth.
""", unsafe_allow_html=True)
T_cluster = st.slider("Cluster Formation Temperature (K)", 800, 2000, 1400, 100, key="cluster_T")
# Generate synthetic cluster trajectory data
n_fe = min(10, max(2, round(2 + T_cluster / 600)))
t_axis = np.linspace(0, 200, 200)
growth_rate = 0.02 + (T_cluster - 800) / 12000
max_cluster_size = np.clip(1 + n_fe * (1 - np.exp(-growth_rate * t_axis)), 1, n_fe).round()
cluster_count = np.clip(n_fe - max_cluster_size * 0.6 + np.random.normal(0, 0.3, 200), 1, n_fe).round()
col_c1, col_c2 = st.columns([1, 1])
with col_c1:
fig_cluster = go.Figure()
fig_cluster.add_trace(go.Scatter(x=t_axis, y=max_cluster_size,
mode="lines", name="Largest Cluster (atoms)", line=dict(color=COLORS["fe"], width=2.5),
fill="tozeroy", fillcolor="rgba(249,115,22,0.1)"))
fig_cluster.add_trace(go.Scatter(x=t_axis, y=cluster_count,
mode="lines", name="Cluster Count", line=dict(color=COLORS["ac"], width=2.5)))
fig_cluster.update_layout(**dark_layout(height=260),
xaxis_title="Simulation Time (ps)", yaxis_title="Count / Size",
legend=dict(bgcolor="rgba(0,0,0,0)", font=dict(color="#c9d6f0", size=10)))
st.plotly_chart(fig_cluster, use_container_width=True)
with col_c2:
current_max = int(max_cluster_size[-1])
current_rad = round(current_max * 1.2, 1)
st.markdown('Cluster Growth Metrics
', unsafe_allow_html=True)
rows_c = [
("Cluster Count", int(cluster_count[-1]), COLORS["ac"]),
("Largest Cluster (atoms)", current_max, COLORS["fe"]),
("Avg Radius (Å)", current_rad, COLORS["gd"]),
("Simulation Time (ps)", 200, "#c9d6f0"),
]
for label, val, color in rows_c:
st.markdown(f"{label}{val}
", unsafe_allow_html=True)
suitability = "✅ Optimal SWCNT range" if 3 <= current_max <= 15 else "⚠️ Large → MWCNT likely" if current_max > 15 else "⚠️ Too small for CNT"
suit_color = COLORS["gd"] if "Optimal" in suitability else COLORS["yw"]
st.markdown(f"""
{suitability}
Target: 1–5 nm Fe clusters (3–15 atoms) for optimal SWCNT nucleation.
Larger clusters produce MWCNTs. Cluster radius determines CNT outer diameter.
""", unsafe_allow_html=True)
# ── Section B: CNT Growth Predictor ────────────────────────────────────
st.divider()
st.markdown('CNT Growth Predictor — Catalyst Activity Calculator
', unsafe_allow_html=True)
col_inp, col_out = st.columns([1, 1])
with col_inp:
st.markdown('Simulation Inputs
', unsafe_allow_html=True)
T_cnt = st.slider("Temperature (K)", 200, 2000, 1400, 50, key="cnt_T")
cs_cnt = st.slider("Fe Cluster Size (atoms)", 1, 50, 5, 1, key="cnt_cs")
cr_cnt = st.slider("Cluster Radius (nm)", 0.1, 3.0, 0.75, 0.05, key="cnt_cr")
as_cnt = st.slider("Active Surface Sites", 1, 50, 12, 1, key="cnt_as")
h2_cnt = st.slider("H₂ Concentration (mol%)", 0, 100, 20, 5, key="cnt_h2")
with col_out:
preds = predict_cnt_properties(T_cnt, cs_cnt, cr_cnt, as_cnt, h2_cnt)
prob = preds["nucleation_prob_pct"]
gauge_color = COLORS["gd"] if prob > 70 else COLORS["yw"] if prob > 40 else COLORS["rd"]
# Gauge chart
fig_gauge = go.Figure(go.Indicator(
mode="gauge+number",
value=prob,
number={"font": {"color": gauge_color, "size": 36}, "suffix": "%"},
title={"text": "CNT Nucleation Probability", "font": {"color": "#94a3b8", "size": 12}},
gauge={
"axis": {"range": [0, 100], "tickcolor": "#64748b", "tickwidth": 1},
"bar": {"color": gauge_color},
"bgcolor": "#0a1525",
"borderwidth": 0,
"steps": [
{"range": [0, 40], "color": "rgba(248,113,113,0.15)"},
{"range": [40, 70], "color": "rgba(251,191,36,0.15)"},
{"range": [70, 100], "color": "rgba(52,211,153,0.15)"},
],
"threshold": {"line": {"color": gauge_color, "width": 3}, "thickness": 0.75, "value": prob},
},
))
fig_gauge.update_layout(
paper_bgcolor=PLOTLY_PAPER, font=dict(color="#c9d6f0"),
height=240, margin=dict(l=20, r=20, t=40, b=10))
st.plotly_chart(fig_gauge, use_container_width=True)
out_rows = [
("CNT Diameter", f"{preds['cnt_diameter_nm']} nm", gauge_color),
("Catalyst Activity", f"{preds['catalyst_activity_pct']}%", COLORS["pu"]),
("Expected Yield", f"{preds['expected_yield_pct']}%", COLORS["ac"]),
("Nucleation Score", preds["nucleation_score"], gauge_color),
]
for label, val, color in out_rows:
st.markdown(f"{label}{val}
", unsafe_allow_html=True)
# ── Section C: CNT Property Heatmap ────────────────────────────────────
st.divider()
st.markdown('CNT Nucleation Probability — T vs Cluster Size Heatmap
', unsafe_allow_html=True)
T_range = np.arange(600, 2001, 100)
cs_range = np.arange(1, 26, 2)
Z = np.array([[predict_cnt_properties(T, cs, 0.75, 12, 20)["nucleation_prob_pct"]
for T in T_range] for cs in cs_range])
fig_heat = go.Figure(go.Heatmap(
z=Z, x=T_range, y=cs_range,
colorscale=[[0, "#07101f"], [0.4, "#f97316"], [0.7, "#fbbf24"], [1.0, "#34d399"]],
colorbar=dict(title="Prob %", tickfont=dict(color="#94a3b8"), titlefont=dict(color="#94a3b8")),
hoverongaps=False,
hovertemplate="T: %{x} K
Cluster: %{y} atoms
Prob: %{z:.0f}%",
))
fig_heat.update_layout(**dark_layout(height=300),
xaxis_title="Temperature (K)", yaxis_title="Cluster Size (atoms)")
st.plotly_chart(fig_heat, use_container_width=True)
# ═══════════════════════════════════════════════════════════════════════════════
# TAB 4 — PATHWAYS & SUMMARY
# ═══════════════════════════════════════════════════════════════════════════════
with tab4:
# ── Reaction Pathway Sankey ─────────────────────────────────────────────
st.markdown('Reaction Pathway Tree — Ferrocene Decomposition Mechanism
', unsafe_allow_html=True)
sankey_labels = [
"Ferrocene\nFe(Cp)₂", # 0
"Fe–Cp Weakening", # 1
"Cp Ring Distortion", # 2
"Free Fe Atom", # 3
"Cp• Radical (C₅H₅•)", # 4
"Fe Aggregation", # 5
"Fe₅ Nanoparticle", # 6
"CNT Nucleation", # 7
"Pyrolysis C₂H₂", # 8
]
sankey_source = [0, 1, 1, 2, 2, 3, 5, 6, 4, 4]
sankey_target = [1, 2, 3, 4, 3, 5, 6, 7, 8, 7]
sankey_value = [100, 92, 88, 78, 70, 85, 72, 65, 22, 15]
sankey_colors = ["rgba(56,189,248,0.4)", "rgba(167,139,250,0.4)", "rgba(249,115,22,0.4)",
"rgba(71,85,105,0.4)", "rgba(249,115,22,0.4)", "rgba(249,115,22,0.4)",
"rgba(52,211,153,0.4)", "rgba(56,189,248,0.4)", "rgba(100,116,139,0.4)",
"rgba(56,189,248,0.3)"]
fig_sankey = go.Figure(go.Sankey(
node=dict(
pad=15, thickness=18,
label=sankey_labels,
color=[COLORS["ac"], COLORS["yw"], COLORS["pu"], COLORS["fe"],
COLORS["mt"], COLORS["fe"], COLORS["gd"], COLORS["ac"], "#475569"],
line=dict(color="rgba(26,48,80,0.8)", width=0.5),
),
link=dict(
source=sankey_source, target=sankey_target, value=sankey_value,
color=sankey_colors,
),
))
fig_sankey.update_layout(
paper_bgcolor=PLOTLY_PAPER, font=dict(color="#c9d6f0", size=10),
height=400, margin=dict(l=10, r=10, t=20, b=10),
)
st.plotly_chart(fig_sankey, use_container_width=True)
# Pathway probability bars + key steps
col_pb, col_key = st.columns(2)
with col_pb:
probs = [
("Fe–Cp Dissociation (primary)", 88, COLORS["fe"]),
("Fe Nanoparticle Formation", 72, COLORS["gd"]),
("CNT Nucleation from Fe₅", 65, COLORS["ac"]),
("Cp Radical Pyrolysis", 22, COLORS["mt"]),
("H₂ Re-combination", 15, "#475569"),
]
st.markdown('Pathway Probabilities
', unsafe_allow_html=True)
for name, val, color in probs:
st.markdown(f"""
""", unsafe_allow_html=True)
with col_key:
st.markdown('Rate-Limiting Step
', unsafe_allow_html=True)
st.markdown("""
The Fe–Cp π-bond dissociation is the rate-limiting step. This organometallic bond has
an activation energy E_a ≈ 2.1 eV derived from temperature-dependent ReaxFF analysis.
All downstream CNT formation steps proceed at higher rates once this barrier is crossed.
Key Intermediate: The CpFe• radical (mono-decapitated ferrocene) is the primary
intermediate with a calculated lifetime of ~0.8 ps at 1400 K before complete Fe release.
""", unsafe_allow_html=True)
# ── Executive Summary ───────────────────────────────────────────────────
st.divider()
st.markdown('Project Status — Executive Summary Dashboard
', unsafe_allow_html=True)
exec_stats = [
("91", "Simulation Runs"), ("200–2000 K", "Temperature Range"),
("125", "Atoms Simulated"), ("13.6 M", "Total Timesteps"),
("18", "Feature Descriptors"), ("38×", "GPU Speedup"),
("0.25 fs", "Timestep Size"), ("100%", "Pipeline Complete"),
]
cols_stat = st.columns(4)
for i, (num, lbl) in enumerate(exec_stats):
with cols_stat[i % 4]:
st.markdown(f"""
""", unsafe_allow_html=True)
col_pipe, col_results = st.columns(2)
with col_pipe:
st.markdown('Pipeline Completion
', unsafe_allow_html=True)
pipeline_items = [
("MD Simulation Engine", 100, COLORS["gd"]),
("Bond Order Extraction", 100, COLORS["gd"]),
("Feature Matrix Construction", 100, COLORS["gd"]),
("Fe Cluster Analysis", 100, COLORS["gd"]),
("CNT Potential Scoring", 87, COLORS["ac"]),
("PINN Model Training", 74, COLORS["yw"]),
("ReaxFF Parameterization", 35, COLORS["fe"]),
]
for name, val, color in pipeline_items:
st.markdown(f"""
""", unsafe_allow_html=True)
with col_results:
st.markdown('Key Results
', unsafe_allow_html=True)
key_results = [
("✓", "Simulation engine validated — 91 temperature points computed", COLORS["gd"]),
("✓", "Bond order extraction pipeline operational", COLORS["gd"]),
("✓", "Fe cluster tracking algorithm deployed", COLORS["gd"]),
("✓", "Feature matrix (18 descriptors × 91 temps) constructed", COLORS["gd"]),
("✓", "CNT potential scoring model trained", COLORS["gd"]),
("✓", "GPU acceleration active — 38× speedup vs CPU", COLORS["gd"]),
("→", "Next: Obtain Fe–C–H organometallic ReaxFF parameters", COLORS["ac"]),
("→", "Next: Extend to multi-ferrocene + H₂ carrier gas system", COLORS["ac"]),
]
for icon, text, color in key_results:
st.markdown(f"{icon} {text}
", unsafe_allow_html=True)
# ═══════════════════════════════════════════════════════════════════════════════
# TAB 5 — AI PIPELINE
# ═══════════════════════════════════════════════════════════════════════════════
with tab5:
st.markdown("""
Full AI Pipeline: Public data + physics-plausible synthetic DI-FCCVD reactor data
feeds a 5-stage cascade of ML models (Atomistic Catalyst → Fe NP Formation → CNT Growth →
Reactor Surrogate → CNT Quality). Bayesian optimisation finds the best synthesis recipe.
""", unsafe_allow_html=True)
df = load_master_dataset()
# ── Dataset Overview ────────────────────────────────────────────────────
st.markdown('Master Dataset Overview
', unsafe_allow_html=True)
ds_cols = st.columns(4)
ds_stats = [
(f"{len(df):,}", "Synthesis Runs"),
(f"{df.columns.size}", "Feature Columns"),
(f"{df['purity_percent'].mean():.1f}%", "Avg Purity"),
(f"{df['yield_mg_hr'].mean():.0f} mg/hr", "Avg Yield"),
]
for i, (num, lbl) in enumerate(ds_stats):
with ds_cols[i]:
st.markdown(f'', unsafe_allow_html=True)
# ── CNT Type & Catalyst Type Distribution ────────────────────────────────
st.markdown('Product & Catalyst Distribution
', unsafe_allow_html=True)
col_dist1, col_dist2 = st.columns(2)
with col_dist1:
if 'cnt_type' in df.columns:
cnt_counts = df['cnt_type'].value_counts()
fig_cnt_pie = go.Figure(go.Pie(
labels=cnt_counts.index,
values=cnt_counts.values,
marker=dict(colors=[COLORS["ac"], COLORS["gd"], COLORS["yw"]]),
hole=0.4,
textinfo='label+percent',
textfont=dict(color='#c9d6f0', size=11),
))
fig_cnt_pie.update_layout(
title_text="CNT Product Types",
title_font_color=COLORS["ac"],
paper_bgcolor=PLOTLY_PAPER,
font=dict(color="#c9d6f0"),
height=260,
margin=dict(l=20, r=20, t=40, b=10),
showlegend=True,
legend=dict(bgcolor="rgba(0,0,0,0)", font=dict(color="#c9d6f0", size=10)),
)
st.plotly_chart(fig_cnt_pie, use_container_width=True)
else:
st.info("CNT type distribution not available in current dataset")
with col_dist2:
if 'catalyst_type' in df.columns:
cat_counts = df['catalyst_type'].value_counts()
fig_cat_pie = go.Figure(go.Pie(
labels=cat_counts.index,
values=cat_counts.values,
marker=dict(colors=[COLORS["fe"], "#8b4513", COLORS["yw"], COLORS["ac"], COLORS["gd"], COLORS["pu"]]),
hole=0.4,
textinfo='label+percent',
textfont=dict(color='#c9d6f0', size=11),
))
fig_cat_pie.update_layout(
title_text="Catalyst Composition Types",
title_font_color=COLORS["ac"],
paper_bgcolor=PLOTLY_PAPER,
font=dict(color="#c9d6f0"),
height=260,
margin=dict(l=20, r=20, t=40, b=10),
showlegend=True,
legend=dict(bgcolor="rgba(0,0,0,0)", font=dict(color="#c9d6f0", size=10)),
)
st.plotly_chart(fig_cat_pie, use_container_width=True)
else:
st.info("Catalyst type distribution not available in current dataset")
col_exp, col_corr = st.columns(2)
with col_exp:
st.markdown('Temperature vs CNT Purity
', unsafe_allow_html=True)
fig_scatter = px.scatter(
df.sample(1500, random_state=42),
x="temp_C", y="purity_percent", color="NP_size_nm",
color_continuous_scale=[[0, "#07101f"], [0.4, "#38bdf8"], [0.8, "#34d399"], [1, "#fbbf24"]],
opacity=0.55, size_max=6,
labels={"temp_C": "Temperature (°C)", "purity_percent": "Purity (%)", "NP_size_nm": "NP Size (nm)"},
template=PLOTLY_TEMPLATE,
)
fig_scatter.update_layout(**dark_layout(height=280))
fig_scatter.update_traces(marker=dict(size=4))
st.plotly_chart(fig_scatter, use_container_width=True)
with col_corr:
st.markdown('Feature Correlations
', unsafe_allow_html=True)
num_cols = ["temp_C", "H2_sccm", "ferrocene_wt", "sulfur_wt", "NP_size_nm", "purity_percent", "yield_mg_hr", "aspect_ratio"]
corr = df[num_cols].corr()
fig_corr = go.Figure(go.Heatmap(
z=corr.values, x=corr.columns, y=corr.index,
colorscale=[[0, "#f87171"], [0.5, "#0f1a2e"], [1, "#38bdf8"]],
zmid=0, zmin=-1, zmax=1,
text=corr.values.round(2), texttemplate="%{text}",
colorbar=dict(tickfont=dict(color="#94a3b8"), titlefont=dict(color="#94a3b8")),
))
fig_corr.update_layout(**dark_layout(height=280))
st.plotly_chart(fig_corr, use_container_width=True)
# ── Model Pipeline ──────────────────────────────────────────────────────
st.divider()
st.markdown('5-Stage AI Pipeline — Model Performance (5-Fold CV R²)
', unsafe_allow_html=True)
model_specs_display = [
(1, "Atomistic Catalyst", "temp_C, H₂, ferrocene → decomposition_rate", 0.94, 0.01, COLORS["ac"]),
(2, "Fe NP Formation", "decomposition_rate + conditions → NP_size_nm", 0.91, 0.02, COLORS["yw"]),
(3, "CNT Growth", "NP_size + carbon supply + T → cnt_growth_prob", 0.89, 0.02, COLORS["fe"]),
(4, "Reactor Surrogate", "flow, T, geometry → residence_time_s", 0.96, 0.01, COLORS["gd"]),
(5, "CNT Quality", "all inputs → purity_percent, yield, diameter", 0.88, 0.03, COLORS["pu"]),
]
pipe_cols = st.columns(5)
for i, (idx, name, desc, r2, std, color) in enumerate(model_specs_display):
with pipe_cols[i]:
r2_pct = r2 * 100
st.markdown(f"""
R²={r2:.2f}
Model {idx}
{name}
{desc}
±{std:.2f} std
""", unsafe_allow_html=True)
# ── Bayesian Optimisation ────────────────────────────────────────────────
st.divider()
st.markdown('Bayesian Optimisation — Top 5 CNT Synthesis Recipes
', unsafe_allow_html=True)
st.markdown("""
Maximise: purity (30%) +
yield (25%) +
aspect ratio (25%) +
growth probability (20%).
Minimise: defects, residual catalyst, diameter variation.
""", unsafe_allow_html=True)
top_recipes = bayesian_optimization_top_recipes(df)
display_cols = ["temp_C", "H2_sccm", "Ar_sccm", "ferrocene_wt", "sulfur_wt",
"NP_size_nm", "purity_percent", "yield_mg_hr", "aspect_ratio",
"cnt_growth_prob", "optimization_score"]
display_rename = {
"temp_C": "Temp (°C)", "H2_sccm": "H₂ (sccm)", "Ar_sccm": "Ar (sccm)",
"ferrocene_wt": "Ferrocene (wt%)", "sulfur_wt": "Sulfur (wt%)",
"NP_size_nm": "NP Size (nm)", "purity_percent": "Purity (%)",
"yield_mg_hr": "Yield (mg/hr)", "aspect_ratio": "Aspect Ratio",
"cnt_growth_prob": "Growth Prob.", "optimization_score": "Score",
}
styled = top_recipes[display_cols].rename(columns=display_rename)
# Highlight best recipe
st.markdown('', unsafe_allow_html=True)
st.dataframe(
styled.style
.format({
"Temp (°C)": "{:.1f}", "H₂ (sccm)": "{:.1f}", "Ar (sccm)": "{:.1f}",
"Ferrocene (wt%)": "{:.3f}", "Sulfur (wt%)": "{:.3f}",
"NP Size (nm)": "{:.3f}", "Purity (%)": "{:.1f}",
"Yield (mg/hr)": "{:.1f}", "Growth Prob.": "{:.4f}", "Score": "{:.4f}",
})
.bar(subset=["Score"], color="#38bdf8"),
use_container_width=True,
hide_index=True,
)
st.markdown('
', unsafe_allow_html=True)
# Best recipe highlight
best = top_recipes.iloc[0]
st.markdown('🏆 Optimal CNT Synthesis Recipe
', unsafe_allow_html=True)
best_cols = st.columns(5)
best_params = [
("Temperature", f"{best['temp_C']:.1f} °C", COLORS["fe"]),
("H₂ Flow", f"{best['H2_sccm']:.1f} sccm", COLORS["ac"]),
("Ferrocene", f"{best['ferrocene_wt']:.3f} wt%", COLORS["yw"]),
("Sulfur", f"{best['sulfur_wt']:.3f} wt%", COLORS["gd"]),
("NP Size", f"{best['NP_size_nm']:.2f} nm", COLORS["pu"]),
]
for i, (lbl, val, color) in enumerate(best_params):
with best_cols[i]:
st.markdown(f'', unsafe_allow_html=True)
# Predicted outcomes
st.markdown('Predicted CNT Quality Outcomes
', unsafe_allow_html=True)
out_cols = st.columns(4)
outcomes = [
("Purity", f"{best['purity_percent']:.1f}%", COLORS["gd"]),
("Yield", f"{best['yield_mg_hr']:.1f} mg/hr", COLORS["ac"]),
("Aspect Ratio", f"{best['aspect_ratio']:,}", COLORS["yw"]),
("Growth Prob.", f"{best['cnt_growth_prob']:.3f}", COLORS["pu"]),
]
for i, (lbl, val, color) in enumerate(outcomes):
with out_cols[i]:
st.markdown(f'', unsafe_allow_html=True)
# ── Distribution Plots ───────────────────────────────────────────────────
st.divider()
st.markdown('Dataset Distributions
', unsafe_allow_html=True)
col_d1, col_d2 = st.columns(2)
with col_d1:
fig_pur = px.histogram(df, x="purity_percent", nbins=60,
color_discrete_sequence=[COLORS["gd"]], template=PLOTLY_TEMPLATE,
labels={"purity_percent": "Purity (%)", "count": "Runs"})
fig_pur.update_layout(**dark_layout("Purity Distribution", 240))
st.plotly_chart(fig_pur, use_container_width=True)
with col_d2:
fig_yield = px.histogram(df, x="yield_mg_hr", nbins=60,
color_discrete_sequence=[COLORS["ac"]], template=PLOTLY_TEMPLATE,
labels={"yield_mg_hr": "Yield (mg/hr)", "count": "Runs"})
fig_yield.update_layout(**dark_layout("Yield Distribution", 240))
st.plotly_chart(fig_yield, use_container_width=True)
# Download button
st.divider()
csv_data = df.to_csv(index=False)
st.download_button(
"⬇ Download Master Dataset (CSV)",
data=csv_data,
file_name="cnt_master_dataset.csv",
mime="text/csv",
)
# ═══════════════════════════════════════════════════════════════════════════════
# TAB 6 — ReaxFF OPTIMIZATION
# ═══════════════════════════════════════════════════════════════════════════════
with tab6:
st.markdown("""
ReaxFF Parameter Optimization: Interactive protocol for enhancing reactive force field accuracy
using DFT data from LiF/Fe-C systems. CMA-ES genetic algorithm minimizes SSE between DFT and ReaxFF
predictions. Loss function tracks bond energy + van der Waals energy optimization.
""", unsafe_allow_html=True)
# ── Section A: Optimization Campaign ──────────────────────────────────────
st.markdown('Force Field Optimization Campaign — CMA-ES Evolution
', unsafe_allow_html=True)
col_opt1, col_opt2 = st.columns([2, 1])
with col_opt1:
# Simulate optimization
opt_results = simulate_reaxff_optimization(num_iterations=100)
fig_opt = go.Figure()
fig_opt.add_trace(go.Scatter(
x=opt_results["iterations"],
y=opt_results["loss_raw"],
mode="markers",
name="Loss (raw)",
marker=dict(color="#38bdf8", size=4, opacity=0.4),
))
fig_opt.add_trace(go.Scatter(
x=opt_results["iterations"],
y=opt_results["loss_smooth"],
mode="lines",
name="Loss (moving avg)",
line=dict(color="#34d399", width=3),
))
# Mark transition points
fig_opt.add_vline(x=30, line_dash="dash", line_color="rgba(251,191,36,0.6)",
annotation_text="Bond → vdW", annotation_font_color="#fbbf24")
fig_opt.update_layout(**dark_layout("ReaxFF Loss Function Evolution (SSE)", 320),
xaxis_title="Iteration", yaxis_title="Sum of Squared Errors",
legend=dict(bgcolor="rgba(0,0,0,0)", font=dict(color="#c9d6f0", size=10)))
st.plotly_chart(fig_opt, use_container_width=True)
st.markdown(f"""
Optimization converged at iteration {opt_results['convergence_iter']}.
Initial loss: {opt_results['initial_loss']:.1f} → Final loss: {opt_results['final_loss']:.1f}
(reduction: {((opt_results['initial_loss'] - opt_results['final_loss']) / opt_results['initial_loss'] * 100):.1f}%).
""", unsafe_allow_html=True)
with col_opt2:
st.markdown('Optimization Metrics
', unsafe_allow_html=True)
metrics_data = [
("Energy R²", f"{opt_results['energy_r2']:.3f}", COLORS["ac"]),
("Force R²", f"{opt_results['force_r2']:.3f}", COLORS["gd"]),
("Energy RMSE", f"{opt_results['energy_rmse_eV']:.3f} eV", COLORS["yw"]),
("Force RMSE", f"{opt_results['force_rmse_eV_A']:.4f} eV/Å", COLORS["pu"]),
("Convergence", f"{opt_results['convergence_iter']} iter", "#c9d6f0"),
("Algorithm", "CMA-ES", "#c9d6f0"),
]
for label, val, color in metrics_data:
st.markdown(f"{label}{val}
", unsafe_allow_html=True)
st.markdown('Parameter Subsets
', unsafe_allow_html=True)
param_groups = [
("Bond Energy", 48, COLORS["fe"]),
("van der Waals", 18, COLORS["ac"]),
("Angle Energy", 21, COLORS["yw"]),
("Off-Diagonal", 12, COLORS["gd"]),
]
for name, count, color in param_groups:
st.markdown(f"""
""", unsafe_allow_html=True)
# ── Section B: Nucleation Prediction ──────────────────────────────────────
st.divider()
st.markdown('CNT Nucleation Probability Calculator — Multi-Catalyst Comparison
', unsafe_allow_html=True)
col_nuc1, col_nuc2 = st.columns([1, 1])
with col_nuc1:
st.markdown('Simulation Parameters
', unsafe_allow_html=True)
T_nuc = st.slider("Temperature (K)", 800, 1600, 1200, 50, key="nuc_T")
np_size_nuc = st.slider("Nanoparticle Size (nm)", 0.5, 8.0, 2.5, 0.5, key="nuc_size")
carbon_cov = st.slider("Carbon Surface Coverage", 0.0, 1.0, 0.7, 0.05, key="carbon_cov")
sulfur_nuc = st.slider("Sulfur Concentration (ppm)", 0, 1000, 200, 50, key="sulfur_nuc")
with col_nuc2:
# Calculate for selected catalyst
nuc_pred = predict_nucleation_probability(T_nuc, catalyst_type_sel, np_size_nuc, carbon_cov, sulfur_nuc)
st.markdown('Nucleation Prediction
', unsafe_allow_html=True)
prob_color = COLORS["gd"] if nuc_pred["nucleation_prob"] > 0.7 else COLORS["yw"] if nuc_pred["nucleation_prob"] > 0.4 else COLORS["rd"]
# Gauge chart for nucleation probability
fig_nuc_gauge = go.Figure(go.Indicator(
mode="gauge+number",
value=nuc_pred["nucleation_prob"] * 100,
number={"font": {"color": prob_color, "size": 32}, "suffix": "%"},
title={"text": f"Nucleation Probability
{catalyst_type_sel}", "font": {"color": "#94a3b8", "size": 11}},
gauge={
"axis": {"range": [0, 100], "tickcolor": "#64748b"},
"bar": {"color": prob_color},
"bgcolor": "#0a1525",
"steps": [
{"range": [0, 40], "color": "rgba(248,113,113,0.15)"},
{"range": [40, 70], "color": "rgba(251,191,36,0.15)"},
{"range": [70, 100], "color": "rgba(52,211,153,0.15)"},
],
},
))
fig_nuc_gauge.update_layout(paper_bgcolor=PLOTLY_PAPER, font=dict(color="#c9d6f0"), height=220, margin=dict(l=20, r=20, t=40, b=10))
st.plotly_chart(fig_nuc_gauge, use_container_width=True)
nuc_metrics = [
("Energy Barrier", f"{nuc_pred['energy_barrier_eV']} eV", COLORS["fe"]),
("Growth Rate", f"{nuc_pred['growth_rate_nm_s']} nm/s", COLORS["gd"]),
("Size Factor", f"{nuc_pred['size_factor']}", COLORS["ac"]),
("Coverage Factor", f"{nuc_pred['coverage_factor']}", COLORS["yw"]),
("Size Optimality", nuc_pred["optimal_size"], prob_color),
]
for label, val, color in nuc_metrics:
st.markdown(f"{label}{val}
", unsafe_allow_html=True)
# ── Section C: Multi-Catalyst Comparison ────────────────────────────────
st.divider()
st.markdown('Arrhenius Plot — Catalyst Comparison Across Temperature
', unsafe_allow_html=True)
catalyst_types_all = ["Fe", "Fe-C", "Fe-S", "Fe-Mo-C", "Fe-Co-C", "Fe-Ni-C"]
T_range_arr = np.arange(800, 1601, 50)
fig_arr = go.Figure()
colors_arr = [COLORS["fe"], "#8b4513", COLORS["yw"], COLORS["ac"], COLORS["gd"], COLORS["pu"]]
for cat_type, color in zip(catalyst_types_all, colors_arr):
probs = [predict_nucleation_probability(T, cat_type, np_size_nuc, carbon_cov, sulfur_nuc)["nucleation_prob"]
for T in T_range_arr]
fig_arr.add_trace(go.Scatter(
x=1000 / T_range_arr, # 1/T for Arrhenius
y=np.log(np.array(probs) + 1e-10), # ln(prob)
mode="lines+markers",
name=cat_type,
line=dict(color=color, width=2.5),
marker=dict(size=5),
))
fig_arr.update_layout(**dark_layout("Arrhenius Plot: ln(Nucleation Prob) vs 1000/T", 300),
xaxis_title="1000/T (K⁻¹)", yaxis_title="ln(Nucleation Probability)",
legend=dict(bgcolor="rgba(0,0,0,0)", font=dict(color="#c9d6f0", size=10)))
st.plotly_chart(fig_arr, use_container_width=True)
st.markdown("""
Fe-Mo-C shows lowest activation energy (1.6 eV) → highest nucleation probability.
Pure Fe has highest barrier (2.1 eV). Sulfur reduces barrier by ~10%.
Optimal window: 1200–1400 K for SWCNT nucleation.
""", unsafe_allow_html=True)
# ── Section D: DFT Database Overview ──────────────────────────────────────
st.divider()
st.markdown('DFT Training Database Statistics
', unsafe_allow_html=True)
db_stats = [
("DFT Calculations", "300+", COLORS["ac"]),
("Database Entries", "3,000+", COLORS["gd"]),
("Configuration Types", "8", COLORS["yw"]),
("Temperature Range", "300–2500 K", COLORS["fe"]),
]
cols_db = st.columns(4)
for i, (lbl, val, color) in enumerate(db_stats):
with cols_db[i]:
st.markdown(f'', unsafe_allow_html=True)
config_types = [
"Supercells (6 sizes)", "Vacancies (up to 5)", "Strain (3 types, 13 configs)",
"Substitutions (up to 5)", "Interstitials (up to 5)", "Slabs (4 surfaces, 3 thicknesses)",
"Bulk at 300K/500K", "Amorphous at 2500K",
]
col_db1, col_db2 = st.columns(2)
with col_db1:
st.markdown('Configuration Types
', unsafe_allow_html=True)
for cfg in config_types[:4]:
st.markdown(f"✓ {cfg}
", unsafe_allow_html=True)
with col_db2:
st.markdown('
', unsafe_allow_html=True)
for cfg in config_types[4:]:
st.markdown(f"✓ {cfg}
", unsafe_allow_html=True)