Source code for biosteam.wastewater.high_rate.membrane_bioreactor

# -*- coding: utf-8 -*-
# Bioindustrial-Park: BioSTEAM's Premier Biorefinery Models and Results
# Copyright (C) 2021-2024, Yalin Li <mailto.yalin.li@gmail.com>
#
# This module is under the UIUC open-source license. See
# github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/LICENSE.txt
# for license details.

import math
import biosteam as bst
from . import (
    default_insolubles,
    InternalCirculationRx, WWTpump,
    compute_stream_COD, format_str, get_BD_dct, get_split_dct, cost_pump,
)
DesignError = bst.exceptions.DesignError

__all__ = ('AnMBR',)

_ft_to_m = 0.3048 # auom('ft').conversion_factor('m')
_ft2_to_m2 = 0.09290 # auom('ft2').conversion_factor('m2')
_ft3_to_m3 = 0.02832 # auom('ft3').conversion_factor('m3')
_ft3_to_gal = 7.4805 # auom('ft3').conversion_factor('gallon')
_m3_to_gal = 264.1721 # auom('m3').conversion_factor('gal')
_cmh_to_mgd = _m3_to_gal * 24 / 1e6 # cubic meter per hour to million gallon per day
_lb_to_kg = 0.4536 # auom('lb').conversion_factor('kg')


# %%

[docs] class AnMBR(bst.Unit): """ Anaerobic membrane bioreactor (AnMBR) for wastewater treatment as in Shoener et al. [6]_ Some assumptions adopted from Humbird et al. [2]_ In addition to the anaerobic treatment, an optional second stage can be added, which can be aerobic filter or granular activated carbon (GAC). Parameters ---------- ins : * [0] influent * [1] recycle (optional) * [2] NaOCl * [3] citric acid * [4] bisulfite * [5] air (optional) outs : * [0] biogas * [1] effluent * [2] waste sludge * [3] air (optional) reactor_type : str Can either be "CSTR" for continuous stirred tank reactor or "AF" for anaerobic filter. membrane_configuration : str Can either be "cross-flow" or "submerged". membrane_type : str Can be "hollow fiber" ("submerged" configuration only), "flat sheet" (either "cross-flow" or "submerged" configuration), or "multi-tube" ("cross-flow" configuration only). membrane_material : str Can be any of the plastics ("PES", "PVDF", "PET", "PTFE") for any of the membrane types ("hollow fiber", "flat sheet", "multi-tube"), or "sintered steel" for "flat sheet", or "ceramic" for "multi-tube". membrane_unit_cost : float Cost of membrane, [$/ft2] include_aerobic_filter : bool Whether to include an aerobic filtration process in this AnMBR, can only be True in "AF" (not "CSTR") reactor. add_GAC : bool If to add granular activated carbon to enhance biomass retention, can only be True for the "submerged" configuration. include_degassing_membrane : bool If to include a degassing membrane to enhance methane (generated through the digestion reaction) recovery. Y_biogas : float Biogas yield, [kg biogas/kg consumed COD]. Y_biomass : float Biomass yield, [kg biomass/kg consumed COD]. biodegradability : float or dict Biodegradability of chemicals, when shown as a float, all biodegradable chemicals are assume to have the same degradability. split : dict Component-wise split to the treated water. E.g., {'Water':1, 'WWTsludge':0} indicates all of the water goes to the treated water and all of the WWTsludge goes to the wasted sludge. Default splits (based on the membrane bioreactor in [2]_) will be used if not provided. sludge_conc : float Concentration of biomass in the waste sludge stream, in g/L. Note that the solids content of the effluent should be smaller than the solids content of the waste sludge stream. T : float Temperature of the reactor. Will not control temperature if provided as None. include_pump_building_cost : bool Whether to include the construction cost of pump building. include_excavation_cost : bool Whether to include the construction cost of excavation. kwargs : dict Other keyword arguments (e.g., J_max, SGD). """ _N_ins = 6 # influent, recycle (optional), naocl, citric acid, bisulfite, air (optional) _N_outs = 4 # biogas, effluent, waste sludge, air (optional) # Equipment-related parameters F_BM_pumps = 1.18 * (1+0.007/100) # 0.007 is for miscellaneous costs _F_BM_default = { 'Membrane': 1 + 0.15, # assume 15% for replacement labor 'Pumps': F_BM_pumps, 'Pump building': F_BM_pumps, 'Pump excavation': F_BM_pumps, 'Blowers': 2 * 1.11, 'Blower building': 1.11, # Set bare module factor to 1 if not otherwise provided 'Reactor excavation': 1, 'Wall concrete': 1, 'Slab concrete': 1, 'GAC': 1, 'Air pipes': 1, 'Degassing membrane': 1, } _default_equipment_lifetime = { 'Membrane': 10, 'Pumps': 15, 'Blowers': 15, } _N_train_min = 2 _cas_per_tank_spare = 2 _mod_surface_area = { 'hollow fiber': 370, 'flat sheet': 1.45/_ft2_to_m2, 'multi-tube': 32/_ft2_to_m2 } _mod_per_cas = None _mod_per_cas_range = { 'hollow fiber': (30, 48), # min, max 'flat sheet': (150, 200), 'multi-tube': (44, 48) } _cas_per_tank = None _cas_per_tank_range = (16, 22) _N_blower = 0 _W_tank = 21 _D_tank = 12 _W_dist = 4.5 _W_eff = 4.5 _L_well = 8 _W_well = 8 _D_well = 12 _excav_slope = 1.5 _constr_access = 3 # Operation-related parameters _HRT = 10 _J_max = 12 _TMP_dct = { 'cross-flow': 2.5, 'submerged': 2.5, } _TMP_aerobic = None _recir_ratio = 2.25 # from the 0.5-4 uniform range in ref [1] _v_cross_flow = 1.2 # from the 0.4-2 uniform range in ref [1] _v_GAC = 8 _SGD = 0.625 # from the 0.05-1.2 uniform range in ref [1] _AFF = 3.33 _sludge_conc = 10.5 _refresh_rxns = InternalCirculationRx._refresh_rxns # Other equipment auxiliary_unit_names = ('heat_exchanger',) _pumps = ('perm', 'retent', 'recir', 'sludge', 'naocl', 'citric', 'bisulfite', 'AF', 'AeF') def _init(self, reactor_type='CSTR', membrane_configuration='cross-flow', membrane_type='multi-tube', membrane_material='ceramic', membrane_unit_cost=8, include_aerobic_filter=False, add_GAC=False, include_degassing_membrane=True, Y_biogas=0.86, Y_biomass=0.05, # from the 0.02-0.08 uniform range in ref [1] biodegradability={}, split={}, sludge_conc=10.5, T=35+273.15, include_pump_building_cost=False, include_excavation_cost=False, **kwargs ): self.reactor_type = reactor_type self.include_aerobic_filter = include_aerobic_filter self.membrane_configuration = membrane_configuration self.membrane_type = membrane_type self.membrane_material = membrane_material self.membrane_unit_cost = membrane_unit_cost self.add_GAC = add_GAC self.include_degassing_membrane = include_degassing_membrane self.Y_biogas = Y_biogas self.Y_biomass = Y_biomass self.biodegradability = \ biodegradability if biodegradability else get_BD_dct(self.chemicals) self.split = split if split else get_split_dct(self.chemicals) self.sludge_conc = sludge_conc self.T = T self.include_pump_building_cost = include_pump_building_cost self.include_excavation_cost = include_excavation_cost # Initialize the attributes ID = self.ID self._inf = bst.Stream(f'{ID}_inf') self._mixed = bst.Stream(f'{ID}_mixed') self._retent = bst.Stream(f'{ID}_retent') self._recir = bst.Stream(f'{ID}_recir') hx_in = bst.Stream(f'{ID}_hx_in') hx_out = bst.Stream(f'{ID}_hx_out') # Add '.' in ID for auxiliary units self.heat_exchanger = bst.HXutility(ID=f'.{ID}_hx', ins=hx_in, outs=hx_out) self._refresh_rxns() for k, v in kwargs.items(): setattr(self, k, v) self._check_design() def _check_design(self): reactor_type = self.reactor_type m_config = self.membrane_configuration m_type = self.membrane_type m_material = self.membrane_material if reactor_type == 'CSTR': if self.include_aerobic_filter: raise DesignError('Aerobic filtration cannot be used in CSTR.') if m_config == 'submerged': if not m_type in ('hollow fiber', 'flat sheet'): raise DesignError('Only "hollow fiber" or "flat sheet" is allowed ' 'for "submerged" membrane, not {m_type}.') else: # cross-flow if not m_type in ('flat sheet', 'multi-tube'): raise DesignError('Only "flat sheet" or "multi-tube" is allowed ' 'for "cross-flow" membrane, not {m_type}.') if self.add_GAC: raise DesignError('No GAC should be added ' '(i.e., `add_GAC` can only be False' 'for "cross-flow" membrane.') plastics = ('PES', 'PVDF', 'PET', 'PTFE') if m_type == 'hollow fiber': if not m_material in plastics: raise DesignError(f'Only plastic materials {plastics} ' 'allowed for "hollow fiber" membrane', f'not "{m_material}".') elif m_type == 'flat sheet': if not m_material in (*plastics, 'sintered steel'): raise DesignError(f'Only plastic materials {plastics} and "sintered steel"' 'allowed for "flat sheet" membrane', f'not "{m_material}".') else: # multi-tube if not m_material in (*plastics, 'ceramic'): raise DesignError(f'Only plastic materials {plastics} and "ceramic"' 'allowed for "multi-tube" membrane', f'not "{m_material}".') @staticmethod def _degassing(original_stream, receiving_stream): InternalCirculationRx._degassing(original_stream, receiving_stream) @staticmethod def compute_COD(stream): r""" Compute the chemical oxygen demand (COD) of a given stream in kg-O2/m3 by summing the COD of each chemical in the stream using: .. math:: COD [\frac{kg}{m^3}] = mol_{chemical} [\frac{kmol}{m^3}] * \frac{g O_2}{mol chemical} """ return compute_stream_COD(stream) # ========================================================================= # _run # ========================================================================= def _run(self): raw, recycled, naocl, citric, bisulfite, air_in = self.ins biogas, perm, sludge, air_out = self.outs degassing = self._degassing # Initialize the streams biogas.phase = 'g' biogas.empty() mixed = self._mixed mixed.mix_from((raw, recycled)) self._inf.copy_like(mixed) # this stream will be preserved (i.e., no reaction) # Chemicals for cleaning, assume all chemicals will be used up # 2.2 L/yr/cmd of 12.5 wt% solution (15% vol) naocl.empty() naocl.imass['NaOCl', 'Water'] = [0.125, 1-0.125] naocl.F_vol = (2.2/1e3/365/24) * (mixed.F_vol*24) # m3/hr solution # 0.6 L/yr/cmd of 100 wt% solution, 13.8 lb/kg citric.empty() citric.ivol['CitricAcid'] = (0.6/1e3/365/24) * (mixed.F_vol*24) # m3/hr pure # 0.35 L/yr/cmd of 38% solution, 3.5 lb/gal bisulfite.empty() bisulfite.imass['Bisulfite', 'Water'] = [0.38, 1-0.38] bisulfite.F_vol = (0.35/1e3/365/24) * (mixed.F_vol*24) # m3/hr solution # For pump design self._compute_mod_case_tank_N() Q_R_mgd, Q_IR_mgd = self._compute_liq_flows() retent, recir = self._retent, self._recir retent.copy_like(mixed) recir.copy_like(mixed) retent.F_mass *= Q_R_mgd / self.Q_mgd recir.F_mass *= Q_IR_mgd / self.Q_mgd # Effluents self.growth_rxns(mixed.mol) self.biogas_rxns(mixed.mol) mixed.split_to(perm, sludge, self._isplit.data) degassing(perm, biogas) degassing(sludge, biogas) sludge_conc = self._sludge_conc insolubles = tuple(i.ID for i in self.chemicals if i.ID in default_insolubles) m_insolubles = sludge.imass[insolubles].sum() if m_insolubles/sludge.F_vol <= sludge_conc: old = sludge.ivol['Water'] sludge.ivol['Water'] = new = min(m_insolubles/sludge_conc, sludge.ivol['Water'] + perm.ivol['Water']) perm.ivol['Water'] += old - new if perm.imass['Water'] < 0: raise ValueError('Not enough moisture in the influent for waste sludge ' f'with {sludge_conc} g/L sludge concentration.') # Gas for sparging, no sparging needed if submerged or using GAC air_out.link_with(air_in) air_in.T = 17 + 273.15 self._design_blower() if self.T is not None: perm.T = sludge.T = biogas.T = air_out.T = self.T # Called by _run def _compute_mod_case_tank_N(self): N_mod_min, N_mod_max = self.mod_per_cas_range[self.membrane_type] N_cas_min, N_cas_max = self.cas_per_tank_range mod_per_cas, cas_per_tank = N_mod_min, N_cas_min J_max = self.J_max while self.J > J_max: mod_per_cas += 1 if mod_per_cas == N_mod_max + 1: if cas_per_tank == N_cas_max + 1: self.N_train+=1 mod_per_cas, cas_per_tank = N_mod_min, N_cas_min else: cas_per_tank += 1 mod_per_cas = N_mod_min self._mod_per_cas, self._cas_per_tank = \ mod_per_cas, cas_per_tank # Called by _run def _compute_liq_flows(self): m_type = self.membrane_type if m_type == 'multi-tube': # Cross-flow flow rate per module, # based on manufacture specifications for compact 33, [m3/hr] Q_cross_flow = 53.5 * self.v_cross_flow Q_R_cmh = self.N_mod_tot * Q_cross_flow # total retentate flow rate, [m3/hr] Q_R_mgd = Q_R_cmh * _cmh_to_mgd # [mgd] Q_mgd, recir_ratio = self.Q_mgd, self.recir_ratio if Q_mgd*recir_ratio >= Q_R_mgd: Q_IR_mgd = Q_mgd*recir_ratio - Q_R_mgd else: Q_IR_mgd = 0 # # Gives really large recir_ration, # # probably shouldn't back-calculate this way # self._recir_ratio = Q_R_mgd / Q_mgd if self.add_GAC: Q_upflow_req = (self.v_GAC/_ft_to_m) * \ self.L_membrane_tank*self.W_tank*self.N_train * 24 * _ft3_to_gal / 1e6 Q_IR_add_mgd = max(0, (Q_mgd+Q_IR_mgd)-Q_upflow_req) Q_IR_mgd += Q_IR_add_mgd self._recir_ratio = Q_IR_mgd / Q_mgd return Q_R_mgd, Q_IR_mgd # Called by _run def _design_blower(self): air = self.ins[-1] if (not self.add_GAC) and (self.membrane_configuration=='submerged'): gas = self.SGD * self.mod_surface_area*_ft2_to_m2 # [m3/h] gas /= (_ft3_to_m3 * 60) # [ft3/min] gas_train = gas * self.N_train*self.cas_per_tank*self.mod_per_cas TCFM = math.ceil(gas_train) # total cubic ft per min N = 1 if TCFM <= 30000: CFMB = TCFM / N # cubic ft per min per blower while CFMB > 7500: N += 1 CFMB = TCFM / N elif 30000 < TCFM <= 72000: CFMB = TCFM / N while CFMB > 18000: N += 1 CFMB = TCFM / N else: CFMB = TCFM / N while CFMB > 100000: N += 1 CFMB = TCFM / N gas_m3_hr = TCFM * _ft3_to_m3 * 60 # ft3/min to m3/hr air.ivol['N2'] = 0.79 air.ivol['O2'] = 0.21 air.F_vol = gas_m3_hr else: # no sparging/blower needed TCFM = CFMB = 0. N = -1 # to account for the spare air.empty() D = self.design_results D['Total air flow [CFM]'] = TCFM D['Blower capacity [CFM]'] = CFMB D['Blowers'] = self._N_blower = N + 1 # add a spare # ========================================================================= # _design # ========================================================================= def _design(self): D = self.design_results D['Treatment train'] = self.N_train D['Cassette per train'] = self.cas_per_tank D['Module per cassette'] = self.mod_per_cas D['Total membrane modules'] = self.N_mod_tot # Step A: Reactor and membrane tanks # Call the corresponding design function # (_design_CSTR or _design_AF) func = getattr(self, f'_design_{self.reactor_type}') wall, slab, excavation = func() D['Wall concrete [ft3]'] = wall D['Slab concrete [ft3]'] = slab D['Excavation [ft3]'] = excavation # Optional addition of packing media (used in filters) ldpe, hdpe = 0., 0. for i in (self.AF, self.AeF): if i is None: continue ldpe += i.design_results['Packing LDPE [m3]'] hdpe += i.design_results['Packing HDPE [m3]'] # Optional addition of GAC D['GAC [kg]'] = self._design_GAC() # Step B: Membrane # Call the corresponding design function # (_design_hollow_fiber, _design_flat_sheet, or _design_multi_tube) m_type = format_str(self.membrane_type) func = getattr(self, f'_design_{m_type}') D['Membrane [m3]'] = func() # Step C: Pumps pipe, pumps, hdpe = self._design_pump() D['Pipe stainless steel [kg]'] = pipe D['Pump stainless steel [kg]'] = pumps D['Pump chemical storage HDPE [m3]'] = hdpe # Step D: Degassing membrane D['Degassing membrane'] = self.N_degasser # Total volume D['Total volume [ft3]'] = self.V_tot ### Step A functions ### # Called by _design def _design_CSTR(self): N_train = self.N_train W_dist, L_CSTR , W_eff, L_membrane_tank = \ self.W_dist, self.L_CSTR, self.W_eff, self.L_membrane_tank W_PB, W_BB, D_tank = self.W_PB, self.W_BB, self.D_tank t_wall, t_slab = self.t_wall, self.t_slab SL, CA = self.excav_slope, self.constr_access ### Concrete calculation ### W_N_trains = (self.W_tank+2*t_wall)*N_train - t_wall*(N_train-1) D = D_tank + 2 # add 2 ft of freeboard t = t_wall + t_slab get_VWC = lambda L1, N: N * t_wall * L1 * D get_VSC = lambda L2: t * L2 * W_N_trains # Concrete for distribution channel, [ft3] VWC_dist = get_VWC(L1=(W_N_trains+W_dist), N=2) VSC_dist = get_VSC(L2=(W_dist+2*t_wall)) # Concrete for CSTR tanks, [ft3] VWC_CSTR = get_VWC(L1=L_CSTR, N=(N_train+1)) VSC_CSTR = get_VSC(L2=L_CSTR) # Concrete for effluent channel, [ft3] VWC_eff = get_VWC(L1=(W_N_trains+W_eff), N=2) VSC_eff = get_VSC(L2=(W_eff+2*t_wall)) # Concrete for the pump/blower building, [ft3] VWC_PBB = get_VWC(L1=(W_N_trains+W_PB+W_BB), N=2) VSC_PBB = get_VSC(L2=(W_PB+t_wall+W_BB)) if self.membrane_configuration == 'submerged': VWC_membrane_tank, VSC_membrane_tank, VWC_well, VSC_well = \ self._design_membrane_tank(self, D, N_train, W_N_trains, self.L_membrane_tank) else: VWC_membrane_tank, VSC_membrane_tank, VWC_well, VSC_well = 0., 0., 0., 0. # Total volume of wall concrete, [ft3] VWC = VWC_dist + VWC_CSTR + VWC_eff + VWC_PBB + VWC_membrane_tank + VWC_well # Total volume of slab concrete [ft3] VSC = VSC_dist + VSC_CSTR + VSC_eff + VSC_PBB + VSC_membrane_tank + VSC_well ### Excavation calculation ### get_VEX = lambda L_bttom, W_bottom, diff: \ 0.5 * D_tank * (L_bottom*W_bottom+(L_bottom+diff)*(W_bottom+diff)) # bottom+top L_bottom = W_dist + L_CSTR + W_eff + L_membrane_tank + 2*CA W_bottom = W_N_trains + 2*CA diff = D_tank * SL # Excavation volume for membrane tanks, [ft3] VEX_membrane_tank = get_VEX(L_bottom, W_bottom, diff) # Excavation volume for pump/blower building, [ft3] VEX_PBB = get_VEX((W_PB+W_BB+2*CA), W_bottom, diff) VEX = VEX_membrane_tank + VEX_PBB return VWC, VSC, VEX # Called by _design def _design_AF(self): raise RuntimeError('Design not implemented.') # Called by _design_CSTR/_design_AF def _design_membrane_tank(self, D, N_train, W_N_trains, L_membrane_tank, t_wall, t_slab): L_well, W_well, D_well = self.L_well, self.W_well, self.D_well # Concrete for membrane tanks, [ft3] t = t_wall + t_slab VWC_membrane_tank = (N_train+1) * t_wall * L_membrane_tank * D VSC_membrane_tank = t * L_membrane_tank * W_N_trains # Concrete for wet well (mixed liquor storage), [ft3] L = L_well + 2*t_wall W = W_well + 2*t_wall VWC_well = 2 * t_wall * (L_well+W) * D_well VSC_well = (t_slab+t_wall) * L * W return VWC_membrane_tank, VSC_membrane_tank, VWC_well, VSC_well # Called by _design def _design_GAC(self): if not self.add_GAC: return 0 raise RuntimeError('Design not implemented.') ### Step B functions ### # Called by _design def _design_hollow_fiber(self): raise RuntimeError('Design not implemented.') # Called by _design def _design_flat_sheet(self): raise RuntimeError('Design not implemented.') # Called by _design def _design_multi_tube(self): # # 0.01478 is volume of material for each membrane tube, [m3] # # L_tube OD ID # V_tube = 3 * math.pi/4 * ((6e-3)**2-(5.2e-3)**2) # V_SU = 700 * V_tube # V for each small unit [m3] # M_SU = 1.78*1e3 * V_SU # mass = density*volume, [kg/m3], not used return self.N_mod_tot*0.01478 ### Step C function ### # Called by _design def _design_pump(self): rx_type, m_config, pumps = \ self.reactor_type, self.membrane_configuration, self._pumps ins_dct = { 'perm': self.outs[1].proxy(), 'retent': self._retent, 'recir': self._recir, 'sludge': self.outs[2].proxy(), 'naocl': self.ins[2].proxy(), 'citric': self.ins[3].proxy(), 'bisulfite': self.ins[4].proxy(), } type_dct = { 'perm': f'permeate_{m_config}', 'retent': f'retentate_{rx_type}', 'recir': f'recirculation_{rx_type}', 'sludge': 'sludge', 'naocl': 'chemical', 'citric': 'chemical', 'bisulfite': 'chemical', } inputs_dct = { 'perm': (self.cas_per_tank, self.D_tank, self.TMP_anaerobic, self.include_aerobic_filter), 'retent': (self.cas_per_tank,), 'recir': (self.L_CSTR,), 'sludge': (), 'naocl': (), 'citric': (), 'bisulfite': (), } WWTpump._batch_adding_pump(self, pumps[:-2], ins_dct, type_dct, inputs_dct) self.AF_pump = self.AF.lift_pump if self.AF else None self.AeF_pump = self.AeF.lift_pump if self.AeF else None pipe_ss, pump_ss, hdpe = 0., 0., 0. for i in pumps: p = getattr(self, f'{i}_pump') if p == None: continue p.simulate() pipe_ss += p.design_results['Pipe stainless steel [kg]'] pump_ss += p.design_results['Pump stainless steel [kg]'] hdpe += p.design_results['Chemical storage HDPE [m3]'] return pipe_ss, pump_ss, hdpe # ========================================================================= # _cost # ========================================================================= def _cost(self): D, C = self.design_results, self.baseline_purchase_costs # Concrete and excavation VEX, VWC, VSC = \ D['Excavation [ft3]'], D['Wall concrete [ft3]'], D['Slab concrete [ft3]'] # 27 is to convert the VEX from ft3 to yard3 C['Reactor excavation'] = VEX/27*8 if self.include_excavation_cost else 0. C['Wall concrete'] = VWC / 27 * 650 C['Slab concrete'] = VSC / 27 * 350 # Membrane C['Membrane'] = self.membrane_unit_cost * D['Membrane [m3]'] / _ft2_to_m2 # GAC # $13.78/kg C['GAC'] = 13.78 * D['GAC [kg]'] # Packing material ldpe, hdpe = 0., 0. for i in (self.AF, self.AeF): if i is None: continue ldpe += i.baseline_purchase_costs['Packing LDPE [m3]'] hdpe += i.baseline_purchase_costs['Packing HDPE [m3]'] # Heat loss, assume air is 17°C, ground is 10°C T = self.T if T is None: loss = 0. else: N_train, L_CSTR, W_tank, D_tank = \ self.N_train, self.L_CSTR, self.W_tank, self.D_tank A_W = 2 * (L_CSTR+W_tank) * D_tank A_F = L_CSTR * W_tank A_W *= N_train * _ft2_to_m2 A_F *= N_train * _ft2_to_m2 loss = 0.7 * (T-(17+273.15)) * A_W # 0.7 W/m2/°C for wall loss += 1.7 * (T-(10+273.15)) * A_F # 1.7 W/m2/°C for floor loss += 0.95 * (T-(17+273.15)) * A_F # 0.95 W/m2/°C for floating cover loss *= 3.6 # W (J/s) to kJ/hr # Stream heating hx = self.heat_exchanger inf = self._inf hx_ins0, hx_outs0 = hx.ins[0], hx.outs[0] hx_ins0.copy_flow(inf) hx_outs0.copy_flow(inf) hx_ins0.T = inf.T hx_outs0.T = T hx.H = hx_outs0.H + loss # stream heating and heat loss hx.simulate_as_auxiliary_exchanger(ins=hx.ins, outs=hx.outs) # Pump # Note that maintenance and operating costs are included as a lumped # number in the biorefinery thus not included here pumps, building = cost_pump(self) C['Pumps'] = pumps C['Pump building'] = building if self.include_pump_building_cost else 0. C['Pump excavation'] = VEX/27*0.3 if self.include_excavation_cost else 0. # Blower and air pipe TCFM, CFMB = D['Total air flow [CFM]'], D['Blower capacity [CFM]'] C['Air pipes'], C['Blowers'], C['Blower building'] = self._cost_blower(TCFM, CFMB) # Degassing membrane C['Degassing membrane'] = 10000 * D['Degassing membrane'] # Pumping pumping = 0. for ID in self._pumps: p = getattr(self, f'{ID}_pump') if p is None: continue pumping += p.power_utility.rate # Gas sparging = 0. # submerge design not implemented degassing = 3 * self.N_degasser # assume each uses 3 kW self.power_utility.rate = sparging + degassing + pumping # Called by _cost def _cost_blower(self, TCFM, CFMB): AFF = self.AFF # Air pipes # Note that the original codes use CFMD instead of TCFM for air pipes, # but based on the coding they are equivalent if TCFM <= 1000: air_pipes = 617.2 * AFF * (TCFM**0.2553) elif 1000 < TCFM <= 10000: air_pipes = 1.43 * AFF * (TCFM**1.1337) else: air_pipes = 28.59 * AFF * (TCFM**0.8085) # Blowers if TCFM <= 30000: ratio = 0.7 * (CFMB**0.6169) blowers = 58000*ratio / 100 elif 30000 < TCFM <= 72000: ratio = 0.377 * (CFMB**0.5928) blowers = 218000*ratio / 100 else: ratio = 0.964 * (CFMB**0.4286) blowers = 480000*ratio / 100 # Blower building area = 128 * (TCFM**0.256) # building area, [ft2] building = area * 90 # 90 is the unit price, [USD/ft2] return air_pipes, blowers, building ### Reactor configuration ### @property def reactor_type(self): """ [str] Can either be "CSTR" for continuous stirred tank reactor or "AF" for anaerobic filter. """ return self._reactor_type @reactor_type.setter def reactor_type(self, i): if not i.upper() in ('CSTR', 'AF'): raise ValueError('`reactor_type` can only be "CSTR", or "AF", ' f'not "{i}".') self._reactor_type = i.upper() @property def membrane_configuration(self): """[str] Can either be "cross-flow" or "submerged".""" return self._membrane_configuration @membrane_configuration.setter def membrane_configuration(self, i): i = 'cross-flow' if i.lower() in ('cross flow', 'crossflow') else i if not i.lower() in ('cross-flow', 'submerged'): raise ValueError('`membrane_configuration` can only be "cross-flow", ' f'or "submerged", not "{i}".') self._membrane_configuration = i.lower() @property def membrane_type(self): """ [str] Can be "hollow fiber" ("submerged" configuration only), "flat sheet" (either "cross-flow" or "submerged" configuration), or "multi-tube" ("cross-flow" configuration only). """ return self._membrane_type @membrane_type.setter def membrane_type(self, i): i = 'multi-tube' if i.lower() in ('multi tube', 'multitube') else i if not i.lower() in ('hollow fiber', 'flat sheet', 'multi-tube'): raise ValueError('`membrane_type` can only be "hollow fiber", ' f'"flat sheet", or "multi-tube", not "{i}".') self._membrane_type = i.lower() @property def membrane_material(self): """ [str] Can be any of the plastics ("PES", "PVDF", "PET", "PTFE") for any of the membrane types ("hollow fiber", "flat sheet", "multi-tube"), or "sintered steel" for "flat sheet", or "ceramic" for "multi-tube". """ return self._membrane_material @membrane_material.setter def membrane_material(self, i): plastics = ('PES', 'PVDF', 'PET', 'PTFE') if i.upper() in plastics: self._membrane_material = i.upper() elif i.lower() in ('sintered steel', 'ceramic'): self._membrane_material = i.lower() else: raise ValueError(f'`membrane_material` can only be plastics materials ' f'{plastics}, "sintered steel", or "ceramic", not {i}.') ### Reactor/membrane tank ### @property def AF(self): """[:class:`~.FilterTank`] Anaerobic filter tank.""" if self.reactor_type == 'CSTR': return None return self._AF @property def AeF(self): """[:class:`~.FilterTank`] Aerobic filter tank.""" if not self.include_aerobic_filter: return None return self._AeF @property def N_train(self): """[int] Number of treatment train.""" if not hasattr(self, '_N_train'): return self._N_train_min return self._N_train @N_train.setter def N_train(self, i): self._N_train = math.ceil(i) @property def cas_per_tank_spare(self): """[int] Number of spare cassettes per train.""" return self._cas_per_tank_spare @cas_per_tank_spare.setter def cas_per_tank_spare(self, i): self._cas_per_tank_spare = math.ceil(i) @property def mod_per_cas_range(self): """ [tuple] Range (min, max) of the number of membrane modules per cassette for the current membrane type. """ return self._mod_per_cas_range @mod_per_cas_range.setter def mod_per_cas_range(self, i): self._mod_per_cas_range[self.membrane_type] = \ tuple(math.floor(i[0]), math.floor(i[1])) @property def mod_per_cas(self): """ [float] Number of membrane modules per cassette for the current membrane type. """ return self._mod_per_cas or self._mod_per_cas_range[self.membrane_type][0] @property def cas_per_tank_range(self): """ [tuple] Range (min, max) of the number of membrane cassette per tank (same for all membrane types). """ return self._cas_per_tank_range @cas_per_tank_range.setter def cas_per_tank_range(self, i): self._cas_per_tank_range = tuple(math.floor(i[0]), math.floor(i[1])) @property def cas_per_tank(self): """ [float] Number of membrane cassettes per tank for the current membrane type. """ return self._cas_per_tank or self._cas_per_tank_range[0] @property def N_mod_tot(self): """[int] Total number of memberane modules.""" return self.N_train * self.cas_per_tank * self.mod_per_cas @property def mod_surface_area(self): """ [float] Surface area of the membrane for the current membrane type, [m2/module]. Note that one module is one sheet for plat sheet and one tube for multi-tube. """ return self._mod_surface_area[self.membrane_type] @mod_surface_area.setter def mod_surface_area(self, i): self._mod_surface_area[self.membrane_type] = float(i) @property def L_CSTR(self): """[float] Length of the CSTR tank, [ft].""" if self.reactor_type == 'AF': return 0 return self._inf.F_vol/_ft3_to_m3*self.HRT/(self.N_train*self.W_tank*self.D_tank) @property def L_membrane_tank(self): """[float] Length of the membrane tank, [ft].""" return math.ceil((self.cas_per_tank+self.cas_per_tank_spare)*3.4) @property def W_tank(self): """[float] Width of the reactor/membrane tank (same value), [ft].""" return self._W_tank @W_tank.setter def W_tank(self, i): self._W_tank = float(i) @property def D_tank(self): """[float] Depth of the reactor/membrane tank (same value), [ft].""" return self._D_tank @D_tank.setter def D_tank(self, i): self._D_tank = float(i) @property def W_dist(self): """[float] Width of the distribution channel, [ft].""" return self._W_dist @W_dist.setter def W_dist(self, i): self._W_dist = float(i) @property def W_eff(self): """[float] Width of the effluent channel, [ft].""" return self._W_eff @W_eff.setter def W_eff(self, i): self._W_eff = float(i) @property def t_wall(self): """ [float] Concrete wall thickness, [ft]. Minimum of 1 ft with 1 in added for every ft of depth over 12 ft. """ return 1 + max(self.D_tank-12, 0)/12 @property def t_slab(self): """ [float] Concrete slab thickness, [ft]. 2 in thicker than the wall thickness. """ return self.t_wall+2/12 @property def V_tot(self): """[float] Total volume of the unit, [ft3].""" return self.D_tank*self.W_tank*self.L_CSTR*self.N_train @property def OLR(self): """[float] Organic loading rate, [kg COD/m3/hr].""" return self.compute_COD(self.ins[0])*self.ins[0].F_vol/(self.V_tot*_ft3_to_m3) ### Pump/blower ### @property def N_blower(self): """ [int] Number of blowers needed for gas sparging (not needed for some designs). Note that this is not used in costing (the cost is estimated based on the total sparging gas need). """ if not self.add_GAC and self.membrane_configuration=='submerged': return self._N_blower return 0 @property def N_degasser(self): """ [int] Number of degassing membrane needed for dissolved biogas removal (optional). """ if self.include_degassing_membrane: return math.ceil(self.Q_cmd/24/30) # assume each can hand 30 m3/d of influent return 0 @property def W_PB(self): """[float] Width of the pump building, [ft].""" if self.membrane_configuration == 'submerged': N = self.cas_per_tank else: # cross-flow N = math.ceil(self.L_CSTR/((1+8/12)+(3+4/12))) if 0 <= N <= 10: W_PB = 27 + 4/12 elif 11 <= N <= 16: W_PB = 29 + 6/12 elif 17 <= N <= 22: W_PB = 31 + 8/12 elif 23 <= N <= 28: W_PB = 35 elif N >= 29: W_PB = 38 + 4/12 else: W_PB = 0 return W_PB @property def L_BB(self): """[float] Length of the blower building, [ft].""" if self.membrane_configuration == 'submerged': return (69+6/12) if self.cas_per_tank<=18 else (76 + 8/12) return 0 @property def W_BB(self): """[float] Width of the blower building, [ft].""" if self.membrane_configuration == 'submerged': return (18+8/12) if self.cas_per_tank<=18 else 22 return 0 ### Wet well (submerged only) ### @property def L_well(self): """ [float] Length of the wet well, [ft]. Only needed for submerged configuration. """ return self._L_well if self.membrane_configuration == 'submerged' else 0 @L_well.setter def L_well(self, i): self._L_well = float(i) @property def W_well(self): """ [float] Width of the wet well, [ft]. Only needed for submerged configuration. """ return self._W_well if self.membrane_configuration == 'submerged' else 0 @W_well.setter def W_well(self, i): self._W_well = float(i) @property def D_well(self): """ [float] Depth of the wet well, [ft]. Only needed for submerged configuration. """ return self._D_well if self.membrane_configuration == 'submerged' else 0 @D_well.setter def D_well(self, i): self._D_well = float(i) ### Excavation ### @property def excav_slope(self): """[float] Slope for excavation (horizontal/vertical).""" return self._excav_slope @excav_slope.setter def excav_slope(self, i): self._excav_slope = float(i) @property def constr_access(self): """[float] Extra room for construction access, [ft].""" return self._constr_access @constr_access.setter def constr_access(self, i): self._constr_access = float(i) ### Operation-related parameters ### @property def Q_mgd(self): """ [float] Influent volumetric flow rate in million gallon per day, [mgd]. """ return self.ins[0].F_vol*_m3_to_gal*24/1e6 @property def Q_gpm(self): """[float] Influent volumetric flow rate in gallon per minute, [gpm].""" return self.Q_mgd*1e6/24/60 @property def Q_cmd(self): """ [float] Influent volumetric flow rate in cubic meter per day, [cmd]. """ return self.Q_mgd *1e6/_m3_to_gal # [m3/day] @property def Q_cfs(self): """[float] Influent volumetric flow rate in cubic feet per second, [cfs].""" return self.Q_mgd*1e6/24/60/60/_ft3_to_gal @property def HRT(self): """ [float] Hydraulic retention time, [hr]. """ return self._HRT @HRT.setter def HRT(self, i): self._HRT = float(i) @property def recir_ratio(self): """ [float] Internal recirculation ratio, will be updated in simulation if the originally set ratio is not adequate for the desired flow required by GAC (if applicable). """ return self._recir_ratio @recir_ratio.setter def recir_ratio(self, i): self._recir_ratio = float(i) @property def J_max(self): """[float] Maximum membrane flux, [L/m2/hr].""" return self._J_max @J_max.setter def J_max(self, i): self._J_max = float(i) @property def J(self): """[float] Membrane flux, [L/m2/hr].""" # Based on the flux of one train being offline SA = (self.N_train-1) * self.cas_per_tank * self.mod_per_cas * self.mod_surface_area return self._inf.F_vol*1e3/SA # 1e3 is conversion from m3 to L @property def TMP_anaerobic(self): """[float] Transmembrane pressure in the anaerobic reactor, [psi].""" return self._TMP_dct[self.membrane_configuration] @TMP_anaerobic.setter def TMP_anaerobic(self, i): self._TMP_dct[self.membrane_configuration] = float(i) @property def TMP_aerobic(self): """ [float] Transmembrane pressure in the aerobic filter, [psi]. Defaulted to half of the reactor TMP. """ if not self._include_aerobic_filter: return 0. else: return self._TMP_aerobic or self._TMP_dct[self.membrane_configuration]/2 @TMP_aerobic.setter def TMP_aerobic(self, i): self._TMP_aerobic = float(i) @property def SGD(self): """[float] Specific gas demand, [m3 gas/m2 membrane area/h].""" return self._SGD @SGD.setter def SGD(self, i): self._SGD = float(i) @property def AFF(self): """ [float] Air flow fraction, used in air pipe costing. The default value is calculated as STE/6 (STE stands for standard oxygen transfer efficiency, and default STE is 20). If using different STE value, AFF should be 1 if STE/6<1 and 3.33 if STE/6>1. """ return self._AFF @AFF.setter def AFF(self, i): self._AFF = float(i) @property def v_cross_flow(self): """ [float] Cross-flow velocity, [m/s]. """ return self._v_cross_flow if self.membrane_configuration=='cross-flow' else 0 @v_cross_flow.setter def v_cross_flow(self, i): self._v_cross_flow = float(i) @property def v_GAC(self): """ [float] Upflow velocity for GAC bed expansion, [m/hr]. """ return self._v_GAC if self.add_GAC==True else 0 @v_GAC.setter def v_GAC(self, i): self._v_GAC = float(i) @property def biodegradability(self): """ [float of dict] Biodegradability of chemicals, when shown as a float, all biodegradable chemicals are assumed to have the same degradability. """ return self._biodegradability @biodegradability.setter def biodegradability(self, i): if not isinstance(i, dict): if not 0<=i<=1: raise ValueError('`biodegradability` should be within [0, 1], ' f'the input value {i} is outside the range.') self._biodegradability = dict.fromkeys(self.chemicals.IDs, i) else: for k, v in i.items(): if not 0<=v<=1: raise ValueError('`biodegradability` should be within [0, 1], ' f'the input value for chemical "{k}" is ' 'outside the range.') self._biodegradability = dict.fromkeys(self.chemicals.IDs, i).update(i) self._refresh_rxns() @property def sludge_conc(self): """Concentration of biomass ("WWTsludge") in the waste sludge, [g/L].""" return self._sludge_conc @sludge_conc.setter def sludge_conc(self, i): self._sludge_conc = i @property def i_rm(self): """[:class:`np.array`] Removal of each chemical in this reactor.""" return self._i_rm @property def split(self): """Component-wise split to the treated water.""" return self._split @split.setter def split(self, i): self._split = i self._isplit = self.chemicals.isplit(i, order=None) @property def biogas_rxns(self): """ [:class:`tmo.ParallelReaction`] Biogas production reactions. """ return self._biogas_rxns @property def growth_rxns(self): """ [:class:`tmo.ParallelReaction`] Biomass (WWTsludge) growth reactions. """ return self._growth_rxns @property def organic_rm(self): """[float] Overall organic (COD) removal rate.""" Qi, Qe = self._inf.F_vol, self.outs[1].F_vol Si, Se = self.compute_COD(self._inf), self.compute_COD(self.outs[1]) return 1 - Qe*Se/(Qi*Si)