Source code for biosteam.units.splitting

# -*- 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 splitting flows.

.. contents:: :local:
    
.. autoclass:: biosteam.units.splitting.Splitter
.. autoclass:: biosteam.units.splitting.PhaseSplitter 
.. autoclass:: biosteam.units.splitting.MockSplitter
.. autoclass:: biosteam.units.splitting.ReversedSplitter

"""
from .. import Unit
from thermosteam._graphics import splitter_graphics
from thermosteam import separations
import biosteam as bst
import numpy as np

__all__ = ('Splitter', 'PhaseSplitter', 'FakeSplitter', 'MockSplitter',
           'ReversedSplitter', 'Separator')

[docs] class Splitter(Unit): """ Create a splitter that separates mixed streams based on splits. Parameters ---------- ins : Inlet fluid to be split. outs : * [0] Split stream * [1] Remainder stream split : Should be one of the following * [float] The fraction of net feed in the 0th outlet stream * [array_like] Componentwise split of feed to 0th outlet stream * [dict] ID-split pairs of feed to 0th outlet stream order=None : Iterable[str], defaults to biosteam.settings.chemicals.IDs Chemical order of split. Examples -------- Create a Splitter object with an ID, a feed stream, two outlet streams, and an overall split: >>> from biosteam import units, settings, Stream >>> settings.set_thermo(['Water', 'Ethanol'], cache=True) >>> feed = Stream('feed', Water=20, Ethanol=10, T=340) >>> S1 = units.Splitter('S1', ins=feed, outs=('top', 'bot'), split=0.1) >>> S1.simulate() >>> S1.show() Splitter: S1 ins... [0] feed phase: 'l', T: 340 K, P: 101325 Pa flow (kmol/hr): Water 20 Ethanol 10 outs... [0] top phase: 'l', T: 340 K, P: 101325 Pa flow (kmol/hr): Water 2 Ethanol 1 [1] bot phase: 'l', T: 340 K, P: 101325 Pa flow (kmol/hr): Water 18 Ethanol 9 Create a Splitter object, but this time with a componentwise split using a dictionary: >>> S1 = units.Splitter('S1', ins=feed, outs=('top', 'bot'), ... split={'Water': 0.1, 'Ethanol': 0.99}) >>> S1.simulate() >>> S1.show() Splitter: S1 ins... [0] feed phase: 'l', T: 340 K, P: 101325 Pa flow (kmol/hr): Water 20 Ethanol 10 outs... [0] top phase: 'l', T: 340 K, P: 101325 Pa flow (kmol/hr): Water 2 Ethanol 9.9 [1] bot phase: 'l', T: 340 K, P: 101325 Pa flow (kmol/hr): Water 18 Ethanol 0.1 Create a Splitter object using componentwise split, but this time specify the order: >>> S1 = units.Splitter('S1', ins=feed, outs=('top', 'bot'), ... order=('Ethanol', 'Water'), ... split=(0.99, 0.10)) >>> S1.simulate() >>> S1.show() Splitter: S1 ins... [0] feed phase: 'l', T: 340 K, P: 101325 Pa flow (kmol/hr): Water 20 Ethanol 10 outs... [0] top phase: 'l', T: 340 K, P: 101325 Pa flow (kmol/hr): Water 2 Ethanol 9.9 [1] bot phase: 'l', T: 340 K, P: 101325 Pa flow (kmol/hr): Water 18 Ethanol 0.1 Splits can also be altered after creating the splitter: >>> S1.split = 0.5 >>> S1.isplit.show() SplitIndexer: Water 0.5 Ethanol 0.5 >>> S1.isplit['Water'] = 1.0 >>> S1.isplit.show() SplitIndexer: Water 1 Ethanol 0.5 >>> S1.split = [0.9, 0.8] >>> S1.isplit.show() SplitIndexer: Water 0.9 Ethanol 0.8 """ _N_outs = 2 _graphics = splitter_graphics @property def isplit(self): """[ChemicalIndexer] Componentwise split of feed to 0th outlet stream.""" return self._isplit @property def split(self): """[SparseArray] Componentwise split of feed to 0th outlet stream.""" return self._isplit.data @split.setter def split(self, values): split = self.split if split is not values: split[:] = values def _init(self, split, order=None): self._isplit = self.thermo.chemicals.isplit(split, order) def _run(self): feed = self._ins[0] isplit = self._isplit if isplit.chemicals is not feed.chemicals: self._reset_thermo(feed._thermo) feed.split_to(*self.outs, isplit.data)
# def _create_material_balance_equations(self, composition_sensitive): # fresh_inlets, process_inlets, equations = self._begin_equations(composition_sensitive) # top, bottom = self.outs # ones = np.ones(self.chemicals.size) # minus_ones = -ones # zeros = np.zeros(self.chemicals.size) # # Overall flows # eq_overall = {} # for i in self.outs: # eq_overall[i] = ones # for i in process_inlets: # if i in eq_overall: del eq_overall[i] # else: eq_overall[i] = minus_ones # equations.append( # (eq_overall, sum([i.mol for i in fresh_inlets], zeros)) # ) # # Top and bottom flows # eq_outs = {} # split = self.split # minus_split = -split # for i in process_inlets: eq_outs[i] = minus_split # rhs = split * sum([i.mol for i in fresh_inlets], zeros) # eq_outs[top] = ones # equations.append( # (eq_outs, rhs) # ) # return equations # def _get_energy_departure_coefficient(self, stream): # coeff = -stream.C # return (self, coeff) # def _create_energy_departure_equations(self): # coeff = {self: self.ins[0].C} # self.ins[0]._update_energy_departure_coefficient(coeff) # return [(coeff, self.H_in - self.H_out)] # def _update_energy_variable(self, departure): # for i in self.outs: i.T += departure
[docs] class PhaseSplitter(Unit): """ Create a PhaseSplitter object that splits the feed to outlets by phase. Parameters ---------- ins : Feed. outs : Outlets. Notes ----- Phases allocate to outlets in alphabetical order. For example, if the feed.phases is 'gls' (i.e. gas, liquid, and solid), the phases of the outlets will be 'g', 'l', and 's'. Examples -------- >>> import biosteam as bst >>> bst.settings.set_thermo(['Water', 'Ethanol'], cache=True) >>> feed = bst.Stream('feed', Water=10, Ethanol=10) >>> feed.vle(V=0.5, P=101325) >>> s1 = bst.Stream('s1') >>> s2 = bst.Stream('s2') >>> PS = bst.PhaseSplitter('PS', feed, [s1, s2]) >>> PS.simulate() >>> PS.show() PhaseSplitter: PS ins... [0] feed phases: ('g', 'l'), T: 353.94 K, P: 101325 Pa flow (kmol/hr): (g) Water 3.87 Ethanol 6.13 (l) Water 6.13 Ethanol 3.87 outs... [0] s1 phase: 'g', T: 353.94 K, P: 101325 Pa flow (kmol/hr): Water 3.87 Ethanol 6.13 [1] s2 phase: 'l', T: 353.94 K, P: 101325 Pa flow (kmol/hr): Water 6.13 Ethanol 3.87 """ _N_ins = 1 _N_outs = 2 _graphics = splitter_graphics def _run(self): separations.phase_split(*self.ins, self.outs)
[docs] class MockSplitter(Unit): """ Create a MockSplitter object that does nothing when simulated. """ _graphics = Splitter._graphics _N_ins = 1 _N_outs = 2 _outs_size_is_fixed = False def _run(self): pass
MockSplitter.line = 'Splitter' FakeSplitter = MockSplitter
[docs] class ReversedSplitter(Unit): """ Create a splitter that, when simulated, sets the inlet stream based on outlet streams. Must have only one input stream. The outlet streams will have the same temperature, pressure and phase as the inlet. """ _graphics = Splitter._graphics _N_ins = 1 _N_outs = 2 _outs_size_is_fixed = False power_utility = None heat_utilities = () results = None def _run(self): inlet, = self.ins outlets = self.outs reversed_split(inlet, outlets)
def reversed_split(inlet, outlets): inlet.mol[:] = sum([i.mol for i in outlets]) T = inlet.T P = inlet.P phase = inlet.phase for out in outlets: out.T = T out.P = P out.phase = phase class Separator(Unit): _N_ins = 1 _N_outs = 2 _ins_size_is_fixed = False @property def isplit(self): """[ChemicalIndexer] Componentwise split of feed to 0th outlet stream.""" return self._isplit @property def split(self): """[SparseArray] Componentwise split of feed to 0th outlet stream.""" return self._isplit.data @split.setter def split(self, values): split = self.split if split is not values: split[:] = values def _init(self, split, order=None, T=None, P=None, phases=None): self._isplit = self.thermo.chemicals.isplit(split, order) self.T_specification = self.T = T self.P = P self.phases = phases def _run(self): ins = self._ins top, bottom = self._outs top.mix_from(ins) top.split_to(*self.outs, self._isplit.data) if self.T_specification: top.T = bottom.T = self.T_specification if self.phases: top.phase, bottom.phase = self.phases if self.P: top.P = bottom.P = self.P def _create_material_balance_equations(self, composition_sensitive): fresh_inlets, process_inlets, equations = self._begin_equations(composition_sensitive) top, bottom = self.outs ones = np.ones(self.chemicals.size) minus_ones = -ones zeros = np.zeros(self.chemicals.size) # Overall flows eq_overall = {} for i in self.outs: eq_overall[i] = ones for i in process_inlets: if i in eq_overall: del eq_overall[i] else: eq_overall[i] = minus_ones equations.append( (eq_overall, sum([i.mol for i in fresh_inlets], zeros)) ) # Top and bottom flows eq_outs = {} split = self.split minus_split = -split for i in process_inlets: eq_outs[i] = minus_split rhs = split * sum([i.mol for i in fresh_inlets], zeros) eq_outs[top] = ones equations.append( (eq_outs, rhs) ) return equations def _get_energy_departure_coefficient(self, stream): if self.T_specification is not None: return coeff = -stream.C return (self, coeff) def _create_energy_departure_equations(self): if self.T_specification is not None: return [] coeff = {self: self.ins[0].C} self.ins[0]._update_energy_departure_coefficient(coeff) return [(coeff, self.H_in - self.H_out)] def _update_energy_variable(self, departure): if self.T is None: self.T = self.outs[0].T self.T = T = self.T + departure for i in self.outs: i.T = T