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"""
{label}{val}%
""", 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"""
{name}{val}%
""", 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"""
{num}
{lbl}
""", 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"""
{name}{val}%
""", 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'
{num}
{lbl}
', 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'
{val}
{lbl}
', 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'
{val}
{lbl}
', 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"""
{name}{count}
""", 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'
{val}
{lbl}
', 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)