Spaces:
Running
Running
| 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 | |