import numpy as np import pandas as pd from pathlib import Path np.random.seed(42) def fecp_bond_order(T: float) -> float: return float(np.maximum(0.02, 0.589 - np.maximum(0, (T - 600) / 1400) * 0.57)) def cc_bond_order(T: float) -> float: return float(np.maximum(0.05, 1.22 - np.maximum(0, (T - 1100) / 900) * 1.05)) def ch_bond_order(T: float) -> float: return float(np.maximum(0.04, 0.93 - np.maximum(0, (T - 900) / 1000) * 0.80)) def generate_bond_order_data() -> pd.DataFrame: temps = np.arange(200, 2001, 50) return pd.DataFrame({ "temperature_K": temps, "fecp_bond_order": [fecp_bond_order(T) for T in temps], "cc_bond_order": [cc_bond_order(T) for T in temps], "ch_bond_order": [ch_bond_order(T) for T in temps], "fecp_survival_pct": [min(100, max(2, (fecp_bond_order(T) / 0.589) * 100)) for T in temps], "cc_survival_pct": [min(100, max(3, (cc_bond_order(T) / 1.22) * 100)) for T in temps], "ch_survival_pct": [min(100, max(4, (ch_bond_order(T) / 0.93) * 100)) for T in temps], }) def generate_master_dataset(n: int = 8000) -> pd.DataFrame: """ Synthetic DI-FCCVD reactor runs. Each row = one synthesis experiment. Features span catalyst chemistry → reactor conditions → CNT quality. Includes multiple catalyst types: Fe, Fe-C, Fe-S, Fe-Mo-C, Fe-Co-C, Fe-Ni-C """ rng = np.random.default_rng(42) # CNT product type selection cnt_types = rng.choice(['SWCNT', 'DWCNT', 'MWCNT'], size=n, p=[0.4, 0.3, 0.3]) # Catalyst composition catalyst_types = rng.choice(['Fe', 'Fe-C', 'Fe-S', 'Fe-Mo-C', 'Fe-Co-C', 'Fe-Ni-C'], size=n, p=[0.2, 0.15, 0.2, 0.15, 0.15, 0.15]) temp_C = rng.uniform(600, 1100, n) H2_sccm = rng.uniform(50, 500, n) Ar_sccm = rng.uniform(100, 1000, n) ferrocene_wt = rng.uniform(0.5, 5.0, n) sulfur_wt = rng.uniform(0.0, 1.0, n) # Add promoter metals mo_ppm = np.where(np.char.find(catalyst_types.astype(str), 'Mo') >= 0, rng.uniform(10, 200, n), 0.0) co_ppm = np.where(np.char.find(catalyst_types.astype(str), 'Co') >= 0, rng.uniform(50, 500, n), 0.0) ni_ppm = np.where(np.char.find(catalyst_types.astype(str), 'Ni') >= 0, rng.uniform(50, 500, n), 0.0) injection_depth_cm = rng.uniform(5, 25, n) temp_norm = (temp_C - 600) / 500 h2_norm = H2_sccm / 500 fe_norm = ferrocene_wt / 5.0 s_norm = sulfur_wt / 1.0 # Decomposition rate — higher T, more ferrocene → higher rate decomposition_rate = ( 0.3 + 0.5 * temp_norm + 0.2 * fe_norm + rng.normal(0, 0.03, n) ).clip(0.05, 1.0) # NP size — optimal ~1300°C, sulfur reduces size np_size_nm = ( 2.5 + 2.0 * np.sin(temp_norm * np.pi) - 0.8 * s_norm + 0.3 * fe_norm + rng.normal(0, 0.2, n) ).clip(0.5, 8.0) # Residence time — depends on flow rates and injection depth residence_time_s = ( (injection_depth_cm / 15) * (800 / (H2_sccm + Ar_sccm)) * 10 + rng.normal(0, 0.5, n) ).clip(0.5, 20.0) # CNT diameter ≈ 1.8 * NP radius (empirical scaling) CNT_diameter_nm = (np_size_nm * 0.85 + rng.normal(0, 0.15, n)).clip(0.5, 7.0) # Purity — high T, high H2, sulfur additive all help purity_percent = ( 40 + 25 * temp_norm + 15 * h2_norm + 10 * s_norm - 5 * np.abs(np_size_nm - 2.0) + rng.normal(0, 3, n) ).clip(20, 99) # Yield — high ferrocene, high T, good residence time yield_mg_hr = ( 50 + 120 * fe_norm + 80 * temp_norm + 30 * (residence_time_s / 20) + rng.normal(0, 10, n) ).clip(5, 400) # Aspect ratio — long CNTs need right temp + H2 + injection depth aspect_ratio = ( 500 + 2000 * temp_norm * h2_norm + 300 * (injection_depth_cm / 25) - 100 * CNT_diameter_nm + rng.normal(0, 200, n) ).clip(100, 10000) # CNT growth probability score (target variable) cnt_growth_prob = ( 0.1 + 0.3 * temp_norm + 0.2 * h2_norm + 0.15 * s_norm + 0.15 * (1 - np.abs(np_size_nm - 2.0) / 6) + 0.1 * fe_norm + rng.normal(0, 0.04, n) ).clip(0.02, 0.98) # Wall thickness (for DWCNT and MWCNT) wall_layers = np.where(cnt_types == 'SWCNT', 1, np.where(cnt_types == 'DWCNT', 2, rng.integers(3, 15, n))) # Nucleation energy barrier (catalyst-dependent) nucleation_barrier_eV = np.where(catalyst_types == 'Fe', 2.1, np.where(catalyst_types == 'Fe-S', 1.8, np.where(catalyst_types == 'Fe-Mo-C', 1.6, np.where(catalyst_types == 'Fe-Co-C', 1.7, np.where(catalyst_types == 'Fe-Ni-C', 1.75, 1.9))))) df = pd.DataFrame({ "cnt_type": cnt_types, "catalyst_type": catalyst_types, "temp_C": np.round(temp_C, 1), "H2_sccm": np.round(H2_sccm, 1), "Ar_sccm": np.round(Ar_sccm, 1), "ferrocene_wt": np.round(ferrocene_wt, 3), "sulfur_wt": np.round(sulfur_wt, 3), "mo_ppm": np.round(mo_ppm, 1), "co_ppm": np.round(co_ppm, 1), "ni_ppm": np.round(ni_ppm, 1), "injection_depth_cm": np.round(injection_depth_cm, 1), "decomposition_rate": np.round(decomposition_rate, 4), "NP_size_nm": np.round(np_size_nm, 3), "residence_time_s": np.round(residence_time_s, 2), "CNT_diameter_nm": np.round(CNT_diameter_nm, 3), "wall_layers": wall_layers, "nucleation_barrier_eV": np.round(nucleation_barrier_eV, 3), "purity_percent": np.round(purity_percent, 1), "yield_mg_hr": np.round(yield_mg_hr, 1), "aspect_ratio": np.round(aspect_ratio).astype(int), "cnt_growth_prob": np.round(cnt_growth_prob, 4), }) return df def get_ferrocene_atoms(T_K: float): """ Returns atom positions for two ferrocene molecules in a simulation box. Positions jitter with temperature. """ rng = np.random.default_rng(int(T_K * 1000) % 2**31) f = max(0.0, min(1.0, (T_K - 200) / 1800)) atoms = [] for mol_offset, cx, cz in [("A", -4, 0), ("B", 4, 2)]: # Fe displacement increases with T above 1100 K fe_disp = f * 2.5 if T_K > 1100 else 0 amp_fe = 0.08 + f * 0.15 fe_pos = np.array([ cx + (rng.random() - 0.5) * amp_fe * 2 + fe_disp * (rng.random() - 0.5), 0 + (rng.random() - 0.5) * amp_fe * 2 + fe_disp * (rng.random() - 0.5), cz + (rng.random() - 0.5) * amp_fe * 2 + fe_disp * (rng.random() - 0.5), ]) atoms.append({"type": "Fe", "x": fe_pos[0], "y": fe_pos[1], "z": fe_pos[2], "mol": mol_offset}) for ring_y in [1.65, -1.65]: for i in range(5): angle = i * 2 * np.pi / 5 r = 2.0 amp_c = 0.08 + f * 0.25 cx_c = cx + r * np.cos(angle) cz_c = cz + r * np.sin(angle) atoms.append({ "type": "C", "x": cx_c + (rng.random() - 0.5) * amp_c * 2, "y": ring_y + (rng.random() - 0.5) * amp_c * 2, "z": cz_c + (rng.random() - 0.5) * amp_c * 2, "mol": mol_offset, }) amp_h = 0.08 + f * 0.45 hx = cx + (r + 1.1) * np.cos(angle) hz = cz + (r + 1.1) * np.sin(angle) atoms.append({ "type": "H", "x": hx + (rng.random() - 0.5) * amp_h * 2, "y": ring_y + (rng.random() - 0.5) * amp_h * 2, "z": hz + (rng.random() - 0.5) * amp_h * 2, "mol": mol_offset, }) return atoms def save_dataset(output_path: str = "data/master_dataset.csv"): df = generate_master_dataset() Path(output_path).parent.mkdir(parents=True, exist_ok=True) df.to_csv(output_path, index=False) return df