Source code for biosteam.units.mixing

# -*- 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.
"""
.. contents:: :local:

.. autoclass:: biosteam.units.mixing.Mixer
.. autoclass:: biosteam.units.mixing.SteamMixer
.. autoclass:: biosteam.units.mixing.MockMixer

"""
from .._unit import Unit
from thermosteam._graphics import mixer_graphics
import flexsolve as flx
import biosteam as bst
import numpy as np
from typing import Optional

__all__ = ('Mixer', 'SteamMixer', 'FakeMixer', 'MockMixer')

[docs] class Mixer(Unit): """ Create a mixer that mixes any number of streams together. Parameters ---------- ins : Inlet fluids to be mixed. outs : Mixed outlet fluid. rigorous : Whether to perform vapor-liquid equilibrium. Notes ----- When streams at different pressures are mixed, BioSTEAM assumes valves reduce the pressure of the streams being mixed to prevent backflow (pressure needs to decrease in the direction of flow according to Bernoulli's principle). The outlet pressure will be the minimum pressure of all inlet streams. Examples -------- Mix two streams: >>> from biosteam import units, settings, Stream >>> settings.set_thermo(['Ethanol', 'Water'], cache=True) >>> s1 = Stream('s1', Water=20, T=350) >>> s2 = Stream('s2', Ethanol=30, T=300) >>> M1 = units.Mixer('M1', ins=(s1, s2), outs='s3') >>> M1.simulate() >>> M1.show() Mixer: M1 ins... [0] s1 phase: 'l', T: 350 K, P: 101325 Pa flow (kmol/hr): Water 20 [1] s2 phase: 'l', T: 300 K, P: 101325 Pa flow (kmol/hr): Ethanol 30 outs... [0] s3 phase: 'l', T: 315.14 K, P: 101325 Pa flow (kmol/hr): Ethanol 30 Water 20 """ _graphics = mixer_graphics _N_outs = 1 _N_ins = 2 _ins_size_is_fixed = False def _assert_compatible_property_package(self): pass # Not necessary for mixing streams def _init(self, rigorous: Optional[bool]=False, conserve_phases: Optional[bool]=False): self.rigorous = rigorous self.conserve_phases = conserve_phases def _run(self): s_out, = self.outs s_out.mix_from(self.ins, vle=self.rigorous, conserve_phases=getattr(self, 'conserve_phases', None)) V = s_out.vapor_fraction if V == 0: self._B = 0 elif V == 1: self._B = np.inf else: self._B = V / (1 - V) def _get_energy_departure_coefficient(self, stream): if stream.phases == ('g', 'l'): vapor, liquid = stream if vapor.isempty(): with liquid.temporary_phase('g'): coeff = liquid.H else: coeff = -vapor.h * liquid.F_mol else: coeff = -stream.C return (self, coeff) def _create_energy_departure_equations(self): # Ll: C1dT1 - Ce2*dT2 - Cr0*dT0 - hv2*L2*dB2 = Q1 - H_out + H_in # gl: hV1*L1*dB1 - hv2*L2*dB2 - Ce2*dT2 - Cr0*dT0 = Q1 + H_in - H_out outlet = self.outs[0] phases = outlet.phases if phases == ('g', 'l'): vapor, liquid = outlet coeff = {} if vapor.isempty(): with liquid.temporary_phase('g'): coeff[self] = liquid.H else: coeff[self] = vapor.h * liquid.F_mol else: coeff = {self: outlet.C} for i in self.ins: i._update_energy_departure_coefficient(coeff) return [(coeff, self.H_in - self.H_out)] def _create_material_balance_equations(self, composition_sensitive): fresh_inlets, process_inlets, equations = self._begin_equations(composition_sensitive) outlet, = self.outs if len(outlet) == 1: ones = np.ones(self.chemicals.size) minus_ones = -ones zeros = np.zeros(self.chemicals.size) # Overall flows eq_overall = {outlet: ones} for i in process_inlets: eq_overall[i] = minus_ones equations.append( (eq_overall, sum([i.mol for i in fresh_inlets], zeros)) ) else: top, bottom = outlet ones = np.ones(self.chemicals.size) minus_ones = -ones zeros = np.zeros(self.chemicals.size) # Overall flows eq_overall = {} for i in outlet: eq_overall[i] = ones for i in process_inlets: eq_overall[i] = minus_ones equations.append( (eq_overall, sum([i.mol for i in fresh_inlets], zeros)) ) # Top to bottom flows B = self._B eq_outs = {} if B == np.inf: eq_outs[bottom] = ones elif B == 0: eq_outs[top] = ones else: bp = outlet.bubble_point_at_P() outlet.T = bp.T S = bp.K * B eq_outs[top] = ones eq_outs[bottom] = -S equations.append( (eq_outs, zeros) ) return equations def _update_energy_variable(self, departure): phases = self.outs[0].phases if phases == ('g', 'l'): self._B += departure else: self.outs[0].T += departure def _update_nonlinearities(self): pass
[docs] class SteamMixer(Unit): """ Create a mixer that varies the flow of steam to achieve a specified outlet pressure and varies the flow of process water to achieve a specified solids loading (by wt). Parameters ---------- ins : * [0] Feed * [1] Steam * [2] Process water outs : Mixed product. P : float Outlet pressure. T : float Outlet temperature. solids_loading : float, optional Final solids loading after mixing in process water. soilds_loading_includes_steam : bool, optional Whether to include steam in solids loading calculation. Examples -------- >>> import biosteam as bst >>> bst.settings.set_thermo(['Water', 'Glucose']) >>> feed = bst.Stream('feed', Water=10, Glucose=10) >>> M1 = bst.SteamMixer(None, ins=[feed, 'steam', 'process_water'], outs='outlet', T=431.15, P=557287.5, solids_loading=0.3) >>> M1.simulate() >>> M1.show('cwt100') # Note that outlet solids loading is not exactly 0.3 because of the steam requirement. SteamMixer ins... [0] feed phase: 'l', T: 298.15 K, P: 101325 Pa composition (%): Water 9.09 Glucose 90.9 ------- 1.98e+03 kg/hr [1] steam phase: 'g', T: 454.77 K, P: 1.041e+06 Pa composition (%): Water 100 ----- 1.28e+03 kg/hr [2] process_water phase: 'l', T: 298.15 K, P: 101325 Pa composition (%): Water 100 ----- 4.02e+03 kg/hr outs... [0] outlet phase: 'l', T: 431.15 K, P: 557288 Pa composition (%): Water 75.3 Glucose 24.7 ------- 7.29e+03 kg/hr >>> M1.solids_loading_includes_steam = True >>> M1.simulate() >>> M1.show('cwt100') # Now the outlet solids content is exactly 0.3 SteamMixer ins... [0] feed phase: 'l', T: 298.15 K, P: 101325 Pa composition (%): Water 9.09 Glucose 90.9 ------- 1.98e+03 kg/hr [1] steam phase: 'g', T: 454.77 K, P: 1.041e+06 Pa composition (%): Water 100 ----- 1.02e+03 kg/hr [2] process_water phase: 'l', T: 298.15 K, P: 101325 Pa composition (%): Water 100 ----- 3.01e+03 kg/hr outs... [0] outlet phase: 'l', T: 431.15 K, P: 557288 Pa composition (%): Water 70 Glucose 30 ------- 6.01e+03 kg/hr """ _N_outs = 1 _N_ins = 3 _ins_size_is_fixed = False _graphics = mixer_graphics installation_cost = purchase_cost = 0. def _init(self, P, T=None, solids_loading=None, liquid_IDs=['7732-18-5'], solid_IDs=None, solids_loading_includes_steam=None): self.P = P self.T = T self.solids_loading = solids_loading self.solids_loading_includes_steam = solids_loading_includes_steam self.liquid_IDs = tuple(liquid_IDs) self.solid_IDs = solid_IDs @property def steam(self): return self.ins[1] def reset_cache(self, isdynamic=None): for utility in bst.HeatUtility.heating_agents: if utility.P > self.P: break self.steam.copy_like(utility) def pressure_objective_function(self, steam_mol): mixed = self.outs[0] self.steam.imol['7732-18-5'] = steam_mol # Only change water if self.P: mixed.P = self.P # Assume pumps take care of this if self.solids_loading_includes_steam and self.solids_loading: solids_loading = self.solids_loading feed, steam, process_water, *others = self.ins process_water.empty() feeds = self.ins chemicals = self.chemicals index = chemicals.get_index(self.liquid_IDs) available_water = 18.01528 * sum([(j.sum() if hasattr((j:=i.mol[index]), 'sum') else j) for i in feeds if i]) solid_IDs = self.solid_IDs if solid_IDs: F_mass_solids = sum([i.imass[solid_IDs].sum() for i in feeds if i]) else: F_mass_feed = sum([i.F_mass for i in feeds if i]) F_mass_solids = F_mass_feed - available_water required_water = F_mass_solids * (1. - solids_loading) / solids_loading process_water.imol['7732-18-5'] = max(required_water - available_water, 0.) / 18.01528 mixed.mix_from(self.ins, energy_balance=False) H = sum([i.H for i in self.ins]) Tmax = mixed.chemicals.Water.Tc - 1 mixed.T = Tmax Hmax = mixed.H if H > Hmax: mixed.T = Tmax + (H - Hmax) / mixed.chemicals.Water.Cn('l', Tmax) else: mixed.H = H if self.T: return self.T - mixed.T else: # If no pressure, assume it is at the boiling point P_new = mixed.chemicals.Water.Psat(min(mixed.T, Tmax)) return self.P - P_new def _setup(self): super()._setup() if self.steam.isempty(): self.reset_cache() def _run(self): solids_loading = self.solids_loading if solids_loading is not None and not self.solids_loading_includes_steam: # Solids loading need to be achieved first before mixing with steam # to avoid pumping issues (see Humbird 2011 NREL report). feed, steam, process_water, *others = self.ins process_water.empty() feeds = [feed, *others] chemicals = self.chemicals index = chemicals.get_index(self.liquid_IDs) available_water = 18.01528 * sum([(j.sum() if hasattr((j:=i.mol[index]), 'sum') else j) for i in feeds if i]) solid_IDs = self.solid_IDs if solid_IDs: F_mass_solids = sum([i.imass[solid_IDs].sum() for i in feeds if i]) else: F_mass_feed = sum([i.F_mass for i in feeds if i]) F_mass_solids = F_mass_feed - available_water required_water = F_mass_solids * (1. - solids_loading) / solids_loading process_water.imol['7732-18-5'] = max(required_water - available_water, 0.) / 18.01528 else: steam = self.steam steam_mol = steam.F_mol or 1. f = self.pressure_objective_function steam_mol = flx.IQ_interpolation(f, *flx.find_bracket(f, 0., steam_mol, None, None), xtol=1e-2, ytol=1e-4, maxiter=500, checkroot=False) self.outs[0].P = self.P def _design(self): steam = self.ins[1] warm_process_water = self.ins[2] mixed = self.outs[0] self.add_heat_utility(steam.H + warm_process_water.H, mixed.T)
[docs] class MockMixer(Unit): """ Create a MockMixer object that does nothing when simulated. """ _graphics = Mixer._graphics _N_ins = 2 _N_outs = 1 _ins_size_is_fixed = False def _run(self): pass
MockMixer.line = 'Mixer' FakeMixer = MockMixer