Source code for biosteam.units.solids_separation

# -*- 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 the separation of solids 
(e.g. centrifugation, expression, filtration).

.. contents:: :local:
    
.. autoclass:: biosteam.units.solids_separation.SolidsSeparator
.. autoclass:: biosteam.units.solids_separation.SolidsCentrifuge
.. autoclass:: biosteam.units.solids_separation.RotaryVacuumFilter 
.. autoclass:: biosteam.units.solids_separation.PressureFilter
.. autoclass:: biosteam.units.solids_separation.ScrewPress

References
----------
.. [1] Seider, Warren D., et al. (2017). "Cost Accounting and Capital Cost 
    Estimation". In Product and Process Design Principles: Synthesis, Analysis,
    and Evaluation (pp. 481-485). New York: Wiley.
.. [2] 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

"""
from .decorators import cost
from .splitting import Splitter
from .design_tools import compute_vacuum_system_power_and_cost
from warnings import warn
from ..exceptions import lb_warning, InfeasibleRegion
from .decorators import cost
from biosteam.utils import remove_undefined_chemicals, default_chemical_dict
import numpy as np
import biosteam as bst
from thermosteam import separations
from math import exp, log, ceil

__all__ = ('SolidsSeparator', 'RotaryVacuumFilter', 'CrushingMill', 
           'PressureFilter', 'SolidsCentrifuge', 'RVF',
           'ScrewPress',)

[docs] class SolidsSeparator(Splitter): """ Create SolidsSeparator object. Parameters ---------- ins : Inlet fluids with solids. outs : * [0] Retentate. * [1] Permeate. split : array_like Component splits to 0th output stream moisture_content : float Fraction of water in solids """ _N_ins = 1 _ins_size_is_fixed = False def _init(self, split, order=None, moisture_content=None, moisture_ID=None, strict_moisture_content=None ): Splitter._init(self, order=order, split=split) #: Moisture content of retentate self.moisture_content = moisture_content self.strict_moisture_content = strict_moisture_content if moisture_content is not None: if moisture_ID is None: moisture_ID = '7732-18-5' self.moisture_ID = moisture_ID def _run(self): if self.moisture_content is None: separations.mix_and_split( self.ins, *self.outs, self.split, ) else: moisture_ID = self.moisture_ID self.isplit[moisture_ID] = 0. separations.mix_and_split_with_moisture_content( self.ins, *self.outs, self.split, self.moisture_content, self.moisture_ID, self.strict_moisture_content, )
# if self._recycle_system and self._system.algorithm == 'Phenomena oriented': # ID = self.moisture_ID # if not ID: return # top, bottom = self.outs # top_mol = top.imol[ID] # self.isplit[ID] = top_mol / (top_mol + bottom.imol[ID]) # def _update_nonlinearities(self): # outs = self.outs # data = [i.get_data() for i in outs] # self._run() # for i, j in zip(outs, data): i.set_data(j)
[docs] class SolidsCentrifuge(SolidsSeparator): """ Create a solids centrifuge that separates out solids according to user defined split. Capital cost is based on [1]_. Parameters ---------- ins : Inlet fluid with solids. outs : * [0] Solids-rich stream. * [1] Liquid-rich stream. split : array_like or dict[str, float] Component splits. order=None : Iterable[str] Species order of split. Defaults to Stream.chemicals.IDs. solids : tuple[str] IDs of solids. moisture_content : float Fraction of water in stream. centrifuge_type : str Type of the centrifuge, either 'reciprocating_pusher' (1-20 ton/hr solids) or 'scroll_solid_bowl' (2-40 ton/hr solids). """ _units = {'Solids loading': 'ton/hr', 'Flow rate': 'm3/hr'} solids_loading_range = { 'reciprocating_pusher': (1, 20), 'scroll_solid_bowl': (2, 40) } kWhr_per_m3 = 1.40 def _init(self, split, order=None, solids=None, moisture_content=0.40, centrifuge_type='scroll_solid_bowl', moisture_ID=None, strict_moisture_content=None): SolidsSeparator._init( self, moisture_content=moisture_content, split=split, order=order, moisture_ID=moisture_ID, strict_moisture_content=strict_moisture_content ) if solids is None: solids = [i.ID for i in self.chemicals if i.locked_state == 's'] self.solids = solids self.centrifuge_type = centrifuge_type @property def solids(self): return self._solids @solids.setter def solids(self, solids): self._solids = tuple(solids) @property def centrifuge_type(self): return self._centrifuge_type @centrifuge_type.setter def centrifuge_type(self, i): if not i in ('reciprocating_pusher', 'scroll_solid_bowl'): raise ValueError('`centrifuge_type` can only be "reciprocating_pusher" or ' f'"scroll_solid_bowl", not {i}.') self._centrifuge_type = i def _design(self): solids, centrifuge_type = self._solids, self.centrifuge_type ts = sum([s.imass[solids].sum() for s in self.ins if not s.isempty()]) # Total solids ts *= 0.0011023 # To short tons (2000 lbs/hr) self.design_results['Solids loading'] = ts lb, ub = self.solids_loading_range[centrifuge_type] if ts < lb: lb_warning(self, 'Solids loading', ts, 'ton/hr', lb) self.design_results['Number of centrifuges'] = ceil(ts/ub) cost = 68040*(ts**0.5) if centrifuge_type else 170100*(ts**0.3) cost *= bst.CE / 567 self.baseline_purchase_costs['Centrifuges'] = cost self.F_BM['Centrifuges'] = 2.03 self.design_results['Flow rate'] = F_vol_in = self.F_vol_in self.power_utility(F_vol_in * self.kWhr_per_m3)
[docs] class RotaryVacuumFilter(SolidsSeparator): """ Create a RotaryVacuumFilter object. Parameters ---------- ins : * [0] Feed * [1] Wash water outs : * [0] Retentate * [1] Permeate split : array_like or dict[str, float] Component splits. moisture_content : float Fraction of water in retentate. """ auxiliary_unit_names = ('vacuum_system',) _F_BM_default = {'Vessels': 2.32, 'Vacuum system': 1.0} #: Revolutions per second rps = 20/3600 #: Radius of the vessel (m) radius = 1 #: Suction pressure (Pa) P_suction = 1500. #: For crystals (lb/day-ft^2) filter_rate = 6000 _kwargs = {'moisture_content': 0.80} # fraction _bounds = {'Individual area': (10, 800)} _units = {'Area': 'ft^2', 'Individual area': 'ft^2'} def _design(self): flow = sum([stream.F_mass for stream in self.outs]) self.design_results['Area'] = self._calc_Area(flow, self.filter_rate) def _cost(self): Design = self.design_results Area = Design['Area'] ub = self._bounds['Individual area'][1] N_vessels = np.ceil(Area/ub) iArea = Area/N_vessels # individual vessel self.parallel['self'] = N_vessels self.parallel['vacuum_system'] = 1 Design['Individual area'] = iArea self._load_vacuum_system(Area, N_vessels) logArea = np.log(iArea) Cost = np.exp(11.796 - 0.1905 * logArea + 0.0554 * logArea**2) self.baseline_purchase_costs['Vessels'] = Cost * bst.CE/567 def _load_vacuum_system(self, area, N_vessels): s_cake, s_vacuumed = self.outs radius = self.radius Area = self.design_results['Individual area'] N = self.parallel['self'] vacummed_air = s_vacuumed.F_vol # Flow rate sucked-in displaces air air_density = 1.2754 # kg /m3 self.vacuum_system = bst.VacuumSystem( F_mass=vacummed_air * air_density, F_vol=vacummed_air, P_suction=self.P_suction, vessel_volume=N * radius * Area * 0.0929 / 2., # m3 ) @staticmethod def _calc_Area(flow, filter_rate): """Return area in ft^2 given flow in kg/hr and filter rate in lb/day-ft^2.""" return flow * 52.91 / filter_rate
RVF = RotaryVacuumFilter @cost('Flow rate', units='kg/hr', cost=1.5e6, CE=541.7, n=0.6, S=335e3, kW=2010, BM=2.3) class CrushingMill(SolidsSeparator): """ Create crushing mill unit operation for the separation of sugarcane juice from the bagasse. Parameters ---------- ins : * [0] Shredded sugar cane * [1] Recycle water outs : * [0] Bagasse * [1] Juice split : array_like or dict[str, float] Splits of chemicals to the bagasse. moisture_content : float Fraction of water in Bagasse. """ _hp2kW = 0.7457
[docs] @cost('Retentate flow rate', 'Flitrate tank agitator', cost=26e3, CE=551, kW=7.5*_hp2kW, S=31815, n=0.5, BM=1.5) @cost('Retentate flow rate', 'Discharge pump', cost=13040, CE=551, S=31815, n=0.8, BM=2.3) @cost('Retentate flow rate', 'Filtrate tank', cost=103e3, S=31815, CE=551, BM=2.0, n=0.7) @cost('Retentate flow rate', 'Feed pump', kW=74.57, cost= 18173, S=31815, CE=551, n=0.8, BM=2.3) @cost('Retentate flow rate', 'Stillage tank 531', cost=174800, CE=551, S=31815, n=0.7, BM=2.0) @cost('Retentate flow rate', 'Mafifold flush pump', kW=74.57, cost=17057, CE=551, S=31815, n=0.8, BM=2.3) @cost('Retentate flow rate', 'Recycled water tank', cost=1520, CE=551, S=31815, n=0.7, BM=3.0) @cost('Retentate flow rate', 'Wet cake screw', kW=15*_hp2kW, cost=2e4, CE=521.9, S=28630, n=0.8, BM=1.7) @cost('Retentate flow rate', 'Wet cake conveyor', kW=10*_hp2kW, cost=7e4, CE=521.9, S=28630, n=0.8, BM=1.7) @cost('Retentate flow rate', 'Pressure filter', cost=3294700, CE=551, S=31815, n=0.8, BM=1.7) @cost('Retentate flow rate', 'Pressing air compressor receiver tank', cost=8e3, CE=551, S=31815, n=0.7, BM=3.1) @cost('Retentate flow rate', 'Cloth wash pump', kW=150*_hp2kW, cost=29154, CE=551, S=31815, n=0.8, BM=2.3) @cost('Retentate flow rate', 'Dry air compressor receiver tank', cost=17e3, CE=551, S=31815, n=0.7, BM=3.1) @cost('Retentate flow rate', 'Pressing air pressure filter', cost=75200, CE=521.9, S=31815, n=0.6, kW=112, BM=1.6) @cost('Retentate flow rate', 'Dry air pressure filter (2)', cost=405000, CE=521.9, S=31815, n=0.6, kW=1044, BM=1.6) class PressureFilter(SolidsSeparator): """ Create a pressure filter for the separation of structural carbohydrates, lignin, cell mass, and other solids. Capital costs are based on [2]_. Parameters ---------- ins : Contains structural carbohydrates, lignin, cell mass, and other solids. outs : * [0] Retentate (i.e. solids) * [1] Filtrate split : array_like or dict[str, float] Splits of chemicals to the retentate. Defaults to values used in the 2011 NREL report on cellulosic ethanol as given in [2]_. moisture_content : float, optional Moisture content of retentate. Defaults to 0.35 """ _units = {'Retentate flow rate': 'kg/hr'} def _init(self, moisture_content=0.35, split=None): if split is None: chemicals = self.chemicals split = dict( Furfural=0.03571, Glycerol=0.03714, LacticAcid=0.03727, SuccinicAcid=0.03714, HNO3=0.03716, Denaturant=0.03714, DAP=0.03716, AmmoniumAcetate=0.03727, AmmoniumSulfate=0.03716, NaNO3=0.03716, Oil=0.03714, HMF=0.03571, Glucose=0.03647, Xylose=0.03766, Sucrose=0.0359, Mannose=0.0359, Galactose=0.0359, Arabinose=0.0359, Extract=0.03727, Tar=0.9799, CaO=0.9799, Ash=0.9799, NaOH=0.03716, Lignin=0.98, SolubleLignin=0.03727, GlucoseOligomer=0.03722, GalactoseOligomer=0.03722, MannoseOligomer=0.03722, XyloseOligomer=0.03722, ArabinoseOligomer=0.03722, Z_mobilis=0.9799, T_reesei=0.9799, Protein=0.98, Enzyme=0.98, Glucan=0.9801, Xylan=0.9811, Xylitol=0.03714, Cellobiose=0.0359, DenaturedEnzyme=0.98, Arabinan=0.9792, Mannan=0.9792, Galactan=0.9792, WWTsludge=0.9799, Cellulase=0.03727 ) remove_undefined_chemicals(split, chemicals) default_chemical_dict(split, chemicals, 0.03714, 0.03714, 0.9811) bst.SolidsSeparator._init(self, moisture_content=moisture_content, split=split) def _design(self): self.design_results['Retentate flow rate'] = self.outs[0].F_mass
PressureFilter._stacklevel += 1 #: TODO: Check BM assumption. Use 1.39 for crushing unit operations for now.
[docs] @cost('Flow rate', units='lb/hr', CE=567, lb=150, ub=12000, BM=1.39, f=lambda S: exp((11.0991 - 0.3580*log(S) + 0.05853*log(S)**2))) class ScrewPress(SolidsSeparator): """ Create screw press unit operation for the expression of liquids from solids. Capital cost is based on [1]_. Parameters ---------- ins : * [0] Solids + liquid outs : * [1] Solids (retentate) * [0] Liquids (permeate) split : array_like or dict[str, float] Component splits. moisture_content : float Fraction of water in solids. """ kWh_per_bmt = 37.2 # From Perry's Handbook, 18-126 # Energy consumption may be drastically different depending on the application # - 5 to 12 bdmt (tonne dry biomass) https://www.andritz.com/products-en/group/pulp-and-paper/service-solutions/screw-press-service/screw-press-upgrade-case-study-1-less def _cost(self): self._decorated_cost() biomass = self.ins[0] bmt = biomass.F_mass * 0.001 self.add_power_utility(bmt * self.kWh_per_bmt)