Source code for biosteam.units.drying

# -*- coding: utf-8 -*-
# BioSTEAM: The Biorefinery Simulation and Techno-Economic Analysis Modules
# Copyright (C) 2020-2023, Yoel Cortes-Pena <yoelcortes@gmail.com>
# 
# This module is under the UIUC open-source license. See 
# github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/LICENSE.txt
# for license details.
"""
This module contains unit operations for drying solids.

.. contents:: :local:
    
.. autoclass:: biosteam.units.drying.DrumDryer

References
----------
.. [1] Kwiatkowski, J. R.; McAloon, A. J.; Taylor, F.; Johnston, D. B. 
    Modeling the Process and Costs of Fuel Ethanol Production by the Corn 
    Dry-Grind Process. Industrial Crops and Products 2006, 23 (3), 288–296.
    https://doi.org/10.1016/j.indcrop.2005.08.004.

"""
import flexsolve as flx
import numpy as np
from thermosteam import separations as sep
from .design_tools import (
    CEPCI_by_year,
    cylinder_diameter_from_volume, 
    cylinder_area,
)
from .decorators import cost
from .._unit import Unit

__all__ = ('DrumDryer', 'ThermalOxidizer')

# TODO: The drum dryer is carbon steel. Add material factors later
[docs] @cost('Peripheral drum area', CE=CEPCI_by_year[2007], ub=7854.0, BM=2.06, S=1235.35, units='m2', n=0.6, cost=0.52 * 2268000., kW=938.866) class DrumDryer(Unit): """ Create a drum dryer that dries solids by passing hot air (heated by burning natural gas). Parameters ---------- ins : * [0] Wet solids. * [1] Dry gas. * [2] Natural gas. outs : * [0] Dried solids * [1] Hot gas * [2] Emissions split : dict[str, float] Component splits to hot gas (stream [1]). R : float, optional Flow of hot gas over evaporation. Defaults to 1.4 wt gas / wt evap. H : float, optional Specific evaporation rate [kg/hr/m3]. Defaults to 20. length_to_diameter : float, optional Note that the drum is horizontal. Defaults to 25. T : float, optional Operating temperature [K]. Defaults to 343.15. moisture_content : float Moisture content of solids [wt / wt]. Defaults to 0.10. Notes ----- The flow rate for gas in the inlet is varied to meet the `R` specification (i.e. flow of hot gas over flow rate evaporated). The flow rate of inlet natural gas is also altered to meet the heat demand. The default parameter values are based on heuristics for drying dried distillers grains with solubles (DDGS). Examples -------- >>> import biosteam as bst >>> from biorefineries import corn as c >>> bst.settings.set_thermo(c.create_chemicals()) >>> feed = bst.Stream('feed', phase='l', T=352.33, P=101325, ... Water=0.6749, Ethanol=5.041e-06, Ash=0.01978, Yeast=0.008452, ... CaO=0.0001446, TriOlein=0.02702, H2SO4=0.001205, Fiber=0.1508, ... SolubleProtein=0.04805, InsolubleProtein=0.06967, ... total_flow=32720, units='kg/hr', ... ) >>> dryer = bst.DrumDryer('D610', ... (feed, 'dryer_air', 'natural_gas'), ... ('dryed_solids', 'hot_air', 'emissions'), ... moisture_content=0.10, split=dict(Ethanol=1.0) ... ) >>> dryer.simulate() >>> dryer.show('cwt100') DrumDryer: D610 ins... [0] feed phase: 'l', T: 352.33 K, P: 101325 Pa composition (%): Water 67.5 Ethanol 0.000504 Ash 1.98 Yeast 0.845 CaO 0.0145 TriOlein 2.7 H2SO4 0.12 Fiber 15.1 SolubleProtein 4.8 InsolubleProtein 6.97 ---------------- 3.27e+04 kg/hr [1] dryer_air phase: 'g', T: 298.15 K, P: 1.01325e+06 Pa composition (%): O2 29.1 N2 70.9 -- 3.22e+04 kg/hr [2] natural_gas phase: 'g', T: 298.15 K, P: 101325 Pa composition (%): CH4 100 --- 1.11e+03 kg/hr outs... [0] dryed_solids phase: 'l', T: 343.15 K, P: 101325 Pa composition (%): Water 10 Ash 5.48 Yeast 2.34 CaO 0.04 TriOlein 7.48 H2SO4 0.334 Fiber 41.7 SolubleProtein 13.3 InsolubleProtein 19.3 ---------------- 1.18e+04 kg/hr [1] hot_air phase: 'g', T: 343.15 K, P: 1.01325e+06 Pa composition (%): Water 39.4 Ethanol 0.000311 O2 17.6 N2 43 ------- 5.31e+04 kg/hr [2] emissions phase: 'g', T: 373.15 K, P: 101325 Pa composition (%): Water 45 CO2 55 ----- 5.56e+03 kg/hr >>> dryer.results() Drum dryer Units D610 Electricity Power kW 845 Cost USD/hr 66 Natural gas (inlet) Flow kg/hr 1.11e+03 Cost USD/hr 243 Design Evaporation kg/hr 2.09e+04 Volume 1.05e+03 Diameter m 3.76 Length 94 Peripheral drum area m2 1.11e+03 Purchase cost Drum dryer USD 1.2e+06 Total purchase cost USD 1.2e+06 Utility cost USD/hr 309 """ # auxiliary_unit_names = ('heat_exchanger',) _units = {'Evaporation': 'kg/hr', 'Peripheral drum area': 'm2', 'Diameter': 'm'} _N_ins = 3 _N_outs = 3 @property def isplit(self): """[ChemicalIndexer] Componentwise split of feed to 0th outlet stream.""" return self._isplit @property def split(self): """[Array] Componentwise split of feed to 0th outlet stream.""" return self._isplit.data @property def natural_gas(self): """[Stream] Natural gas to satisfy steam and electricity requirements.""" return self.ins[2] def _init(self, split, R=1.4, H=20., length_to_diameter=25, T=343.15, P=10*101325, moisture_content=0.15, utility_agent='Natural gas', gas_composition=None, moisture_ID=None): self._isplit = self.chemicals.isplit(split) self.define_utility('Natural gas', self.natural_gas) self.P = P self.T = T self.R = R self.H = H self.gas_composition = gas_composition self.length_to_diameter = length_to_diameter self.moisture_content = moisture_content self.utility_agent = utility_agent self.moisture_ID = moisture_ID @property def utility_agent(self): return self._utility_agent @utility_agent.setter def utility_agent(self, utility_agent): if utility_agent not in ('Natural gas', 'Steam'): raise ValueError(f"utility agent must be either 'Steam' or 'Natural gas'; not '{utility_agent}'") self._utility_agent = utility_agent def _run(self): wet_solids, air, natural_gas = self.ins dry_solids, hot_air, emissions = self.outs wet_solids.split_to(hot_air, dry_solids, self.split) sep.adjust_moisture_content(dry_solids, hot_air, self.moisture_content, self.moisture_ID) hot_air.P = air.P = self.P emissions.phase = air.phase = natural_gas.phase = hot_air.phase = 'g' design_results = self.design_results design_results['Evaporation'] = evaporation = hot_air.F_mass gas_composition = self.gas_composition if gas_composition is None: gas_composition = [('N2', 0.78), ('O2', 0.32)] total_gas_flow = self.R * evaporation for ID, x in gas_composition: air.imass[ID] = x * total_gas_flow hot_air.mol += air.mol dry_solids.T = hot_air.T = self.T emissions.T = self.T + 30. natural_gas.empty() emissions.empty() if self.utility_agent == 'Natural gas': LHV = self.chemicals.CH4.LHV def f(CH4): CO2 = CH4 H2O = 2. * CH4 natural_gas.imol['CH4'] = CH4 emissions.imol['CO2', 'H2O'] = [CO2, H2O] duty = (dry_solids.H + hot_air.H + emissions.H) - (wet_solids.H + air.H + natural_gas.H) CH4 = duty / LHV return CH4 flx.wegstein(f, 0., 1e-3) def _design(self): length_to_diameter = self.length_to_diameter design_results = self.design_results design_results['Volume'] = volume = design_results['Evaporation'] / self.H design_results['Diameter'] = diameter = cylinder_diameter_from_volume(volume, length_to_diameter) design_results['Length'] = length = diameter * length_to_diameter design_results['Peripheral drum area'] = cylinder_area(diameter, length) if self.utility_agent == 'Steam': self.add_heat_utility(self.H_out - self.H_in, self.T)
class ThermalOxidizer(Unit): """ Create a ThermalOxidizer that burns any remaining combustibles. Parameters ---------- ins : [0] Feed gas [1] Air [2] Natural gas outs : Emissions. tau : float, optional Residence time [hr]. Defaults to 0.00014 (0.5 seconds). duty_per_kg : float, optional Duty per kg of feed. Defaults to 105858 kJ / kg. V_wf : float, optional Fraction of working volume. Defaults to 0.95. Notes ----- Adiabatic operation is assumed. Simulation and cost is based on [1]_. """ _N_ins = 3 _N_outs = 1 max_volume = 20. # m3 _F_BM_default = {'Vessels': 2.06} # Assume same as dryer @property def natural_gas(self): """[Stream] Natural gas to satisfy steam and electricity requirements.""" return self.ins[2] def _init(self, tau=0.00014, duty_per_kg=61., V_wf=0.95): self.define_utility('Natural gas', self.natural_gas) self.tau = tau self.duty_per_kg = duty_per_kg self.V_wf = V_wf def _run(self): feed, air, ng = self.ins ng.imol['CH4'] = self.duty_per_kg * feed.F_mass / self.chemicals.CH4.LHV ng.phase = 'g' emissions, = self.outs ng_burned = ng.copy() combustion_rxns = self.chemicals.get_combustion_reactions() # Enough oxygen must be present in air to burn natural gas combustion_rxns.force_reaction(ng_burned) O2 = max(-ng_burned.imol['O2'], 0.) air.imol['N2', 'O2'] = [0.79/0.21 * O2, O2] # Enough oxygen must be present in air to burn feed as well emissions.mix_from(self.ins) dummy_emissions = emissions.copy() combustion_rxns.force_reaction(dummy_emissions) O2 = max(-dummy_emissions.imol['O2'], 0.) # Missing oxygen air.imol['N2', 'O2'] += [0.79/0.21 * O2, O2] emissions.mix_from(self.ins) # Account for temperature raise combustion_rxns.adiabatic_reaction(emissions) def _design(self): design_results = self.design_results volume = self.tau * self.outs[0].F_vol / self.V_wf V_max = self.max_volume design_results['Number of vessels'] = N = np.ceil(volume / V_max) design_results['Vessel volume'] = volume / N design_results['Total volume'] = volume def _cost(self): design_results = self.design_results N = design_results['Number of vessels'] vessel_volume = design_results['Vessel volume'] C = self.baseline_purchase_costs C['Vessels'] = N * 918300. * (vessel_volume / 13.18)**0.6