Spaces:
Running
Running
| """CLI for the biofuel screening tool.""" | |
| from dataclasses import dataclass, field | |
| from typing import Optional, List, Dict, Tuple | |
| class ScreeningConfig: | |
| csv_path: str | |
| mode: str # "pure_component" or "mixture" | |
| target_cn: float | |
| filters: Dict = field(default_factory=lambda: { | |
| "bp": (60.0, 250.0), | |
| "density": (720.0, None), | |
| "lhv": (30.0, None), | |
| "dynamic_viscosity": (2.0, None), | |
| }) | |
| def _get_csv_path() -> str: | |
| import os | |
| while True: | |
| path = input("Path to CSV file (must have a 'smiles' column): ").strip() | |
| if not path: | |
| print(" Path cannot be empty.") | |
| continue | |
| if not os.path.isfile(path): | |
| print(f" File not found: {path}") | |
| continue | |
| return path | |
| def _get_filter_bounds(label: str, default_lo, default_hi) -> Tuple: | |
| lo_str = input(f" Min {label} [{default_lo}]: ").strip() | |
| hi_str = input(f" Max {label} [{default_hi if default_hi is not None else 'none'}]: ").strip() | |
| try: | |
| lo = float(lo_str) if lo_str else default_lo | |
| except ValueError: | |
| lo = default_lo | |
| try: | |
| hi = float(hi_str) if hi_str else default_hi | |
| except ValueError: | |
| hi = default_hi | |
| return (lo, hi) | |
| def _get_filter_config() -> Dict: | |
| print("\n Boiling Point (°C):") | |
| bp = _get_filter_bounds("BP (°C)", 60.0, 250.0) | |
| print(" Density (kg/m³):") | |
| density_lo, _ = _get_filter_bounds("density", 720.0, None) | |
| print(" LHV (MJ/kg):") | |
| lhv_lo, _ = _get_filter_bounds("LHV", 30.0, None) | |
| print(" Dynamic Viscosity (cSt):") | |
| visc_lo, _ = _get_filter_bounds("viscosity", 2.0, None) | |
| return { | |
| "bp": bp, | |
| "density": (density_lo, None), | |
| "lhv": (lhv_lo, None), | |
| "dynamic_viscosity": (visc_lo, None), | |
| } | |
| def _get_custom_base_fuel(): | |
| from rdkit import Chem | |
| from rdkit.Chem import Descriptors | |
| print("\n" + "=" * 70) | |
| print("CUSTOM BASE FUEL CONFIGURATION") | |
| print("=" * 70) | |
| while True: | |
| s = input("\nNumber of base fuel components (2-10): ").strip() | |
| if not s.isdigit(): | |
| print(" Enter a positive integer.") | |
| continue | |
| n = int(s) | |
| if n < 2: | |
| print(" Must have at least 2 components.") | |
| continue | |
| if n > 10: | |
| if input(" Warning: many components may slow predictions. Continue? (y/n): ").strip().lower() != "y": | |
| continue | |
| break | |
| smiles_list, frac_list, total = [], [], 0.0 | |
| for i in range(n): | |
| print(f"\nComponent {i + 1}/{n}:") | |
| while True: | |
| smi = input(" SMILES: ").strip() | |
| if not smi: | |
| print(" Cannot be empty.") | |
| continue | |
| mol = Chem.MolFromSmiles(smi) | |
| if mol is None: | |
| print(f" Invalid SMILES.") | |
| continue | |
| formula = Chem.rdMolDescriptors.CalcMolFormula(mol) | |
| mw = Descriptors.MolWt(mol) | |
| print(f" {formula} (MW: {mw:.2f} g/mol)") | |
| break | |
| while True: | |
| if i == n - 1: | |
| rem = 1.0 - total | |
| print(f" Remaining fraction: {rem:.4f}") | |
| if input(" Use remaining? (y/n): ").strip().lower() == "y": | |
| frac = rem | |
| break | |
| try: | |
| frac = float(input(f" Mole fraction (remaining: {1.0 - total:.4f}): ").strip()) | |
| except ValueError: | |
| print(" Invalid number.") | |
| continue | |
| if frac <= 0 or total + frac > 1.0001: | |
| print(" Invalid fraction.") | |
| continue | |
| break | |
| smiles_list.append(smi) | |
| frac_list.append(frac) | |
| total += frac | |
| if abs(total - 1.0) > 1e-6: | |
| frac_list = [f / total for f in frac_list] | |
| print("\n" + "=" * 70) | |
| print("CUSTOM BASE FUEL SUMMARY") | |
| print("=" * 70) | |
| for j, (s, f) in enumerate(zip(smiles_list, frac_list), 1): | |
| mol = Chem.MolFromSmiles(s) | |
| formula = Chem.rdMolDescriptors.CalcMolFormula(mol) | |
| print(f" {j}. {formula:<15} {s[:40]:<42} {f:.4f}") | |
| print(f" {'TOTAL':<58} {sum(frac_list):.4f}") | |
| print("=" * 70) | |
| if input("\nUse this custom base fuel? (y/n): ").strip().lower() != "y": | |
| print(" Cancelled — defaulting to fossil diesel.") | |
| return None, None | |
| return smiles_list, frac_list | |
| def get_user_config() -> ScreeningConfig: | |
| print("=" * 70) | |
| print("BIOFUEL SCREENING TOOL") | |
| print("=" * 70) | |
| csv_path = _get_csv_path() | |
| print("\nScreening Mode:") | |
| print(" 1. Pure component screening") | |
| print(" 2. Mixture / blend screening") | |
| while True: | |
| choice = input("Select mode (1 or 2): ").strip() | |
| if choice in ("1", "2"): | |
| break | |
| print(" Please enter 1 or 2.") | |
| mode = "pure_component" if choice == "1" else "mixture" | |
| print(f" Mode: {'Pure Component' if mode == 'pure_component' else 'Mixture'} Screening") | |
| while True: | |
| try: | |
| target_cn = float(input("\nTarget cetane number (CN): ").strip()) | |
| if target_cn <= 0: | |
| print(" CN must be positive.") | |
| continue | |
| break | |
| except ValueError: | |
| print(" Invalid input.") | |
| if mode == "pure_component": | |
| print("\nProperty Constraint Filters:") | |
| print(" Defaults: BP 60–250 °C | density ≥ 720 kg/m³ | LHV ≥ 30 MJ/kg | viscosity ≥ 2 cSt") | |
| if input("Use defaults? (y/n) [y]: ").strip().lower() == "n": | |
| filters = _get_filter_config() | |
| else: | |
| filters = { | |
| "bp": (60.0, 250.0), | |
| "density": (720.0, None), | |
| "lhv": (30.0, None), | |
| "dynamic_viscosity": (2.0, None), | |
| } | |
| print("\n" + "=" * 70) | |
| print("CONFIGURATION SUMMARY") | |
| print("=" * 70) | |
| print(f" Mode: Pure Component Screening") | |
| print(f" CSV: {csv_path}") | |
| print(f" Target CN: {target_cn}") | |
| for prop, (lo, hi) in filters.items(): | |
| bounds = f"{lo if lo is not None else '—'} – {hi if hi is not None else '—'}" | |
| print(f" {prop}: {bounds}") | |
| print("=" * 70) | |
| return ScreeningConfig(csv_path=csv_path, mode=mode, target_cn=target_cn, filters=filters) | |
| # --- mixture mode --- | |
| # CSV must follow mixture_database.csv format: | |
| # Name, fuel_1_smiles, fraction_fuel_1, fuel_2_smiles, fraction_fuel_2, ... | |
| print("\n CSV must follow the mixture_database.csv format") | |
| print(" (columns: Name, fuel_1_smiles, fraction_fuel_1, fuel_2_smiles, ...)") | |
| print("\n" + "=" * 70) | |
| print("CONFIGURATION SUMMARY") | |
| print("=" * 70) | |
| print(f" Mode: Mixture Screening") | |
| print(f" CSV: {csv_path}") | |
| print(f" Target CN: {target_cn}") | |
| print("=" * 70) | |
| return ScreeningConfig(csv_path=csv_path, mode=mode, target_cn=target_cn) | |