Spaces:
Running
Running
| 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 ─────────────────────────────────────────────────────────────── | |
| def load_bond_data() -> pd.DataFrame: | |
| return generate_bond_order_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 | |
| 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(""" | |
| <style> | |
| /* Dark theme accents */ | |
| .stApp { background: #07101f; } | |
| .metric-card { | |
| background: #0f1a2e; | |
| border: 1px solid #1a3050; | |
| border-radius: 10px; | |
| padding: 0.75rem 1rem; | |
| text-align: center; | |
| } | |
| .metric-num { font-size: 1.6rem; font-weight: 800; color: #38bdf8; } | |
| .metric-lbl { font-size: 0.7rem; color: #64748b; text-transform: uppercase; letter-spacing: 0.07em; margin-top: 0.2rem; } | |
| .bar-container { background: #0a1525; border-radius: 99px; height: 10px; overflow: hidden; margin-top: 4px; } | |
| .bar-fill { height: 100%; border-radius: 99px; } | |
| .section-title { | |
| font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.1em; | |
| color: #38bdf8; border-bottom: 1px solid #1a3050; padding-bottom: 0.3rem; | |
| margin-bottom: 0.5rem; font-weight: 700; | |
| } | |
| .obs-box { | |
| background: #0a1525; border: 1px solid #1a3050; border-radius: 8px; | |
| padding: 0.75rem 1rem; font-size: 0.85rem; line-height: 1.65; color: #c9d6f0; | |
| } | |
| .frame-desc { | |
| background: #0a1525; border-radius: 8px; padding: 0.75rem; | |
| font-size: 0.82rem; line-height: 1.65; color: #c9d6f0; min-height: 120px; | |
| } | |
| .recipe-card { | |
| background: #0f1a2e; border: 1px solid #1a3050; border-radius: 10px; padding: 1rem; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ── Sidebar ─────────────────────────────────────────────────────────────────── | |
| with st.sidebar: | |
| st.markdown('<p style="color:#38bdf8;font-size:1.1rem;font-weight:800;letter-spacing:.03em">⚗️ Fe(Cp)₂ → CNT Platform</p>', unsafe_allow_html=True) | |
| st.markdown('<p style="color:#64748b;font-size:.78rem">AI-Driven CNT Manufacturing Simulator</p>', unsafe_allow_html=True) | |
| st.divider() | |
| st.markdown('<div class="section-title">Global Controls</div>', 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('<div class="section-title">System Status</div>', 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:** <span style="color:{cnt_color};font-weight:700">{m["cnt_potential_score"]}</span>', unsafe_allow_html=True) | |
| st.divider() | |
| st.markdown('<p style="color:#64748b;font-size:.72rem">ReaxFF MD · 91 Temperature Points · 13.6M Timesteps · GPU Accelerated</p>', 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(""" | |
| <div class="obs-box"> | |
| <b>Ferrocene Decomposition — Digital Twin Reactor:</b> 3D simulation of two ferrocene molecules | |
| (Fe, C, H atoms) under reactive MD conditions. ReaxFF molecular dynamics scans 200 K → 2000 K. | |
| <b style="color:#f97316">Fe</b> atoms are orange, <b style="color:#94a3b8">C</b> dark grey, | |
| <b style="color:#e2e8f0">H</b> white. Drag to rotate. | |
| </div> | |
| """, 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"<b>{atype}</b><br>x: %{{x:.2f}} Å<br>y: %{{y:.2f}} Å<br>z: %{{z:.2f}} Å<extra></extra>", | |
| )) | |
| # 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 <b>{T_twin} K</b>: Molecules are thermally stable. All Fe–Cp bonds intact. Atoms vibrate around equilibrium. Ferrocene retains sandwich geometry." | |
| elif T_twin < 900: | |
| obs = f"At <b>{T_twin} K</b>: 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 <b>{T_twin} K</b>: 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 <b>{T_twin} K</b>: 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 <b>{T_twin} K</b>: Complete ferrocene decomposition. Fe atoms aggregating into catalyst nanoparticle. CNT nucleation potential is <b style='color:#34d399'>HIGH</b>. Cp fragments may further pyrolyze." | |
| st.markdown(f'<div class="obs-box">{obs}</div>', unsafe_allow_html=True) | |
| with col_metrics: | |
| st.markdown('<div class="section-title">Live Reactor Metrics</div>', 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"<div style='display:flex;justify-content:space-between;font-size:.82rem;padding:.2rem 0'><span style='color:#64748b'>{label}</span><span style='font-weight:700;color:{color}'>{val}</span></div>", unsafe_allow_html=True) | |
| st.markdown('<div class="section-title" style="margin-top:.8rem">Bond Statistics</div>', 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"<div style='display:flex;justify-content:space-between;font-size:.82rem;padding:.2rem 0'><span style='color:#64748b'>{label}</span><span style='font-weight:700;color:{color}'>{val}</span></div>", unsafe_allow_html=True) | |
| st.markdown('<div class="section-title" style="margin-top:.8rem">Cluster Analysis</div>', 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"<div style='display:flex;justify-content:space-between;font-size:.82rem;padding:.2rem 0'><span style='color:#64748b'>{label}</span><span style='font-weight:700;color:{color}'>{val}</span></div>", unsafe_allow_html=True) | |
| st.markdown('<div class="section-title" style="margin-top:.8rem">System Info</div>', 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"<div style='display:flex;justify-content:space-between;font-size:.82rem;padding:.15rem 0'><span style='color:#64748b'>{label}</span><span style='font-weight:700;color:{color}'>{val}</span></div>", 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 <b>0.589</b>. 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 <b>~0.42</b>. 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 <b>~0.18</b> — near breaking threshold. Asymmetric distortion is the precursor to full Fe release. <b>CpFe• radical</b> 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. <b>Optimal catalyst size</b> for SWCNT nucleation via vapour-liquid-solid (VLS) mechanism. CNT nucleation probability: <b style='color:#34d399'>HIGH</b>. Expected CNT diameter ≈ 1.5 nm."}, | |
| ] | |
| # ── Section A: Molecular Movie ────────────────────────────────────────── | |
| st.markdown('<div class="section-title">Molecular Decomposition Movie — Frame-by-Frame Pathway</div>', 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 = """ | |
| <div style="font-size:.8rem;line-height:1.9;padding:.5rem"> | |
| <span style="color:#f97316"><b>T ≈ 900–1100 K</b></span> — Fe–Cp π-bond weakens<br> | |
| <span style="color:#fbbf24"><b>T ≈ 1100–1300 K</b></span> — Cp ring distortion & separation<br> | |
| <span style="color:#34d399"><b>T > 1300 K</b></span> — Fe atom released to gas phase<br> | |
| <span style="color:#38bdf8"><b>T > 1400 K</b></span> — Fe aggregation → nanoparticle<br> | |
| <span style="color:#a78bfa"><b>T > 1500 K</b></span> — Catalyst nanoparticle (CNT nucleation site) | |
| </div>""" | |
| st.markdown(pathway_html, unsafe_allow_html=True) | |
| with col_framedesc: | |
| frame = FRAMES[frame_idx] | |
| st.markdown(f'<p style="font-size:.8rem;color:{frame["color"]};font-weight:700">Frame {frame_idx+1}/6 — {frame["name"]} @ {frame["T"]}</p>', unsafe_allow_html=True) | |
| st.markdown(f'<div class="frame-desc">{frame["desc"]}</div>', unsafe_allow_html=True) | |
| # Frame progress bars | |
| st.markdown('<div class="section-title" style="margin-top:.8rem">Decomposition State</div>', 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""" | |
| <div style="margin:.35rem 0"> | |
| <div style="display:flex;justify-content:space-between;font-size:.75rem;color:#94a3b8"> | |
| <span>{label}</span><span style="color:{color};font-weight:700">{val}%</span> | |
| </div> | |
| <div class="bar-container"><div class="bar-fill" style="width:{val}%;background:{color}"></div></div> | |
| </div>""", unsafe_allow_html=True) | |
| # ── Section B: Bond Order vs Temperature ─────────────────────────────── | |
| st.divider() | |
| st.markdown('<div class="section-title">Bond Order vs Temperature — Animated Temperature Sweep</div>', 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(""" | |
| <p style="font-size:.78rem;color:#64748b"> | |
| <span style="color:#f97316">●</span> <b>Fe–Cp</b> (orange) weakens first — thermally labile organometallic π-bond. | |
| <span style="color:#38bdf8;margin-left:1rem">●</span> <b>C–C</b> (cyan) most covalently robust. | |
| <span style="color:#fbbf24;margin-left:1rem">●</span> <b>C–H</b> (yellow) moderately stable. | |
| The intersection of Fe–Cp with the dashed threshold gives T<sub>decomp</sub>. | |
| </p>""", unsafe_allow_html=True) | |
| # ── Section C: Bond Breaking Network ────────────────────────────────── | |
| st.divider() | |
| st.markdown('<div class="section-title">Bond Survival Landscape — Temperature vs Bond Strength</div>', 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('<div class="section-title">Fe Nanoparticle Formation — Catalyst Evolution Simulator</div>', unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div class="obs-box"> | |
| After ferrocene decomposes, released Fe atoms aggregate due to Fe–Fe attractive interactions. | |
| <b>Fe cluster size 1–5 nm</b> is the optimal range for floating-catalyst CVD CNT growth. | |
| </div>""", 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('<div class="section-title">Cluster Growth Metrics</div>', 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"<div style='display:flex;justify-content:space-between;font-size:.82rem;padding:.25rem 0'><span style='color:#64748b'>{label}</span><span style='font-weight:700;color:{color}'>{val}</span></div>", 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""" | |
| <div style="margin-top:.8rem;padding:.7rem;background:#0a1525;border-radius:8px;border:1px solid #1a3050;font-size:.82rem"> | |
| <div style="color:{suit_color};font-weight:700">{suitability}</div> | |
| <div style="color:#64748b;margin-top:.3rem">Target: <b>1–5 nm</b> Fe clusters (3–15 atoms) for optimal SWCNT nucleation. | |
| Larger clusters produce MWCNTs. Cluster radius determines CNT outer diameter.</div> | |
| </div>""", unsafe_allow_html=True) | |
| # ── Section B: CNT Growth Predictor ──────────────────────────────────── | |
| st.divider() | |
| st.markdown('<div class="section-title">CNT Growth Predictor — Catalyst Activity Calculator</div>', unsafe_allow_html=True) | |
| col_inp, col_out = st.columns([1, 1]) | |
| with col_inp: | |
| st.markdown('<div class="section-title">Simulation Inputs</div>', 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"<div style='display:flex;justify-content:space-between;font-size:.82rem;padding:.22rem 0'><span style='color:#64748b'>{label}</span><span style='font-weight:700;color:{color}'>{val}</span></div>", unsafe_allow_html=True) | |
| # ── Section C: CNT Property Heatmap ──────────────────────────────────── | |
| st.divider() | |
| st.markdown('<div class="section-title">CNT Nucleation Probability — T vs Cluster Size Heatmap</div>', 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<br>Cluster: %{y} atoms<br>Prob: %{z:.0f}%<extra></extra>", | |
| )) | |
| 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('<div class="section-title">Reaction Pathway Tree — Ferrocene Decomposition Mechanism</div>', 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('<div class="section-title">Pathway Probabilities</div>', unsafe_allow_html=True) | |
| for name, val, color in probs: | |
| st.markdown(f""" | |
| <div style="margin:.4rem 0"> | |
| <div style="display:flex;justify-content:space-between;font-size:.78rem;color:#94a3b8"> | |
| <span>{name}</span><span style="color:{color};font-weight:700">{val}%</span> | |
| </div> | |
| <div class="bar-container"><div class="bar-fill" style="width:{val}%;background:{color}"></div></div> | |
| </div>""", unsafe_allow_html=True) | |
| with col_key: | |
| st.markdown('<div class="section-title">Rate-Limiting Step</div>', unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div class="obs-box" style="font-size:.82rem"> | |
| The <b>Fe–Cp π-bond dissociation</b> is the rate-limiting step. This organometallic bond has | |
| an activation energy <b>E_a ≈ 2.1 eV</b> derived from temperature-dependent ReaxFF analysis. | |
| All downstream CNT formation steps proceed at higher rates once this barrier is crossed. | |
| <br><br> | |
| <b>Key Intermediate:</b> The <b>CpFe• radical</b> (mono-decapitated ferrocene) is the primary | |
| intermediate with a calculated lifetime of ~0.8 ps at 1400 K before complete Fe release. | |
| </div>""", unsafe_allow_html=True) | |
| # ── Executive Summary ─────────────────────────────────────────────────── | |
| st.divider() | |
| st.markdown('<div class="section-title">Project Status — Executive Summary Dashboard</div>', 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""" | |
| <div class="metric-card" style="margin:.3rem 0"> | |
| <div class="metric-num">{num}</div> | |
| <div class="metric-lbl">{lbl}</div> | |
| </div>""", unsafe_allow_html=True) | |
| col_pipe, col_results = st.columns(2) | |
| with col_pipe: | |
| st.markdown('<div class="section-title" style="margin-top:1rem">Pipeline Completion</div>', 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""" | |
| <div style="margin:.4rem 0"> | |
| <div style="display:flex;justify-content:space-between;font-size:.78rem;color:#94a3b8"> | |
| <span>{name}</span><span style="color:{color};font-weight:700">{val}%</span> | |
| </div> | |
| <div class="bar-container"><div class="bar-fill" style="width:{val}%;background:{color}"></div></div> | |
| </div>""", unsafe_allow_html=True) | |
| with col_results: | |
| st.markdown('<div class="section-title" style="margin-top:1rem">Key Results</div>', 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"<div style='font-size:.82rem;padding:.2rem 0;color:#c9d6f0'><span style='color:{color}'>{icon}</span> {text}</div>", unsafe_allow_html=True) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # TAB 5 — AI PIPELINE | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| with tab5: | |
| st.markdown(""" | |
| <div class="obs-box"> | |
| <b>Full AI Pipeline:</b> 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. | |
| </div>""", unsafe_allow_html=True) | |
| df = load_master_dataset() | |
| # ── Dataset Overview ──────────────────────────────────────────────────── | |
| st.markdown('<div class="section-title" style="margin-top:1rem">Master Dataset Overview</div>', 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'<div class="metric-card"><div class="metric-num">{num}</div><div class="metric-lbl">{lbl}</div></div>', unsafe_allow_html=True) | |
| # ── CNT Type & Catalyst Type Distribution ──────────────────────────────── | |
| st.markdown('<div class="section-title" style="margin-top:1rem">Product & Catalyst Distribution</div>', 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('<div class="section-title" style="margin-top:1rem">Temperature vs CNT Purity</div>', 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('<div class="section-title" style="margin-top:1rem">Feature Correlations</div>', 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('<div class="section-title">5-Stage AI Pipeline — Model Performance (5-Fold CV R²)</div>', 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""" | |
| <div style="background:#0f1a2e;border:1px solid #1a3050;border-radius:10px;padding:.8rem;text-align:center"> | |
| <div style="font-size:1.4rem;font-weight:800;color:{color}">R²={r2:.2f}</div> | |
| <div style="font-size:.65rem;color:{color};font-weight:700;margin:.2rem 0">Model {idx}</div> | |
| <div style="font-size:.72rem;font-weight:700;color:#c9d6f0;margin:.3rem 0">{name}</div> | |
| <div style="font-size:.65rem;color:#64748b;line-height:1.5">{desc}</div> | |
| <div class="bar-container" style="margin-top:.5rem"> | |
| <div class="bar-fill" style="width:{r2_pct:.0f}%;background:{color}"></div> | |
| </div> | |
| <div style="font-size:.65rem;color:#475569;margin-top:.25rem">±{std:.2f} std</div> | |
| </div>""", unsafe_allow_html=True) | |
| # ── Bayesian Optimisation ──────────────────────────────────────────────── | |
| st.divider() | |
| st.markdown('<div class="section-title">Bayesian Optimisation — Top 5 CNT Synthesis Recipes</div>', unsafe_allow_html=True) | |
| st.markdown(""" | |
| <p style="font-size:.8rem;color:#64748b"> | |
| Maximise: <span style="color:#34d399">purity (30%)</span> + | |
| <span style="color:#38bdf8">yield (25%)</span> + | |
| <span style="color:#fbbf24">aspect ratio (25%)</span> + | |
| <span style="color:#a78bfa">growth probability (20%)</span>. | |
| Minimise: defects, residual catalyst, diameter variation. | |
| </p>""", 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('<div class="recipe-card" style="margin:.5rem 0">', 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('</div>', unsafe_allow_html=True) | |
| # Best recipe highlight | |
| best = top_recipes.iloc[0] | |
| st.markdown('<div class="section-title" style="margin-top:1rem">🏆 Optimal CNT Synthesis Recipe</div>', 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'<div class="metric-card"><div class="metric-num" style="font-size:1.1rem;color:{color}">{val}</div><div class="metric-lbl">{lbl}</div></div>', unsafe_allow_html=True) | |
| # Predicted outcomes | |
| st.markdown('<div class="section-title" style="margin-top:1rem">Predicted CNT Quality Outcomes</div>', 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'<div class="metric-card" style="margin-top:.3rem"><div class="metric-num" style="color:{color}">{val}</div><div class="metric-lbl">{lbl}</div></div>', unsafe_allow_html=True) | |
| # ── Distribution Plots ─────────────────────────────────────────────────── | |
| st.divider() | |
| st.markdown('<div class="section-title">Dataset Distributions</div>', 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(""" | |
| <div class="obs-box"> | |
| <b>ReaxFF Parameter Optimization:</b> 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. <b style="color:#38bdf8">Loss function</b> tracks bond energy + van der Waals energy optimization. | |
| </div>""", unsafe_allow_html=True) | |
| # ── Section A: Optimization Campaign ────────────────────────────────────── | |
| st.markdown('<div class="section-title" style="margin-top:1rem">Force Field Optimization Campaign — CMA-ES Evolution</div>', 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""" | |
| <p style="font-size:.8rem;color:#64748b"> | |
| Optimization converged at iteration <b style="color:#34d399">{opt_results['convergence_iter']}</b>. | |
| Initial loss: <b>{opt_results['initial_loss']:.1f}</b> → Final loss: <b>{opt_results['final_loss']:.1f}</b> | |
| (reduction: <b style="color:#34d399">{((opt_results['initial_loss'] - opt_results['final_loss']) / opt_results['initial_loss'] * 100):.1f}%</b>). | |
| </p>""", unsafe_allow_html=True) | |
| with col_opt2: | |
| st.markdown('<div class="section-title">Optimization Metrics</div>', 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"<div style='display:flex;justify-content:space-between;font-size:.82rem;padding:.25rem 0'><span style='color:#64748b'>{label}</span><span style='font-weight:700;color:{color}'>{val}</span></div>", unsafe_allow_html=True) | |
| st.markdown('<div class="section-title" style="margin-top:.8rem">Parameter Subsets</div>', 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""" | |
| <div style="margin:.35rem 0"> | |
| <div style="display:flex;justify-content:space-between;font-size:.75rem;color:#94a3b8"> | |
| <span>{name}</span><span style="color:{color};font-weight:700">{count}</span> | |
| </div> | |
| <div class="bar-container"><div class="bar-fill" style="width:{count * 1.5}%;background:{color}"></div></div> | |
| </div>""", unsafe_allow_html=True) | |
| # ── Section B: Nucleation Prediction ────────────────────────────────────── | |
| st.divider() | |
| st.markdown('<div class="section-title">CNT Nucleation Probability Calculator — Multi-Catalyst Comparison</div>', unsafe_allow_html=True) | |
| col_nuc1, col_nuc2 = st.columns([1, 1]) | |
| with col_nuc1: | |
| st.markdown('<div class="section-title">Simulation Parameters</div>', 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('<div class="section-title">Nucleation Prediction</div>', 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<br>{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"<div style='display:flex;justify-content:space-between;font-size:.8rem;padding:.2rem 0'><span style='color:#64748b'>{label}</span><span style='font-weight:700;color:{color}'>{val}</span></div>", unsafe_allow_html=True) | |
| # ── Section C: Multi-Catalyst Comparison ──────────────────────────────── | |
| st.divider() | |
| st.markdown('<div class="section-title">Arrhenius Plot — Catalyst Comparison Across Temperature</div>', 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(""" | |
| <p style="font-size:.8rem;color:#64748b"> | |
| <b style="color:#fbbf24">Fe-Mo-C</b> shows lowest activation energy (1.6 eV) → highest nucleation probability. | |
| <b style="color:#f97316">Pure Fe</b> has highest barrier (2.1 eV). <b style="color:#fbbf24">Sulfur</b> reduces barrier by ~10%. | |
| <b style="color:#34d399">Optimal window:</b> 1200–1400 K for SWCNT nucleation. | |
| </p>""", unsafe_allow_html=True) | |
| # ── Section D: DFT Database Overview ────────────────────────────────────── | |
| st.divider() | |
| st.markdown('<div class="section-title">DFT Training Database Statistics</div>', 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'<div class="metric-card"><div class="metric-num" style="color:{color}">{val}</div><div class="metric-lbl">{lbl}</div></div>', 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('<div class="section-title" style="margin-top:.8rem">Configuration Types</div>', unsafe_allow_html=True) | |
| for cfg in config_types[:4]: | |
| st.markdown(f"<div style='font-size:.82rem;padding:.2rem 0;color:#c9d6f0'><span style='color:#34d399'>✓</span> {cfg}</div>", unsafe_allow_html=True) | |
| with col_db2: | |
| st.markdown('<div class="section-title" style="margin-top:.8rem"> </div>', unsafe_allow_html=True) | |
| for cfg in config_types[4:]: | |
| st.markdown(f"<div style='font-size:.82rem;padding:.2rem 0;color:#c9d6f0'><span style='color:#34d399'>✓</span> {cfg}</div>", unsafe_allow_html=True) | |