From 23f0cdb1e248a16ee4382a6bfdee75dc6a599dbb Mon Sep 17 00:00:00 2001 From: Xingdong Shi <147021014+XingdongShi@users.noreply.github.com> Date: Sun, 3 Aug 2025 14:54:27 +0800 Subject: [PATCH 1/2] Add files via upload --- biorefineries/microalgae/TRY_analysis.py | 457 +++++++++ biorefineries/microalgae/_chemicals.py | 404 ++++++++ biorefineries/microalgae/analysis_utils.py | 110 +++ biorefineries/microalgae/facilities.py | 646 ++++++++++++ biorefineries/microalgae/lca.py | 935 ++++++++++++++++++ biorefineries/microalgae/lca_output.py | 380 +++++++ .../microalgae/parameter_distributions.xlsx | Bin 0 -> 12430 bytes biorefineries/microalgae/run_analysis.py | 185 ++++ biorefineries/microalgae/sesitivity.py | 776 +++++++++++++++ biorefineries/microalgae/streams.py | 30 + biorefineries/microalgae/system.py | 350 +++++++ biorefineries/microalgae/tea.py | 26 + biorefineries/microalgae/uncertainties.py | 310 ++++++ biorefineries/microalgae/units.py | 700 +++++++++++++ biorefineries/microalgae/utils.py | 90 ++ biorefineries/microalgae/vis.py | 797 +++++++++++++++ 16 files changed, 6196 insertions(+) create mode 100644 biorefineries/microalgae/TRY_analysis.py create mode 100644 biorefineries/microalgae/_chemicals.py create mode 100644 biorefineries/microalgae/analysis_utils.py create mode 100644 biorefineries/microalgae/facilities.py create mode 100644 biorefineries/microalgae/lca.py create mode 100644 biorefineries/microalgae/lca_output.py create mode 100644 biorefineries/microalgae/parameter_distributions.xlsx create mode 100644 biorefineries/microalgae/run_analysis.py create mode 100644 biorefineries/microalgae/sesitivity.py create mode 100644 biorefineries/microalgae/streams.py create mode 100644 biorefineries/microalgae/system.py create mode 100644 biorefineries/microalgae/tea.py create mode 100644 biorefineries/microalgae/uncertainties.py create mode 100644 biorefineries/microalgae/units.py create mode 100644 biorefineries/microalgae/utils.py create mode 100644 biorefineries/microalgae/vis.py diff --git a/biorefineries/microalgae/TRY_analysis.py b/biorefineries/microalgae/TRY_analysis.py new file mode 100644 index 00000000..70712d7b --- /dev/null +++ b/biorefineries/microalgae/TRY_analysis.py @@ -0,0 +1,457 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Microalgae biorefinery TRY (Titer, Rate, Yield) analysis + +This module performs TRY analysis for the microalgae biorefinery system, +analyzing the effects of fermentation titer, productivity rate, and yield +on techno-economic and environmental metrics. + +@author: Xingdong Shi +@version: 0.0.1 +""" + +from warnings import filterwarnings +filterwarnings('ignore') +import copy +import numpy as np +import pandas as pd +import biosteam as bst +import thermosteam as tmo +from datetime import datetime +from biosteam.utils import TicToc +import os + +# Import microalgae system components +from . import system as microalgae_system +from . import lca +from . import tea +from ._chemicals import chems + +# Set up the system +bst.settings.set_thermo(chems) +microalgae_sys = microalgae_system.microalgae_mcca_sys +microalgae_tea = microalgae_system.microalgae_tea + +# Create LCA object +from . import analysis_utils +microalgae_lca = analysis_utils.create_microalgae_lca_simple(microalgae_sys, microalgae_tea) + +print('\n\nLoaded microalgae system for TRY analysis.') + +# TRY analysis parameters +titer_range = np.linspace(10, 100, 10) # g/L, fermentation titer +rate_range = np.linspace(0.5, 5.0, 10) # g/L/h, productivity rate +yield_range = np.linspace(0.1, 0.8, 10) # g product/g substrate, yield + +# Results storage +try_results = { + 'Titer': [], + 'Rate': [], + 'Yield': [], + 'MPSP': [], + 'GWP100a': [], + 'FEC': [], + 'Production_Rate': [], # kg/h + 'Annual_Production': [], # kg/year + 'Conversion': [], # fraction +} + +def get_fermentation_unit(): + """Get the fermentation unit from the system.""" + for unit in microalgae_sys.units: + if 'ferment' in unit.ID.lower() or 'MCCA' in unit.ID: + return unit + return None + +def run_try_analysis(): + """Run TRY analysis for microalgae biorefinery.""" + + timer = TicToc('timer') + timer.tic() + + print(f'\n\nStarting TRY analysis...') + print(f'Titer range: {titer_range[0]:.1f} - {titer_range[-1]:.1f} g/L') + print(f'Rate range: {rate_range[0]:.1f} - {rate_range[-1]:.1f} g/L/h') + print(f'Yield range: {yield_range[0]:.2f} - {yield_range[-1]:.2f} g/g') + + # Get fermentation unit + fermentation_unit = get_fermentation_unit() + if fermentation_unit is None: + print("Warning: Could not find fermentation unit") + return None + + print(f'Found fermentation unit: {fermentation_unit.ID}') + + total_simulations = len(titer_range) * len(rate_range) * len(yield_range) + simulation_count = 0 + + # Store baseline values + baseline_titer = getattr(fermentation_unit, 'titer', 50.0) # Default 50 g/L + baseline_rate = getattr(fermentation_unit, 'productivity', 2.0) # Default 2 g/L/h + baseline_yield = getattr(fermentation_unit, 'fermentation_yield', 0.4) # Default 0.4 g/g + + for titer in titer_range: + for rate in rate_range: + for yield_val in yield_range: + simulation_count += 1 + + if simulation_count % 100 == 0: + print(f'Completed {simulation_count}/{total_simulations} simulations') + + try: + # Set TRY parameters + if hasattr(fermentation_unit, 'titer'): + fermentation_unit.titer = titer + if hasattr(fermentation_unit, 'productivity'): + fermentation_unit.productivity = rate + if hasattr(fermentation_unit, 'fermentation_yield'): + fermentation_unit.fermentation_yield = yield_val + + # Alternative parameter names + if hasattr(fermentation_unit, 'product_titer'): + fermentation_unit.product_titer = titer + if hasattr(fermentation_unit, 'volumetric_productivity'): + fermentation_unit.volumetric_productivity = rate + if hasattr(fermentation_unit, 'conversion_efficiency'): + fermentation_unit.conversion_efficiency = yield_val + + # Simulate system + microalgae_sys.simulate() + + # Calculate metrics + main_product = analysis_utils.get_main_product_stream(microalgae_sys) + mpsp = microalgae_tea.solve_price(main_product) if main_product else np.nan + gwp = microalgae_lca.GWP + fec = microalgae_lca.FEC + + # Calculate production metrics + product_stream = None + for stream in microalgae_sys.products: + if stream.get_total_flow('kg/hr') > 0: + product_stream = stream + break + + if product_stream: + production_rate = product_stream.get_total_flow('kg/hr') + annual_production = production_rate * 24 * 365 # kg/year + else: + production_rate = 0 + annual_production = 0 + + # Calculate conversion (simplified) + conversion = yield_val # Approximation + + # Store results + try_results['Titer'].append(titer) + try_results['Rate'].append(rate) + try_results['Yield'].append(yield_val) + try_results['MPSP'].append(mpsp) + try_results['GWP100a'].append(gwp) + try_results['FEC'].append(fec) + try_results['Production_Rate'].append(production_rate) + try_results['Annual_Production'].append(annual_production) + try_results['Conversion'].append(conversion) + + except Exception as e: + print(f'Error in simulation {simulation_count}: {str(e)}') + # Store NaN values for failed simulations + try_results['Titer'].append(titer) + try_results['Rate'].append(rate) + try_results['Yield'].append(yield_val) + try_results['MPSP'].append(np.nan) + try_results['GWP100a'].append(np.nan) + try_results['FEC'].append(np.nan) + try_results['Production_Rate'].append(np.nan) + try_results['Annual_Production'].append(np.nan) + try_results['Conversion'].append(np.nan) + + # Restore baseline values + if hasattr(fermentation_unit, 'titer'): + fermentation_unit.titer = baseline_titer + if hasattr(fermentation_unit, 'productivity'): + fermentation_unit.productivity = baseline_rate + if hasattr(fermentation_unit, 'fermentation_yield'): + fermentation_unit.fermentation_yield = baseline_yield + + timer.toc() + + return pd.DataFrame(try_results) + +def analyze_try_results(results_df): + """Analyze TRY results and generate summary statistics.""" + + analysis = {} + + # Basic statistics + metrics = ['MPSP', 'GWP100a', 'FEC', 'Production_Rate', 'Annual_Production'] + + for metric in metrics: + valid_data = results_df[metric].dropna() + if len(valid_data) > 0: + analysis[metric] = { + 'min': valid_data.min(), + 'max': valid_data.max(), + 'mean': valid_data.mean(), + 'std': valid_data.std(), + 'median': valid_data.median(), + 'q25': valid_data.quantile(0.25), + 'q75': valid_data.quantile(0.75) + } + + # Find optimal conditions + valid_results = results_df.dropna(subset=['MPSP']) + if len(valid_results) > 0: + # Minimum MPSP + min_mpsp_idx = valid_results['MPSP'].idxmin() + analysis['optimal_MPSP'] = { + 'titer': valid_results.loc[min_mpsp_idx, 'Titer'], + 'rate': valid_results.loc[min_mpsp_idx, 'Rate'], + 'yield': valid_results.loc[min_mpsp_idx, 'Yield'], + 'MPSP': valid_results.loc[min_mpsp_idx, 'MPSP'], + 'GWP100a': valid_results.loc[min_mpsp_idx, 'GWP100a'], + 'FEC': valid_results.loc[min_mpsp_idx, 'FEC'] + } + + # Minimum GWP + if 'GWP100a' in valid_results.columns: + min_gwp_idx = valid_results['GWP100a'].idxmin() + analysis['optimal_GWP'] = { + 'titer': valid_results.loc[min_gwp_idx, 'Titer'], + 'rate': valid_results.loc[min_gwp_idx, 'Rate'], + 'yield': valid_results.loc[min_gwp_idx, 'Yield'], + 'MPSP': valid_results.loc[min_gwp_idx, 'MPSP'], + 'GWP100a': valid_results.loc[min_gwp_idx, 'GWP100a'], + 'FEC': valid_results.loc[min_gwp_idx, 'FEC'] + } + + # Correlation analysis + correlation_metrics = ['Titer', 'Rate', 'Yield', 'MPSP', 'GWP100a', 'FEC'] + correlation_data = results_df[correlation_metrics].dropna() + if len(correlation_data) > 10: + analysis['correlations'] = correlation_data.corr() + + return analysis + +def save_try_results(results_df, analysis, filename_prefix='microalgae_try'): + """Save TRY analysis results to files.""" + + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + + # Save to Excel + excel_filename = f'{filename_prefix}_results_{timestamp}.xlsx' + with pd.ExcelWriter(excel_filename) as writer: + # Save full results + results_df.to_excel(writer, sheet_name='TRY_Results', index=False) + + # Save summary statistics + if 'MPSP' in analysis: + summary_data = [] + for metric in ['MPSP', 'GWP100a', 'FEC', 'Production_Rate', 'Annual_Production']: + if metric in analysis: + row = {'Metric': metric} + row.update(analysis[metric]) + summary_data.append(row) + + summary_df = pd.DataFrame(summary_data) + summary_df.to_excel(writer, sheet_name='Summary_Statistics', index=False) + + # Save optimal conditions + if 'optimal_MPSP' in analysis: + optimal_data = [] + for opt_type in ['optimal_MPSP', 'optimal_GWP']: + if opt_type in analysis: + row = {'Optimization': opt_type.replace('optimal_', '')} + row.update(analysis[opt_type]) + optimal_data.append(row) + + optimal_df = pd.DataFrame(optimal_data) + optimal_df.to_excel(writer, sheet_name='Optimal_Conditions', index=False) + + # Save correlations + if 'correlations' in analysis: + analysis['correlations'].to_excel(writer, sheet_name='Correlations') + + print(f'\nTRY results saved to {excel_filename}') + + # Save summary text report + txt_filename = f'{filename_prefix}_summary_{timestamp}.txt' + with open(txt_filename, 'w') as f: + f.write("Microalgae Biorefinery TRY Analysis Summary\n") + f.write("=" * 50 + "\n\n") + + f.write(f"Analysis Parameters:\n") + f.write(f"Titer range: {titer_range[0]:.1f} - {titer_range[-1]:.1f} g/L\n") + f.write(f"Rate range: {rate_range[0]:.1f} - {rate_range[-1]:.1f} g/L/h\n") + f.write(f"Yield range: {yield_range[0]:.2f} - {yield_range[-1]:.2f} g/g\n") + f.write(f"Total simulations: {len(results_df)}\n\n") + + # Summary statistics + if 'MPSP' in analysis: + f.write("Summary Statistics:\n") + for metric in ['MPSP', 'GWP100a', 'FEC']: + if metric in analysis: + stats = analysis[metric] + f.write(f"\n{metric}:\n") + f.write(f" Min: {stats['min']:.3f}\n") + f.write(f" Max: {stats['max']:.3f}\n") + f.write(f" Mean: {stats['mean']:.3f}\n") + f.write(f" Std: {stats['std']:.3f}\n") + f.write(f" Median: {stats['median']:.3f}\n") + + # Optimal conditions + if 'optimal_MPSP' in analysis: + f.write("\nOptimal Conditions:\n") + + f.write("\nMinimum MPSP:\n") + opt_mpsp = analysis['optimal_MPSP'] + f.write(f" Titer: {opt_mpsp['titer']:.1f} g/L\n") + f.write(f" Rate: {opt_mpsp['rate']:.1f} g/L/h\n") + f.write(f" Yield: {opt_mpsp['yield']:.3f} g/g\n") + f.write(f" MPSP: {opt_mpsp['MPSP']:.3f} USD/kg\n") + f.write(f" GWP: {opt_mpsp['GWP100a']:.3f} kg CO2-eq/kg\n") + + if 'optimal_GWP' in analysis: + f.write("\nMinimum GWP:\n") + opt_gwp = analysis['optimal_GWP'] + f.write(f" Titer: {opt_gwp['titer']:.1f} g/L\n") + f.write(f" Rate: {opt_gwp['rate']:.1f} g/L/h\n") + f.write(f" Yield: {opt_gwp['yield']:.3f} g/g\n") + f.write(f" MPSP: {opt_gwp['MPSP']:.3f} USD/kg\n") + f.write(f" GWP: {opt_gwp['GWP100a']:.3f} kg CO2-eq/kg\n") + + print(f'Summary saved to {txt_filename}') + +def run_single_parameter_analysis(): + """Run single parameter analysis (varying one parameter at a time).""" + + print('\n\nRunning single parameter analysis...') + + # Get fermentation unit + fermentation_unit = get_fermentation_unit() + if fermentation_unit is None: + print("Warning: Could not find fermentation unit") + return None + + # Store baseline values + baseline_titer = getattr(fermentation_unit, 'titer', 50.0) + baseline_rate = getattr(fermentation_unit, 'productivity', 2.0) + baseline_yield = getattr(fermentation_unit, 'fermentation_yield', 0.4) + + single_param_results = { + 'Parameter': [], + 'Value': [], + 'MPSP': [], + 'GWP100a': [], + 'FEC': [] + } + + # Vary titer + for titer in titer_range: + try: + # Reset to baseline + if hasattr(fermentation_unit, 'titer'): + fermentation_unit.titer = titer + if hasattr(fermentation_unit, 'productivity'): + fermentation_unit.productivity = baseline_rate + if hasattr(fermentation_unit, 'fermentation_yield'): + fermentation_unit.fermentation_yield = baseline_yield + + microalgae_sys.simulate() + + main_product = analysis_utils.get_main_product_stream(microalgae_sys) + single_param_results['Parameter'].append('Titer') + single_param_results['Value'].append(titer) + single_param_results['MPSP'].append(microalgae_tea.solve_price(main_product) if main_product else np.nan) + single_param_results['GWP100a'].append(microalgae_lca.GWP) + single_param_results['FEC'].append(microalgae_lca.FEC) + + except Exception as e: + print(f'Error varying titer to {titer}: {str(e)}') + + # Vary rate + for rate in rate_range: + try: + # Reset to baseline + if hasattr(fermentation_unit, 'titer'): + fermentation_unit.titer = baseline_titer + if hasattr(fermentation_unit, 'productivity'): + fermentation_unit.productivity = rate + if hasattr(fermentation_unit, 'fermentation_yield'): + fermentation_unit.fermentation_yield = baseline_yield + + microalgae_sys.simulate() + + main_product = analysis_utils.get_main_product_stream(microalgae_sys) + single_param_results['Parameter'].append('Rate') + single_param_results['Value'].append(rate) + single_param_results['MPSP'].append(microalgae_tea.solve_price(main_product) if main_product else np.nan) + single_param_results['GWP100a'].append(microalgae_lca.GWP) + single_param_results['FEC'].append(microalgae_lca.FEC) + + except Exception as e: + print(f'Error varying rate to {rate}: {str(e)}') + + # Vary yield + for yield_val in yield_range: + try: + # Reset to baseline + if hasattr(fermentation_unit, 'titer'): + fermentation_unit.titer = baseline_titer + if hasattr(fermentation_unit, 'productivity'): + fermentation_unit.productivity = baseline_rate + if hasattr(fermentation_unit, 'fermentation_yield'): + fermentation_unit.fermentation_yield = yield_val + + microalgae_sys.simulate() + + main_product = analysis_utils.get_main_product_stream(microalgae_sys) + single_param_results['Parameter'].append('Yield') + single_param_results['Value'].append(yield_val) + single_param_results['MPSP'].append(microalgae_tea.solve_price(main_product) if main_product else np.nan) + single_param_results['GWP100a'].append(microalgae_lca.GWP) + single_param_results['FEC'].append(microalgae_lca.FEC) + + except Exception as e: + print(f'Error varying yield to {yield_val}: {str(e)}') + + # Restore baseline values + if hasattr(fermentation_unit, 'titer'): + fermentation_unit.titer = baseline_titer + if hasattr(fermentation_unit, 'productivity'): + fermentation_unit.productivity = baseline_rate + if hasattr(fermentation_unit, 'fermentation_yield'): + fermentation_unit.fermentation_yield = baseline_yield + + return pd.DataFrame(single_param_results) + +if __name__ == '__main__': + print("Starting microalgae biorefinery TRY analysis...") + + try: + # Run full TRY analysis + results_df = run_try_analysis() + + if results_df is not None: + # Analyze results + analysis = analyze_try_results(results_df) + + # Save results + save_try_results(results_df, analysis) + + # Run single parameter analysis + single_param_df = run_single_parameter_analysis() + + if single_param_df is not None: + # Save single parameter results + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + single_param_filename = f'microalgae_single_param_{timestamp}.xlsx' + single_param_df.to_excel(single_param_filename, index=False) + print(f'Single parameter results saved to {single_param_filename}') + + print("\nTRY analysis completed successfully!") + + except Exception as e: + print(f"\nError during TRY analysis: {str(e)}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/biorefineries/microalgae/_chemicals.py b/biorefineries/microalgae/_chemicals.py new file mode 100644 index 00000000..810ecfc1 --- /dev/null +++ b/biorefineries/microalgae/_chemicals.py @@ -0,0 +1,404 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Sat June 21 20:50:00 2025 + +Microalgae biorefinery to produce medium chain fatty acids +by anaerobic fermentation without external electron donor addition- chemicals database + +References +---------- +[1] BioSTEAM Documentation: + https://biosteam.readthedocs.io/en/latest/API/thermosteam/Chemicals.html +[2] Cortes-Peña et al., BioSTEAM: A Fast and Flexible Platform for the Design, + Simulation, and Techno-Economic Analysis of Biorefineries under Uncertainty. + ACS Sustainable Chem. Eng. 2020, 8 (8), 3302–3310. +[3] 3-Hydroxypropionic acid biorefineries project: + https://github.com/BioSTEAMDevelopmentGroup/Bioindustrial-Park/tree/master/biorefineries/HP + +@author: Xingdong Shi +@version: 0.0.2 +""" + +#import biosteam as bst +import thermosteam as tmo +from thermosteam import functional as fn +#from biorefineries.cellulosic import chemicals as cellulosic_chems +#import biorefineries.sugarcane as sc +#from biorefineries.sugarcane import chemicals as sugarcane_chems +from biorefineries.cornstover import create_chemicals as cs_create_chemicals +from biorefineries.corn import create_chemicals as corn_create_chemicals + +# Import chemicals from other biorefineries +cornstover_chems = cs_create_chemicals() +corn_chems = corn_create_chemicals() + +# Create microalgae chemicals system +chems = Microalgae_chemicals = tmo.Chemicals([]) + +# Track chemicals by creation method +database_chemicals_dict = {} +copied_chemicals_dict = {} +defined_chemicals_dict = {} + +def chemical_database(ID, phase=None, **kwargs): + """Create chemical from database""" + chemical = tmo.Chemical(ID, **kwargs) + if phase: + chemical.at_state(phase) + chemical.phase_ref = phase + chems.append(chemical) + database_chemicals_dict[ID] = f'{ID}: {chemical.formula}/{chemical.MW}' + return chemical + + +def chemical_copied(ID, ref_chemical, **data): + """Copy existing chemical""" + chemical = ref_chemical.copy(ID) + chems.append(chemical) + for i, j in data.items(): + setattr(chemical, i, j) + copied_chemicals_dict[ID] = f'{ID}: {chemical.formula}/{chemical.MW}' + return chemical + +def chemical_defined(ID, **kwargs): + """Define custom chemical""" + chemical = tmo.Chemical.blank(ID, **kwargs) + chems.append(chemical) + defined_chemicals_dict[ID] = f'{ID}: {chemical.formula}/{chemical.MW}' + return chemical + +# ============================================================================= +# Constants +# ============================================================================= + +# Microalgae properties based on Nannochloropsis salina +# https://www.sciencedirect.com/science/article/pii/S0378382016302983 +Cp = 3.949 # J/g/K +rho = 1.0093 # g/cm3 +_cal2joule = 4.184 + +# ============================================================================= +# Basic chemicals +# ============================================================================= + +H2O = chemical_database('H2O') + +# ============================================================================= +# Gases +# ============================================================================= + +O2 = chemical_database('O2', phase='g', Hf=0) +N2 = chemical_database('N2', phase='g', Hf=0) +CH4 = chemical_database('CH4', phase='g') +CO2 = chemical_database('CO2', phase='g') +H2 = chemical_database('H2', phase='g', Hf=0) +NH3 = chemical_database('NH3', phase='g', Hf=-10963*_cal2joule) +NitricOxide = chemical_database('NitricOxide', phase='g') +NO2 = chemical_database('NO2', phase='g') +H2S = chemical_database('H2S', phase='g', Hf=-4927*_cal2joule) +SO2 = chemical_database('SO2', phase='g') + +# ============================================================================= +# Solids for combustion +# ============================================================================= + +P4O10 = chemical_database('P4O10', phase='s', Hf=-713.2*_cal2joule) + +# ============================================================================= +# Soluble inorganics +# ============================================================================= + +HCl = chemical_database('HCl', phase='l') +H2SO4 = chemical_database('H2SO4', phase='l') +HNO3 = chemical_database('HNO3', phase='l', Hf=-41406*_cal2joule) +NaOH = chemical_database('NaOH', phase='l') +NH4OH = chemical_database('NH4OH', phase='l') + +# Lime (Calcium hydroxide) for pH control and boiler +Lime = chemical_database('Lime', search_ID='CalciumDihydroxide', phase='l') + +# Calcium sulfate for boiler ash +CaSO4 = chemical_database('CaSO4', phase='s', Hf=-342531*_cal2joule) +# Use Lastovka solid model instead of default Perry 151 model +CaSO4.Cn.move_up_model_priority('LASTOVKA_S', 0) + +# Sodium sulfate for neutralization reactions +Na2SO4 = chemical_database('Na2SO4', phase='l', Hf=-1356.38e3) +AmmoniumSulfate = chemical_database('AmmoniumSulfate', phase='l', Hf=-1186.8e3) # 新增硫酸铵,主流BioSTEAM项目标准 + +# ============================================================================= +# Products +# ============================================================================= + +# Fatty acids +AceticAcid = chemical_database('AceticAcid') +PropionicAcid = chemical_database('PropionicAcid') +ButyricAcid = chemical_database('ButyricAcid') +ValericAcid = chemical_database('ValericAcid') +CaproicAcid = chemical_database('CaproicAcid') +HeptanoicAcid = chemical_database('HeptanoicAcid') +CaprylicAcid = chemical_database('CaprylicAcid') + +# Alcohols +Ethanol = chemical_database('Ethanol') +Butanol = chemical_database('Butanol') +Hexanol = chemical_database('Hexanol') + + +# Sugars and carbohydrates +Glucose = chemical_database('Glucose', phase='l') +Fructose = chemical_database('Fructose', phase='l') + +# Sugar oligomers +GlucoseOligomer = chemical_defined('GlucoseOligomer', phase='l', formula='C6H10O5', + Hf=-233200*_cal2joule) +GlucoseOligomer.copy_models_from(Glucose, ['Hvap', 'Psat', 'Cn', 'mu', 'kappa']) +Extract = chemical_copied('Extract', Glucose) + +Xylose = chemical_database('Xylose') +Xylose.copy_models_from(Glucose, ['Hvap', 'Psat', 'mu']) +XyloseOligomer = chemical_defined('XyloseOligomer', phase='l', formula='C5H8O4', + Hf=-182100*_cal2joule) +XyloseOligomer.copy_models_from(Xylose, ['Hvap', 'Psat', 'Cn', 'mu']) + +Sucrose = chemical_database('Sucrose', phase='l') +Sucrose.Cn.move_up_model_priority('DADGOSTAR_SHAW', 0) +Cellobiose = chemical_database('Cellobiose', phase='l', Hf=-480900*_cal2joule) + +Mannose = chemical_database('Mannose', phase='l', Hf=Glucose.Hf) +Mannose.copy_models_from(Glucose, ['Hvap', 'Psat', 'Cn', 'mu']) +MannoseOligomer = chemical_copied('MannoseOligomer', GlucoseOligomer) + +Galactose = chemical_database('Galactose', phase='l', Hf=Glucose.Hf) +Galactose.copy_models_from(Glucose, ['Hvap', 'Psat', 'Cn','mu']) +GalactoseOligomer = chemical_copied('GalactoseOligomer', GlucoseOligomer) + +Arabinose = chemical_database('Arabinose', phase='l', Hf=Xylose.Hf) +Arabinose.copy_models_from(Xylose, ['Hvap', 'Psat', 'mu']) +ArabinoseOligomer = chemical_copied('ArabinoseOligomer', XyloseOligomer) + +SolubleLignin = chemical_database('SolubleLignin', search_ID='Vanillin', + phase='l', Hf=-108248*_cal2joule) + +# ============================================================================= +# Extractants and solvents +# ============================================================================= +Hexane = chemical_database('Hexane', phase='l') +Heptane = chemical_database('Heptane', phase='l') +Octane = chemical_database('Octane', phase='l') +Toluene = chemical_database('Toluene', phase='l') +Benzene = chemical_database('Benzene', phase='l') +Chloroform = chemical_database('Chloroform', phase='l') +Dichloromethane = chemical_database('Dichloromethane', phase='l') +Octanol = chemical_database('Octanol', phase= 'l') + +# Ionic liquid extractants +TOA = chemical_database('TOA', search_ID='tri-n-octylamine') +AQ336 = chemical_database('AQ336', search_ID='63393-96-4') # aliquat 336 +AQ336.copy_models_from(TOA, ('Psat', 'Hvap', 'V')) +AQ336._Dortmund = TOA.Dortmund +AQ336.Hfus = TOA.Hfus + +# ============================================================================= +# Microalgae components +# ============================================================================= + +# Protein - main microalgae component +Protein = chemical_defined('Protein', + phase='s', + formula='CH1.57O0.31N0.29S0.007', + Hf=-17618*_cal2joule) + +# Carbohydrate +Carbohydrate = chemical_defined('Carbohydrate', + phase='s', + formula='CH2O', + MW=180.16, + Hf=-233000) + +# Ash +Ash = chemical_defined('Ash', + phase='s', + MW=1, + Hf=0, + HHV=0, + LHV=0) + + +WWTsludge = chemical_defined('WWTsludge', phase='s', formula='CH1.66O0.33N0.22', MW=24.6, Hf=-110000) +# ============================================================================= +# Enzymes and microorganisms +# ============================================================================= + +# Base enzyme +BaseEnzyme = chemical_defined('BaseEnzyme', + phase='s', + MW=110, + Hf=-17618*_cal2joule, + HHV=0, + LHV=0) + +# Specific enzymes +AlphaAmylase = chemical_copied('AlphaAmylase', BaseEnzyme, Hf=-2000000) +GlucoAmylase = chemical_copied('GlucoAmylase', BaseEnzyme, Hf=-2500000) +Lipase = chemical_copied('Lipase', BaseEnzyme, Hf=-1800000) +Protease = chemical_copied('Protease', BaseEnzyme, Hf=-2200000) + +# Fermentation microorganisms +FermMicrobe = chemical_defined('FermMicrobe', phase='l', + formula='CH1.78O0.44N0.24', Hf=-103310.) + +# ============================================================================= +# Insoluble organics +# ============================================================================= + +Glucan = chemical_defined('Glucan', phase='s', formula='C6H10O5', Hf=-233200*_cal2joule) +Glucan.copy_models_from(Glucose, ['Cn']) +Mannan = chemical_copied('Mannan', Glucan) +Galactan = chemical_copied('Galactan', Glucan) + +Xylan = chemical_defined('Xylan', phase='s', formula='C5H8O4', Hf=-182100*_cal2joule) +Xylan.copy_models_from(Xylose, ['Cn']) +Arabinan = chemical_copied('Arabinan', Xylan) + +Cellulose = chemical_database('Cellulose', phase='s') +Hemicellulose = chemical_database('Hemicellulose', phase='s') + +# ============================================================================= +# Mixtures +# ============================================================================= +# Boiler chemicals +BoilerChems = chemical_database('BoilerChems', search_ID='DiammoniumPhosphate', + phase='l', Hf=0, HHV=0, LHV=0) + +# ============================================================================= +# Fillers +# ============================================================================= + +BaghouseBag = chemical_defined('BaghouseBag', phase='s', MW=1, Hf=0, HHV=0, LHV=0) +BaghouseBag.Cn.add_model(0) +CoolingTowerChems = chemical_copied('CoolingTowerChems', BaghouseBag) + +Microalgae = chemical_defined( + 'Microalgae', + phase='s', + formula='C2.8H0.4O1.5N0.3', # comsuption from microalgal biomass componets https://www.sciencedirect.com/science/article/pii/S0196890419313184?via%3Dihub + Hf=-2138000, +) + + + + +# ============================================================================= +# Chemical groups for simulation +# ============================================================================= + +chemical_groups = dict( + OtherSugars = ('Arabinose', 'Mannose', 'Galactose', 'Cellobiose', + 'Sucrose', 'Fructose'), + SugarOligomers = ('GlucoseOligomer', 'XyloseOligomer', 'GalactoseOligomer', + 'ArabinoseOligomer', 'MannoseOligomer'), + InorganicSolubleSolids = ('NaOH', 'HNO3', 'NaNO3', 'BoilerChems', + 'Na2SO4', 'AmmoniumSulfate'), + Furfurals = ('Furfural', 'HMF'), + OtherOrganics = ('Xylitol', 'Glycerol'), + COSOxNOxH2S = ('NitricOxide', 'NO2', 'SO2', 'H2S'), + Proteins = ('Protein', 'BaseEnzyme', 'AlphaAmylase', + 'GlucoAmylase', 'Lipase', 'Protease', 'Microalgae'), + CellMass = ('FermMicrobe', 'WWTsludge'), + OtherInsolubleSolids = ('Ash', 'CalciumDihydroxide', 'CaSO4', + 'BaghouseBag', 'CoolingTowerChems'), + OtherStructuralCarbohydrates = ('Glucan', 'Xylan', 'Arabinan', + 'Mannan', 'Galactan', 'Cellulose', 'Hemicellulose'), + SeparatelyListedOrganics = ('Ethanol', 'Glucose', 'Xylose', 'AceticAcid'), + SpearatedlyListedOthers = ('H2O', 'NH3', 'H2SO4', 'CO2', 'CH4', 'O2', 'N2'), + MCCA = ('CaproicAcid', 'HeptanoicAcid', 'CaprylicAcid'), + SCFA = ('AceticAcid', 'PropionicAcid', 'ButyricAcid', 'ValericAcid'), + Alcohols = ('Ethanol', 'Butanol', 'Hexanol'), + Extractants = ('Hexane', 'Heptane', 'Octane', 'Toluene', + 'Benzene', 'Chloroform', 'Dichloromethane', + 'TOA', 'AQ336', 'Octanal') +) +phase_change_chemicals = ['Methanol', 'MCCA', 'SCFA', 'H2O', 'MethylAcetate', 'Denaturant', + 'AceticAcid', 'MethylAcetate', 'MethylLactate', + 'EthylLactate', 'Furfural', 'MethylSuccinate', + 'SuccinicAcid', 'LacticAcid', 'HMF', 'Alcohols', 'Extractants'] + + +soluble_groups = ('OtherSugars', 'SugarOligomers', + 'Furfurals', 'OtherOrganics', 'Proteins', 'CellMass', + 'SeparatelyListedOrganics') +soluble_organics = list(sum([chemical_groups[i] for i in soluble_groups], ())) + +solubles = tuple(soluble_organics) + chemical_groups['InorganicSolubleSolids'] + ('H2SO4',) + +insoluble_groups = ('OtherInsolubleSolids', 'OtherStructuralCarbohydrates') +insolubles = sum([chemical_groups[i] for i in insoluble_groups], ('WWTsludge',)) + +for chem in chems: + if chem.ID in phase_change_chemicals: pass + elif chem.locked_state: pass + else: + # Set phase_ref to avoid missing model errors + if chem.phase_ref == 'g': + chem.at_state('g') + if chem.ID in solubles: + chem.phase_ref = 'l' + chem.at_state('l') + if chem.ID in insolubles: + chem.phase_ref = 's' + chem.at_state('s') + +# Set chemical heat capacity +# Cp of biomass (1.25 J/g/K) from Leow et al., Green Chemistry 2015, 17 (6), 3584–3599 +for chemical in (Protein, FermMicrobe, BaseEnzyme): + chemical.Cn.add_model(1.25*chemical.MW) + +# Set chemical molar volume following assumptions in lipidcane biorefinery, +# assume densities for solulables and insolubles to be 1e5 and 1540 kg/m3, respectively + +def set_rho(chemical, rho): + V = fn.rho_to_V(rho, chemical.MW) + chemical.V.add_model(V, top_priority=True) + +for chemical in chems: + if chemical.ID in phase_change_chemicals: pass + elif chemical.ID in solubles: set_rho(chemical, 1e5) + elif chemical.ID in insolubles: set_rho(chemical, 1540) + +# The Lakshmi Prasad model gives negative kappa values for some chemicals +for chemical in chems: + if chemical.locked_state: + try: chemical.kappa.move_up_model_priority('Lakshmi Prasad', -1) + except: pass + +# Default missing properties of chemicals to those of water, +for chemical in chems: chemical.default() + + +# ============================================================================= +# Set Hfus to 0 if None +# ============================================================================= + +for chem in Microalgae_chemicals: + if chem.Hfus is None: + chem.Hfus = 0 + +# ============================================================================= +# Add chemicals from other biorefineries +# ============================================================================= + +chems.append(corn_chems.Starch) +chems.append(corn_chems.Fiber) +chems.append(corn_chems.SolubleProtein) +chems.append(corn_chems.InsolubleProtein) +chems.append(corn_chems.Oil) +chems.append(corn_chems.Yeast) + + +chems.compile() +tmo.settings.set_thermo(chems) + + + diff --git a/biorefineries/microalgae/analysis_utils.py b/biorefineries/microalgae/analysis_utils.py new file mode 100644 index 00000000..6c311e42 --- /dev/null +++ b/biorefineries/microalgae/analysis_utils.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Microalgae biorefinery analysis utilities + +Helper functions for creating LCA objects and other analysis utilities. + +@author: Xingdong Shi +@version: 0.0.1 +""" + +from . import lca + +def create_microalgae_lca_simple(system, tea=None): + """ + Simplified function to create microalgae LCA object with default parameters. + + Parameters + ---------- + system : System + Microalgae production system + tea : TEA, optional + TEA object (not used in current implementation) + + Returns + ------- + LCA + Life cycle assessment object + """ + + # Find main product + main_product = None + for stream in system.products: + if stream.get_total_flow('kg/hr') > 0: + main_product = stream + break + + if main_product is None: + # If no products found, try to find caproic acid stream + for stream in system.streams: + if 'caproic' in stream.ID.lower(): + main_product = stream + break + + # Find boiler + boiler = None + for unit in system.units: + if 'BT' in unit.ID or 'boiler' in unit.ID.lower(): + boiler = unit + break + + # Default main product chemical IDs + main_product_chemical_IDs = ['CaproicAcid'] + + # Create LCA object + microalgae_lca = lca.create_microalgae_lca( + system=system, + main_product=main_product, + main_product_chemical_IDs=main_product_chemical_IDs, + boiler=boiler + ) + + return microalgae_lca + +def get_fermentation_unit(system): + """ + Find the fermentation unit in the system. + + Parameters + ---------- + system : System + Microalgae production system + + Returns + ------- + Unit or None + Fermentation unit if found, None otherwise + """ + for unit in system.units: + if ('ferment' in unit.ID.lower() or + 'MCCA' in unit.ID or + 'R' in unit.ID and hasattr(unit, 'conversion')): + return unit + return None + +def get_main_product_stream(system): + """ + Find the main product stream in the system. + + Parameters + ---------- + system : System + Microalgae production system + + Returns + ------- + Stream or None + Main product stream if found, None otherwise + """ + # First try to find streams with significant flow + for stream in system.products: + if stream.get_total_flow('kg/hr') > 0: + return stream + + # If no products found, try to find caproic acid stream + for stream in system.streams: + if 'caproic' in stream.ID.lower() and stream.get_total_flow('kg/hr') > 0: + return stream + + return None \ No newline at end of file diff --git a/biorefineries/microalgae/facilities.py b/biorefineries/microalgae/facilities.py new file mode 100644 index 00000000..92e1eb03 --- /dev/null +++ b/biorefineries/microalgae/facilities.py @@ -0,0 +1,646 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat June 21 10:50:00 2025 + +Microalgae biorefinery to produce medium chain fatty acids +by anaerobic fermentation without external electron donor addition- facilities + +References +---------- +[1] BioSTEAM Documentation: + https://biosteam.readthedocs.io/en/latest/tutorial/Creating_a_System.html +[2] Cortes-Peña et al., BioSTEAM: A Fast and Flexible Platform for the Design, + Simulation, and Techno-Economic Analysis of Biorefineries under Uncertainty. + ACS Sustainable Chem. Eng. 2020, 8 (8), 3302–3310. +[3] 3-Hydroxypropionic acid biorefineries project: + https://github.com/BioSTEAMDevelopmentGroup/Bioindustrial-Park/tree/master/biorefineries/HP +[4] Succinic projest + https://github.com/BioSTEAMDevelopmentGroup/Bioindustrial-Park/tree/master/biorefineries/succinic + +@author: Xingdong Shi +@version: 0.0.1 +""" + +import biosteam as bst +import thermosteam as tmo +from biosteam import HeatUtility, Facility +from biosteam.units.decorators import cost +from thermosteam import Stream +from .utils import CEPCI +import flexsolve as flx + +__all__ = ('CIP', 'ADP', 'CT', 'PWC', 'BT', 'HXNWithMin') + + +# %% + +# ============================================================================= +# Clean-in-place system +# ============================================================================= +@cost(basis='Flow rate', ID='System', units='kg/hr', + cost=421000, S=63, CE=CEPCI[2009], n=0.6, BM=1.8) +class CIP(Facility): + network_priority = 3 + line = 'Clean-in-place system' + + +# %% + +# ============================================================================= +# Air distribution package, size based on stream 950 in Humbird et al. +# ============================================================================= +@cost(basis='Flow rate', ID='Plant air compressor', units='kg/hr', + kW=111.855, cost=28000, S=83333, CE=CEPCI[2010], n=0.6, BM=1.6) +@cost(basis='Flow rate', ID='Plant air reciever', units='kg/hr', + cost=16000, S=83333, CE=CEPCI[2009], n=0.6, BM=3.1) +@cost(basis='Flow rate', ID='Instrument air dryer', units='kg/hr', + cost=15000, S=83333, CE=CEPCI[2009], n=0.6, BM=1.8) +class ADP(Facility): + network_priority = 3 + line = 'Air distribution package' + + def __init__(self, ID='', ins=None, outs=(), ratio=None): + Facility.__init__(self, ID, ins, outs) + self.ratio = ratio + + def _design(self): + self.design_results['Flow rate'] = 83333 * self.ratio + + +# %% + +# ============================================================================= +# Chilled water package +# ============================================================================= + +# Use BioSTEAM built-in ChilledWaterPackage class +CWP = bst.ChilledWaterPackage + + +# %% + +# ============================================================================= +# Cooling tower +# ============================================================================= + +# Use BioSTEAM built-in CoolingTower class +CT = bst.CoolingTower + + +# %% + +# ============================================================================= +# Process water center +# ============================================================================= + +@cost(basis='Flow rate', ID='Tank', units='kg/hr', + cost=250000, S=451555, CE=CEPCI[2009], n=0.7, BM=1.7) +@cost(basis='Flow rate', ID='Circulating pump', units='kg/hr', + kW=55.9275, cost=15292, S=518924, CE=CEPCI[2010], n=0.8, BM=3.1) +@cost(basis='Flow rate', ID='Makeup water pump', units='kg/hr', + kW=14.914, cost=6864, S=155564, CE=CEPCI[2010], n=0.8, BM=3.1) +class PWC(Facility): + _N_ins = 2 + _N_outs = 2 + _units= {'Flow rate': 'kg/hr'} + + network_priority = 2 + line = 'Process water center' + + def __init__(self, ID='', ins=None, outs=(), process_water_streams=None, + recycled_blowdown_streams=None): + Facility.__init__(self, ID, ins, outs) + self.process_water_streams = process_water_streams + self.recycled_blowdown_streams = recycled_blowdown_streams + + def _run(self): + makeup, RO_water = self.ins + process_water, discharged = self.outs + + water_demand = sum(i.imol['Water'] for i in self.process_water_streams) + water_needs = water_demand - RO_water.imol['Water'] + self.recycled_water = RO_water.imass['Water'] + + if self.recycled_blowdown_streams: + water_needs -= sum(i.imol['Water'] for i in self.recycled_blowdown_streams) + self.recycled_water += sum(i.imass['Water'] for i in self.recycled_blowdown_streams) + + if water_needs > 0: + makeup.imol['Water'] = water_needs + discharged.empty() + else: + discharged.imol['Water'] = - water_needs + makeup.empty() + + process_water.mol = makeup.mol + RO_water.mol - discharged.mol + + self.design_results['Flow rate'] = self.F_mass_in + +# ============================================================================= +# Boiler and turbogenerator +# ============================================================================= + + +@cost(basis='Flow rate', ID='Boiler', units='kg/hr', + cost=36500000, S=239000, CE=CEPCI[2009], n=0.8, BM=1.8) +class BT(bst.Facility): + """ + Create a BoilerTurbogenerator object that will calculate electricity + generation from burning the feed. It also takes into account how much + steam is being produced, and the required cooling utility of the turbo + generator. All capital cost correlations are based on [1]_. + + Parameters + ---------- + ins : + * [0] Liquid/solid feed that will be burned. + * [1] Gas feed that will be burned. + * [2] Make-up water. + * [3] Natural gas/fuel to satisfy steam and electricity demand. + * [4] Lime for flue gas desulfurization. + * [5] Boiler chemicals. + * [6] Air or oxygen-rich gas. + outs : + * [0] Total emissions produced. + * [1] Blowdown water. + * [2] Ash disposal. + boiler_efficiency : float, optional + Fraction of heat transferred to steam. Defaults to 0.8. + turbo_generator_efficiency : float, optional + Fraction of steam heat converted to electricity. Defaults to 0.85. + agent : UtilityAgent, optional + Steam produced. Defaults to low pressure steam. + other_agents = () : Iterable[UtilityAgent], optional + Other steams produced. Defaults to all other heating agents. + fuel_source : str, optional + Name fuel used to satisfy steam and electricity demand. Defaults to 'CH4'. + fuel_price : float, optional + Price of natural gas [USD/kg]. Same as `bst.stream_utility_prices['Natural gas']`, + which defaults to 0.218. + ash_disposal_price : float, optional + Price of disposing ash [USD/kg]. Same as `bst.stream_utility_prices['Ash disposal']`, + which defaults to -0.0318. + satisfy_system_electricity_demand : bool, optional + Whether to purchase natural gas to satisfy system electricity demand + if there is not enough heat from process feeds (i.e., inlets 0 and 1). + If True, natural gas is purchased to satisfy system heat and electricity demand + when there is not enough heat from the feed and gas. + If False, natural gas is only purchased to satisfy system heat demand + and electricity will be purchased from the grid if there is not + enough heat from the feeds. + In either case, if there is excess heat from the process feeds, + electricity will still be produced. + boiler_efficiency_basis : str, optional + Basis of boiler efficiency. Defaults to 'LHV' (i.e., lower heating value). + 'HHV' (i.e., higher heating value) is also a valid basis. + + Examples + -------- + Create a boiler-turbogenerator system that uses sugarcane bagasse to + produce steam for a distillation unit and any excess steam for surplus electricity: + + >>> import biosteam as bst + >>> from biorefineries import cane + >>> chemicals = cane.create_sugarcane_chemicals() + >>> chemicals.define_group( + ... name='Fiber', + ... IDs=['Cellulose', 'Hemicellulose', 'Lignin'], + ... composition=[0.4704 , 0.2775, 0.2520], + ... wt=True, # Composition is given as weight + ... ) + >>> bst.settings.set_thermo(chemicals) + >>> dilute_ethanol = bst.Stream('dilute_ethanol', Water=1390, Ethanol=590) + >>> bagasse = bst.Stream('bagasse', Water=0.4, Fiber=0.6, total_flow=8e4, units='kg/hr') + >>> with bst.System('sys') as sys: + ... D1 = bst.BinaryDistillation('D1', ins=dilute_ethanol, Lr=0.999, Hr=0.89, k=1.25, LHK=('Ethanol', 'Water')) + ... BT = bst.BoilerTurbogenerator('BT') + ... BT.ins[0] = bagasse + >>> sys.simulate() + >>> BT.show() + BoilerTurbogenerator: BT + ins... + [0] bagasse + phase: 'l', T: 298.15 K, P: 101325 Pa + flow (kmol/hr): Water 1.78e+03 + Cellulose 139 + Hemicellulose 101 + Lignin 79.5 + [1] - + phase: 'l', T: 298.15 K, P: 101325 Pa + flow: 0 + [2] - + phase: 'l', T: 298.15 K, P: 101325 Pa + flow (kmol/hr): Water 488 + [3] - + phase: 'g', T: 288.71 K, P: 101560 Pa + flow: 0 + [4] - + phase: 'l', T: 298.15 K, P: 101325 Pa + flow: 0 + [5] - + phase: 'l', T: 298.15 K, P: 101325 Pa + flow (kmol/hr): Ash 0.567 + [6] - + phase: 'g', T: 298.15 K, P: 101325 Pa + flow (kmol/hr): O2 9.85e+03 + N2 4.23e+04 + outs... + [0] emissions + phase: 'g', T: 394.15 K, P: 101325 Pa + flow (kmol/hr): Water 3.19e+03 + CO2 1.98e+03 + O2 7.84e+03 + N2 4.23e+04 + [1] rejected_water_and_blowdown + phase: 'l', T: 298.15 K, P: 101325 Pa + flow (kmol/hr): Water 488 + [2] ash_disposal + phase: 'l', T: 298.15 K, P: 101325 Pa + flow (kmol/hr): Water 0.00944 + Ash 0.567 + + >>> BT.results() # Steam and electricity are produced, so costs are negative + Boiler turbogenerator Units BT + Electricity Power kW -1.31e+05 + Cost USD/hr -1.02e+04 + Low pressure steam Duty kJ/hr -7.32e+07 + Flow kmol/hr -1.89e+03 + Cost USD/hr -450 + Cooling water Duty kJ/hr -8.42e+07 + Flow kmol/hr 5.75e+04 + Cost USD/hr 28.1 + Fuel (inlet) Flow kg/hr 0 + Cost USD/hr 0 + Ash disposal (outlet) Flow kg/hr 0.737 + Cost USD/hr 0.0234 + Design Work kW 1.33e+05 + Flow rate kg/hr 2.93e+05 + Ash disposal kg/hr 0.737 + Purchase cost Baghouse bags USD 81.1 + Boiler USD 3.33e+07 + Deaerator USD 3.58e+05 + Amine addition pkg USD 4.69e+04 + Hot process water softener system USD 9.16e+04 + Turbogenerator USD 1.94e+07 + Total purchase cost USD 5.32e+07 + Utility cost USD/hr -1.07e+04 + + Notes + ----- + The flow rate of natural gas, lime, and boiler chemicals (streams 3-5) + is set by the BoilerTurbogenerator object during simulation. + + References + ---------- + .. [1] Humbird, D., Davis, R., Tao, L., Kinchin, C., Hsu, D., Aden, A., + Dudgeon, D. (2011). Process Design and Economics for Biochemical + Conversion of Lignocellulosic Biomass to Ethanol: Dilute-Acid + Pretreatment and Enzymatic Hydrolysis of Corn Stover + (No. NREL/TP-5100-47764, 1013269). https://doi.org/10.2172/1013269 + + """ + ticket_name = 'BT' + network_priority = 1 + boiler_blowdown = 0.03 + # Reverse osmosis (RO) typically rejects 25% of water, but the boiler-feed water is assumed to come after RO. + # Setting this parameter to a fraction more than zero effectively assumes that this unit accounts for reverse osmosis. + RO_rejection = 0 + _N_ins = 7 + _N_outs = 3 + _units = {'Flow rate': 'kg/hr', + 'Work': 'kW', + 'Ash disposal': 'kg/hr'} + + def __init__(self, ID='', ins=None, + outs=('emissions', + 'rejected_water_and_blowdown', + 'ash_disposal'), + thermo=None, *, + boiler_efficiency=None, + turbogenerator_efficiency=None, + side_steam=None, + agent=None, + other_agents=None, + fuel_price=None, + natural_gas_price=None, + ash_disposal_price=None, + T_emissions=None, + CO2_emissions_concentration=None, + satisfy_system_electricity_demand=None, + boiler_efficiency_basis=None, + fuel_source=None, + oxygen_rich_gas_composition=None, + ): + if boiler_efficiency_basis is None: boiler_efficiency_basis = 'LHV' + if boiler_efficiency is None: boiler_efficiency = 0.80 + if turbogenerator_efficiency is None: turbogenerator_efficiency = 0.85 + if satisfy_system_electricity_demand is None: satisfy_system_electricity_demand = True + if fuel_source is None: fuel_source = 'CH4' + if oxygen_rich_gas_composition is None: oxygen_rich_gas_composition = dict(O2=21, N2=79, phase='g', units='kg/hr') + if CO2_emissions_concentration is None: CO2_emissions_concentration = 0.055 # Usually between 4 - 7 for biomass and natural gas (https://www.sciencedirect.com/science/article/pii/S0957582021005127) + bst.Facility.__init__(self, ID, ins, outs, thermo) + settings = bst.settings + self.boiler_efficiency_basis = boiler_efficiency_basis + self.agent = agent = agent or settings.get_heating_agent('low_pressure_steam') + self.fuel_source = fuel_source + self.define_utility('Fuel', self.fuel) + self.define_utility('Ash disposal', self.ash_disposal) + self.boiler_efficiency = boiler_efficiency + self.turbogenerator_efficiency = turbogenerator_efficiency + self.steam_utilities = [] + self.steam_demand = agent.to_stream() + self.side_steam = side_steam + self.other_agents = [i for i in settings.heating_agents if i is not agent] if other_agents is None else other_agents + self.CO2_emissions_concentration = CO2_emissions_concentration + self.oxygen_rich_gas_composition = oxygen_rich_gas_composition + if T_emissions is not None: + raise ValueError('setting T_emissions is not yet implemented') + # T_emissions should dictate the efficiency of either the boiler or the turbogenerator. + # Note that T_emissions is tied to the energy balance. + self.T_emissions = T_emissions + if natural_gas_price is not None: + self.fuel_price = natural_gas_price + elif fuel_price is not None: + self.fuel_price = fuel_price + if ash_disposal_price is not None: self.ash_disposal_price = ash_disposal_price + self.satisfy_system_electricity_demand = satisfy_system_electricity_demand + + def _get_desulfurization_rxn_and_coreactant(self): + try: + return self.desulfurization_reaction, self._ID_lime + except: + chemicals = self.chemicals + CAS_lime = '1305-62-0' + has_common_name = 'Ca(OH)2' in chemicals + if CAS_lime in chemicals or has_common_name: + if not has_common_name: chemicals.set_synonym(CAS_lime, 'Ca(OH)2') + self.desulfurization_reaction = rxn = tmo.Reaction( + 'SO2 + Ca(OH)2 + 0.5 O2 -> CaSO4 + H2O', 'SO2', 0.92, chemicals + ) + self._ID_lime = ID = 'Ca(OH)2' + return rxn, ID + CAS_lime = '1305-78-8' + has_common_name = 'CaO' in chemicals + if CAS_lime in chemicals or has_common_name: + if not has_common_name: chemicals.set_synonym(CAS_lime, 'CaO') + self.desulfurization_reaction = rxn = tmo.Reaction( + 'SO2 + CaO + 0.5 O2 -> CaSO4', 'SO2', 0.92, chemicals + ) + self._ID_lime = ID = 'CaO' + return rxn, ID + raise RuntimeError( + "lime is required for boiler, but no chemical 'CaO' or 'Ca(OH)2' " + "available in thermodynamic property package" + ) + + @property + def emissions(self): + return self.outs[0] + + @property + def blowdown_water(self): + return self.outs[1] + + @property + def makeup_water(self): + """[Stream] Makeup water due to boiler blowdown.""" + return self.ins[2] + + @property + def fuel(self): + """[Stream] Fuel used to satisfy steam and electricity requirements.""" + return self.ins[3] + natural_gas = fuel + + @property + def air(self): + """[Stream] Air or oxygen rich gas used to supply oxygen for combustion.""" + return self.ins[6] + oxygen_rich_gas = air + + @property + def ash_disposal(self): + """[Stream] Ash disposal.""" + return self.outs[2] + + @property + def fuel_price(self): + """[Float] Price of natural gas, same as `bst.stream_utility_prices['Natural gas']`.""" + return bst.stream_utility_prices['Fuel'] + @fuel_price.setter + def fuel_price(self, new_price): + bst.stream_utility_prices['Fuel'] = new_price + natural_gas_price = fuel_price + + @property + def ash_disposal_price(self): + """[Float] Price of ash disposal, same as `bst.stream_utility_prices['Ash disposal']`.""" + return bst.stream_utility_prices['Ash disposal'] + + @ash_disposal_price.setter + def ash_disposal_price(self, ash_disposal_price): + bst.stream_utility_prices['Ash disposal'] = ash_disposal_price + + def _run(self): pass + + def _load_utility_agents(self): + steam_utilities = self.steam_utilities + steam_utilities.clear() + agent = self.agent + units = self.other_units + for agent in (*self.other_agents, agent): + ID = agent.ID + for u in units: + for hu in u.heat_utilities: + agent = hu.agent + if agent and agent.ID == ID: + steam_utilities.append(hu) + self.electricity_demand = sum([u.power_utility.consumption for u in units]) + + def _design(self): + B_eff = self.boiler_efficiency + TG_eff = self.turbogenerator_efficiency + steam_demand = self.steam_demand + Design = self.design_results + chemicals = self.chemicals + self._load_utility_agents() + mol_steam = sum([i.flow for i in self.steam_utilities]) + feed_solids, feed_gas, makeup_water, fuel, lime, chems, oxygen_rich_gas = self.ins + oxygen_rich_gas.empty() + if self.fuel_source == 'CH4': + fuel.phase = 'g' + fuel.set_property('T', 60, 'degF') + fuel.set_property('P', 14.73, 'psi') + emissions, blowdown_water, ash_disposal = self.outs + if not lime.price: + lime.price = 0.19937504680689402 + if not chems.price: + chems.price = 4.995862254032183 + H_steam = sum([i.duty for i in self.steam_utilities]) + side_steam = self.side_steam + if side_steam: + H_steam += side_steam.H + mol_steam += side_steam.F_mol + steam_demand.imol['7732-18-5'] = mol_steam + self.combustion_reactions = combustion_rxns = chemicals.get_combustion_reactions() + non_empty_feeds = [i for i in (feed_solids, feed_gas) if not i.isempty()] + boiler_efficiency_basis = self.boiler_efficiency_basis + fuel_source = self.fuel_source + def calculate_excess_electricity_at_natual_gas_flow(fuel_flow): + if fuel_flow: + fuel_flow = abs(fuel_flow) + fuel.imol[fuel_source] = fuel_flow + else: + fuel.empty() + if boiler_efficiency_basis == 'LHV': + H_combustion = fuel.LHV + for feed in non_empty_feeds: H_combustion += feed.LHV + elif boiler_efficiency_basis == 'HHV': + H_combustion = fuel.HHV + for feed in non_empty_feeds: H_combustion += feed.HHV + else: + raise ValueError( + f"invalid boiler efficiency basis {boiler_efficiency_basis}; " + f"valid values include 'LHV', or 'HHV'" + ) + self.H_content = H_content = B_eff * H_combustion + self.H_loss_to_emissions = H_combustion - H_content + H_electricity = H_content - H_steam # Heat available for the turbogenerator + electricity = H_electricity * TG_eff # Electricity produced + self.cooling_duty = electricity - H_electricity + Design['Work'] = work = electricity / 3600 + duty_over_mol = 39000 # kJ / mol-superheated steam + self.total_steam = H_content / duty_over_mol #: [float] Total steam produced by the boiler (kmol/hr) + Design['Flow rate'] = flow_rate = self.total_steam * 18.01528 + + if self.satisfy_system_electricity_demand: + boiler = self.cost_items['Boiler'] + rate_boiler = boiler.kW * flow_rate / boiler.S + return work - self.electricity_demand - rate_boiler + else: + return work + + self._excess_electricity_without_fuel = excess_electricity = calculate_excess_electricity_at_natual_gas_flow(0) + if excess_electricity < 0: + f = calculate_excess_electricity_at_natual_gas_flow + lb = 0. + fuel.imol[fuel_source] = 1 + ub = - excess_electricity * 3600 / fuel.LHV + while f(ub) < 0.: + lb = ub + ub *= 2 + flx.IQ_interpolation(f, lb, ub, xtol=1, ytol=1) + + if self.cooling_duty > 0.: + # In the event that no electricity is produced and the solver + # solution for natural gas is slightly below the requirement for steam + # (this would lead to a positive duty). + self.cooling_duty = 0. + Design['Work'] = 0. + + emissions.T = 298.15 # Will be updated later with the energy balance + emissions.P = 101325 + emissions.phase = 'g' + emissions_mol = emissions.mol + emissions_mol[:] = fuel.mol + for feed in non_empty_feeds: emissions_mol[:] += feed.mol + combustion_rxns.force_reaction(emissions_mol) + O2_consumption = -emissions.imol['O2'] + oxygen_rich_gas.reset_flow(**self.oxygen_rich_gas_composition) + z_O2 = oxygen_rich_gas.imol['O2'] / oxygen_rich_gas.F_mol + oxygen_rich_gas.F_mol = O2_consumption / z_O2 + emissions_mol += oxygen_rich_gas.mol + F_emissions = emissions.F_mass + z_CO2 = emissions.imass['CO2'] / F_emissions + z_CO2_target = self.CO2_emissions_concentration + if z_CO2 > z_CO2_target: + F_emissions_new = z_CO2 * F_emissions / z_CO2_target + dF_emissions = F_emissions_new - F_emissions + oxygen_rich_gas.F_mass = F_mass_O2_new = oxygen_rich_gas.F_mass + dF_emissions + emissions_mol += oxygen_rich_gas.mol * (dF_emissions / F_mass_O2_new) + emissions.H += self.H_loss_to_emissions + hu_cooling = bst.HeatUtility() + hu_cooling(self.cooling_duty, steam_demand.T) + hus_heating = bst.HeatUtility.sum_by_agent(self.steam_utilities) + for hu in hus_heating: hu.reverse() + self.heat_utilities = [*hus_heating, hu_cooling] + water_index = chemicals.index('7732-18-5') + makeup_water.mol[water_index] = blowdown_water.mol[water_index] = ( + self.total_steam * self.boiler_blowdown * 1 / (1 - self.RO_rejection) + ) + ash_IDs = [i.ID for i in self.chemicals if not i.formula] + emissions_mol = emissions.mol + SO2_produced = 0 + if 'SO2' in chemicals: + SO2_produced += emissions.imol['SO2'] + if 'CaSO4' in chemicals: + SO2_produced += emissions.imol['CaSO4'] + ash_IDs.append('CaSO4') + if SO2_produced: + rxn, ID_lime = self._get_desulfurization_rxn_and_coreactant() + # FGD lime scaled based on SO2 generated, + # 20% stoichiometric excess based on P52 of ref [1] + rxn.force_reaction(emissions) + lime.imol[ID_lime] = lime_mol = SO2_produced * 1.2 + emissions_mol.remove_negatives() + else: + lime.empty() + # About 0.4536 kg/hr of boiler chemicals are needed per 234484 kg/hr steam produced + chems.imol['Ash'] = boiler_chems = 1.9345e-06 * Design['Flow rate'] + ash_disposal.empty() + ash_disposal.copy_flow(emissions, IDs=tuple(ash_IDs), remove=True) + ash_disposal.imol['Ash'] += boiler_chems + dry_ash = ash_disposal.F_mass + moisture = min(emissions.imass['Water'], dry_ash * 0.3) # ~20% moisture + ash_disposal.imass['Water'] = moisture + emissions.imass['Water'] -= moisture + Design['Ash disposal'] = dry_ash + moisture + if SO2_produced: + if ID_lime == 'Ca(OH)2': # Ca(OH)2 + lime.imol['Water'] = 4 * lime_mol # Its a slurry + else: # CaO + lime.imol['Water'] = 5 * lime_mol + + def _cost(self): + self._decorated_cost() + self.power_utility.production = self.design_results['Work'] + + +# %% +# ============================================================================= +# Custom Heat Exchanger Network with minimum external heat utility constraint +# ============================================================================= + +class HXNWithMin(bst.facilities.HeatExchangerNetwork): + """Heat-exchanger network that caps maximum heat recovery so that the required + external hot utility duty is not lower than ``min_heat_util`` (kJ/hr). + + Parameters + ---------- + min_heat_util : float, optional + Minimum positive duty (kJ/hr) that must still be met by external + heating utilities (e.g., steam). Defaults to 0 (i.e., no cap). + T_min_app : float, optional + Minimum approach temperature for pinch analysis [K]. + """ + + def __init__(self, ID='', *, T_min_app=10.0, min_heat_util=0.0): + self.min_heat_util = min_heat_util # kJ/hr + super().__init__(ID, T_min_app=T_min_app) + + def _cost(self): + # First run default costing which also computes heat utility loads + super()._cost() + # After synthesis, `actual_heat_util_load` is positive if external hot utility is required + # and negative/zero if process heat is sufficient. + hot_util_load = getattr(self, 'actual_heat_util_load', None) + if hot_util_load is None: + return # Nothing to do + if hot_util_load < self.min_heat_util: + make_up_duty = self.min_heat_util - hot_util_load # kJ/hr to be supplied by steam + # Create additional heat utility demand so external steam is still counted + hu = bst.HeatUtility.get_agent('low_pressure_steam') + hu(make_up_duty, 423.15) # Typical LP steam temperature ~150 °C (423 K) + self.heat_utilities.append(hu) + + diff --git a/biorefineries/microalgae/lca.py b/biorefineries/microalgae/lca.py new file mode 100644 index 00000000..26b3a186 --- /dev/null +++ b/biorefineries/microalgae/lca.py @@ -0,0 +1,935 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat July 20 10:50:00 2025 + +Microalgae biorefinery to produce medium chain fatty acids +by anaerobic fermentation without external electron donor addition- LCA analysis + +References +---------- +[1] BioSTEAM Documentation: + https://biosteam.readthedocs.io/en/latest/tutorial/Creating_a_System.html +[2] Cortes-Peña et al., BioSTEAM: A Fast and Flexible Platform for the Design, + Simulation, and Techno-Economic Analysis of Biorefineries under Uncertainty. + ACS Sustainable Chem. Eng. 2020, 8 (8), 3302–3310. +[3] 3-Hydroxypropionic acid biorefineries project: + https://github.com/BioSTEAMDevelopmentGroup/Bioindustrial-Park/tree/master/biorefineries/HP +[4] Succinic projest + https://github.com/BioSTEAMDevelopmentGroup/Bioindustrial-Park/tree/master/biorefineries/succinic + +@author: Xingdong Shi +@version: 0.0.1 +""" + +from thermosteam import Stream +import numpy as np +import pandas as pd +import biosteam as bst +from biosteam.units.decorators import cost +from ._chemicals import chems + +__all__ = ['LCA'] + +class LCA: + """ + 微藻生物炼制厂生命周期环境影响评估类。 + + 参数 + ---------- + system : System + 微藻生产系统 + CFs : dict + 格式: {: {key: float}} + 其中key可以是流名称(对于complex_feeds)、化学品ID或'Electricity' + main_product : Stream + 主产品流 + main_product_chemical_IDs : list + 主产品化学品ID列表 + boiler : Unit + 锅炉设备单元 + by_products : list, 可选 + 副产品流列表 + complex_feeds : dict, 可选 + 复合进料流,格式: {key: (Stream, mass_kind)} + 其中mass_kind必须是('wet', 'dry')之一 + cooling_tower : Unit, 可选 + 冷却塔设备 + chilled_water_processing_units : list, 可选 + 冷冻水处理单元列表 + has_turbogenerator : bool, 可选 + 是否有涡轮发电机 + functional_unit : str, 可选 + 功能单位,如 '1 kg', '1 metric ton', '1 MJ (by LHV)', '1 MJ (by HHV)' + functional_quantity_per_h_fn : callable, 可选 + 计算每小时功能数量的函数 + add_EOL_GWP : bool, 可选 + 是否添加产品(主产品和副产品)的寿命终止温室气体排放 + input_biogenic_carbon_streams : list, 可选 + 生物来源碳输入流列表 + """ + + # 关键影响类别 + GWP_key = 'GWP_100' + FEC_key = 'FEC' + WC_key = 'WC' + + def __init__(self, + system, + CFs, + main_product, + main_product_chemical_IDs, + boiler, + by_products=[], + complex_feeds={}, + cooling_tower=None, + chilled_water_processing_units=[], + has_turbogenerator=None, + functional_unit=None, + functional_quantity_per_h_fn=None, + add_EOL_GWP=False, + input_biogenic_carbon_streams=[]): + + self.system = system + system._LCA = self + self.chemicals = chemicals = main_product.chemicals + self.units = self.system.units + self.flowsheet = self.system.flowsheet + self.streams = self.system.streams + + self.input_biogenic_carbon_streams = input_biogenic_carbon_streams + + self.add_EOL_GWP = add_EOL_GWP + + self.complex_feeds = complex_feeds + + kg_product_per_h_fn = lambda: self.main_product.imass[self.main_product_chemical_IDs].sum() + MJ_LHV_product_per_h_fn = lambda: self.main_product.LHV + MJ_HHV_product_per_h_fn = lambda: self.main_product.HHV + + if functional_unit and functional_quantity_per_h_fn: + raise AttributeError('必须提供functional_unit或functional_quantity_per_h_fn,但不能同时提供两者。') + + elif not functional_unit: + self.functional_unit = functional_unit = '1 kg' + self.functional_quantity_per_h_fn = kg_product_per_h_fn + + elif not functional_quantity_per_h_fn: + self.functional_unit = functional_unit + if functional_unit == '1 kg': + self.functional_quantity_per_h_fn = kg_product_per_h_fn + elif functional_unit == '1 metric ton': + self.functional_quantity_per_h_fn = lambda: kg_product_per_h_fn() * 1000. + elif functional_unit == '1 MJ (by LHV)': + self.functional_quantity_per_h_fn = MJ_LHV_product_per_h_fn + elif functional_unit == '1 MJ (by HHV)': + self.functional_quantity_per_h_fn = MJ_HHV_product_per_h_fn + + self.CFs = CFs + + self._chemical_IDs = [chem.ID for chem in chemicals] + + self._CF_streams = _CF_streams = {} + for impact_category in CFs.keys(): + _CF_streams[impact_category] = ic_CF_stream = Stream(f'{system.ID}_{impact_category}_CF_stream') + for k, v in CFs[impact_category].items(): + if not k in list(complex_feeds.keys()) + ['Electricity']: + try: + ic_CF_stream.imass[k] = v + except: + pass # 假设其他complex_feed IDs存在于CFs.keys()中 + + self._LCA_stream = Stream(f'{system.ID}_LCA_stream') + + self.main_product_chemical_IDs = main_product_chemical_IDs + self.main_product = main_product + self.by_products = by_products + + self.chem_IDs = [i.ID for i in chemicals] + + if has_turbogenerator is None: + has_turbogenerator = boiler.power_utility.production > 0. + self.has_turbogenerator = has_turbogenerator + + self.BT = self.boiler = boiler + try: + self.natural_gas = self.BT.natural_gas + except: + # 假设biogas替代自然气 + self.natural_gas = next((stream for stream in self.system.streams + if 'biogas' in stream.ID.lower() or + (hasattr(stream, 'price') and stream.price and + stream.imass['CH4'] > 0)), None) + if not self.natural_gas: + # 创建一个空的流作为自然气流的替代 + self.natural_gas = Stream('natural_gas', CH4=0) + + self.CT = self.cooling_tower = cooling_tower + self.CWP_units = self.chilled_water_processing_units = chilled_water_processing_units + + self._CO2_MW = self.chemicals.CO2.MW + + @property + def system_carbon_balance(self): + """计算系统碳平衡""" + total_C_in = sum([feed.get_atomic_flow('C') for feed in self.feeds]) + total_C_out = self.main_product.get_atomic_flow('C') +\ + sum([i.get_atomic_flow('C') for i in self.by_products]) +\ + sum([emission.get_atomic_flow('C') for emission in self.emissions]) + return total_C_out/total_C_in if total_C_in > 0 else 0 + + @property + def products(self): + """获取所有产品流(主产品和副产品)""" + return [self.main_product] + self.by_products + + @property + def functional_quantity_per_h(self): + """计算每小时功能数量""" + return self.functional_quantity_per_h_fn() + + @property + def emissions(self): + """获取排放流列表""" + emissions = list(self.system.products) + for i in self.products: + if i in emissions: + emissions.remove(i) + return emissions + + @property + def feeds(self): + """获取原料流列表""" + return self.system.feeds + + @property + def LCA_stream(self): + """创建LCA评估用的综合流""" + _LCA_stream = self._LCA_stream + to_mix = list(self.feeds) + for s, m_k in self.complex_feeds.values(): + if s in to_mix: + to_mix.remove(s) + _LCA_stream.mix_from(to_mix) + return _LCA_stream + + def get_material_impact_array(self, impact_category): + """获取特定影响类别的物质影响数组""" + return self.LCA_stream.mass*self._CF_streams[impact_category].mass + + def get_material_impact(self, impact_category): + """计算特定影响类别的物质影响""" + return self.get_material_impact_array(impact_category).sum() / self.functional_quantity_per_h + + @property + def net_electricity(self): + """计算净电力消耗""" + return self.system.power_utility.rate + + def get_net_electricity_impact(self, impact_category): + """计算特定影响类别的净电力影响""" + if 'Electricity' in self.CFs[impact_category]: + return self.net_electricity * self.CFs[impact_category]['Electricity'] / self.functional_quantity_per_h + return 0 + + @property + def EOL_GWP(self): + """计算寿命终止全球变暖潜能值""" + return sum([i.get_atomic_flow('C') for i in [self.main_product] + self.by_products]) *\ + self._CO2_MW/self.functional_quantity_per_h + + @property + def direct_emissions_GWP(self): + """计算直接排放全球变暖潜能值""" + return sum([stream.get_atomic_flow('C') for stream in self.emissions]) *\ + self._CO2_MW / self.functional_quantity_per_h + + @property + def biogenic_emissions_GWP(self): + """计算生物来源直接排放全球变暖潜能值""" + return sum([i.get_atomic_flow('C') for i in self.input_biogenic_carbon_streams]) *\ + self._CO2_MW/self.functional_quantity_per_h + + @property + def direct_non_biogenic_emissions_GWP(self): + """计算非生物来源直接排放全球变暖潜能值""" + return self.direct_emissions_GWP -\ + self.biogenic_emissions_GWP +\ + int(self.add_EOL_GWP)*self.EOL_GWP + + def get_complex_feeds_impact(self, impact_category): + """计算复合进料流在特定影响类别的影响""" + tot_cfs_impact = 0. + impact_CFs = self.CFs[impact_category] + for k, (s, mass_kind) in self.complex_feeds.items(): + if k in impact_CFs: + mass = s.F_mass + if mass_kind=='dry': mass -= s.imass['Water'] + tot_cfs_impact += impact_CFs[k] * mass + return tot_cfs_impact/self.functional_quantity_per_h + + def get_total_impact(self, impact_category): + """计算特定影响类别的总影响""" + tot_impact = self.get_complex_feeds_impact(impact_category) +\ + self.get_material_impact(impact_category) +\ + self.get_net_electricity_impact(impact_category) + if impact_category in ('GWP', 'GWP_100', 'GWP100'): + tot_impact += self.direct_non_biogenic_emissions_GWP + return tot_impact + + + def get_material_impact_breakdown(self, impact_category): + """获取特定影响类别的物质影响明细""" + # 初始化关键化学品的影响字典 + chemical_impact_dict = {'H2SO4':0, 'NaOH':0, 'NH4OH':0, 'CH4':0, 'CO2':0, 'Octanol':0} + LCA_stream = self.LCA_stream + ic_CF_stream = self._CF_streams[impact_category] + functional_quantity_per_h = self.functional_quantity_per_h + # 添加其他化学品的影响 + chemical_impact_dict_additional = {ID: LCA_stream.imass[ID] * ic_CF_stream.imass[ID] / functional_quantity_per_h + for ID in self.chem_IDs} + # 移除值为0的项 + for k in list(chemical_impact_dict_additional.keys()): + if chemical_impact_dict_additional[k] == 0.: del(chemical_impact_dict_additional[k]) + chemical_impact_dict.update(chemical_impact_dict_additional) + return chemical_impact_dict + + def get_material_impact_breakdown_as_fraction_of_material_impact(self, impact_category): + """获取物质影响明细,表示为物质影响的比例""" + chemical_impact_dict = self.get_material_impact_breakdown(impact_category) + tot_material_impact = self.get_material_impact(impact_category) + if tot_material_impact > 0: + for k,v in chemical_impact_dict.items(): + chemical_impact_dict[k] /= tot_material_impact + return chemical_impact_dict + + def get_material_impact_breakdown_as_fraction_of_total_impact(self, impact_category): + """获取物质影响明细,表示为总影响的比例""" + chemical_impact_dict = self.get_material_impact_breakdown(impact_category) + tot_impact = self.get_total_impact(impact_category) + if tot_impact > 0: + for k,v in chemical_impact_dict.items(): + chemical_impact_dict[k] /= tot_impact + return chemical_impact_dict + + def get_natural_gas_impact(self, impact_category): + """计算自然气在特定影响类别的影响""" + if 'CH4' in self.CFs[impact_category]: + return self.CFs[impact_category]['CH4']*self.natural_gas.F_mass/self.functional_quantity_per_h + return 0 + + @property + def natural_gas_combustion_GWP(self): + """计算自然气燃烧的全球变暖潜能值""" + return (self.natural_gas.get_atomic_flow('C')) * self._CO2_MW / self.functional_quantity_per_h + + def get_complex_feed_impact_by_ID(self, impact_category, complex_feed_ID): + """计算特定复合进料流在特定影响类别的影响""" + if complex_feed_ID in self.complex_feeds and complex_feed_ID in self.CFs[impact_category]: + s, mass_kind = self.complex_feeds[complex_feed_ID] + mass = s.F_mass + if mass_kind=='dry': mass -= s.imass['Water'] + return self.CFs[impact_category][complex_feed_ID] * mass + return 0 + + def get_material_impact_by_ID(self, impact_category, material_ID): + """计算特定物质在特定影响类别的影响""" + if material_ID in self.CFs[impact_category]: + return self.CFs[impact_category][material_ID] * self.LCA_stream.imass[material_ID] + return 0 + + @property + def electricity_demand(self): + """计算电力需求""" + return sum([i.power_utility.consumption for i in self.system.units]) + + @property + def cooling_electricity_demand(self): + """计算冷却系统电力需求""" + return (self.CT.power_utility.rate if self.CT else 0) + \ + sum([i.power_utility.rate for i in self.CWP_units]) + + def __repr__(self): + """返回LCA对象的字符串表示""" + return f'LCA对象,用于{self.system.ID}。影响类别: {list(self.CFs.keys())}。' + + def show(self): + """打印LCA对象信息""" + print(self.__repr__()) + _ipython_display_ = show + + def generate_report(self, output_file=None): + """生成LCA报告""" + report = [] + report.append(f"======= 微藻生物炼制厂生命周期评估报告 =======") + report.append(f"系统ID: {self.system.ID}") + report.append(f"功能单位: {self.functional_unit}") + report.append(f"主产品: {self.main_product.ID}") + + report.append("\n----- 系统碳平衡 -----") + report.append(f"碳平衡: {self.system_carbon_balance:.4f}") + + report.append("\n----- 影响类别结果 -----") + for impact_category in self.CFs.keys(): + total_impact = self.get_total_impact(impact_category) + report.append(f"{impact_category}: {total_impact:.4f} 单位/{self.functional_unit}") + + report.append(f" - 物质影响: {self.get_material_impact(impact_category):.4f} ({self.get_material_impact(impact_category)/total_impact*100:.1f}%)") + report.append(f" - 电力影响: {self.get_net_electricity_impact(impact_category):.4f} ({self.get_net_electricity_impact(impact_category)/total_impact*100 if total_impact else 0:.1f}%)") + report.append(f" - 复合进料影响: {self.get_complex_feeds_impact(impact_category):.4f} ({self.get_complex_feeds_impact(impact_category)/total_impact*100 if total_impact else 0:.1f}%)") + + if impact_category in ('GWP', 'GWP_100', 'GWP100'): + report.append(f" - 直接排放GWP: {self.direct_emissions_GWP:.4f} ({self.direct_emissions_GWP/total_impact*100 if total_impact else 0:.1f}%)") + report.append(f" - 生物来源排放GWP: {self.biogenic_emissions_GWP:.4f}") + report.append(f" - 非生物来源直接排放GWP: {self.direct_non_biogenic_emissions_GWP:.4f}") + + if output_file: + with open(output_file, 'w') as f: + f.write('\n'.join(report)) + + return '\n'.join(report) + + def get_impact_dataframe(self): + """返回包含所有影响结果的DataFrame""" + data = {} + + for impact_category in self.CFs.keys(): + data[f'{impact_category}_总影响'] = [self.get_total_impact(impact_category)] + data[f'{impact_category}_物质影响'] = [self.get_material_impact(impact_category)] + data[f'{impact_category}_电力影响'] = [self.get_net_electricity_impact(impact_category)] + data[f'{impact_category}_复合进料影响'] = [self.get_complex_feeds_impact(impact_category)] + + if impact_category in ('GWP', 'GWP_100', 'GWP100'): + data[f'{impact_category}_直接排放'] = [self.direct_emissions_GWP] + data[f'{impact_category}_生物来源排放'] = [self.biogenic_emissions_GWP] + data[f'{impact_category}_非生物来源直接排放'] = [self.direct_non_biogenic_emissions_GWP] + + data['功能单位'] = [self.functional_unit] + data['系统碳平衡'] = [self.system_carbon_balance] + + return pd.DataFrame(data) + + def export_results(self, file_path): + """导出结果到Excel文件""" + df = self.get_impact_dataframe() + df.to_excel(file_path) + return f"结果已导出到 {file_path}" + + def analyze_emissions_breakdown(self, threshold=1e-6): + """ + 分析emissions的详细组成 + + 参数 + ---------- + threshold : float + 显示阈值,只显示流量大于此值的组分 + + 返回 + ---------- + dict + 包含emissions分析结果的字典 + """ + # 获取所有emissions流 + emissions_streams = [] + for unit in self.system.units: + if hasattr(unit, 'outs'): + for out_stream in unit.outs: + if out_stream and out_stream not in self.products: + emissions_streams.append(out_stream) + + # 去重 + unique_emissions = [] + for stream in emissions_streams: + if stream not in unique_emissions: + unique_emissions.append(stream) + + results = { + 'total_streams': len(unique_emissions), + 'streams_analysis': [] + } + + print("=== Emissions详细分解分析 ===") + print(f"总emissions流数量: {len(unique_emissions)}") + + for i, stream in enumerate(unique_emissions): + print(f"\n--- Emissions流 {i+1}: {stream.ID} ---") + print(f"来源单元: {stream.source.ID if stream.source else 'Unknown'}") + print(f"总流量: {stream.F_mass:.3f} kg/h") + print(f"总摩尔流量: {stream.F_mol:.3f} kmol/h") + + # 分析组分 + components = [] + total_mass = stream.F_mass + total_mol = stream.F_mol + + print("\n组分详细:") + print("组分名称".ljust(20) + "质量流量(kg/h)".ljust(15) + "摩尔流量(kmol/h)".ljust(15) + "质量分数(%)".ljust(12) + "摩尔分数(%)".ljust(12)) + print("-" * 80) + + for chem in stream.chemicals: + mass_flow = stream.imass[chem.ID] + mol_flow = stream.imol[chem.ID] + + if mass_flow > threshold: # 只显示有意义的流量 + mass_fraction = (mass_flow / total_mass * 100) if total_mass > 0 else 0 + mol_fraction = (mol_flow / total_mol * 100) if total_mol > 0 else 0 + + print(f"{chem.ID:<20} {mass_flow:>14.3f} {mol_flow:>14.3f} {mass_fraction:>11.2f} {mol_fraction:>11.2f}") + + components.append({ + 'chemical': chem.ID, + 'mass_flow': mass_flow, + 'mol_flow': mol_flow, + 'mass_fraction': mass_fraction, + 'mol_fraction': mol_fraction + }) + + # 碳含量分析 + total_carbon = 0 + carbon_components = [] + + for chem in stream.chemicals: + if hasattr(chem, 'formula') and chem.formula: + carbon_atoms = 0 + formula = chem.formula + if 'C' in formula: + c_index = formula.find('C') + if c_index + 1 < len(formula) and formula[c_index + 1].isdigit(): + num_str = "" + i = c_index + 1 + while i < len(formula) and formula[i].isdigit(): + num_str += formula[i] + i += 1 + carbon_atoms = int(num_str) + else: + carbon_atoms = 1 + + if carbon_atoms > 0: + mol_flow = stream.imol[chem.ID] + carbon_mol = mol_flow * carbon_atoms + if carbon_mol > threshold: + total_carbon += carbon_mol + carbon_components.append({ + 'chemical': chem.ID, + 'mol_flow': mol_flow, + 'carbon_atoms': carbon_atoms, + 'carbon_mol': carbon_mol + }) + + if carbon_components: + print(f"\n含碳组分分析:") + print("组分名称".ljust(20) + "摩尔流量(kmol/h)".ljust(15) + "碳原子数".ljust(10) + "碳摩尔流量(kmol/h)".ljust(15)) + print("-" * 65) + + for comp in carbon_components: + print(f"{comp['chemical']:<20} {comp['mol_flow']:>14.3f} {comp['carbon_atoms']:>9} {comp['carbon_mol']:>14.3f}") + + print(f"\n总碳摩尔流量: {total_carbon:.3f} kmol/h") + print(f"总碳质量流量: {total_carbon * 12.01:.3f} kg/h") + + # CO2分析 + if 'CO2' in stream.chemicals: + co2_mol = stream.imol['CO2'] + co2_mass = stream.imass['CO2'] + if co2_mol > threshold: + co2_mass_fraction = (co2_mass / total_mass * 100) if total_mass > 0 else 0 + co2_mol_fraction = (co2_mol / total_mol * 100) if total_mol > 0 else 0 + + print(f"\nCO2分析:") + print(f"CO2摩尔流量: {co2_mol:.3f} kmol/h") + print(f"CO2质量流量: {co2_mass:.3f} kg/h") + print(f"CO2质量分数: {co2_mass_fraction:.2f}%") + print(f"CO2摩尔分数: {co2_mol_fraction:.2f}%") + print(f"CO2当量质量: {co2_mol * 44.01:.3f} kg/h") + + # 其他重要气体 + important_gases = ['H2O', 'N2', 'O2', 'SO2', 'NO', 'NO2', 'CH4', 'N2O'] + found_gases = [] + + for gas in important_gases: + if gas in stream.chemicals: + mol_flow = stream.imol[gas] + mass_flow = stream.imass[gas] + if mol_flow > threshold: + mass_fraction = (mass_flow / total_mass * 100) if total_mass > 0 else 0 + mol_fraction = (mol_flow / total_mol * 100) if total_mol > 0 else 0 + found_gases.append({ + 'gas': gas, + 'mol_flow': mol_flow, + 'mass_flow': mass_flow, + 'mass_fraction': mass_fraction, + 'mol_fraction': mol_fraction + }) + + if found_gases: + print(f"\n其他重要气体:") + for gas_info in found_gases: + print(f"{gas_info['gas']}: {gas_info['mol_flow']:.3f} kmol/h, {gas_info['mass_flow']:.3f} kg/h, {gas_info['mass_fraction']:.2f}% mass, {gas_info['mol_fraction']:.2f}% mol") + + # 热力学性质 + print(f"\n热力学性质:") + print(f"温度: {stream.T:.2f} K ({stream.T - 273.15:.2f} °C)") + print(f"压力: {stream.P:.0f} Pa ({stream.P/101325:.3f} atm)") + print(f"焓: {stream.H:.3f} kJ/h") + + # 保存分析结果 + stream_analysis = { + 'stream_id': stream.ID, + 'source_unit': stream.source.ID if stream.source else 'Unknown', + 'total_mass_flow': stream.F_mass, + 'total_mol_flow': stream.F_mol, + 'components': components, + 'carbon_analysis': { + 'total_carbon_mol': total_carbon, + 'total_carbon_mass': total_carbon * 12.01, + 'carbon_components': carbon_components + }, + 'thermodynamic_properties': { + 'temperature': stream.T, + 'pressure': stream.P, + 'enthalpy': stream.H + } + } + + results['streams_analysis'].append(stream_analysis) + + return results + + # 便捷属性 - GWP相关 + @property + def material_GWP(self): + return self.get_material_impact(self.GWP_key) + + @property + def feedstock_GWP(self): + return self.get_complex_feeds_impact(self.GWP_key) + + @property + def net_electricity_GWP(self): + return self.get_net_electricity_impact(self.GWP_key) + + @property + def GWP(self): + return self.get_total_impact(self.GWP_key) + + # 便捷属性 - FEC相关 + @property + def material_FEC(self): + return self.get_material_impact(self.FEC_key) + + @property + def feedstock_FEC(self): + return self.get_complex_feeds_impact(self.FEC_key) + + @property + def net_electricity_FEC(self): + return self.get_net_electricity_impact(self.FEC_key) + + @property + def FEC(self): + return self.get_total_impact(self.FEC_key) + + # 便捷属性 - WC相关 + @property + def material_WC(self): + return self.get_material_impact(self.WC_key) + + @property + def feedstock_WC(self): + return self.get_complex_feeds_impact(self.WC_key) + + @property + def net_electricity_WC(self): + return self.get_net_electricity_impact(self.WC_key) + + @property + def WC(self): + return self.get_total_impact(self.WC_key) + + +# 使用示例 +def create_microalgae_lca(system, main_product, main_product_chemical_IDs, boiler): + """ + 创建微藻生物炼制厂的LCA对象 + + 参数 + ---------- + system : System + 微藻生产系统 + main_product : Stream + 主产品流 + main_product_chemical_IDs : list + 主产品化学品ID列表 + boiler : Unit + 锅炉设备单元 + + 返回 + ---------- + LCA + 生命周期评估对象 + """ + # 影响因子 + # 示例数据,实际应用中需要替换为准确的数据 + CFs = { + 'GWP_100':{ + 'Electricity': 0.36, # [kg*CO2*eq / kWhr] From GREET; NG-Fired Simple-Cycle Gas Turbine CHP Plant + # 0.66 is the GWP from producing diesel from GREET; Conventional diesel from crude oil for US Refineries. + # Downstream fuel emissions are added in. Accounts for how biodiesel has less energy than diesel. + 'CH4': 0.33, # Natural gas from shell conventional recovery, GREET; includes non-biogenic emissions + + 'H2SO4': 0.04447, # kg CO2e/kg + 'NaOH': 2.01, # GREET + 'NH4OH': 1.28304, # multiplied by chemicals.NH3.MW/chemicals.NH4OH.MW, + 'CalciumDihydroxide': 1.29, # GREET + 'Ethanol': 1.44, # from BDO project + + 'GlucoAmylase': 6.16, # from Succinic project + 'AlphaAmylase': 6.16, # from Succinic project + + 'Octanol': 1.9, # kg CO2e/kg + + 'Microalgae': 0.15, # + }, + 'FEC': { + 'Electricity': 10.0, # MJ/kWh + 'H2SO4': 568.98/1e3, + 'NaOH': 29, # MJ/kg + 'NH4OH': 42 * 0.4860, # multiplied by chemicals.NH3.MW/chemicals.NH4OH.MW, + 'CalciumDihydroxide': 4.0, # MJ/kg + 'CH4': 55.0, # MJ/kg + 'Ethanol': 16, + 'Octanol': 45.0, # MJ/kg + 'GlucoAmylase': 90.0, # MJ/kg + 'AlphaAmylase': 90.0, # MJ/kg + 'Microalgae': 2.0, # MJ/kg + }, + 'WC': { # 水消耗 + 'Electricity': 0.005, # m3 / kWh + 'H2SO4': 0.01, # m3 / kg + 'NaOH': 0.02, # m3 / kg + 'NH4OH': 0.015, # m3 / kg + 'CalciumDihydroxide': 0.007, # m3 / kg + 'CH4': 0.0, # m3 / kg + 'CO2': 0.0, # m3 / kg + 'Octanol': 0.05, # m3 / kg + 'GlucoAmylase': 0.1, # m3 / kg + 'AlphaAmylase': 0.1, # m3 / kg + 'Microalgae': 0.2, # m3 / kg + } + } + + # 获取系统中的冷却塔和冷冻水处理单元 + cooling_tower = None + chilled_water_processing_units = [] + + for unit in system.units: + if unit.__class__.__name__ == 'CoolingTower': + cooling_tower = unit + elif unit.__class__.__name__ == 'ChilledWaterPackage': + chilled_water_processing_units.append(unit) + + # 尝试找到系统中的锅炉涡轮发电机 + if boiler is None: + for unit in system.units: + # 优先查找锅炉涡轮发电机 + if (isinstance(unit, bst.facilities.BoilerTurbogenerator) or + ('BT' in unit.ID and hasattr(unit, 'turbogenerator_efficiency'))): + boiler = unit + break + + # 如果没有找到锅炉涡轮发电机,查找普通锅炉 + if boiler is None: + for unit in system.units: + if 'boiler' in unit.__class__.__name__.lower() or 'BT' in unit.ID: + boiler = unit + break + + # 如果没有找到锅炉,使用最大加热负荷的热交换器 + if boiler is None: + heat_exchangers = [] + for unit in system.units: + if unit.__class__.__name__ == 'HXutility' and hasattr(unit, 'duty') and unit.duty > 0: + heat_exchangers.append(unit) + + if heat_exchangers: + # 选择具有最大加热负荷的热交换器 + boiler = max(heat_exchangers, key=lambda x: x.duty) + + # 创建复合进料字典 + complex_feeds = { + 'Microalgae': (system.feeds[0], 'dry'), # 微藻进料 + } + + # 确定生物来源碳输入流 + # 找到微藻原料流 + microalgae_feed = None + for feed in system.feeds: + if 'microalgae' in feed.ID.lower(): + microalgae_feed = feed + break + + # 如果没有找到microalgae流,使用第一个包含碳的原料流 + if microalgae_feed is None: + for feed in system.feeds: + if feed.get_atomic_flow('C') > 0: + microalgae_feed = feed + break + + input_biogenic_carbon_streams = [microalgae_feed] if microalgae_feed else [] + + # 创建LCA对象 + microalgae_lca = LCA( + system=system, + CFs=CFs, + main_product=main_product, + main_product_chemical_IDs=main_product_chemical_IDs, + boiler=boiler, + by_products=[system.flowsheet.stream.butyric_acid_product, + system.flowsheet.stream.heptanoic_acid_product, + system.flowsheet.stream.caprylic_acid_product], + complex_feeds=complex_feeds, + cooling_tower=cooling_tower, + chilled_water_processing_units=chilled_water_processing_units, + has_turbogenerator=None, # 自动确定 + functional_unit='1 kg', # 每千克产品 + add_EOL_GWP=True, + input_biogenic_carbon_streams=input_biogenic_carbon_streams + ) + + return microalgae_lca + +def print_non_biogenic_breakdown(lca, threshold=1e-4): + print('\n-- 非生物源直接排放明细 (kg CO2-eq / 功能单位) --') + tot = 0 + for s in lca.emissions: + if s in lca.input_biogenic_carbon_streams: + # 生物源流 => 扣除,不计入非生物 + continue + C = s.get_atomic_flow('C') # kmol/h + if C == 0: + continue + impact = C * 44 / lca.functional_quantity_per_h + if abs(impact) > threshold: + print(f'{s.ID:<15} {impact:8.4f}') + tot += impact + if lca.add_EOL_GWP: + print(f'End-of-Life {lca.EOL_GWP:8.4f}') + tot += lca.EOL_GWP + print('-----------------------------------------') + print(f'Total {tot:8.4f}') + + +# 主程序运行示例 +if __name__ == "__main__": + import biosteam as bst + from microalgae import system + from microalgae._chemicals import chems + + # 初始化系统 + bst.settings.set_thermo(chems) + microalgae_sys = system.create_microalgae_MCCA_production_sys() + microalgae_sys.simulate() + + # 获取主产品流和主产品化学品ID + main_product = microalgae_sys.flowsheet.stream.caproic_acid_product + main_product_chemical_IDs = ['CaproicAcid'] + + # 尝试找到锅炉单元 + boiler = None + for unit in microalgae_sys.units: + if hasattr(unit, 'heat_utilities') and unit.heat_utilities: + boiler = unit + break + + # 如果没有找到锅炉,使用第一个热交换器 + if boiler is None: + for unit in microalgae_sys.units: + if unit.__class__.__name__ == 'HXutility': + boiler = unit + break + + # 创建LCA对象 + lca = create_microalgae_lca( + system=microalgae_sys, + main_product=main_product, + main_product_chemical_IDs=main_product_chemical_IDs, + boiler=boiler + ) + + # 输出LCA结果 + print("\n===== 微藻生物炼制厂LCA结果 =====") + print(f"系统碳平衡: {lca.system_carbon_balance:.4f}") + print(f"GWP (kg CO2e/{lca.functional_unit}): {lca.GWP:.4f}") + print(f"FEC (MJ/{lca.functional_unit}): {lca.FEC:.4f}") + print(f"WC (m³/{lca.functional_unit}): {lca.WC:.4f}") + + # 输出GWP明细 + print(f"----- GWP明细 -----") + print(f"物质影响: {lca.material_GWP:.4f} ({lca.material_GWP/lca.GWP*100 if lca.GWP else 0:.1f}%)") + print(f"电力影响: {lca.net_electricity_GWP:.4f} ({lca.net_electricity_GWP/lca.GWP*100 if lca.GWP else 0:.1f}%)") + print(f"原料影响: {lca.feedstock_GWP:.4f} ({lca.feedstock_GWP/lca.GWP*100 if lca.GWP else 0:.1f}%)") + print(f"直接排放(非生物源): {lca.direct_non_biogenic_emissions_GWP:.4f} ({lca.direct_non_biogenic_emissions_GWP/lca.GWP*100 if lca.GWP else 0:.1f}%)") + + # 分析emissions分解 + #print(f"\n{'='*60}") + #lca.analyze_emissions_breakdown() + + # 生成并保存详细报告 + report = lca.generate_report() + print("\n完整报告已生成。报表示例:") + print(report[:500] + "...\n") + + # 导出结果到Excel + # lca.export_results("microalgae_lca_results.xlsx") + + # 1) 物质影响 – 按单一化学品列出 + mat_break = lca.get_material_impact_breakdown('GWP_100') + print('\n-- Material impact breakdown (kg CO2-eq / kg product) --') + for k, v in mat_break.items(): + if abs(v) > 1e-6: # 过滤 0 + print(f'{k:<15} {v:8.4f}') + + # 2) 电力影响 – 直接由净电力×CF 得到 + elec_CF = lca.CFs['GWP_100']['Electricity'] + net_kWh = lca.net_electricity # kWh h⁻¹ + per_kg = net_kWh * elec_CF / lca.functional_quantity_per_h + print(f'\nElectricity ({net_kWh:.3f} kWh/h) × {elec_CF} = {per_kg:.4f} kg CO2-eq') + + # 3) 复合进料(feedstock)影响 + feed_imp = lca.get_complex_feeds_impact('GWP_100') + print(f'\nFeedstock impact = {feed_imp:.4f} kg CO2-eq') + + # 若想看各复合进料流的贡献: + for ID, (s, _) in lca.complex_feeds.items(): + impact = lca.get_complex_feed_impact_by_ID('GWP_100', ID) + print(f' {ID:<15} {impact:8.4f}') + + # 4) 直接排放 + print(f'\nDirect non-biogenic emissions GWP = {lca.direct_non_biogenic_emissions_GWP:.4f}') + print('\n 排放流详细:') + for s in lca.emissions: + C = s.get_atomic_flow("C") + if C>0: + stream_gwp = C * 44 / lca.functional_quantity_per_h # 44 g CO2 / mol C + if stream_gwp!=0: + print(f' {s.ID:<15} {stream_gwp:8.4f} kg CO2-eq') + + # 若已把生物源排放列入 `input_biogenic_carbon_streams`, + # 也可打印 biogenic 值: + print(f'\nBiogenic CO2 credit = {lca.biogenic_emissions_GWP:.4f}') + print_non_biogenic_breakdown(lca) + + # u = microalgae_sys.flowsheet.unit + # s = microalgae_sys.flowsheet.stream + + # s.s49.show() + # print('From:', s.s49.source) + # print('To :', s.s49.sink) + + # s.s51.show() + # u.R501.show() + # print('From :', s.s51.source) + # print('To :', s.s51.sink) + + + + diff --git a/biorefineries/microalgae/lca_output.py b/biorefineries/microalgae/lca_output.py new file mode 100644 index 00000000..2208c1df --- /dev/null +++ b/biorefineries/microalgae/lca_output.py @@ -0,0 +1,380 @@ +# -*- coding: utf-8 -*- +""" +Microalgae LCA Output Module + +基于succinic项目的lca_output.py模式,为microalgae项目提供LCA分析、报告和数据导出功能。 + +@author: Xingdong Shi +@version: 0.0.1 +""" + +import sys +import os +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +from pathlib import Path + +# 添加项目路径以便导入 +current_dir = Path(__file__).parent +project_root = current_dir.parent +sys.path.insert(0, str(project_root)) + +try: + from . import system + from ._chemicals import chems + from .lca import LCA, create_microalgae_lca +except ImportError: + # 如果相对导入失败,尝试绝对导入 + try: + from microalgae import system + from microalgae._chemicals import chems + from microalgae.lca import LCA, create_microalgae_lca + except ImportError: + print("警告:无法导入microalgae模块,请确保在正确的环境中运行") + +def get_gwp_breakdown_data(lca): + """ + 获取GWP分解数据,按照microalgae项目的计算方式 + """ + material_GWP_breakdown = lca.get_material_impact_breakdown('GWP_100') + + gwp_breakdown = { + 'feedstock*': lca.feedstock_GWP, + 'H2SO4': material_GWP_breakdown.get('H2SO4', 0), + 'NaOH': material_GWP_breakdown.get('NaOH', 0), + 'NH4OH': material_GWP_breakdown.get('NH4OH', 0), + 'CalciumDihydroxide': material_GWP_breakdown.get('CalciumDihydroxide', 0), + 'Ethanol': material_GWP_breakdown.get('Ethanol', 0), + 'Octanol': material_GWP_breakdown.get('Octanol', 0), + 'GlucoAmylase': material_GWP_breakdown.get('GlucoAmylase', 0), + 'AlphaAmylase': material_GWP_breakdown.get('AlphaAmylase', 0), + 'CH4': material_GWP_breakdown.get('CH4', 0), + 'net electricity': lca.net_electricity_GWP, + 'direct non-biogenic emissions': lca.direct_non_biogenic_emissions_GWP, + } + + # 计算正值的总和,然后归一化 + tot_positive_GWP = sum([v for v in gwp_breakdown.values() if v > 0]) + if tot_positive_GWP > 0: + for k, v in gwp_breakdown.items(): + gwp_breakdown[k] = v / tot_positive_GWP + else: # Fallback if no positive values + tot_abs_GWP = sum([abs(v) for v in gwp_breakdown.values()]) + if tot_abs_GWP > 0: + for k, v in gwp_breakdown.items(): + gwp_breakdown[k] = v / tot_abs_GWP + + return gwp_breakdown + +def export_gwp_breakdown_data(lca, output_file=None): + """ + 导出GWP分解数据到CSV文件 + + 参数 + ---------- + lca : LCA + LCA对象 + output_file : str, 可选 + 输出文件路径,默认为'microalgae_gwp_breakdown.csv' + """ + if output_file is None: + output_file = 'microalgae_gwp_breakdown.csv' + + gwp_breakdown = get_gwp_breakdown_data(lca) + + # 创建DataFrame + df = pd.DataFrame(list(gwp_breakdown.items()), columns=['Component', 'Percentage']) + df['Percentage'] = df['Percentage'] * 100 # 转换为百分比 + + # 保存到CSV + df.to_csv(output_file, index=False) + print(f"GWP分解数据已保存到: {output_file}") + + return df + +def print_microalgae_lca_results(lca): + """ + 打印microalgae LCA结果 + + 参数 + ---------- + lca : LCA + LCA对象 + """ + print("\n" + "="*60) + print("MICROALGAE 生物炼制厂生命周期评估结果") + print("="*60) + + print(f"\n功能单位: {lca.functional_unit}") + print(f"系统碳平衡: {lca.system_carbon_balance:.4f}") + + # 主要影响类别结果 + print(f"\n----- 主要影响类别 -----") + print(f"GWP (kg CO2e/{lca.functional_unit}): {lca.GWP:.4f}") + print(f"FEC (MJ/{lca.functional_unit}): {lca.FEC:.4f}") + print(f"WC (m³/{lca.functional_unit}): {lca.WC:.4f}") + + # GWP分解 + print(f"\n----- GWP分解 -----") + print(f"物质影响: {lca.material_GWP:.4f} ({lca.material_GWP/lca.GWP*100 if lca.GWP else 0:.1f}%)") + print(f"原料影响: {lca.feedstock_GWP:.4f} ({lca.feedstock_GWP/lca.GWP*100 if lca.GWP else 0:.1f}%)") + print(f"电力影响: {lca.net_electricity_GWP:.4f} ({lca.net_electricity_GWP/lca.GWP*100 if lca.GWP else 0:.1f}%)") + print(f"直接排放(非生物源): {lca.direct_non_biogenic_emissions_GWP:.4f} ({lca.direct_non_biogenic_emissions_GWP/lca.GWP*100 if lca.GWP else 0:.1f}%)") + + # 物质影响明细 + print(f"\n----- 物质影响明细 (kg CO2e/{lca.functional_unit}) -----") + material_breakdown = lca.get_material_impact_breakdown('GWP_100') + for component, impact in material_breakdown.items(): + if abs(impact) > 1e-6: + print(f"{component:<20}: {impact:8.4f}") + + # 电力影响详情 + print(f"\n----- 电力影响详情 -----") + elec_CF = lca.CFs['GWP_100']['Electricity'] + net_kWh = lca.net_electricity + per_unit = net_kWh * elec_CF / lca.functional_quantity_per_h + print(f"净电力消耗: {net_kWh:.3f} kWh/h") + print(f"电力影响因子: {elec_CF} kg CO2e/kWh") + print(f"电力影响: {per_unit:.4f} kg CO2e/{lca.functional_unit}") + + # 原料影响详情 + print(f"\n----- 原料影响详情 -----") + for ID, (stream, mass_kind) in lca.complex_feeds.items(): + impact = lca.get_complex_feed_impact_by_ID('GWP_100', ID) + print(f"{ID:<20}: {impact:8.4f} kg CO2e/{lca.functional_unit}") + + # 直接排放详情 + print(f"\n----- 直接排放详情 -----") + print(f"总直接排放GWP: {lca.direct_emissions_GWP:.4f}") + print(f"生物来源排放GWP: {lca.biogenic_emissions_GWP:.4f}") + print(f"非生物来源直接排放GWP: {lca.direct_non_biogenic_emissions_GWP:.4f}") + + # 排放流详细 + print(f"\n排放流详细:") + for stream in lca.emissions: + C = stream.get_atomic_flow("C") + if C > 0: + stream_gwp = C * 44 / lca.functional_quantity_per_h + if abs(stream_gwp) > 1e-6: + print(f" {stream.ID:<20}: {stream_gwp:8.4f} kg CO2e/{lca.functional_unit}") + + # GWP分解数据 + print(f"\n----- GWP分解数据 -----") + gwp_breakdown = get_gwp_breakdown_data(lca) + + print("GWP分解数据:") + print("="*50) + total_percentage = 0 + for component, percentage in gwp_breakdown.items(): + print(f"{component:<30} {percentage*100:6.2f}%") + if percentage > 0: # 只计算正值 + total_percentage += percentage * 100 + print("-"*50) + print(f"{'总计':<30} {total_percentage:6.2f}%") + + print("\n" + "="*60) + +def create_microalgae_lca_scenario(system, main_product, main_product_chemical_IDs, boiler=None): + """ + 创建microalgae LCA场景 + + 参数 + ---------- + system : System + 微藻生产系统 + main_product : Stream + 主产品流 + main_product_chemical_IDs : list + 主产品化学品ID列表 + boiler : Unit, 可选 + 锅炉设备单元 + + 返回 + ---------- + LCA + 生命周期评估对象 + """ + # 创建LCA对象 + lca = create_microalgae_lca( + system=system, + main_product=main_product, + main_product_chemical_IDs=main_product_chemical_IDs, + boiler=boiler, + add_EOL_GWP=True + ) + + return lca + +def export_lca_results_to_excel(lca, output_file=None): + """ + 导出LCA结果到Excel文件 + + 参数 + ---------- + lca : LCA + LCA对象 + output_file : str, 可选 + 输出文件路径,默认为'microalgae_lca_results.xlsx' + """ + if output_file is None: + output_file = 'microalgae_lca_results.xlsx' + + # 创建结果字典 + results = { + '基本信息': { + '功能单位': [lca.functional_unit], + '系统碳平衡': [lca.system_carbon_balance], + '主产品': [lca.main_product.ID], + }, + '主要影响类别': { + 'GWP (kg CO2e/功能单位)': [lca.GWP], + 'FEC (MJ/功能单位)': [lca.FEC], + 'WC (m³/功能单位)': [lca.WC], + }, + 'GWP分解': { + '物质影响 (kg CO2e/功能单位)': [lca.material_GWP], + '原料影响 (kg CO2e/功能单位)': [lca.feedstock_GWP], + '电力影响 (kg CO2e/功能单位)': [lca.net_electricity_GWP], + '直接排放(非生物源) (kg CO2e/功能单位)': [lca.direct_non_biogenic_emissions_GWP], + }, + '物质影响明细': lca.get_material_impact_breakdown('GWP_100'), + 'GWP分解百分比': get_gwp_breakdown_data(lca) + } + + # 创建Excel文件 + with pd.ExcelWriter(output_file, engine='openpyxl') as writer: + # 基本信息 + pd.DataFrame(results['基本信息']).to_excel(writer, sheet_name='基本信息', index=False) + + # 主要影响类别 + pd.DataFrame(results['主要影响类别']).to_excel(writer, sheet_name='主要影响类别', index=False) + + # GWP分解 + pd.DataFrame(results['GWP分解']).to_excel(writer, sheet_name='GWP分解', index=False) + + # 物质影响明细 + material_df = pd.DataFrame(list(results['物质影响明细'].items()), + columns=['物质', '影响 (kg CO2e/功能单位)']) + material_df.to_excel(writer, sheet_name='物质影响明细', index=False) + + # GWP分解百分比 + gwp_percent_df = pd.DataFrame(list(results['GWP分解百分比'].items()), + columns=['组分', '百分比']) + gwp_percent_df['百分比'] = gwp_percent_df['百分比'] * 100 + gwp_percent_df.to_excel(writer, sheet_name='GWP分解百分比', index=False) + + print(f"LCA结果已导出到: {output_file}") + return output_file + +def compare_lca_scenarios(scenarios, output_file=None): + """ + 比较多个LCA场景 + + 参数 + ---------- + scenarios : dict + 场景字典,格式: {scenario_name: lca_object} + output_file : str, 可选 + 输出文件路径 + """ + if output_file is None: + output_file = 'microalgae_lca_comparison.xlsx' + + # 创建比较数据 + comparison_data = {} + + for scenario_name, lca in scenarios.items(): + comparison_data[scenario_name] = { + 'GWP (kg CO2e/功能单位)': lca.GWP, + 'FEC (MJ/功能单位)': lca.FEC, + 'WC (m³/功能单位)': lca.WC, + '物质影响 (kg CO2e/功能单位)': lca.material_GWP, + '原料影响 (kg CO2e/功能单位)': lca.feedstock_GWP, + '电力影响 (kg CO2e/功能单位)': lca.net_electricity_GWP, + '直接排放(非生物源) (kg CO2e/功能单位)': lca.direct_non_biogenic_emissions_GWP, + } + + # 创建DataFrame并保存 + df = pd.DataFrame(comparison_data).T + df.to_excel(output_file) + + print(f"场景比较结果已保存到: {output_file}") + return df + +def generate_lca_report(lca, output_file=None): + """ + 生成详细的LCA报告 + + 参数 + ---------- + lca : LCA + LCA对象 + output_file : str, 可选 + 输出文件路径 + """ + if output_file is None: + output_file = 'microalgae_lca_report.txt' + + report = lca.generate_report(output_file) + + if output_file: + print(f"详细报告已保存到: {output_file}") + + return report + +# 主程序 +if __name__ == "__main__": + import biosteam as bst + + # 初始化系统 + bst.settings.set_thermo(chems) + microalgae_sys = system.create_microalgae_MCCA_production_sys() + microalgae_sys.simulate() + + # 获取主产品流和主产品化学品ID + main_product = microalgae_sys.flowsheet.stream.caproic_acid_product + main_product_chemical_IDs = ['CaproicAcid'] + + # 尝试找到锅炉涡轮发电机 + boiler = None + for unit in microalgae_sys.units: + # 优先查找锅炉涡轮发电机 + if 'BT' in unit.ID and hasattr(unit, 'turbogenerator_efficiency'): + boiler = unit + break + + # 如果没有找到锅炉涡轮发电机,查找普通锅炉 + if boiler is None: + for unit in microalgae_sys.units: + if hasattr(unit, 'heat_utilities') and unit.heat_utilities: + boiler = unit + break + + # 如果没有找到锅炉,使用第一个热交换器 + if boiler is None: + for unit in microalgae_sys.units: + if unit.__class__.__name__ == 'HXutility': + boiler = unit + break + + # 创建LCA对象 + lca = create_microalgae_lca_scenario( + system=microalgae_sys, + main_product=main_product, + main_product_chemical_IDs=main_product_chemical_IDs, + boiler=boiler, + add_EOL_GWP=True + ) + + # 打印结果 + print_microalgae_lca_results(lca) + + # 导出GWP分解数据 + export_gwp_breakdown_data(lca) + + # 导出完整结果到Excel + export_lca_results_to_excel(lca) + + # 生成详细报告 + generate_lca_report(lca) \ No newline at end of file diff --git a/biorefineries/microalgae/parameter_distributions.xlsx b/biorefineries/microalgae/parameter_distributions.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..35644b34bbb0b9f9b42f9addd466961917ecad0f GIT binary patch literal 12430 zcmeHtWmFv7)@|eN4KxxgSa5d=9;2#jbgenM_Fi+Zxz}3Caxkz!06YK@0058!oVj%KJfHx8KsW#Z2Y?9u zO3cpI8EorppzdxDcG72Zv#}=2f`z8d06;%I|KH<(@d%Wt4=O^lVzwk-qbI*cdv*LK z!f?gcLmm4V%90;~woto+ifDIaEnTL4O(2-hyqb3Y5o z!7*hoZi8*M1~qmjyd)@rDsTYH#}kafhE9W!-J{t3I?kkB%Va-%YlqZg9ZlZ|qaO4< zmbo|_-4XW@(st90BwJ25>GA)-9bIJ)lyE0rpC4Zbeu}Jokh;Qp@FD=e_Rzu`y&3pQ zb+~$6Ak7S39WYUol^9K7wpn2hBXz{Yd$MRxsLP}4bfEmXvq{!PhTl5$LEL<0TzRRywg(iPViWXkm|J(nCi5wE};X3`==!pS8L z#uH5R)9qH!Sv0yXgPIUkJ17l?)NyC*#ttX@OvUHzpb3Za%I}XR;`TZjnaz}W^;(l( z{2L~6#?(=_7TCn&Oz5bIV9r$a{yPMPC(mDd?GZBDIu1l@#n2T1pdc!hov2Rtag=*v`+9mF$(dLTeY~8dmq6NE7 zDO;*USmZ&+)=|pYv)l9gJcbAwI?A;P&VU2}3m21A`6inn6{lM6!;n(xCF*D1!RoPSQWbDbY@w^RYB5`i;^! zOJwrQ#ogTJ)`y3?w~?#9OG*qEHh|@*Mxv1L7>hwSZiEp9E&+5SpFq8NYW`sJoQmFTtpJB{_8} z0rW7pKGgnnQsV+O#AG7ix|X(vYWT%A{zyB|@%u!ti>p(sYV9{a_DlE=OwcRlw#L;x zFD!YfjuRSZ)=ZiyCWtd$mhD6qVcpjVsk5exH`=vFpswr*cDe^AbGW4E=17+ZW;S)( z2ilnBFm6lgOHZ7W3BjL3tPo&4*OOf415iZ@6U6Ga5^%U3jqEU`9JY5*cTm1Q3V&$J zgTFg89bs^y7?+Pa$2!b@p&uyjpQ9V%dt_Ca$&GqIqU?;5lE6eVV+y*|k5U^wgXc-= ze0?ga+0^Q`M$S>4RQ^JRQ@xyN-gJMDYi8(>8vme^OUZL$OX>NutAnXWup8PU{<-wp z`lWP7U&z4BMszLy$^HWq?<7w>YWA|*?HfL{hlH+H*H_w)gv!M-^|nNbx~cL*5C2Hi z7=*Ze*r#8FpU6rGK!kcC>c0ZoU+Mbq;0E=iL!X}g-+h$C3|Mxuph{c?Jp_!jIIwvu z2RoRKs3zDXzw@<^N)c;&r*WTnd2_chA4l2y9>iX^OQ&d0U%nv#^z4A(>k*}udE zY-|O0)HN{pJoKx%oOLP{91Ps_)|6J0mSyx>{+lGB&;p|X=#F{2T+~_R5ON2>8J@zF zqbTDYtC?&eBL&CUJdDv{PBP&!V2G-pa9kZ(c;?5UN2{hHOAbN)`;q7k6g;-$@7-|X zel66R$FFFjvDU%2gGraf!=kab+WjYk@t{ghf|e9vc(G2LoOIlP9x~1u!Z^Korc8_hnRnuF-PgDP&i@lT-^qAYzLjQvX002Eb@yBBC zWDW*9J2C&du>M^2Q)2b)=E#7-M>Ge*WQWEPsp65i`AM1@#UAW73oZ)B4iV7ZFew-c zw@3Wb3aT%XfD@Ih@Iz;5Fy)`G5VHHwQ?zOsUnr@{W_+quJ;=)ueoaE-8h1@LD{DR{ zD)Vu9=YDrBWMi?h)D8eGBDyCx8~7bJ!hOaUOQ_;mHILdWGx4uVb%rfYWJTn?=JB~Z zDFm6ieAvjYC>@hEF~@Jfb{S~TJN@vW&aNVN@4`&i8F`-1*B()iCZS_l1I7waqvA zt5C0%XKqzjtJvwAzMi_aZ<=Y(GijX{(e0$4UnDIj_fR);`TIV3gm9W z@Q1xbW-NI@+xh;@K4UZ@C9MJQ1ON#hs*!Xnt2&1j9Q54K^A7(|AX~vRFZfcqn{Q^@ zCrbgc``NJkR$mU%&q)xJeBS2x>z?y@RoyaYa4Ec8vB0(npby`@2E5KvP!a4yfK(H{ zn8wateGKNp31KzR2+GTGPcw047fLn@jc&%6ydyMoliw!}>tUDdRyDw9z!p+?#Oa0K z@cHEfzfN&U3$}ASm_Y_q$6#N&EXo1RWso+FCgs(Oh;d1eF{T)@rrS%o{>|YOcGYY> zpo}+9$}tgoNQd>Jfl~g*{Iwp3CimO>;la6%b_UzUN1+Fgx}^o8+m6;ezLxpN>!Y3X z2gjR>6G(5BW5cxV>y&Hee3I^IjKzb;ceZDP3tKKuSB@I{+J-r+(YQhC0u@EqTG<`-SVfk=Ezu)tXi2yqw?LtKz7{ z<)XSBr$cDs=TAz#%}}TQ}6=3Zf}?S7MZh! zSF81gS9iw0;wZ_*V=77b3w#QD<9X2Bd7{_1AGcL}&plmQ%0`c^pfEWjizyulE_^#UGEUCE>4!i*%^Q?4TE&$wo}jy~35tB_qp8 zut=3VPMT()3hT*Qjz>;#)Jeo94$`W#x~3ue>$^WA(&4MTNKZ)t?*tk6J3)_5@Nw>@?O z+Yb!nw+BG9tq|twBo2NcROPEcg>ua*5v-{5xKUuV_*cGIPVGfnEEYp*Nr1YYx4m*$ zG6)LHI@69UX#`7~vO3#`m@w7FHqW9Xnl}G}@PjWiN99i-3wrwD@SX7533=6*5zYlM zz7Wrbcx4C!295$%ps?GV5&_bPiuwdy>FNpcvxv@ZWl*<7)Cg<&9w)J0<-3BN3EyNE z&SoN7LJwYLj&ql&+7|iX&-t1sQ_4IM)ev!>5~oD+Wz~_OuvZq7y0ATZ4@*}XYihH zshy4f)?6&E?w4_|=TT*0>G#`R#CS$*QpN*lUOHJq3AK2}uD?PJW+Y_(G-D?V*`wVxc&{l=n)FEAr#lfrXUtS|%${br$*e@Zb-p z)w~+hLvp+odZj-^X&B9|AF9_iPbbq*$ixLb*ds2cu7$5B#!^;I(?2^4sKqjBL;T1p zhOLSS4tL|KqjAwXzh;|;Tvw)sU+3A-c_b*CuGkPW@=)&unnCU4qg*oW-=9$q&1D~Q zof>J5^Qe6(lj1WoeZ6NL)sc+-Np&8vs36V=E#FWgz-DB5meuiM~$U+4>#y}T(iYn=rHAAZl%3-^yz&9XkbpehIS@N{$^%0jjh66TiO{P-( zQCkHzHA?&iN#F!W6nM$+RIYSx!nw$;VzE2ad;H8;XlkEtu6X=mHZ_tK>frwjqV$7g zx4r`NeY52!r3``-D{DVBH!foxf7p4`vt16ct!3`aiN>v<3E1fIx2kczbNp2ViEqp( z^&4-kwqTI!3YRMlMXPXj2#N5J`2|a5U=2<}O@|1wp5fmlR-R$KW6%qsU|JUGxfoA> zwb1%n!ZQd9m*1MRDf5PYA30#hLjNLhuqRp3Tll!+u)7snxgwH?17U@1{#p&dk@!{G ziK%foW9slC0J`{T{NEqFt6C}`a2xb7e(%H`B@9#kvcbm(MVMF;Q{w46uMng&N9Uv0 zudnm%+|9O2c7$$l;V)#A>b}3miNZY&q-1_eR^rNuB7D=}*{7To=u))(#xC>+H?qY!b{grKSH=ma{_P5lz>-mNr zY*12=Fo~I+8EZHeD}yp>3u74qCeJfB&!7UdsSZ;_Qj-j3uI0AV=(Y>Wp?H67j}}}S z_~QiDS>r@bK%I$!ZlKhFv+a?=kN zd3uFf1Y@?RbST!8F;sWKRAQQOe@YwB$+oH?w#}_3 zX9?P-BL6<23>zmA$MlAe_vzk04}ny7>|oam z&eGQPPKa-DF!aKP?@PvFll`N};9)k{1HZ$nQqashW?w1h;NghMbE?VfLR8<9z1O8d zNUQk5WXs4~l2#ClKKx_7Zn;xJbe@>GIuTM$^1SsT9;+VSD;OB=;iyUnHtH5i3ZbwO zXuJ!~nv0aP_KOsLly1Y)@}voBkil`O3+ljng2I`gWhwPlc7vNeWxosabalsr==wH9 z=dg+Ea6-|g?6SuSdi2sp+B`Lpk`NDj@a=PxmA91Ry=8$CbM0AF3*~AxL#x!lmo~Au z+HAa#@4NMW=|}g5L_xG^{Q?_3fW7_xbv=tbSGd?+T)0HaZ6U#jW9FyBpaorhq3 zb)SZ174`$$j!(5hfpnJPHIu*2AOmfEVb&%zEfZwhR&IOfJm+z z3Rsfw5QlmDMny1`?lnC~{Vw=OLsz6uWd~j)n+wtMb*q*SGS2(ZSluWebAom(pgwN; zEA`t+n$ITQ{Vq1`F?4F_>P6x8H|wg!M-S_(n3$sds~7iCNPweat*EGi=WqPI>} zI-%Q(-3}g6{wZj|_KZ(@KBZ^|kN^O*e+4ZkXLoC`)6aeN7tO&Bb6l7$_}l!LU#u>S z2Wrq{M83R+f>O=+YU}sfBCIm(Sc+_?G$pI<^(m#`1kGFyIi(0q%7JfE>%ym=6QLr`l3)7f7scdPmt$iRO%gUaWz{} zh1MHxb%hSP8_*$zUSJO2E9imYDqy~0oIq&v*l;xm8fUZF2O&L^$e76t*@N6c2jrLf z$nkbLH#+f$I4oY{bZlpM3pnU8pb!bo7iAokFQ1tV$x&iePhB7PG-3EWY*Z6cfP~LS zJCOutq&VBiE)+q0Ai)HRIiAw;HB0*{dom#epVjO|ESn7fTXFfk&q2=aJX3*Hk)}#{ zha&MAFb8r&wUr@8myFdWxjI&YIn1fTAG-kHh%>c&$};tr$|m2<<1KaC;h#;phTtJf zw99W3TzRCAELu8-#yUHboug+;$CZ&UXF8^Cy;e6#;lkSE7odFQLg^ zsS#tZ^|mAIL?%I>CC|~?mBloe&a5);S*er2SzyRe*GPxN4axfu$7sHnB$x&{O9)ra z);(l?Md6O`_WW-Pc{c_*U8l$z@N*&MWG@po%kV>}qMY?Ed3U*)T#c=o529!Ks=j5F z@y&0)mpQ|0<=UF=%{E|)ca zihkU)R`F{Fwwgt zQTzJT3)X4l&^j}n4^qh_Eu_2v&j|16Z&{x$h~YLU!vl6Az6JcSj)4ag((KJRzm1LG zLG--&cFT*rR^Phw?AqBPF(akVuh=iM0x0c&K;w^yK;3?3pnbypOZEO<8kC%D1e!dt zloA~P0RF*Jb3;e4iJG&cg{|2yvX*Gg+2XKbdP}X6mf#9-CF2#6#pTGtK#9M=QV4Pj z&wFiV+-1dlX>D;0BNS{#`8w)4@d19#nyHk#w9_TGNTK^Oacymlpw7wp(+iguC9#sO z+4q|KMxsu!(y1=lXD9YY5}m~UiZ2Nm8%IRzw+pVWReE?$sx_NzImEshbXTmgNBdbN z>X{QJ5+*~9Y9)6w)j?x(<3O|Hl2*@!IKZn)MI?(p-&pV=sSq6~4+6zScWkMYubGqw zG-GM^!XgiF+#hsK?{&V*A@(j6r{)M=xl*Mu+}D$HHarHUD64AO51U45`O|DBL&)0^ z64xa)U!-igzdfa4#f=Bz;Zo4RV>5Fe&JR~W3!44V4Ug)$ek+VBV+QooQ0IxHSCawx z=#saqhjwPJoJ}UYor$zt>7+RkKr{aI#h6|sKgd7am-cINH+0M%H4+1HZWA(8h!xi& zZLcG}`-WFq=MwCpMqdgv9v)DxZJ{cuq%-;r71QA$>!SclU*s3oAr(5^IbGGcnjoCS zFwZFF-f*1CkSlfz!*h6x>&*%Ak=3Tmx?TI;HoFzY`*x_tfQF(QleF$nHnxH+Dc?1g z+!bN6wJs>-&pA+Y9NrM^9o^DNrr!^TzH2zz$BtvhQJLSf&yD~~cLnc`uo;C&8uNky zvahvwwiL)?EmeXsyvSbr8y+2%$4ED$P6_+yJw8;iw^c%ivl&y@RB%_NsmH{~PiwJF zY#?iWe`Y`5Pki6_IPI)2>v>D0$W6D~o0PNMtJl^r9;2qhhNs#VMGPWY(c#4l{ursQ z4(W21oeJyM?}~(};>Pif;6oX3)tKNeTFy&n1nP%>z5EfOizO>mX)TNJ$h0GgfYl+P z`0l&uUTf~noT38`>z5!-@>{y}wr~@%sONO&ukrMin`hM}Hj7tP*WnYmo}1|`Ta6MB zR|WxhGeNXFI>(qgY)J6x$Y?tIZeG)ib~}}G@&$A9wpad1mflz|%S$s31YwH_5WLpU zSvZQwjp(aQ3sP|{b(QJOyqRKy5zANwP1qT3bxf-n*e+e7wVkM9`}Z;qZ4997?2}hK zpGrntl$$qgx=sOgBz2suT;-EVd(DJ{+q&_Q@4>?<)}EW2NIJ1(xY=fmfmuR|@{b!A z|6JnWw_%fOdt#&L({}awzgYRRxba7N`d`(KKa>zt|hfK(VIv^gUa=FZah<2}*$ zk|Il+Qv-x*X2qH#hIMbP06sxgornDo-lPJy*{ws;r^A6Vyisq2vZQ%?B@Rw~jYRfb zFVn2N(P8s!z73pRKIw!1OwgC1+=n$iy*&di0PvKf|Cyk-e_D{7!H#NRXXl?Q)9?4J zOhy*A`*DM^Ys$c$M~l0HAtMGvTT@n`mJxy6T92aK7~iMvz&u>;Dq1el3~DNcXaWL} zO~y`y#0>`h8$gu{m5?vd{n9kW5b(vSBoB9rr_f=;*5*S!F^Uj6Cb{@W$52rE%J(ZQ zN|$d1;RywA#=lda7q;4OUN94Kd|SQ?h_)TO3B6Yj_Ylr?VrmC9+`bjDfhg<%C3ZYN z92DK|gQ^N zCZtX4b>@&06GaHyr1BdEDb|W1=AIPjHLx!IIogf;E_O4tCe9HJ{p3KIPH`r|b@Y^4 z{MfEX^xosKGRGUK7W~fAX$%-1mu3}aO`-zNX2_V55ywGU%LD{)K)ighH&vFVhCf9;9b(;4CvE;CU zzL=QMqAw8Dr8D8c_b0KPQ<;=rcR~e(LqI{hBc!A;Kl?KCZ!CJe&lRWkeno$tSV{PfHh^XP{>5!0SeI={TkI6PU zH$M5xX-s6uqt8}Q7QQIQsb2CfqD!ohs_EohyRt^&Au|b=W07a+ zut#y;^9!w&ZbLA+&j?2+;|YeAjgmB8Iy?C|4zh<%Dz(q@Y{FR&tY{w(XM3&= z>j-W|8X}LGSs|UhRECmy90}ja2(es!s|T7NnMdvZk==ay=U7QkK8g8@-GA{(Lwo!G zy5!T4{c)tkwAs%6?8<_>K@UEROtGa6eyORFL+>#S0myi0f%)+0Q?sM=S)GznUG@jE#SY%Ng0Mlo4kIw*wR-(r3!TJeKtN@c&V z082DRXhQ-go>Bv(b5Sh)K0faR@9s|?x9r+vNG-j2Y@hXPW~W@D0g1U>;9(GuLIUXn z@J8z_Gbvi21fIcMp~4*cR+T{6yE%QEQj%wGK2E!(rmcH!Nb1dc$W+?VZ$KBAZ$pCD zRjG+3%lGx&+^v6oo`mFF;y0@eE@h#MFL;e}OQ@uJio?`DLQ;nKijW@eK5hI|WB*bs zP|%D|JEni`ivIWf{rmb4J<`f@e+T$`kLJG(e_pGeNb;AC&EE}w?_2xRbo!}e@JD0A zzq{CeH~xDs&7YR+ILlUcu8|6NS{X$t^Ao>cmONQ>Xi|Gw`3)m)tJ bFXn$l0cAP3ClCIaJwyZi%uxUtf8PBMgubsk literal 0 HcmV?d00001 diff --git a/biorefineries/microalgae/run_analysis.py b/biorefineries/microalgae/run_analysis.py new file mode 100644 index 00000000..12dfc8b7 --- /dev/null +++ b/biorefineries/microalgae/run_analysis.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Microalgae biorefinery analysis runner + +This script runs uncertainty and TRY analysis for the microalgae biorefinery. + +@author: Xingdong Shi +@version: 0.0.1 +""" + +import sys +import os +from datetime import datetime + +def run_uncertainty_analysis(): + """Run uncertainty analysis.""" + print("=" * 60) + print("MICROALGAE BIOREFINERY UNCERTAINTY ANALYSIS") + print("=" * 60) + + try: + from . import uncertainties + + print("Starting uncertainty analysis...") + results_dict, results = uncertainties.run_uncertainty_analysis() + + print("\nSaving results...") + uncertainties.save_results(results_dict, results) + + print("Uncertainty analysis completed successfully!") + return True + + except Exception as e: + print(f"Error in uncertainty analysis: {str(e)}") + import traceback + traceback.print_exc() + return False + +def run_try_analysis(): + """Run TRY analysis.""" + print("\n" + "=" * 60) + print("MICROALGAE BIOREFINERY TRY ANALYSIS") + print("=" * 60) + + try: + from . import TRY_analysis + + print("Starting TRY analysis...") + results_df = TRY_analysis.run_try_analysis() + + if results_df is not None: + print("\nAnalyzing results...") + analysis = TRY_analysis.analyze_try_results(results_df) + + print("Saving results...") + TRY_analysis.save_try_results(results_df, analysis) + + print("Running single parameter analysis...") + single_param_df = TRY_analysis.run_single_parameter_analysis() + + if single_param_df is not None: + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + single_param_filename = f'microalgae_single_param_{timestamp}.xlsx' + single_param_df.to_excel(single_param_filename, index=False) + print(f'Single parameter results saved to {single_param_filename}') + + print("TRY analysis completed successfully!") + return True + else: + print("TRY analysis failed to generate results.") + return False + + except Exception as e: + print(f"Error in TRY analysis: {str(e)}") + import traceback + traceback.print_exc() + return False + +def run_quick_baseline(): + """Run a quick baseline analysis to verify system functionality.""" + print("\n" + "=" * 60) + print("MICROALGAE BIOREFINERY BASELINE CHECK") + print("=" * 60) + + try: + from . import system as microalgae_system + from . import lca + + print("Loading system...") + microalgae_sys = microalgae_system.microalgae_mcca_sys + microalgae_tea = microalgae_system.microalgae_tea + + print("Creating LCA object...") + from . import analysis_utils + microalgae_lca = analysis_utils.create_microalgae_lca_simple(microalgae_sys, microalgae_tea) + + print("Running baseline simulation...") + microalgae_sys.simulate() + + print("Calculating metrics...") + from . import analysis_utils + main_product = analysis_utils.get_main_product_stream(microalgae_sys) + mpsp = microalgae_tea.solve_price(main_product) if main_product else float('nan') + gwp = microalgae_lca.GWP + fec = microalgae_lca.FEC + + print(f"\nBaseline Results:") + print(f"MPSP: {mpsp:.3f} USD/kg") + print(f"GWP100a: {gwp:.3f} kg CO2-eq/kg") + print(f"FEC: {fec:.3f} MJ/kg") + + # Save baseline results + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + baseline_filename = f'microalgae_baseline_{timestamp}.txt' + + with open(baseline_filename, 'w') as f: + f.write("Microalgae Biorefinery Baseline Results\n") + f.write("=" * 40 + "\n\n") + f.write(f"Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n") + f.write(f"MPSP: {mpsp:.6f} USD/kg\n") + f.write(f"GWP100a: {gwp:.6f} kg CO2-eq/kg\n") + f.write(f"FEC: {fec:.6f} MJ/kg\n\n") + + # System information + f.write("System Information:\n") + f.write(f"Number of units: {len(microalgae_sys.units)}\n") + f.write(f"Number of streams: {len(microalgae_sys.streams)}\n") + f.write(f"Number of feeds: {len(microalgae_sys.feeds)}\n") + f.write(f"Number of products: {len(microalgae_sys.products)}\n") + + print(f"Baseline results saved to {baseline_filename}") + print("Baseline check completed successfully!") + return True + + except Exception as e: + print(f"Error in baseline check: {str(e)}") + import traceback + traceback.print_exc() + return False + +def main(): + """Main function to run all analyses.""" + print("MICROALGAE BIOREFINERY COMPREHENSIVE ANALYSIS") + print("=" * 60) + print(f"Start time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + + # Run baseline check first + baseline_success = run_quick_baseline() + + if not baseline_success: + print("\nBaseline check failed. Stopping analysis.") + return + + # Ask user which analysis to run + print("\nWhich analysis would you like to run?") + print("1. Uncertainty analysis only") + print("2. TRY analysis only") + print("3. Both analyses") + print("4. Exit") + + choice = input("\nEnter your choice (1-4): ").strip() + + if choice == '1': + run_uncertainty_analysis() + elif choice == '2': + run_try_analysis() + elif choice == '3': + uncertainty_success = run_uncertainty_analysis() + if uncertainty_success: + run_try_analysis() + elif choice == '4': + print("Exiting...") + return + else: + print("Invalid choice. Running both analyses by default...") + uncertainty_success = run_uncertainty_analysis() + if uncertainty_success: + run_try_analysis() + + print(f"\nEnd time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print("Analysis completed!") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/biorefineries/microalgae/sesitivity.py b/biorefineries/microalgae/sesitivity.py new file mode 100644 index 00000000..f9b1cf1b --- /dev/null +++ b/biorefineries/microalgae/sesitivity.py @@ -0,0 +1,776 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +BioSTEAM Sensitivity Analysis for Microalgae MCCA Biorefinery +""" + +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +import biosteam as bst +from biosteam.evaluation import Model, Metric +from chaospy import distributions as shape +import os +from matplotlib.colors import LinearSegmentedColormap +import seaborn as sns +from scipy.stats import spearmanr +import warnings +from microalgae.system import compute_labor_cost +from microalgae._chemicals import chems +from biosteam import main_flowsheet +from microalgae import system +from microalgae.system import create_microalgae_MCCA_production_sys +from microalgae.tea import create_tea + +# Suppress hot utility load warnings +warnings.filterwarnings("ignore", message="Hot utility load is negative.*", category=RuntimeWarning) + +# Set up plotting configuration +def setup_plotting_config(): + """Configure plotting environment""" + plt.rcParams['font.family'] = 'sans-serif' + plt.rcParams['font.sans-serif'] = ['Arial', 'DejaVu Sans'] + plt.rcParams['axes.unicode_minus'] = False + +# Initialize system +def initialize_system(): + """Initialize the biorefinery system and TEA""" + # Set up thermodynamics + bst.settings.set_thermo(chems) + main_flowsheet.clear() + + # Create system + from microalgae.system import create_microalgae_MCCA_production_sys + system = create_microalgae_MCCA_production_sys() + system.simulate() + + # Create TEA object + from microalgae.tea import create_tea + u = system.flowsheet.unit + dry_tpd = u.U101.ins[0].F_mass * 24 / 1000 # kg/h -> t/d + tea = create_tea( + system=system, + IRR=0.10, + duration=(2024, 2045), + depreciation='MACRS7', + income_tax=0.21, + operating_days=330, + lang_factor=None, + construction_schedule=(0.08, 0.60, 0.32), + startup_months=3, + startup_FOCfrac=1, + startup_salesfrac=0.5, + startup_VOCfrac=0.75, + WC_over_FCI=0.05, + finance_interest=0.08, + finance_years=10, + finance_fraction=0.4, + OSBL_units=(u.CT, u.CWP, u.ADP, u.PWC), + warehouse=0.04, + site_development=0.09, + additional_piping=0.045, + proratable_costs=0.10, + field_expenses=0.10, + construction=0.20, + contingency=0.10, + other_indirect_costs=0.10, + labor_cost=max(0.5e6, compute_labor_cost(dry_tpd)), + labor_burden=0.90, + property_insurance=0.007, + maintenance=0.03, + boiler_turbogenerator=None, + steam_power_depreciation='MACRS20' + ) + + return system, tea + +# Create evaluation model +def create_evaluation_model(microalgae_sys, microalgae_tea): + from biosteam.evaluation import Model, Metric + import chaospy as cp + + u = microalgae_sys.flowsheet.unit # 修正:使用flowsheet.unit而不是unit + s = microalgae_sys.flowsheet.stream # 修正:使用flowsheet.stream而不是stream + tea = microalgae_tea + + # 首先定义所有setter函数 + def set_microalgae_price(price): + u.U101.ins[0].price = price + + def set_caproic_acid_price(price): + s.caproic_acid_product.price = price + + def set_caproic_acid_yield_factor(factor): + u.R301.caproic_acid_yield_factor = factor + + def set_operating_days(days): + tea.operating_days = days + + def set_maintenance_factor(factor): + tea.maintenance = factor + + # 创建模型 + model = Model(microalgae_sys) + + # 定义指标 + model.metrics = [ + Metric('ROI', lambda: tea.ROI * 100, '%'), + Metric('MSP', lambda: tea.solve_price(s.caproic_acid_product), '$/kg'), + Metric('TCI', lambda: tea.TCI, '$'), + Metric('VOC', lambda: tea.VOC, '$'), + Metric('FOC', lambda: tea.FOC, '$') + ] + + # 之后再注册参数 - 修改为使用命名参数 + model.parameter(setter=set_microalgae_price, element=u.U101.ins[0], + name='Microalgae Price', kind='isolated', + units='$/kg', distribution=shape.Triangle(-0.2, -0.1, 0.1)) + + model.parameter(setter=set_caproic_acid_price, element=s.caproic_acid_product, + name='Caproic Acid Price', kind='isolated', + units='$/kg', distribution=shape.Triangle(2.0, 2.89, 4.0)) + + model.parameter(setter=set_caproic_acid_yield_factor, element=u.R301, + name='Caproic Acid Yield Factor', kind='isolated', + distribution=shape.Triangle(0.7, 1.0, 1.3)) + + model.parameter(setter=set_operating_days, element=tea, + name='Operating Days', kind='isolated', + distribution=shape.Triangle(300, 330, 350)) + + model.parameter(setter=set_maintenance_factor, element=tea, + name='Maintenance Factor', kind='isolated', + distribution=shape.Triangle(0.02, 0.03, 0.05)) + + # Print registered parameters to debug + print("Registered parameters:") + for i, param in enumerate(model.parameters): + print(f"{i+1}. {param.name} (index: {param.index})") + + return model + +# Univariate sensitivity analysis +def run_univariate_analysis(model, parameter_name, values=None, metrics=None): + """Run univariate sensitivity analysis""" + # Find parameter + parameter = None + for param in model.parameters: + if param.name == parameter_name: + parameter = param + break + + if parameter is None: + raise ValueError(f"Parameter '{parameter_name}' not found") + + # Generate value range + if values is None: + dist = parameter.distribution + if hasattr(dist, 'lower') and hasattr(dist, 'upper'): + values = np.linspace(dist.lower, dist.upper, 11) + else: + raise ValueError(f"Parameter '{parameter_name}' has no defined distribution range") + + print(f"Running univariate sensitivity analysis: {parameter_name}") + print(f"Parameter range: {min(values)} - {max(values)}") + + # Create sample points + baseline = model.get_baseline_sample() + results = [] + + # Evaluate each value + for value in values: + sample = baseline.copy() + sample[parameter.index] = value + # Directly call model for results + result = model(sample) + result[parameter.name] = value + results.append(result) + + # Combine results + df = pd.DataFrame(results) + + return df + +# Bivariate sensitivity analysis +def run_bivariate_analysis(model, param_name1, param_name2, metric_name='ROI', + values1=None, values2=None, save=True): + """Run bivariate sensitivity analysis""" + # Find matching parameters + param1 = param2 = None + for param in model.parameters: + if param.name == param_name1: + param1 = param + elif param.name == param_name2: + param2 = param + if param1 and param2: + break + + if not param1: + raise ValueError(f"Parameter '{param_name1}' not found") + if not param2: + raise ValueError(f"Parameter '{param_name2}' not found") + + # Set default value ranges + if values1 is None: + if hasattr(param1, 'distribution'): + dist = param1.distribution + if hasattr(dist, 'lower') and hasattr(dist, 'upper'): + values1 = np.linspace(dist.lower, dist.upper, 6) + + if values2 is None: + if hasattr(param2, 'distribution'): + dist = param2.distribution + if hasattr(dist, 'lower') and hasattr(dist, 'upper'): + values2 = np.linspace(dist.lower, dist.upper, 5) + + # Ensure values1 and values2 are 1D arrays + values1 = np.ravel(values1) + values2 = np.ravel(values2) + + print(f"Running bivariate sensitivity analysis: {param_name1} vs {param_name2}") + print(f"{param_name1} range: {min(values1)} - {max(values1)}") + print(f"{param_name2} range: {min(values2)} - {max(values2)}") + + # Get baseline sample + baseline = model.get_baseline_sample() + + # Create result grid + grid_data = np.zeros((len(values1), len(values2))) + + # Find correct metric index + metric_index = None + for i, metric in enumerate(model.metrics): + if metric.name == metric_name: + metric_index = i + break + + if metric_index is None: + raise ValueError(f"Metric '{metric_name}' not found. Available metrics: {[m.name for m in model.metrics]}") + + # Evaluate each parameter combination + for i, val1 in enumerate(values1): + for j, val2 in enumerate(values2): + sample = baseline.copy() + sample[param1.index] = val1 + sample[param2.index] = val2 + + # Directly evaluate model to get series result + result_series = model(sample) + # Use iloc instead of direct indexing + grid_data[i, j] = result_series.iloc[metric_index] if metric_index is not None else result_series[metric_name] + + # Create DataFrame for saving and plotting + result_df = pd.DataFrame(grid_data, index=values1, columns=values2) + + # Save results + if save: + os.makedirs('results', exist_ok=True) + safe_name1 = 'param1_' + str(hash(param_name1) % 10000) + safe_name2 = 'param2_' + str(hash(param_name2) % 10000) + safe_metric = 'metric_' + str(hash(metric_name) % 10000) + result_df.to_csv(f'results/bivariate_{safe_name1}_{safe_name2}_{safe_metric}.csv') + + # Create colormap + if metric_name == 'ROI': + cmap = 'RdYlGn' + center = 0 + else: + cmap = 'viridis' + center = None + + # Plot heatmap + plt.figure(figsize=(10, 8)) + ax = sns.heatmap(result_df, annot=True, cmap=cmap, center=center, + fmt=".2f", # Two decimal places + annot_kws={"size": 9}, # Adjust annotation size + cbar_kws={'label': f'{metric_name}'}) + + ax.set_title(f'Two-Variable Sensitivity Analysis: {metric_name}') + ax.set_xlabel(param_name2) + ax.set_ylabel(param_name1) + plt.tight_layout() + + # Save figure + if save: + os.makedirs('figures', exist_ok=True) + plt.savefig(f'figures/heatmap_{safe_name1}_{safe_name2}_{safe_metric}.png', dpi=300, bbox_inches='tight') + + return result_df + +# Monte Carlo analysis +def run_monte_carlo(model, n_samples=1000, sample_type='LHS', save=True): + """Run Monte Carlo uncertainty analysis using manual sample generation""" + print(f"Running Monte Carlo simulation ({n_samples} samples)...") + + # Get parameter distributions + param_distributions = {} + for param in model.parameters: + if hasattr(param, 'distribution'): + param_distributions[param.index] = param.distribution + + # Get metric name mapping + metric_names = {} + for i, metric in enumerate(model.metrics): + metric_names[i] = metric.name # Store position and metric name mapping + + # Get baseline sample as template + baseline = model.get_baseline_sample() + results = [] + + # Generate samples and evaluate each one + for i in range(n_samples): + if i % 100 == 0 and i > 0: + print(f"Completed {i}/{n_samples} samples") + + # Copy baseline sample + sample = baseline.copy() + + # Generate random values for each parameter + for param_name, distribution in param_distributions.items(): + if sample_type == 'LHS': + # Simplified Latin Hypercube Sampling + u = (i + 0.5) / n_samples # Ensure uniform coverage + if hasattr(distribution, 'ppf'): + sample[param_name] = distribution.ppf(u) + else: + # Fall back to regular random sampling + sample[param_name] = distribution.sample() + else: + # Regular random sampling + sample[param_name] = distribution.sample() + + # Evaluate sample + try: + # Get metric values + result_series = model(sample) + + # Create dictionary with parameter and metric values + result_dict = {} + + # Add parameter values + for param_name in param_distributions.keys(): + result_dict[param_name] = sample[param_name] + + # Add metric values using our defined names + for i, name in metric_names.items(): + result_dict[name] = result_series.iloc[i] + + results.append(result_dict) + except Exception as e: + print(f"Sample {i} evaluation failed: {str(e)}") + + # Combine results + df_results = pd.DataFrame(results) + + # Save results + if save: + os.makedirs('results', exist_ok=True) + df_results.to_csv('results/monte_carlo_results.csv', index=False) + + # Plot histograms - use correct column names + for metric_name in metric_names.values(): + if metric_name in df_results.columns: # Confirm column exists + plt.figure(figsize=(10, 6)) + + # Plot histogram and kernel density estimate + sns.histplot(df_results[metric_name], kde=True) + + # Add vertical lines for mean and median + mean_val = df_results[metric_name].mean() + median_val = df_results[metric_name].median() + plt.axvline(mean_val, color='r', linestyle='-', + label=f'Mean: {mean_val:.3f}') + plt.axvline(median_val, color='g', linestyle='--', + label=f'Median: {median_val:.3f}') + + plt.xlabel(metric_name) + plt.ylabel('Frequency') + plt.title(f'Monte Carlo Simulation: {metric_name} Distribution') + plt.legend() + + if save: + os.makedirs('figures', exist_ok=True) + plt.savefig(f'figures/monte_carlo_{metric_name}.png', dpi=300) + + return df_results + +# Run scenario analysis +def run_scenario_analysis(model, scenarios, save=True): + """Run predefined scenario analysis""" + print("Running scenario analysis...") + + # Get metric name mapping + metric_names = {} + for i, metric in enumerate(model.metrics): + metric_names[i] = metric.name + + # Create result containers + metrics = list(metric_names.values()) + results = {metric: [] for metric in metrics} + results['Scenario'] = [] + + # Get baseline sample as template + baseline = model.get_baseline_sample() + + for scenario_name, params in scenarios.items(): + results['Scenario'].append(scenario_name) + print(f"\nApplying scenario: {scenario_name}") + + # Display parameters + for param_name, value in params.items(): + print(f" {param_name} = {value}") + + # Create scenario sample + sample = baseline.copy() + + # Set scenario parameter values + for param in model.parameters: + if param.name in params: + sample[param.index] = params[param.name] + + # Directly evaluate model + try: + result_series = model(sample) + + # Add metric results + for i, metric_name in metric_names.items(): + value = result_series.iloc[i] + results[metric_name].append(value) + print(f" {metric_name}: {value}") + except Exception as e: + print(f"Scenario evaluation failed: {str(e)}") + # Add missing values + for metric_name in metric_names.values(): + results[metric_name].append(None) + + # Create result DataFrame + df_results = pd.DataFrame(results) + + # Save results + if save: + os.makedirs('results', exist_ok=True) + df_results.to_csv('results/scenario_analysis.csv', index=False) + + # Plot bar charts + for metric_name in metric_names.values(): + plt.figure(figsize=(10, 6)) + plt.bar(df_results['Scenario'], df_results[metric_name]) + plt.title(f'{metric_name} Across Scenarios') + plt.ylabel(f'{metric_name}') + plt.xticks(rotation=45) + plt.tight_layout() + + if save: + os.makedirs('figures', exist_ok=True) + plt.savefig(f'figures/scenario_{metric_name}.png', dpi=300, bbox_inches='tight') + + return df_results + +def plot_contour_from_heatmap(result_df, param_name1, param_name2, metric_name, + cmap='viridis', levels=15, save=True): + """Convert heatmap data to contour plot, similar to cane project style""" + fig, ax = plt.subplots(figsize=(10, 8)) + + # Get coordinate grid + x = result_df.columns.astype(float) + y = result_df.index.astype(float) + X, Y = np.meshgrid(x, y) + Z = result_df.values + + # Create custom colormap + if metric_name == 'ROI': + # ROI uses red-to-green gradient + cmap = LinearSegmentedColormap.from_list("ROI", ["#FF5555", "#FFFFFF", "#55FF55"]) + # Find zero point as center + vmin, vmax = Z.min(), Z.max() + if vmin < 0 < vmax: + vabs = max(abs(vmin), abs(vmax)) + norm = plt.Normalize(-vabs, vabs) + else: + norm = plt.Normalize(vmin, vmax) + else: + norm = None + + # Plot contour + cs = ax.contourf(X, Y, Z, levels=levels, cmap=cmap, norm=norm) + cs2 = ax.contour(X, Y, Z, levels=levels, colors='k', linewidths=0.5, alpha=0.3) + + # Add labels + ax.clabel(cs2, inline=True, fontsize=8, fmt='%.2f') + + # Add colorbar + cbar = plt.colorbar(cs, ax=ax) + cbar.set_label(metric_name) + + # Set labels + ax.set_xlabel(param_name2) + ax.set_ylabel(param_name1) + ax.set_title(f'Contour Map: {metric_name} as function of {param_name1} and {param_name2}') + + # Save figure + if save: + os.makedirs('figures', exist_ok=True) + safe_name1 = 'param1_' + str(hash(param_name1) % 10000) + safe_name2 = 'param2_' + str(hash(param_name2) % 10000) + safe_metric = 'metric_' + str(hash(metric_name) % 10000) + plt.savefig(f'figures/contour_{safe_name1}_{safe_name2}_{safe_metric}.png', + dpi=300, bbox_inches='tight') + + return fig, ax + +def plot_improved_sensitivity(df, parameter_name, metrics=None, save=True): + """Plot improved sensitivity curves similar to cane project style""" + if metrics is None: + metrics = [col for col in df.columns if col != parameter_name] + + # Create separate chart for each metric + for metric in metrics: + fig, ax = plt.subplots(figsize=(10, 6)) + + # Get data + x = df[parameter_name] + y = df[metric] + + # Plot curve with markers + ax.plot(x, y, '-o', lw=2.5, markersize=8, color='#3366CC', + markerfacecolor='white', markeredgecolor='#3366CC', markeredgewidth=2) + + # Add grid + ax.grid(True, linestyle='--', alpha=0.6) + + # Set title and labels + ax.set_title(f'Sensitivity: {metric} vs {parameter_name}', fontsize=14) + ax.set_xlabel(parameter_name, fontsize=12) + ax.set_ylabel(metric, fontsize=12) + + # Beautify axes + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + + # Save chart + if save: + os.makedirs('figures', exist_ok=True) + safe_param = 'param_' + str(hash(parameter_name) % 10000) + safe_metric = 'metric_' + str(hash(metric) % 10000) + plt.savefig(f'figures/sensitivity_{safe_param}_{safe_metric}_improved.png', + dpi=300, bbox_inches='tight') + + return fig, ax + +def plot_improved_monte_carlo(df, metric_name, save=True): + """Plot enhanced Monte Carlo distribution plot similar to cane project style""" + fig, ax = plt.subplots(figsize=(12, 7)) + + # Separate histogram and KDE curve plotting + # First plot histogram without KDE + sns.histplot(df[metric_name], kde=False, bins=25, ax=ax, + color='#3366CC', edgecolor='white', alpha=0.7) + + # Then plot KDE curve separately + sns.kdeplot(df[metric_name], ax=ax, color='red', lw=2) + + # Calculate and add statistics + mean_val = df[metric_name].mean() + median_val = df[metric_name].median() + std_val = df[metric_name].std() + + # Add vertical lines + ax.axvline(mean_val, color='red', linestyle='-', lw=2, + label=f'Mean: {mean_val:.3f}') + ax.axvline(median_val, color='green', linestyle='--', lw=2, + label=f'Median: {median_val:.3f}') + + # Add distribution area + p10 = df[metric_name].quantile(0.1) + p90 = df[metric_name].quantile(0.9) + ax.axvspan(p10, p90, alpha=0.2, color='grey', + label=f'10-90% CI: [{p10:.3f}, {p90:.3f}]') + + # Set title and labels + ax.set_title(f'Monte Carlo Distribution: {metric_name}', fontsize=14) + ax.set_xlabel(metric_name, fontsize=12) + ax.set_ylabel('Frequency', fontsize=12) + + # Add legend + ax.legend() + + # Save chart + if save: + os.makedirs('figures', exist_ok=True) + safe_metric = 'metric_' + str(hash(metric_name) % 10000) + plt.savefig(f'figures/monte_carlo_{safe_metric}_improved.png', + dpi=300, bbox_inches='tight') + + return fig, ax + +def plot_tornado(df, metric_name, parameters=None, n_parameters=10, save=True): + """Plot tornado diagram showing parameter influence on specific metric""" + # Calculate Spearman correlation coefficients + if parameters is None: + # Exclude metric columns, keep only parameter columns + parameters = [col for col in df.columns if col != metric_name] + + # Calculate correlation for each parameter with the metric + correlations = [] + for param in parameters: + corr, _ = spearmanr(df[param], df[metric_name]) + correlations.append((param, corr)) + + # Sort by correlation coefficient absolute value + correlations.sort(key=lambda x: abs(x[1]), reverse=True) + + # Keep only top n_parameters + if n_parameters < len(correlations): + correlations = correlations[:n_parameters] + + # Extract data for plotting + param_names, corr_values = zip(*correlations) + + # Plot tornado diagram + fig, ax = plt.subplots(figsize=(12, 8)) + + # Determine colors + colors = ['#3366CC' if c > 0 else '#CC3366' for c in corr_values] + + # Plot horizontal bars + bars = ax.barh(range(len(param_names)), corr_values, color=colors, + height=0.6, edgecolor='white', linewidth=1) + + # Add parameter name labels + ax.set_yticks(range(len(param_names))) + ax.set_yticklabels(param_names) + + # Set axes + ax.set_xlabel('Spearman Correlation Coefficient', fontsize=12) + ax.set_title(f'Parameter Importance for {metric_name}', fontsize=14) + + # Add vertical line at zero + ax.axvline(0, color='black', linestyle='-', linewidth=0.5) + + # Add grid + ax.grid(axis='x', linestyle='--', alpha=0.3) + + # Beautify + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + + # Add value labels for each bar + for i, bar in enumerate(bars): + width = bar.get_width() + label_x_pos = width + 0.01 if width >= 0 else width - 0.08 + ax.text(label_x_pos, bar.get_y() + bar.get_height()/2, + f'{width:.3f}', va='center') + + # Save chart + if save: + os.makedirs('figures', exist_ok=True) + safe_metric = 'metric_' + str(hash(metric_name) % 10000) + plt.savefig(f'figures/tornado_{safe_metric}.png', dpi=300, bbox_inches='tight') + + return fig, ax + +# Main program +if __name__ == "__main__": + print("Running BioSTEAM Sensitivity Analysis...") + + # Initialize system and model + bst.settings.set_thermo(chems) + main_flowsheet.clear() + microalgae_sys = create_microalgae_MCCA_production_sys() + u = microalgae_sys.flowsheet.unit + s = microalgae_sys.flowsheet.stream + + # Create TEA object + dry_tpd = u.U101.ins[0].F_mass * 24 / 1000 # kg/h -> t/d + microalgae_tea = create_tea( + system=microalgae_sys, + IRR=0.10, + duration=(2024, 2045), + depreciation='MACRS7', + income_tax=0.21, + operating_days=330, + lang_factor=None, + construction_schedule=(0.08, 0.60, 0.32), + startup_months=3, + startup_FOCfrac=1, + startup_salesfrac=0.5, + startup_VOCfrac=0.75, + WC_over_FCI=0.05, + finance_interest=0.08, + finance_years=10, + finance_fraction=0.4, + OSBL_units=(u.CT, u.CWP, u.ADP, u.PWC), + warehouse=0.04, + site_development=0.09, + additional_piping=0.045, + proratable_costs=0.10, + field_expenses=0.10, + construction=0.20, + contingency=0.10, + other_indirect_costs=0.10, + labor_cost=max(0.5e6, compute_labor_cost(dry_tpd)), + labor_burden=0.90, + property_insurance=0.007, + maintenance=0.03, + boiler_turbogenerator=None, + steam_power_depreciation='MACRS20' + ) + + # Create evaluation model + model = create_evaluation_model(microalgae_sys, microalgae_tea) + + # Define scenarios + scenarios = { + 'Optimistic': { + 'Microalgae price': -0.2, # Higher tipping fee income + 'Caproic acid price': 3.5, # Higher product price + 'Caproic acid yield factor': 1.2, # Higher than baseline yield + 'Operating days': 350, # More operating days + 'Maintenance factor': 0.02, # Lower maintenance cost + }, + 'Baseline': { + 'Microalgae price': -0.1, # Medium tipping fee income + 'Caproic acid price': 2.89, + 'Caproic acid yield factor': 1.0, + 'Operating days': 330, + 'Maintenance factor': 0.03, + }, + 'Pessimistic': { + 'Microalgae price': 0.0, # No tipping fee income + 'Caproic acid price': 2.0, # Lower product price + 'Caproic acid yield factor': 0.8, # Lower than baseline yield + 'Operating days': 300, # Fewer operating days + 'Maintenance factor': 0.05, # Higher maintenance cost + }, + 'Purchase': { + 'Microalgae price': 0.1, # Need to purchase feedstock + 'Caproic acid price': 3.2, # Higher product price + 'Caproic acid yield factor': 1.15, # Higher yield + 'Operating days': 330, + 'Maintenance factor': 0.03, + } + } + + # Single variable sensitivity analysis + print("\n1. Analyzing the effect of microalgae price on ROI and MSP...") + microalgae_price_results = run_univariate_analysis(model, 'Microalgae price') + plot_improved_sensitivity(microalgae_price_results, 'Microalgae price') + + # Bivariate analysis + print("\n2. Analyzing the effect of microalgae price and caproic acid price on ROI...") + heatmap_results = run_bivariate_analysis(model, 'Microalgae price', 'Caproic acid price', 'ROI') + plot_contour_from_heatmap(heatmap_results, 'Microalgae price', 'Caproic acid price', 'ROI') + + # Monte Carlo analysis + print("\n3. Running Monte Carlo simulation...") + monte_carlo_results = run_monte_carlo(model, n_samples=10000) + plot_improved_monte_carlo(monte_carlo_results, 'ROI') + + # Tornado diagram + plot_tornado(monte_carlo_results, 'ROI') + + # Scenario analysis + print("\n4. Running scenario analysis...") + scenario_results = run_scenario_analysis(model, scenarios) + + print("\nSensitivity analysis complete! Results saved in 'results' and 'figures' folders") \ No newline at end of file diff --git a/biorefineries/microalgae/streams.py b/biorefineries/microalgae/streams.py new file mode 100644 index 00000000..ce4026e8 --- /dev/null +++ b/biorefineries/microalgae/streams.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat June 28 10:00:00 2025 + +Microalgae biorefinery to produce medium chain fatty acids +by anaerobic fermentation without external electron donor addition- feed streams + +References +---------- +[1] BioSTEAM Documentation: + https://biosteam.readthedocs.io/en/latest/tutorial/Creating_a_System.html +[2] Cortes-Peña et al., BioSTEAM: A Fast and Flexible Platform for the Design, + Simulation, and Techno-Economic Analysis of Biorefineries under Uncertainty. + ACS Sustainable Chem. Eng. 2020, 8 (8), 3302–3310. +[3] 3-Hydroxypropionic acid biorefineries project: + https://github.com/BioSTEAMDevelopmentGroup/Bioindustrial-Park/tree/master/biorefineries/HP +[4] Succinic projest + https://github.com/BioSTEAMDevelopmentGroup/Bioindustrial-Park/tree/master/biorefineries/succinic + +@author: Xingdong Shi +@version: 0.0.1 +""" + +from biosteam import stream_kwargs + +microalgae_feed = stream_kwargs('microalgae', + Microalgae=8000, + units='kg/hr', + price = 0.15 # this is a baseline value for TEA (cost for cultivation, harvesting and trasporting) +) diff --git a/biorefineries/microalgae/system.py b/biorefineries/microalgae/system.py new file mode 100644 index 00000000..4d8b5b00 --- /dev/null +++ b/biorefineries/microalgae/system.py @@ -0,0 +1,350 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat July 05 12:50:00 2025 + +Microalgae biorefinery to produce medium chain fatty acids +by anaerobic fermentation without external electron donor addition- system + +References +---------- +[1] BioSTEAM Documentation: + https://biosteam.readthedocs.io/en/latest/tutorial/Creating_a_System.html +[2] Cortes-Peña et al., BioSTEAM: A Fast and Flexible Platform for the Design, + Simulation, and Techno-Economic Analysis of Biorefineries under Uncertainty. + ACS Sustainable Chem. Eng. 2020, 8 (8), 3302–3310. +[3] 3-Hydroxypropionic acid biorefineries project: + https://github.com/BioSTEAMDevelopmentGroup/Bioindustrial-Park/tree/master/biorefineries/HP +[4] Succinic projest + https://github.com/BioSTEAMDevelopmentGroup/Bioindustrial-Park/tree/master/biorefineries/succinic + +@author: Xingdong Shi +@version: 0.0.8 +""" + +import biosteam as bst +import numpy as np +import thermosteam as tmo +from biosteam import Stream, SystemFactory +from biosteam.units import MixTank, Pump, StorageTank, HXutility, Mixer, Splitter, MultiStageMixerSettlers +from biosteam.facilities import AirDistributionPackage, ProcessWaterCenter, CoolingTower, ChilledWaterPackage +from biosteam import main_flowsheet +from .units import ( + FeedstockPreprocessing, AcidPretreatmentReactor, Saccharification, SolidLiquidSeparation, MCCAFermentation, + ButanolDistillation, ButyricAcidDistillation, CaproicAcidDistillation, HeptanoicAcidDistillation, CaprylicAcidDistillation, + NeutralizationTank, AnaerobicDigestion +) +from .utils import price +from .facilities import BT, HXNWithMin +from ._chemicals import chems +from .tea import create_tea +from .streams import microalgae_feed +import warnings +import re +warnings.filterwarnings("ignore", message=r".*Hot utility load is negative.*", category=RuntimeWarning, module=r"biosteam\.facilities") +warnings.filterwarnings("ignore", message=".*has been replaced in registry") +warnings.filterwarnings("ignore", category=bst.exceptions.CostWarning) +warnings.filterwarnings("ignore", message=".*moisture.*is smaller than the desired.*") +warnings.filterwarnings("ignore", message=".*moisture of influent.*is smaller than the desired.*") + +# ------------------------------------------------------------------ +# Utility: labor cost scaling with plant size +# ------------------------------------------------------------------ + +def compute_labor_cost(dry_tpd: float, + base_tpd: float = 2205.0, + base_cost: float = 3212962.0, + exponent: float = 0.2, + floor_tpd: float = 100.0, + floor_cost: float = 0.5e6) -> float: + """Scale labor cost with plant size but keep a fixed cost below *floor_tpd*. + Parameters + ---------- + dry_tpd + Dry feed rate (t/d). + base_tpd, base_cost, exponent + Parameters of the power-law model. + floor_tpd, floor_cost + If dry_tpd < floor_tpd, return floor_cost (representing baseline staffing). + """ + if dry_tpd < floor_tpd: + return floor_cost + return base_cost * (dry_tpd / base_tpd) ** exponent + +# Set up the main flowsheet and thermodynamic environment +bst.settings.set_thermo(chems) +main_flowsheet.clear() +flowsheet = bst.Flowsheet('MCCA') +bst.main_flowsheet.set_flowsheet(flowsheet) + +# System settings +bst.System.default_converge_method = 'wegstein' +bst.System.default_maxiter = 2000 +bst.System.default_molar_tolerance = 1e-1 +bst.System.default_relative_molar_tolerance = 1e-1 # supersedes absolute tolerance +bst.System.strict_convergence = False # True => throw exception if system does not converge; False => continue with unconverged system + +@SystemFactory( + ID='Microalgae_MCCA_production', + ins=[dict(microalgae_feed, thermo=chems)], + outs=[ + #dict(ID='butanol_product', thermo=chems), + dict(ID='butyric_acid_product', thermo=chems), + dict(ID='caproic_acid_product', thermo=chems), + dict(ID='heptanoic_acid_product', thermo=chems), + dict(ID='caprylic_acid_product', thermo=chems)] +) +def create_microalgae_MCCA_production_sys(ins, outs): + # Set the thermodynamic package explicitly + tmo.settings.set_thermo(chems) + + # Main feed and product + microalgae_feed, = ins + ( #butanol_product, + butyric_acid_product, + caproic_acid_product, + heptanoic_acid_product, + caprylic_acid_product, + ) = outs + + # Calculate all required stream properties based on feed + microalgae_mass = microalgae_feed.F_mass + microalgae_water_mass = microalgae_mass / 0.04 # 4% solid loading + microalgae_water = Stream('microalgae_water', Water=microalgae_water_mass, units='kg/hr') + # H2SO4 for microalgae biomass hydrolysis + acid_loading = 1.47 # g H2SO4 / g microalgae + acid_purity = 0.93 + water_mass = microalgae_mass * (1 - 0.04) / 0.04 + pure_H2SO4 = microalgae_mass * acid_loading + acid_solution_mass = pure_H2SO4 / acid_purity + water_mass_acid = acid_solution_mass * (1 - acid_purity) + SulfuricAcid = Stream('sulfuricacid', H2SO4=pure_H2SO4, Water=water_mass_acid, units='kg/hr', price=price['SulfuricAcid']) + # Ammonium Hydroxide for neutralization + h2so4_mol = pure_H2SO4 * 1000 / 98 # mol mass + nh4oh_mol = h2so4_mol * 0.1 # preadjustment + nh4oh_mass = nh4oh_mol * 35 / 1000 # mol mass to mass + ammonium_hydroxide = Stream('ammonium_hydroxide', NH4OH=nh4oh_mass, units='kg/hr', price=price['AmmoniumHydroxide']) + # Enzyme dosages + glucoamylase_mass = float(microalgae_mass * 0.0011) # 确保为标量 + alpha_amylase_mass = float(microalgae_mass * 0.0082) # 确保为标量,ref from cron project + glucoamylase = Stream('glucoamylase', GlucoAmylase=glucoamylase_mass, units='kg/hr', price=price['GlucoAmylase']) + alpha_amylase = Stream('alpha_amylase', AlphaAmylase=alpha_amylase_mass, units='kg/hr', price=price['AlphaAmylase']) + # NaOH for pH adjustment + naoh1_mass = microalgae_mass * 0.02 # pH 4.5 + naoh2_mass = microalgae_mass * 0.02 # pH 5.5 + total_naoh_mass = naoh1_mass + naoh2_mass + naoh = Stream('naoh', NaOH=total_naoh_mass, units='kg/hr', price=price['NaOH']) + # Yeast addition in MCCA fermentation + yeast_mass = microalgae_mass * 5 / 75 + yeast = Stream('yeast', Yeast=yeast_mass, units='kg/hr', price=price['Yeast']) + # Octanol for extraction + fresh_octanol = Stream('fresh_octanol', Octanol=200, units='kg/hr', price=price['Octanol']) + octanol_recycle = Stream('octanol_recycle', Octanol=50, units='kg/hr') + octanol_feed = bst.Stream() + # Assign prices to product streams + #butanol_product.price = price['Butanol'] + butyric_acid_product.price = price['ButyricAcid'] + caproic_acid_product.price = price['CaproicAcid'] + heptanoic_acid_product.price = price['HeptanoicAcid'] + caprylic_acid_product.price = price['CaprylicAcid'] + + # ===================== + # Area 1: Microalgae process + # ===================== + U101 = FeedstockPreprocessing('U101', microalgae_feed, thermo=chems) + + # ===================== + # Area 2: Hydrolysis and saccharification + # ===================== + T201 = StorageTank('T201', SulfuricAcid) + P201 = Pump('P201', T201-0, P=5e5, pump_type='Default') + M201 = Mixer('M201', [P201-0, microalgae_water, U101-0]) + P202 = Pump('P202', M201-0) + H201 = HXutility('H201', P202-0, T=121+273.15) + R201 = AcidPretreatmentReactor('R201', H201-0) + T202 = StorageTank('T202', ammonium_hydroxide) + P203 = Pump('P203', T202-0) + R202 = NeutralizationTank('R202', [R201-0, P203-0]) + P204 = Pump('P204', R202-0) + H202 = HXutility('H202', P204-0, T=55+273.15) + T203 = StorageTank('T203', glucoamylase) + P205 = Pump('P205', T203-0, P=25e5, pump_type='Default', dP_design=24e5, ignore_NPSH=True) + T204 = StorageTank('T204', alpha_amylase) + P206 = Pump('P206', T204-0, P=25e5, pump_type='Default', dP_design=24e5, ignore_NPSH=True) + T205 = StorageTank('T205', naoh) + P207 = Pump('P207', T205-0, P=25e5, pump_type='Default', dP_design=24e5, ignore_NPSH=True) + S201 = Splitter('S201', P207-0, split=naoh1_mass/total_naoh_mass) + M202 = Mixer('M202', [H202-0, P205-0, S201-0]) + R203 = Saccharification('R203', M202-0) + H203 = HXutility('H203', R203-0, T=90+273.15) + M203 = Mixer('M203', [H203-0, P206-0, S201-1]) + R204 = Saccharification('R204', M203-0) + S202 = SolidLiquidSeparation('S202', R204-0) + P208 = Pump('P208', S202-0) + + # ===================== + # Area 3: Fermentation for MCCA production + # ===================== + H301 = HXutility('H301', P208-0, T=37+273.15) + T301 = StorageTank('T301', yeast) + P301 = Pump('P301', T301-0) + R301 = MCCAFermentation('R301', [H301-0, P301-0], microalgae_mass_flow=microalgae_mass, titer=60.0) + T302 = MixTank('T302', [R301-1]) + S301 = SolidLiquidSeparation('S301', R301-0) + + # ===================== + # Area 4: Product extraction + # ===================== + M401 = Mixer('M401', [fresh_octanol, octanol_recycle], octanol_feed) + @M401.add_specification(run=True) + def adjust_fresh_octanol(): + total_octanol = 200 + recycle = octanol_recycle.imass['Octanol'] + fresh = max(total_octanol - recycle, 1e-3) + fresh_octanol.imass['Octanol'] = fresh + IDs = ['Water', 'AceticAcid', 'PropionicAcid', 'ButyricAcid', 'ValericAcid', 'CaproicAcid','CaprylicAcid', 'HeptanoicAcid', 'Butanol', 'Octanol'] + K = np.array([1/5000, 0.24, 1.29, 5000/1, 13.58, 5000/1, 5000/1, 5000/1, 5000/1, 100000/1]) + S402 = MultiStageMixerSettlers( + 'S402', + partition_data={'K': K, 'IDs': IDs}, + N_stages=5, + ins=[S301-0, M401-0], + outs=['extracted_organic', 'aqueous_raffinate'] + ) + #D401 = ButanolDistillation('D401', S402-0) + #D401.check_LHK = False + D402 = ButyricAcidDistillation('D402', S402-0) + D402.check_LHK = False + D403 = CaproicAcidDistillation('D403', D402-1) + D403.check_LHK = False + D404 = HeptanoicAcidDistillation('D404', D403-1) + D404.check_LHK = False + D405 = CaprylicAcidDistillation('D405', D404-1, ['', octanol_recycle]) + D405.check_LHK = False + + # ===================== + # Area 5: Waste reuse for biogas production + # ===================== + M501 = Mixer('M501', [S301-1, S202-1, S402-1]) + R501 = AnaerobicDigestion('R501', M501-0, microalgae_mass = microalgae_mass) + M503 = Mixer('M503', R501-1) + M502 = Mixer('M502', [R501-0, T302-0]) + + # ===================== + # Area 6: Facilities requirements + # ===================== + #T601 = StorageTank('T601', D401-0, tau=60.*24., V_wf=0.9, vessel_type='Floating roof', vessel_material='Stainless steel') + #P601 = Pump('P601', T601-0, butanol_product) + T602 = StorageTank('T602', D402-0, tau=30.*24., V_wf=0.9, vessel_type='Floating roof', vessel_material='Stainless steel') + P602 = Pump('P602', T602-0, butyric_acid_product) + T603 = StorageTank('T603', D403-0, tau=30.*24., V_wf=0.9, vessel_type='Floating roof', vessel_material='Stainless steel') + P603 = Pump('P603', T603-0, caproic_acid_product) + T604 = StorageTank('T604', D404-0, tau=60.*24., V_wf=0.9, vessel_type='Floating roof', vessel_material='Stainless steel') + P604 = Pump('P604', T604-0, heptanoic_acid_product) + T605 = StorageTank('T605', D405-0, tau=60.*24., V_wf=0.9, vessel_type='Floating roof', vessel_material='Stainless steel') + P605 = Pump('P605', T605-0, caprylic_acid_product) + CT = CoolingTower('CT') + HXN601 = HXNWithMin('HXN601', T_min_app=10, min_heat_util=2e6) + PWC = ProcessWaterCenter('PWC') + ADP = AirDistributionPackage('ADP') + CWP = ChilledWaterPackage('CWP') + + # 添加锅炉涡轮发电机 + # 使用厌氧消化产生的沼气作为燃料,并允许购买额外天然气 + BT601 = bst.facilities.BoilerTurbogenerator('BT601', + ins=(R501-2, M502-0, '', '', '', ''), + satisfy_system_electricity_demand=True, # 允许购买天然气 + boiler_efficiency=0.9, # 提高锅炉效率 + turbogenerator_efficiency=0.85) # 提高涡轮发电机效率 + + WastewaterT = bst.create_high_rate_wastewater_treatment_system('WastewaterT', + M503-0, # Use diluted wastewater stream + skip_IC=True, # Skip internal circulation to avoid division by zero + process_ID='6' # Use process ID 6 for unit numbering + ) + +# ========================================== +# TEA Analysis +# ========================================== +# Create system and TEA objects at module level for import +u = flowsheet.unit +s = flowsheet.stream +microalgae_mcca_sys = create_microalgae_MCCA_production_sys() +microalgae_mcca_sys.simulate() + +# TEA analysis +# Dry biomass feed rate in ton per day (t/d) +dry_tpd = u.U101.ins[0].F_mass * 24 / 1000 # kg/h -> t/d +microalgae_tea = create_tea(system=microalgae_mcca_sys, IRR=0.10, duration=(2024, 2045), + depreciation='MACRS7', income_tax=0.21, + operating_days=330, + lang_factor= None, construction_schedule=(0.08, 0.60, 0.32), + startup_months=3, startup_FOCfrac=1, startup_salesfrac=0.5, + startup_VOCfrac=0.75, WC_over_FCI=0.05, + finance_interest=0.08, finance_years=10, finance_fraction=0.4, + OSBL_units=(u.CT, u.CWP, u.ADP, u.PWC), + warehouse=0.04, site_development=0.09, additional_piping=0.045, + proratable_costs=0.10, field_expenses=0.10, construction=0.20, + contingency=0.10, other_indirect_costs=0.10, + labor_cost=max(0.5e6, compute_labor_cost(dry_tpd)), + labor_burden=0.90, property_insurance=0.007, maintenance=0.03, boiler_turbogenerator=None, + steam_power_depreciation='MACRS20') + +# Main script: run additional analysis and print results +if __name__ == '__main__': + microalgae_mcca_sys.diagram('cluster', format='png') + microalgae_mcca_sys.print() + print("\n===== Techno-Economic Analysis (TEA) Main Results =====") + # Use the system's main product stream directly for price calculation + caproic_acid_product = s.caproic_acid_product + if caproic_acid_product is not None: + if caproic_acid_product.price is None or caproic_acid_product.price == 0: + caproic_acid_product.price = 4.5 + price = microalgae_tea.solve_price(caproic_acid_product) + print(f"Caproic Acid Minimum Selling Price: {price:.2f} $/kg") + if caproic_acid_product.F_mass > 0 and caproic_acid_product.price > 0: + print("Caproic Acid Unit Production Cost:", microalgae_tea.production_costs([caproic_acid_product])) + print("NPV:", microalgae_tea.NPV) + print("TCI:", microalgae_tea.TCI) + print("FCI:", microalgae_tea.FCI) + print("DPI:", microalgae_tea.DPI) + print("TDC:", microalgae_tea.TDC) + print("FOC:", microalgae_tea.FOC) + print("VOC:", microalgae_tea.VOC) + print("AOC:", microalgae_tea.AOC) + print("ROI:", microalgae_tea.ROI) + print("PBP:", microalgae_tea.PBP) + print("Annual Depreciation:", microalgae_tea.annual_depreciation) + print("Sales:", microalgae_tea.sales) + print("Material Cost:", microalgae_tea.material_cost) + print("Utility Cost:", microalgae_tea.utility_cost) + print("CAPEX Table:\n", microalgae_tea.CAPEX_table()) + print("FOC Table:\n", microalgae_tea.FOC_table()) + print("Cashflow Table:\n", microalgae_tea.get_cashflow_table()) + + + + # Quick check: product flows in each units + # print("\n===== Stream Mass Flows for Each Unit (kg/hr) =====") + # for u_ in microalgae_mcca_sys.units: + # print(f"\n[{u_.ID} - {u_.__class__.__name__}]") + # for i, stream in enumerate(u_.ins): + # if stream: + # print(f" Inlet {i+1} ({stream.ID}):") + # for chem, flow in zip(stream.chemicals.IDs, stream.mass): + # if abs(flow) > 1e-6: + # print(f" {chem}: {flow:.2f} kg/hr") + # for i, stream in enumerate(u_.outs): + # if stream: + # print(f" Outlet {i+1} ({stream.ID}):") + # for chem, flow in zip(stream.chemicals.IDs, stream.mass): + # if abs(flow) > 1e-6: + # print(f" {chem}: {flow:.2f} kg/hr") + + # Quick check: product flows and prices + # for p in (#s.butanol_product, + # s.caproic_acid_product,s.heptanoic_acid_product, s.caprylic_acid_product, s.butyric_acid_product): + # print(f"{p.ID}: {p.F_mass:.2f} kg/h @ {p.price} $/kg") + +# ========================================== +# Sensitive analysis +# ========================================== diff --git a/biorefineries/microalgae/tea.py b/biorefineries/microalgae/tea.py new file mode 100644 index 00000000..41fe29b5 --- /dev/null +++ b/biorefineries/microalgae/tea.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat July 20 17:50:00 2025 + +Microalgae biorefinery to produce medium chain fatty acids +by anaerobic fermentation without external electron donor addition- TEA + +References +---------- +[1] BioSTEAM Documentation: + https://biosteam.readthedocs.io/en/latest/tutorial/Creating_a_System.html +[2] Cortes-Peña et al., BioSTEAM: A Fast and Flexible Platform for the Design, + Simulation, and Techno-Economic Analysis of Biorefineries under Uncertainty. + ACS Sustainable Chem. Eng. 2020, 8 (8), 3302–3310. +[3] 3-Hydroxypropionic acid biorefineries project: + https://github.com/BioSTEAMDevelopmentGroup/Bioindustrial-Park/tree/master/biorefineries/HP +[4] Succinic projest + https://github.com/BioSTEAMDevelopmentGroup/Bioindustrial-Park/tree/master/biorefineries/succinic + +@author: Xingdong Shi +@version: 0.0.8 +""" + +import biosteam as bst +from biorefineries.tea.cellulosic_ethanol_tea import CellulosicEthanolTEA as create_tea + diff --git a/biorefineries/microalgae/uncertainties.py b/biorefineries/microalgae/uncertainties.py new file mode 100644 index 00000000..1868dbdd --- /dev/null +++ b/biorefineries/microalgae/uncertainties.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Microalgae biorefinery uncertainty and sensitivity analysis + +This module performs uncertainty and sensitivity analysis for the microalgae biorefinery +system, including Monte Carlo simulations and Spearman correlation analysis. + +@author: Xingdong Shi +@version: 0.0.1 +""" + +from warnings import filterwarnings +filterwarnings('ignore') +import numpy as np +import pandas as pd +import biosteam as bst +import thermosteam as tmo +from datetime import datetime +from biosteam.utils import TicToc +import os + +# Import microalgae system components +from . import system as microalgae_system +from . import lca +from . import tea +from ._chemicals import chems + +# Set up the system +bst.settings.set_thermo(chems) +microalgae_sys = microalgae_system.microalgae_mcca_sys +microalgae_tea = microalgae_system.microalgae_tea + +# Create LCA object +from . import analysis_utils +microalgae_lca = analysis_utils.create_microalgae_lca_simple(microalgae_sys, microalgae_tea) + +# Create model +# Get main product for TEA calculations +main_product = analysis_utils.get_main_product_stream(microalgae_sys) + +model = bst.Model(microalgae_sys, metrics=[ + bst.Metric('MPSP', lambda: microalgae_tea.solve_price(main_product), 'USD/kg'), + bst.Metric('GWP100a', lambda: microalgae_lca.GWP, 'kg CO2-eq/kg'), + bst.Metric('FEC', lambda: microalgae_lca.FEC, 'MJ/kg'), +]) + +print('\n\nLoaded microalgae system for uncertainty analysis.') + +# Analysis parameters +N_simulations_per_mode = 2000 +percentiles = [0, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 1] +notification_interval = 10 + +results_dict = { + 'Baseline': { + 'MPSP': {}, + 'GWP100a': {}, + 'FEC': {}, + 'GWP Breakdown': {}, + 'FEC Breakdown': {} + }, + 'Uncertainty': { + 'MPSP': {}, + 'GWP100a': {}, + 'FEC': {} + }, + 'Sensitivity': { + 'Spearman': { + 'MPSP': {}, + 'GWP100a': {}, + 'FEC': {} + } + } +} + +# Parameter distribution file +parameter_distributions_filename = 'parameter_distributions.xlsx' + +def run_uncertainty_analysis(): + """Run uncertainty and sensitivity analysis for microalgae biorefinery.""" + + timer = TicToc('timer') + timer.tic() + + # Set seed for reproducibility + np.random.seed(3221) + + print(f'\n\nLoading parameter distributions...') + + # Get current directory + current_dir = os.path.dirname(os.path.abspath(__file__)) + param_file_path = os.path.join(current_dir, parameter_distributions_filename) + + # Load parameter distributions + model.parameters = () + + # Create namespace dictionary for microalgae system + namespace_dict = { + 'microalgae_sys': microalgae_sys, + 'microalgae_tea': microalgae_tea, + 'microalgae_lca': microalgae_lca, + } + + # Add all units to namespace + for unit in microalgae_sys.units: + namespace_dict[unit.ID] = unit + + # Add all streams to namespace + for stream in microalgae_sys.streams: + namespace_dict[stream.ID] = stream + + # Add chemicals to namespace + for chemical in chems: + namespace_dict[chemical.ID] = chemical + + model.load_parameter_distributions(param_file_path, namespace_dict) + print(f'\nLoaded parameter distributions.') + + parameters = model.get_parameters() + + # Baseline analysis + print(f'\n\nRunning baseline analysis...') + model.metrics_at_baseline() + baseline_metrics = model.metrics_at_baseline() + + results_dict['Baseline']['MPSP'] = baseline_metrics[0] + results_dict['Baseline']['GWP100a'] = baseline_metrics[1] + results_dict['Baseline']['FEC'] = baseline_metrics[2] + + # Get baseline breakdowns + microalgae_sys.simulate() + + # GWP breakdown + material_GWP_breakdown = microalgae_lca.material_GWP_breakdown + gwp_breakdown = { + 'feedstock*': microalgae_lca.feedstock_GWP, + 'H2SO4': material_GWP_breakdown.get('H2SO4', 0), + 'NaOH': material_GWP_breakdown.get('NaOH', 0), + 'NH4OH': material_GWP_breakdown.get('NH4OH', 0), + 'CalciumDihydroxide': material_GWP_breakdown.get('CalciumDihydroxide', 0), + 'Ethanol': material_GWP_breakdown.get('Ethanol', 0), + 'Octanol': material_GWP_breakdown.get('Octanol', 0), + 'GlucoAmylase': material_GWP_breakdown.get('GlucoAmylase', 0), + 'AlphaAmylase': material_GWP_breakdown.get('AlphaAmylase', 0), + 'CH4': material_GWP_breakdown.get('CH4', 0), + 'net electricity': microalgae_lca.net_electricity_GWP, + 'direct non-biogenic emissions': microalgae_lca.direct_non_biogenic_emissions_GWP, + } + + # Normalize GWP breakdown + tot_positive_GWP = sum([v for v in gwp_breakdown.values() if v > 0]) + if tot_positive_GWP > 0: + for k, v in gwp_breakdown.items(): + gwp_breakdown[k] = v / tot_positive_GWP + + results_dict['Baseline']['GWP Breakdown'] = gwp_breakdown + + # FEC breakdown (simplified) + fec_breakdown = { + 'feedstock*': microalgae_lca.feedstock_FEC if hasattr(microalgae_lca, 'feedstock_FEC') else 0, + 'materials': sum(material_GWP_breakdown.values()), # Approximation + 'net electricity': microalgae_lca.net_electricity_FEC if hasattr(microalgae_lca, 'net_electricity_FEC') else 0, + } + + results_dict['Baseline']['FEC Breakdown'] = fec_breakdown + + print(f'\nBaseline MPSP: {baseline_metrics[0]:.3f} USD/kg') + print(f'Baseline GWP100a: {baseline_metrics[1]:.3f} kg CO2-eq/kg') + print(f'Baseline FEC: {baseline_metrics[2]:.3f} MJ/kg') + + # Monte Carlo simulation + print(f'\n\nRunning Monte Carlo simulation with {N_simulations_per_mode} samples...') + + def notification(i): + if i % notification_interval == 0: + print(f'Completed {i}/{N_simulations_per_mode} simulations') + + samples = model.sample(N_simulations_per_mode, rule='L') + model.load_samples(samples) + model.evaluate(notification=notification, autoload=False, autosave=False) + + results = model.table + + # Process uncertainty results + metrics = ['MPSP', 'GWP100a', 'FEC'] + for metric in metrics: + metric_values = results[metric].values + percentile_values = np.percentile(metric_values, [p*100 for p in percentiles]) + results_dict['Uncertainty'][metric] = dict(zip(percentiles, percentile_values)) + + # Sensitivity analysis (Spearman correlation) + print(f'\n\nCalculating Spearman correlations...') + + for metric in metrics: + correlations = {} + metric_values = results[metric].values + + for param in parameters: + param_values = results[param.name].values + # Calculate Spearman correlation + correlation = np.corrcoef( + np.argsort(np.argsort(param_values)), + np.argsort(np.argsort(metric_values)) + )[0, 1] + correlations[param.name] = correlation + + # Sort by absolute correlation + sorted_correlations = dict(sorted(correlations.items(), + key=lambda x: abs(x[1]), + reverse=True)) + results_dict['Sensitivity']['Spearman'][metric] = sorted_correlations + + timer.toc() + + return results_dict, results + +def save_results(results_dict, results, filename_prefix='microalgae_uncertainty'): + """Save analysis results to files.""" + + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + + # Save results dictionary + results_df = pd.DataFrame() + + # Baseline results + baseline_data = { + 'Metric': ['MPSP', 'GWP100a', 'FEC'], + 'Baseline': [ + results_dict['Baseline']['MPSP'], + results_dict['Baseline']['GWP100a'], + results_dict['Baseline']['FEC'] + ] + } + + # Add uncertainty percentiles + for percentile in percentiles: + baseline_data[f'P{int(percentile*100)}'] = [ + results_dict['Uncertainty']['MPSP'][percentile], + results_dict['Uncertainty']['GWP100a'][percentile], + results_dict['Uncertainty']['FEC'][percentile] + ] + + results_df = pd.DataFrame(baseline_data) + + # Save to Excel + excel_filename = f'{filename_prefix}_results_{timestamp}.xlsx' + with pd.ExcelWriter(excel_filename) as writer: + results_df.to_excel(writer, sheet_name='Summary', index=False) + + # Save full Monte Carlo results + results.to_excel(writer, sheet_name='Monte_Carlo', index=False) + + # Save sensitivity analysis + for metric in ['MPSP', 'GWP100a', 'FEC']: + sensitivity_data = pd.DataFrame( + list(results_dict['Sensitivity']['Spearman'][metric].items()), + columns=['Parameter', 'Spearman_Correlation'] + ) + sensitivity_data.to_excel(writer, sheet_name=f'Sensitivity_{metric}', index=False) + + # Save GWP breakdown + gwp_breakdown_df = pd.DataFrame( + list(results_dict['Baseline']['GWP Breakdown'].items()), + columns=['Component', 'Fraction'] + ) + gwp_breakdown_df.to_excel(writer, sheet_name='GWP_Breakdown', index=False) + + print(f'\nResults saved to {excel_filename}') + + # Save summary text report + txt_filename = f'{filename_prefix}_summary_{timestamp}.txt' + with open(txt_filename, 'w') as f: + f.write("Microalgae Biorefinery Uncertainty Analysis Summary\n") + f.write("=" * 50 + "\n\n") + + f.write("Baseline Results:\n") + f.write(f"MPSP: {results_dict['Baseline']['MPSP']:.3f} USD/kg\n") + f.write(f"GWP100a: {results_dict['Baseline']['GWP100a']:.3f} kg CO2-eq/kg\n") + f.write(f"FEC: {results_dict['Baseline']['FEC']:.3f} MJ/kg\n\n") + + f.write("Uncertainty Analysis (percentiles):\n") + for metric in ['MPSP', 'GWP100a', 'FEC']: + f.write(f"\n{metric}:\n") + for percentile in [0.05, 0.25, 0.5, 0.75, 0.95]: + value = results_dict['Uncertainty'][metric][percentile] + f.write(f" P{int(percentile*100)}: {value:.3f}\n") + + f.write("\nTop 10 Most Influential Parameters (by absolute Spearman correlation):\n") + for metric in ['MPSP', 'GWP100a', 'FEC']: + f.write(f"\n{metric}:\n") + correlations = results_dict['Sensitivity']['Spearman'][metric] + for i, (param, corr) in enumerate(list(correlations.items())[:10]): + f.write(f" {i+1}. {param}: {corr:.3f}\n") + + print(f'Summary saved to {txt_filename}') + +if __name__ == '__main__': + print("Starting microalgae biorefinery uncertainty analysis...") + + try: + results_dict, results = run_uncertainty_analysis() + save_results(results_dict, results) + + print("\nAnalysis completed successfully!") + + except Exception as e: + print(f"\nError during analysis: {str(e)}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/biorefineries/microalgae/units.py b/biorefineries/microalgae/units.py new file mode 100644 index 00000000..7c44f866 --- /dev/null +++ b/biorefineries/microalgae/units.py @@ -0,0 +1,700 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Sat June 23 17:41:00 2025 + +Microalgae biorefinery to produce medium chain fatty acids +by anaerobic fermentation without external electron donor addition- chemicals database + +References +---------- +[1] BioSTEAM Documentation: + https://biosteam.readthedocs.io/en/latest/API/thermosteam/Chemicals.html +[2] Cortes-Peña et al., BioSTEAM: A Fast and Flexible Platform for the Design, + Simulation, and Techno-Economic Analysis of Biorefineries under Uncertainty. + ACS Sustainable Chem. Eng. 2020, 8 (8), 3302–3310. +[3] 3-Hydroxypropionic acid biorefineries project: + https://github.com/BioSTEAMDevelopmentGroup/Bioindustrial-Park/tree/master/biorefineries/HP + +@author: Xingdong Shi +@version: 0.0.1 +""" + +import numpy as np +import biosteam as bst +import math +from biosteam import Unit +from biosteam.exceptions import DesignError +from biosteam.units.design_tools import pressure_vessel_material_factors as factors +from biosteam.units.decorators import cost +from biosteam.units import HXutility, Mixer, MixTank, Pump, StorageTank, StirredTankReactor +from thermosteam import Stream +import thermosteam as tmo +from ._chemicals import chems +from .utils import baseline_feedflow, CEPCI +from thermosteam import reaction as rxn +from biorefineries.lactic._units import Reactor + +def anaerobic_rxn(reactant): + MW = getattr(chems, reactant).MW + return rxn.Reaction(f"{1/MW}{reactant} -> {P_ch4}CH4 + {P_co2}CO2 + {P_sludge}WWTsludge", reactant, 0.91) + +def find_split(*tuples): + + split = {} + for ID, flow0, flow1 in tuples: + total = flow0 + flow1 + split[ID] = flow0 / total if total else 0 + return split + + +Rxn = tmo.reaction.Reaction +ParallelRxn = tmo.reaction.ParallelReaction +ln = math.log +exp = math.exp +_gal2m3 = 0.003785 +_gpm2m3hr = 0.227124 +chems = tmo.settings.chemicals +organics = ['Glucose', 'Xylose', 'Protein', 'Ash', 'AceticAcid', 'Ethanol'] +if 'WWTsludge' in organics: + organics.remove('WWTsludge') +P_sludge = 0.05/0.91/chems.WWTsludge.MW +MW = np.array([chems.CH4.MW, chems.CO2.MW]) +CH4_molcomp = 0.60 +mass = np.array([CH4_molcomp, 1-CH4_molcomp])*MW +mass /= mass.sum() +mass *= 0.381/(0.91) +P_ch4, P_co2 = mass/MW +anaerobic_digestion = rxn.ParallelReaction([anaerobic_rxn(i) for i in organics]) + +splits = [ + ('Glucose', 3, 42), + ('Xylose', 7, 85), + ('Protein', 51, 18), + ('Ash', 813, 280), + ('AceticAcid', 0, 5), + ('Ethanol', 1, 15), +] + +tmo.settings.set_thermo(chems) + + +# %% +# ========================= +# Microalgae Crushing +# ========================= +@cost(basis='Flow rate', ID='System', units='kg/hr', + kW=511.3205, cost=13329690, S=94697, CE=CEPCI[2009], n=0.6, BM=1.7) +class FeedstockPreprocessing(Unit): + # 2205 U.S. ton/day (2000 metric tonne/day) as in Humbird et al. + _baseline_flow_rate = baseline_feedflow.sum() + _cached_flow_rate = 2205 + _N_ins = 1 + _N_outs = 1 + + def _run(self): + self.outs[0].copy_like(self.ins[0]) + s = self.outs[0] + for comp in s.chemicals.IDs: + if comp != 'Microalgae': + s.imass[comp] = 0 + +# %% +# ============================================================================= +# Pretreatment +# ============================================================================= +@cost(basis='Flow rate', ID='Tank', units='kg/hr', + cost=6210, S=1981, CE=CEPCI[2010], n=0.7, BM=3) +@cost(basis='Flow rate', ID='Pump', units='kg/hr', + cost=8000, S=3720, CE=CEPCI[2009], n=0.8, BM=2.3) +class SulfuricAcidAdditionTank(StorageTank): + _N_ins = 1 + _N_outs = 1 + # acid_volume_per_biomass = 0.8 mL/g + # acid_density = 1.84 # g/mL + acid_loading = 1.47 # g H2SO4 / g microalgae + + def _run(self): + feed = self.ins[0] + out = self.outs[0] + microalgae_mass = feed.imass['Microalgae'] + + pure_H2SO4 = microalgae_mass * self.acid_loading + + acid_purity = 0.93 + acid_solution_mass = pure_H2SO4 / acid_purity + water_mass = acid_solution_mass * (1 - acid_purity) + out.copy_like(feed) + out.imass['H2SO4'] = pure_H2SO4 + out.imass['H2O'] = water_mass + +# Sulfuric acid in-line mixer +@cost(basis='Flow rate', ID='Mixer', units='kg/hr', + cost=6000, S=136260, CE=CEPCI[2009], n=0.5, BM=1) +class SulfuricAcidMixer(Unit): + _N_ins = 2 + _N_outs = 1 + def _run(self): + microalgae, acid = self.ins + out = self.outs[0] + out.mix_from([microalgae, acid]) + +# Pretreatment reactor +@cost(basis='Dry flow rate', ID='Pretreatment reactor', units='kg/hr', + kW=512, cost=1981240, S=83333, CE=CEPCI[2009], n=0.6, BM=1.5) +class AcidPretreatmentReactor(Unit): + _N_ins = 1 + _N_outs = 1 + T = 121 + 273.15 # K + t = 20 / 60 # h + + microalgae_decomposition = tmo.reaction.Reaction( + 'Microalgae -> 0.5 Protein + 0.159 Lipid + 0.234 Carbohydrate + 0.107 Ash', + reactant='Microalgae', + X=1.0, + basis='wt' + ) + + def _run(self): + feed = self.ins[0] + out = self.outs[0] + out.copy_like(feed) + self.microalgae_decomposition(out) + +# pH adjustment +@cost(basis='Flow rate', ID='Mixer', units='kg/hr', + # Size basis on the total flow, not just ammonia, + # thus assuming difference caused by MWs of NH3 and NH4OH is negligible + cost=50000, S=157478, CE=CEPCI[2009], n=0.5, BM=1.5) +class NAOHMixer(Mixer): + def _run(self): + NAOH, water = self.ins + mixture = self.outs[0] + + # Make 10% solution + water.imass['Water'] = NAOH.imass['NAOH'] * (1-0.1)/0.1 + mixture.mix_from([NAOH, water]) + + +@cost(basis='Flow rate', ID='Tank', units='kg/hr', + cost=236000, S=410369, CE=CEPCI[2009], n=0.7, BM=2) +@cost(basis='Flow rate', ID='Agitator', units='kg/hr', + kW=7.457, cost=21900, S=410369, CE=CEPCI[2009], n=0.5, BM=1.5) +class NAOHAdditionTank(MixTank): + # Based on experimental data: 0.64kg NaOH consumed per ton of microalgae + naoh_mass_per_ton_microalgae = 0.64 # kg NaOH per ton microalgae + + def _run(self): + # Calculate required NaOH based on microalgae mass + if hasattr(self, 'biomass_mass') and self.biomass_mass > 0: + required_naoh = self.biomass_mass * self.naoh_mass_per_ton_microalgae / 1000 # convert to kg/hr + # Set NaOH input amount + if len(self.ins) > 0: + self.ins[0].imass['NaOH'] = required_naoh + + # Reaction definition Reactant Conversion + self.neutralization_rxn = Rxn('2 NaOH + H2SO4 -> Na2SO4 + 2 H2O', 'H2SO4', 1) + + MixTank._run(self) + self.neutralization_rxn.adiabatic_reaction(self.outs[0]) + +@cost(basis='Flow rate', ID='Pump', units='kg/hr', + kW=74.57, cost=22500, S=402194, CE=CEPCI[2009], n=0.8, BM=2.3) +class HydrolysatePump(Unit): + _units = {'Flow rate': 'kg/hr'} + _graphics = Pump._graphics + + def _design(self): + Design = self.design_results + Design['Flow rate'] = self.outs[0].F_mass + + + +# %% +# ============================================================================= +# Conversion +# ============================================================================= +# Enzyme hydrolysate mixer +@cost(basis='Flow rate', ID='Mixer', units='kg/hr', + kW=74.57, cost=109000, S=379938, CE=CEPCI[2009], n=0.5, BM=1.7) +class EnzymeHydrolysateMixer(Mixer): + _N_ins = 3 + _N_outs = 1 + + # Experimental data: 6kg GlucoAmylase and 266.7kg AlphaAmylase consumed per ton of microalgae + glucoamylase_per_ton = 6 # kg per ton microalgae + alphaamylase_per_ton = 266.7 # kg per ton microalgae + # enzyme_mass_per_ton_microalgae = 172.7 # kg enzyme per ton microalgae (deprecated) + + def _run(self): + hydrolysate, enzyme, water = self.ins + effluent = self.outs[0] + + # Calculate consumption of two enzymes based on microalgae mass + if hasattr(self, 'biomass_mass') and self.biomass_mass > 0: + required_glucoamylase = self.biomass_mass * self.glucoamylase_per_ton / 1000 # kg/hr + required_alphaamylase = self.biomass_mass * self.alphaamylase_per_ton / 1000 # kg/hr + enzyme.imass['GlucoAmylase'] = required_glucoamylase + enzyme.imass['AlphaAmylase'] = required_alphaamylase + else: + # If no biomass_mass attribute, use default method + enzyme.imass['GlucoAmylase'] = hydrolysate.F_mass * 0.006 # 0.6% + enzyme.imass['AlphaAmylase'] = hydrolysate.F_mass * 0.2667 # 26.67% + + effluent.mix_from([hydrolysate, enzyme, water]) + + + +@cost(basis='Saccharification tank size', ID='Saccharification tanks', units='kg', + cost=3840000, S=421776*24, CE=CEPCI[2009], n=0.7, BM=2) +@cost(basis='Slurry flow rate', ID='Saccharification transfer pumps', units='kg/hr', + kW=74.57, cost=47200, S=421776*24, CE=CEPCI[2009], n=0.8, BM=2.3) +class Saccharification(Unit): + _N_ins = 1 + _N_outs = 1 + _units= {'Saccharification tank size': 'kg', + 'Slurry flow rate': 'kg/hr'} + + # Residence time of countinuous saccharification tanks (hr) + tau_saccharification = 3 + + def _init(self, ID='', ins=None, outs=(), T=50+273.15): + + self.T = T + + # Based on Table 9 on Page 28 of Humbird et al. + #!!! Yalin updated the glucan-to-glucose conversion to match the table + self.saccharification_rxns = ParallelRxn([ + # Reaction definition Reactant Conversion + Rxn('Carbohydrate + H2O -> Glucose', 'Carbohydrate', 1) + ]) + + # self.saccharified_stream = Stream(None) + + def _run(self): + feed = self.ins[0] + ss= self.outs[0] + ss.copy_like(feed) + ss.T = self.T + self.saccharification_rxns(ss.mol) + neutralization_rxn = Rxn('2 NaOH + H2SO4 -> Na2SO4 + 2 H2O', 'NaOH', 1) + neutralization_rxn(ss) + + + + def _design(self): + Design = self.design_results + total_mass_flow = self.ins[0].F_mass + Design['Saccharification tank size'] = total_mass_flow * self.tau_saccharification + Design['Slurry flow rate'] = total_mass_flow + +## MCCA Fermentation +@cost(basis='Fermenter size', ID='Fermenter', units='kg', + cost=5128000, S=(42607+443391+948+116)*(60+36), + CE=CEPCI[2009], n=0.7, BM=1.5) +@cost(basis='Fermenter size', ID='Fermenter agitator', units='kg', + # Scaling basis based on sum of all streams into fermenter + # (304, 306, 311, and 312 in ref [1]) + # and total residence time (batch hydrolysis and fermentation) + # kW=268.452, + kW=0, # overwritten; power utility calculated separately based on batch IBRL power per unit volume + cost=630000, + S=(42607+443391+948+116)*(60+36), + CE=CEPCI[2009], n=0.7, BM=1.5) + +class MCCAFermentation(StirredTankReactor): + _N_ins = 5 + _N_outs = 3 + + auxiliary_unit_names = ('heat_exchanger',) + _units= { + **Reactor._units, + 'Fermenter size': 'kg', + 'Recirculation flow rate': 'kg/hr', + 'Broth flow rate': 'kg/hr', + } + + tau_batch_turnaround = 12 # in hr, seedmentaiton time + tau_cofermentation = 15 * 24 # in hr, 15 d + mol_NH4OH_per_acid_pH_control = 0.2988 # from IBRL batch run + mol_lime_per_acid_pH_control = 0.2988/2 # mol NH4OH / 2 + pH = 5.0 + + def __init__(self, ID='', ins=None, outs=(), thermo=None, *, T=37+273.15, + P=101325, V_wf=0.8, length_to_diameter=0.6, + kW_per_m3=0.02 * 0.7457 / 0.075, # ref from succinic projects + mixing_intensity=300, + #wall_thickness_factor=1, + vessel_material='Stainless steel 304', + vessel_type='Vertical', + neutralization=True, + neutralizing_agent='Lime', + mode='batch', feed_freq=1, + pH_control=True, + base_neutralizes_product=True, + tau = tau_cofermentation, + microalgae_mass_flow: float | None = None, # kg/hr basis of original algae for yield calc + titer: float = 60.0, # g/L - 默认浓度为60 g/L + # allow_dilution=False, + # allow_concentration=False, + # sugars=None + ): + + StirredTankReactor.__init__(self, ID, ins, outs) + + self.T = T + self.P = P + self.V_wf = V_wf + self.length_to_diameter = length_to_diameter + self.mixing_intensity = mixing_intensity + self.kW_per_m3 = kW_per_m3 + #self.wall_thickness_factor = wall_thickness_factor + self.vessel_material = vessel_material + self.vessel_type = vessel_type + self.neutralization = neutralization + self.mode = mode + self.feed_freq = feed_freq + self.pH_control=pH_control + self.base_neutralizes_product = base_neutralizes_product + self.neutralization = neutralization + self.neutralizing_agent = neutralizing_agent + self.tau = tau + #self.allow_dilution = allow_dilution + #self.allow_concentration = allow_concentration + #self.sugars = sugars or tuple(i.ID for i in self.chemicals.sugars) + + # Store optional explicit biomass flow for yield calculations + self.microalgae_mass_flow = microalgae_mass_flow + + # 添加titer参数 + self.titer = titer + + ID = self.ID + self._mixed_feed = Stream(f'{ID}_mixed_feed') + self._tot_feed = Stream(f'{ID}_tot_feed') + # Before reaction, after reaction, with last feed + self._single_feed0 = Stream(f'{ID}_single_feed0') + self._single_feed1 = Stream(f'{ID}_single_feed1') + self._last = Stream(f'{ID}_last') + self._init = Stream(f'{ID}_init') + hx_in = Stream(f'{ID}_hx_in') + hx_out = Stream(f'{ID}_hx_out') + # Add '.' in ID for auxiliary units + self.heat_exchanger = HXutility(ID=f'.{ID}_hx', ins=hx_in, outs=hx_out, T=T) + + # the reaction is simplified for simulation + #self.mcca_rxns = ParallelRxn([ + #Rxn('Protein -> AceticAcid', 'Protein', 0.1), + #Rxn('Protein -> PropionicAcid', 'Protein', 0.1), + #Rxn('Protein -> ButyricAcid', 'Protein', 0.1), + #Rxn('Protein -> CaproicAcid', 'Protein', 0.1), + #Rxn('Glucose -> Ethanol', 'Glucose', 0.018), + #Rxn('Glucose -> Butanol', 'Glucose', 0.0022), + #Rxn('Glucose -> AceticAcid', 'Glucose', 0.10094), + #Rxn('Glucose -> PropionicAcid', 'Glucose', 0.00977), + #Rxn('Glucose -> ValericAcid', 'Glucose', 0.012), + #Rxn('Glucose -> HeptanoicAcid', 'Glucose', 0.004), + #Rxn('Glucose -> ButyricAcid', 'Glucose', 0.122), + #Rxn('Glucose -> CaproicAcid', 'Glucose', 0.134), + #Rxn('Glucose -> CaprylicAcid', 'Glucose', 0.008), + #]) + + # This is a Siplified Neutralization, because we don't want to affect the products yield + self.lime_neutralization_rxns = ParallelRxn([ + # Reaction definition Reactant Conversion + Rxn('H2SO4 + Lime -> CaSO4 + 2 H2O', 'H2SO4', 1) + ]) + + self.lime_pH_control_rxns = ParallelRxn([ + # Reaction definition Reactant Conversion + Rxn('H2SO4 + Lime -> CaSO4 + 2 H2O', 'H2SO4', 1) + ]) + + + # self.mcca_rxns = ParallelRxn([ + # Rxn('Glucose -> 2 Ethanol + 2 CO2', 'Glucose', 0.7), + # Rxn('Glucose -> 2 AceticAcid + 2 CO2 + 2 H2', 'Glucose', 0.2), + # Rxn('Glucose -> 2 PropionicAcid + 2 CO2 + 2 H2', 'Glucose', 0.1), + # Rxn('5 PropionicAcid + 6 Ethanol -> 5 ValericAcid + AceticAcid + 4 H2O + 2 H2', 'PropionicAcid', 0.7), + # Rxn('5 ValericAcid + 6 Ethanol -> 5 HeptanoicAcid + AceticAcid + 4 H2O + 2 H2', 'ValericAcid', 0.7), + # Rxn('ButyricAcid + Ethanol -> AceticAcid + Butanol', 'ButyricAcid', 0.1), + # Rxn('4 AceticAcid + 6 Ethanol -> 5 ButyricAcid + 4 H2O + 2 H2', 'AceticAcid', 0.7), + # Rxn('5 ButyricAcid + 6 Ethanol -> 5 CaproicAcid + AceticAcid + 4 H2O + 2 H2', 'ButyricAcid', 0.7), + # Rxn('5 CaproicAcid + 6 Ethanol -> 5 CaprylicAcid + AceticAcid + 4 H2O + 2 H2', 'CaproicAcid', 0.2), + # ]) + + def _run(self): + substrate, yeast_seed, FermMicrobe, lime, n2 = self.ins + broth, gas, fermentation_waste = self.outs + broth.mix_from(self.ins) + broth.T = self.T + broth.P = self.P + # Determine basis biomass mass (kg/hr) + microalgae_mass = self.microalgae_mass_flow + + # 获取产率因子和浓度 + caproic_acid_yield_factor = getattr(self, 'caproic_acid_yield_factor', 1.0) + titer = getattr(self, 'titer', 60.0) # g/L + + # 计算浓度对产率的影响 - 非线性关系 + # 当浓度过高时,会抑制产率 + # 基准浓度设为60 g/L,此时无抑制 + base_titer = 60.0 # g/L + + # 计算抑制系数 (产物抑制模型) + # 高于基准浓度时,产率降低;低于时,产率略有提高 + if titer > base_titer: + # 产物抑制模型:exp(-k*(titer-base_titer)) + inhibition_factor = np.exp(-0.01 * (titer - base_titer)) + else: + # 低浓度时略有提高,但提高有限 + inhibition_factor = 1.0 + 0.005 * (base_titer - titer) + + # 应用抑制效应到产率因子 + effective_yield_factor = caproic_acid_yield_factor * inhibition_factor + + base_yields = { + 'Ethanol': 0.018, + 'Butanol': 0.004, + 'AceticAcid': 0.10, + 'PropionicAcid': 0.009, + 'ButyricAcid': 0.2, + 'ValericAcid': 0.012, + 'CaproicAcid': 0.27, + 'HeptanoicAcid': 0.006, + 'CaprylicAcid': 0.04 + } + + total_base_yield = sum(base_yields.values()) + base_caproic_yield = base_yields['CaproicAcid'] + + # 使用有效产率因子计算新产率 + new_caproic_yield = base_caproic_yield * effective_yield_factor + delta_caproic = new_caproic_yield - base_caproic_yield + total_other_base_yield = total_base_yield - base_caproic_yield + + # 计算发酵液体积 (L) - 用于实现目标浓度 + # 假设发酵液密度为1 kg/L + target_caproic_mass = microalgae_mass * new_caproic_yield # kg/hr + target_volume_L = target_caproic_mass * 1000 / titer # 转换为L + + # 调整产品质量以匹配目标浓度 + if abs(delta_caproic) > 1e-6 and total_other_base_yield > 1e-6: + adjustment_factor = (total_other_base_yield - delta_caproic) / total_other_base_yield + for compound, base_yield in base_yields.items(): + if compound == 'CaproicAcid': + broth.imass[compound] = target_caproic_mass + else: + broth.imass[compound] = microalgae_mass * base_yield * adjustment_factor + else: + for compound, base_yield in base_yields.items(): + if compound == 'CaproicAcid': + broth.imass[compound] = target_caproic_mass + else: + broth.imass[compound] = microalgae_mass * base_yield + + # 确保总水量与目标体积一致 + total_water_needed = max(target_volume_L - broth.F_vol, 0) # L + broth.imass['Water'] = total_water_needed # 假设密度为1 kg/L + + # 气体产物 + gas.copy_like(n2) + gas.imass['H2'] = microalgae_mass * 0.001 + gas.imass['CH4'] = microalgae_mass * 0.002 + + + def _design(self): + Design = self.design_results + Design['Fermenter size'] = self.outs[0].F_mass * self.tau + Design['Recirculation flow rate'] = self.F_mass_in + Design['Broth flow rate'] = self.outs[0].F_mass + +# %% +# ========================= +# Solid-Liquid Separation +# ========================= +@cost(basis='Flow rate', ID='Filtrate discharge pump', units='kg/hr', + # Power not specified, based on filtrate tank discharge pump + kW=55.9275, cost=13040, S=31815, CE=CEPCI[2010], n=0.8, BM=2.3) +@cost(basis='Flow rate', ID='Filtrate tank', units='kg/hr', + cost=103000, S=31815, CE=CEPCI[2010], n=0.7, BM=2.0) +@cost(basis='Flow rate', ID='Flitrate tank agitator', units='kg/hr', + kW=5.59275, cost=26000, S=337439, CE=CEPCI[2009], n=0.5, BM=1.5) +# ref from Biorefineries.HP +class SolidLiquidSeparation(Unit): + _N_ins = 1 + _N_outs = 2 + def _run(self): + feed = self.ins[0] + filtrate, residue = self.outs + residue.empty() + insolubles = ('Protein', 'Lipid', 'Ash', 'TriOlein', 'AlphaAmylase', 'GlucoAmylase', 'Yeast') + for comp in feed.chemicals.IDs: + if comp in insolubles: + residue.imass[comp] = feed.imass[comp] + filtrate.imass[comp] = 0 + else: + filtrate.imass[comp] = feed.imass[comp] + residue.imass[comp] = 0 + + filtrate.T = feed.T + filtrate.P = feed.P + residue.T = feed.T + residue.P = feed.P + + + +# %% +# ========================= +# Distillation Units +# ========================= +@cost(basis='Flow rate', ID='Butanol Distillation', units='kg/hr', cost=2400000, S=10000, CE=CEPCI[2019], n=0.6, BM=2.5) +class ButanolDistillation(bst.units.BinaryDistillation): + def __init__(self, ID, ins=None, outs=()): + super().__init__( + ID, ins=ins, outs=outs, + LHK=('Butanol', 'Octanol'), + Lr=0.99, Hr=0.99, k=1.2, + partial_condenser=False, + is_divided=True + ) + +@cost(basis='Flow rate', ID='Butyric Acid Distillation', units='kg/hr', cost=2400000, S=10000, CE=CEPCI[2019], n=0.6, BM=2.5) +class ButyricAcidDistillation(bst.units.BinaryDistillation): + def __init__(self, ID, ins=None, outs=()): + super().__init__( + ID, ins=ins, outs=outs, + LHK=('ButyricAcid', 'Octanol'), + Lr=0.99, Hr=0.99, k=1.2, + partial_condenser=False, + is_divided=True + ) + +@cost(basis='Flow rate', ID='Caproic Acid Distillation', units='kg/hr', cost=2400000, S=10000, CE=CEPCI[2019], n=0.6, BM=2.5) +class CaproicAcidDistillation(bst.units.BinaryDistillation): + def __init__(self, ID, ins=None, outs=()): + super().__init__( + ID, ins=ins, outs=outs, + LHK=('CaproicAcid', 'Octanol'), + Lr=0.99, Hr=0.99, k=1.2, + partial_condenser=False, + is_divided=True + ) + +@cost(basis='Flow rate', ID='Heptanoic Acid Distillation', units='kg/hr', cost=2400000, S=10000, CE=CEPCI[2019], n=0.6, BM=2.5) +class HeptanoicAcidDistillation(bst.units.BinaryDistillation): + def __init__(self, ID, ins=None, outs=()): + super().__init__( + ID, ins=ins, outs=outs, + LHK=('HeptanoicAcid', 'Octanol'), + Lr=0.99, Hr=0.99, k=1.2, + partial_condenser=False, + is_divided=True + ) + +@cost(basis='Flow rate', ID='Caprylic Acid Distillation', units='kg/hr', cost=2400000, S=10000, CE=CEPCI[2019], n=0.6, BM=2.5) +class CaprylicAcidDistillation(bst.units.BinaryDistillation): + def __init__(self, ID, ins=None, outs=()): + super().__init__( + ID, ins=ins, outs=outs, + LHK=('CaprylicAcid', 'Octanol'), + Lr=0.99, Hr=0.99, k=1.2, + partial_condenser=False, + is_divided=True, + product_specification_format='Recovery' + ) + +# %% +# ========================= +# Other Units +# ========================= +@cost(basis='Flow rate', ID='NeutralizationTank', units='kg/hr', + # Size basis on the total flow, not just ammonia, + # thus assuming difference caused by MWs of NH3 and NH4OH is negligible + cost=5000, S=157478, CE=CEPCI[2009], n=0.5, BM=1.5) +class NeutralizationTank(Unit): + _N_ins = 2 + _N_outs = 1 + def _run(self): + acid_stream, ammonium_hydroxide = self.ins + self.outs[0].empty() + self.outs[0].mix_from([acid_stream, ammonium_hydroxide]) + neutralization_rxn = rxn.Rxn('2 NH4OH + H2SO4 -> AmmoniumSulfate + 2 H2O', 'NH4OH', 1) + neutralization_rxn(self.outs[0]) + + + + +@cost('Flow rate', 'Anaerobic Digestion Reactor', units= 'kg/hr', kW=4.4, S=339151, + cost=8000000, CE=CEPCI[2010], n=0.7, BM=1.8) +class AnaerobicDigestion(bst.Unit): + def __init__(self, ID='', ins=None, outs=(), *, tau=15*24, microalgae_mass: float|None=None, **kwargs): + super().__init__(ID, ins, outs, **kwargs) + self.tau = tau + self.microalgae_mass = microalgae_mass + _N_ins = 1 + _N_outs = 3 + + #: Working volume fraction (filled tank to total tank volume) + V_wf = 0.8 + + #: Number of reactors + N_reactors = 0 + + #: Number of transfer pumps + N_transfer_pumps = 1 + + #: Number of recirculation pumps + N_recirculation_pumps = 0 + T = 37 + 273.15 + + degrade_IDs = [ + 'AceticAcid', 'PropionicAcid', 'ButyricAcid', 'ValericAcid', + 'CaproicAcid', 'HeptanoicAcid', 'CaprylicAcid', + 'Ethanol', 'Butanol', 'Glucose', 'Protein', + 'AlphaAmylase', 'GlucoAmylase', 'TriOlein', 'WWTsludge' + ] + inorganic_IDs = ['Ash', 'H2SO4', 'Na2SO4', 'AmmoniumSulfate', 'H2O'] + + def _run(self): + feed = self.ins[0] + biogas, waste, sludge = self.outs + if self.microalgae_mass is None: + raise ValueError('microalgae_mass must be supplied when instantiating AnaerobicDigestion.') + microalgae_mass = self.microalgae_mass + # 100ml H2, 100 mL CH4 per 1g microalgae + # 1 mol CH4 = 22.4L = 22400ml,1 mol H2 = 22.4L = 22400ml + H2_mol = microalgae_mass * 100 / 22400 # 单位:g * ml/g / ml/mol = mol + CH4_mol = microalgae_mass * 200 / 22400 + biogas.empty() + biogas.imol['H2'] = H2_mol + biogas.imol['CH4'] = CH4_mol + biogas.phase = 'g' + biogas.T = self.T + waste.empty() + sludge.empty() + for ID in self.degrade_IDs: + amount = feed.imass[ID] + waste.imass[ID] = amount * 0.02 # 2% into wastewater, 98% remove rate + sludge.imass[ID] = amount * 0.25 # 40% into sludge + for ID in self.inorganic_IDs: + amount = feed.imass[ID] + waste.imass[ID] = amount * 0.95 + sludge.imass[ID] = amount * 0.05 + sludge.imass['H2O'] = feed.imass['H2O']*0.3 + waste.imass['H2O'] = feed.imass['H2O']*0.7 + sludge.imass['Microalgae'] = 0 + waste.phase = 'l' + waste.T = sludge.T = self.T + + + + +__all__ = ( + 'FeedstockPreprocessing', 'AcidPretreatmentReactor', 'GlucoamylaseHydrolysis', + 'AlphaAmylaseHydrolysis', 'SolidLiquidSeparation', 'MCCAFermentation', + 'MembraneExtraction', 'OctanolExtraction', 'MultiStageDistillation', + 'NeutralizationTank' +) + + diff --git a/biorefineries/microalgae/utils.py b/biorefineries/microalgae/utils.py new file mode 100644 index 00000000..cd177ad0 --- /dev/null +++ b/biorefineries/microalgae/utils.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Sat July 07 13:15:00 2025 + +Microalgae biorefinery to produce medium chain fatty acids +by anaerobic fermentation without external electron donor addition + +References +---------- +[1] BioSTEAM Documentation: + https://biosteam.readthedocs.io/en/latest/API/thermosteam/Chemicals.html +[2] Cortes-Peña et al., BioSTEAM: A Fast and Flexible Platform for the Design, + Simulation, and Techno-Economic Analysis of Biorefineries under Uncertainty. + ACS Sustainable Chem. Eng. 2020, 8 (8), 3302–3310. +[3] 3-Hydroxypropionic acid biorefineries project: + https://github.com/BioSTEAMDevelopmentGroup/Bioindustrial-Park/tree/master/biorefineries/HP + +@author: Xingdong Shi +@version: 0.0.1 +""" + +import numpy as np +import pandas as pd +import thermosteam as tmo +from ._chemicals import chems + +_kg_per_ton = 907.18474 +# Baseline feedstock flow rate +def get_feedstock_flow(dry_composition, moisture_content, dry_flow): + dry_array = chems.kwarray(dry_composition) + wet_flow = dry_flow / (1-moisture_content) + moisture_array = chems.kwarray(dict(Water=moisture_content)) + feedstock_flow = wet_flow * (dry_array*(1-moisture_content)+moisture_array) + return feedstock_flow + +# !!! This is the dry composition of corn stover; update to the composition you need +dry_composition = dict( + Carbohydrate=0.234, Lipid=0.059, Protein=0.5369, Ash=0.0512, Extract=0.1189) + +moisture_content = 0.05 #!!! This is the moisture content of preprocessed corn stover; update to the moisture content you need +dry_feedstock_flow = 2205. * _kg_per_ton / 24. # !!! Changing this will alter the total flow without altering the composition + +baseline_feedflow = get_feedstock_flow(dry_composition, moisture_content, + dry_feedstock_flow) + +# Price dictionary for all relevant chemicals and products +price = { + 'GlucoAmylase': 6.16, # $/kg (NREL 2018) + 'AlphaAmylase': 6.16, # $/kg (NREL 2018) + 'Yeast': 2, # $/kg Alibaba + 'Ethanol': 1.78, # $/kg https://catcost.chemcatbio.org/materials-library + 'Octanol': 1.00, # $/lb https://web.archive.org/web/20161125084558/http://www.icis.com:80/chemicals/channel-info-chemicals-a-z/ + 'AceticAcid': 1.31, # $/kg https://catcost.chemcatbio.org/materials-library + 'ButyricAcid': 1.72, # $/kg https://www.imarcgroup.com/butyric-acid-pricing-report? + 'CaproicAcid': 2.89, # $/kg Increasing the economic value of lignocellulosic stillage through medium-chain fatty acid production + 'Butanol': 2.27, # $/kg https://catcost.chemcatbio.org/materials-library + #'Butanol': 0.7, # $/lb https://web.archive.org/web/20161125084558/http://www.icis.com:80/chemicals/channel-info-chemicals-a-z/ + 'HeptanoicAcid': 3.17, # $/kg https://www.expertmarketresearch.com/prefeasibility-reports/heptanoic-acid-manufacturing-plant-project-report + 'CaprylicAcid': 5.07, # $/kg Increasing the economic value of lignocellulosic stillage through medium-chain fatty acid production + 'SulfuricAcid': 0.11, # $/kg https://catcost.chemcatbio.org/materials-library + 'Lime': 0.12, # https://catcost.chemcatbio.org/materials-library + 'AmmoniumHydroxide': 0.204, # BDO, succinic, HP program + 'NaOH': 1.01, #https://catcost.chemcatbio.org/materials-library +} + +# Chemical Engineering Plant Cost Index from Chemical Engineering Magzine +# (https://www.chemengonline.com/the-magazine/) +CEPCI = {1997: 386.5, + 1998: 389.5, + 2007: 525.4, + 2009: 521.9, + 2010: 550.8, + 2011: 585.7, + 2012: 584.6, + 2013: 567.3, + 2014: 576.1, + 2016: 541.7, + 2017: 567.5, + 2018: 603.1, + 2019: 607.5, + 2020: 596.2, + 2021: 708.8, + 2022: 816.0, + 2023: 797.9} + + + + + diff --git a/biorefineries/microalgae/vis.py b/biorefineries/microalgae/vis.py new file mode 100644 index 00000000..889e356b --- /dev/null +++ b/biorefineries/microalgae/vis.py @@ -0,0 +1,797 @@ +""" +Created on Thu Jun 20 15:30:00 2024 + +Visualization module for Microalgae biorefinery to produce medium chain fatty acids +This script creates technical, economic and environmental figures for the system + +@author: Xingdong Shi +""" + +import os +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt + +import biosteam as bst +import thermosteam as tmo +from biosteam.evaluation import Model, Metric +from chaospy import distributions as shape + +from .lca import create_microalgae_lca +from .system import create_microalgae_MCCA_production_sys, compute_labor_cost +from .tea import create_tea + +# Set style for publication quality plots +plt.rcParams['font.family'] = 'sans-serif' +plt.rcParams['font.sans-serif'] = ['Arial'] +plt.rcParams['font.size'] = 8 +plt.rcParams['axes.linewidth'] = 0.5 +plt.rcParams['axes.titlesize'] = 10 +plt.rcParams['axes.labelsize'] = 9 +plt.rcParams['xtick.labelsize'] = 8 +plt.rcParams['ytick.labelsize'] = 8 +plt.rcParams['legend.fontsize'] = 7 + +def calculate_breakdown_data(tea_obj, system): + """从TEA对象和系统中提取实际的成本分布数据""" + + try: + # 总投资成本 + total_TCI = tea_obj.TCI + total_VOC = tea_obj.VOC + total_FOC = tea_obj.FOC + + # 按类别分组单元 + feedstock_units = [] + conversion_units = [] + separation_units = [] + wastewater_units = [] + facilities_units = [] + + # 分类系统中的单元 + for unit in system.units: + unit_name = unit.__class__.__name__ + unit_id = unit.ID + + if 'U1' in unit_id or 'feed' in unit_id.lower(): + feedstock_units.append(unit) + elif 'R3' in unit_id or 'ferment' in unit_id.lower(): + conversion_units.append(unit) + elif any(x in unit_id for x in ['S3', 'D3', 'F3', 'M3']): + separation_units.append(unit) + elif 'waste' in unit_id.lower() or 'W' in unit_id: + wastewater_units.append(unit) + else: + facilities_units.append(unit) + + # 计算各类别的设备成本 + feedstock_cost = sum(getattr(unit, 'purchase_cost', 0) for unit in feedstock_units) + conversion_cost = sum(getattr(unit, 'purchase_cost', 0) for unit in conversion_units) + separation_cost = sum(getattr(unit, 'purchase_cost', 0) for unit in separation_units) + wastewater_cost = sum(getattr(unit, 'purchase_cost', 0) for unit in wastewater_units) + facilities_cost = sum(getattr(unit, 'purchase_cost', 0) for unit in facilities_units) + + # 归一化为百分比 + total_equipment_cost = feedstock_cost + conversion_cost + separation_cost + wastewater_cost + facilities_cost + if total_equipment_cost > 0: + equipment_costs = { + 'Feedstock': feedstock_cost / total_equipment_cost * 100, + 'Conversion': conversion_cost / total_equipment_cost * 100, + 'Separation': separation_cost / total_equipment_cost * 100, + 'Wastewater': wastewater_cost / total_equipment_cost * 100, + 'Cooling utility facilities': 0, + 'Boiler & turbogenerator': 0, + 'Other facilities': facilities_cost / total_equipment_cost * 100, + 'Heat exchanger network': 0, + 'Natural gas (steam)': 0, + 'Natural gas (drying)': 0, + 'Fixed operating costs': 0, + 'Credits': 0 + } + else: + # 回退到估算值 + equipment_costs = {k: np.nan for k in [ + 'Feedstock','Conversion','Separation','Wastewater', + 'Cooling utility facilities','Boiler & turbogenerator','Other facilities', + 'Heat exchanger network','Natural gas (steam)','Natural gas (drying)', + 'Fixed operating costs','Credits']} + + # 计算公用设施消耗 + total_cooling = total_heating = total_electricity = 0 + + for unit in system.units: + # 冷却负荷 + if hasattr(unit, 'heat_utilities'): + for hu in unit.heat_utilities: + if hu.duty < 0: + total_cooling += abs(hu.duty) + else: + total_heating += hu.duty + + # 电力消耗 + if hasattr(unit, 'power_utility') and unit.power_utility: + total_electricity += abs(unit.power_utility.power) + + # 按单元类型分配公用设施负荷 (简化分配) + cooling_duty = {k: np.nan for k in equipment_costs} + + heating_duty = {k: np.nan for k in equipment_costs} + + electricity = {k: np.nan for k in equipment_costs} + + # 运营成本分配 (基于FOC和VOC) + operating_cost = {k: np.nan for k in equipment_costs} + + # 输出实际数值供参考 + print(f"实际TEA数据:") + print(f" 总投资成本 (TCI): ${total_TCI/1e6:.1f} M") + print(f" 年度变动成本 (VOC): ${total_VOC/1e6:.1f} M/y") + print(f" 年度固定成本 (FOC): ${total_FOC/1e6:.1f} M/y") + print(f" 总加热负荷: {total_heating/1e6:.1f} MW") + print(f" 总冷却负荷: {total_cooling/1e6:.1f} MW") + print(f" 总电力消耗: {total_electricity/1e3:.1f} MW") + + except Exception as e: + print(f"获取实际成本数据时出错: {str(e)}") + + return { + 'Installed\nequipment\ncost': equipment_costs, + 'Cooling\nduty': cooling_duty, + 'Heating\nduty': heating_duty, + 'Electricity\nconsumption': electricity, + 'Operating\ncost': operating_cost + } + + +def create_plots_from_monte_carlo(save_path=None): + """Create and save publication-quality figures for microalgae MCCA system using Monte Carlo results.""" + + + try: + bst.main_flowsheet.clear() + bst.main_flowsheet.set_flowsheet('_') + + from microalgae._chemicals import chems + tmo.settings.set_thermo(chems) + except: + pass + + # 检查results目录是否存在,如果不存在则创建 + os.makedirs('results', exist_ok=True) + + # 定义结果文件路径 + mc_file_path = 'results/monte_carlo_results.csv' + + # 移除所有缓存检查,直接运行新模拟 + print("运行新的Monte Carlo模拟...") + # 初始化系统 + microalgae_sys = create_microalgae_MCCA_production_sys() + microalgae_sys.simulate() + + # 创建TEA + u = microalgae_sys.flowsheet.unit + s = microalgae_sys.flowsheet.stream + dry_tpd = u.U101.ins[0].F_mass * 24 / 1000 # kg/h -> t/d + microalgae_tea = create_tea( + system=microalgae_sys, + IRR=0.10, + duration=(2024, 2045), + depreciation='MACRS7', + income_tax=0.21, + operating_days=330, + lang_factor=None, + construction_schedule=(0.08, 0.60, 0.32), + startup_months=3, + startup_FOCfrac=1, + startup_salesfrac=0.5, + startup_VOCfrac=0.75, + WC_over_FCI=0.05, + finance_interest=0.08, + finance_years=10, + finance_fraction=0.4, + OSBL_units=(u.CT, u.CWP, u.ADP, u.PWC), + warehouse=0.04, + site_development=0.09, + additional_piping=0.045, + proratable_costs=0.10, + field_expenses=0.10, + construction=0.20, + contingency=0.10, + other_indirect_costs=0.10, + labor_cost=max(0.5e6, compute_labor_cost(dry_tpd)), + labor_burden=0.90, + property_insurance=0.007, + maintenance=0.03, + boiler_turbogenerator=None, + steam_power_depreciation='MACRS20' + ) + + # 设置主产品和相关变量,用于后续LCA计算 + main_product = s.caproic_acid_product + main_product_chemical_IDs = ['CaproicAcid'] + + # 尝试找到锅炉单元 + boiler = None + for unit in microalgae_sys.units: + if hasattr(unit, 'heat_utilities') and unit.heat_utilities: + boiler = unit + break + + # 如果没有找到锅炉,使用热交换器 + if boiler is None: + for unit in microalgae_sys.units: + if unit.__class__.__name__ == 'HXutility': + boiler = unit + break + + # 创建评估模型 + model = Model(microalgae_sys) + + # 定义setter函数 + def set_microalgae_price(price): + u.U101.ins[0].price = price + + def set_caproic_acid_price(price): + s.caproic_acid_product.price = price + + def set_caproic_acid_yield_factor(factor): + u.R301.caproic_acid_yield_factor = factor + + def set_operating_days(days): + microalgae_tea.operating_days = days + + def set_maintenance_factor(factor): + microalgae_tea.maintenance = factor + + # 定义指标 + model.metrics = [ + Metric('ROI', lambda: microalgae_tea.ROI * 100, '%'), + Metric('MSP', lambda: microalgae_tea.solve_price(s.caproic_acid_product), '$/kg'), + Metric('TCI', lambda: microalgae_tea.TCI, '$'), + Metric('VOC', lambda: microalgae_tea.VOC, '$'), + Metric('FOC', lambda: microalgae_tea.FOC, '$'), + Metric('GWP', lambda: create_microalgae_lca(system=microalgae_sys, + main_product=main_product, + main_product_chemical_IDs=main_product_chemical_IDs, + boiler=boiler).GWP, 'kg CO2 eq./kg') + ] + + # 注册参数 + model.parameter(setter=set_microalgae_price, element=u.U101.ins[0], + name='Microalgae Price', kind='isolated', + units='$/kg', distribution=shape.Triangle(-0.2, -0.1, 0.1)) + + model.parameter(setter=set_caproic_acid_price, element=s.caproic_acid_product, + name='Caproic Acid Price', kind='isolated', + units='$/kg', distribution=shape.Triangle(2.0, 2.89, 4.0)) + + model.parameter(setter=set_caproic_acid_yield_factor, element=u.R301, + name='Caproic Acid Yield Factor', kind='isolated', + distribution=shape.Triangle(0.7, 1.0, 1.3)) + + model.parameter(setter=set_operating_days, element=microalgae_tea, + name='Operating Days', kind='isolated', + distribution=shape.Triangle(300, 330, 350)) + + model.parameter(setter=set_maintenance_factor, element=microalgae_tea, + name='Maintenance Factor', kind='isolated', + distribution=shape.Triangle(0.02, 0.03, 0.05)) + + # 运行Monte Carlo模拟 + n_samples = 10 + + # 获取参数分布 + param_distributions = {} + for param in model.parameters: + if hasattr(param, 'distribution'): + param_distributions[param.index] = param.distribution + + # 获取指标名称映射 + metric_names = {} + for i, metric in enumerate(model.metrics): + metric_names[i] = metric.name + + # 获取基准样本作为模板 + baseline = model.get_baseline_sample() + results = [] + + # 生成样本并评估 + for i in range(n_samples): + if i % 100 == 0 and i > 0: + print(f"Completed {i}/{n_samples} samples") + + # 复制基准样本 + sample = baseline.copy() + + # 为每个参数生成随机值 + for param_name, distribution in param_distributions.items(): + # 简化的拉丁超立方抽样 + u_val = (i + 0.5) / n_samples + if hasattr(distribution, 'ppf'): + sample[param_name] = distribution.ppf(u_val) + else: + sample[param_name] = distribution.sample() + + # 评估样本 + try: + result_series = model(sample) + + # 创建包含参数和指标值的字典 + result_dict = {} + + # 添加参数值 + for param_name in param_distributions.keys(): + result_dict[param_name] = sample[param_name] + + # 添加指标值 + for i, name in metric_names.items(): + result_dict[name] = result_series.iloc[i] + + # 保障 GWP 指标存在(某些情况下 Metric 计算可能失败) + if 'GWP' not in result_dict or np.isnan(result_dict['GWP']): + try: + lca_tmp = create_microalgae_lca(system=microalgae_sys, + main_product=main_product, + main_product_chemical_IDs=main_product_chemical_IDs, + boiler=boiler) + result_dict['GWP'] = lca_tmp.GWP + except Exception as _: + result_dict['GWP'] = np.nan + + results.append(result_dict) + except Exception as e: + print(f"Sample {i} evaluation failed: {str(e)}") + + # 组合结果 + monte_carlo_results = pd.DataFrame(results) + + # 保存结果 + monte_carlo_results.to_csv(mc_file_path, index=False) + + # ========================================================================= + # FIGURE A: MFSP comparison across different scales (Monte Carlo based) + # ========================================================================= + fig = plt.figure(figsize=(7, 8)) + + # A: MFSP across different scales + ax1 = plt.subplot2grid((3, 2), (0, 0)) + + # 使用 Monte Carlo 样本结果绘制 MFSP 分布 (均值 ± 标准差) + if 'MSP' in monte_carlo_results.columns: + mfsp_series = monte_carlo_results['MSP'].dropna() + mean_mfsp = mfsp_series.mean() + std_mfsp = mfsp_series.std() + ax1.bar(['Monte Carlo'], [mean_mfsp], yerr=[std_mfsp], color='#A52A2A', alpha=0.7, width=0.5) + # 价格范围背景 + ax1.axhspan(2.5, 3.5, alpha=0.3, color='gray', label='Bio-based MFSP range') + ax1.axhspan(1.0, 2.5, alpha=0.2, color='gray', label='Market price range') + ax1.set_ylim(0, max(5.0, mean_mfsp*1.5)) + else: + ax1.text(0.5, 0.5, 'No MSP data', ha='center', va='center') + + ax1.set_ylabel('MFSP [$kg^{-1}$]') + ax1.set_title('MFSP (Monte Carlo mean ± std)') + ax1.set_xticks([]) + + legend_elements = [ + plt.Rectangle((0,0), 1, 1, fc='gray', alpha=0.3, label='Bio-based MFSP range'), + plt.Rectangle((0,0), 1, 1, fc='gray', alpha=0.2, label='Market price range') + ] + ax1.legend(handles=legend_elements, loc='upper left', bbox_to_anchor=(1.05, 1), fontsize=7) + + # Cost and utility breakdown chart - 使用计算数据 + ax2 = plt.subplot2grid((3, 2), (0, 1)) + + # 通过计算获取实际的成本和公用设施数据 + # 获取分类数据 + breakdown_data = calculate_breakdown_data(microalgae_tea, microalgae_sys) + + # 定义类别 + categories = ['Installed\nequipment\ncost', 'Cooling\nduty', 'Heating\nduty', 'Electricity\nconsumption', 'Operating\ncost'] + + # 创建数据字典 + data = {} + for key in list(breakdown_data.values())[0].keys(): # 使用第一个字典的键 + data[key] = [] + for category in categories: + data[key].append(breakdown_data[category][key]) + + # 创建DataFrame用于绘图 + df = pd.DataFrame(data, index=categories) + + # 保存成本与公用设施百分比分布到 CSV + try: + df.to_csv(os.path.join('results', 'cost_utility_breakdown_percent.csv')) + except Exception as _: + pass + + # 绘制堆叠条形图 + bottom = np.zeros(len(categories)) + colors = ['#FADBD8', '#A52A2A', '#641E16', '#FFFFFF', '#EAECEE', '#D5D8DC', + '#85929E', '#2C3E50', '#1C2833', '#17202A', '#7D3C98', '#FFFFFF'] + hatches = ['', '', '', '', '', '', '', '', '', '', '', '////'] + + for i, (col, color, hatch) in enumerate(zip(df.columns, colors, hatches)): + ax2.bar(categories, df[col], bottom=bottom, label=col, color=color, hatch=hatch) + bottom += df[col] + + # 计算每个类别的总和和单位 - 使用实际数据 + try: + # 从TEA获取实际数值 + actual_TCI = microalgae_tea.TCI / 1e6 # M$ + actual_heating = sum(abs(hu.duty) for unit in microalgae_sys.units + if hasattr(unit, 'heat_utilities') + for hu in unit.heat_utilities if hu.duty > 0) / 1e6 # MW + actual_cooling = sum(abs(hu.duty) for unit in microalgae_sys.units + if hasattr(unit, 'heat_utilities') + for hu in unit.heat_utilities if hu.duty < 0) / 1e6 # MW + actual_electricity = sum(abs(unit.power_utility.power) for unit in microalgae_sys.units + if hasattr(unit, 'power_utility') and unit.power_utility) / 1e3 # MW + actual_operating = (microalgae_tea.VOC + microalgae_tea.FOC) / 1e6 # M$/y + + sum_values = [actual_TCI, actual_cooling, actual_heating, actual_electricity, actual_operating] + except Exception as e: + print(f"[WARNING] Failed to fetch aggregate totals: {e}") + sum_values = [np.nan]*5 + + units = ['[$10^6$ $]', '[MW]', '[MW]', '[MW]', '[$10^6$ $/y]'] + + # 添加总和值和单位 + for i, category in enumerate(categories): + if i < len(sum_values) and not np.isnan(sum_values[i]): + plt.text(i, 105, f'sum: {sum_values[i]:.1f}', ha='center', fontsize=6) + plt.text(i, 112, units[i], ha='center', fontsize=6) + + ax2.set_ylim(-40, 120) + ax2.set_ylabel('Cost and Utility Breakdown [%]') + # 将图例移到图右侧 + ax2.legend(loc='upper left', bbox_to_anchor=(1.05, 1.0), fontsize=5) + + # ========================================================================= + # FIGURE B: GWP comparison and breakdown (使用LCA计算) + # ========================================================================= + + # B: GWP across different scales - 使用LCA计算的实际数据 + ax3 = plt.subplot2grid((3, 2), (1, 0)) + + # 使用与MFSP相同的条件计算GWP值 + # 这里复用之前设置的conditions + if 'GWP' in monte_carlo_results.columns: + gwp_series = monte_carlo_results['GWP'].dropna() + mean_gwp = gwp_series.mean() + std_gwp = gwp_series.std() + ax3.bar(['Monte Carlo'], [mean_gwp], yerr=[std_gwp], color='#A52A2A', alpha=0.7, width=0.5) + ax3.set_ylim(0, max(5.0, mean_gwp*1.3)) + else: + ax3.text(0.5,0.5,'No GWP data',ha='center',va='center') + ax3.set_ylim(0,5) + + # 添加参考线 - 行业基准值 + f1_value = 3.1 # 行业平均值示例 + f2_value = 1.7 # 行业最佳实践示例 + ax3.axhline(y=f1_value, linestyle='--', color='black', linewidth=0.5) # f1 + ax3.axhline(y=f2_value, linestyle='--', color='black', linewidth=0.5) # f2 + + # 文字标注 + ax3.text(2.1, f1_value, '$f_1$', fontsize=8) + ax3.text(2.1, f2_value, '$f_2$', fontsize=8) + + ax3.set_ylabel('GWP$_{100}$ [kg CO$_2$ eq. kg$^{-1}$]') + + # GWP breakdown chart - 使用LCA的实际数据 + ax4 = plt.subplot2grid((3, 2), (1, 1)) + + # 从LCA获取GWP明细 + # 重新计算当前状态的LCA + lca = create_microalgae_lca( + system=microalgae_sys, + main_product=main_product, + main_product_chemical_IDs=main_product_chemical_IDs, + boiler=boiler + ) + + # 将数据转换为百分比的字典 + gwp_total = abs(lca.GWP) if lca.GWP != 0 else 1.0 # 避免除零错误 + gwp_data = { + 'Feedstock': [max(0, lca.feedstock_GWP/gwp_total*100)], + 'Material Inputs': [max(0, lca.material_GWP/gwp_total*100)], + 'Electricity': [max(0, lca.net_electricity_GWP/gwp_total*100)], + 'Direct Emissions': [max(0, lca.direct_non_biogenic_emissions_GWP/gwp_total*100)], + } + + # 如果有负值(碳捕获),添加Credits类别 + if lca.feedstock_GWP < 0: + gwp_data['Credits'] = [min(0, lca.feedstock_GWP/gwp_total*100)] # 负值作为贷项 + else: + gwp_data['Credits'] = [0] + + # 创建DataFrame以进行绘图 + gwp_df = pd.DataFrame(gwp_data) + + # 保存 GWP breakdown 百分比到 CSV + try: + gwp_df.to_csv(os.path.join('results', 'gwp_breakdown_percent.csv'), index=False) + except Exception as _: + pass + + # 绘制堆叠条形图 - GWP + gwp_bottom = np.zeros(1) + gwp_colors = ['#FADBD8', '#A52A2A', '#1C2833', '#85929E', '#FFFFFF'] + gwp_hatches = ['', '', '', '/////', '////'] + + for i, (col, color, hatch) in enumerate(zip(gwp_df.columns, gwp_colors, gwp_hatches)): + ax4.bar(['GWP$_{100}$ Breakdown'], gwp_df[col], bottom=gwp_bottom, label=col, color=color, hatch=hatch) + gwp_bottom += gwp_df[col] + + ax4.set_ylim(-40, 100) + ax4.set_ylabel('GWP$_{100}$ Breakdown [%]') + # 将图例移到图右侧 + ax4.legend(loc='upper left', bbox_to_anchor=(1.05, 1.0), fontsize=5) + + # 添加脚注文本 - 基于实际LCA数据 + footnote = "*Feedstock (microalgae) farming, harvesting,\n"\ + "transportation, storage, and handling. Credit for\n"\ + f"fixed carbon: {lca.feedstock_GWP:.2f} kg CO2 eq./kg.\n"\ + "Sum of direct biogenic emissions: "\ + f"{lca.biogenic_emissions_GWP:.2f} kg CO2 eq./kg." + plt.figtext(0.1, 0.02, footnote, fontsize=7, ha='left') + + # ========================================================================= + # FIGURES C-F: Contour plots for yield vs titer (使用实际计算数据) + # ========================================================================= + + # 定义产率和浓度的网格点 - 与组合图保持一致 + yields = np.linspace(30, 80, 10) # % theoretical - 与组合图一致 + titers = np.linspace(30, 100, 10) # g/L - 与组合图一致 + + # 创建数据存储数组 + Z_neutral_mfsp = np.zeros((len(titers), len(yields))) + Z_lowph_mfsp = np.zeros((len(titers), len(yields))) + Z_neutral_gwp = np.zeros((len(titers), len(yields))) + Z_lowph_gwp = np.zeros((len(titers), len(yields))) + + # 创建网格 - 与组合图一致 + Y, T = np.meshgrid(yields, titers) + + # 保存原始参数 + original_yield_factor = u.R301.caproic_acid_yield_factor if hasattr(u.R301, 'caproic_acid_yield_factor') else 1.0 + original_titer = u.R301.titer if hasattr(u.R301, 'titer') else 60.0 + + print("开始计算等高线图数据...") + # 计算每个网格点的MFSP和GWP值 + for i, titer in enumerate(titers): + for j, yield_pct in enumerate(yields): + try: + # 改进产率因子转换公式 - 更合理地映射产率到产率因子 + yield_factor = 0.7 + (yield_pct - 30) / (80 - 30) * 0.6 # 映射到0.7-1.3范围 + + # 设置参数 + if hasattr(u.R301, 'caproic_acid_yield_factor'): + u.R301.caproic_acid_yield_factor = yield_factor + + # 设置滴度参数 + if hasattr(u.R301, 'titer'): + u.R301.titer = titer + + # 中性发酵条件 + microalgae_sys.simulate() + + # 创建LCA计算GWP + try: + neutral_lca = create_microalgae_lca( + system=microalgae_sys, + main_product=main_product, + main_product_chemical_IDs=main_product_chemical_IDs, + boiler=boiler + ) + neutral_gwp = neutral_lca.GWP + except Exception as e: + # 如果LCA计算失败,使用简单估算模型 + print(f"LCA计算失败:{str(e)}") + raise + + # 计算MFSP + mfsp = microalgae_tea.solve_price(s.caproic_acid_product) + Z_neutral_mfsp[i, j] = mfsp + Z_neutral_gwp[i, j] = neutral_gwp + + # 低pH发酵条件 (假设pH对产率有5%的影响) + if hasattr(u.R301, 'caproic_acid_yield_factor'): + u.R301.caproic_acid_yield_factor = yield_factor * 0.95 + + microalgae_sys.simulate() + + # 创建LCA计算GWP + try: + lowph_lca = create_microalgae_lca( + system=microalgae_sys, + main_product=main_product, + main_product_chemical_IDs=main_product_chemical_IDs, + boiler=boiler + ) + lowph_gwp = lowph_lca.GWP + except Exception as e: + # 如果LCA计算失败,使用简单估算模型 + print(f"低pH LCA计算失败:{str(e)}") + raise + + mfsp_lowph = microalgae_tea.solve_price(s.caproic_acid_product) + Z_lowph_mfsp[i, j] = mfsp_lowph + Z_lowph_gwp[i, j] = lowph_gwp + + # 仅显示部分计算结果以减少输出 + if (i == 0 or i == len(titers)-1) and (j == 0 or j == len(yields)-1): + print(f"计算完成: 浓度={titer:.1f}, 产率={yield_pct:.1f}%, 产率因子={yield_factor:.3f}, MFSP={mfsp:.2f}, GWP={neutral_gwp:.2f}") + + except Exception as e: + print(f"[WARNING] Convergence failure (T={titer:.1f}, Y={yield_pct:.1f}%): {e}") + Z_neutral_mfsp[i, j] = np.nan + Z_lowph_mfsp[i, j] = np.nan + Z_neutral_gwp[i, j] = np.nan + Z_lowph_gwp[i, j] = np.nan + + # 恢复原始参数 + if hasattr(u.R301, 'caproic_acid_yield_factor'): + u.R301.caproic_acid_yield_factor = original_yield_factor + if hasattr(u.R301, 'titer'): + u.R301.titer = original_titer + microalgae_sys.simulate() + + # 添加平滑处理并在此处保存等高线数据矩阵 + try: + from scipy.ndimage import gaussian_filter + Z_neutral_mfsp = gaussian_filter(Z_neutral_mfsp, sigma=0.7) + Z_lowph_mfsp = gaussian_filter(Z_lowph_mfsp, sigma=0.7) + Z_neutral_gwp = gaussian_filter(Z_neutral_gwp, sigma=0.7) + Z_lowph_gwp = gaussian_filter(Z_lowph_gwp, sigma=0.7) + print("数据平滑处理已应用") + except ImportError: + print("无法导入scipy.ndimage,跳过数据平滑处理") + + # 保存等高线数据到 NPZ + try: + np.savez(os.path.join('results', 'contour_data.npz'), + yields=yields, titers=titers, + Z_neutral_mfsp=Z_neutral_mfsp, Z_lowph_mfsp=Z_lowph_mfsp, + Z_neutral_gwp=Z_neutral_gwp, Z_lowph_gwp=Z_lowph_gwp) + except Exception as _: + pass + + # Plot C: Neutral fermentation MFSP + ax5 = plt.subplot2grid((3, 2), (2, 0)) + plt.title('Neutral Fermentation') + cs = ax5.contourf(Y, T, Z_neutral_mfsp, 30, cmap='viridis_r') + + # 计算合适的轮廓线级别 + try: + # 计算均匀分布的8个轮廓线级别,确保递增 + min_val = np.nanmin(Z_neutral_mfsp) + max_val = np.nanmax(Z_neutral_mfsp) + print(f"MFSP数据范围: {min_val:.3f} - {max_val:.3f}") + + if min_val >= max_val or np.isnan(min_val) or np.isnan(max_val): + # 如果数据范围有问题,使用默认值 + levels = [1.0, 1.5, 2.0, 2.5, 3.0] + print("使用默认级别") + else: + levels = np.linspace(min_val, max_val, 8) + # 确保级别是递增的且唯一 + levels = np.unique(np.sort(levels)) + if len(levels) < 3: # 如果唯一级别太少,使用默认值 + levels = np.linspace(min_val, max_val, 5) + levels = np.round(levels, 3) + + print(f"MFSP等高线级别: {levels}") + # 验证级别是递增的 + if len(levels) < 2 or not np.all(np.diff(levels) > 0): + levels = [1.0, 1.5, 2.0, 2.5, 3.0] + print("回退到默认级别") + except Exception as e: + print(f"级别计算错误: {e}") + levels = [1.0, 1.5, 2.0, 2.5, 3.0] + + contours = ax5.contour(Y, T, Z_neutral_mfsp, levels=levels, colors='black', linewidths=0.5) + ax5.clabel(contours, inline=True, fontsize=6, fmt='%.2f') + ax5.set_xlabel('Yield [%theoretical]') + ax5.set_ylabel('Titer [g L$^{-1}$]') + + # Add extra space on the right and place colorbar there + fig.subplots_adjust(right=0.85) + cbar = fig.colorbar(cs, ax=ax5, location='right', pad=0.02) + + # 标记基准案例点 + base_yield_pct = 50.0 # 基准产率百分比 + base_titer = original_titer # 基准浓度 + ax5.plot(base_yield_pct, base_titer, 'D', color='white', markersize=5, markeredgecolor='black', markeredgewidth=0.5) + + # Adjust layout + plt.tight_layout() + fig.subplots_adjust(hspace=0.4, wspace=0.3, left=0.1, right=0.9, top=0.95, bottom=0.1) + + # Save the figure if a path is provided + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + + # Create additional GWP contour plots + fig2 = plt.figure(figsize=(7, 3)) + + # Plot E: Neutral fermentation GWP + ax7 = plt.subplot(1, 2, 1) + plt.title('Neutral Fermentation') + cs = ax7.contourf(Y, T, Z_neutral_gwp, 30, cmap='viridis_r') + + # 计算合适的轮廓线级别 + try: + # 计算均匀分布的8个轮廓线级别,确保递增 + min_val = np.nanmin(Z_neutral_gwp) + max_val = np.nanmax(Z_neutral_gwp) + print(f"GWP数据范围: {min_val:.3f} - {max_val:.3f}") + + if min_val >= max_val or np.isnan(min_val) or np.isnan(max_val): + # 如果数据范围有问题,使用默认值 + levels = [1.5, 2.0, 2.5, 3.0, 3.5] + print("使用默认级别") + else: + levels = np.linspace(min_val, max_val, 8) + # 确保级别是递增的且唯一 + levels = np.unique(np.sort(levels)) + if len(levels) < 3: # 如果唯一级别太少,使用默认值 + levels = np.linspace(min_val, max_val, 5) + levels = np.round(levels, 3) + + print(f"GWP等高线级别: {levels}") + # 验证级别是递增的 + if len(levels) < 2 or not np.all(np.diff(levels) > 0): + levels = [1.5, 2.0, 2.5, 3.0, 3.5] + print("回退到默认级别") + except Exception as e: + print(f"级别计算错误: {e}") + levels = [1.5, 2.0, 2.5, 3.0, 3.5] + + contours = ax7.contour(Y, T, Z_neutral_gwp, levels=levels, colors='black', linewidths=0.5) + ax7.clabel(contours, inline=True, fontsize=6, fmt='%.2f') + ax7.set_xlabel('Yield [%theoretical]') + ax7.set_ylabel('Titer [g L$^{-1}$]') + + # 添加基准点 + ax7.plot(base_yield_pct, base_titer, 'D', color='white', markersize=5, markeredgecolor='black', markeredgewidth=0.5) + + + # Add colorbar outside of the two subplots + fig2.subplots_adjust(right=0.85) + cbar = fig2.colorbar(cs, ax=ax7, location='right', pad=0.02) + + # Adjust layout + fig2.subplots_adjust(wspace=0.3, left=0.1, right=0.9) + + # Save the second figure if a path is provided + if save_path: + plt.savefig(save_path.replace('.png', '_gwp.png'), dpi=300, bbox_inches='tight') + + # ------------------------------------------------------------------ + # Additionally split every Axes object in fig and fig2 into its own + # image file so that users can access each sub-plot independently. + # ------------------------------------------------------------------ + base_dir = os.path.dirname(save_path) or '.' + split_dir = os.path.join(base_dir, 'combined_subplots') + os.makedirs(split_dir, exist_ok=True) + + # Helper to save each axis + def _save_axes_individually(fig_obj, tag): + fig_obj.canvas.draw() # Ensure renderer exists + renderer = fig_obj.canvas.get_renderer() + for i, ax in enumerate(fig_obj.axes, start=1): + # Use axis title as part of filename when available, else index + title = ax.get_title() or f'{tag}_{i}' + safe_title = ''.join(c if c.isalnum() else '_' for c in title)[:40] + fname = f'{tag}_{safe_title or i}.png' + path = os.path.join(split_dir, fname) + bbox = ax.get_tightbbox(renderer).transformed(fig_obj.dpi_scale_trans.inverted()) + fig_obj.savefig(path, dpi=300, bbox_inches=bbox) + + _save_axes_individually(fig, 'fig1') + _save_axes_individually(fig2, 'fig2') + + return fig, fig2 + +create_plots_from_monte_carlo('microalgae_mcca_summary.png') \ No newline at end of file From 504256ed6df8108be0f10f5b78be87b44e777cc4 Mon Sep 17 00:00:00 2001 From: Xingdong Shi Date: Thu, 7 Aug 2025 14:31:21 +0800 Subject: [PATCH 2/2] Add enhanced microalgae biorefinery with yeast recycling and multiple system variants --- biorefineries/microalgae/TRY_analysis.py | 619 ++++--------- biorefineries/microalgae/_chemicals.py | 5 +- biorefineries/microalgae/lca.py | 813 ++++-------------- biorefineries/microalgae/model_utils.py | 184 ++++ .../microalgae/parameter_distributions.xlsx | Bin 12430 -> 10732 bytes biorefineries/microalgae/streams.py | 2 +- biorefineries/microalgae/system.py | 206 +++-- biorefineries/microalgae/system_etanol.py | 422 +++++++++ biorefineries/microalgae/system_no_yeast.py | 415 +++++++++ biorefineries/microalgae/tea.py | 1 + biorefineries/microalgae/tea_breakdown.py | 204 +++++ biorefineries/microalgae/uncertainties.py | 804 ++++++++++------- biorefineries/microalgae/units.py | 427 +++++---- biorefineries/microalgae/utils.py | 35 +- 14 files changed, 2501 insertions(+), 1636 deletions(-) create mode 100644 biorefineries/microalgae/model_utils.py create mode 100644 biorefineries/microalgae/system_etanol.py create mode 100644 biorefineries/microalgae/system_no_yeast.py create mode 100644 biorefineries/microalgae/tea_breakdown.py diff --git a/biorefineries/microalgae/TRY_analysis.py b/biorefineries/microalgae/TRY_analysis.py index 70712d7b..368f2312 100644 --- a/biorefineries/microalgae/TRY_analysis.py +++ b/biorefineries/microalgae/TRY_analysis.py @@ -1,457 +1,162 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Microalgae biorefinery TRY (Titer, Rate, Yield) analysis - -This module performs TRY analysis for the microalgae biorefinery system, -analyzing the effects of fermentation titer, productivity rate, and yield -on techno-economic and environmental metrics. - -@author: Xingdong Shi -@version: 0.0.1 -""" - -from warnings import filterwarnings -filterwarnings('ignore') -import copy -import numpy as np -import pandas as pd -import biosteam as bst -import thermosteam as tmo -from datetime import datetime -from biosteam.utils import TicToc -import os - -# Import microalgae system components -from . import system as microalgae_system -from . import lca -from . import tea -from ._chemicals import chems - -# Set up the system -bst.settings.set_thermo(chems) -microalgae_sys = microalgae_system.microalgae_mcca_sys -microalgae_tea = microalgae_system.microalgae_tea - -# Create LCA object -from . import analysis_utils -microalgae_lca = analysis_utils.create_microalgae_lca_simple(microalgae_sys, microalgae_tea) - -print('\n\nLoaded microalgae system for TRY analysis.') - -# TRY analysis parameters -titer_range = np.linspace(10, 100, 10) # g/L, fermentation titer -rate_range = np.linspace(0.5, 5.0, 10) # g/L/h, productivity rate -yield_range = np.linspace(0.1, 0.8, 10) # g product/g substrate, yield - -# Results storage -try_results = { - 'Titer': [], - 'Rate': [], - 'Yield': [], - 'MPSP': [], - 'GWP100a': [], - 'FEC': [], - 'Production_Rate': [], # kg/h - 'Annual_Production': [], # kg/year - 'Conversion': [], # fraction -} - -def get_fermentation_unit(): - """Get the fermentation unit from the system.""" - for unit in microalgae_sys.units: - if 'ferment' in unit.ID.lower() or 'MCCA' in unit.ID: - return unit - return None - -def run_try_analysis(): - """Run TRY analysis for microalgae biorefinery.""" - - timer = TicToc('timer') - timer.tic() - - print(f'\n\nStarting TRY analysis...') - print(f'Titer range: {titer_range[0]:.1f} - {titer_range[-1]:.1f} g/L') - print(f'Rate range: {rate_range[0]:.1f} - {rate_range[-1]:.1f} g/L/h') - print(f'Yield range: {yield_range[0]:.2f} - {yield_range[-1]:.2f} g/g') - - # Get fermentation unit - fermentation_unit = get_fermentation_unit() - if fermentation_unit is None: - print("Warning: Could not find fermentation unit") - return None - - print(f'Found fermentation unit: {fermentation_unit.ID}') - - total_simulations = len(titer_range) * len(rate_range) * len(yield_range) - simulation_count = 0 - - # Store baseline values - baseline_titer = getattr(fermentation_unit, 'titer', 50.0) # Default 50 g/L - baseline_rate = getattr(fermentation_unit, 'productivity', 2.0) # Default 2 g/L/h - baseline_yield = getattr(fermentation_unit, 'fermentation_yield', 0.4) # Default 0.4 g/g - - for titer in titer_range: - for rate in rate_range: - for yield_val in yield_range: - simulation_count += 1 - - if simulation_count % 100 == 0: - print(f'Completed {simulation_count}/{total_simulations} simulations') - - try: - # Set TRY parameters - if hasattr(fermentation_unit, 'titer'): - fermentation_unit.titer = titer - if hasattr(fermentation_unit, 'productivity'): - fermentation_unit.productivity = rate - if hasattr(fermentation_unit, 'fermentation_yield'): - fermentation_unit.fermentation_yield = yield_val - - # Alternative parameter names - if hasattr(fermentation_unit, 'product_titer'): - fermentation_unit.product_titer = titer - if hasattr(fermentation_unit, 'volumetric_productivity'): - fermentation_unit.volumetric_productivity = rate - if hasattr(fermentation_unit, 'conversion_efficiency'): - fermentation_unit.conversion_efficiency = yield_val - - # Simulate system - microalgae_sys.simulate() - - # Calculate metrics - main_product = analysis_utils.get_main_product_stream(microalgae_sys) - mpsp = microalgae_tea.solve_price(main_product) if main_product else np.nan - gwp = microalgae_lca.GWP - fec = microalgae_lca.FEC - - # Calculate production metrics - product_stream = None - for stream in microalgae_sys.products: - if stream.get_total_flow('kg/hr') > 0: - product_stream = stream - break - - if product_stream: - production_rate = product_stream.get_total_flow('kg/hr') - annual_production = production_rate * 24 * 365 # kg/year - else: - production_rate = 0 - annual_production = 0 - - # Calculate conversion (simplified) - conversion = yield_val # Approximation - - # Store results - try_results['Titer'].append(titer) - try_results['Rate'].append(rate) - try_results['Yield'].append(yield_val) - try_results['MPSP'].append(mpsp) - try_results['GWP100a'].append(gwp) - try_results['FEC'].append(fec) - try_results['Production_Rate'].append(production_rate) - try_results['Annual_Production'].append(annual_production) - try_results['Conversion'].append(conversion) - - except Exception as e: - print(f'Error in simulation {simulation_count}: {str(e)}') - # Store NaN values for failed simulations - try_results['Titer'].append(titer) - try_results['Rate'].append(rate) - try_results['Yield'].append(yield_val) - try_results['MPSP'].append(np.nan) - try_results['GWP100a'].append(np.nan) - try_results['FEC'].append(np.nan) - try_results['Production_Rate'].append(np.nan) - try_results['Annual_Production'].append(np.nan) - try_results['Conversion'].append(np.nan) - - # Restore baseline values - if hasattr(fermentation_unit, 'titer'): - fermentation_unit.titer = baseline_titer - if hasattr(fermentation_unit, 'productivity'): - fermentation_unit.productivity = baseline_rate - if hasattr(fermentation_unit, 'fermentation_yield'): - fermentation_unit.fermentation_yield = baseline_yield - - timer.toc() - - return pd.DataFrame(try_results) - -def analyze_try_results(results_df): - """Analyze TRY results and generate summary statistics.""" - - analysis = {} - - # Basic statistics - metrics = ['MPSP', 'GWP100a', 'FEC', 'Production_Rate', 'Annual_Production'] - - for metric in metrics: - valid_data = results_df[metric].dropna() - if len(valid_data) > 0: - analysis[metric] = { - 'min': valid_data.min(), - 'max': valid_data.max(), - 'mean': valid_data.mean(), - 'std': valid_data.std(), - 'median': valid_data.median(), - 'q25': valid_data.quantile(0.25), - 'q75': valid_data.quantile(0.75) - } - - # Find optimal conditions - valid_results = results_df.dropna(subset=['MPSP']) - if len(valid_results) > 0: - # Minimum MPSP - min_mpsp_idx = valid_results['MPSP'].idxmin() - analysis['optimal_MPSP'] = { - 'titer': valid_results.loc[min_mpsp_idx, 'Titer'], - 'rate': valid_results.loc[min_mpsp_idx, 'Rate'], - 'yield': valid_results.loc[min_mpsp_idx, 'Yield'], - 'MPSP': valid_results.loc[min_mpsp_idx, 'MPSP'], - 'GWP100a': valid_results.loc[min_mpsp_idx, 'GWP100a'], - 'FEC': valid_results.loc[min_mpsp_idx, 'FEC'] - } - - # Minimum GWP - if 'GWP100a' in valid_results.columns: - min_gwp_idx = valid_results['GWP100a'].idxmin() - analysis['optimal_GWP'] = { - 'titer': valid_results.loc[min_gwp_idx, 'Titer'], - 'rate': valid_results.loc[min_gwp_idx, 'Rate'], - 'yield': valid_results.loc[min_gwp_idx, 'Yield'], - 'MPSP': valid_results.loc[min_gwp_idx, 'MPSP'], - 'GWP100a': valid_results.loc[min_gwp_idx, 'GWP100a'], - 'FEC': valid_results.loc[min_gwp_idx, 'FEC'] - } - - # Correlation analysis - correlation_metrics = ['Titer', 'Rate', 'Yield', 'MPSP', 'GWP100a', 'FEC'] - correlation_data = results_df[correlation_metrics].dropna() - if len(correlation_data) > 10: - analysis['correlations'] = correlation_data.corr() - - return analysis - -def save_try_results(results_df, analysis, filename_prefix='microalgae_try'): - """Save TRY analysis results to files.""" - - timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') - - # Save to Excel - excel_filename = f'{filename_prefix}_results_{timestamp}.xlsx' - with pd.ExcelWriter(excel_filename) as writer: - # Save full results - results_df.to_excel(writer, sheet_name='TRY_Results', index=False) - - # Save summary statistics - if 'MPSP' in analysis: - summary_data = [] - for metric in ['MPSP', 'GWP100a', 'FEC', 'Production_Rate', 'Annual_Production']: - if metric in analysis: - row = {'Metric': metric} - row.update(analysis[metric]) - summary_data.append(row) - - summary_df = pd.DataFrame(summary_data) - summary_df.to_excel(writer, sheet_name='Summary_Statistics', index=False) - - # Save optimal conditions - if 'optimal_MPSP' in analysis: - optimal_data = [] - for opt_type in ['optimal_MPSP', 'optimal_GWP']: - if opt_type in analysis: - row = {'Optimization': opt_type.replace('optimal_', '')} - row.update(analysis[opt_type]) - optimal_data.append(row) - - optimal_df = pd.DataFrame(optimal_data) - optimal_df.to_excel(writer, sheet_name='Optimal_Conditions', index=False) - - # Save correlations - if 'correlations' in analysis: - analysis['correlations'].to_excel(writer, sheet_name='Correlations') - - print(f'\nTRY results saved to {excel_filename}') - - # Save summary text report - txt_filename = f'{filename_prefix}_summary_{timestamp}.txt' - with open(txt_filename, 'w') as f: - f.write("Microalgae Biorefinery TRY Analysis Summary\n") - f.write("=" * 50 + "\n\n") - - f.write(f"Analysis Parameters:\n") - f.write(f"Titer range: {titer_range[0]:.1f} - {titer_range[-1]:.1f} g/L\n") - f.write(f"Rate range: {rate_range[0]:.1f} - {rate_range[-1]:.1f} g/L/h\n") - f.write(f"Yield range: {yield_range[0]:.2f} - {yield_range[-1]:.2f} g/g\n") - f.write(f"Total simulations: {len(results_df)}\n\n") - - # Summary statistics - if 'MPSP' in analysis: - f.write("Summary Statistics:\n") - for metric in ['MPSP', 'GWP100a', 'FEC']: - if metric in analysis: - stats = analysis[metric] - f.write(f"\n{metric}:\n") - f.write(f" Min: {stats['min']:.3f}\n") - f.write(f" Max: {stats['max']:.3f}\n") - f.write(f" Mean: {stats['mean']:.3f}\n") - f.write(f" Std: {stats['std']:.3f}\n") - f.write(f" Median: {stats['median']:.3f}\n") - - # Optimal conditions - if 'optimal_MPSP' in analysis: - f.write("\nOptimal Conditions:\n") - - f.write("\nMinimum MPSP:\n") - opt_mpsp = analysis['optimal_MPSP'] - f.write(f" Titer: {opt_mpsp['titer']:.1f} g/L\n") - f.write(f" Rate: {opt_mpsp['rate']:.1f} g/L/h\n") - f.write(f" Yield: {opt_mpsp['yield']:.3f} g/g\n") - f.write(f" MPSP: {opt_mpsp['MPSP']:.3f} USD/kg\n") - f.write(f" GWP: {opt_mpsp['GWP100a']:.3f} kg CO2-eq/kg\n") - - if 'optimal_GWP' in analysis: - f.write("\nMinimum GWP:\n") - opt_gwp = analysis['optimal_GWP'] - f.write(f" Titer: {opt_gwp['titer']:.1f} g/L\n") - f.write(f" Rate: {opt_gwp['rate']:.1f} g/L/h\n") - f.write(f" Yield: {opt_gwp['yield']:.3f} g/g\n") - f.write(f" MPSP: {opt_gwp['MPSP']:.3f} USD/kg\n") - f.write(f" GWP: {opt_gwp['GWP100a']:.3f} kg CO2-eq/kg\n") - - print(f'Summary saved to {txt_filename}') - -def run_single_parameter_analysis(): - """Run single parameter analysis (varying one parameter at a time).""" - - print('\n\nRunning single parameter analysis...') - - # Get fermentation unit - fermentation_unit = get_fermentation_unit() - if fermentation_unit is None: - print("Warning: Could not find fermentation unit") - return None - - # Store baseline values - baseline_titer = getattr(fermentation_unit, 'titer', 50.0) - baseline_rate = getattr(fermentation_unit, 'productivity', 2.0) - baseline_yield = getattr(fermentation_unit, 'fermentation_yield', 0.4) - - single_param_results = { - 'Parameter': [], - 'Value': [], - 'MPSP': [], - 'GWP100a': [], - 'FEC': [] - } - - # Vary titer - for titer in titer_range: - try: - # Reset to baseline - if hasattr(fermentation_unit, 'titer'): - fermentation_unit.titer = titer - if hasattr(fermentation_unit, 'productivity'): - fermentation_unit.productivity = baseline_rate - if hasattr(fermentation_unit, 'fermentation_yield'): - fermentation_unit.fermentation_yield = baseline_yield - - microalgae_sys.simulate() - - main_product = analysis_utils.get_main_product_stream(microalgae_sys) - single_param_results['Parameter'].append('Titer') - single_param_results['Value'].append(titer) - single_param_results['MPSP'].append(microalgae_tea.solve_price(main_product) if main_product else np.nan) - single_param_results['GWP100a'].append(microalgae_lca.GWP) - single_param_results['FEC'].append(microalgae_lca.FEC) - - except Exception as e: - print(f'Error varying titer to {titer}: {str(e)}') - - # Vary rate - for rate in rate_range: - try: - # Reset to baseline - if hasattr(fermentation_unit, 'titer'): - fermentation_unit.titer = baseline_titer - if hasattr(fermentation_unit, 'productivity'): - fermentation_unit.productivity = rate - if hasattr(fermentation_unit, 'fermentation_yield'): - fermentation_unit.fermentation_yield = baseline_yield - - microalgae_sys.simulate() - - main_product = analysis_utils.get_main_product_stream(microalgae_sys) - single_param_results['Parameter'].append('Rate') - single_param_results['Value'].append(rate) - single_param_results['MPSP'].append(microalgae_tea.solve_price(main_product) if main_product else np.nan) - single_param_results['GWP100a'].append(microalgae_lca.GWP) - single_param_results['FEC'].append(microalgae_lca.FEC) - - except Exception as e: - print(f'Error varying rate to {rate}: {str(e)}') - - # Vary yield - for yield_val in yield_range: - try: - # Reset to baseline - if hasattr(fermentation_unit, 'titer'): - fermentation_unit.titer = baseline_titer - if hasattr(fermentation_unit, 'productivity'): - fermentation_unit.productivity = baseline_rate - if hasattr(fermentation_unit, 'fermentation_yield'): - fermentation_unit.fermentation_yield = yield_val - - microalgae_sys.simulate() - - main_product = analysis_utils.get_main_product_stream(microalgae_sys) - single_param_results['Parameter'].append('Yield') - single_param_results['Value'].append(yield_val) - single_param_results['MPSP'].append(microalgae_tea.solve_price(main_product) if main_product else np.nan) - single_param_results['GWP100a'].append(microalgae_lca.GWP) - single_param_results['FEC'].append(microalgae_lca.FEC) - - except Exception as e: - print(f'Error varying yield to {yield_val}: {str(e)}') - - # Restore baseline values - if hasattr(fermentation_unit, 'titer'): - fermentation_unit.titer = baseline_titer - if hasattr(fermentation_unit, 'productivity'): - fermentation_unit.productivity = baseline_rate - if hasattr(fermentation_unit, 'fermentation_yield'): - fermentation_unit.fermentation_yield = baseline_yield - - return pd.DataFrame(single_param_results) - -if __name__ == '__main__': - print("Starting microalgae biorefinery TRY analysis...") - - try: - # Run full TRY analysis - results_df = run_try_analysis() - - if results_df is not None: - # Analyze results - analysis = analyze_try_results(results_df) - - # Save results - save_try_results(results_df, analysis) - - # Run single parameter analysis - single_param_df = run_single_parameter_analysis() - - if single_param_df is not None: - # Save single parameter results - timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') - single_param_filename = f'microalgae_single_param_{timestamp}.xlsx' - single_param_df.to_excel(single_param_filename, index=False) - print(f'Single parameter results saved to {single_param_filename}') - - print("\nTRY analysis completed successfully!") - - except Exception as e: - print(f"\nError during TRY analysis: {str(e)}") - import traceback - traceback.print_exc() \ No newline at end of file +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Titer, Rate, Yield (TRY) analysis for microalgae biorefinery + +Based on succinic project but adapted for microalgae system structure +""" + +import numpy as np +import pandas as pd +import os +from .uncertainties import create_model + +def run_TRY_analysis(): + """Run Titer, Rate, Yield analysis for microalgae system""" + print("Creating model for TRY analysis...") + model, namespace_dict = create_model() + + # Load parameter distributions + current_dir = os.path.dirname(__file__) + param_file = os.path.join(current_dir, 'parameter_distributions.xlsx') + + if not os.path.exists(param_file): + raise FileNotFoundError(f"Parameter distributions file not found: {param_file}") + + print(f"Loading parameter distributions from {param_file}...") + model.load_parameter_distributions(param_file, namespace_dict) + + # Define TRY parameter ranges based on microalgae system + # These ranges should be adjusted based on your specific system + titer_range = np.linspace(1.0, 4.0, 11) # g/L - caproic acid titer + yield_range = np.linspace(0.15, 0.35, 11) # g/g - C6 yield + + results = [] + + # Find TRY parameters from loaded parameters + titer_param = None + yield_param = None + + for param in model.parameters: + param_name_lower = param.name.lower() + if 'titer' in param_name_lower and 'c6' in param_name_lower: + titer_param = param + print(f"Found titer parameter: {param.name}") + elif 'yield' in param_name_lower and 'c6' in param_name_lower: + yield_param = param + print(f"Found yield parameter: {param.name}") + + if titer_param is None: + print("Warning: Could not find titer parameter. Available parameters:") + for param in model.parameters: + print(f" - {param.name}") + # Use first parameter with 'titer' in name as fallback + for param in model.parameters: + if 'titer' in param.name.lower(): + titer_param = param + print(f"Using fallback titer parameter: {param.name}") + break + + if yield_param is None: + print("Warning: Could not find yield parameter. Available parameters:") + for param in model.parameters: + print(f" - {param.name}") + # Use first parameter with 'yield' in name as fallback + for param in model.parameters: + if 'yield' in param.name.lower(): + yield_param = param + print(f"Using fallback yield parameter: {param.name}") + break + + if titer_param is None or yield_param is None: + print("Error: Could not find both titer and yield parameters") + return None + + # Run TRY analysis + print(f"\nRunning TRY analysis...") + print(f"Titer range: {min(titer_range):.2f} - {max(titer_range):.2f}") + print(f"Yield range: {min(yield_range):.3f} - {max(yield_range):.3f}") + + baseline_sample = model.get_baseline_sample() + total_combinations = len(titer_range) * len(yield_range) + completed = 0 + + for i, titer in enumerate(titer_range): + for j, yield_val in enumerate(yield_range): + sample = baseline_sample.copy() + sample[titer_param.index] = titer + sample[yield_param.index] = yield_val + + try: + result = model(sample) + result['Titer'] = titer + result['Yield'] = yield_val + results.append(result) + completed += 1 + + if completed % 10 == 0: + print(f"Completed {completed}/{total_combinations} combinations") + + except Exception as e: + print(f"Error at titer={titer:.2f}, yield={yield_val:.3f}: {e}") + continue + + if results: + results_df = pd.DataFrame(results) + print(f"\nTRY analysis completed! {len(results)} successful evaluations out of {total_combinations} total combinations") + + # Print summary statistics + print("\nSummary statistics:") + for col in ['MFSP', 'GWP', 'FEC', 'TCI']: + if col in results_df.columns: + print(f"{col}: min={results_df[col].min():.4f}, max={results_df[col].max():.4f}, mean={results_df[col].mean():.4f}") + + return results_df + else: + print("No successful evaluations in TRY analysis") + return None + +def analyze_TRY_results(results_df, save_path=None): + """Analyze TRY results and identify optimal regions""" + if results_df is None: + print("No results to analyze") + return + + print("\nAnalyzing TRY results...") + + # Find optimal conditions (minimum MFSP) + if 'MFSP' in results_df.columns: + min_mfsp_idx = results_df['MFSP'].idxmin() + optimal_conditions = results_df.loc[min_mfsp_idx] + print(f"\nOptimal conditions (minimum MFSP):") + print(f" Titer: {optimal_conditions['Titer']:.2f}") + print(f" Yield: {optimal_conditions['Yield']:.3f}") + print(f" MFSP: {optimal_conditions['MFSP']:.4f} $/kg") + if 'GWP' in results_df.columns: + print(f" GWP: {optimal_conditions['GWP']:.4f} kg CO2-eq/kg") + + # Save results if path provided + if save_path: + results_df.to_csv(save_path, index=False) + print(f"\nResults saved to: {save_path}") + +if __name__ == '__main__': + try: + results = run_TRY_analysis() + + if results is not None: + # Analyze results + analyze_TRY_results(results) + + # Save results + results_dir = 'results' + if not os.path.exists(results_dir): + os.makedirs(results_dir) + + save_path = os.path.join(results_dir, 'TRY_analysis_results.csv') + analyze_TRY_results(results, save_path) + + except Exception as e: + print(f"Error in TRY analysis: {e}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/biorefineries/microalgae/_chemicals.py b/biorefineries/microalgae/_chemicals.py index 810ecfc1..3616db2d 100644 --- a/biorefineries/microalgae/_chemicals.py +++ b/biorefineries/microalgae/_chemicals.py @@ -192,6 +192,7 @@ def chemical_defined(ID, **kwargs): Chloroform = chemical_database('Chloroform', phase='l') Dichloromethane = chemical_database('Dichloromethane', phase='l') Octanol = chemical_database('Octanol', phase= 'l') +OleylAlcohol = chemical_database('OleylAlcohol', phase= 'l') # Ionic liquid extractants TOA = chemical_database('TOA', search_ID='tri-n-octylamine') @@ -234,7 +235,7 @@ def chemical_defined(ID, **kwargs): # Base enzyme BaseEnzyme = chemical_defined('BaseEnzyme', phase='s', - MW=110, + formula='CH1.57O0.31N0.29S0.007', Hf=-17618*_cal2joule, HHV=0, LHV=0) @@ -283,7 +284,7 @@ def chemical_defined(ID, **kwargs): Microalgae = chemical_defined( 'Microalgae', phase='s', - formula='C2.8H0.4O1.5N0.3', # comsuption from microalgal biomass componets https://www.sciencedirect.com/science/article/pii/S0196890419313184?via%3Dihub + formula='C3.2H0.4O1.5N0.3', # comsuption from microalgal biomass componets https://www.sciencedirect.com/science/article/pii/S0196890419313184?via%3Dihub Hf=-2138000, ) diff --git a/biorefineries/microalgae/lca.py b/biorefineries/microalgae/lca.py index 26b3a186..e57410e5 100644 --- a/biorefineries/microalgae/lca.py +++ b/biorefineries/microalgae/lca.py @@ -18,60 +18,21 @@ https://github.com/BioSTEAMDevelopmentGroup/Bioindustrial-Park/tree/master/biorefineries/succinic @author: Xingdong Shi -@version: 0.0.1 +@version: 0.0.9 """ from thermosteam import Stream import numpy as np import pandas as pd import biosteam as bst -from biosteam.units.decorators import cost +from .system import create_microalgae_MCCA_production_sys from ._chemicals import chems __all__ = ['LCA'] class LCA: - """ - 微藻生物炼制厂生命周期环境影响评估类。 - - 参数 - ---------- - system : System - 微藻生产系统 - CFs : dict - 格式: {: {key: float}} - 其中key可以是流名称(对于complex_feeds)、化学品ID或'Electricity' - main_product : Stream - 主产品流 - main_product_chemical_IDs : list - 主产品化学品ID列表 - boiler : Unit - 锅炉设备单元 - by_products : list, 可选 - 副产品流列表 - complex_feeds : dict, 可选 - 复合进料流,格式: {key: (Stream, mass_kind)} - 其中mass_kind必须是('wet', 'dry')之一 - cooling_tower : Unit, 可选 - 冷却塔设备 - chilled_water_processing_units : list, 可选 - 冷冻水处理单元列表 - has_turbogenerator : bool, 可选 - 是否有涡轮发电机 - functional_unit : str, 可选 - 功能单位,如 '1 kg', '1 metric ton', '1 MJ (by LHV)', '1 MJ (by HHV)' - functional_quantity_per_h_fn : callable, 可选 - 计算每小时功能数量的函数 - add_EOL_GWP : bool, 可选 - 是否添加产品(主产品和副产品)的寿命终止温室气体排放 - input_biogenic_carbon_streams : list, 可选 - 生物来源碳输入流列表 - """ - - # 关键影响类别 GWP_key = 'GWP_100' FEC_key = 'FEC' - WC_key = 'WC' def __init__(self, system, @@ -83,7 +44,6 @@ def __init__(self, complex_feeds={}, cooling_tower=None, chilled_water_processing_units=[], - has_turbogenerator=None, functional_unit=None, functional_quantity_per_h_fn=None, add_EOL_GWP=False, @@ -95,21 +55,14 @@ def __init__(self, self.units = self.system.units self.flowsheet = self.system.flowsheet self.streams = self.system.streams - self.input_biogenic_carbon_streams = input_biogenic_carbon_streams - self.add_EOL_GWP = add_EOL_GWP - self.complex_feeds = complex_feeds kg_product_per_h_fn = lambda: self.main_product.imass[self.main_product_chemical_IDs].sum() MJ_LHV_product_per_h_fn = lambda: self.main_product.LHV MJ_HHV_product_per_h_fn = lambda: self.main_product.HHV - - if functional_unit and functional_quantity_per_h_fn: - raise AttributeError('必须提供functional_unit或functional_quantity_per_h_fn,但不能同时提供两者。') - - elif not functional_unit: + if not functional_unit: self.functional_unit = functional_unit = '1 kg' self.functional_quantity_per_h_fn = kg_product_per_h_fn @@ -125,71 +78,36 @@ def __init__(self, self.functional_quantity_per_h_fn = MJ_HHV_product_per_h_fn self.CFs = CFs - self._chemical_IDs = [chem.ID for chem in chemicals] - self._CF_streams = _CF_streams = {} for impact_category in CFs.keys(): _CF_streams[impact_category] = ic_CF_stream = Stream(f'{system.ID}_{impact_category}_CF_stream') for k, v in CFs[impact_category].items(): - if not k in list(complex_feeds.keys()) + ['Electricity']: - try: + if k not in complex_feeds and k != 'Electricity': + try: ic_CF_stream.imass[k] = v except: - pass # 假设其他complex_feed IDs存在于CFs.keys()中 + pass self._LCA_stream = Stream(f'{system.ID}_LCA_stream') - self.main_product_chemical_IDs = main_product_chemical_IDs self.main_product = main_product self.by_products = by_products - self.chem_IDs = [i.ID for i in chemicals] - - if has_turbogenerator is None: - has_turbogenerator = boiler.power_utility.production > 0. - self.has_turbogenerator = has_turbogenerator - - self.BT = self.boiler = boiler - try: - self.natural_gas = self.BT.natural_gas - except: - # 假设biogas替代自然气 - self.natural_gas = next((stream for stream in self.system.streams - if 'biogas' in stream.ID.lower() or - (hasattr(stream, 'price') and stream.price and - stream.imass['CH4'] > 0)), None) - if not self.natural_gas: - # 创建一个空的流作为自然气流的替代 - self.natural_gas = Stream('natural_gas', CH4=0) - self.CT = self.cooling_tower = cooling_tower self.CWP_units = self.chilled_water_processing_units = chilled_water_processing_units - self._CO2_MW = self.chemicals.CO2.MW - @property - def system_carbon_balance(self): - """计算系统碳平衡""" - total_C_in = sum([feed.get_atomic_flow('C') for feed in self.feeds]) - total_C_out = self.main_product.get_atomic_flow('C') +\ - sum([i.get_atomic_flow('C') for i in self.by_products]) +\ - sum([emission.get_atomic_flow('C') for emission in self.emissions]) - return total_C_out/total_C_in if total_C_in > 0 else 0 - @property def products(self): - """获取所有产品流(主产品和副产品)""" return [self.main_product] + self.by_products @property def functional_quantity_per_h(self): - """计算每小时功能数量""" return self.functional_quantity_per_h_fn() @property def emissions(self): - """获取排放流列表""" emissions = list(self.system.products) for i in self.products: if i in emissions: @@ -198,12 +116,10 @@ def emissions(self): @property def feeds(self): - """获取原料流列表""" return self.system.feeds @property def LCA_stream(self): - """创建LCA评估用的综合流""" _LCA_stream = self._LCA_stream to_mix = list(self.feeds) for s, m_k in self.complex_feeds.values(): @@ -213,51 +129,42 @@ def LCA_stream(self): return _LCA_stream def get_material_impact_array(self, impact_category): - """获取特定影响类别的物质影响数组""" return self.LCA_stream.mass*self._CF_streams[impact_category].mass def get_material_impact(self, impact_category): - """计算特定影响类别的物质影响""" return self.get_material_impact_array(impact_category).sum() / self.functional_quantity_per_h @property def net_electricity(self): - """计算净电力消耗""" return self.system.power_utility.rate def get_net_electricity_impact(self, impact_category): - """计算特定影响类别的净电力影响""" if 'Electricity' in self.CFs[impact_category]: return self.net_electricity * self.CFs[impact_category]['Electricity'] / self.functional_quantity_per_h return 0 @property def EOL_GWP(self): - """计算寿命终止全球变暖潜能值""" return sum([i.get_atomic_flow('C') for i in [self.main_product] + self.by_products]) *\ self._CO2_MW/self.functional_quantity_per_h @property def direct_emissions_GWP(self): - """计算直接排放全球变暖潜能值""" return sum([stream.get_atomic_flow('C') for stream in self.emissions]) *\ self._CO2_MW / self.functional_quantity_per_h @property def biogenic_emissions_GWP(self): - """计算生物来源直接排放全球变暖潜能值""" return sum([i.get_atomic_flow('C') for i in self.input_biogenic_carbon_streams]) *\ self._CO2_MW/self.functional_quantity_per_h @property def direct_non_biogenic_emissions_GWP(self): - """计算非生物来源直接排放全球变暖潜能值""" return self.direct_emissions_GWP -\ self.biogenic_emissions_GWP +\ int(self.add_EOL_GWP)*self.EOL_GWP def get_complex_feeds_impact(self, impact_category): - """计算复合进料流在特定影响类别的影响""" tot_cfs_impact = 0. impact_CFs = self.CFs[impact_category] for k, (s, mass_kind) in self.complex_feeds.items(): @@ -268,7 +175,6 @@ def get_complex_feeds_impact(self, impact_category): return tot_cfs_impact/self.functional_quantity_per_h def get_total_impact(self, impact_category): - """计算特定影响类别的总影响""" tot_impact = self.get_complex_feeds_impact(impact_category) +\ self.get_material_impact(impact_category) +\ self.get_net_electricity_impact(impact_category) @@ -278,340 +184,21 @@ def get_total_impact(self, impact_category): def get_material_impact_breakdown(self, impact_category): - """获取特定影响类别的物质影响明细""" - # 初始化关键化学品的影响字典 - chemical_impact_dict = {'H2SO4':0, 'NaOH':0, 'NH4OH':0, 'CH4':0, 'CO2':0, 'Octanol':0} + chemical_impact_dict = {'H2SO4':0, 'NaOH':0, 'NH4OH':0, 'CH4':0, 'CO2':0, 'OleylAlcohol':0} LCA_stream = self.LCA_stream ic_CF_stream = self._CF_streams[impact_category] functional_quantity_per_h = self.functional_quantity_per_h - # 添加其他化学品的影响 chemical_impact_dict_additional = {ID: LCA_stream.imass[ID] * ic_CF_stream.imass[ID] / functional_quantity_per_h for ID in self.chem_IDs} - # 移除值为0的项 for k in list(chemical_impact_dict_additional.keys()): if chemical_impact_dict_additional[k] == 0.: del(chemical_impact_dict_additional[k]) chemical_impact_dict.update(chemical_impact_dict_additional) return chemical_impact_dict - - def get_material_impact_breakdown_as_fraction_of_material_impact(self, impact_category): - """获取物质影响明细,表示为物质影响的比例""" - chemical_impact_dict = self.get_material_impact_breakdown(impact_category) - tot_material_impact = self.get_material_impact(impact_category) - if tot_material_impact > 0: - for k,v in chemical_impact_dict.items(): - chemical_impact_dict[k] /= tot_material_impact - return chemical_impact_dict - - def get_material_impact_breakdown_as_fraction_of_total_impact(self, impact_category): - """获取物质影响明细,表示为总影响的比例""" - chemical_impact_dict = self.get_material_impact_breakdown(impact_category) - tot_impact = self.get_total_impact(impact_category) - if tot_impact > 0: - for k,v in chemical_impact_dict.items(): - chemical_impact_dict[k] /= tot_impact - return chemical_impact_dict - - def get_natural_gas_impact(self, impact_category): - """计算自然气在特定影响类别的影响""" - if 'CH4' in self.CFs[impact_category]: - return self.CFs[impact_category]['CH4']*self.natural_gas.F_mass/self.functional_quantity_per_h - return 0 - - @property - def natural_gas_combustion_GWP(self): - """计算自然气燃烧的全球变暖潜能值""" - return (self.natural_gas.get_atomic_flow('C')) * self._CO2_MW / self.functional_quantity_per_h - - def get_complex_feed_impact_by_ID(self, impact_category, complex_feed_ID): - """计算特定复合进料流在特定影响类别的影响""" - if complex_feed_ID in self.complex_feeds and complex_feed_ID in self.CFs[impact_category]: - s, mass_kind = self.complex_feeds[complex_feed_ID] - mass = s.F_mass - if mass_kind=='dry': mass -= s.imass['Water'] - return self.CFs[impact_category][complex_feed_ID] * mass - return 0 - def get_material_impact_by_ID(self, impact_category, material_ID): - """计算特定物质在特定影响类别的影响""" - if material_ID in self.CFs[impact_category]: - return self.CFs[impact_category][material_ID] * self.LCA_stream.imass[material_ID] - return 0 - - @property - def electricity_demand(self): - """计算电力需求""" - return sum([i.power_utility.consumption for i in self.system.units]) - - @property - def cooling_electricity_demand(self): - """计算冷却系统电力需求""" - return (self.CT.power_utility.rate if self.CT else 0) + \ - sum([i.power_utility.rate for i in self.CWP_units]) - - def __repr__(self): - """返回LCA对象的字符串表示""" - return f'LCA对象,用于{self.system.ID}。影响类别: {list(self.CFs.keys())}。' - - def show(self): - """打印LCA对象信息""" - print(self.__repr__()) - _ipython_display_ = show - - def generate_report(self, output_file=None): - """生成LCA报告""" - report = [] - report.append(f"======= 微藻生物炼制厂生命周期评估报告 =======") - report.append(f"系统ID: {self.system.ID}") - report.append(f"功能单位: {self.functional_unit}") - report.append(f"主产品: {self.main_product.ID}") - - report.append("\n----- 系统碳平衡 -----") - report.append(f"碳平衡: {self.system_carbon_balance:.4f}") - - report.append("\n----- 影响类别结果 -----") - for impact_category in self.CFs.keys(): - total_impact = self.get_total_impact(impact_category) - report.append(f"{impact_category}: {total_impact:.4f} 单位/{self.functional_unit}") - - report.append(f" - 物质影响: {self.get_material_impact(impact_category):.4f} ({self.get_material_impact(impact_category)/total_impact*100:.1f}%)") - report.append(f" - 电力影响: {self.get_net_electricity_impact(impact_category):.4f} ({self.get_net_electricity_impact(impact_category)/total_impact*100 if total_impact else 0:.1f}%)") - report.append(f" - 复合进料影响: {self.get_complex_feeds_impact(impact_category):.4f} ({self.get_complex_feeds_impact(impact_category)/total_impact*100 if total_impact else 0:.1f}%)") - - if impact_category in ('GWP', 'GWP_100', 'GWP100'): - report.append(f" - 直接排放GWP: {self.direct_emissions_GWP:.4f} ({self.direct_emissions_GWP/total_impact*100 if total_impact else 0:.1f}%)") - report.append(f" - 生物来源排放GWP: {self.biogenic_emissions_GWP:.4f}") - report.append(f" - 非生物来源直接排放GWP: {self.direct_non_biogenic_emissions_GWP:.4f}") - - if output_file: - with open(output_file, 'w') as f: - f.write('\n'.join(report)) - - return '\n'.join(report) - - def get_impact_dataframe(self): - """返回包含所有影响结果的DataFrame""" - data = {} - - for impact_category in self.CFs.keys(): - data[f'{impact_category}_总影响'] = [self.get_total_impact(impact_category)] - data[f'{impact_category}_物质影响'] = [self.get_material_impact(impact_category)] - data[f'{impact_category}_电力影响'] = [self.get_net_electricity_impact(impact_category)] - data[f'{impact_category}_复合进料影响'] = [self.get_complex_feeds_impact(impact_category)] - - if impact_category in ('GWP', 'GWP_100', 'GWP100'): - data[f'{impact_category}_直接排放'] = [self.direct_emissions_GWP] - data[f'{impact_category}_生物来源排放'] = [self.biogenic_emissions_GWP] - data[f'{impact_category}_非生物来源直接排放'] = [self.direct_non_biogenic_emissions_GWP] - - data['功能单位'] = [self.functional_unit] - data['系统碳平衡'] = [self.system_carbon_balance] - - return pd.DataFrame(data) - - def export_results(self, file_path): - """导出结果到Excel文件""" - df = self.get_impact_dataframe() - df.to_excel(file_path) - return f"结果已导出到 {file_path}" - - def analyze_emissions_breakdown(self, threshold=1e-6): - """ - 分析emissions的详细组成 - - 参数 - ---------- - threshold : float - 显示阈值,只显示流量大于此值的组分 - - 返回 - ---------- - dict - 包含emissions分析结果的字典 - """ - # 获取所有emissions流 - emissions_streams = [] - for unit in self.system.units: - if hasattr(unit, 'outs'): - for out_stream in unit.outs: - if out_stream and out_stream not in self.products: - emissions_streams.append(out_stream) - - # 去重 - unique_emissions = [] - for stream in emissions_streams: - if stream not in unique_emissions: - unique_emissions.append(stream) - - results = { - 'total_streams': len(unique_emissions), - 'streams_analysis': [] - } - - print("=== Emissions详细分解分析 ===") - print(f"总emissions流数量: {len(unique_emissions)}") - - for i, stream in enumerate(unique_emissions): - print(f"\n--- Emissions流 {i+1}: {stream.ID} ---") - print(f"来源单元: {stream.source.ID if stream.source else 'Unknown'}") - print(f"总流量: {stream.F_mass:.3f} kg/h") - print(f"总摩尔流量: {stream.F_mol:.3f} kmol/h") - - # 分析组分 - components = [] - total_mass = stream.F_mass - total_mol = stream.F_mol - - print("\n组分详细:") - print("组分名称".ljust(20) + "质量流量(kg/h)".ljust(15) + "摩尔流量(kmol/h)".ljust(15) + "质量分数(%)".ljust(12) + "摩尔分数(%)".ljust(12)) - print("-" * 80) - - for chem in stream.chemicals: - mass_flow = stream.imass[chem.ID] - mol_flow = stream.imol[chem.ID] - - if mass_flow > threshold: # 只显示有意义的流量 - mass_fraction = (mass_flow / total_mass * 100) if total_mass > 0 else 0 - mol_fraction = (mol_flow / total_mol * 100) if total_mol > 0 else 0 - - print(f"{chem.ID:<20} {mass_flow:>14.3f} {mol_flow:>14.3f} {mass_fraction:>11.2f} {mol_fraction:>11.2f}") - - components.append({ - 'chemical': chem.ID, - 'mass_flow': mass_flow, - 'mol_flow': mol_flow, - 'mass_fraction': mass_fraction, - 'mol_fraction': mol_fraction - }) - - # 碳含量分析 - total_carbon = 0 - carbon_components = [] - - for chem in stream.chemicals: - if hasattr(chem, 'formula') and chem.formula: - carbon_atoms = 0 - formula = chem.formula - if 'C' in formula: - c_index = formula.find('C') - if c_index + 1 < len(formula) and formula[c_index + 1].isdigit(): - num_str = "" - i = c_index + 1 - while i < len(formula) and formula[i].isdigit(): - num_str += formula[i] - i += 1 - carbon_atoms = int(num_str) - else: - carbon_atoms = 1 - - if carbon_atoms > 0: - mol_flow = stream.imol[chem.ID] - carbon_mol = mol_flow * carbon_atoms - if carbon_mol > threshold: - total_carbon += carbon_mol - carbon_components.append({ - 'chemical': chem.ID, - 'mol_flow': mol_flow, - 'carbon_atoms': carbon_atoms, - 'carbon_mol': carbon_mol - }) - - if carbon_components: - print(f"\n含碳组分分析:") - print("组分名称".ljust(20) + "摩尔流量(kmol/h)".ljust(15) + "碳原子数".ljust(10) + "碳摩尔流量(kmol/h)".ljust(15)) - print("-" * 65) - - for comp in carbon_components: - print(f"{comp['chemical']:<20} {comp['mol_flow']:>14.3f} {comp['carbon_atoms']:>9} {comp['carbon_mol']:>14.3f}") - - print(f"\n总碳摩尔流量: {total_carbon:.3f} kmol/h") - print(f"总碳质量流量: {total_carbon * 12.01:.3f} kg/h") - - # CO2分析 - if 'CO2' in stream.chemicals: - co2_mol = stream.imol['CO2'] - co2_mass = stream.imass['CO2'] - if co2_mol > threshold: - co2_mass_fraction = (co2_mass / total_mass * 100) if total_mass > 0 else 0 - co2_mol_fraction = (co2_mol / total_mol * 100) if total_mol > 0 else 0 - - print(f"\nCO2分析:") - print(f"CO2摩尔流量: {co2_mol:.3f} kmol/h") - print(f"CO2质量流量: {co2_mass:.3f} kg/h") - print(f"CO2质量分数: {co2_mass_fraction:.2f}%") - print(f"CO2摩尔分数: {co2_mol_fraction:.2f}%") - print(f"CO2当量质量: {co2_mol * 44.01:.3f} kg/h") - - # 其他重要气体 - important_gases = ['H2O', 'N2', 'O2', 'SO2', 'NO', 'NO2', 'CH4', 'N2O'] - found_gases = [] - - for gas in important_gases: - if gas in stream.chemicals: - mol_flow = stream.imol[gas] - mass_flow = stream.imass[gas] - if mol_flow > threshold: - mass_fraction = (mass_flow / total_mass * 100) if total_mass > 0 else 0 - mol_fraction = (mol_flow / total_mol * 100) if total_mol > 0 else 0 - found_gases.append({ - 'gas': gas, - 'mol_flow': mol_flow, - 'mass_flow': mass_flow, - 'mass_fraction': mass_fraction, - 'mol_fraction': mol_fraction - }) - - if found_gases: - print(f"\n其他重要气体:") - for gas_info in found_gases: - print(f"{gas_info['gas']}: {gas_info['mol_flow']:.3f} kmol/h, {gas_info['mass_flow']:.3f} kg/h, {gas_info['mass_fraction']:.2f}% mass, {gas_info['mol_fraction']:.2f}% mol") - - # 热力学性质 - print(f"\n热力学性质:") - print(f"温度: {stream.T:.2f} K ({stream.T - 273.15:.2f} °C)") - print(f"压力: {stream.P:.0f} Pa ({stream.P/101325:.3f} atm)") - print(f"焓: {stream.H:.3f} kJ/h") - - # 保存分析结果 - stream_analysis = { - 'stream_id': stream.ID, - 'source_unit': stream.source.ID if stream.source else 'Unknown', - 'total_mass_flow': stream.F_mass, - 'total_mol_flow': stream.F_mol, - 'components': components, - 'carbon_analysis': { - 'total_carbon_mol': total_carbon, - 'total_carbon_mass': total_carbon * 12.01, - 'carbon_components': carbon_components - }, - 'thermodynamic_properties': { - 'temperature': stream.T, - 'pressure': stream.P, - 'enthalpy': stream.H - } - } - - results['streams_analysis'].append(stream_analysis) - - return results - - # 便捷属性 - GWP相关 - @property - def material_GWP(self): - return self.get_material_impact(self.GWP_key) - - @property - def feedstock_GWP(self): - return self.get_complex_feeds_impact(self.GWP_key) - - @property - def net_electricity_GWP(self): - return self.get_net_electricity_impact(self.GWP_key) - @property def GWP(self): return self.get_total_impact(self.GWP_key) - # 便捷属性 - FEC相关 @property def material_FEC(self): return self.get_material_impact(self.FEC_key) @@ -625,57 +212,142 @@ def net_electricity_FEC(self): return self.get_net_electricity_impact(self.FEC_key) @property - def FEC(self): - return self.get_total_impact(self.FEC_key) - - # 便捷属性 - WC相关 - @property - def material_WC(self): - return self.get_material_impact(self.WC_key) - - @property - def feedstock_WC(self): - return self.get_complex_feeds_impact(self.WC_key) + def material_FEC_breakdown(self): + return self.get_material_impact_breakdown(self.FEC_key) @property - def net_electricity_WC(self): - return self.get_net_electricity_impact(self.WC_key) - - @property - def WC(self): - return self.get_total_impact(self.WC_key) + def FEC(self): + return self.get_total_impact(self.FEC_key) + def get_detailed_GWP_breakdown(self): + breakdown = {} + gwp_key = self.GWP_key + gwp_cfs = self.CFs[gwp_key] + functional_quantity = self.functional_quantity_per_h + + # 1. Feedstock impact + feedstock_impact = self.get_complex_feeds_impact(gwp_key) + breakdown['feedstock'] = feedstock_impact + + # 2. Material impacts from various chemicals + chemical_impacts = self.get_material_impact_breakdown(gwp_key) + + # Extract major chemicals (enzymes and oleyl alcohol go to other materials) + major_chemicals = ['NaOH', 'NH4OH', 'Lime'] + + for chem in major_chemicals: + if chem in chemical_impacts and abs(chemical_impacts[chem]) > 1e-6: + breakdown[chem.lower()] = chemical_impacts[chem] + + # 3. Other materials (including enzymes and oleyl alcohol) + other_materials = 0.0 + for chem_id, impact in chemical_impacts.items(): + if chem_id not in major_chemicals and abs(impact) > 1e-6: + other_materials += impact + breakdown['other_materials'] = other_materials + -# 使用示例 -def create_microalgae_lca(system, main_product, main_product_chemical_IDs, boiler): - """ - 创建微藻生物炼制厂的LCA对象 + + # 5. Net electricity impact + net_electricity_impact = self.get_net_electricity_impact(gwp_key) + breakdown['net_electricity'] = net_electricity_impact + + # 6. Direct emissions - only show final non-biogenic emissions + breakdown['direct_non_biogenic_emissions'] = self.direct_non_biogenic_emissions_GWP + + breakdown['total_actual'] = self.GWP + + return breakdown - 参数 - ---------- - system : System - 微藻生产系统 - main_product : Stream - 主产品流 - main_product_chemical_IDs : list - 主产品化学品ID列表 - boiler : Unit - 锅炉设备单元 + def print_GWP_breakdown(self, sort_by_magnitude=True, show_small_contributions=False): + breakdown = self.get_detailed_GWP_breakdown() + + print(f"Total GWP: {breakdown['total_actual']:.4f} kg CO2e/{self.functional_unit}") + print("\nComponent contributions:") + print("-" * 80) + print(f"{'Component Name':<30} {'Contribution':<15} {'Percentage':<10} {'Unit'}") + print("-" * 80) - 返回 - ---------- - LCA - 生命周期评估对象 - """ - # 影响因子 - # 示例数据,实际应用中需要替换为准确的数据 + display_items = [] + total_gwp = breakdown['total_actual'] + component_map = { + 'feedstock': 'Microalgae feedstock', + 'naoh': 'Sodium hydroxide (NaOH)', + 'nh4oh': 'Ammonium hydroxide (NH4OH)', + 'lime': 'Lime', + 'other_materials': 'Other materials', + 'net_electricity': 'Net electricity', + 'direct_non_biogenic_emissions': 'Direct non-biogenic emissions' + } + + for key, value in breakdown.items(): + if key == 'total_actual': + continue + + if not show_small_contributions and abs(value) < 1e-4: + continue + + display_name = component_map.get(key, key) + percentage = (value / total_gwp * 100) if total_gwp != 0 else 0 + display_items.append((display_name, value, percentage)) + + if sort_by_magnitude: + display_items.sort(key=lambda x: abs(x[1]), reverse=True) + + for name, value, percentage in display_items: + unit = f"kg CO2e/{self.functional_unit}" + print(f"{name:<30} {value:>14.4f} {percentage:>9.1f}% {unit}") + + return breakdown + + def get_GWP_breakdown_dataframe(self): + breakdown = self.get_detailed_GWP_breakdown() + + component_map = { + 'feedstock': 'Microalgae feedstock', + 'naoh': 'Sodium hydroxide (NaOH)', + 'nh4oh': 'Ammonium hydroxide (NH4OH)', + 'lime': 'Lime', + 'other_materials': 'Other materials', + 'net_electricity': 'Net electricity', + 'direct_non_biogenic_emissions': 'Direct non-biogenic emissions' + } + + data = [] + total_gwp = breakdown['total_actual'] + + for key, value in breakdown.items(): + if key == 'total_actual': + continue + + display_name = component_map.get(key, key) + percentage = (value / total_gwp * 100) if total_gwp != 0 else 0 + + data.append({ + 'Component': display_name, + 'Original_key': key, + f'GWP_contribution (kg CO2e/{self.functional_unit})': value, + 'Percentage (%)': percentage, + 'Absolute_value': abs(value) + }) + + df = pd.DataFrame(data) + df = df.sort_values('Absolute_value', ascending=False) + df = df.drop('Absolute_value', axis=1) + + return df + + +def create_microalgae_lca(system, main_product, main_product_chemical_IDs, boiler): CFs = { + # ============================================================================= + # 100-year global warming potential (GWP) in kg CO2-eq/kg + # ============================================================================= 'GWP_100':{ 'Electricity': 0.36, # [kg*CO2*eq / kWhr] From GREET; NG-Fired Simple-Cycle Gas Turbine CHP Plant # 0.66 is the GWP from producing diesel from GREET; Conventional diesel from crude oil for US Refineries. # Downstream fuel emissions are added in. Accounts for how biodiesel has less energy than diesel. 'CH4': 0.33, # Natural gas from shell conventional recovery, GREET; includes non-biogenic emissions - 'H2SO4': 0.04447, # kg CO2e/kg 'NaOH': 2.01, # GREET 'NH4OH': 1.28304, # multiplied by chemicals.NH3.MW/chemicals.NH4OH.MW, @@ -685,10 +357,17 @@ def create_microalgae_lca(system, main_product, main_product_chemical_IDs, boile 'GlucoAmylase': 6.16, # from Succinic project 'AlphaAmylase': 6.16, # from Succinic project - 'Octanol': 1.9, # kg CO2e/kg - - 'Microalgae': 0.15, # + 'OleylAlcohol': 1.9, # kg CO2e/kg + 'Lime': 1.29 * 56.0774/74.093, # CaO to Ca(OH)2 + 'Microalgae': 0.1, # + 'CO2': 0.87104, # ecoinvent 3.8 carbon dioxide production, liquid, RoW + 'CH4': 0.40, # NA NG from shale and conventional recovery + 'Electricity': 0.4490 # kg CO2-eq/kWh GREET 2022 US Mix # assume production==consumption, both in kg CO2-eq/kWh }, + + # ============================================================================= + # Fossil energy consumption (FEC), in MJ/kg of material + # ============================================================================= 'FEC': { 'Electricity': 10.0, # MJ/kWh 'H2SO4': 568.98/1e3, @@ -697,86 +376,32 @@ def create_microalgae_lca(system, main_product, main_product_chemical_IDs, boile 'CalciumDihydroxide': 4.0, # MJ/kg 'CH4': 55.0, # MJ/kg 'Ethanol': 16, - 'Octanol': 45.0, # MJ/kg + 'OleylAlcohol': 45.0, # MJ/kg 'GlucoAmylase': 90.0, # MJ/kg 'AlphaAmylase': 90.0, # MJ/kg + 'Lime': 4.896 * 56.0774/74.093, # CaO to Ca(OH)2 'Microalgae': 2.0, # MJ/kg - }, - 'WC': { # 水消耗 - 'Electricity': 0.005, # m3 / kWh - 'H2SO4': 0.01, # m3 / kg - 'NaOH': 0.02, # m3 / kg - 'NH4OH': 0.015, # m3 / kg - 'CalciumDihydroxide': 0.007, # m3 / kg - 'CH4': 0.0, # m3 / kg - 'CO2': 0.0, # m3 / kg - 'Octanol': 0.05, # m3 / kg - 'GlucoAmylase': 0.1, # m3 / kg - 'AlphaAmylase': 0.1, # m3 / kg - 'Microalgae': 0.2, # m3 / kg + 'Electricity': 5.724, # MJ/kWh # GREET 2022 US Mix #assume production==consumption, both in MJ/kWh + 'CH4': 50, # NA NG from shale and conventional recovery } } - # 获取系统中的冷却塔和冷冻水处理单元 - cooling_tower = None - chilled_water_processing_units = [] + cooling_tower = next((unit for unit in system.units + if unit.__class__.__name__ == 'CoolingTower'), None) + chilled_water_processing_units = [unit for unit in system.units + if unit.__class__.__name__ == 'ChilledWaterPackage'] + boiler = system.flowsheet.unit.BT601 - for unit in system.units: - if unit.__class__.__name__ == 'CoolingTower': - cooling_tower = unit - elif unit.__class__.__name__ == 'ChilledWaterPackage': - chilled_water_processing_units.append(unit) - - # 尝试找到系统中的锅炉涡轮发电机 - if boiler is None: - for unit in system.units: - # 优先查找锅炉涡轮发电机 - if (isinstance(unit, bst.facilities.BoilerTurbogenerator) or - ('BT' in unit.ID and hasattr(unit, 'turbogenerator_efficiency'))): - boiler = unit - break - - # 如果没有找到锅炉涡轮发电机,查找普通锅炉 - if boiler is None: - for unit in system.units: - if 'boiler' in unit.__class__.__name__.lower() or 'BT' in unit.ID: - boiler = unit - break - - # 如果没有找到锅炉,使用最大加热负荷的热交换器 - if boiler is None: - heat_exchangers = [] - for unit in system.units: - if unit.__class__.__name__ == 'HXutility' and hasattr(unit, 'duty') and unit.duty > 0: - heat_exchangers.append(unit) - - if heat_exchangers: - # 选择具有最大加热负荷的热交换器 - boiler = max(heat_exchangers, key=lambda x: x.duty) - - # 创建复合进料字典 complex_feeds = { - 'Microalgae': (system.feeds[0], 'dry'), # 微藻进料 + 'Microalgae': (system.feeds[0], 'dry'), } - # 确定生物来源碳输入流 - # 找到微藻原料流 - microalgae_feed = None - for feed in system.feeds: - if 'microalgae' in feed.ID.lower(): - microalgae_feed = feed - break - - # 如果没有找到microalgae流,使用第一个包含碳的原料流 - if microalgae_feed is None: - for feed in system.feeds: - if feed.get_atomic_flow('C') > 0: - microalgae_feed = feed - break + microalgae_feed = next((feed for feed in system.feeds + if 'microalgae' in feed.ID.lower()), + next((feed for feed in system.feeds + if feed.get_atomic_flow('C') > 0), None)) input_biogenic_carbon_streams = [microalgae_feed] if microalgae_feed else [] - - # 创建LCA对象 microalgae_lca = LCA( system=system, CFs=CFs, @@ -789,65 +414,20 @@ def create_microalgae_lca(system, main_product, main_product_chemical_IDs, boile complex_feeds=complex_feeds, cooling_tower=cooling_tower, chilled_water_processing_units=chilled_water_processing_units, - has_turbogenerator=None, # 自动确定 - functional_unit='1 kg', # 每千克产品 + functional_unit='1 kg', add_EOL_GWP=True, input_biogenic_carbon_streams=input_biogenic_carbon_streams ) return microalgae_lca -def print_non_biogenic_breakdown(lca, threshold=1e-4): - print('\n-- 非生物源直接排放明细 (kg CO2-eq / 功能单位) --') - tot = 0 - for s in lca.emissions: - if s in lca.input_biogenic_carbon_streams: - # 生物源流 => 扣除,不计入非生物 - continue - C = s.get_atomic_flow('C') # kmol/h - if C == 0: - continue - impact = C * 44 / lca.functional_quantity_per_h - if abs(impact) > threshold: - print(f'{s.ID:<15} {impact:8.4f}') - tot += impact - if lca.add_EOL_GWP: - print(f'End-of-Life {lca.EOL_GWP:8.4f}') - tot += lca.EOL_GWP - print('-----------------------------------------') - print(f'Total {tot:8.4f}') - - -# 主程序运行示例 if __name__ == "__main__": - import biosteam as bst - from microalgae import system - from microalgae._chemicals import chems - - # 初始化系统 bst.settings.set_thermo(chems) - microalgae_sys = system.create_microalgae_MCCA_production_sys() + microalgae_sys = create_microalgae_MCCA_production_sys() microalgae_sys.simulate() - - # 获取主产品流和主产品化学品ID main_product = microalgae_sys.flowsheet.stream.caproic_acid_product main_product_chemical_IDs = ['CaproicAcid'] - - # 尝试找到锅炉单元 - boiler = None - for unit in microalgae_sys.units: - if hasattr(unit, 'heat_utilities') and unit.heat_utilities: - boiler = unit - break - - # 如果没有找到锅炉,使用第一个热交换器 - if boiler is None: - for unit in microalgae_sys.units: - if unit.__class__.__name__ == 'HXutility': - boiler = unit - break - - # 创建LCA对象 + boiler = microalgae_sys.flowsheet.unit.BT601 lca = create_microalgae_lca( system=microalgae_sys, main_product=main_product, @@ -855,80 +435,11 @@ def print_non_biogenic_breakdown(lca, threshold=1e-4): boiler=boiler ) - # 输出LCA结果 - print("\n===== 微藻生物炼制厂LCA结果 =====") - print(f"系统碳平衡: {lca.system_carbon_balance:.4f}") - print(f"GWP (kg CO2e/{lca.functional_unit}): {lca.GWP:.4f}") - print(f"FEC (MJ/{lca.functional_unit}): {lca.FEC:.4f}") - print(f"WC (m³/{lca.functional_unit}): {lca.WC:.4f}") - - # 输出GWP明细 - print(f"----- GWP明细 -----") - print(f"物质影响: {lca.material_GWP:.4f} ({lca.material_GWP/lca.GWP*100 if lca.GWP else 0:.1f}%)") - print(f"电力影响: {lca.net_electricity_GWP:.4f} ({lca.net_electricity_GWP/lca.GWP*100 if lca.GWP else 0:.1f}%)") - print(f"原料影响: {lca.feedstock_GWP:.4f} ({lca.feedstock_GWP/lca.GWP*100 if lca.GWP else 0:.1f}%)") - print(f"直接排放(非生物源): {lca.direct_non_biogenic_emissions_GWP:.4f} ({lca.direct_non_biogenic_emissions_GWP/lca.GWP*100 if lca.GWP else 0:.1f}%)") - - # 分析emissions分解 - #print(f"\n{'='*60}") - #lca.analyze_emissions_breakdown() - - # 生成并保存详细报告 - report = lca.generate_report() - print("\n完整报告已生成。报表示例:") - print(report[:500] + "...\n") - - # 导出结果到Excel - # lca.export_results("microalgae_lca_results.xlsx") - - # 1) 物质影响 – 按单一化学品列出 - mat_break = lca.get_material_impact_breakdown('GWP_100') - print('\n-- Material impact breakdown (kg CO2-eq / kg product) --') - for k, v in mat_break.items(): - if abs(v) > 1e-6: # 过滤 0 - print(f'{k:<15} {v:8.4f}') - - # 2) 电力影响 – 直接由净电力×CF 得到 - elec_CF = lca.CFs['GWP_100']['Electricity'] - net_kWh = lca.net_electricity # kWh h⁻¹ - per_kg = net_kWh * elec_CF / lca.functional_quantity_per_h - print(f'\nElectricity ({net_kWh:.3f} kWh/h) × {elec_CF} = {per_kg:.4f} kg CO2-eq') - - # 3) 复合进料(feedstock)影响 - feed_imp = lca.get_complex_feeds_impact('GWP_100') - print(f'\nFeedstock impact = {feed_imp:.4f} kg CO2-eq') - - # 若想看各复合进料流的贡献: - for ID, (s, _) in lca.complex_feeds.items(): - impact = lca.get_complex_feed_impact_by_ID('GWP_100', ID) - print(f' {ID:<15} {impact:8.4f}') - - # 4) 直接排放 - print(f'\nDirect non-biogenic emissions GWP = {lca.direct_non_biogenic_emissions_GWP:.4f}') - print('\n 排放流详细:') - for s in lca.emissions: - C = s.get_atomic_flow("C") - if C>0: - stream_gwp = C * 44 / lca.functional_quantity_per_h # 44 g CO2 / mol C - if stream_gwp!=0: - print(f' {s.ID:<15} {stream_gwp:8.4f} kg CO2-eq') - - # 若已把生物源排放列入 `input_biogenic_carbon_streams`, - # 也可打印 biogenic 值: - print(f'\nBiogenic CO2 credit = {lca.biogenic_emissions_GWP:.4f}') - print_non_biogenic_breakdown(lca) - - # u = microalgae_sys.flowsheet.unit - # s = microalgae_sys.flowsheet.stream - - # s.s49.show() - # print('From:', s.s49.source) - # print('To :', s.s49.sink) + # Call detailed breakdown method + breakdown_data = lca.print_GWP_breakdown(sort_by_magnitude=True, show_small_contributions=False) + # Save DataFrame to CSV file (optional) + # breakdown_df.to_csv('microalgae_gwp_breakdown.csv', index=False, encoding='utf-8-sig') - # s.s51.show() - # u.R501.show() - # print('From :', s.s51.source) - # print('To :', s.s51.sink) diff --git a/biorefineries/microalgae/model_utils.py b/biorefineries/microalgae/model_utils.py new file mode 100644 index 00000000..bf7c1053 --- /dev/null +++ b/biorefineries/microalgae/model_utils.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Model utilities for microalgae biorefinery uncertainty analysis + +Based on succinic project's model_utils.py but adapted for microalgae system structure +""" +from pandas import DataFrame, read_excel +import chaospy as shape +import numpy as np +import biosteam as bst +from biosteam.evaluation import Model +from .system import microalgae_mcca_sys, microalgae_tea + +class MicroalgaeModel(bst.Model): + def __init__(self, system, metrics=None, specification=None, + parameters=None, retry_evaluation=True, exception_hook='warn', + namespace_dict={}): + Model.__init__(self, system=system, specification=specification, + parameters=parameters, retry_evaluation=retry_evaluation, exception_hook=exception_hook) + self.namespace_dict = namespace_dict + # Set metrics after initialization + if metrics is not None: + self.metrics = metrics + + def load_parameter_distributions(self, distributions, namespace_dict=None): + namespace_dict = namespace_dict or self.namespace_dict + + df = distributions + if type(df) is not DataFrame: + df = read_excel(distributions) + + create_function = self.create_function + param = self.parameter + + for i, row in df.iterrows(): + name = row['Parameter name'] + element = row['Element'] + kind = row['Kind'] + units = row['Units'] + baseline = row['Baseline'] + shape_data = row['Shape'] + lower, midpoint, upper = row['Lower'], row['Midpoint'], row['Upper'] + load_statements = str(row['Load statement']) + + D = None + if shape_data.lower() in ['triangular', 'triangle']: + D = shape.Triangle(lower, midpoint, upper) + elif shape_data.lower() in ['uniform']: + D = shape.Uniform(lower, upper) + + if D is not None: + param(name=name, + setter=create_function(load_statements, namespace_dict), + element=element, + kind=kind, + units=units, + baseline=baseline, + distribution=D) + + def create_function(self, code, namespace_dict): + def wrapper_fn(statement): + def f(x): + namespace_dict['x'] = x + exec(statement, namespace_dict) + return f + return wrapper_fn(code) + +def create_unit_groups(): + units_dict = {unit.ID: unit for unit in microalgae_mcca_sys.units} + unit_groups = [] + + # Area 1: Microalgae cultivation and harvesting + cultivation_units = [units_dict[uid] for uid in ['U101'] if uid in units_dict] + if cultivation_units: + unit_groups.append(bst.UnitGroup('Cultivation and harvesting', units=cultivation_units)) + + # Area 2: Pretreatment and hydrolysis + pretreatment_unit_ids = ['T201', 'P201', 'M201', 'P202', 'H201', 'R201', + 'T202', 'P203', 'R202', 'P204', 'H202', 'T203', + 'P205', 'T204', 'P206', 'T205', 'P207', 'S201', + 'M202', 'R203', 'H203', 'M203', 'R204', 'S202', 'P208'] + pretreatment_units = [units_dict[uid] for uid in pretreatment_unit_ids if uid in units_dict] + if pretreatment_units: + unit_groups.append(bst.UnitGroup('Pretreatment and hydrolysis', units=pretreatment_units)) + + # Area 3: Conversion + conversion_units = [units_dict[uid] for uid in ['H301', 'M301', 'T301', 'P301', 'R301', 'T302', 'S301'] if uid in units_dict] + if conversion_units: + unit_groups.append(bst.UnitGroup('Conversion', units=conversion_units)) + + # Area 4: Separation + separation_units = [units_dict[uid] for uid in ['M401', 'S402', 'D401', 'D402', 'D403', 'D404', 'D405'] if uid in units_dict] + if separation_units: + unit_groups.append(bst.UnitGroup('Separation', units=separation_units)) + + # Area 5: Waste treatment - Anaerobic digestion + waste_units = [units_dict[uid] for uid in ['M501', 'R501', 'M502', 'M503'] if uid in units_dict] + if waste_units: + unit_groups.append(bst.UnitGroup('Waste treatment and biogas', units=waste_units)) + + # Area 6: Storage + storage_unit_ids = ['T601', 'P601', 'T602', 'P602', 'T603', 'P603', 'T604', 'P604', 'T605', 'P605'] + storage_units = [units_dict[uid] for uid in storage_unit_ids if uid in units_dict] + if storage_units: + unit_groups.append(bst.UnitGroup('Storage', units=storage_units)) + + # Wastewater + wastewater_units = [units_dict[uid] for uid in ['WastewaterT'] if uid in units_dict] + remaining_units = [u for u in microalgae_mcca_sys.units + if u not in [unit for group in unit_groups for unit in group.units] + and u.ID not in ['WastewaterT']] + wastewater_units.extend(remaining_units) + if wastewater_units: + unit_groups.append(bst.UnitGroup('Wastewater treatment', units=wastewater_units)) + + # Boiler & turbogenerator + bt_units = [units_dict[uid] for uid in ['BT601'] if uid in units_dict] + if bt_units: + unit_groups.append(bst.UnitGroup('BT', units=bt_units)) + + # Heat Exchanger Network + hxn_units = [units_dict[uid] for uid in ['HXN601'] if uid in units_dict] + if hxn_units: + hxn_group = bst.UnitGroup('Heat exchange network', units=hxn_units) + hxn_group.filter_savings = False + unit_groups.append(hxn_group) + + # Other facilities + facility_units = [units_dict[uid] for uid in ['CT', 'PWC', 'ADP', 'CWP'] if uid in units_dict] + remaining_units = [u for u in microalgae_mcca_sys.units + if u not in [unit for group in unit_groups for unit in group.units] + and u.ID not in ['CT', 'PWC', 'ADP', 'CWP']] + facility_units.extend(remaining_units) + if facility_units: + unit_groups.append(bst.UnitGroup('Other facilities', units=facility_units)) + + # Fixed Operating Costs + foc_group = bst.UnitGroup('Fixed operating costs') + unit_groups.append(foc_group) + for ug in unit_groups: + ug.autofill_metrics(shorthand=False, + electricity_production=False, + electricity_consumption=True, + material_cost=True) + + # Special metric configurations + for ug in unit_groups: + if ug.name in ['Storage', 'Other facilities'] and ug.metrics: + for metric in ug.metrics: + if metric.name.lower() == 'material cost': + metric.getter = lambda: 0.0 + break + + # BT unit group + for ug in unit_groups: + if ug.name == 'BT' and ug.metrics: + for i, metric in enumerate(ug.metrics): + if metric.name.lower() == 'material cost': + ug.metrics[i] = bst.evaluation.Metric( + 'Material cost', + getter=lambda: microalgae_tea.utility_cost / microalgae_tea.operating_days / 24, + units='USD/hr', + element=None + ) + break + + # Fixed Operating Costs + for ug in unit_groups: + if ug.name == 'Fixed operating costs' and ug.metrics: + for i, metric in enumerate(ug.metrics): + if metric.name.lower() == 'material cost': + ug.metrics[i] = bst.evaluation.Metric( + 'Material cost', + getter=lambda: microalgae_tea.FOC / microalgae_tea.operating_days / 24, + units='USD/hr', + element=None + ) + break + + return unit_groups + +def get_unit_groups(): + return create_unit_groups() \ No newline at end of file diff --git a/biorefineries/microalgae/parameter_distributions.xlsx b/biorefineries/microalgae/parameter_distributions.xlsx index 35644b34bbb0b9f9b42f9addd466961917ecad0f..05505cdcda89e5cfa0e3e6194b83a7e60d4c898d 100644 GIT binary patch delta 7711 zcmZ8mbx>U0lO3Evhu|)O!9BRU2MO*T+}#EU?l3q62u^Sd?gR@CL4&(Pa9_UPR_(XD z@4Z{~Zr!f_qwAhNUFXD^9Dl-BQGkQT0U!d90RR9cAcx7$(G~^(0M+8ZhlM2feC%UG z3psdpAdu3R?h#sagGQBa=}F)QWs5-%@rS(=HLR)`-JsS-cFb~Sr|>kZ86 znBix~Q|c~C-|)OEypi`0$Jn;)FNim*spaX`a^;oW9q6H6TRuAn1;TO24+0%`L-XU)#W)3H;kF9%8Wsdkg z-i1Jnb4cJBmVLdevN$L|Y$m~j>v1@-_;nLo!ncb~g>+SLK1CdnLvI;6src4n_lHvaQLS3XLfPENKD+9=67Yb-xcH(UW5VA~^aXWph%-%NE&n*{L#h#;{BZ zWiH=MN;N%n=%k?>7K(crE3PDK{-LjBN>y-5vGK{MY|NeRXwb{ z{gCxP{x;Llt{p`U^jasJpFpRR_9n@$3{k(Lgln?h{HEJ85ia7Kci>JNWPMheJt&{* zQWg(ba*#WN+$)o0C42?VKC<(o4L9QYZi2)0G)EvbjF9M^u0X84rYU6E@+o;hk1tJT z)6QI90hDH9!Jg4TGJ2DAOsSY7hC}yoCd4*1){HdtIR-kZlntDm8nodMX z&t@?^CccHPYpoaSg5m{j7?T@km*La%$uAM3E>^HU3sJ_Toz&tct-E4=vAKgX{|^Td3C~aFCobG0scO)f;P)V=u052*gX8%EA2;%2t^8O@E{*}Td(eQ=P_R|a&s}-FHY;{5JZx_Rh zf~f}(YyRcWTQx1S4t#oPtodvb>TAx4L+%mbF93$E7UG)@L-GQ5v-YjXk764Ap5132 zr^*9b^ladebU9F$3En2DbbYESe7r>_sq_s*IXUwPyOc}lUF>%CSc?354!1wHd5pU8 zxXNdsb<%=;v^Aa)gL$jvMuOedgpI%9;S}(SLI~puylT986`qh8A(^TW7RQxwSG6;i z-|4AnIrE8?zE~mV{-*@=jw$rol)fONqCVnKp;4;GbwUtN2tl6E{fNO{Q1cT7*jFl_=`4ak5k2& zO}y+~k9kjQ6Swe)SoP^#s^oXfQP)4XQXO%2{YNeJ6AFF`41V@WjXctl*F~|-F7ppI z=kE9W`Xbc!SwE)m=*h9yoYT^TRRV^!^vRym;xZAEVq`qS(~I|>$&W?vs*d3x_i86| zJskmRIO=`IYQW4GiHo6wh3YORV)-*ou*j}42L0+&Z%;=i_5{yFo~V~-VocIxF0q-% z#yU}&nq;dN1x3rb3udlpSyC0J;AwzN3Y!d&+onAy_d3T^8PZr7oOwF};S8UkXo{8D z&`E-fu`f-JvpJ(t1WW1YSgIaCQnKdk%uJj+f9flm&`v7&JV8K?9Atur?uI-`@6q9 z-}!OBwrcE(Kr}AA+Mh4ZUfhIpa*WQZW~jSX?c)nhnx?#5*e51-50+V0R@+a0@C-&v zF*4)m9v4Z4;oVMC$wYr?pqtEx;?d{|%0{Cw`&g?Mgr?#VH{7u>6EzTMM(~yLfFTMB z-|N!V3AbqEqFg3}W$$h&IPUF6_H?V~Mo}d<-7%?{bNiZWS3NZHt=ci#(@U-ylEBM%5mr0b-`1B<+>>~|J41*w z`v9&^EpWY3XeMUupz­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 BPCftt diff --git a/biorefineries/microalgae/streams.py b/biorefineries/microalgae/streams.py index ce4026e8..277a2220 100644 --- a/biorefineries/microalgae/streams.py +++ b/biorefineries/microalgae/streams.py @@ -24,7 +24,7 @@ from biosteam import stream_kwargs microalgae_feed = stream_kwargs('microalgae', - Microalgae=8000, + Microalgae=5000, units='kg/hr', price = 0.15 # this is a baseline value for TEA (cost for cultivation, harvesting and trasporting) ) diff --git a/biorefineries/microalgae/system.py b/biorefineries/microalgae/system.py index 4d8b5b00..25e10ed2 100644 --- a/biorefineries/microalgae/system.py +++ b/biorefineries/microalgae/system.py @@ -25,22 +25,21 @@ import numpy as np import thermosteam as tmo from biosteam import Stream, SystemFactory -from biosteam.units import MixTank, Pump, StorageTank, HXutility, Mixer, Splitter, MultiStageMixerSettlers -from biosteam.facilities import AirDistributionPackage, ProcessWaterCenter, CoolingTower, ChilledWaterPackage +from biosteam.units import Pump, StorageTank, HXutility, Mixer, Splitter, MultiStageMixerSettlers +from biosteam.facilities import AirDistributionPackage, ProcessWaterCenter, CoolingTower, ChilledWaterPackage, HeatExchangerNetwork from biosteam import main_flowsheet from .units import ( FeedstockPreprocessing, AcidPretreatmentReactor, Saccharification, SolidLiquidSeparation, MCCAFermentation, - ButanolDistillation, ButyricAcidDistillation, CaproicAcidDistillation, HeptanoicAcidDistillation, CaprylicAcidDistillation, NeutralizationTank, AnaerobicDigestion ) from .utils import price -from .facilities import BT, HXNWithMin from ._chemicals import chems from .tea import create_tea from .streams import microalgae_feed import warnings -import re -warnings.filterwarnings("ignore", message=r".*Hot utility load is negative.*", category=RuntimeWarning, module=r"biosteam\.facilities") +# Filter out specific warnings +warnings.filterwarnings("ignore", message="phase equilibrium solution results in negative flow rates") +warnings.filterwarnings("ignore", message=".*has no defined Dortmund groups.*") warnings.filterwarnings("ignore", message=".*has been replaced in registry") warnings.filterwarnings("ignore", category=bst.exceptions.CostWarning) warnings.filterwarnings("ignore", message=".*moisture.*is smaller than the desired.*") @@ -49,23 +48,12 @@ # ------------------------------------------------------------------ # Utility: labor cost scaling with plant size # ------------------------------------------------------------------ - def compute_labor_cost(dry_tpd: float, base_tpd: float = 2205.0, base_cost: float = 3212962.0, exponent: float = 0.2, floor_tpd: float = 100.0, floor_cost: float = 0.5e6) -> float: - """Scale labor cost with plant size but keep a fixed cost below *floor_tpd*. - Parameters - ---------- - dry_tpd - Dry feed rate (t/d). - base_tpd, base_cost, exponent - Parameters of the power-law model. - floor_tpd, floor_cost - If dry_tpd < floor_tpd, return floor_cost (representing baseline staffing). - """ if dry_tpd < floor_tpd: return floor_cost return base_cost * (dry_tpd / base_tpd) ** exponent @@ -92,7 +80,7 @@ def compute_labor_cost(dry_tpd: float, dict(ID='caproic_acid_product', thermo=chems), dict(ID='heptanoic_acid_product', thermo=chems), dict(ID='caprylic_acid_product', thermo=chems)] -) + ) def create_microalgae_MCCA_production_sys(ins, outs): # Set the thermodynamic package explicitly tmo.settings.set_thermo(chems) @@ -103,8 +91,7 @@ def create_microalgae_MCCA_production_sys(ins, outs): butyric_acid_product, caproic_acid_product, heptanoic_acid_product, - caprylic_acid_product, - ) = outs + caprylic_acid_product) = outs # Calculate all required stream properties based on feed microalgae_mass = microalgae_feed.F_mass @@ -118,16 +105,23 @@ def create_microalgae_MCCA_production_sys(ins, outs): acid_solution_mass = pure_H2SO4 / acid_purity water_mass_acid = acid_solution_mass * (1 - acid_purity) SulfuricAcid = Stream('sulfuricacid', H2SO4=pure_H2SO4, Water=water_mass_acid, units='kg/hr', price=price['SulfuricAcid']) + + # Store reference for later specification + _sulfuric_acid_stream = SulfuricAcid # Ammonium Hydroxide for neutralization h2so4_mol = pure_H2SO4 * 1000 / 98 # mol mass nh4oh_mol = h2so4_mol * 0.1 # preadjustment nh4oh_mass = nh4oh_mol * 35 / 1000 # mol mass to mass ammonium_hydroxide = Stream('ammonium_hydroxide', NH4OH=nh4oh_mass, units='kg/hr', price=price['AmmoniumHydroxide']) # Enzyme dosages - glucoamylase_mass = float(microalgae_mass * 0.0011) # 确保为标量 - alpha_amylase_mass = float(microalgae_mass * 0.0082) # 确保为标量,ref from cron project + glucoamylase_mass = float(microalgae_mass * 0.0011) # ref from cron project + alpha_amylase_mass = float(microalgae_mass * 0.0082) # ref from cron project glucoamylase = Stream('glucoamylase', GlucoAmylase=glucoamylase_mass, units='kg/hr', price=price['GlucoAmylase']) alpha_amylase = Stream('alpha_amylase', AlphaAmylase=alpha_amylase_mass, units='kg/hr', price=price['AlphaAmylase']) + + # Store references for later specification + _glucoamylase_stream = glucoamylase + _alpha_amylase_stream = alpha_amylase # NaOH for pH adjustment naoh1_mass = microalgae_mass * 0.02 # pH 4.5 naoh2_mass = microalgae_mass * 0.02 # pH 5.5 @@ -135,11 +129,13 @@ def create_microalgae_MCCA_production_sys(ins, outs): naoh = Stream('naoh', NaOH=total_naoh_mass, units='kg/hr', price=price['NaOH']) # Yeast addition in MCCA fermentation yeast_mass = microalgae_mass * 5 / 75 - yeast = Stream('yeast', Yeast=yeast_mass, units='kg/hr', price=price['Yeast']) - # Octanol for extraction - fresh_octanol = Stream('fresh_octanol', Octanol=200, units='kg/hr', price=price['Octanol']) - octanol_recycle = Stream('octanol_recycle', Octanol=50, units='kg/hr') - octanol_feed = bst.Stream() + fresh_yeast = Stream('fresh_yeast', Yeast=yeast_mass, units='kg/hr', price=price['Yeast']) + yeast_recycle = Stream('yeast_recycle', Yeast=0, units='kg/hr') + yeast_feed = bst.Stream() + # OleylAlcohol for extraction + fresh_oleylalcohol = Stream('fresh_oleylalcohol', OleylAlcohol=200, units='kg/hr', price=price['OleylAlcohol']) + oleylalcohol_recycle = Stream('oleylalcohol_recycle', OleylAlcohol=50, units='kg/hr') + oleylalcohol_feed = bst.Stream() # Assign prices to product streams #butanol_product.price = price['Butanol'] butyric_acid_product.price = price['ButyricAcid'] @@ -185,41 +181,130 @@ def create_microalgae_MCCA_production_sys(ins, outs): # Area 3: Fermentation for MCCA production # ===================== H301 = HXutility('H301', P208-0, T=37+273.15) - T301 = StorageTank('T301', yeast) + + # Yeast mixing for fresh and recycle + M301 = Mixer('M301', [fresh_yeast, yeast_recycle], yeast_feed) + @M301.add_specification(run=True) + def adjust_fresh_yeast(): + total_yeast_needed = yeast_mass + recycle = yeast_recycle.imass['Yeast'] + fresh = max(total_yeast_needed - recycle, total_yeast_needed * 0.05) # 5% fresh yeast supply + fresh_yeast.imass['Yeast'] = fresh + + T301 = StorageTank('T301', yeast_feed) P301 = Pump('P301', T301-0) - R301 = MCCAFermentation('R301', [H301-0, P301-0], microalgae_mass_flow=microalgae_mass, titer=60.0) - T302 = MixTank('T302', [R301-1]) + R301 = MCCAFermentation('R301', [H301-0, P301-0], ['', '', '', yeast_recycle], microalgae_mass_flow=microalgae_mass, titer = 2.003) + + # Add C6 yield factor specification + @R301.add_specification(run=True) + def set_C6_yield_factor(factor=1.0): + R301.caproic_acid_yield_factor = factor + + # Add C6 titer specification + @R301.add_specification(run=True) + def set_C6_titer(titer=2.003): + R301.titer = titer + + # Bind specification functions as methods for parameter loading + R301.set_C6_yield_factor = lambda x: setattr(R301, 'caproic_acid_yield_factor', x) + R301.set_C6_titer = lambda x: setattr(R301, 'titer', x) + + T302 = Mixer('T302', [R301-1]) S301 = SolidLiquidSeparation('S301', R301-0) # ===================== # Area 4: Product extraction # ===================== - M401 = Mixer('M401', [fresh_octanol, octanol_recycle], octanol_feed) + M401 = Mixer('M401', [fresh_oleylalcohol, oleylalcohol_recycle], oleylalcohol_feed) @M401.add_specification(run=True) - def adjust_fresh_octanol(): - total_octanol = 200 - recycle = octanol_recycle.imass['Octanol'] - fresh = max(total_octanol - recycle, 1e-3) - fresh_octanol.imass['Octanol'] = fresh - IDs = ['Water', 'AceticAcid', 'PropionicAcid', 'ButyricAcid', 'ValericAcid', 'CaproicAcid','CaprylicAcid', 'HeptanoicAcid', 'Butanol', 'Octanol'] + def adjust_fresh_oleylalcohol(): + total_oleylalcohol = 200 + recycle = oleylalcohol_recycle.imass['OleylAlcohol'] + fresh = max(total_oleylalcohol - recycle, 1e-3) + fresh_oleylalcohol.imass['OleylAlcohol'] = fresh + IDs = ['Water', 'AceticAcid', 'PropionicAcid', 'ButyricAcid', 'ValericAcid', 'CaproicAcid','CaprylicAcid', 'HeptanoicAcid', 'Butanol', 'OleylAlcohol'] K = np.array([1/5000, 0.24, 1.29, 5000/1, 13.58, 5000/1, 5000/1, 5000/1, 5000/1, 100000/1]) S402 = MultiStageMixerSettlers( 'S402', partition_data={'K': K, 'IDs': IDs}, N_stages=5, - ins=[S301-0, M401-0], - outs=['extracted_organic', 'aqueous_raffinate'] + ins=[S301-0, M401-0] ) + + # Add extraction efficiency specification + original_K = K.copy() + @S402.add_specification(run=True) + def set_extraction_efficiency(efficiency=1.0): + S402.partition_data['K'] = original_K * efficiency + + # Bind as method for parameter loading + S402.set_extraction_efficiency = lambda x: setattr(S402, 'partition_data', {'K': original_K * x, 'IDs': IDs}) + #D401 = ButanolDistillation('D401', S402-0) #D401.check_LHK = False - D402 = ButyricAcidDistillation('D402', S402-0) - D402.check_LHK = False - D403 = CaproicAcidDistillation('D403', D402-1) - D403.check_LHK = False - D404 = HeptanoicAcidDistillation('D404', D403-1) - D404.check_LHK = False - D405 = CaprylicAcidDistillation('D405', D404-1, ['', octanol_recycle]) - D405.check_LHK = False + D402 = bst.BinaryDistillation('D402', S402-0, LHK=('ButyricAcid', 'CaproicAcid'), + Lr=0.99, Hr=0.99, k=1.2, + partial_condenser=False, + is_divided=True) + #D402.check_LHK = False + D403 = bst.BinaryDistillation('D403', D402-1, LHK=('CaproicAcid', 'HeptanoicAcid'), + Lr=0.99, Hr=0.99, k=1.2, + partial_condenser=False, + is_divided=True + ) + #D403.check_LHK = False + D404 = bst.BinaryDistillation('D404', D403-1, LHK=('HeptanoicAcid', 'CaprylicAcid'), + Lr=0.99, Hr=0.99, k=1.2, + partial_condenser=False, + is_divided=True) + #D404.check_LHK = False + D405 = bst.BinaryDistillation('D405', D404-1, ['', oleylalcohol_recycle], LHK=('CaprylicAcid', 'OleylAlcohol'), + Lr=0.99, Hr=0.99, k=1.2, + partial_condenser=False, + is_divided=True, + product_specification_format='Recovery') + #D405.check_LHK = False + + # Add distillation efficiency specification + distillation_units = [D402, D403, D404, D405] + @D403.add_specification(run=True) # Use D403 as the representative unit + def set_distillation_efficiency(efficiency=1.0): + for unit in distillation_units: + unit.Lr = 0.99 * efficiency + unit.Hr = 0.99 * efficiency + + # Bind as method for parameter loading + D403.set_distillation_efficiency = lambda x: [setattr(unit, 'Lr', 0.99 * x) or setattr(unit, 'Hr', 0.99 * x) for unit in distillation_units] + + #Add acid loading factor specification to a suitable unit + @R201.add_specification(run=True) # Add to acid pretreatment reactor + def set_acid_loading_factor(factor=1.0): + pure_H2SO4_new = microalgae_mass * acid_loading * factor + acid_solution_mass_new = pure_H2SO4_new / acid_purity + water_mass_acid_new = acid_solution_mass_new * (1 - acid_purity) + _sulfuric_acid_stream.imass['H2SO4'] = pure_H2SO4_new + _sulfuric_acid_stream.imass['Water'] = water_mass_acid_new + + # Bind as method for parameter loading + def _set_acid_loading_factor(factor): + pure_H2SO4_new = microalgae_mass * acid_loading * factor + acid_solution_mass_new = pure_H2SO4_new / acid_purity + water_mass_acid_new = acid_solution_mass_new * (1 - acid_purity) + _sulfuric_acid_stream.imass['H2SO4'] = pure_H2SO4_new + _sulfuric_acid_stream.imass['Water'] = water_mass_acid_new + R201.set_acid_loading_factor = _set_acid_loading_factor + + # Add enzyme loading factor specification to a suitable unit + @R203.add_specification(run=True) # Add to first saccharification reactor + def set_enzyme_loading_factor(factor=1.0): + _glucoamylase_stream.imass['GlucoAmylase'] = microalgae_mass * 0.0011 * factor + _alpha_amylase_stream.imass['AlphaAmylase'] = microalgae_mass * 0.0082 * factor + + # Bind as method for parameter loading + def _set_enzyme_loading_factor(factor): + _glucoamylase_stream.imass['GlucoAmylase'] = microalgae_mass * 0.0011 * factor + _alpha_amylase_stream.imass['AlphaAmylase'] = microalgae_mass * 0.0082 * factor + R203.set_enzyme_loading_factor = _set_enzyme_loading_factor # ===================== # Area 5: Waste reuse for biogas production @@ -243,18 +328,18 @@ def adjust_fresh_octanol(): T605 = StorageTank('T605', D405-0, tau=60.*24., V_wf=0.9, vessel_type='Floating roof', vessel_material='Stainless steel') P605 = Pump('P605', T605-0, caprylic_acid_product) CT = CoolingTower('CT') - HXN601 = HXNWithMin('HXN601', T_min_app=10, min_heat_util=2e6) + HXN601 = HeatExchangerNetwork('HXN601', + #T_min_app=10, + #min_heat_util=2e6 + ) PWC = ProcessWaterCenter('PWC') ADP = AirDistributionPackage('ADP') CWP = ChilledWaterPackage('CWP') - - # 添加锅炉涡轮发电机 - # 使用厌氧消化产生的沼气作为燃料,并允许购买额外天然气 BT601 = bst.facilities.BoilerTurbogenerator('BT601', ins=(R501-2, M502-0, '', '', '', ''), - satisfy_system_electricity_demand=True, # 允许购买天然气 - boiler_efficiency=0.9, # 提高锅炉效率 - turbogenerator_efficiency=0.85) # 提高涡轮发电机效率 + satisfy_system_electricity_demand=True, + boiler_efficiency=0.9, + turbogenerator_efficiency=0.85) WastewaterT = bst.create_high_rate_wastewater_treatment_system('WastewaterT', M503-0, # Use diluted wastewater stream @@ -262,6 +347,7 @@ def adjust_fresh_octanol(): process_ID='6' # Use process ID 6 for unit numbering ) + # ========================================== # TEA Analysis # ========================================== @@ -276,20 +362,19 @@ def adjust_fresh_octanol(): dry_tpd = u.U101.ins[0].F_mass * 24 / 1000 # kg/h -> t/d microalgae_tea = create_tea(system=microalgae_mcca_sys, IRR=0.10, duration=(2024, 2045), depreciation='MACRS7', income_tax=0.21, - operating_days=330, + operating_days=330, lang_factor= None, construction_schedule=(0.08, 0.60, 0.32), startup_months=3, startup_FOCfrac=1, startup_salesfrac=0.5, startup_VOCfrac=0.75, WC_over_FCI=0.05, finance_interest=0.08, finance_years=10, finance_fraction=0.4, - OSBL_units=(u.CT, u.CWP, u.ADP, u.PWC), + OSBL_units=(u.CT, u.CWP, u.ADP, u.PWC, u.BT601), warehouse=0.04, site_development=0.09, additional_piping=0.045, proratable_costs=0.10, field_expenses=0.10, construction=0.20, contingency=0.10, other_indirect_costs=0.10, labor_cost=max(0.5e6, compute_labor_cost(dry_tpd)), - labor_burden=0.90, property_insurance=0.007, maintenance=0.03, boiler_turbogenerator=None, + labor_burden=0.90, property_insurance=0.007, maintenance=0.03, boiler_turbogenerator=u.BT601, steam_power_depreciation='MACRS20') -# Main script: run additional analysis and print results if __name__ == '__main__': microalgae_mcca_sys.diagram('cluster', format='png') microalgae_mcca_sys.print() @@ -320,8 +405,6 @@ def adjust_fresh_octanol(): print("CAPEX Table:\n", microalgae_tea.CAPEX_table()) print("FOC Table:\n", microalgae_tea.FOC_table()) print("Cashflow Table:\n", microalgae_tea.get_cashflow_table()) - - # Quick check: product flows in each units # print("\n===== Stream Mass Flows for Each Unit (kg/hr) =====") @@ -345,6 +428,3 @@ def adjust_fresh_octanol(): # s.caproic_acid_product,s.heptanoic_acid_product, s.caprylic_acid_product, s.butyric_acid_product): # print(f"{p.ID}: {p.F_mass:.2f} kg/h @ {p.price} $/kg") -# ========================================== -# Sensitive analysis -# ========================================== diff --git a/biorefineries/microalgae/system_etanol.py b/biorefineries/microalgae/system_etanol.py new file mode 100644 index 00000000..fc291d79 --- /dev/null +++ b/biorefineries/microalgae/system_etanol.py @@ -0,0 +1,422 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat July 05 12:50:00 2025 + +Microalgae biorefinery to produce medium chain fatty acids +by anaerobic fermentation without external electron donor addition- system + +References +---------- +[1] BioSTEAM Documentation: + https://biosteam.readthedocs.io/en/latest/tutorial/Creating_a_System.html +[2] Cortes-Peña et al., BioSTEAM: A Fast and Flexible Platform for the Design, + Simulation, and Techno-Economic Analysis of Biorefineries under Uncertainty. + ACS Sustainable Chem. Eng. 2020, 8 (8), 3302–3310. +[3] 3-Hydroxypropionic acid biorefineries project: + https://github.com/BioSTEAMDevelopmentGroup/Bioindustrial-Park/tree/master/biorefineries/HP +[4] Succinic projest + https://github.com/BioSTEAMDevelopmentGroup/Bioindustrial-Park/tree/master/biorefineries/succinic + +@author: Xingdong Shi +@version: 0.0.8 +""" + +import biosteam as bst +import numpy as np +import thermosteam as tmo +from biosteam import Stream, SystemFactory +from biosteam.units import Pump, StorageTank, HXutility, Mixer, Splitter, MultiStageMixerSettlers +from biosteam.facilities import AirDistributionPackage, ProcessWaterCenter, CoolingTower, ChilledWaterPackage, HeatExchangerNetwork +from biosteam import main_flowsheet +from .units import ( + FeedstockPreprocessing, AcidPretreatmentReactor, Saccharification, SolidLiquidSeparation, MCCAFermentation, + NeutralizationTank, AnaerobicDigestion +) +from .utils import price +from ._chemicals import chems +from .tea import create_tea +from .streams import microalgae_feed +import warnings +# Filter out specific warnings +# warnings.filterwarnings("ignore", message="phase equilibrium solution results in negative flow rates") +# warnings.filterwarnings("ignore", message=".*has no defined Dortmund groups.*") +# warnings.filterwarnings("ignore", message=".*has been replaced in registry") +# warnings.filterwarnings("ignore", category=bst.exceptions.CostWarning) +# warnings.filterwarnings("ignore", message=".*moisture.*is smaller than the desired.*") +# warnings.filterwarnings("ignore", message=".*moisture of influent.*is smaller than the desired.*") + +# ------------------------------------------------------------------ +# Utility: labor cost scaling with plant size +# ------------------------------------------------------------------ +def compute_labor_cost(dry_tpd: float, + base_tpd: float = 2205.0, + base_cost: float = 3212962.0, + exponent: float = 0.2, + floor_tpd: float = 100.0, + floor_cost: float = 0.5e6) -> float: + if dry_tpd < floor_tpd: + return floor_cost + return base_cost * (dry_tpd / base_tpd) ** exponent + +# Set up the main flowsheet and thermodynamic environment +bst.settings.set_thermo(chems) +main_flowsheet.clear() +flowsheet = bst.Flowsheet('MCCA_ethanol') +bst.main_flowsheet.set_flowsheet(flowsheet) + +# System settings +bst.System.default_converge_method = 'wegstein' +bst.System.default_maxiter = 2000 +bst.System.default_molar_tolerance = 1e-1 +bst.System.default_relative_molar_tolerance = 1e-1 # supersedes absolute tolerance +bst.System.strict_convergence = False # True => throw exception if system does not converge; False => continue with unconverged system + +@SystemFactory( + ID='Microalgae_MCCA_production_ethanol', + ins=[dict(microalgae_feed, thermo=chems)], + outs=[ + #dict(ID='butanol_product', thermo=chems), + dict(ID='butyric_acid_product', thermo=chems), + dict(ID='caproic_acid_product', thermo=chems), + dict(ID='heptanoic_acid_product', thermo=chems), + dict(ID='caprylic_acid_product', thermo=chems)] + ) +def create_microalgae_MCCA_production_sys_ethanol(ins, outs): + # Set the thermodynamic package explicitly + tmo.settings.set_thermo(chems) + + # Main feed and product + microalgae_feed, = ins + ( #butanol_product, + butyric_acid_product, + caproic_acid_product, + heptanoic_acid_product, + caprylic_acid_product) = outs + + # Calculate all required stream properties based on feed + microalgae_mass = microalgae_feed.F_mass + microalgae_water_mass = microalgae_mass / 0.04 # 4% solid loading + microalgae_water = Stream('microalgae_water', Water=microalgae_water_mass, units='kg/hr') + # H2SO4 for microalgae biomass hydrolysis + acid_loading = 1.47 # g H2SO4 / g microalgae + acid_purity = 0.93 + water_mass = microalgae_mass * (1 - 0.04) / 0.04 + pure_H2SO4 = microalgae_mass * acid_loading + acid_solution_mass = pure_H2SO4 / acid_purity + water_mass_acid = acid_solution_mass * (1 - acid_purity) + SulfuricAcid = Stream('sulfuricacid', H2SO4=pure_H2SO4, Water=water_mass_acid, units='kg/hr', price=price['SulfuricAcid']) + + # Store reference for later specification + _sulfuric_acid_stream = SulfuricAcid + # Ammonium Hydroxide for neutralization + h2so4_mol = pure_H2SO4 * 1000 / 98 # mol mass + nh4oh_mol = h2so4_mol * 0.1 # preadjustment + nh4oh_mass = nh4oh_mol * 35 / 1000 # mol mass to mass + ammonium_hydroxide = Stream('ammonium_hydroxide', NH4OH=nh4oh_mass, units='kg/hr', price=price['AmmoniumHydroxide']) + # Enzyme dosages + glucoamylase_mass = float(microalgae_mass * 0.0011) # ref from cron project + alpha_amylase_mass = float(microalgae_mass * 0.0082) # ref from cron project + glucoamylase = Stream('glucoamylase', GlucoAmylase=glucoamylase_mass, units='kg/hr', price=price['GlucoAmylase']) + alpha_amylase = Stream('alpha_amylase', AlphaAmylase=alpha_amylase_mass, units='kg/hr', price=price['AlphaAmylase']) + + # Store references for later specification + _glucoamylase_stream = glucoamylase + _alpha_amylase_stream = alpha_amylase + # NaOH for pH adjustment + naoh1_mass = microalgae_mass * 0.02 # pH 4.5 + naoh2_mass = microalgae_mass * 0.02 # pH 5.5 + total_naoh_mass = naoh1_mass + naoh2_mass + naoh = Stream('naoh', NaOH=total_naoh_mass, units='kg/hr', price=price['NaOH']) + # Ethanol addition in MCCA fermentation + # Accroding to the litterature, the dosgae of ethanol COD (2.09/g) would be ~2.0 times to microalgae COD (1.3475/g). + # Therefore, the etanol mass is 1.28 times of microalgae mass + # https://www.sciencedirect.com/science/article/pii/S0043135419309923?via%3Dihub + # https://www.sciencedirect.com/science/article/pii/S0045653521029465?via%3Dihub + + ethanol_mass = microalgae_mass * 1.28 + ethanol = Stream('ethanol', ethanol=ethanol_mass, units='kg/hr', price=price['Ethanol']) + # OleylAlcohol for extraction + fresh_oleylalcohol = Stream('fresh_oleylalcohol', OleylAlcohol=200, units='kg/hr', price=price['OleylAlcohol']) + oleylalcohol_recycle = Stream('oleylalcohol_recycle', OleylAlcohol=50, units='kg/hr') + oleylalcohol_feed = bst.Stream() + # Assign prices to product streams + #butanol_product.price = price['Butanol'] + butyric_acid_product.price = price['ButyricAcid'] + caproic_acid_product.price = price['CaproicAcid'] + heptanoic_acid_product.price = price['HeptanoicAcid'] + caprylic_acid_product.price = price['CaprylicAcid'] + + # ===================== + # Area 1: Microalgae process + # ===================== + U101 = FeedstockPreprocessing('U101', microalgae_feed, thermo=chems) + + # ===================== + # Area 2: Hydrolysis and saccharification + # ===================== + T201 = StorageTank('T201', SulfuricAcid) + P201 = Pump('P201', T201-0, P=5e5, pump_type='Default') + M201 = Mixer('M201', [P201-0, microalgae_water, U101-0]) + P202 = Pump('P202', M201-0) + H201 = HXutility('H201', P202-0, T=121+273.15) + R201 = AcidPretreatmentReactor('R201', H201-0) + T202 = StorageTank('T202', ammonium_hydroxide) + P203 = Pump('P203', T202-0) + R202 = NeutralizationTank('R202', [R201-0, P203-0]) + P204 = Pump('P204', R202-0) + H202 = HXutility('H202', P204-0, T=55+273.15) + T203 = StorageTank('T203', glucoamylase) + P205 = Pump('P205', T203-0, P=25e5, pump_type='Default', dP_design=24e5, ignore_NPSH=True) + T204 = StorageTank('T204', alpha_amylase) + P206 = Pump('P206', T204-0, P=25e5, pump_type='Default', dP_design=24e5, ignore_NPSH=True) + T205 = StorageTank('T205', naoh) + P207 = Pump('P207', T205-0, P=25e5, pump_type='Default', dP_design=24e5, ignore_NPSH=True) + S201 = Splitter('S201', P207-0, split=naoh1_mass/total_naoh_mass) + M202 = Mixer('M202', [H202-0, P205-0, S201-0]) + R203 = Saccharification('R203', M202-0) + H203 = HXutility('H203', R203-0, T=90+273.15) + M203 = Mixer('M203', [H203-0, P206-0, S201-1]) + R204 = Saccharification('R204', M203-0) + S202 = SolidLiquidSeparation('S202', R204-0) + P208 = Pump('P208', S202-0) + + # ===================== + # Area 3: Fermentation for MCCA production + # ===================== + H301 = HXutility('H301', P208-0, T=37+273.15) + T301 = StorageTank('T301', ethanol) + P301 = Pump('P301', T301-0) + R301 = MCCAFermentation('R301', [H301-0, P301-0], microalgae_mass_flow=microalgae_mass, titer = 2.003) + + # Add C6 yield factor specification + @R301.add_specification(run=True) + def set_C6_yield_factor(factor=1.0): + R301.caproic_acid_yield_factor = factor + + # Add C6 titer specification + @R301.add_specification(run=True) + def set_C6_titer(titer=2.003): + R301.titer = titer + + # Bind specification functions as methods for parameter loading + R301.set_C6_yield_factor = lambda x: setattr(R301, 'caproic_acid_yield_factor', x) + R301.set_C6_titer = lambda x: setattr(R301, 'titer', x) + + T302 = Mixer('T302', [R301-1]) + S301 = SolidLiquidSeparation('S301', R301-0) + + # ===================== + # Area 4: Product extraction + # ===================== + M401 = Mixer('M401', [fresh_oleylalcohol, oleylalcohol_recycle], oleylalcohol_feed) + @M401.add_specification(run=True) + def adjust_fresh_oleylalcohol(): + total_oleylalcohol = 200 + recycle = oleylalcohol_recycle.imass['OleylAlcohol'] + fresh = max(total_oleylalcohol - recycle, 1e-3) + fresh_oleylalcohol.imass['OleylAlcohol'] = fresh + IDs = ['Water', 'AceticAcid', 'PropionicAcid', 'ButyricAcid', 'ValericAcid', 'CaproicAcid','CaprylicAcid', 'HeptanoicAcid', 'Butanol', 'OleylAlcohol'] + K = np.array([1/5000, 0.24, 1.29, 5000/1, 13.58, 5000/1, 5000/1, 5000/1, 5000/1, 100000/1]) + S402 = MultiStageMixerSettlers( + 'S402', + partition_data={'K': K, 'IDs': IDs}, + N_stages=5, + ins=[S301-0, M401-0] + ) + + # Add extraction efficiency specification + original_K = K.copy() + @S402.add_specification(run=True) + def set_extraction_efficiency(efficiency=1.0): + S402.partition_data['K'] = original_K * efficiency + + # Bind as method for parameter loading + S402.set_extraction_efficiency = lambda x: setattr(S402, 'partition_data', {'K': original_K * x, 'IDs': IDs}) + + #D401 = ButanolDistillation('D401', S402-0) + #D401.check_LHK = False + D402 = bst.BinaryDistillation('D402', S402-0, LHK=('ButyricAcid', 'CaproicAcid'), + Lr=0.99, Hr=0.99, k=1.2, + partial_condenser=False, + is_divided=True) + #D402.check_LHK = False + D403 = bst.BinaryDistillation('D403', D402-1, LHK=('CaproicAcid', 'HeptanoicAcid'), + Lr=0.99, Hr=0.99, k=1.2, + partial_condenser=False, + is_divided=True + ) + #D403.check_LHK = False + D404 = bst.BinaryDistillation('D404', D403-1, LHK=('HeptanoicAcid', 'CaprylicAcid'), + Lr=0.99, Hr=0.99, k=1.2, + partial_condenser=False, + is_divided=True) + #D404.check_LHK = False + D405 = bst.BinaryDistillation('D405', D404-1, ['', oleylalcohol_recycle], LHK=('CaprylicAcid', 'OleylAlcohol'), + Lr=0.99, Hr=0.99, k=1.2, + partial_condenser=False, + is_divided=True, + product_specification_format='Recovery') + #D405.check_LHK = False + + # Add distillation efficiency specification + distillation_units = [D402, D403, D404, D405] + @D403.add_specification(run=True) # Use D403 as the representative unit + def set_distillation_efficiency(efficiency=1.0): + for unit in distillation_units: + unit.Lr = 0.99 * efficiency + unit.Hr = 0.99 * efficiency + + # Bind as method for parameter loading + D403.set_distillation_efficiency = lambda x: [setattr(unit, 'Lr', 0.99 * x) or setattr(unit, 'Hr', 0.99 * x) for unit in distillation_units] + + #Add acid loading factor specification to a suitable unit + @R201.add_specification(run=True) # Add to acid pretreatment reactor + def set_acid_loading_factor(factor=1.0): + pure_H2SO4_new = microalgae_mass * acid_loading * factor + acid_solution_mass_new = pure_H2SO4_new / acid_purity + water_mass_acid_new = acid_solution_mass_new * (1 - acid_purity) + _sulfuric_acid_stream.imass['H2SO4'] = pure_H2SO4_new + _sulfuric_acid_stream.imass['Water'] = water_mass_acid_new + + # Bind as method for parameter loading + def _set_acid_loading_factor(factor): + pure_H2SO4_new = microalgae_mass * acid_loading * factor + acid_solution_mass_new = pure_H2SO4_new / acid_purity + water_mass_acid_new = acid_solution_mass_new * (1 - acid_purity) + _sulfuric_acid_stream.imass['H2SO4'] = pure_H2SO4_new + _sulfuric_acid_stream.imass['Water'] = water_mass_acid_new + R201.set_acid_loading_factor = _set_acid_loading_factor + + # Add enzyme loading factor specification to a suitable unit + @R203.add_specification(run=True) # Add to first saccharification reactor + def set_enzyme_loading_factor(factor=1.0): + _glucoamylase_stream.imass['GlucoAmylase'] = microalgae_mass * 0.0011 * factor + _alpha_amylase_stream.imass['AlphaAmylase'] = microalgae_mass * 0.0082 * factor + + # Bind as method for parameter loading + def _set_enzyme_loading_factor(factor): + _glucoamylase_stream.imass['GlucoAmylase'] = microalgae_mass * 0.0011 * factor + _alpha_amylase_stream.imass['AlphaAmylase'] = microalgae_mass * 0.0082 * factor + R203.set_enzyme_loading_factor = _set_enzyme_loading_factor + + # ===================== + # Area 5: Waste reuse for biogas production + # ===================== + M501 = Mixer('M501', [S301-1, S202-1, S402-1]) + R501 = AnaerobicDigestion('R501', M501-0, microalgae_mass = microalgae_mass) + M503 = Mixer('M503', R501-1) + M502 = Mixer('M502', [R501-0, T302-0]) + + # ===================== + # Area 6: Facilities requirements + # ===================== + #T601 = StorageTank('T601', D401-0, tau=60.*24., V_wf=0.9, vessel_type='Floating roof', vessel_material='Stainless steel') + #P601 = Pump('P601', T601-0, butanol_product) + T602 = StorageTank('T602', D402-0, tau=30.*24., V_wf=0.9, vessel_type='Floating roof', vessel_material='Stainless steel') + P602 = Pump('P602', T602-0, butyric_acid_product) + T603 = StorageTank('T603', D403-0, tau=30.*24., V_wf=0.9, vessel_type='Floating roof', vessel_material='Stainless steel') + P603 = Pump('P603', T603-0, caproic_acid_product) + T604 = StorageTank('T604', D404-0, tau=60.*24., V_wf=0.9, vessel_type='Floating roof', vessel_material='Stainless steel') + P604 = Pump('P604', T604-0, heptanoic_acid_product) + T605 = StorageTank('T605', D405-0, tau=60.*24., V_wf=0.9, vessel_type='Floating roof', vessel_material='Stainless steel') + P605 = Pump('P605', T605-0, caprylic_acid_product) + CT = CoolingTower('CT') + HXN601 = HeatExchangerNetwork('HXN601', + #T_min_app=10, + #min_heat_util=2e6 + ) + PWC = ProcessWaterCenter('PWC') + ADP = AirDistributionPackage('ADP') + CWP = ChilledWaterPackage('CWP') + BT601 = bst.facilities.BoilerTurbogenerator('BT601', + ins=(R501-2, M502-0, '', '', '', ''), + satisfy_system_electricity_demand=True, + boiler_efficiency=0.9, + turbogenerator_efficiency=0.85) + + WastewaterT = bst.create_high_rate_wastewater_treatment_system('WastewaterT', + M503-0, # Use diluted wastewater stream + skip_IC=True, # Skip internal circulation to avoid division by zero + process_ID='6' # Use process ID 6 for unit numbering + ) + +# ========================================== +# TEA Analysis +# ========================================== +# Create system and TEA objects at module level for import +u = flowsheet.unit +s = flowsheet.stream +microalgae_mcca_sys_ethanol = create_microalgae_MCCA_production_sys_ethanol() +microalgae_mcca_sys_ethanol.simulate() + +# TEA analysis +# Dry biomass feed rate in ton per day (t/d) +dry_tpd = u.U101.ins[0].F_mass * 24 / 1000 # kg/h -> t/d +microalgae_tea_ethanol = create_tea(system=microalgae_mcca_sys_ethanol, IRR=0.10, duration=(2024, 2045), + depreciation='MACRS7', income_tax=0.21, + operating_days=330, + lang_factor= None, construction_schedule=(0.08, 0.60, 0.32), + startup_months=3, startup_FOCfrac=1, startup_salesfrac=0.5, + startup_VOCfrac=0.75, WC_over_FCI=0.05, + finance_interest=0.08, finance_years=10, finance_fraction=0.4, + OSBL_units=(u.CT, u.CWP, u.ADP, u.PWC, u.BT601), + warehouse=0.04, site_development=0.09, additional_piping=0.045, + proratable_costs=0.10, field_expenses=0.10, construction=0.20, + contingency=0.10, other_indirect_costs=0.10, + labor_cost=max(0.5e6, compute_labor_cost(dry_tpd)), + labor_burden=0.90, property_insurance=0.007, maintenance=0.03, boiler_turbogenerator=u.BT601, + steam_power_depreciation='MACRS20') + +if __name__ == '__main__': + microalgae_mcca_sys_ethanol.diagram('cluster', format='png') + microalgae_mcca_sys_ethanol.print() + print("\n===== Techno-Economic Analysis (TEA) Main Results =====") + # Use the system's main product stream directly for price calculation + caproic_acid_product = s.caproic_acid_product + if caproic_acid_product is not None: + if caproic_acid_product.price is None or caproic_acid_product.price == 0: + caproic_acid_product.price = 4.5 + price = microalgae_tea_ethanol.solve_price(caproic_acid_product) + print(f"Caproic Acid Minimum Selling Price: {price:.2f} $/kg") + if caproic_acid_product.F_mass > 0 and caproic_acid_product.price > 0: + print("Caproic Acid Unit Production Cost:", microalgae_tea_ethanol.production_costs([caproic_acid_product])) + print("NPV:", microalgae_tea_ethanol.NPV) + print("TCI:", microalgae_tea_ethanol.TCI) + print("FCI:", microalgae_tea_ethanol.FCI) + print("DPI:", microalgae_tea_ethanol.DPI) + print("TDC:", microalgae_tea_ethanol.TDC) + print("FOC:", microalgae_tea_ethanol.FOC) + print("VOC:", microalgae_tea_ethanol.VOC) + print("AOC:", microalgae_tea_ethanol.AOC) + print("ROI:", microalgae_tea_ethanol.ROI) + print("PBP:", microalgae_tea_ethanol.PBP) + print("Annual Depreciation:", microalgae_tea_ethanol.annual_depreciation) + print("Sales:", microalgae_tea_ethanol.sales) + print("Material Cost:", microalgae_tea_ethanol.material_cost) + print("Utility Cost:", microalgae_tea_ethanol.utility_cost) + print("CAPEX Table:\n", microalgae_tea_ethanol.CAPEX_table()) + print("FOC Table:\n", microalgae_tea_ethanol.FOC_table()) + print("Cashflow Table:\n", microalgae_tea_ethanol.get_cashflow_table()) + + # Quick check: product flows in each units + # print("\n===== Stream Mass Flows for Each Unit (kg/hr) =====") + # for u_ in microalgae_mcca_sys.units: + # print(f"\n[{u_.ID} - {u_.__class__.__name__}]") + # for i, stream in enumerate(u_.ins): + # if stream: + # print(f" Inlet {i+1} ({stream.ID}):") + # for chem, flow in zip(stream.chemicals.IDs, stream.mass): + # if abs(flow) > 1e-6: + # print(f" {chem}: {flow:.2f} kg/hr") + # for i, stream in enumerate(u_.outs): + # if stream: + # print(f" Outlet {i+1} ({stream.ID}):") + # for chem, flow in zip(stream.chemicals.IDs, stream.mass): + # if abs(flow) > 1e-6: + # print(f" {chem}: {flow:.2f} kg/hr") + + # Quick check: product flows and prices + # for p in (#s.butanol_product, + # s.caproic_acid_product,s.heptanoic_acid_product, s.caprylic_acid_product, s.butyric_acid_product): + # print(f"{p.ID}: {p.F_mass:.2f} kg/h @ {p.price} $/kg") + diff --git a/biorefineries/microalgae/system_no_yeast.py b/biorefineries/microalgae/system_no_yeast.py new file mode 100644 index 00000000..c289e496 --- /dev/null +++ b/biorefineries/microalgae/system_no_yeast.py @@ -0,0 +1,415 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat July 05 12:50:00 2025 + +Microalgae biorefinery to produce medium chain fatty acids +by anaerobic fermentation without external electron donor addition- system + +References +---------- +[1] BioSTEAM Documentation: + https://biosteam.readthedocs.io/en/latest/tutorial/Creating_a_System.html +[2] Cortes-Peña et al., BioSTEAM: A Fast and Flexible Platform for the Design, + Simulation, and Techno-Economic Analysis of Biorefineries under Uncertainty. + ACS Sustainable Chem. Eng. 2020, 8 (8), 3302–3310. +[3] 3-Hydroxypropionic acid biorefineries project: + https://github.com/BioSTEAMDevelopmentGroup/Bioindustrial-Park/tree/master/biorefineries/HP +[4] Succinic projest + https://github.com/BioSTEAMDevelopmentGroup/Bioindustrial-Park/tree/master/biorefineries/succinic + +@author: Xingdong Shi +@version: 0.0.8 +""" + +import biosteam as bst +import numpy as np +import thermosteam as tmo +from biosteam import Stream, SystemFactory +from biosteam.units import Pump, StorageTank, HXutility, Mixer, Splitter, MultiStageMixerSettlers +from biosteam.facilities import AirDistributionPackage, ProcessWaterCenter, CoolingTower, ChilledWaterPackage, HeatExchangerNetwork +from biosteam import main_flowsheet +from .units import ( + FeedstockPreprocessing, AcidPretreatmentReactor, Saccharification, SolidLiquidSeparation, MCCAFermentation_no_yeast, + NeutralizationTank, AnaerobicDigestion +) +from .utils import price +from ._chemicals import chems +from .tea import create_tea +from .streams import microalgae_feed +import warnings +# Filter out specific warnings +# warnings.filterwarnings("ignore", message="phase equilibrium solution results in negative flow rates") +# warnings.filterwarnings("ignore", message=".*has no defined Dortmund groups.*") +# warnings.filterwarnings("ignore", message=".*has been replaced in registry") +# warnings.filterwarnings("ignore", category=bst.exceptions.CostWarning) +# warnings.filterwarnings("ignore", message=".*moisture.*is smaller than the desired.*") +# warnings.filterwarnings("ignore", message=".*moisture of influent.*is smaller than the desired.*") + +# ------------------------------------------------------------------ +# Utility: labor cost scaling with plant size +# ------------------------------------------------------------------ +def compute_labor_cost(dry_tpd: float, + base_tpd: float = 2205.0, + base_cost: float = 3212962.0, + exponent: float = 0.2, + floor_tpd: float = 100.0, + floor_cost: float = 0.5e6) -> float: + if dry_tpd < floor_tpd: + return floor_cost + return base_cost * (dry_tpd / base_tpd) ** exponent + +# Set up the main flowsheet and thermodynamic environment +bst.settings.set_thermo(chems) +main_flowsheet.clear() +flowsheet = bst.Flowsheet('MCCA') +bst.main_flowsheet.set_flowsheet(flowsheet) + +# System settings +bst.System.default_converge_method = 'wegstein' +bst.System.default_maxiter = 2000 +bst.System.default_molar_tolerance = 1e-1 +bst.System.default_relative_molar_tolerance = 1e-1 # supersedes absolute tolerance +bst.System.strict_convergence = False # True => throw exception if system does not converge; False => continue with unconverged system + +@SystemFactory( + ID='Microalgae_MCCA_production_no_yeast', + ins=[dict(microalgae_feed, thermo=chems)], + outs=[ + #dict(ID='butanol_product', thermo=chems), + dict(ID='butyric_acid_product', thermo=chems), + dict(ID='caproic_acid_product', thermo=chems), + dict(ID='heptanoic_acid_product', thermo=chems), + dict(ID='caprylic_acid_product', thermo=chems)] + ) +def create_microalgae_MCCA_production_no_yeast_sys(ins, outs): + # Set the thermodynamic package explicitly + tmo.settings.set_thermo(chems) + + # Main feed and product + microalgae_feed, = ins + ( #butanol_product, + butyric_acid_product, + caproic_acid_product, + heptanoic_acid_product, + caprylic_acid_product) = outs + + # Calculate all required stream properties based on feed + microalgae_mass = microalgae_feed.F_mass + microalgae_water_mass = microalgae_mass / 0.04 # 4% solid loading + microalgae_water = Stream('microalgae_water', Water=microalgae_water_mass, units='kg/hr') + # H2SO4 for microalgae biomass hydrolysis + acid_loading = 1.47 # g H2SO4 / g microalgae + acid_purity = 0.93 + water_mass = microalgae_mass * (1 - 0.04) / 0.04 + pure_H2SO4 = microalgae_mass * acid_loading + acid_solution_mass = pure_H2SO4 / acid_purity + water_mass_acid = acid_solution_mass * (1 - acid_purity) + SulfuricAcid = Stream('sulfuricacid', H2SO4=pure_H2SO4, Water=water_mass_acid, units='kg/hr', price=price['SulfuricAcid']) + + # Store reference for later specification + _sulfuric_acid_stream = SulfuricAcid + # Ammonium Hydroxide for neutralization + h2so4_mol = pure_H2SO4 * 1000 / 98 # mol mass + nh4oh_mol = h2so4_mol * 0.1 # preadjustment + nh4oh_mass = nh4oh_mol * 35 / 1000 # mol mass to mass + ammonium_hydroxide = Stream('ammonium_hydroxide', NH4OH=nh4oh_mass, units='kg/hr', price=price['AmmoniumHydroxide']) + # Enzyme dosages + glucoamylase_mass = float(microalgae_mass * 0.0011) # ref from cron project + alpha_amylase_mass = float(microalgae_mass * 0.0082) # ref from cron project + glucoamylase = Stream('glucoamylase', GlucoAmylase=glucoamylase_mass, units='kg/hr', price=price['GlucoAmylase']) + alpha_amylase = Stream('alpha_amylase', AlphaAmylase=alpha_amylase_mass, units='kg/hr', price=price['AlphaAmylase']) + + # Store references for later specification + _glucoamylase_stream = glucoamylase + _alpha_amylase_stream = alpha_amylase + # NaOH for pH adjustment + naoh1_mass = microalgae_mass * 0.02 # pH 4.5 + naoh2_mass = microalgae_mass * 0.02 # pH 5.5 + total_naoh_mass = naoh1_mass + naoh2_mass + naoh = Stream('naoh', NaOH=total_naoh_mass, units='kg/hr', price=price['NaOH']) + # OleylAlcohol for extraction + fresh_oleylalcohol = Stream('fresh_oleylalcohol', OleylAlcohol=200, units='kg/hr', price=price['OleylAlcohol']) + oleylalcohol_recycle = Stream('oleylalcohol_recycle', OleylAlcohol=50, units='kg/hr') + oleylalcohol_feed = bst.Stream() + # Assign prices to product streams + #butanol_product.price = price['Butanol'] + butyric_acid_product.price = price['ButyricAcid'] + caproic_acid_product.price = price['CaproicAcid'] + heptanoic_acid_product.price = price['HeptanoicAcid'] + caprylic_acid_product.price = price['CaprylicAcid'] + + # ===================== + # Area 1: Microalgae process + # ===================== + U101 = FeedstockPreprocessing('U101', microalgae_feed, thermo=chems) + + # ===================== + # Area 2: Hydrolysis and saccharification + # ===================== + T201 = StorageTank('T201', SulfuricAcid) + P201 = Pump('P201', T201-0, P=5e5, pump_type='Default') + M201 = Mixer('M201', [P201-0, microalgae_water, U101-0]) + P202 = Pump('P202', M201-0) + H201 = HXutility('H201', P202-0, T=121+273.15) + R201 = AcidPretreatmentReactor('R201', H201-0) + T202 = StorageTank('T202', ammonium_hydroxide) + P203 = Pump('P203', T202-0) + R202 = NeutralizationTank('R202', [R201-0, P203-0]) + P204 = Pump('P204', R202-0) + H202 = HXutility('H202', P204-0, T=55+273.15) + T203 = StorageTank('T203', glucoamylase) + P205 = Pump('P205', T203-0, P=25e5, pump_type='Default', dP_design=24e5, ignore_NPSH=True) + T204 = StorageTank('T204', alpha_amylase) + P206 = Pump('P206', T204-0, P=25e5, pump_type='Default', dP_design=24e5, ignore_NPSH=True) + T205 = StorageTank('T205', naoh) + P207 = Pump('P207', T205-0, P=25e5, pump_type='Default', dP_design=24e5, ignore_NPSH=True) + S201 = Splitter('S201', P207-0, split=naoh1_mass/total_naoh_mass) + M202 = Mixer('M202', [H202-0, P205-0, S201-0]) + R203 = Saccharification('R203', M202-0) + H203 = HXutility('H203', R203-0, T=90+273.15) + M203 = Mixer('M203', [H203-0, P206-0, S201-1]) + R204 = Saccharification('R204', M203-0) + S202 = SolidLiquidSeparation('S202', R204-0) + P208 = Pump('P208', S202-0) + + # ===================== + # Area 3: Fermentation for MCCA production + # ===================== + H301 = HXutility('H301', P208-0, T=37+273.15) + T301 = StorageTank('T301', H301-0) + P301 = Pump('P301', T301-0) + R301 = MCCAFermentation_no_yeast('R301', [H301-0, P301-0], microalgae_mass_flow=microalgae_mass, titer = 1.208) + + # Add C6 yield factor specification + @R301.add_specification(run=True) + def set_C6_yield_factor(factor=1.0): + R301.caproic_acid_yield_factor = factor + + # Add C6 titer specification + @R301.add_specification(run=True) + def set_C6_titer(titer=2.003): + R301.titer = titer + + # Bind specification functions as methods for parameter loading + R301.set_C6_yield_factor = lambda x: setattr(R301, 'caproic_acid_yield_factor', x) + R301.set_C6_titer = lambda x: setattr(R301, 'titer', x) + + T302 = Mixer('T302', [R301-1]) + S301 = SolidLiquidSeparation('S301', R301-0) + + # ===================== + # Area 4: Product extraction + # ===================== + M401 = Mixer('M401', [fresh_oleylalcohol, oleylalcohol_recycle], oleylalcohol_feed) + @M401.add_specification(run=True) + def adjust_fresh_oleylalcohol(): + total_oleylalcohol = 200 + recycle = oleylalcohol_recycle.imass['OleylAlcohol'] + fresh = max(total_oleylalcohol - recycle, 1e-3) + fresh_oleylalcohol.imass['OleylAlcohol'] = fresh + IDs = ['Water', 'AceticAcid', 'PropionicAcid', 'ButyricAcid', 'ValericAcid', 'CaproicAcid','CaprylicAcid', 'HeptanoicAcid', 'Butanol', 'OleylAlcohol'] + K = np.array([1/5000, 0.24, 1.29, 5000/1, 13.58, 5000/1, 5000/1, 5000/1, 5000/1, 100000/1]) + S402 = MultiStageMixerSettlers( + 'S402', + partition_data={'K': K, 'IDs': IDs}, + N_stages=5, + ins=[S301-0, M401-0] + ) + + # Add extraction efficiency specification + original_K = K.copy() + @S402.add_specification(run=True) + def set_extraction_efficiency(efficiency=1.0): + S402.partition_data['K'] = original_K * efficiency + + # Bind as method for parameter loading + S402.set_extraction_efficiency = lambda x: setattr(S402, 'partition_data', {'K': original_K * x, 'IDs': IDs}) + + #D401 = ButanolDistillation('D401', S402-0) + #D401.check_LHK = False + D402 = bst.BinaryDistillation('D402', S402-0, LHK=('ButyricAcid', 'CaproicAcid'), + Lr=0.99, Hr=0.99, k=1.2, + partial_condenser=False, + is_divided=True) + #D402.check_LHK = False + D403 = bst.BinaryDistillation('D403', D402-1, LHK=('CaproicAcid', 'HeptanoicAcid'), + Lr=0.99, Hr=0.99, k=1.2, + partial_condenser=False, + is_divided=True + ) + #D403.check_LHK = False + D404 = bst.BinaryDistillation('D404', D403-1, LHK=('HeptanoicAcid', 'CaprylicAcid'), + Lr=0.99, Hr=0.99, k=1.2, + partial_condenser=False, + is_divided=True) + #D404.check_LHK = False + D405 = bst.BinaryDistillation('D405', D404-1, ['', oleylalcohol_recycle], LHK=('CaprylicAcid', 'OleylAlcohol'), + Lr=0.99, Hr=0.99, k=1.2, + partial_condenser=False, + is_divided=True, + product_specification_format='Recovery') + #D405.check_LHK = False + + # Add distillation efficiency specification + distillation_units = [D402, D403, D404, D405] + @D403.add_specification(run=True) # Use D403 as the representative unit + def set_distillation_efficiency(efficiency=1.0): + for unit in distillation_units: + unit.Lr = 0.99 * efficiency + unit.Hr = 0.99 * efficiency + + # Bind as method for parameter loading + D403.set_distillation_efficiency = lambda x: [setattr(unit, 'Lr', 0.99 * x) or setattr(unit, 'Hr', 0.99 * x) for unit in distillation_units] + + #Add acid loading factor specification to a suitable unit + @R201.add_specification(run=True) # Add to acid pretreatment reactor + def set_acid_loading_factor(factor=1.0): + pure_H2SO4_new = microalgae_mass * acid_loading * factor + acid_solution_mass_new = pure_H2SO4_new / acid_purity + water_mass_acid_new = acid_solution_mass_new * (1 - acid_purity) + _sulfuric_acid_stream.imass['H2SO4'] = pure_H2SO4_new + _sulfuric_acid_stream.imass['Water'] = water_mass_acid_new + + # Bind as method for parameter loading + def _set_acid_loading_factor(factor): + pure_H2SO4_new = microalgae_mass * acid_loading * factor + acid_solution_mass_new = pure_H2SO4_new / acid_purity + water_mass_acid_new = acid_solution_mass_new * (1 - acid_purity) + _sulfuric_acid_stream.imass['H2SO4'] = pure_H2SO4_new + _sulfuric_acid_stream.imass['Water'] = water_mass_acid_new + R201.set_acid_loading_factor = _set_acid_loading_factor + + # Add enzyme loading factor specification to a suitable unit + @R203.add_specification(run=True) # Add to first saccharification reactor + def set_enzyme_loading_factor(factor=1.0): + _glucoamylase_stream.imass['GlucoAmylase'] = microalgae_mass * 0.0011 * factor + _alpha_amylase_stream.imass['AlphaAmylase'] = microalgae_mass * 0.0082 * factor + + # Bind as method for parameter loading + def _set_enzyme_loading_factor(factor): + _glucoamylase_stream.imass['GlucoAmylase'] = microalgae_mass * 0.0011 * factor + _alpha_amylase_stream.imass['AlphaAmylase'] = microalgae_mass * 0.0082 * factor + R203.set_enzyme_loading_factor = _set_enzyme_loading_factor + + # ===================== + # Area 5: Waste reuse for biogas production + # ===================== + M501 = Mixer('M501', [S301-1, S202-1, S402-1]) + R501 = AnaerobicDigestion('R501', M501-0, microalgae_mass = microalgae_mass) + M503 = Mixer('M503', R501-1) + M502 = Mixer('M502', [R501-0, T302-0]) + + # ===================== + # Area 6: Facilities requirements + # ===================== + #T601 = StorageTank('T601', D401-0, tau=60.*24., V_wf=0.9, vessel_type='Floating roof', vessel_material='Stainless steel') + #P601 = Pump('P601', T601-0, butanol_product) + T602 = StorageTank('T602', D402-0, tau=30.*24., V_wf=0.9, vessel_type='Floating roof', vessel_material='Stainless steel') + P602 = Pump('P602', T602-0, butyric_acid_product) + T603 = StorageTank('T603', D403-0, tau=30.*24., V_wf=0.9, vessel_type='Floating roof', vessel_material='Stainless steel') + P603 = Pump('P603', T603-0, caproic_acid_product) + T604 = StorageTank('T604', D404-0, tau=60.*24., V_wf=0.9, vessel_type='Floating roof', vessel_material='Stainless steel') + P604 = Pump('P604', T604-0, heptanoic_acid_product) + T605 = StorageTank('T605', D405-0, tau=60.*24., V_wf=0.9, vessel_type='Floating roof', vessel_material='Stainless steel') + P605 = Pump('P605', T605-0, caprylic_acid_product) + CT = CoolingTower('CT') + HXN601 = HeatExchangerNetwork('HXN601', + #T_min_app=10, + #min_heat_util=2e6 + ) + PWC = ProcessWaterCenter('PWC') + ADP = AirDistributionPackage('ADP') + CWP = ChilledWaterPackage('CWP') + BT601 = bst.facilities.BoilerTurbogenerator('BT601', + ins=(R501-2, M502-0, '', '', '', ''), + satisfy_system_electricity_demand=True, + boiler_efficiency=0.9, + turbogenerator_efficiency=0.85) + + WastewaterT = bst.create_high_rate_wastewater_treatment_system('WastewaterT', + M503-0, # Use diluted wastewater stream + skip_IC=True, # Skip internal circulation to avoid division by zero + process_ID='6' # Use process ID 6 for unit numbering + ) + + +# ========================================== +# TEA Analysis +# ========================================== +# Create system and TEA objects at module level for import +u = flowsheet.unit +s = flowsheet.stream +microalgae_mcca_sys_no_yeast = create_microalgae_MCCA_production_no_yeast_sys() +microalgae_mcca_sys_no_yeast.simulate() + +# TEA analysis +# Dry biomass feed rate in ton per day (t/d) +dry_tpd = u.U101.ins[0].F_mass * 24 / 1000 # kg/h -> t/d +microalgae_tea_no_yeast = create_tea(system=microalgae_mcca_sys_no_yeast, IRR=0.10, duration=(2024, 2045), + depreciation='MACRS7', income_tax=0.21, + operating_days=330, + lang_factor= None, construction_schedule=(0.08, 0.60, 0.32), + startup_months=3, startup_FOCfrac=1, startup_salesfrac=0.5, + startup_VOCfrac=0.75, WC_over_FCI=0.05, + finance_interest=0.08, finance_years=10, finance_fraction=0.4, + OSBL_units=(u.CT, u.CWP, u.ADP, u.PWC, u.BT601), + warehouse=0.04, site_development=0.09, additional_piping=0.045, + proratable_costs=0.10, field_expenses=0.10, construction=0.20, + contingency=0.10, other_indirect_costs=0.10, + labor_cost=max(0.5e6, compute_labor_cost(dry_tpd)), + labor_burden=0.90, property_insurance=0.007, maintenance=0.03, boiler_turbogenerator=u.BT601, + steam_power_depreciation='MACRS20') + +if __name__ == '__main__': + microalgae_mcca_sys_no_yeast.diagram('cluster', format='png') + microalgae_mcca_sys_no_yeast.print() + print("\n===== Techno-Economic Analysis (TEA) Main Results =====") + # Use the system's main product stream directly for price calculation + caproic_acid_product = s.caproic_acid_product + if caproic_acid_product is not None: + if caproic_acid_product.price is None or caproic_acid_product.price == 0: + caproic_acid_product.price = 4.5 + price = microalgae_tea_no_yeast.solve_price(caproic_acid_product) + print(f"Caproic Acid Minimum Selling Price: {price:.2f} $/kg") + if caproic_acid_product.F_mass > 0 and caproic_acid_product.price > 0: + print("Caproic Acid Unit Production Cost:", microalgae_tea_no_yeast.production_costs([caproic_acid_product])) + print("NPV:", microalgae_tea_no_yeast.NPV) + print("TCI:", microalgae_tea_no_yeast.TCI) + print("FCI:", microalgae_tea_no_yeast.FCI) + print("DPI:", microalgae_tea_no_yeast.DPI) + print("TDC:", microalgae_tea_no_yeast.TDC) + print("FOC:", microalgae_tea_no_yeast.FOC) + print("VOC:", microalgae_tea_no_yeast.VOC) + print("AOC:", microalgae_tea_no_yeast.AOC) + print("ROI:", microalgae_tea_no_yeast.ROI) + print("PBP:", microalgae_tea_no_yeast.PBP) + print("Annual Depreciation:", microalgae_tea_no_yeast.annual_depreciation) + print("Sales:", microalgae_tea_no_yeast.sales) + print("Material Cost:", microalgae_tea_no_yeast.material_cost) + print("Utility Cost:", microalgae_tea_no_yeast.utility_cost) + print("CAPEX Table:\n", microalgae_tea_no_yeast.CAPEX_table()) + print("FOC Table:\n", microalgae_tea_no_yeast.FOC_table()) + print("Cashflow Table:\n", microalgae_tea_no_yeast.get_cashflow_table()) + + # Quick check: product flows in each units + # print("\n===== Stream Mass Flows for Each Unit (kg/hr) =====") + # for u_ in microalgae_mcca_sys.units: + # print(f"\n[{u_.ID} - {u_.__class__.__name__}]") + # for i, stream in enumerate(u_.ins): + # if stream: + # print(f" Inlet {i+1} ({stream.ID}):") + # for chem, flow in zip(stream.chemicals.IDs, stream.mass): + # if abs(flow) > 1e-6: + # print(f" {chem}: {flow:.2f} kg/hr") + # for i, stream in enumerate(u_.outs): + # if stream: + # print(f" Outlet {i+1} ({stream.ID}):") + # for chem, flow in zip(stream.chemicals.IDs, stream.mass): + # if abs(flow) > 1e-6: + # print(f" {chem}: {flow:.2f} kg/hr") + + # Quick check: product flows and prices + # for p in (#s.butanol_product, + # s.caproic_acid_product,s.heptanoic_acid_product, s.caprylic_acid_product, s.butyric_acid_product): + # print(f"{p.ID}: {p.F_mass:.2f} kg/h @ {p.price} $/kg") + diff --git a/biorefineries/microalgae/tea.py b/biorefineries/microalgae/tea.py index 41fe29b5..17f65dc3 100644 --- a/biorefineries/microalgae/tea.py +++ b/biorefineries/microalgae/tea.py @@ -24,3 +24,4 @@ import biosteam as bst from biorefineries.tea.cellulosic_ethanol_tea import CellulosicEthanolTEA as create_tea + diff --git a/biorefineries/microalgae/tea_breakdown.py b/biorefineries/microalgae/tea_breakdown.py new file mode 100644 index 00000000..6667af12 --- /dev/null +++ b/biorefineries/microalgae/tea_breakdown.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +""" +Microalgae TEA Breakdown Analysis + +@author: Xingdong Shi +""" + +import pandas as pd +import numpy as np +import biosteam as bst +from .model_utils import get_unit_groups +from .system import microalgae_mcca_sys, microalgae_tea + +class BTElectricityRevenueConfig: + def __init__(self, + electricity_price=0.07, # USD/kWh + include_in_material_cost=True, + include_in_operating_cost=False): + self.electricity_price = electricity_price + self.include_in_material_cost = include_in_material_cost + self.include_in_operating_cost = include_in_operating_cost + +bt_revenue_config = BTElectricityRevenueConfig() + +def create_tea_breakdown_data(unit_groups=None, tea=microalgae_tea, print_output=False, fractions=False): + unit_groups = unit_groups or get_unit_groups() + system_electricity_revenue = calculate_system_electricity_revenue() + first_metrics_group = None + for ug in unit_groups: + if hasattr(ug, 'metrics') and ug.metrics: + first_metrics_group = ug + break + metric_breakdowns = {metric.name: {} for metric in first_metrics_group.metrics} + for ug in unit_groups: + if hasattr(ug, 'metrics') and ug.metrics: + for metric in ug.metrics: + denominator = 1.0 + + if fractions: + metric_name = metric.name + if 'cost' in metric_name.lower() or 'installed' in metric_name.lower(): + denominator = tea.installed_equipment_cost / 1e6 + elif 'cooling' in metric_name.lower(): + denominator = 0 + for unit in microalgae_mcca_sys.units: + if hasattr(unit, 'heat_utilities'): + for hu in unit.heat_utilities: + if hasattr(hu, 'duty') and hu.duty > 0: + denominator += hu.duty / 1e6 + elif 'heating' in metric_name.lower(): + denominator = 0 + for unit in microalgae_mcca_sys.units: + if hasattr(unit, 'heat_utilities'): + for hu in unit.heat_utilities: + if hasattr(hu, 'duty') and hu.duty < 0: + denominator -= hu.duty / 1e6 + elif 'electricity' in metric_name.lower() or 'power' in metric_name.lower(): + if 'consumption' in metric_name.lower(): + denominator = microalgae_mcca_sys.power_utility.consumption / 1e3 + elif 'production' in metric_name.lower(): + denominator = microalgae_mcca_sys.power_utility.production / 1e3 + elif 'material' in metric_name.lower(): + denominator = tea.material_cost / tea.operating_hours + metric_value = metric() if callable(metric) else 0 + if ug.name == 'BT': + should_add_revenue = ( + (bt_revenue_config.include_in_material_cost and 'material' in metric.name.lower()) or + (bt_revenue_config.include_in_operating_cost and ('operating' in metric.name.lower() or 'variable' in metric.name.lower())) + ) + if should_add_revenue: + if fractions: + electricity_revenue_to_add = system_electricity_revenue + else: + electricity_revenue_to_add = system_electricity_revenue / tea.operating_hours + metric_value += electricity_revenue_to_add + if ug.name == 'Storage': + continue + elif ug.name == 'Other facilities': + storage_value = 0 + for storage_ug in unit_groups: + if storage_ug.name == 'Storage': + if hasattr(storage_ug, 'metrics') and storage_ug.metrics: + storage_metric = storage_ug.metrics[ug.metrics.index(metric)] if ug.metrics.index(metric) < len(storage_ug.metrics) else None + storage_value = storage_metric() if callable(storage_metric) else 0 + break + + combined_name = 'Storage and ' + ug.name + metric_breakdowns[metric.name][combined_name] = (metric_value + storage_value) / denominator + else: + metric_breakdowns[metric.name][ug.name] = metric_value / denominator + if print_output and first_metrics_group: + for metric in first_metrics_group.metrics: + print(f"\n\n----- {metric.name} ({metric.units}) -----") + metric_breakdown = metric_breakdowns.get(metric.name, {}) + for group_name, value in metric_breakdown.items(): + print(f"{group_name}: {value:.3f}") + return metric_breakdowns + +def calculate_system_electricity_revenue(): + power_breakdown = get_system_power_breakdown() + net_power_demand = power_breakdown['net_electricity'] # kW + operating_hours = microalgae_tea.operating_hours + + if net_power_demand < 0: + surplus_electricity = abs(net_power_demand) # kW + annual_electricity_revenue = surplus_electricity * bt_revenue_config.electricity_price * operating_hours + return -annual_electricity_revenue + else: + electricity_cost = net_power_demand * bt_revenue_config.electricity_price * operating_hours + return electricity_cost + +def create_tea_breakdown_dataframe(unit_groups=None, fraction=True, + scale_fractions_to_positive_values=False): + unit_groups = unit_groups or get_unit_groups() + df = bst.UnitGroup.df_from_groups( + unit_groups, + fraction=fraction, + scale_fractions_to_positive_values=scale_fractions_to_positive_values + ) + + system_electricity_revenue = calculate_system_electricity_revenue() + if system_electricity_revenue != 0 and 'BT' in df.index: + if 'Material cost' in df.columns: + if fraction: + total_annual_material_cost = microalgae_tea.material_cost + revenue_fraction = system_electricity_revenue / total_annual_material_cost + df.loc['BT', 'Material cost'] += revenue_fraction + else: + revenue_hourly = system_electricity_revenue / microalgae_tea.operating_hours + df.loc['BT', 'Material cost'] += revenue_hourly + return df + +def get_cost_breakdown_by_category(tea): + breakdown = {} + breakdown['Installed equipment cost'] = tea.installed_equipment_cost / 1e6 + breakdown['ISBL installed equipment cost'] = getattr(tea, 'ISBL_installed_equipment_cost', 0) / 1e6 + breakdown['OSBL installed equipment cost'] = getattr(tea, 'OSBL_installed_equipment_cost', 0) / 1e6 + breakdown['Fixed operating cost'] = tea.FOC / 1e6 + breakdown['Variable operating cost'] = tea.VOC / 1e6 + breakdown['Material cost'] = tea.material_cost / 1e6 + breakdown['Utility cost'] = tea.utility_cost / 1e6 + breakdown['Total capital investment'] = tea.TCI / 1e6 + breakdown['Fixed capital investment'] = tea.FCI / 1e6 + breakdown['Total depreciable capital'] = tea.TDC / 1e6 + breakdown['Direct permanent investment'] = tea.DPI / 1e6 + return breakdown + +def get_system_power_breakdown(): + power_breakdown = { + 'total_consumption': 0, + 'total_production': 0, + 'net_electricity': 0, + 'unit_details': {} + } + + for unit in microalgae_mcca_sys.units: + if hasattr(unit, 'power_utility') and unit.power_utility: + pu = unit.power_utility + consumption = pu.consumption + production = pu.production + if consumption != 0 or production != 0: + power_breakdown['unit_details'][unit.ID] = { + 'consumption': consumption, + 'production': production, + 'net': consumption - production + } + power_breakdown['total_consumption'] += consumption + power_breakdown['total_production'] += production + power_breakdown['net_electricity'] = (power_breakdown['total_consumption'] - + power_breakdown['total_production']) + return power_breakdown + +def analyze_microalgae_tea_breakdown(): + microalgae_mcca_sys.simulate() + system_electricity_revenue = calculate_system_electricity_revenue() + power_breakdown = get_system_power_breakdown() + unit_groups = get_unit_groups() + breakdown_data = create_tea_breakdown_data(unit_groups, microalgae_tea, print_output=True) + df_breakdown = create_tea_breakdown_dataframe( + unit_groups, + fraction=True, + scale_fractions_to_positive_values=False + ) + df_breakdown = create_tea_breakdown_dataframe( + unit_groups, + fraction=True, + scale_fractions_to_positive_values=False + ) + cost_breakdown = get_cost_breakdown_by_category(microalgae_tea) + results = { + 'breakdown_data': breakdown_data, + 'breakdown_dataframe': df_breakdown, + 'cost_breakdown': cost_breakdown, + 'unit_groups': unit_groups, + 'tea_system': microalgae_mcca_sys, + 'power_breakdown': power_breakdown, + 'system_electricity_revenue': system_electricity_revenue + } + return results + +if __name__ == "__main__": + analyze_microalgae_tea_breakdown() + + diff --git a/biorefineries/microalgae/uncertainties.py b/biorefineries/microalgae/uncertainties.py index 1868dbdd..d4ea2a92 100644 --- a/biorefineries/microalgae/uncertainties.py +++ b/biorefineries/microalgae/uncertainties.py @@ -1,310 +1,494 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Microalgae biorefinery uncertainty and sensitivity analysis - -This module performs uncertainty and sensitivity analysis for the microalgae biorefinery -system, including Monte Carlo simulations and Spearman correlation analysis. - -@author: Xingdong Shi -@version: 0.0.1 -""" - -from warnings import filterwarnings -filterwarnings('ignore') -import numpy as np -import pandas as pd -import biosteam as bst -import thermosteam as tmo -from datetime import datetime -from biosteam.utils import TicToc -import os - -# Import microalgae system components -from . import system as microalgae_system -from . import lca -from . import tea -from ._chemicals import chems - -# Set up the system -bst.settings.set_thermo(chems) -microalgae_sys = microalgae_system.microalgae_mcca_sys -microalgae_tea = microalgae_system.microalgae_tea - -# Create LCA object -from . import analysis_utils -microalgae_lca = analysis_utils.create_microalgae_lca_simple(microalgae_sys, microalgae_tea) - -# Create model -# Get main product for TEA calculations -main_product = analysis_utils.get_main_product_stream(microalgae_sys) - -model = bst.Model(microalgae_sys, metrics=[ - bst.Metric('MPSP', lambda: microalgae_tea.solve_price(main_product), 'USD/kg'), - bst.Metric('GWP100a', lambda: microalgae_lca.GWP, 'kg CO2-eq/kg'), - bst.Metric('FEC', lambda: microalgae_lca.FEC, 'MJ/kg'), -]) - -print('\n\nLoaded microalgae system for uncertainty analysis.') - -# Analysis parameters -N_simulations_per_mode = 2000 -percentiles = [0, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 1] -notification_interval = 10 - -results_dict = { - 'Baseline': { - 'MPSP': {}, - 'GWP100a': {}, - 'FEC': {}, - 'GWP Breakdown': {}, - 'FEC Breakdown': {} - }, - 'Uncertainty': { - 'MPSP': {}, - 'GWP100a': {}, - 'FEC': {} - }, - 'Sensitivity': { - 'Spearman': { - 'MPSP': {}, - 'GWP100a': {}, - 'FEC': {} - } - } -} - -# Parameter distribution file -parameter_distributions_filename = 'parameter_distributions.xlsx' - -def run_uncertainty_analysis(): - """Run uncertainty and sensitivity analysis for microalgae biorefinery.""" - - timer = TicToc('timer') - timer.tic() - - # Set seed for reproducibility - np.random.seed(3221) - - print(f'\n\nLoading parameter distributions...') - - # Get current directory - current_dir = os.path.dirname(os.path.abspath(__file__)) - param_file_path = os.path.join(current_dir, parameter_distributions_filename) - - # Load parameter distributions - model.parameters = () - - # Create namespace dictionary for microalgae system - namespace_dict = { - 'microalgae_sys': microalgae_sys, - 'microalgae_tea': microalgae_tea, - 'microalgae_lca': microalgae_lca, - } - - # Add all units to namespace - for unit in microalgae_sys.units: - namespace_dict[unit.ID] = unit - - # Add all streams to namespace - for stream in microalgae_sys.streams: - namespace_dict[stream.ID] = stream - - # Add chemicals to namespace - for chemical in chems: - namespace_dict[chemical.ID] = chemical - - model.load_parameter_distributions(param_file_path, namespace_dict) - print(f'\nLoaded parameter distributions.') - - parameters = model.get_parameters() - - # Baseline analysis - print(f'\n\nRunning baseline analysis...') - model.metrics_at_baseline() - baseline_metrics = model.metrics_at_baseline() - - results_dict['Baseline']['MPSP'] = baseline_metrics[0] - results_dict['Baseline']['GWP100a'] = baseline_metrics[1] - results_dict['Baseline']['FEC'] = baseline_metrics[2] - - # Get baseline breakdowns - microalgae_sys.simulate() - - # GWP breakdown - material_GWP_breakdown = microalgae_lca.material_GWP_breakdown - gwp_breakdown = { - 'feedstock*': microalgae_lca.feedstock_GWP, - 'H2SO4': material_GWP_breakdown.get('H2SO4', 0), - 'NaOH': material_GWP_breakdown.get('NaOH', 0), - 'NH4OH': material_GWP_breakdown.get('NH4OH', 0), - 'CalciumDihydroxide': material_GWP_breakdown.get('CalciumDihydroxide', 0), - 'Ethanol': material_GWP_breakdown.get('Ethanol', 0), - 'Octanol': material_GWP_breakdown.get('Octanol', 0), - 'GlucoAmylase': material_GWP_breakdown.get('GlucoAmylase', 0), - 'AlphaAmylase': material_GWP_breakdown.get('AlphaAmylase', 0), - 'CH4': material_GWP_breakdown.get('CH4', 0), - 'net electricity': microalgae_lca.net_electricity_GWP, - 'direct non-biogenic emissions': microalgae_lca.direct_non_biogenic_emissions_GWP, - } - - # Normalize GWP breakdown - tot_positive_GWP = sum([v for v in gwp_breakdown.values() if v > 0]) - if tot_positive_GWP > 0: - for k, v in gwp_breakdown.items(): - gwp_breakdown[k] = v / tot_positive_GWP - - results_dict['Baseline']['GWP Breakdown'] = gwp_breakdown - - # FEC breakdown (simplified) - fec_breakdown = { - 'feedstock*': microalgae_lca.feedstock_FEC if hasattr(microalgae_lca, 'feedstock_FEC') else 0, - 'materials': sum(material_GWP_breakdown.values()), # Approximation - 'net electricity': microalgae_lca.net_electricity_FEC if hasattr(microalgae_lca, 'net_electricity_FEC') else 0, - } - - results_dict['Baseline']['FEC Breakdown'] = fec_breakdown - - print(f'\nBaseline MPSP: {baseline_metrics[0]:.3f} USD/kg') - print(f'Baseline GWP100a: {baseline_metrics[1]:.3f} kg CO2-eq/kg') - print(f'Baseline FEC: {baseline_metrics[2]:.3f} MJ/kg') - - # Monte Carlo simulation - print(f'\n\nRunning Monte Carlo simulation with {N_simulations_per_mode} samples...') - - def notification(i): - if i % notification_interval == 0: - print(f'Completed {i}/{N_simulations_per_mode} simulations') - - samples = model.sample(N_simulations_per_mode, rule='L') - model.load_samples(samples) - model.evaluate(notification=notification, autoload=False, autosave=False) - - results = model.table - - # Process uncertainty results - metrics = ['MPSP', 'GWP100a', 'FEC'] - for metric in metrics: - metric_values = results[metric].values - percentile_values = np.percentile(metric_values, [p*100 for p in percentiles]) - results_dict['Uncertainty'][metric] = dict(zip(percentiles, percentile_values)) - - # Sensitivity analysis (Spearman correlation) - print(f'\n\nCalculating Spearman correlations...') - - for metric in metrics: - correlations = {} - metric_values = results[metric].values - - for param in parameters: - param_values = results[param.name].values - # Calculate Spearman correlation - correlation = np.corrcoef( - np.argsort(np.argsort(param_values)), - np.argsort(np.argsort(metric_values)) - )[0, 1] - correlations[param.name] = correlation - - # Sort by absolute correlation - sorted_correlations = dict(sorted(correlations.items(), - key=lambda x: abs(x[1]), - reverse=True)) - results_dict['Sensitivity']['Spearman'][metric] = sorted_correlations - - timer.toc() - - return results_dict, results - -def save_results(results_dict, results, filename_prefix='microalgae_uncertainty'): - """Save analysis results to files.""" - - timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') - - # Save results dictionary - results_df = pd.DataFrame() - - # Baseline results - baseline_data = { - 'Metric': ['MPSP', 'GWP100a', 'FEC'], - 'Baseline': [ - results_dict['Baseline']['MPSP'], - results_dict['Baseline']['GWP100a'], - results_dict['Baseline']['FEC'] - ] - } - - # Add uncertainty percentiles - for percentile in percentiles: - baseline_data[f'P{int(percentile*100)}'] = [ - results_dict['Uncertainty']['MPSP'][percentile], - results_dict['Uncertainty']['GWP100a'][percentile], - results_dict['Uncertainty']['FEC'][percentile] - ] - - results_df = pd.DataFrame(baseline_data) - - # Save to Excel - excel_filename = f'{filename_prefix}_results_{timestamp}.xlsx' - with pd.ExcelWriter(excel_filename) as writer: - results_df.to_excel(writer, sheet_name='Summary', index=False) - - # Save full Monte Carlo results - results.to_excel(writer, sheet_name='Monte_Carlo', index=False) - - # Save sensitivity analysis - for metric in ['MPSP', 'GWP100a', 'FEC']: - sensitivity_data = pd.DataFrame( - list(results_dict['Sensitivity']['Spearman'][metric].items()), - columns=['Parameter', 'Spearman_Correlation'] - ) - sensitivity_data.to_excel(writer, sheet_name=f'Sensitivity_{metric}', index=False) - - # Save GWP breakdown - gwp_breakdown_df = pd.DataFrame( - list(results_dict['Baseline']['GWP Breakdown'].items()), - columns=['Component', 'Fraction'] - ) - gwp_breakdown_df.to_excel(writer, sheet_name='GWP_Breakdown', index=False) - - print(f'\nResults saved to {excel_filename}') - - # Save summary text report - txt_filename = f'{filename_prefix}_summary_{timestamp}.txt' - with open(txt_filename, 'w') as f: - f.write("Microalgae Biorefinery Uncertainty Analysis Summary\n") - f.write("=" * 50 + "\n\n") - - f.write("Baseline Results:\n") - f.write(f"MPSP: {results_dict['Baseline']['MPSP']:.3f} USD/kg\n") - f.write(f"GWP100a: {results_dict['Baseline']['GWP100a']:.3f} kg CO2-eq/kg\n") - f.write(f"FEC: {results_dict['Baseline']['FEC']:.3f} MJ/kg\n\n") - - f.write("Uncertainty Analysis (percentiles):\n") - for metric in ['MPSP', 'GWP100a', 'FEC']: - f.write(f"\n{metric}:\n") - for percentile in [0.05, 0.25, 0.5, 0.75, 0.95]: - value = results_dict['Uncertainty'][metric][percentile] - f.write(f" P{int(percentile*100)}: {value:.3f}\n") - - f.write("\nTop 10 Most Influential Parameters (by absolute Spearman correlation):\n") - for metric in ['MPSP', 'GWP100a', 'FEC']: - f.write(f"\n{metric}:\n") - correlations = results_dict['Sensitivity']['Spearman'][metric] - for i, (param, corr) in enumerate(list(correlations.items())[:10]): - f.write(f" {i+1}. {param}: {corr:.3f}\n") - - print(f'Summary saved to {txt_filename}') - -if __name__ == '__main__': - print("Starting microalgae biorefinery uncertainty analysis...") - - try: - results_dict, results = run_uncertainty_analysis() - save_results(results_dict, results) - - print("\nAnalysis completed successfully!") - - except Exception as e: - print(f"\nError during analysis: {str(e)}") - import traceback - traceback.print_exc() \ No newline at end of file +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +# Bioindustrial-Park: BioSTEAM's Premier Biorefinery Models and Results +# Copyright (C) 2021-, Sarang Bhagwat +# +# This module is under the UIUC open-source license. See +# github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/LICENSE.txt +# for license details. + +Uncertainty analysis for microalgae biorefinery + +Based on succinic project but adapted for microalgae system structure +""" + +from warnings import filterwarnings +filterwarnings('ignore') +import numpy as np +import pandas as pd +import biosteam as bst +# import contourplots +from .system import microalgae_mcca_sys, microalgae_tea +from .lca import create_microalgae_lca +from .model_utils import MicroalgaeModel +from biosteam.evaluation import Metric +from datetime import datetime +from biosteam.utils import TicToc +import os + +chdir = os.chdir +microalgae_filepath = os.path.dirname(__file__) +microalgae_results_filepath = os.path.join(microalgae_filepath, 'analyses', 'results') + +# Create results directory if it doesn't exist +if not os.path.exists(microalgae_results_filepath): + os.makedirs(microalgae_results_filepath) + +system = microalgae_sys = microalgae_mcca_sys +tea = microalgae_tea + +# Create model with metrics +def create_model(): + """Create evaluation model with metrics for microalgae system""" + # Get system components + u = microalgae_mcca_sys.flowsheet.unit + s = microalgae_mcca_sys.flowsheet.stream + + # Find main product and boiler + main_product = s.caproic_acid_product + main_product_chemical_IDs = ['CaproicAcid'] + + # Find boiler + boiler = None + for unit in microalgae_mcca_sys.units: + if hasattr(unit, 'natural_gas') or 'BT' in unit.ID: + boiler = unit + break + + # Create LCA object + lca = create_microalgae_lca(microalgae_mcca_sys, main_product, main_product_chemical_IDs, boiler) + + # Define metrics + metrics = [ + Metric('MPSP', lambda: microalgae_tea.solve_price(main_product), '$/kg'), + Metric('TCI', lambda: microalgae_tea.TCI/1e6, 'MM$'), + Metric('VOC', lambda: microalgae_tea.VOC/1e6, 'MM$/y'), + Metric('FOC', lambda: microalgae_tea.FOC/1e6, 'MM$/y'), + Metric('GWP', lambda: lca.GWP, 'kg CO2-eq/kg'), + Metric('FEC', lambda: lca.FEC, 'MJ/kg'), + ] + + # Create namespace for parameter loading + namespace_dict = { + 'microalgae_sys': microalgae_mcca_sys, + 'microalgae_tea': microalgae_tea, + 'u': u, + 's': s, + 'lca': lca, + 'bst': bst, + 'np': np, + # Add chemical streams for easier access + 'microalgae': None, # Will be set dynamically + 'GlucoAmylase': None, # Will be set dynamically + 'AlphaAmylase': None, # Will be set dynamically + 'Yeast': None, + 'OleylAlcohol': None, + 'base_fermentation': None, + 'FGD_lime': None, + 'PowerUtility': bst.PowerUtility, + } + + # Try to find chemical streams + for stream in microalgae_mcca_sys.feeds: + if 'microalgae' in stream.ID.lower(): + namespace_dict['microalgae'] = stream + break + + # Create model + model = MicroalgaeModel(microalgae_mcca_sys, metrics=metrics, namespace_dict=namespace_dict) + + return model, lca, namespace_dict + +model, lca, namespace_dict = create_model() + +def get_adjusted_MSP(): + """Get adjusted minimum selling price""" + return microalgae_tea.solve_price(microalgae_sys.flowsheet.stream.caproic_acid_product) + +# %% + +N_simulations_per_mode = 2000 # 2000 + +percentiles = [0, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 1] + +notification_interval = 10 + +results_dict = {'Baseline':{'MPSP':{}, 'GWP100a':{}, 'FEC':{}, + 'GWP Breakdown':{}, 'FEC Breakdown':{},}, + 'Uncertainty':{'MPSP':{}, 'GWP100a':{}, 'FEC':{}}, + 'Sensitivity':{'Spearman':{'MPSP':{}, 'GWP100a':{}, 'FEC':{}}},} + +modes = [ + 'baseline', + ] + +parameter_distributions_filenames = [ + 'parameter_distributions.xlsx', + ] + +#%% + +timer = TicToc('timer') +timer.tic() + +# Set seed to make sure each time the same set of random numbers will be used +np.random.seed(3221) # 3221 + +for i in range(len(modes)): + mode = modes[i] + parameter_distributions_filename = os.path.join(microalgae_filepath, parameter_distributions_filenames[i]) + + print(f'\n\nLoading parameter distributions ({mode}) ...') + model.parameters = () + model.load_parameter_distributions(parameter_distributions_filename, namespace_dict) + print(f'\nLoaded parameter distributions ({mode}).') + + parameters = model.get_parameters() + + print('\n\nLoading samples ...') + samples = model.sample(N=N_simulations_per_mode, rule='L') + model.load_samples(samples) + print('\nLoaded samples.') + + model.exception_hook = 'warn' + print('\n\nSimulating baseline ...') + baseline_initial = model.metrics_at_baseline() + baseline = pd.DataFrame(data=np.array([[i for i in baseline_initial.values],]), + columns=baseline_initial.keys()) + + results_dict['Baseline']['MPSP'][mode] = get_adjusted_MSP() + results_dict['Baseline']['GWP100a'][mode] = tot_GWP = lca.GWP + results_dict['Baseline']['FEC'][mode] = tot_FEC = lca.FEC + + # GWP breakdown analysis + try: + material_GWP_breakdown = lca.material_GWP_breakdown + + results_dict['Baseline']['GWP Breakdown'][mode] = { + 'feedstock': lca.feedstock_GWP, + 'material inputs': lca.material_GWP, + 'natural gas\n(for steam generation)': getattr(lca, 'ng_GWP', 0), + 'net electricity': lca.net_electricity_GWP, + 'direct non-biogenic\nemissions': getattr(lca, 'direct_emissions_GWP', 0), + } + + tot_positive_GWP = sum([v for v in results_dict['Baseline']['GWP Breakdown'][mode].values() if v>0]) + if tot_positive_GWP > 0: + for k, v in results_dict['Baseline']['GWP Breakdown'][mode].items(): + results_dict['Baseline']['GWP Breakdown'][mode][k] = v/tot_positive_GWP + except Exception as e: + print(f"Warning: Could not calculate GWP breakdown: {e}") + results_dict['Baseline']['GWP Breakdown'][mode] = {} + + # FEC breakdown analysis + try: + material_FEC_breakdown = lca.material_FEC_breakdown + + results_dict['Baseline']['FEC Breakdown'][mode] = { + 'feedstock': lca.feedstock_FEC, + 'material inputs': lca.material_FEC, + 'natural gas\n(for steam generation)': getattr(lca, 'ng_FEC', 0), + 'net electricity': lca.net_electricity_FEC, + } + + tot_positive_FEC = sum([v for v in results_dict['Baseline']['FEC Breakdown'][mode].values() if v>0]) + if tot_positive_FEC > 0: + for k, v in results_dict['Baseline']['FEC Breakdown'][mode].items(): + results_dict['Baseline']['FEC Breakdown'][mode][k] = v/tot_positive_FEC + except Exception as e: + print(f"Warning: Could not calculate FEC breakdown: {e}") + results_dict['Baseline']['FEC Breakdown'][mode] = {} + + print(f"\nSimulated baseline. MPSP = ${round(results_dict['Baseline']['MPSP'][mode],2)}/kg.") + print('\n\nEvaluating ...') + model.evaluate(notify=notification_interval, autoload=None, autosave=None, file=None) + print('\nFinished evaluation.') + + # Baseline results + print('\n\nRe-simulating baseline ...') + baseline_end = model.metrics_at_baseline() + print(f"\nRe-simulated baseline. MPSP = ${round(results_dict['Baseline']['MPSP'][mode],2)}/kg.") + dateTimeObj = datetime.now() + minute = '0' + str(dateTimeObj.minute) if len(str(dateTimeObj.minute))==1 else str(dateTimeObj.minute) + file_to_save = os.path.join(microalgae_results_filepath, + f'_microalgae_{dateTimeObj.year}.{dateTimeObj.month}.{dateTimeObj.day}-{dateTimeObj.hour}.{minute}'\ + + f'_{N_simulations_per_mode}sims') + + baseline.index = ('initial', ) + baseline.to_excel(file_to_save+'_'+mode+'_0_baseline.xlsx') + + # Parameters + parameters = model.get_parameters() + index_parameters = len(model.get_baseline_sample()) + parameter_values = model.table.iloc[:, :index_parameters].copy() + + #%% + + # TEA results + for index_TEA, i in enumerate(model.metrics): + if hasattr(i, 'element') and i.element == 'LCA': + break + else: + index_TEA = len(model.metrics) - 2 # Assume last 2 are LCA metrics + + index_TEA = index_parameters + index_TEA + TEA_results = model.table.iloc[:, index_parameters:index_TEA].copy() + TEA_percentiles = TEA_results.quantile(q=percentiles) + + # LCA_results + LCA_results = model.table.iloc[:, index_TEA::].copy() + LCA_percentiles = LCA_results.quantile(q=percentiles) + + # # Spearman's rank correlation + table = model.table + model.table = model.table.dropna() + + spearman_results = model.spearman() + spearman_results.columns = pd.Index([i.name_with_units for i in model.metrics]) + + model.table = table + + # Calculate the cumulative probabilities of each parameter + probabilities = {} + for i in range(index_parameters): + p = parameters[i] + p_values = parameter_values.iloc[:, 2*i] + probabilities[p.name] = p.distribution.cdf(p_values) + parameter_values.insert(loc=2*i+1, + column=(parameter_values.iloc[:, 2*i].name[0], 'Probability'), + value=probabilities[p.name], + allow_duplicates=True) + + run_number = samples.shape[0] + + #%% + '''Output to Excel''' + with pd.ExcelWriter(file_to_save+'_'+mode+'_1_full_evaluation.xlsx') as writer: + parameter_values.to_excel(writer, sheet_name='Parameters') + TEA_results.to_excel(writer, sheet_name='TEA results') + TEA_percentiles.to_excel(writer, sheet_name='TEA percentiles') + LCA_results.to_excel(writer, sheet_name='LCA results') + LCA_percentiles.to_excel(writer, sheet_name='LCA percentiles') + spearman_results.to_excel(writer, sheet_name='Spearman') + model.table.to_excel(writer, sheet_name='Raw data') + + # Extract results for plotting + # Find the MPSP column + mpsp_col = None + for col in model.table.columns: + if 'MPSP' in str(col) or 'price' in str(col).lower(): + mpsp_col = col + break + + # Find GWP column + gwp_col = None + for col in model.table.columns: + if 'GWP' in str(col): + gwp_col = col + break + + # Find FEC column + fec_col = None + for col in model.table.columns: + if 'FEC' in str(col): + fec_col = col + break + + if mpsp_col is not None: + results_dict['Uncertainty']['MPSP'][mode] = model.table[mpsp_col] + else: + print("Warning: Could not find MPSP column in results") + results_dict['Uncertainty']['MPSP'][mode] = [results_dict['Baseline']['MPSP'][mode]] * len(model.table) + + if gwp_col is not None: + results_dict['Uncertainty']['GWP100a'][mode] = model.table[gwp_col] + else: + print("Warning: Could not find GWP column in results") + results_dict['Uncertainty']['GWP100a'][mode] = [results_dict['Baseline']['GWP100a'][mode]] * len(model.table) + + if fec_col is not None: + results_dict['Uncertainty']['FEC'][mode] = model.table[fec_col] + else: + print("Warning: Could not find FEC column in results") + results_dict['Uncertainty']['FEC'][mode] = [results_dict['Baseline']['FEC'][mode]] * len(model.table) + + # Spearman correlations for sensitivity analysis + df_rho, df_p = model.spearman_r() + + if mpsp_col is not None and mpsp_col in df_rho.columns: + results_dict['Sensitivity']['Spearman']['MPSP'][mode] = df_rho[mpsp_col] + else: + results_dict['Sensitivity']['Spearman']['MPSP'][mode] = pd.Series() + + if gwp_col is not None and gwp_col in df_rho.columns: + results_dict['Sensitivity']['Spearman']['GWP100a'][mode] = df_rho[gwp_col] + else: + results_dict['Sensitivity']['Spearman']['GWP100a'][mode] = pd.Series() + + if fec_col is not None and fec_col in df_rho.columns: + results_dict['Sensitivity']['Spearman']['FEC'][mode] = df_rho[fec_col] + else: + results_dict['Sensitivity']['Spearman']['FEC'][mode] = pd.Series() + +#%% Clean up NaN values for plotting +metrics = ['MPSP', 'GWP100a', 'FEC'] +tot_NaN_vals_dict = results_dict['Errors'] = {metric: {mode: 0 for mode in modes} for metric in metrics} +for mode in modes: + for metric in metrics: + median_val = 1.5 # Default fallback value + if len(results_dict['Uncertainty'][metric][mode]) > 0: + median_val = np.nanmedian(results_dict['Uncertainty'][metric][mode]) + if np.isnan(median_val): + median_val = 1.5 + + for i in range(len(results_dict['Uncertainty'][metric][mode])): + if np.isnan(results_dict['Uncertainty'][metric][mode].iloc[i]): + results_dict['Uncertainty'][metric][mode].iloc[i] = median_val + tot_NaN_vals_dict[metric][mode] += 1 + +# %% Plots - temporarily disabled due to contourplots dependency +# MPSP_units = r"$\mathrm{\$}\cdot\mathrm{kg}^{-1}$" +# GWP_units = r"$\mathrm{kg}$"+" "+ r"$\mathrm{CO}_{2}\mathrm{-eq.}\cdot\mathrm{kg}^{-1}$" +# FEC_units = r"$\mathrm{MJ}\cdot\mathrm{kg}^{-1}$" + +scenario_name_labels = ['Baseline'] + +def get_small_range(num, offset): + return(num-offset, num+offset) + +print(f'\nAnalysis completed. Timer: {timer.toc():.2f} seconds') +print(f'Results saved to: {file_to_save}') + +# Print summary +print('\n=== UNCERTAINTY ANALYSIS SUMMARY ===') +for mode in modes: + print(f'\n{mode.upper()} MODE:') + print(f' MPSP: {results_dict["Baseline"]["MPSP"][mode]:.3f} $/kg') + print(f' GWP: {results_dict["Baseline"]["GWP100a"][mode]:.3f} kg CO2-eq/kg') + print(f' FEC: {results_dict["Baseline"]["FEC"][mode]:.3f} MJ/kg') + + if len(results_dict['Uncertainty']['MPSP'][mode]) > 0: + print(f' MPSP range: {np.min(results_dict["Uncertainty"]["MPSP"][mode]):.3f} - {np.max(results_dict["Uncertainty"]["MPSP"][mode]):.3f} $/kg') + print(f' GWP range: {np.min(results_dict["Uncertainty"]["GWP100a"][mode]):.3f} - {np.max(results_dict["Uncertainty"]["GWP100a"][mode]):.3f} kg CO2-eq/kg') + print(f' FEC range: {np.min(results_dict["Uncertainty"]["FEC"][mode]):.3f} - {np.max(results_dict["Uncertainty"]["FEC"][mode]):.3f} MJ/kg') + +# Print detailed breakdown analysis +print('\n=== DETAILED BREAKDOWN ANALYSIS ===') +for mode in modes: + print(f'\n{mode.upper()} MODE BREAKDOWN:') + + # GWP Breakdown + if results_dict['Baseline']['GWP Breakdown'][mode]: + print(f'\nGWP Breakdown (Total: {results_dict["Baseline"]["GWP100a"][mode]:.3f} kg CO2-eq/kg):') + gwp_breakdown = results_dict['Baseline']['GWP Breakdown'][mode] + total_gwp = sum([abs(v) for v in gwp_breakdown.values()]) + for component, value in gwp_breakdown.items(): + percentage = (value / total_gwp * 100) if total_gwp > 0 else 0 + print(f' {component}: {value:.4f} kg CO2-eq/kg ({percentage:.1f}%)') + + # FEC Breakdown + if results_dict['Baseline']['FEC Breakdown'][mode]: + print(f'\nFEC Breakdown (Total: {results_dict["Baseline"]["FEC"][mode]:.3f} MJ/kg):') + fec_breakdown = results_dict['Baseline']['FEC Breakdown'][mode] + total_fec = sum([abs(v) for v in fec_breakdown.values()]) + for component, value in fec_breakdown.items(): + percentage = (value / total_fec * 100) if total_fec > 0 else 0 + print(f' {component}: {value:.4f} MJ/kg ({percentage:.1f}%)') + +# Print sensitivity analysis results +print('\n=== SENSITIVITY ANALYSIS (SPEARMAN CORRELATIONS) ===') +for mode in modes: + print(f'\n{mode.upper()} MODE SENSITIVITY:') + + # MPSP Correlations + if not results_dict['Sensitivity']['Spearman']['MPSP'][mode].empty: + print(f'\nTop 10 correlations with MPSP:') + mpsp_corr = results_dict['Sensitivity']['Spearman']['MPSP'][mode].copy() + # Remove NaN values and sort by absolute correlation + mpsp_corr = mpsp_corr.dropna() + if len(mpsp_corr) > 0: + sorted_corr = mpsp_corr.abs().sort_values(ascending=False) + for i, (param, abs_corr) in enumerate(sorted_corr.head(10).items()): + actual_corr = mpsp_corr[param] + print(f' {i+1:2d}. {param}: {actual_corr:.3f}') + else: + print(' No significant correlations found') + + # GWP Correlations + if not results_dict['Sensitivity']['Spearman']['GWP100a'][mode].empty: + print(f'\nTop 10 correlations with GWP:') + gwp_corr = results_dict['Sensitivity']['Spearman']['GWP100a'][mode].copy() + gwp_corr = gwp_corr.dropna() + if len(gwp_corr) > 0: + sorted_corr = gwp_corr.abs().sort_values(ascending=False) + for i, (param, abs_corr) in enumerate(sorted_corr.head(10).items()): + actual_corr = gwp_corr[param] + print(f' {i+1:2d}. {param}: {actual_corr:.3f}') + else: + print(' No significant correlations found') + + # FEC Correlations + if not results_dict['Sensitivity']['Spearman']['FEC'][mode].empty: + print(f'\nTop 10 correlations with FEC:') + fec_corr = results_dict['Sensitivity']['Spearman']['FEC'][mode].copy() + fec_corr = fec_corr.dropna() + if len(fec_corr) > 0: + sorted_corr = fec_corr.abs().sort_values(ascending=False) + for i, (param, abs_corr) in enumerate(sorted_corr.head(10).items()): + actual_corr = fec_corr[param] + print(f' {i+1:2d}. {param}: {actual_corr:.3f}') + else: + print(' No significant correlations found') + +# Print statistics summary +print('\n=== STATISTICAL SUMMARY ===') +for mode in modes: + print(f'\n{mode.upper()} MODE STATISTICS:') + + for metric in ['MPSP', 'GWP100a', 'FEC']: + if len(results_dict['Uncertainty'][metric][mode]) > 0: + data = results_dict['Uncertainty'][metric][mode] + mean_val = np.mean(data) + std_val = np.std(data) + p5 = np.percentile(data, 5) + p95 = np.percentile(data, 95) + median_val = np.median(data) + + units = {'MPSP': '$/kg', 'GWP100a': 'kg CO2-eq/kg', 'FEC': 'MJ/kg'} + unit = units[metric] + + print(f'\n{metric}:') + print(f' Mean: {mean_val:.3f} {unit}') + print(f' Std Dev: {std_val:.3f} {unit}') + print(f' Median: {median_val:.3f} {unit}') + print(f' 5th percentile: {p5:.3f} {unit}') + print(f' 95th percentile: {p95:.3f} {unit}') + print(f' Range: {np.min(data):.3f} - {np.max(data):.3f} {unit}') + +# Print error summary +print('\n=== ERROR SUMMARY ===') +total_errors = 0 +for metric in metrics: + for mode in modes: + errors = tot_NaN_vals_dict[metric][mode] + if errors > 0: + print(f'{metric} ({mode}): {errors} NaN values replaced') + total_errors += errors + +if total_errors == 0: + print('No errors encountered during simulation.') +else: + print(f'Total errors handled: {total_errors}') + +print(f'\n=== ANALYSIS COMPLETED ===') +print(f'Total simulation time: {timer.toc():.2f} seconds') +print(f'Results saved to: {file_to_save}') +print(f'Number of parameters analyzed: {len(parameters)}') +print(f'Number of successful simulations: {len(model.table)}') +print(f'Output files generated:') +print(f' - Baseline: {file_to_save}_{mode}_0_baseline.xlsx') +print(f' - Full results: {file_to_save}_{mode}_1_full_evaluation.xlsx') + +if __name__ == '__main__': + print("Microalgae uncertainty analysis completed!") \ No newline at end of file diff --git a/biorefineries/microalgae/units.py b/biorefineries/microalgae/units.py index 7c44f866..6b2d4003 100644 --- a/biorefineries/microalgae/units.py +++ b/biorefineries/microalgae/units.py @@ -24,8 +24,7 @@ import biosteam as bst import math from biosteam import Unit -from biosteam.exceptions import DesignError -from biosteam.units.design_tools import pressure_vessel_material_factors as factors +from biosteam.units.design_tools import pressure_vessel_material_factors from biosteam.units.decorators import cost from biosteam.units import HXutility, Mixer, MixTank, Pump, StorageTank, StirredTankReactor from thermosteam import Stream @@ -47,7 +46,6 @@ def find_split(*tuples): split[ID] = flow0 / total if total else 0 return split - Rxn = tmo.reaction.Reaction ParallelRxn = tmo.reaction.ParallelReaction ln = math.log @@ -78,7 +76,6 @@ def find_split(*tuples): tmo.settings.set_thermo(chems) - # %% # ========================= # Microalgae Crushing @@ -86,7 +83,6 @@ def find_split(*tuples): @cost(basis='Flow rate', ID='System', units='kg/hr', kW=511.3205, cost=13329690, S=94697, CE=CEPCI[2009], n=0.6, BM=1.7) class FeedstockPreprocessing(Unit): - # 2205 U.S. ton/day (2000 metric tonne/day) as in Humbird et al. _baseline_flow_rate = baseline_feedflow.sum() _cached_flow_rate = 2205 _N_ins = 1 @@ -141,7 +137,7 @@ def _run(self): # Pretreatment reactor @cost(basis='Dry flow rate', ID='Pretreatment reactor', units='kg/hr', - kW=512, cost=1981240, S=83333, CE=CEPCI[2009], n=0.6, BM=1.5) + kW=5120, cost=19812400, S=83333, CE=CEPCI[2009], n=0.6, BM=1.5) class AcidPretreatmentReactor(Unit): _N_ins = 1 _N_outs = 1 @@ -290,27 +286,22 @@ def _design(self): ## MCCA Fermentation @cost(basis='Fermenter size', ID='Fermenter', units='kg', - cost=5128000, S=(42607+443391+948+116)*(60+36), + kW=100, cost=5128000, S=(42607+443391+948+116)*(60+36), CE=CEPCI[2009], n=0.7, BM=1.5) @cost(basis='Fermenter size', ID='Fermenter agitator', units='kg', - # Scaling basis based on sum of all streams into fermenter - # (304, 306, 311, and 312 in ref [1]) - # and total residence time (batch hydrolysis and fermentation) - # kW=268.452, - kW=0, # overwritten; power utility calculated separately based on batch IBRL power per unit volume + kW=10, cost=630000, S=(42607+443391+948+116)*(60+36), CE=CEPCI[2009], n=0.7, BM=1.5) class MCCAFermentation(StirredTankReactor): _N_ins = 5 - _N_outs = 3 + _N_outs = 4 auxiliary_unit_names = ('heat_exchanger',) _units= { **Reactor._units, 'Fermenter size': 'kg', - 'Recirculation flow rate': 'kg/hr', 'Broth flow rate': 'kg/hr', } @@ -334,7 +325,7 @@ def __init__(self, ID='', ins=None, outs=(), thermo=None, *, T=37+273.15, base_neutralizes_product=True, tau = tau_cofermentation, microalgae_mass_flow: float | None = None, # kg/hr basis of original algae for yield calc - titer: float = 60.0, # g/L - 默认浓度为60 g/L + titer: float = 2.003, # g/L # allow_dilution=False, # allow_concentration=False, # sugars=None @@ -359,14 +350,13 @@ def __init__(self, ID='', ins=None, outs=(), thermo=None, *, T=37+273.15, self.neutralization = neutralization self.neutralizing_agent = neutralizing_agent self.tau = tau + #self.allow_dilution = allow_dilution #self.allow_concentration = allow_concentration #self.sugars = sugars or tuple(i.ID for i in self.chemicals.sugars) # Store optional explicit biomass flow for yield calculations self.microalgae_mass_flow = microalgae_mass_flow - - # 添加titer参数 self.titer = titer ID = self.ID @@ -425,41 +415,35 @@ def __init__(self, ID='', ins=None, outs=(), thermo=None, *, T=37+273.15, def _run(self): substrate, yeast_seed, FermMicrobe, lime, n2 = self.ins - broth, gas, fermentation_waste = self.outs + broth, gas, fermentation_waste, yeast_recycle = self.outs broth.mix_from(self.ins) broth.T = self.T broth.P = self.P # Determine basis biomass mass (kg/hr) microalgae_mass = self.microalgae_mass_flow - - # 获取产率因子和浓度 caproic_acid_yield_factor = getattr(self, 'caproic_acid_yield_factor', 1.0) - titer = getattr(self, 'titer', 60.0) # g/L + titer = getattr(self, 'titer', 2.003) # g/L - # 计算浓度对产率的影响 - 非线性关系 - # 当浓度过高时,会抑制产率 - # 基准浓度设为60 g/L,此时无抑制 - base_titer = 60.0 # g/L + base_titer = 2.003 + inhibition_titer = 10.0 - # 计算抑制系数 (产物抑制模型) - # 高于基准浓度时,产率降低;低于时,产率略有提高 if titer > base_titer: - # 产物抑制模型:exp(-k*(titer-base_titer)) - inhibition_factor = np.exp(-0.01 * (titer - base_titer)) + if titer <= inhibition_titer: + inhibition_factor = 1.0 - 0.05 * (titer - base_titer) / (inhibition_titer - base_titer) + else: + inhibition_factor = 0.95 * np.exp(-0.1 * (titer - inhibition_titer)) else: - # 低浓度时略有提高,但提高有限 - inhibition_factor = 1.0 + 0.005 * (base_titer - titer) - - # 应用抑制效应到产率因子 + inhibition_factor = 1.0 + 0.01 * (base_titer - titer) + effective_yield_factor = caproic_acid_yield_factor * inhibition_factor base_yields = { - 'Ethanol': 0.018, + 'Ethanol': 0.01, 'Butanol': 0.004, - 'AceticAcid': 0.10, + 'AceticAcid': 0.1, 'PropionicAcid': 0.009, - 'ButyricAcid': 0.2, - 'ValericAcid': 0.012, + 'ButyricAcid': 0.18, + 'ValericAcid': 0.01, 'CaproicAcid': 0.27, 'HeptanoicAcid': 0.006, 'CaprylicAcid': 0.04 @@ -468,19 +452,15 @@ def _run(self): total_base_yield = sum(base_yields.values()) base_caproic_yield = base_yields['CaproicAcid'] - # 使用有效产率因子计算新产率 new_caproic_yield = base_caproic_yield * effective_yield_factor delta_caproic = new_caproic_yield - base_caproic_yield total_other_base_yield = total_base_yield - base_caproic_yield - # 计算发酵液体积 (L) - 用于实现目标浓度 - # 假设发酵液密度为1 kg/L - target_caproic_mass = microalgae_mass * new_caproic_yield # kg/hr - target_volume_L = target_caproic_mass * 1000 / titer # 转换为L - - # 调整产品质量以匹配目标浓度 + target_caproic_mass = microalgae_mass * new_caproic_yield + target_volume_L = target_caproic_mass * 1000 / titer + if abs(delta_caproic) > 1e-6 and total_other_base_yield > 1e-6: - adjustment_factor = (total_other_base_yield - delta_caproic) / total_other_base_yield + adjustment_factor = max(0.1, (total_other_base_yield - delta_caproic) / total_other_base_yield) for compound, base_yield in base_yields.items(): if compound == 'CaproicAcid': broth.imass[compound] = target_caproic_mass @@ -493,29 +473,245 @@ def _run(self): else: broth.imass[compound] = microalgae_mass * base_yield - # 确保总水量与目标体积一致 - total_water_needed = max(target_volume_L - broth.F_vol, 0) # L - broth.imass['Water'] = total_water_needed # 假设密度为1 kg/L - - # 气体产物 + current_volume = broth.F_vol # L + if current_volume > 0 and titer >= 5.0: + water_adjustment = max(0, target_volume_L - current_volume) + max_water = microalgae_mass * 10 + water_adjustment = min(water_adjustment, max_water) + broth.imass['Water'] += water_adjustment gas.copy_like(n2) - gas.imass['H2'] = microalgae_mass * 0.001 - gas.imass['CH4'] = microalgae_mass * 0.002 + gas.imass['H2'] = microalgae_mass * 0.1 + + # yeast recycle for simulating membrane bioreactor + yeast_recovery_rate = 0.95 + total_yeast_in_broth = broth.imass['Yeast'] + if total_yeast_in_broth > 0: + recoverable_yeast = total_yeast_in_broth * yeast_recovery_rate + yeast_recycle.empty() + yeast_recycle.imass['Yeast'] = recoverable_yeast + yeast_recycle.T = self.T + yeast_recycle.P = self.P + broth.imass['Yeast'] = total_yeast_in_broth - recoverable_yeast + else: + yeast_recycle.empty() + + fermentation_waste.empty() + + def _design(self): + Design = self.design_results + Design['Fermenter size'] = self.outs[0].F_mass * self.tau + Design['Broth flow rate'] = self.outs[0].F_mass + duty = 50 * self.F_mass_in # Heat duty + self.add_heat_utility(duty, self.T) + + +@cost(basis='Fermenter size', ID='Fermenter', units='kg', + kW=100, cost=5128000, S=(42607+443391+948+116)*(60+36), + CE=CEPCI[2009], n=0.7, BM=1.5) +@cost(basis='Fermenter size', ID='Fermenter agitator', units='kg', + kW=10, + cost=630000, + S=(42607+443391+948+116)*(60+36), + CE=CEPCI[2009], n=0.7, BM=1.5) +class MCCAFermentation_no_yeast(StirredTankReactor): + _N_ins = 4 + _N_outs = 3 + + auxiliary_unit_names = ('heat_exchanger',) + _units= { + **Reactor._units, + 'Fermenter size': 'kg', + 'Broth flow rate': 'kg/hr', + } + + tau_batch_turnaround = 12 # in hr, seedmentaiton time + tau_cofermentation = 15 * 24 # in hr, 15 d + mol_NH4OH_per_acid_pH_control = 0.2988 # from IBRL batch run + mol_lime_per_acid_pH_control = 0.2988/2 # mol NH4OH / 2 + pH = 5.0 + + def __init__(self, ID='', ins=None, outs=(), thermo=None, *, T=37+273.15, + P=101325, V_wf=0.8, length_to_diameter=0.6, + kW_per_m3=0.02 * 0.7457 / 0.075, # ref from succinic projects + mixing_intensity=300, + #wall_thickness_factor=1, + vessel_material='Stainless steel 304', + vessel_type='Vertical', + neutralization=True, + neutralizing_agent='Lime', + mode='batch', feed_freq=1, + pH_control=True, + base_neutralizes_product=True, + tau = tau_cofermentation, + microalgae_mass_flow: float | None = None, # kg/hr basis of original algae for yield calc + titer: float = 1.208, # g/L + # allow_dilution=False, + # allow_concentration=False, + # sugars=None + ): + StirredTankReactor.__init__(self, ID, ins, outs) + + self.T = T + self.P = P + self.V_wf = V_wf + self.length_to_diameter = length_to_diameter + self.mixing_intensity = mixing_intensity + self.kW_per_m3 = kW_per_m3 + #self.wall_thickness_factor = wall_thickness_factor + self.vessel_material = vessel_material + self.vessel_type = vessel_type + self.neutralization = neutralization + self.mode = mode + self.feed_freq = feed_freq + self.pH_control=pH_control + self.base_neutralizes_product = base_neutralizes_product + self.neutralization = neutralization + self.neutralizing_agent = neutralizing_agent + self.tau = tau + + #self.allow_dilution = allow_dilution + #self.allow_concentration = allow_concentration + #self.sugars = sugars or tuple(i.ID for i in self.chemicals.sugars) + + # Store optional explicit biomass flow for yield calculations + self.microalgae_mass_flow = microalgae_mass_flow + self.titer = titer + + ID = self.ID + self._mixed_feed = Stream(f'{ID}_mixed_feed') + self._tot_feed = Stream(f'{ID}_tot_feed') + # Before reaction, after reaction, with last feed + self._single_feed0 = Stream(f'{ID}_single_feed0') + self._single_feed1 = Stream(f'{ID}_single_feed1') + self._last = Stream(f'{ID}_last') + self._init = Stream(f'{ID}_init') + hx_in = Stream(f'{ID}_hx_in') + hx_out = Stream(f'{ID}_hx_out') + # Add '.' in ID for auxiliary units + self.heat_exchanger = HXutility(ID=f'.{ID}_hx', ins=hx_in, outs=hx_out, T=T) + + # the reaction is simplified for simulation + #self.mcca_rxns = ParallelRxn([ + #Rxn('Protein -> AceticAcid', 'Protein', 0.1), + #Rxn('Protein -> PropionicAcid', 'Protein', 0.1), + #Rxn('Protein -> ButyricAcid', 'Protein', 0.1), + #Rxn('Protein -> CaproicAcid', 'Protein', 0.1), + #Rxn('Glucose -> Ethanol', 'Glucose', 0.018), + #Rxn('Glucose -> Butanol', 'Glucose', 0.0022), + #Rxn('Glucose -> AceticAcid', 'Glucose', 0.10094), + #Rxn('Glucose -> PropionicAcid', 'Glucose', 0.00977), + #Rxn('Glucose -> ValericAcid', 'Glucose', 0.012), + #Rxn('Glucose -> HeptanoicAcid', 'Glucose', 0.004), + #Rxn('Glucose -> ButyricAcid', 'Glucose', 0.122), + #Rxn('Glucose -> CaproicAcid', 'Glucose', 0.134), + #Rxn('Glucose -> CaprylicAcid', 'Glucose', 0.008), + #]) + + # This is a Siplified Neutralization, because we don't want to affect the products yield + self.lime_neutralization_rxns = ParallelRxn([ + # Reaction definition Reactant Conversion + Rxn('H2SO4 + Lime -> CaSO4 + 2 H2O', 'H2SO4', 1) + ]) + + self.lime_pH_control_rxns = ParallelRxn([ + # Reaction definition Reactant Conversion + Rxn('H2SO4 + Lime -> CaSO4 + 2 H2O', 'H2SO4', 1) + ]) + + + # self.mcca_rxns = ParallelRxn([ + # Rxn('Glucose -> 2 Ethanol + 2 CO2', 'Glucose', 0.7), + # Rxn('Glucose -> 2 AceticAcid + 2 CO2 + 2 H2', 'Glucose', 0.2), + # Rxn('Glucose -> 2 PropionicAcid + 2 CO2 + 2 H2', 'Glucose', 0.1), + # Rxn('5 PropionicAcid + 6 Ethanol -> 5 ValericAcid + AceticAcid + 4 H2O + 2 H2', 'PropionicAcid', 0.7), + # Rxn('5 ValericAcid + 6 Ethanol -> 5 HeptanoicAcid + AceticAcid + 4 H2O + 2 H2', 'ValericAcid', 0.7), + # Rxn('ButyricAcid + Ethanol -> AceticAcid + Butanol', 'ButyricAcid', 0.1), + # Rxn('4 AceticAcid + 6 Ethanol -> 5 ButyricAcid + 4 H2O + 2 H2', 'AceticAcid', 0.7), + # Rxn('5 ButyricAcid + 6 Ethanol -> 5 CaproicAcid + AceticAcid + 4 H2O + 2 H2', 'ButyricAcid', 0.7), + # Rxn('5 CaproicAcid + 6 Ethanol -> 5 CaprylicAcid + AceticAcid + 4 H2O + 2 H2', 'CaproicAcid', 0.2), + # ]) + + def _run(self): + substrate, FermMicrobe, lime, n2 = self.ins + broth, gas, fermentation_waste= self.outs + broth.mix_from(self.ins) + broth.T = self.T + broth.P = self.P + # Determine basis biomass mass (kg/hr) + microalgae_mass = self.microalgae_mass_flow + caproic_acid_yield_factor = getattr(self, 'caproic_acid_yield_factor', 1.0) + titer = getattr(self, 'titer', 1.208) # g/L + + base_titer = 1.208 + inhibition_titer = 10.0 + + if titer > base_titer: + if titer <= inhibition_titer: + inhibition_factor = 1.0 - 0.05 * (titer - base_titer) / (inhibition_titer - base_titer) + else: + inhibition_factor = 0.95 * np.exp(-0.1 * (titer - inhibition_titer)) + else: + inhibition_factor = 1.0 + 0.01 * (base_titer - titer) + + effective_yield_factor = caproic_acid_yield_factor * inhibition_factor + + base_yields = { + 'Ethanol': 0.01, + 'Butanol': 0.002, + 'AceticAcid': 0.1, + 'PropionicAcid': 0.009, + 'ButyricAcid': 0.086, + 'ValericAcid': 0.01, + 'CaproicAcid': 0.08, + 'HeptanoicAcid': 0.004, + 'CaprylicAcid': 0.005 + } + + total_base_yield = sum(base_yields.values()) + base_caproic_yield = base_yields['CaproicAcid'] + + new_caproic_yield = base_caproic_yield * effective_yield_factor + delta_caproic = new_caproic_yield - base_caproic_yield + total_other_base_yield = total_base_yield - base_caproic_yield + + target_caproic_mass = microalgae_mass * new_caproic_yield + target_volume_L = target_caproic_mass * 1000 / titer + + if abs(delta_caproic) > 1e-6 and total_other_base_yield > 1e-6: + adjustment_factor = max(0.1, (total_other_base_yield - delta_caproic) / total_other_base_yield) + for compound, base_yield in base_yields.items(): + if compound == 'CaproicAcid': + broth.imass[compound] = target_caproic_mass + else: + broth.imass[compound] = microalgae_mass * base_yield * adjustment_factor + else: + for compound, base_yield in base_yields.items(): + if compound == 'CaproicAcid': + broth.imass[compound] = target_caproic_mass + else: + broth.imass[compound] = microalgae_mass * base_yield + + current_volume = broth.F_vol + if current_volume > 0 and titer >= 5.0: + water_adjustment = max(0, target_volume_L - current_volume) + max_water = microalgae_mass * 10 + water_adjustment = min(water_adjustment, max_water) + broth.imass['Water'] += water_adjustment + gas.copy_like(n2) + gas.imass['H2'] = microalgae_mass * 0.1 def _design(self): Design = self.design_results Design['Fermenter size'] = self.outs[0].F_mass * self.tau - Design['Recirculation flow rate'] = self.F_mass_in Design['Broth flow rate'] = self.outs[0].F_mass + duty = 50 * self.F_mass_in # Heat duty + self.add_heat_utility(duty, self.T) # %% # ========================= # Solid-Liquid Separation # ========================= -@cost(basis='Flow rate', ID='Filtrate discharge pump', units='kg/hr', - # Power not specified, based on filtrate tank discharge pump - kW=55.9275, cost=13040, S=31815, CE=CEPCI[2010], n=0.8, BM=2.3) @cost(basis='Flow rate', ID='Filtrate tank', units='kg/hr', cost=103000, S=31815, CE=CEPCI[2010], n=0.7, BM=2.0) @cost(basis='Flow rate', ID='Flitrate tank agitator', units='kg/hr', @@ -543,67 +739,6 @@ def _run(self): residue.P = feed.P - -# %% -# ========================= -# Distillation Units -# ========================= -@cost(basis='Flow rate', ID='Butanol Distillation', units='kg/hr', cost=2400000, S=10000, CE=CEPCI[2019], n=0.6, BM=2.5) -class ButanolDistillation(bst.units.BinaryDistillation): - def __init__(self, ID, ins=None, outs=()): - super().__init__( - ID, ins=ins, outs=outs, - LHK=('Butanol', 'Octanol'), - Lr=0.99, Hr=0.99, k=1.2, - partial_condenser=False, - is_divided=True - ) - -@cost(basis='Flow rate', ID='Butyric Acid Distillation', units='kg/hr', cost=2400000, S=10000, CE=CEPCI[2019], n=0.6, BM=2.5) -class ButyricAcidDistillation(bst.units.BinaryDistillation): - def __init__(self, ID, ins=None, outs=()): - super().__init__( - ID, ins=ins, outs=outs, - LHK=('ButyricAcid', 'Octanol'), - Lr=0.99, Hr=0.99, k=1.2, - partial_condenser=False, - is_divided=True - ) - -@cost(basis='Flow rate', ID='Caproic Acid Distillation', units='kg/hr', cost=2400000, S=10000, CE=CEPCI[2019], n=0.6, BM=2.5) -class CaproicAcidDistillation(bst.units.BinaryDistillation): - def __init__(self, ID, ins=None, outs=()): - super().__init__( - ID, ins=ins, outs=outs, - LHK=('CaproicAcid', 'Octanol'), - Lr=0.99, Hr=0.99, k=1.2, - partial_condenser=False, - is_divided=True - ) - -@cost(basis='Flow rate', ID='Heptanoic Acid Distillation', units='kg/hr', cost=2400000, S=10000, CE=CEPCI[2019], n=0.6, BM=2.5) -class HeptanoicAcidDistillation(bst.units.BinaryDistillation): - def __init__(self, ID, ins=None, outs=()): - super().__init__( - ID, ins=ins, outs=outs, - LHK=('HeptanoicAcid', 'Octanol'), - Lr=0.99, Hr=0.99, k=1.2, - partial_condenser=False, - is_divided=True - ) - -@cost(basis='Flow rate', ID='Caprylic Acid Distillation', units='kg/hr', cost=2400000, S=10000, CE=CEPCI[2019], n=0.6, BM=2.5) -class CaprylicAcidDistillation(bst.units.BinaryDistillation): - def __init__(self, ID, ins=None, outs=()): - super().__init__( - ID, ins=ins, outs=outs, - LHK=('CaprylicAcid', 'Octanol'), - Lr=0.99, Hr=0.99, k=1.2, - partial_condenser=False, - is_divided=True, - product_specification_format='Recovery' - ) - # %% # ========================= # Other Units @@ -625,29 +760,13 @@ def _run(self): -@cost('Flow rate', 'Anaerobic Digestion Reactor', units= 'kg/hr', kW=4.4, S=339151, +@cost('Flow rate', 'Anaerobic Digestion Reactor', units= 'kg/hr', kW=100, S=339151, cost=8000000, CE=CEPCI[2010], n=0.7, BM=1.8) class AnaerobicDigestion(bst.Unit): - def __init__(self, ID='', ins=None, outs=(), *, tau=15*24, microalgae_mass: float|None=None, **kwargs): - super().__init__(ID, ins, outs, **kwargs) - self.tau = tau - self.microalgae_mass = microalgae_mass + _N_ins = 1 _N_outs = 3 - - #: Working volume fraction (filled tank to total tank volume) - V_wf = 0.8 - - #: Number of reactors - N_reactors = 0 - - #: Number of transfer pumps - N_transfer_pumps = 1 - - #: Number of recirculation pumps - N_recirculation_pumps = 0 - T = 37 + 273.15 - + degrade_IDs = [ 'AceticAcid', 'PropionicAcid', 'ButyricAcid', 'ValericAcid', 'CaproicAcid', 'HeptanoicAcid', 'CaprylicAcid', @@ -656,16 +775,28 @@ def __init__(self, ID='', ins=None, outs=(), *, tau=15*24, microalgae_mass: floa ] inorganic_IDs = ['Ash', 'H2SO4', 'Na2SO4', 'AmmoniumSulfate', 'H2O'] + def __init__(self, ID='', ins=None, outs=(), *, tau=15*24, microalgae_mass: float|None=None, **kwargs): + super().__init__(ID, ins, outs, **kwargs) + self.tau = tau + self.microalgae_mass = microalgae_mass + self.T = 37 + 273.15 + + V_wf = 0.8 + + N_reactors = 0 + + N_transfer_pumps = 1 + + N_recirculation_pumps = 0 + def _run(self): feed = self.ins[0] biogas, waste, sludge = self.outs if self.microalgae_mass is None: raise ValueError('microalgae_mass must be supplied when instantiating AnaerobicDigestion.') microalgae_mass = self.microalgae_mass - # 100ml H2, 100 mL CH4 per 1g microalgae - # 1 mol CH4 = 22.4L = 22400ml,1 mol H2 = 22.4L = 22400ml - H2_mol = microalgae_mass * 100 / 22400 # 单位:g * ml/g / ml/mol = mol - CH4_mol = microalgae_mass * 200 / 22400 + H2_mol = microalgae_mass * 0.015 + CH4_mol = microalgae_mass * 0.015 biogas.empty() biogas.imol['H2'] = H2_mol biogas.imol['CH4'] = CH4_mol @@ -675,8 +806,8 @@ def _run(self): sludge.empty() for ID in self.degrade_IDs: amount = feed.imass[ID] - waste.imass[ID] = amount * 0.02 # 2% into wastewater, 98% remove rate - sludge.imass[ID] = amount * 0.25 # 40% into sludge + waste.imass[ID] = amount * 0.05 # 5% into wastewater + sludge.imass[ID] = amount * 0.25 # 25% into sludge for ID in self.inorganic_IDs: amount = feed.imass[ID] waste.imass[ID] = amount * 0.95 @@ -686,15 +817,15 @@ def _run(self): sludge.imass['Microalgae'] = 0 waste.phase = 'l' waste.T = sludge.T = self.T + + def _design(self): + Design = self.design_results + Design['Flow rate'] = sum([s.F_mass for s in self.outs]) # kg/hr + duty = 800 * self.microalgae_mass # Heat duty + self.add_heat_utility(duty, self.T) -__all__ = ( - 'FeedstockPreprocessing', 'AcidPretreatmentReactor', 'GlucoamylaseHydrolysis', - 'AlphaAmylaseHydrolysis', 'SolidLiquidSeparation', 'MCCAFermentation', - 'MembraneExtraction', 'OctanolExtraction', 'MultiStageDistillation', - 'NeutralizationTank' -) diff --git a/biorefineries/microalgae/utils.py b/biorefineries/microalgae/utils.py index cd177ad0..6fb4e636 100644 --- a/biorefineries/microalgae/utils.py +++ b/biorefineries/microalgae/utils.py @@ -22,10 +22,12 @@ import numpy as np import pandas as pd +import biosteam as bst import thermosteam as tmo from ._chemicals import chems _kg_per_ton = 907.18474 +_lb_per_kg = 2.20462 # Baseline feedstock flow rate def get_feedstock_flow(dry_composition, moisture_content, dry_flow): dry_array = chems.kwarray(dry_composition) @@ -34,11 +36,11 @@ def get_feedstock_flow(dry_composition, moisture_content, dry_flow): feedstock_flow = wet_flow * (dry_array*(1-moisture_content)+moisture_array) return feedstock_flow -# !!! This is the dry composition of corn stover; update to the composition you need +# !!! This is the dry composition of microalgae; update to the composition you need dry_composition = dict( Carbohydrate=0.234, Lipid=0.059, Protein=0.5369, Ash=0.0512, Extract=0.1189) -moisture_content = 0.05 #!!! This is the moisture content of preprocessed corn stover; update to the moisture content you need +moisture_content = 0.05 #!!! This is the moisture content of preprocessed microalgae; update to the moisture content you need dry_feedstock_flow = 2205. * _kg_per_ton / 24. # !!! Changing this will alter the total flow without altering the composition baseline_feedflow = get_feedstock_flow(dry_composition, moisture_content, @@ -50,7 +52,7 @@ def get_feedstock_flow(dry_composition, moisture_content, dry_flow): 'AlphaAmylase': 6.16, # $/kg (NREL 2018) 'Yeast': 2, # $/kg Alibaba 'Ethanol': 1.78, # $/kg https://catcost.chemcatbio.org/materials-library - 'Octanol': 1.00, # $/lb https://web.archive.org/web/20161125084558/http://www.icis.com:80/chemicals/channel-info-chemicals-a-z/ + 'OleylAlcohol': 1.00, # $/lb https://web.archive.org/web/20161125084558/http://www.icis.com:80/chemicals/channel-info-chemicals-a-z/ 'AceticAcid': 1.31, # $/kg https://catcost.chemcatbio.org/materials-library 'ButyricAcid': 1.72, # $/kg https://www.imarcgroup.com/butyric-acid-pricing-report? 'CaproicAcid': 2.89, # $/kg Increasing the economic value of lignocellulosic stillage through medium-chain fatty acid production @@ -59,9 +61,18 @@ def get_feedstock_flow(dry_composition, moisture_content, dry_flow): 'HeptanoicAcid': 3.17, # $/kg https://www.expertmarketresearch.com/prefeasibility-reports/heptanoic-acid-manufacturing-plant-project-report 'CaprylicAcid': 5.07, # $/kg Increasing the economic value of lignocellulosic stillage through medium-chain fatty acid production 'SulfuricAcid': 0.11, # $/kg https://catcost.chemcatbio.org/materials-library - 'Lime': 0.12, # https://catcost.chemcatbio.org/materials-library 'AmmoniumHydroxide': 0.204, # BDO, succinic, HP program 'NaOH': 1.01, #https://catcost.chemcatbio.org/materials-library + # Baseline from Davis et al., 2018, lower bound is 2015-2019 average of + # hydrate lime in $/ton at plant from Mineral Commodity Summaries 2020. + # 2015: 146.40 * (1.114/1.100) / 907.18474 = 0.163 + # 2016: 145.50 / 907.18474 = 0.160 + # 2017: 147.10 * (1.114/1.134) / 907.18474 = 0.159 + # 2018: 151.50 * (1.114/1.157) / 907.18474 = 0.161 + # 2019: 151.00 * (1.114/1.185) / 907.18474 = 0.156 + # (0.163+0.160+0.159+0.161+0.156) / 5 = 0.160 + # Upper bound is +10% from baseline = 0.1189 * _lb_per_kg * 1.1 = 0.288 + 'Lime': 0.1189 * _lb_per_kg } # Chemical Engineering Plant Cost Index from Chemical Engineering Magzine @@ -84,7 +95,23 @@ def get_feedstock_flow(dry_composition, moisture_content, dry_flow): 2022: 816.0, 2023: 797.9} +_lps = bst.HeatUtility.get_heating_agent('low_pressure_steam') +_mps = bst.HeatUtility.get_heating_agent('medium_pressure_steam') +_hps = bst.HeatUtility.get_heating_agent('high_pressure_steam') +_mps.T = 233 + 273.15 +_hps.T = 266 + 273.15 +_cooling = bst.HeatUtility.get_cooling_agent('cooling_water') +_chilled = bst.HeatUtility.get_cooling_agent('chilled_water') +_cooling.regeneration_price = 0 +_cooling.T = 28 + 273.15 +_cooling.T_limit = _cooling.T + 9 + +# Side steam in CHP not a heat utility, thus will cause problem in TEA utility +# cost calculation if price not set to 0 here, costs for regeneration of heating +# and cooling utilities will be considered as CAPEX and OPEX of CHP and CT, respectively +for i in (_lps, _mps, _hps, _cooling, _chilled): + i.heat_transfer_price = i.regeneration_price = 0