diff --git a/biosteam.egg-info/PKG-INFO b/biosteam.egg-info/PKG-INFO index 6f8522a8e..32b8113d8 100644 --- a/biosteam.egg-info/PKG-INFO +++ b/biosteam.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.2 Name: biosteam -Version: 2.11.5 +Version: 2.11.7 Summary: The Biorefinery Simulation and Techno-Economic Analysis Modules Home-page: https://github.com/BioSTEAMDevelopmentGroup/biosteam Author: Yoel Cortes-Pena @@ -17,7 +17,7 @@ Description: =================================================================== .. image:: http://img.shields.io/badge/docs-latest-brightgreen.svg?style=flat :target: https://biosteam.readthedocs.io/en/latest/ :alt: Documentation - .. image:: http://img.shields.io/badge/license-MIT-blue.svg?style=flat + .. image:: http://img.shields.io/badge/license-OpenSource-blue.svg?style=flat :target: https://github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/LICENSE.txt :alt: license diff --git a/biosteam.egg-info/requires.txt b/biosteam.egg-info/requires.txt index 474557e04..6cee31eec 100644 --- a/biosteam.egg-info/requires.txt +++ b/biosteam.egg-info/requires.txt @@ -1,6 +1,6 @@ IPython>=7.9.0 biorefineries>=2.8.1 -thermosteam>=0.11.3 +thermosteam>=0.11.4 graphviz>=0.8.3 chaospy>=3.0.11 pyqt5>=5.12 diff --git a/build/lib/biosteam/__init__.py b/build/lib/biosteam/__init__.py deleted file mode 100644 index 7bb5b408f..000000000 --- a/build/lib/biosteam/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Sat Oct 28 17:28:09 2017 - -@author: Yoel Rene Cortes-Pena -""" - -#: Chemical engineering plant cost index (defaults to 567.5 at 2017) -CE = 567.5 - -# %% Initialize BioSTEAM - -from ._heat_utility import HeatUtility, UtilityAgent -from ._power_utility import PowerUtility -from ._unit import Unit -from ._system import System -from ._tea import CombinedTEA, TEA -from ._flowsheet import Flowsheet, main_flowsheet -from ._network import Network -from . import utils -from . import units -from . import evaluation -from . import exceptions - -__all__ = ['Unit', 'PowerUtility', 'HeatUtility', 'UtilityAgent', - 'System', 'TEA', 'CombinedTEA', 'utils', 'units', 'evaluation', - 'main_flowsheet', 'Flowsheet', 'CE', 'Chemical', 'Chemicals', 'Stream', - 'MultiStream', 'settings', 'Network', 'speed_up', 'exceptions', - *units.__all__, *evaluation.__all__] - -from thermosteam import Chemical, Chemicals, Thermo, Stream, MultiStream, settings, speed_up -from .evaluation import * -from .units import * - diff --git a/build/lib/biosteam/_digraph/__init__.py b/build/lib/biosteam/_digraph/__init__.py deleted file mode 100644 index 34da0d25e..000000000 --- a/build/lib/biosteam/_digraph/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sat Mar 28 21:56:16 2020 - -@author: yoelr -""" - -from . import (digraph, - widget) - -__all__ = (*digraph.__all__, - *widget.__all__,) - -from .digraph import * -from .widget import * \ No newline at end of file diff --git a/build/lib/biosteam/_digraph/digraph.py b/build/lib/biosteam/_digraph/digraph.py deleted file mode 100644 index da3f121f5..000000000 --- a/build/lib/biosteam/_digraph/digraph.py +++ /dev/null @@ -1,234 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sun Jan 5 09:59:29 2020 - -@author: yoelr -""" -import biosteam as bst -from graphviz import Digraph -from IPython import display -from collections import namedtuple -from thermosteam import Stream - -__all__ = ('digraph_from_units', - 'digraph_from_units_and_streams', - 'digraph_from_units_and_connections', - 'update_digraph_from_units_and_connections', - 'blank_digraph', - 'minimal_digraph', - 'surface_digraph', - 'finalize_digraph', - 'update_surface_units', - 'display_digraph', - 'save_digraph', - 'get_all_connections', - 'get_stream_connection') - -Connection = namedtuple('Connection', - ('source', 'source_index', 'stream', 'sink_index', 'sink'), - module=__name__) - -def has_path(obj): - return hasattr(obj, 'path') - -def get_streams_from_units(units): - return set(sum([i._ins + i._outs for i in units], [])) - -def blank_digraph(format='svg', **graph_attrs): - # Create a digraph and set direction left to right - f = Digraph(format=format) - f.attr(rankdir='LR', **graph_attrs) - return f - -def get_section_inlets_and_outlets(units, streams): - outs = [] - ins = [] - units = tuple(units) - for s in streams: - source = s._source - sink = s._sink - if source in units and sink not in units: - outs.append(s) - elif sink in units and source not in units: - ins.append(s) - return ins, outs - -def minimal_digraph(ID, units, streams, **graph_attrs): - ins, outs = get_section_inlets_and_outlets(units, streams) - product = Stream(None) - product._ID = '' - feed = Stream(None) - feed._ID = '' - feed_box = bst.units.DiagramOnlyStreamUnit('\n'.join([i.ID for i in ins]), - None, feed) - product_box = bst.units.DiagramOnlyStreamUnit('\n'.join([i.ID for i in outs]), - product, None) - system_box = bst.units.DiagramOnlySystemUnit(ID, feed, product) - return digraph_from_units((feed_box, system_box, product_box), - **graph_attrs) - -def surface_digraph(path): - surface_units = set() - old_unit_connections = set() - isa = isinstance - Unit = bst.Unit - for i in path: - if isa(i, Unit): - surface_units.add(i) - elif has_path(i): - update_surface_units(i.ID, i.streams, i.units, - surface_units, old_unit_connections) - f = digraph_from_units(surface_units) - for u, ins, outs in old_unit_connections: - u._ins[:] = ins - u._outs[:] = outs - return f - -def update_surface_units(ID, streams, units, surface_units, old_unit_connections): - outs = [] - ins = [] - feeds = [] - products = [] - StreamUnit = bst.units.DiagramOnlyStreamUnit - SystemUnit = bst.units.DiagramOnlySystemUnit - - for s in streams: - source = s._source - sink = s._sink - if source in units and sink not in units: - if sink: outs.append(s) - else: products.append(s) - u_io = (source, tuple(source.ins), tuple(source.outs)) - old_unit_connections.add(u_io) - elif sink in units and source not in units: - if source: ins.append(s) - else: feeds.append(s) - u_io = (sink, tuple(sink.ins), tuple(sink.outs)) - old_unit_connections.add(u_io) - - if len(feeds) > 1: - feed = Stream(None) - feed._ID = '' - surface_units.add(StreamUnit('\n'.join([i.ID for i in feeds]), - None, feed)) - ins.append(feed) - else: ins += feeds - - if len(products) > 1: - product = Stream(None) - product._ID = '' - surface_units.add(StreamUnit('\n'.join([i.ID for i in products]), - product, None)) - outs.append(product) - else: outs += products - - subsystem_unit = SystemUnit(ID, ins, outs) - surface_units.add(subsystem_unit) - -def digraph_from_units(units, **graph_attrs): - streams = get_streams_from_units(units) - return digraph_from_units_and_streams(units, streams, **graph_attrs) - -def digraph_from_units_and_streams(units, streams, **graph_attrs): - connections = get_all_connections(streams) - return digraph_from_units_and_connections(units, connections, **graph_attrs) - -def digraph_from_units_and_connections(units, connections, **graph_attrs): - f = blank_digraph(**graph_attrs) - update_digraph_from_units_and_connections(f, units, connections) - return f - -def update_digraph_from_units_and_connections(f: Digraph, units, connections): - # Set up unit nodes - unit_names = {} # Contains full description (ID and line) by unit - for u in units: - node = u.get_node() - f.node(**node) - unit_names[u] = node['name'] - add_connections(f, connections, unit_names) - -def get_stream_connection(stream): - source = stream._source - source_index = source._outs.index(stream) if source else None - sink = stream._sink - sink_index = sink._ins.index(stream) if sink else None - return Connection(source, source_index, stream, sink_index, sink) - -def get_all_connections(streams): - return {get_stream_connection(s) - for s in streams - if (s._source or s._sink)} - -def add_connection(f: Digraph, connection, unit_names=None): - source, source_index, stream, sink_index, sink = connection - if unit_names is None: - has_source = bool(source) - has_sink = bool(sink) - else: - has_source = source in unit_names - has_sink = sink in unit_names - - if stream: - # Make stream nodes / unit-stream edges / unit-unit edges - if has_sink and not has_source: - # Feed stream case - f.node(stream.ID) - edge_in = sink._graphics.edge_in - f.attr('edge', arrowtail='none', arrowhead='none', - tailport='e', **edge_in[sink_index]) - f.edge(stream.ID, unit_names[sink]) - elif has_source and not has_sink: - # Product stream case - f.node(stream.ID) - edge_out = source._graphics.edge_out - f.attr('edge', arrowtail='none', arrowhead='none', - headport='w', **edge_out[source_index]) - f.edge(unit_names[source], stream.ID) - elif has_sink and has_source: - # Process stream case - edge_in = sink._graphics.edge_in - edge_out = source._graphics.edge_out - f.attr('edge', arrowtail='none', arrowhead='normal', - **edge_in[sink_index], **edge_out[source_index]) - f.edge(unit_names[source], unit_names[sink], label=stream.ID) - else: - f.node(stream.ID) - elif has_sink and has_source: - # Missing process stream case - edge_in = sink._graphics.edge_in - edge_out = source._graphics.edge_out - f.attr('edge', arrowtail='none', arrowhead='normal', - **edge_in[sink_index], **edge_out[source_index]) - f.edge(unit_names[source], unit_names[sink], style='dashed') - -def add_connections(f: Digraph, connections, unit_names=None): - # Set attributes for graph and streams - f.attr('node', shape='rarrow', fillcolor='#79dae8', - style='filled', orientation='0', width='0.6', - height='0.6', color='black', peripheries='1') - f.attr('graph', splines='normal', overlap='orthoyx', - outputorder='edgesfirst', nodesep='0.15', maxiter='1000000') - f.attr('edge', dir='foward') - for connection in connections: - add_connection(f, connection, unit_names) - -def display_digraph(digraph, format): - if format == 'svg': - x = display.SVG(digraph.pipe(format=format)) - else: - x = display.Image(digraph.pipe(format='png')) - display.display(x) - -def save_digraph(digraph, file, format): - if '.' not in file: - file += '.' + format - img = digraph.pipe(format=format) - f = open(file, 'wb') - f.write(img) - f.close() - -def finalize_digraph(digraph, file, format): - if file: - save_digraph(digraph, file, format) - else: - display_digraph(digraph, format) \ No newline at end of file diff --git a/build/lib/biosteam/_digraph/widget.py b/build/lib/biosteam/_digraph/widget.py deleted file mode 100644 index 3462d4fd0..000000000 --- a/build/lib/biosteam/_digraph/widget.py +++ /dev/null @@ -1,351 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sat Mar 28 21:59:13 2020 - -@author: yoelr -""" -from warnings import warn -from PyQt5.QtWidgets import (QAction, QApplication, QFileDialog, - QLabel, QWidget, QMainWindow, QMenu, - QMessageBox, QScrollArea, QSizePolicy, - QGridLayout, QSizePolicy, QFrame) -from PyQt5.QtCore import QSize, QTimer, Qt -from PyQt5.QtGui import QPixmap, QPalette, QImage, QFont -from .digraph import (get_all_connections, - digraph_from_units_and_connections, - update_digraph_from_units_and_connections, - surface_digraph, - minimal_digraph) - -__all__ = ('FlowsheetWidget',) - -class FlowsheetWidget(QMainWindow): - - def __init__(self, flowsheet): - super().__init__() - type(flowsheet).widget = flowsheet - self.kind = 'thorough' - self._autorefresh = False - self.moveScale = 1 - self.scaleFactor = 1 - self.flowsheet = flowsheet - self.connections = () - self.refresh_rate = 1000 # once per second - - # Set main window - minsize = QSize(400, 200) - self.setMinimumSize(minsize) - self.flowsheetLabel = flowsheetLabel = QLabel() - flowsheetLabel.setAlignment(Qt.AlignCenter) - flowsheetLabel.setScaledContents(True) - flowsheetLabel.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - self.notificationLabel = QLabel() - self.lastScrollPosition = None - - # Scroll Area - scrollArea = QScrollArea(self) - scrollArea.setWidgetResizable(True) - scrollArea.setFocusPolicy(Qt.NoFocus) - - # Flowsheet content - self.content = content = QWidget(self) - scrollArea.setFrameStyle(QFrame.Panel | QFrame.Sunken) - scrollArea.ensureWidgetVisible(content) - content.setFocusPolicy(Qt.NoFocus) - - # Layout which will contain the flowsheet - self.gridLayout = gridLayout = QGridLayout() - gridLayout.addWidget(flowsheetLabel, 0, 0) - content.setLayout(gridLayout) - - scrollArea.setWidget(content) - self.scrollArea = scrollArea - self.setCentralWidget(scrollArea) - self.createActions() - self.createMenus() - self.refresh() - self.autorefresh = True - self.fitContents() - - from biosteam import Unit - Unit.IPYTHON_DISPLAY_UNIT_OPERATIONS = False - - def close(self): - super().close() - from biosteam import Unit - Unit.IPYTHON_DISPLAY_UNIT_OPERATIONS = True - - def moveUp(self): - bar = self.scrollArea.verticalScrollBar() - bar.setValue(bar.value() - self.moveScale * bar.singleStep()) - - def moveDown(self): - bar = self.scrollArea.verticalScrollBar() - bar.setValue(bar.value() + self.moveScale * bar.singleStep()) - - def moveLeft(self): - bar = self.scrollArea.horizontalScrollBar() - bar.setValue(bar.value() - self.moveScale * bar.singleStep()) - - def moveRight(self): - bar = self.scrollArea.horizontalScrollBar() - bar.setValue(bar.value() + self.moveScale * bar.singleStep()) - - def zoomIn(self): - self.scaleImage(1.10) - - def zoomOut(self): - self.scaleImage(0.9) - - def fitContents(self): - self.scaleFactor = 1.0 - flowsheetLabel = self.flowsheetLabel - flowsheetLabel.adjustSize() - label_width = flowsheetLabel.width() - label_height = flowsheetLabel.height() - min_width = label_width + 50 - min_height = label_height + 50 - size = self.size() - old_width = size.width() - width = max(min_width, old_width) - old_height = size.height() - height = max(min_height, old_height) - self.resize(width, height) - - def scaleImage(self, factor): - self.scaleFactor *= factor - self.flowsheetLabel.resize(self.scaleFactor * self.flowsheetLabel.pixmap().size()) - - self.adjustScrollBar(self.scrollArea.horizontalScrollBar(), factor) - self.adjustScrollBar(self.scrollArea.verticalScrollBar(), factor) - - self.zoomInAct.setEnabled(self.scaleFactor < 4.0) - self.zoomOutAct.setEnabled(self.scaleFactor > 0.25) - - def adjustScrollBar(self, scrollBar, factor): - value = int(factor * scrollBar.value() - + ((factor - 1) * scrollBar.pageStep()/2)) - scrollBar.setValue(value) - - def simulate(self): - self.refresh() - try: - self.system.simulate() - except Exception as Error: - text = (f"SIMULATION SUCCESSFUL!\n" - f"{type(self).__name__}: {Error}") - else: - text = "SIMULATION SUCCESSFUL!" - self.printNotification(text) - - def printNotification(self, text): - notificationLabel = self.notificationLabel - notificationLabel.setParent(self) - notificationLabel.setAlignment(Qt.AlignCenter) - notificationLabel.setAutoFillBackground(True) - notificationLabel.setText(text) - font = QFont("Times", 18, QFont.Bold) - notificationLabel.setFont(font) - self.gridLayout.addWidget(notificationLabel, 0, 0) - self.centerScrollArea() - qtimer = QTimer() - qtimer.singleShot(2000, self.removeNotification) - - def centerScrollArea(self): - scrollArea = self.scrollArea - hbar = scrollArea.horizontalScrollBar() - vbar = scrollArea.verticalScrollBar() - self.lastScrollPosition = (vbar.value(), hbar.value()) - vbar = scrollArea.verticalScrollBar() - hbar = scrollArea.horizontalScrollBar() - vbar.setValue((vbar.minimum() + vbar.maximum()) / 2) - hbar.setValue((hbar.minimum() + hbar.maximum()) / 2) - - def removeNotification(self): - notificationLabel = self.notificationLabel - notificationLabel.setParent(None) - lastScrollPosition = self.lastScrollPosition - if lastScrollPosition: - vval, hval = lastScrollPosition - scrollArea = self.scrollArea - vbar = scrollArea.verticalScrollBar() - vbar.setValue(vval) - hbar = scrollArea.horizontalScrollBar() - hbar.setValue(hval) - self.lastScrollPosition = None - - def minimalDiagram(self): - self.kind = 'minimal' - self.refresh(True) - - def surfaceDiagram(self): - self.kind = 'surface' - self.refresh(True) - - def thoroughDiagram(self): - self.kind = 'thorough' - self.refresh(True) - - def createActions(self): - self.exitAct = QAction("E&xit", self, shortcut="Ctrl+Q", - triggered=self.close) - self.refreshAct = QAction("&Refresh", self, shortcut="Ctrl+R", - triggered=self.refresh) - self.zoomInAct = QAction("Zoom &in (10%)", self, shortcut="Ctrl++", - enabled=True, triggered=self.zoomIn) - self.zoomOutAct = QAction("Zoom &out (10%)", self, shortcut="Ctrl+-", - enabled=True, triggered=self.zoomOut) - self.moveUpAct = QAction("Move &up", self, shortcut="Up", - triggered=self.moveUp) - self.moveDownAct = QAction("Move &down", self, shortcut="Down", - triggered=self.moveDown) - self.moveLeftAct = QAction("Move &left", self, shortcut="Left", - triggered=self.moveLeft) - self.moveRightAct = QAction("Move &right", self, shortcut="Right", - triggered=self.moveRight) - self.fitContentsAct = QAction("Fit contents", self, shortcut="Ctrl+F", - triggered=self.fitContents) - self.simulateAct = QAction("&Simulate", self, shortcut="Shift+Return", - triggered=self.simulate) - self.removeNotificationAct = QAction("&Remove notification", self, shortcut="Space", - triggered=self.removeNotification) - self.thoroughDiagramAct = QAction("Thorough &diagram", self, shortcut="Shift+T", - triggered=self.thoroughDiagram) - self.surfaceDiagramAct = QAction("Surface &diagram", self, shortcut="Shift+S", - triggered=self.surfaceDiagram) - self.minimalDiagramAct = QAction("Minimal &diagram", self, shortcut="Shift+M", - triggered=self.minimalDiagram) - self.actions = [self.exitAct, - self.refreshAct, - self.zoomInAct, - self.zoomOutAct, - self.moveUpAct, - self.moveDownAct, - self.moveLeftAct, - self.moveRightAct, - self.fitContentsAct, - self.simulateAct, - self.removeNotificationAct, - self.thoroughDiagramAct, - self.surfaceDiagramAct, - self.minimalDiagramAct] - - def createMenus(self): - self.fileMenu = fileMenu = QMenu("&File", self) - fileMenu.addSeparator() - fileMenu.addAction(self.exitAct) - - self.viewMenu = viewMenu = QMenu("&View", self) - viewMenu.addAction(self.refreshAct) - viewMenu.addAction(self.fitContentsAct) - viewMenu.addSeparator() - viewMenu.addAction(self.zoomInAct) - viewMenu.addAction(self.zoomOutAct) - viewMenu.addSeparator() - viewMenu.addAction(self.moveUpAct) - viewMenu.addAction(self.moveDownAct) - viewMenu.addAction(self.moveLeftAct) - viewMenu.addAction(self.moveRightAct) - viewMenu.addSeparator() - viewMenu.addAction(self.thoroughDiagramAct) - viewMenu.addAction(self.surfaceDiagramAct) - viewMenu.addAction(self.minimalDiagramAct) - - - self.simulationMenu = simulationMenu = QMenu("&Simulation", self) - simulationMenu.addAction(self.simulateAct) - - menuBar = self.menuBar() - menuBar.addMenu(fileMenu) - menuBar.addMenu(viewMenu) - menuBar.addMenu(simulationMenu) - - @property - def title(self): - flowsheet = self.flowsheet - ID = flowsheet.ID - ID = ID.replace('_', ' ').capitalize() - return f"{flowsheet.line} - {ID}" - - @property - def autorefresh(self): - return self._autorefresh - @autorefresh.setter - def autorefresh(self, autorefresh): - autorefresh = bool(autorefresh) - if autorefresh and not self._autorefresh: - self.qtimer = qtimer = QTimer(self) - qtimer.timeout.connect(self.refresh) - qtimer.start(self.refresh_rate) - elif not autorefresh and self._autorefresh: - qtimer = self.qtimer - qtimer.stop() - self._autorefresh = autorefresh - - def get_digraph(self, system, flowsheet, connections): - kind = self.kind - if kind == 'thorough': - digraph = digraph_from_units_and_connections(flowsheet.unit, - connections) - elif kind == 'surface': - digraph = surface_digraph(system.path) - streams = flowsheet.stream.to_set() - units = flowsheet.unit.to_set() - other_units = units.difference(system.units) - other_streams = streams.difference(system.streams) - other_connections = get_all_connections(other_streams) - update_digraph_from_units_and_connections(digraph, - other_units, - other_connections) - elif kind == 'minimal': - digraph = minimal_digraph(flowsheet.ID, - flowsheet.unit, - flowsheet.stream) - else: - raise RuntimeError("no diagram checked") - return digraph - - def refresh(self, force_refresh=False): - self.setWindowTitle(self.title) - flowsheet = self.flowsheet - connections = get_all_connections(flowsheet.stream) - if force_refresh or self.connections != connections: - self.system = system = flowsheet.create_system(flowsheet.ID) - self.connections = connections - digraph = self.get_digraph(system, flowsheet, connections) - img_data = digraph.pipe('png') - img = QImage.fromData(img_data) - pixmap = QPixmap.fromImage(img) - if not pixmap.isNull(): - self.flowsheetLabel.setPixmap(pixmap) - - - -# def moveUpLeft(self): -# self.moveUp() -# self.moveLeft() - -# def moveUpRight(self): -# self.moveUp() -# self.moveRight() - -# def moveDownLeft(self): -# self.moveDown() -# self.moveLeft() - -# def moveDownRight(self): -# self.moveDown() -# self.moveRight() - -# viewMenu.addAction(self.moveUpRightAct) -# viewMenu.addAction(self.moveUpLeftAct) -# viewMenu.addAction(self.moveDownLeftAct) -# viewMenu.addAction(self.moveDownRightAct) -# self.moveUpLeftAct = QAction("&Move up left", self, shortcut="Up+Left", -# triggered=self.moveUpLeft) -# self.moveUpRightAct = QAction("&Move up right", self, shortcut="Up+Right", -# triggered=self.moveUpRight) -# self.moveDownLeftAct = QAction("&Move down left", self, shortcut="Down+Left", -# triggered=self.moveDownLeft) -# self.moveDownRightAct = QAction("&Move down right", self, shortcut="Down+Right", -# triggered=self.moveDownRight) \ No newline at end of file diff --git a/build/lib/biosteam/_facility.py b/build/lib/biosteam/_facility.py deleted file mode 100644 index ef383e295..000000000 --- a/build/lib/biosteam/_facility.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sat Jul 13 02:24:35 2019 - -@author: yoelr -""" -from ._unit import Unit - -__all__ = ('Facility',) - -def get_network_priority(facility): - return facility.network_priority - -class Facility(Unit, isabstract=True, - new_graphics=False): - - @staticmethod - def ordered_facilities(facilities): - """Return facilitied ordered according to their network priority.""" - return sorted(facilities, key=get_network_priority) - - def __init_subclass__(cls, - isabstract=False, - new_graphics=True): - super().__init_subclass__(isabstract, - new_graphics) - if not hasattr(cls, 'network_priority'): - raise NotImplementedError('Facility subclasses must implement a ' - '`network_priority` attribute to designate ' - 'the order of simulation relative to other ' - 'facilities') - - @property - def system(self): - return self._system - diff --git a/build/lib/biosteam/_flowsheet.py b/build/lib/biosteam/_flowsheet.py deleted file mode 100644 index ada1e5e57..000000000 --- a/build/lib/biosteam/_flowsheet.py +++ /dev/null @@ -1,295 +0,0 @@ -# -*- coding: utf-8 -*- -""" -As BioSTEAM objects are created, they are automatically registered. The `main_flowsheet` object allows the user to find any Unit, Stream or System instance. When `main_flowsheet` is called, it simply looks up the item and returns it. -""" -from thermosteam.utils import Registry -from ._digraph import (FlowsheetWidget, - digraph_from_units, - digraph_from_units_and_streams, - finalize_digraph, - minimal_digraph, - update_surface_units) -from thermosteam import Stream -from ._unit import Unit -from ._facility import Facility -from ._system import System -from ._network import Network - -__all__ = ('main_flowsheet', 'Flowsheet') - -# %% Functions - -def get_feeds_from_streams(streams): - isa = isinstance - return [i for i in streams if i._sink and not - (i._source or isa(i._sink, Facility))] - -def sort_feeds_big_to_small(feeds): - feeds.sort(key=lambda feed: -feed.F_mass) - - -# %% Flowsheet search - -class Flowsheets: - __getattribute__ = __getitem__ = object.__getattribute__ - - def __setattr__(self, key, value): - raise TypeError(f"'{type(self).__name__}' object does not support attribute assignment") - __setitem__ = __setattr__ - - def __iter__(self): - yield from self.__dict__.values() - - def __delattr__(self, key): - if key == main_flowsheet.ID: - raise AttributeError('cannot delete main flowsheet') - else: - super().__delattr__(key) - - def __repr__(self): - return f'Register:\n ' + '\n '.join([repr(i) for i in self]) - - -class Flowsheet: - """ - Create a Flowsheet object which stores references to all stream, unit, - and system objects. For a tutorial on flowsheets, visit - :doc:`tutorial/Managing_flowsheets`. - """ - line = "Flowsheet" - - #: [Register] All flowsheets. - flowsheet = Flowsheets() - - def __new__(cls, ID): - self = super().__new__(cls) - - #: [Register] Contains all System objects as attributes. - self.system = Registry() - - #: [Register] Contains all Unit objects as attributes. - self.unit = Registry() - - #: [Register] Contains all Stream objects as attributes. - self.stream = Registry() - - #: [str] ID of flowsheet. - self._ID = ID - self.flowsheet.__dict__[ID] = self - return self - - def __reduce__(self): - return self.from_registries, self.registries - - def __setattr__(self, key, value): - if hasattr(self, '_ID'): - raise TypeError(f"'{type(self).__name__}' object does not support attribute assignment") - else: - super().__setattr__(key, value) - - def view(self): - """ - Create an interactive process flowsheet diagram - that autorefreshes itself. - """ - widget = FlowsheetWidget(self) - widget.show() - return widget - - @property - def ID(self): - return self._ID - - @classmethod - def from_registries(cls, stream, unit, system): - flowsheet = super().__new__(cls) - flowsheet.stream = stream - flowsheet.unit = unit - flowsheet.system = system - return flowsheet - - @property - def registries(self): - return (self.stream, self.unit, self.system) - - def discard(self, ID): - for registry in self.registries: - if ID in registry: - registry.discard(ID) - break - - def update(self, flowsheet): - for registry, other_registry in zip(self.registries, flowsheet.registries): - registry.__dict__.update(other_registry.__dict__) - - @classmethod - def from_flowsheets(cls, ID, flowsheets): - """Return a new flowsheet with all registered objects from the given flowsheets.""" - new = cls(ID) - isa = isinstance - for flowsheet in flowsheets: - if isa(flowsheet, str): - flowsheet = cls.flowsheet[flowsheet] - new.update(flowsheet) - return new - - def diagram(self, kind='surface', file=None, format='svg', **graph_attrs): - """Display all units and attached streams. - - Parameters - ---------- - kind='surface' : Must be one of the following - * **'thorough':** Thoroughly display every unit. - * **'surface':** Display units and recycle systems. - * **'minimal':** Minimally display flowsheet as a box with feeds and products. - - """ - if kind == 'thorough': - f = digraph_from_units_and_streams(self.unit, self.stream, - format=format, **graph_attrs) - elif kind == 'surface': - f = self._surface_digraph(format, **graph_attrs) - elif kind == 'minimal': - f = minimal_digraph(self.ID, self.units, self.streams, **graph_attrs) - else: - raise ValueError(f"kind must be either 'thorough', 'surface', or 'minimal'.") - finalize_digraph(f, file, format) - - def _surface_digraph(self, format, **graph_attrs): - surface_units = set(self.unit) - old_unit_connections = set() - for i in self.system: - if i.recycle and not any(sub.recycle for sub in i.subsystems): - surface_units.difference_update(i.units) - update_surface_units(i.ID, i.streams, i.units, - surface_units, old_unit_connections) - - f = digraph_from_units(surface_units) - for u, ins, outs in old_unit_connections: - u._ins[:] = ins - u._outs[:] = outs - return f - - def create_system(self, ID="", feeds=None, ends=()): - """ - Create a System object from all units and streams defined in the flowsheet. - - Parameters - ---------- - ID : str, optional - Name of system. - ends : Iterable[:class:`~thermosteam.Stream`] - End streams of the system which are not products. Specify this argument - if only a section of the system is wanted, or if recycle streams should be - ignored. - - """ - feeds = get_feeds_from_streams(self.stream) - if feeds: - sort_feeds_big_to_small(feeds) - feedstock, *feeds = feeds - facilities = self.get_facilities() - system = System.from_feedstock(ID, feedstock, feeds, - facilities, ends) - else: - system = System(ID, ()) - return system - - def create_network(self, feeds=None, ends=()): - """ - Create a Network object from all units and streams defined in the flowsheet. - - Parameters - ---------- - ends : Iterable[:class:`~thermosteam.Stream`] - End streams of the system which are not products. Specify this argument - if only a section of the system is wanted, or if recycle streams should be - ignored. - - """ - feeds = get_feeds_from_streams(self.stream) - if feeds: - sort_feeds_big_to_small(feeds) - feedstock, *feeds = feeds - network = Network.from_feedstock(feedstock, feeds, ends) - else: - network = Network([]) - return network - - def create_path(self, feeds=None, ends=()): - isa = isinstance - network = self.create_network(feeds, ends) - net2sys = System.from_network - return tuple([(net2sys('', i) if isa(i, Network) else i) - for i in network.path]) - - def get_facilities(self): - isa = isinstance - return [i for i in self.unit if isa(i, Facility)] - - def __call__(self, ID): - """ - Return requested biosteam item. - - Parameters - ---------- - ID : str - ID of the requested item. - - """ - ID = ID.replace(' ', '_') - obj = (self.stream.search(ID) - or self.unit.search(ID) - or self.system.search(ID)) - if not obj: raise LookupError(f"no registered item '{ID}'") - return obj - - def __str__(self): - return self.ID - - def __repr__(self): - return f'<{type(self).__name__}: {self.ID}>' - - -class MainFlowsheet(Flowsheet): - """ - Create a MainFlowsheet object which automatically registers - biosteam objects as they are created. For a tutorial on flowsheets, - visit :doc:`tutorial/Managing_flowsheets`. - """ - line = "Main flowsheet" - - def set_flowsheet(self, flowsheet): - """Set main flowsheet that is updated with new biosteam objects.""" - if isinstance(flowsheet, Flowsheet): - dct = flowsheet.__dict__ - elif isinstance(flowsheet, str): - if flowsheet in self.flowsheet: - dct = main_flowsheet.flowsheet[flowsheet].__dict__ - else: - new_flowsheet = Flowsheet(flowsheet) - self.flowsheet.__dict__[flowsheet] = new_flowsheet - dct = new_flowsheet.__dict__ - else: - raise TypeError('flowsheet must be a Flowsheet object') - Stream.registry = dct['stream'] - System.registry = dct['system'] - Unit.registry = dct['unit'] - object.__setattr__(self, '__dict__', dct) - - def get_flowsheet(self): - return self.flowsheet[self.ID] - - __setattr__ = Flowsheets.__setattr__ - - def __new__(cls, ID): - main_flowsheet.set_flowsheet(ID) - return main_flowsheet - - def __repr__(self): - return f'<{type(self).__name__}: {self.ID}>' - - -#: [main_flowsheet] Main flowsheet where objects are registered by ID. -main_flowsheet = object.__new__(MainFlowsheet) -main_flowsheet.set_flowsheet('default') diff --git a/build/lib/biosteam/_graphics.py b/build/lib/biosteam/_graphics.py deleted file mode 100644 index e1302a478..000000000 --- a/build/lib/biosteam/_graphics.py +++ /dev/null @@ -1,190 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sat Aug 18 14:36:58 2018 - -@author: yoelr -""" -from .utils import colors - -__all__ = ('UnitGraphics', - 'box_graphics', - 'mixer_graphics', - 'splitter_graphics', - 'vertical_column_graphics', - 'vertical_vessel_graphics', - 'utility_heat_exchanger_graphics', - 'process_heat_exchanger_graphics', - 'process_specification_graphics', - 'system_unit', - 'stream_unit', - 'junction_graphics') - -# %% Base class for unit graphics - -class UnitGraphics: - """Create a UnitGraphics object that contains specifications for - Graphviz node and edge styles.""" - __slots__ = ('node', 'edge_in', 'edge_out', 'taylor_node_to_unit') - - def __init__(self, edge_in, edge_out, node, taylor_node_to_unit=None): - # [dict] Input stream edge settings - self.edge_in = edge_in - - # [dict] Output stream edge settings - self.edge_out = edge_out - - #: [dict] Node settings - self.node = node - - # [function(node, unit)] Taylor node to unit. - self.taylor_node_to_unit = taylor_node_to_unit - - @classmethod - def box(cls, N_ins, N_outs): - edge_in = [{'headport': 'c'} for i in range(N_ins)] - edge_out = [{'tailport': 'c'} for i in range(N_outs)] - return cls(edge_in, edge_out, box_node) - - def get_node_taylored_to_unit(self, unit): - """Return node taylored to unit specifications""" - node = self.node - node['name'] = unit.ID + '\n' + unit.line - taylor_node_to_unit = self.taylor_node_to_unit - if taylor_node_to_unit: - taylor_node_to_unit(node, unit) - return node - - def __repr__(self): - return f'{type(self).__name__}(node={self.node}, edge_in={self.edge_in}, edge_out={self.edge_out})' - - -# %% UnitGraphics components - -single_edge_in = ({'headport': 'c'},) -single_edge_out = ({'tailport': 'c'},) -multi_edge_in = 20 * single_edge_in -multi_edge_out = 20 * single_edge_out -right_edge_out = ({'tailport': 'e'},) -left_edge_in = ({'headport': 'w'},) -top_bottom_edge_out = ({'tailport': 'n'}, {'tailport': 's'}) - -box_node = {'shape': 'box', - 'fillcolor': "white:#CDCDCD", - 'style': 'filled', - 'gradientangle': '0', - 'width': '0.6', - 'height': '0.6', - 'orientation': '0.0', - 'color': 'black', - 'peripheries': '1', - 'margin': 'default'} - -box_graphics = UnitGraphics(single_edge_in, single_edge_out, box_node) - - -# %% All graphics objects used in BioSTEAM - -# Create mixer graphics -node = box_node.copy() -node['shape'] = 'triangle' -node['orientation'] = '270' -mixer_graphics = UnitGraphics(6 * single_edge_in, right_edge_out, node) - -# Create splitter graphics -node = box_node.copy() -node['shape'] = 'triangle' -node['orientation'] = '90' -node['fillcolor'] = "#bfbfbf:white" -splitter_graphics = UnitGraphics(left_edge_in, 6 * single_edge_out, node) - -# Create distillation column graphics -node = box_node.copy() -node['width'] = '1' -node['height'] = '1.2' -vertical_column_graphics = UnitGraphics(single_edge_in, top_bottom_edge_out, node) - -# Create flash column graphics -node = node.copy() -node['height'] = '1.1' -vertical_vessel_graphics = UnitGraphics(single_edge_in, top_bottom_edge_out, node) - -# Single stream heat exchanger node -node = box_node.copy() -node['shape'] = 'circle' -node['color'] = 'none' -node['margin'] = '0' -def taylor_utility_heat_exchanger_node(node, unit): - try: - si = unit.ins[0] - so = unit.outs[0] - H_in = si.H - H_out = so.H - if H_in > H_out: - node['fillcolor'] = '#cfecf0' - node['gradientangle'] = '0' - line = 'Cooling' - elif H_in < H_out: - node['gradientangle'] = '0' - node['fillcolor'] = '#fad6d8' - line = 'Heating' - else: - node['gradientangle'] = '90' - node['fillcolor'] = '#cfecf0:#fad6d8' - line = 'Heat exchanger' - except: - line = 'Heat exchanger' - node['name'] = unit.ID + "\n" + line - -utility_heat_exchanger_graphics = UnitGraphics(single_edge_in, single_edge_out, node, - taylor_utility_heat_exchanger_node) - -# Process heat exchanger network -node = node.copy() -node['shape'] = 'circle' -node['color'] = 'none' -node['margin'] = '0' -node['gradientangle'] = '90' -node['fillcolor'] = '#cfecf0:#fad6d8' -def taylor_process_heat_exchanger_node(node, unit): - node['name'] = unit.ID + "\n Heat exchanger" - -process_heat_exchanger_graphics = UnitGraphics(2 * single_edge_in, 2 *single_edge_out, node, - taylor_process_heat_exchanger_node) - -# Process specification graphics -orange = colors.orange_tint.tint(50) -orange_tint = orange.tint(75) -node = box_node.copy() -node['fillcolor'] = orange_tint.HEX + ':' + orange.HEX -node['shape'] = 'note' -node['margin'] = '0.2' -def taylor_process_specification_node(node, unit): - node['name'] = (f"{unit.ID} - {unit.description}\n" - f"{unit.line}") - -process_specification_graphics = UnitGraphics(single_edge_in, single_edge_out, node, - taylor_process_specification_node) - -# System unit for creating diagrams -node = box_node.copy() -node['peripheries'] = '2' -system_unit = UnitGraphics(multi_edge_in, multi_edge_out, node) - -node = box_node.copy() -node['fillcolor'] = 'white:#79dae8' -stream_unit = UnitGraphics(multi_edge_in, multi_edge_out, node) - - -node = box_node.copy() -def taylor_junction_node(node, unit): - if not any(unit._get_streams()): - node['fontsize'] = '18' - node['shape'] = 'plaintext' - node['fillcolor'] = 'none' - else: - node['width'] = '0.1' - node['shape'] = 'point' - node['color'] = node['fillcolor'] = 'black' - -junction_graphics = UnitGraphics(single_edge_in, single_edge_out, node, - taylor_junction_node) \ No newline at end of file diff --git a/build/lib/biosteam/_heat_utility.py b/build/lib/biosteam/_heat_utility.py deleted file mode 100644 index 0d4dd3d6c..000000000 --- a/build/lib/biosteam/_heat_utility.py +++ /dev/null @@ -1,527 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sat Aug 18 14:25:34 2018 - -@author: yoelr -""" -from thermosteam.base.units_of_measure import convert, DisplayUnits -from thermosteam import Thermo, Stream, ThermalCondition - -__all__ = ('HeatUtility', 'UtilityAgent') - -# Costs from Table 17.1 in Warren D. Seider et al.-Product and Process Design Principles_ Synthesis, Analysis and Evaluation-Wiley (2016) -# ^This table was made using data from Busche, 1995 -# Entry temperature conditions of coolants taken from Table 12.1 in Warren, 2016 - - -# %% Utility agents - -class UtilityAgent(Stream): - """ - Create a UtilityAgent object that defines a utility option. - - Parameters - ---------- - ID='' : str - A unique identification. If ID is None, utility agent will not be registered. - If no ID is given, utility agent will be registered with a unique ID. - flow=() : tuple - All flow rates corresponding to chemical `IDs`. - phase='l' : 'l', 'g', or 's' - Either gas (g), liquid (l), or solid (s). - T=298.15 : float - Temperature [K]. - P=101325 : float - Pressure [Pa]. - units='kmol/hr' : str - Flow rate units of measure (only mass, molar, and - volumetric flow rates are valid). - thermo=None : Thermo - Thermodynamic equilibrium package. Defaults to `biosteam.settings.get_thermo()`. - T_limit : float, optional - Temperature limit of outlet utility streams [K]. If no limit is given, - phase change is assumed. If utility agent heats up, `T_limit` is - the maximum temperature. If utility agent cools down, `T_limit` is - the minumum temperature. - heat_transfer_price : float - Price of transfered heat [USD/kJ]. - regeneration_price : float - Price of regenerating the fluid for reuse [USD/kmol]. - heat_transfer_efficiency : float - Fraction of heat transfered accounting for losses to the environment (must be between 0 to 1). - **chemical_flows : float - ID - flow pairs. - - """ - __slots__ = ('T_limit', '_heat_transfer_price', - '_regeneration_price', 'heat_transfer_efficiency') - def __init__(self, ID='', flow=(), phase='l', T=298.15, P=101325., units='kmol/hr', - thermo=None, T_limit=None, heat_transfer_price=0.0, - regeneration_price=0.0, heat_transfer_efficiency=1.0, - **chemical_flows): - self._TP = ThermalCondition(T, P) - thermo = self._load_thermo(thermo) - self._init_indexer(flow, phase, thermo.chemicals, chemical_flows) - if units != 'kmol/hr': - name, factor = self._get_flow_name_and_factor(units) - flow = getattr(self, name) - flow[:] = self.mol / factor - self._sink = self._source = None - self._init_cache() - self._register(ID) - self.T_limit = T_limit - self.heat_transfer_price = heat_transfer_price - self.regeneration_price = regeneration_price - self.heat_transfer_efficiency = heat_transfer_efficiency - - def to_stream(self, ID=None): - """ - Return a copy as a :class:`~thermosteam.Stream` object. - - Examples - -------- - >>> import biosteam as bst - >>> bst.settings.set_thermo(['Water', 'Ethanol']) - >>> cooling_water = bst.HeatUtility.get_agent('cooling_water') - >>> cooling_water_copy = cooling_water.to_stream('cooling_water_copy') - >>> cooling_water_copy.show(flow='kg/hr') - Stream: cooling_water_copy - phase: 'l', T: 305.37 K, P: 101325 Pa - flow (kg/hr): Water 18 - - """ - new = Stream.__new__(Stream) - new._sink = new._source = None - new._thermo = self._thermo - new._imol = self._imol.copy() - new._TP = self._TP.copy() - new._init_cache() - new.price = 0 - new.ID = ID - return new - - @property - def cost(self): - return 0 - @property - def price(self): - return 0 - - @property - def heat_transfer_price(self): - """Price of transfered heat [USD/kJ].""" - return self._heat_transfer_price - @heat_transfer_price.setter - def heat_transfer_price(self, price): - assert price >= 0, "heat transfer price cannot be negative" - self._heat_transfer_price = price - - @property - def regeneration_price(self): - """Price of regenerating the fluid for reuse [USD/kmol].""" - return self._regeneration_price - @regeneration_price.setter - def regeneration_price(self, price): - assert price >= 0, "regeneration price cannot be negative" - self._regeneration_price = price - - def _info_phaseTP(self, phase, T_units, P_units): - T_limit = convert(self.T_limit, 'K', T_units) - T = convert(self.T, 'K', T_units) - P = convert(self.P, 'Pa', P_units) - s = '' if isinstance(phase, str) else 's' - ht_price = self.heat_transfer_price - rg_price = self.regeneration_price - ht_eff = self.heat_transfer_efficiency - return (f" phase{s}: {repr(phase)}, T: {T:.5g} {T_units}, P: {P:.6g} {P_units}\n" - f" heat_transfer_price: {ht_price:.3g} USD/kJ\n" - f" regeneration_price: {rg_price:.3g} USD/kmol\n" - f" heat_transfer_efficiency: {ht_eff:.3f}\n" - " T_limit: " + (f"{T_limit:.3g} K\n" if T_limit else "None\n")) - - def __repr__(self): - return f"<{type(self).__name__}: {self.ID}>" - - -# Create cooling agnets -thermo_water = Thermo(['Water']) -cooling_water = UtilityAgent('cooling_water', - Water=1, T=305.372, P=101325, - thermo=thermo_water, - T_limit = 324.817, - regeneration_price = 4.8785e-4) -chilled_water = UtilityAgent('chilled_water', - Water=1, T=280.372, P=101325, - thermo=thermo_water, - T_limit = 300.372, - heat_transfer_price = 5e-6) -chilled_brine = UtilityAgent('chilled_brine', - Water=1, T=255.372, P=101325, - thermo=thermo_water, - T_limit = 275.372, - heat_transfer_price = 8.145e-6,) - -# Create heating agents -low_pressure_steam = UtilityAgent('low_pressure_steam', - Water=1, T=411.494, P=344738.0, phase='g', - thermo=thermo_water, - regeneration_price = 0.2378, - heat_transfer_efficiency = 0.95) -medium_pressure_steam = UtilityAgent('medium_pressure_steam', - Water=1, T=454.484, P=1034214.0, phase='g', - thermo=thermo_water, - regeneration_price = 0.2756, - heat_transfer_efficiency = 0.90) -high_pressure_steam = UtilityAgent('high_pressure_steam', - Water=1, T=508.858, P=3102642.0, phase='g', - thermo=thermo_water, - regeneration_price = 0.3171, - heat_transfer_efficiency = 0.85) - -# %% - -class HeatUtility: - """ - Create an HeatUtility object that can choose a utility stream and - calculate utility requirements. It can calculate required flow rate, - temperature change, or phase change of utility. Calculations assume - counter current flow rate. - - Parameters - ---------- - heat_transfer_efficiency=None : float, optional - Enforced fraction of heat transfered from utility (due - to losses to environment). - - Attributes - ---------- - cooling_agents : list[UtilityAgent] - All cooling utilities available. - heating_agents : list[UtilityAgent] - All heating utilities available. - dT : float - Maximum temperature approach of utilities. - duty : float - Energy transfered from utility to the process [kJ/hr]. - flow : float - Flow rate of utility [kmol/hr]. - cost : float - Cost of utility [USD/hr]. - heat_transfer_efficiency : float - Enforced fraction of heat transfered from utility (due - to losses to environment). - T_pinch : float - If cooling, the minimum utility temperature required. - If heating, the maximum utility temperature required. - iscooling : float - Whether the utility is cooling the process. - agent : UtilityAgent - Utility agent being used. - inlet_utility_stream : :class:`~thermosteam.Stream` - outlet_utility_stream : :class:`~thermosteam.Stream` - - Examples - -------- - Create a heat utility: - - .. code-block:: python - - >>> from biosteam import HeatUtility - >>> hu = HeatUtility() - >>> hu.show() - HeatUtility: None - duty: 0 - flow: 0 - cost: 0 - - Calculate utility requirement by calling it with a duty (kJ/hr), and entrance and exit temperature (K): - - .. code-block:: python - - >>> hu(1000, 300, 350) - >>> hu.show() - HeatUtility: low_pressure_steam - duty: 1.05e+03 kJ/hr - flow: 0.0271 kmol/hr - cost: 0.00644 USD/hr - - All results are accessible: - - .. code-block:: python - - >>> hu.ID, hu.duty, hu.flow, hu.cost - ('low_pressure_steam', 1052.6315789473686, 0.027090382090408212, 0.006442092861099073) - - """ - __slots__ = ('inlet_utility_stream', 'outlet_utility_stream', 'duty', - 'flow', 'cost', 'heat_transfer_efficiency', 'T_pinch', - 'iscooling', 'agent') - dT = 5 #: [float] Pinch temperature difference - - #: [DisplayUnits] Units of measure for IPython display - display_units = DisplayUnits(duty='kJ/hr', flow='kmol/hr', cost='USD/hr') - - cooling_agents = [cooling_water, chilled_water, chilled_brine] - heating_agents = [low_pressure_steam, medium_pressure_steam, high_pressure_steam] - - def __init__(self, heat_transfer_efficiency=None): - self.heat_transfer_efficiency = heat_transfer_efficiency - self.empty() - - def __bool__(self): - return bool(self.agent) - - @property - def ID(self): - """[str] ID of utility agent being used.""" - agent = self.agent - return agent.ID if agent else "" - @ID.setter - def ID(self, ID): - """[str] ID of utility agent being used.""" - self.agent = self.get_agent(ID) - - def copy_like(self, other): - """Copy all data from another heat utility.""" - self.inlet_utility_stream = other.inlet_utility_stream.copy() - self.outlet_utility_stream = other.outlet_utility_stream.copy() - self.flow = other.flow - self.duty = other.duty - self.cost = other.cost - self.heat_transfer_efficiency = other.heat_transfer_efficiency - self.T_pinch = other.T_pinch - self.iscooling = other.iscooling - self.agent = other.agent - - def scale(self, factor): - """Scale utility data.""" - self.flow *= factor - self.duty *= factor - self.cost *= factor - self.inlet_utility_stream.mol *= factor - # No need to factor the outlet utility stream - # because it shares the same flow rate data as the inlet - - def empty(self): - """Remove utility requirements.""" - self.cost = self.flow = self.duty = 0 - self.iscooling = self.agent = self.T_pinch = None - - def __call__(self, duty, T_in, T_out=None, agent=None): - """Calculate utility requirements given the essential parameters. - - Parameters - ---------- - duty : float - Unit duty requirement (kJ/hr) - T_in : float - Inlet process stream temperature (K) - T_out : float, optional - Outlet process stream temperature (K) - agent : UtilityAgent, optional - Utility agent to use. Defaults to a suitable agent from - predefined heating/cooling utility agents. - - """ - if duty == 0: - self.empty() - return - T_out = T_out or T_in - iscooling = duty < 0 - - # Note: These are pinch temperatures at the utility inlet and outlet. - # Not to be confused with the inlet and outlet of the process stream. - T_pinch_in, T_pinch_out = self.get_inlet_and_outlet_T_pinch(iscooling, T_in, T_out) - - ## Select heat transfer agent ## - if agent: - self.load_agent(agent) - elif self.iscooling == iscooling and self.T_pinch == T_pinch_in: - agent = self.agent - else: - self.iscooling = iscooling - self.T_pinch = T_pinch_in - if iscooling: - agent = self.get_suitable_cooling_agent(T_pinch_in) - else: - agent = self.get_suitable_heating_agent(T_pinch_in) - self.load_agent(agent) - - ## Calculate utility requirement ## - heat_transfer_efficiency = self.heat_transfer_efficiency or agent.heat_transfer_efficiency - duty = duty/heat_transfer_efficiency - if agent.T_limit: - # Temperature change - self.outlet_utility_stream.T = self.get_T_outlet(T_pinch_out, agent.T_limit, iscooling) - dH = self.inlet_utility_stream.H - self.outlet_utility_stream.H - else: - # Phase change - self.outlet_utility_stream.phase = 'g' if self.inlet_utility_stream.phase == 'l' else 'l' - dH = self.outlet_utility_stream.Hvap - - # Update utility flow - self.outlet_utility_stream.mol[:] *= duty / dH - - # Update and return results - self.flow = F_mol = self.inlet_utility_stream.F_mol - self.duty = duty - self.cost = agent._heat_transfer_price * abs(duty) + agent._regeneration_price * F_mol - - @classmethod - def get_agent(cls, ID): - """Return utility agent with given ID.""" - for agent in cls.heating_agents + cls.cooling_agents: - if agent.ID == ID: return agent - raise KeyError(ID) - - @classmethod - def get_heating_agent(cls, ID): - """Return heating agent with given ID.""" - for agent in cls.heating_agents: - if agent.ID == ID: return agent - raise KeyError(ID) - - @classmethod - def get_cooling_agent(cls, ID): - """Return cooling agent with given ID.""" - for agent in cls.cooling_agents: - if agent.ID == ID: return agent - raise KeyError(ID) - - @classmethod - def get_suitable_heating_agent(cls, T_pinch): - """ - Return a heating agent that works at the pinch temperature. - - Parameters - ---------- - T_pinch : float - Pinch temperature [K]. - - """ - for agent in cls.heating_agents: - if T_pinch < agent.T: return agent - raise RuntimeError(f'no heating agent that can heat over {T_pinch} K') - - @classmethod - def get_suitable_cooling_agent(cls, T_pinch): - """Return a cooling agent that works at the pinch temperature. - - Parameters - ---------- - T_pinch : float - Pinch temperature [K]. - - """ - for agent in cls.cooling_agents: - if T_pinch > agent.T: return agent - raise RuntimeError(f'no cooling agent that can cool under {T_pinch} K') - - def load_agent(self, agent): - """Initialize utility streams with given agent.""" - # Initialize streams - self.inlet_utility_stream = agent.to_stream() - self.outlet_utility_stream = self.inlet_utility_stream.flow_proxy() - self.agent = agent - - def mix_from(self, heat_utilities): - N_heat_utilities = len(heat_utilities) - if N_heat_utilities == 0: - self.empty() - elif N_heat_utilities == 1: - self.copy_like(heat_utilities[0]) - else: - heat_utility, *other_heat_utilities = heat_utilities - agent = heat_utility.agent - assert all([i.agent is agent for i in other_heat_utilities]), ( - "utility agent must be the same to mix heat utilities") - self.load_agent(agent) - self.flow = self.inlet_utility_stream.F_mol = sum([i.flow for i in heat_utilities]) - self.duty = sum([i.duty for i in heat_utilities]) - self.cost = sum([i.cost for i in heat_utilities]) - self.heat_transfer_efficiency = None - self.T_pinch = None - self.iscooling = None - - def reverse(self): - self.flow *= -1 - self.duty *= -1 - self.cost *= -1 - self.inlet_utility_stream, self.outlet_utility_stream = self.outlet_utility_stream, self.inlet_utility_stream - - # Subcalculations - - @staticmethod - def get_T_outlet(T_pinch, T_limit, iscooling): - """ - Return outlet temperature of the utility in a counter current heat exchanger - - Parameters - ---------- - T_pinch : float - Pinch temperature of utility stream [K]. - iscooling : bool - True if utility is loosing energy. - - """ - if iscooling: - return T_limit if T_limit and T_limit < T_pinch else T_pinch - else: - return T_limit if T_limit and T_limit > T_pinch else T_pinch - - @classmethod - def get_inlet_and_outlet_T_pinch(cls, iscooling, T_in, T_out): - """Return pinch inlet and outlet temperature of utility.""" - dT = cls.dT - if iscooling: - assert T_in + 1e-6 >= T_out, "inlet temperature must be higher than outlet temperature if cooling" - T_pinch_in = T_out - dT - T_pinch_out = T_in - dT - else: - assert T_in <= T_out + 1e-6, "inlet temperature must be lower than outlet temperature if heating" - T_pinch_in = T_out + dT - T_pinch_out = T_in + dT - return T_pinch_in, T_pinch_out - - def _info_data(self, duty, flow, cost): - # Get units of measure - su = self.display_units - duty_units = duty or su.duty - flow_units = flow or su.flow - cost_units = cost or su.cost - - # Change units and return info string - flow = self.inlet_utility_stream.get_total_flow(flow_units) - duty = convert(self.duty, 'kJ/hr', duty_units) - cost = convert(self.cost, 'USD/hr', cost_units) - return duty, flow, cost, duty_units, flow_units, cost_units - - def __repr__(self): - if self.agent: - duty, flow, cost, duty_units, flow_units, cost_units = self._info_data(None, None, None) - return f'<{self.ID}: {self.duty:.3g} {duty_units}, {self.flow:.3g} {flow_units}, {self.cost:.3g} {cost_units}>' - else: - return f'<{type(self).__name__}: None>' - - # Representation - def _info(self, duty, flow, cost): - """Return string related to specifications""" - if not self.agent: - return (f'{type(self).__name__}: None\n' - +f' duty: 0\n' - +f' flow: 0\n' - +f' cost: 0') - else: - (duty, flow, cost, duty_units, - flow_units, cost_units) = self._info_data(duty, flow, cost) - return (f'{type(self).__name__}: {self.ID}\n' - +f' duty:{duty: .3g} {duty_units}\n' - +f' flow:{flow: .3g} {flow_units}\n' - +f' cost:{cost: .3g} {cost_units}') - - - def show(self, duty=None, flow=None, cost=None): - """Print all specifications""" - print(self._info(duty, flow, cost)) - _ipython_display_ = show - diff --git a/build/lib/biosteam/_network.py b/build/lib/biosteam/_network.py deleted file mode 100644 index f356968cc..000000000 --- a/build/lib/biosteam/_network.py +++ /dev/null @@ -1,351 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Tue Sep 10 09:01:47 2019 - -@author: yoelr -""" -from ._unit import Unit -from ._facility import Facility -from ._digraph import digraph_from_units_and_streams, finalize_digraph - -# %% Path tools - -def find_linear_and_cyclic_paths_with_recycle(feed, ends): - paths_with_recycle, linear_paths = find_paths_with_and_without_recycle( - feed, ends) - cyclic_paths_with_recycle = set() - for path_with_recycle in paths_with_recycle: - cyclic_path_with_recycle = path_with_recycle_to_cyclic_path_with_recycle(path_with_recycle) - cyclic_paths_with_recycle.add(cyclic_path_with_recycle) - return simplify_linear_paths(linear_paths), cyclic_paths_with_recycle - -def find_paths_with_and_without_recycle(feed, ends): - path = [] - paths_without_recycle = set() - paths_with_recycle = set() - fill_path(feed, path, paths_with_recycle, paths_without_recycle, - ends) - return paths_with_recycle, paths_without_recycle - -def fill_path(feed, path, paths_with_recycle, - paths_without_recycle, - ends): - unit = feed.sink - if not unit or isinstance(unit, Facility) or feed in ends: - paths_without_recycle.add(tuple(path)) - elif unit in path: - path_with_recycle = tuple(path), feed - paths_with_recycle.add(path_with_recycle) - ends.add(feed) - else: - path.append(unit) - first_outlet, *other_outlets = unit.outs - for outlet in other_outlets: - new_path = path.copy() - fill_path(outlet, new_path, - paths_with_recycle, - paths_without_recycle, - ends) - fill_path(first_outlet, path, - paths_with_recycle, - paths_without_recycle, - ends) - -def path_with_recycle_to_cyclic_path_with_recycle(path_with_recycle): - path, recycle = path_with_recycle - unit = recycle.sink - recycle_index = path.index(unit) - return (path[recycle_index:], recycle) - -def simplify_linear_paths(linear_paths): - simplified_linear_paths = [] - linear_paths = sorted(linear_paths, key=len) - while linear_paths: - smaller_path, *linear_paths = linear_paths - simplified_path = simplify_linear_path(smaller_path, linear_paths) - if simplified_path: - simplified_linear_paths.append(simplified_path) - return simplified_linear_paths - -def simplify_linear_path(path, other_paths): - simplified_path = list(path) - for unit in path: - for other_path in other_paths: - if unit in other_path: - simplified_path.remove(unit) - break - return simplified_path - -def load_network_components(path, units, streams, feeds, - products, subnetworks): - isa = isinstance - for i in path: - if isa(i, Unit): - units.add(i) - streams.update(i._ins + i._outs) - feeds.update([i for i in i._ins if not i._source]) - products.update([i for i in i._outs if not i._sink]) - elif isa(i, Network): - feeds.update(i.feeds) - streams.update(i.streams) - products.update(i.products) - units.update(i.units) - subnetworks.add(i) - else: - raise ValueError("path elements must be either Unit or Network " - f"objects not '{type(i).__name__}' objects") - - -# %% Network - -class Network: - """ - Create a Network object that defines a network of unit operations. - - Parameters - ---------- - path : Iterable[Unit or Network] - A path of unit operations and subnetworks. - recycle : :class:`~thermosteam.Stream` - A recycle stream, if any. - - """ - - __slots__ = ('path', 'recycle', 'units', 'subnetworks', - 'feeds', 'products', 'streams') - - def __init__(self, path, recycle=None): - self.path = list(path) - self.recycle = recycle - self.units = units = set() - self.subnetworks = subnetworks = set() - self.streams = streams = set() - self.feeds = feeds = set() - self.products = products = set() - load_network_components(path, units, streams, feeds, - products, subnetworks) - - @classmethod - def from_feedstock(cls, feedstock, feeds=(), ends=None): - """ - Create a Network object from a feedstock. - - Parameters - ---------- - feedstock : :class:`~thermosteam.Stream` - Main feedstock of the process. - feeds : Iterable[:class:`~thermosteam.Stream`] - Additional feeds to the process. - facilities : Iterable[Facility] - Offsite facilities that are simulated only after - completing the path simulation. - ends : Iterable[:class:`~thermosteam.Stream`] - Streams that not products, but are ultimately specified through - process requirements and not by its unit source. - - """ - ends = set(ends) or set() - linear_paths, cyclic_paths_with_recycle = find_linear_and_cyclic_paths_with_recycle( - feedstock, ends) - network = Network(sum(reversed(linear_paths), [])) - recycle_networks = [Network(path, recycle) for path, recycle - in cyclic_paths_with_recycle] - - for recycle_network in recycle_networks: - network.join_network(recycle_network) - isa = isinstance - for feed in feeds: - streams = network.streams - if feed in streams or isa(feed.sink, Facility): - continue - if ends: - new_ends = streams.union(ends) - else: - new_ends = streams - upstream_network = cls.from_feedstock(feed, (), new_ends) - connections = upstream_network.streams.intersection(streams) - connecting_units = {stream._sink for stream in connections - if stream._source and stream._sink} - N_connections = len(connecting_units) - if N_connections == 1: - connecting_unit, = connecting_units - network.join_network_at_unit(upstream_network, - connecting_unit) - elif N_connections == 0: - network._append_network(upstream_network) - else: - raise RuntimeError('path creation failed; multiple recycle ' - 'connections found for a given path') - return network - - def copy_like(self, other): - self.path = other.path - self.recycle = other.recycle - self.units = other.units - self.subnetworks = other.subnetworks - self.streams = other.streams - self.feeds = other.feeds - self.products = other.products - - def __contains__(self, other): - if isinstance(other, Unit): - return other in self.units - elif isinstance(other, Network): - return other in self.subnetworks - else: - return False - - def issubset(self, network): - return self.units.issubset(network.units) - - def isdisjoint(self, network): - return self.units.isdisjoint(network.units) - - def join_network(self, network, downstream=True): - if self.isdisjoint(network): - if downstream: - self._append_network(network) - else: - self._appendleft_network(network) - else: - self._add_subnetwork(network) - - def join_network_at_unit(self, network, unit): - isa = isinstance - self._remove_overlap(network) - has_overlap = False - for index, item in enumerate(self.path): - if isa(item, Network) and unit in item.units: - item.join_network_at_unit(network, unit) - self._update_from_newly_added_network(network) - break - elif unit == item: - self._insert_network(index, network, has_overlap) - break - - def _append_unit(self, unit): - self.units.add(unit) - self.products.update([i for i in unit._outs if i and not i._sink]) - self.feeds.update([i for i in unit._ins if i and not i._source]) - self.streams.update(unit._ins + unit._outs) - - def _update_from_newly_added_network(self, network): - self.subnetworks.add(network) - self.units.update(network.units) - self.streams.update(network.streams) - self.feeds.update(network.feeds) - self.products.update(network.products) - - def _appendleft_network(self, network): - if network.recycle: - self.path.insert(0, network) - else: - for i in reversed(network.path): self.path.insert(0, i) - self._update_from_newly_added_network(network) - - def _append_network(self, network): - if network.recycle: - self.path.append(network) - else: - self.path.extend(network.path) - self._update_from_newly_added_network(network) - - def _insert_network(self, index, network, has_overlap=True): - path = self.path - if has_overlap: - self._remove_overlap(network) - if network.recycle: - path.insert(index, network) - else: - for item in reversed(network.path): - path.insert(index, item) - self._update_from_newly_added_network(network) - - def _add_subnetwork(self, subnetwork): - path = self.path - subunits = subnetwork.units - isa = isinstance - index_found = done = False - subnetworks = self.subnetworks - has_overlap = True - path_tuple = tuple(path) - for i in path_tuple: - if isa(i, Unit): - continue - elif i.issubset(subnetwork): - subnetwork._add_subnetwork(i) - index = path.index(i) - path.remove(i) - subnetworks.remove(i) - index_found = True - elif subnetwork.issubset(i): - i._add_subnetwork(subnetwork) - done = True - if index_found: - self._insert_network(index, subnetwork) - elif not done: - for index, item in enumerate(path_tuple): - if isa(item, Unit): - if item not in subunits: continue - self._insert_network(index, subnetwork) - has_overlap = False - done = True - break - elif isa(item, Network): - if item.isdisjoint(subnetwork): - continue - else: - item._add_subnetwork(subnetwork) - done = True - break - if has_overlap: - self._remove_overlap(subnetwork) - if not done: - self._append_network(subnetwork) - if len(path) == 1 and isa(path[0], Network): - self.copy_like(path[0]) - - def _remove_overlap(self, subnetwork): - path = self.path - for item in tuple(path): - if item in subnetwork: - path.remove(item) - - def diagram(self, file=None, format='png'): - units = self.units - f = digraph_from_units_and_streams(units, self.streams) - finalize_digraph(f, file, format) - - def __repr__(self): - return f"{type(self).__name__}(path={self.path}, recycle={self.recycle})" - - def _info(self, spaces): - info = f"{type(self).__name__}(" - spaces += 4 * " " - end = ',\n' + spaces - path_info = [] - path = self.path - isa = isinstance - info += '\n' + spaces - for i in path: - if isa(i, Unit): - path_info.append(str(i)) - else: - path_info.append(i._info(spaces)) - info += '[' + (end + " ").join(path_info) + ']' - if self.recycle: - info += end + f"recycle={self.recycle})" - else: - info += ')' - return info - - def _ipython_display_(self): - self.show() - - def show(self): - print(self._info(spaces="")) - - - - - \ No newline at end of file diff --git a/build/lib/biosteam/_power_utility.py b/build/lib/biosteam/_power_utility.py deleted file mode 100644 index 67fa0803b..000000000 --- a/build/lib/biosteam/_power_utility.py +++ /dev/null @@ -1,82 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sun Nov 11 11:20:42 2018 - -@author: yoelr -""" -from thermosteam.base.units_of_measure import DisplayUnits, convert - -__all__ = ('PowerUtility',) - -class PowerUtility: - """ - Create an PowerUtility object that, when called, calculates the cost of power (kW) and saves the power and cost. - - Examples - -------- - Create a PowerUtility object: - - .. code-block:: python - - >>> pu = PowerUtility() - >>> pu - - - Call object to calculate cost: - - .. code-block:: python - - >>> pu(rate=500) - >>> pu - - - Results are accessible: - - .. code-block:: python - - >>> pu.rate, pu.cost - (500, 39.1) - - """ - __slots__ = ('rate', 'cost') - - #: [DisplayUnits] Units of measure for IPython display - display_units = DisplayUnits(rate='kW', cost='USD/hr') - - #: [float] USD/kWhr - price = 0.0782 - - def __init__(self): - #: Power requirement (kW) - self.rate = 0 - - #: Cost (USD/hr) - self.cost = 0 - - def __bool__(self): - return bool(self.rate) - - def __call__(self, rate): - """Calculate cost and save results. - - Parameters - ---------- - rate : float - Power requirement (kW) - - """ - self.rate = rate - self.cost = self.price * rate - - def show(self, rate=None, cost=None): - # Get units of measure - display_units = self.display_units - rate_units = rate or display_units.rate - cost_units = cost or display_units.cost - rate = convert(self.rate, 'kW', rate_units) - cost = convert(self.cost, 'USD/hr', cost_units) - return (f'<{type(self).__name__}: {rate:.3g} {rate_units}, {cost:.3g} {cost_units}>') - _ipython_display = show - - def __repr__(self): - return (f'<{type(self).__name__}: {self.rate:.3g} kW, {self.cost:.3g} USD/hr>') \ No newline at end of file diff --git a/build/lib/biosteam/_report/__init__.py b/build/lib/biosteam/_report/__init__.py deleted file mode 100644 index bb81285f1..000000000 --- a/build/lib/biosteam/_report/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Tue Mar 19 20:12:20 2019 - -@author: yoelr -""" - -__all__ = [] - -from . import table -from . import plot - -from .table import * -from .plot import * - -__all__.extend(table.__all__) -__all__.extend(plot.__all__) \ No newline at end of file diff --git a/build/lib/biosteam/_report/plot.py b/build/lib/biosteam/_report/plot.py deleted file mode 100644 index 42f275d1c..000000000 --- a/build/lib/biosteam/_report/plot.py +++ /dev/null @@ -1,103 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sun Feb 10 20:14:13 2019 - -@author: Guest Group -""" -import matplotlib.pyplot as plt -from .table import cost_table -import numpy as np - -__all__ = ['plot_cost_summary'] - -def plot_cost_summary(system, cost='Capital'): - cost = cost.capitalize() - if cost == 'Capital': - colkey = 'Fixed Capital Investment (10^6 USD)' - ylabel = 'Fixed Capital Investment\n[$10^6$ USD]' - elif cost == 'Utility': - colkey = 'Utility Cost (10^6 USD/yr)' - ylabel = 'Utility Cost\n[$10^6$ USD]' - else: - raise ValueError(f"Argument, cost, must be either 'Capital' or 'Utility'") - # Get data - df = cost_table(system) - Type = df['Type'] # Column of unit types - C_unit = df[colkey] # Column of unit costs - C_Type = [] # Cost of all units of a type - t_old = Type[0] - r = 0 - c_type = 0 - Type_ = [] # All types - for t in Type: - if t == t_old: - c_type += C_unit[r] - else: - C_Type.append(c_type) - Type_.append(t_old) - c_type = C_unit[r] - t_old = t - r += 1 - if t != Type_[-1]: - C_Type.append(c_type) - Type_.append(t) - - # Make graph - xpos = np.arange(len(Type_))*1.2 - max_ = max(C_Type) - ypos = np.arange(max_*1.2) - plt.figure(figsize=(18,5)) - plt.bar(xpos, C_Type, align='center', alpha=0.5) - plt.xticks(xpos, Type_, fontsize='11', rotation=30, ha='right') - plt.yticks(ypos, fontsize='11') - plt.xlabel('Unit Operation Type', fontsize='14') - plt.ylabel(ylabel, fontsize='14') - for i, v in enumerate(C_Type): - plt.text(i*1.2 - 0.15, v+0.2, f'{v:.2f}', - color='blue', fontsize='11', fontweight='bold') - plt.show() - return Type_ - - -# def plot_system_summary(system): -# # Get data -# df = report_cost(system.units) -# Type = df['Type'] -# CI_unit = df['Capital Investment (10^6 USD)'] -# CU_unit = df['Utility Cost (10^6 USD/yr)'] -# CI_Type = [] -# CU_Type = [] -# t_old = Type[0] -# r = 0 -# ci = 0 -# cu = 0 -# Type_ = [] -# for t in Type: -# if t == t_old: -# ci += CI_unit[r] -# cu += CU_unit[r] -# else: -# t_old = t -# CI_Type.append(ci) -# CU_Type.append(cu) -# Type_.append(t_old) -# ci = CI_unit[r] -# cu = CU_unit[r] -# r += 1 - -# # Make subplot -# f, (ax1, ax2) = plt.subplots(2, 1, sharey=True) -# pos = np.arange(len(Type_)) -# ax2.bar(pos, CI_Type, align='center', alpha=0.5) -# plt.sca(ax2) -# plt.xticks(pos, Type_) -# plt.ylabel('Capital Investment (10^6 USD)') -# plt.title('Cost Summary') - -# ax1.bar(pos, CU_Type, align='center', alpha=0.5) -# plt.sca(ax1) -# plt.ylabel('Capital Investment (10^6 USD)') -# plt.title('Cost Summary') -# plt.show() - -# return ax1, ax2 \ No newline at end of file diff --git a/build/lib/biosteam/_report/table.py b/build/lib/biosteam/_report/table.py deleted file mode 100644 index d750acfe2..000000000 --- a/build/lib/biosteam/_report/table.py +++ /dev/null @@ -1,333 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sat Nov 17 09:48:34 2018 - -@author: yoelr -""" -import numpy as np -import pandas as pd -from warnings import warn -import openpyxl -from .._tea import TEA, CombinedTEA -from thermosteam import Stream -from thermosteam.base import get_dimensionality, convert, stream_units_of_measure -from thermosteam.exceptions import DimensionError -import os - -DataFrame = pd.DataFrame -ExcelWriter = pd.ExcelWriter - -__all__ = ('stream_table', 'cost_table', 'save_system_results', - 'save_report', 'results_table', 'heat_utilities_table', - 'power_utilities_table') - -def _stream_key(s): - num = s.ID[1:] - if num.isnumeric(): return int(num) - else: return -1 - -# %% Helpful functions - -def _save(tables, writer, sheet_name='Sheet1', n_row=1): - """Save a list of tables as an excel file. - - Parameters - ---------- - tables : iterable[DataFrame] - writer : ExcelWritter - - """ - for t in tables: - label = t.columns.name - t.to_excel(writer, sheet_name, - startrow=n_row, index_label=label) - n_row += len(t.index) + 2 - - return n_row - -# %% Units - -def save_report(system, file='report.xlsx', dpi='300', **stream_properties): - """Save a system report as an xlsx file. - - Parameters - ---------- - file : str - File name to save report - - **stream_properties : str - Additional stream properties and units as key-value pairs (e.g. T='degC', flow='gpm', H='kW', etc..) - - """ - writer = ExcelWriter(file) - units = list(system._costunits) - try: - system.diagram('thorough', file='flowsheet', dpi=str(dpi), format='png') - except: - diagram_completed = False - warn(RuntimeWarning('failed to generate diagram through graphviz'), stacklevel=2) - else: - try: - # Assume openpyxl is used - worksheet = writer.book.create_sheet('Flowsheet') - flowsheet = openpyxl.drawing.image.Image('flowsheet.png') - worksheet.add_image(flowsheet, anchor='A1') - except: - # Assume xlsx writer is used - worksheet = writer.book.add_worksheet('Flowsheet') - flowsheet.insert_image('A1', 'flowsheet.png') - diagram_completed = True - - if system.TEA: - tea = system.TEA - if isinstance(tea, CombinedTEA): - costs = [cost_table(i) for i in tea.TEAs] - _save(costs, writer, 'Itemized costs') - else: - # Cost table - cost = cost_table(tea) - cost.to_excel(writer, 'Itemized costs') - - # Cash flow - tea.get_cashflow_table().to_excel(writer, 'Cash flow') - else: - warn(RuntimeWarning(f'Cannot find TEA object in {repr(system)}. Ignoring TEA sheets.'), stacklevel=2) - - - # Stream tables - # Organize streams by chemicals first - streams_by_chemicals = {} - for i in system.streams: - if not i: continue - chemicals = i.chemicals - if chemicals in streams_by_chemicals: - streams_by_chemicals[chemicals].append(i) - else: - streams_by_chemicals[chemicals] = [i] - stream_tables = [] - for streams in streams_by_chemicals.values(): - stream_tables.append(stream_table(streams, **stream_properties)) - _save(stream_tables, writer, 'Stream table') - - # Heat utility tables - heat_utilities = heat_utilities_table(units) - n_row = _save(heat_utilities, writer, 'Utilities') - - # Power utility table - power_utility = power_utilities_table(units) - power_utility.to_excel(writer, 'Utilities', - index_label='Electricity', - startrow=n_row) - - # General desing requirements - results = results_table(units) - _save(results, writer, 'Design requirements') - writer.save() - if diagram_completed: os.remove("flowsheet.png") - -save_system_results = save_report - -def results_table(units): - """Return a list of results tables for each unit type. - - Parameters - ---------- - units : iterable[Unit] - - Returns - ------- - tables : list[DataFrame] - - """ - units.sort(key=(lambda u: u.line)) - - # Organize units by units of measure: - organized = {} - for u in units: - uom = (*u._units.keys(), u.line) - if uom in organized: organized[uom].append(u) - else: organized[uom] = [u] - - # Make a list of tables, keeping all results with same keys in one table - tables = [] - for units in organized.values(): - # First table with units of measure - table = None - while (table is None) and units: - u, *units = units - table = u.results(include_utilities=False, - include_total_cost=False) - if table is None: continue - for u in units[1:]: - table[u.ID] = u.results(with_units=False, include_utilities=False, - include_total_cost=False) - table.columns.name = (u.line, '') - tables.append(table) - table = u.results() - return tables - -def cost_table(tea): - """Return a cost table as a pandas DataFrame object. - - Parameters - ---------- - units : iterable[Unit] - - Returns - ------- - table : DataFrame - - """ - columns = ('Unit operation', - f'Purchase cost (10^6 USD)', - f'Utility cost (10^6 USD/yr)') - units = tea.units - operating_days = tea.operating_days - N_units = len(units) - array = np.empty((N_units, 3), dtype=object) - IDs = [] - types = array[0:, 0] - C_cap = array[0:, 1] - C_op = array[0:, 2] - - # Get data - for i in range(N_units): - unit = units[i] - types[i] = unit.line - C_cap[i] = unit.purchase_cost / 1e6 - C_op[i] = unit.utility_cost * operating_days * 24 / 1e6 - IDs.append(unit.ID) - - df = DataFrame(array, columns=columns, index=IDs) - if not tea.lang_factor: - df['Installation cost (10^6 USD)'] = [u.installation_cost for u in units] - - return df - -def heat_utilities_table(units): - """Return a list of utility tables for each heat utility source. - - Parameters - ---------- - units : iterable[Unit] - - Returns - ------- - tables : list[DataFrame] - - """ - # Sort heat utilities by unit type, then by utility Type - units = sorted(units, key=(lambda u: type(u).__name__)) - source = {} - heat_utils = [] - for u in units: - hus = u.heat_utilities - if not hus: continue - for i in hus: source[i] = u - heat_utils.extend(hus) - - # Organize heatutility by ID - heat_utils_dict = {} - for i in heat_utils: - ID = i.ID - if ID in heat_utils_dict: heat_utils_dict[ID].append(i) - else: heat_utils_dict[ID] = [i] - - # First table and set Type to compare with - hu = heat_utils[0] - Type = hu.ID - - # Make a list of tables, keeping all results with same Type in one table - tables = [] - for Type, heat_utils in heat_utils_dict.items(): - data = []; index = [] - for hu in heat_utils: - data.append((source[hu].line, hu.duty, hu.flow, hu.cost)) - index.append(source[hu].ID) - table = DataFrame(data, index=index, - columns=('Unit operation', - 'Duty (kJ/hr)', - 'Flow (kmol/hr)', - 'Cost (USD/hr)')) - table.columns.name = Type - tables.append(table) - return tables - - -def power_utilities_table(units): - # Sort power utilities by unit type - units = sorted(units, key=(lambda u: type(u).__name__)) - units = [u for u in units if u.power_utility] - power_utilities = [u.power_utility for u in units] - lenght = len(power_utilities) - data = [] - for i, u, pu in zip(range(lenght), units, power_utilities): - data.append((u.line, pu.rate, pu.cost)) - return DataFrame(data, index=[u.ID for u in units if u.power_utility], - columns=('Unit Operation', 'Rate (kW)', 'Cost (USD/hr)')) - - -# %% Streams - -def stream_table(streams, flow='kg/hr', **props) -> 'DataFrame': - """Return a stream table as a pandas DataFrame object. - - Parameters - - streams : array_like[Stream] - flow : str - Units for flow rate. - props : str - Additional stream properties and units as key-value pairs - - """ - - # Prepare rows and columns - ss = sorted([i for i in streams if i.ID], key=_stream_key) - chemical_IDs = ss[0].chemicals.IDs - n = len(ss) - m = len(chemical_IDs) - p = len(props) - array = np.empty((m+p+5, n), dtype=object) - IDs = n*[None] - sources = array[0, :] - sinks = array[1, :] - phases = array[2, :] - prop_molar_data = array[3:3+p+1,:] - flows = array[p+3, :] - array[p+4, :] = '' - fracs = array[p+5:m+p+5, :] - for j in range(n): - s = ss[j] - sources[j] = str(s.source or '-') - sinks[j] = str(s.sink or '-') - IDs[j] = s.ID - phase = '' - for i in s.phase: - if i == 'l': - phase += 'liquid|' - elif i == 'L': - phase += 'LIQUID|' - elif i == 'g': - phase += 'gas|' - elif i == 's': - phase += 'solid|' - phase = phase.rstrip('|') - phases[j] = phase - flow_j = s.get_flow(flow) - flows[j] = net_j = sum(flow_j) - fracs[:,j] = flow_j/net_j if net_j > 0 else 0 - i = 0 - for attr, units in props.items(): - prop_molar_data[i, j] = s.get_property(attr, units) - i += 1 - - # Set the right units - i = 0 - prop_molar_keys = [f'{attr} ({unit})' for attr, unit in props.items()] - - # Make data frame object - index = ('Source', 'Sink', 'Phase') + tuple(prop_molar_keys) + (f'flow ({flow})', 'Composition:') + tuple(chemical_IDs) - return DataFrame(array, columns=IDs, index=index) - - diff --git a/build/lib/biosteam/_system.py b/build/lib/biosteam/_system.py deleted file mode 100644 index 1a32515c5..000000000 --- a/build/lib/biosteam/_system.py +++ /dev/null @@ -1,740 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sat Aug 18 15:04:55 2018 - -@author: yoelr -""" -from flexsolve import SolverError, conditional_wegstein, conditional_aitken -from ._digraph import (digraph_from_units_and_streams, - minimal_digraph, - surface_digraph, - finalize_digraph) -from thermosteam import Stream -from thermosteam.utils import registered -from .exceptions import try_method_with_object_stamp -from ._network import Network -from ._facility import Facility -from ._unit import Unit -from ._report import save_report -from .utils import colors, strtuple -import biosteam as bst - -__all__ = ('System',) - -# %% Functions for building systems - -def streams_from_path(path): - isa = isinstance - streams = set() - for i in path: - if isa(i, System): - streams.add(i.streams) - elif isa(i, Unit): - streams.update(i._ins + i._outs) - return streams - -def feeds_from_streams(streams): - return {s for s in streams if not s._source} - -def products_from_streams(streams): - return {s for s in streams if not s._sink} - -def filter_out_missing_streams(streams): - streams.intersection_update([i for i in streams if i]) - -# %% Functions for taking care of numerical specifications within a system path - -def run_unit_in_path(unit): - numerical_specification = unit._numerical_specification - if numerical_specification: - method = numerical_specification - else: - method = unit._run - try_method_with_object_stamp(unit, method) - -def converge_system_in_path(system): - numerical_specification = system._numerical_specification - if numerical_specification: - method = numerical_specification - else: - method = system._converge - try_method_with_object_stamp(system, method) - -def simulate_unit_in_path(unit): - numerical_specification = unit._numerical_specification - if numerical_specification: - method = numerical_specification - else: - method = unit.simulate - try_method_with_object_stamp(unit, method) - -def simulate_system_in_path(system): - numerical_specification = system._numerical_specification - if numerical_specification: - method = numerical_specification - else: - method = system.simulate - try_method_with_object_stamp(system, method) - -# %% Debugging and exception handling - -def _evaluate(self, command=None): - """Evaluate a command and request user input for next command. If no command, return. This function is used for debugging a System object.""" - # Done evaluating if no command, exit debugger if 'exit' - if command is None: - Next = colors.next('Next: ') + f'{repr(self)}\n' - info = colors.info("Enter to continue or type to evaluate:\n") - command = input(Next + info + ">>> ") - - if command == 'exit': raise KeyboardInterrupt() - if command: - # Build locals dictionary for evaluating command - lcs = {self.ID: self, 'bst': bst} - try: - out = eval(command, {}, lcs) - except Exception as err: - # Print exception and ask to raise error or continue evaluating - err = colors.exception(f'{type(err).__name__}:') + f' {str(err)}\n\n' - info = colors.info(f"Enter to raise error or type to evaluate:\n") - command = input(err + info + ">>> ") - if command == '': raise err - _evaluate(self, command) - else: - # If successful, continue evaluating - if out is None: pass - elif (not hasattr(out, '_ipython_display_') - or isinstance(out, type)): print(out) - else: out._ipython_display_() - - command = input(">>> ") - _evaluate(self, command) - -def _method_debug(self, func): - """Method decorator for debugging system.""" - def wrapper(*args, **kwargs): - # Run method and ask to evaluate - _evaluate(self) - func(*args, **kwargs) - - wrapper.__name__ = func.__name__ - wrapper.__doc__ = func.__doc__ - wrapper._original = func - return wrapper - -def _notify_run_wrapper(self, func): - """Decorate a System run method to notify you after each loop""" - def wrapper(*args, **kwargs): - if self.recycle: - func(*args, **kwargs) - input(f' Finished loop #{self._iter}\n') - else: - func(*args, **kwargs) - wrapper.__name__ = func.__name__ - wrapper.__doc__ = func.__doc__ - wrapper._original = func - return wrapper - - -# %% Process flow - -class system(type): - @property - def converge_method(self): - """Iterative convergence method ('wegstein', 'aitken', or 'fixed point').""" - return self._converge.__name__[1:] - - @converge_method.setter - def converge_method(self, method): - method = method.lower().replace('-', '').replace(' ', '') - if 'wegstein' == method: - self._converge = self._wegstein - elif 'fixedpoint' == method: - self._converge = self._fixed_point - elif 'aitken' == method: - self._converge = self._aitken - else: - raise ValueError(f"only 'wegstein', 'aitken', and 'fixed point' methods are valid, not '{method}'") - -@registered('SYS') -class System(metaclass=system): - """ - Create a System object that can iteratively run each element in a path - of BioSTREAM objects until the recycle stream is converged. A path can - have function, Unit and/or System objects. When the path contains an - inner System object, it converges/solves it in each loop/iteration. - - Parameters - ---------- - ID : str - A unique identification. If ID is None, instance will not be - registered in flowsheet. - path : tuple[Unit, function and/or System] - A path that is run element by element until the recycle converges. - recycle=None : :class:`~thermosteam.Stream`, optional - A tear stream for the recycle loop. - facilities=() : tuple[Unit, function, and/or System], optional - Offsite facilities that are simulated only after - completing the path simulation. - - """ - ### Class attributes ### - - #: Maximum number of iterations - maxiter = 100 - - #: Molar tolerance (kmol/hr) - molar_tolerance = 0.50 - - #: Temperature tolerance (K) - T_tolerance = 0.10 - - # [dict] Cached downstream systems by (system, unit, with_facilities) keys - _cached_downstream_systems = {} - - @classmethod - def from_feedstock(cls, ID, feedstock, feeds=None, facilities=(), ends=None): - """ - Create a System object from a feedstock. - - Parameters - ---------- - ID : str - Name of system. - feedstock : :class:`~thermosteam.Stream` - Main feedstock of the process. - feeds : Iterable[:class:`~thermosteam.Stream`] - Additional feeds to the process. - facilities : Iterable[Facility] - Offsite facilities that are simulated only after - completing the path simulation. - ends : Iterable[:class:`~thermosteam.Stream`] - Streams that not products, but are ultimately specified through - process requirements and not by its unit source. - - """ - network = Network.from_feedstock(feedstock, feeds, ends) - return cls.from_network(ID, network, facilities) - - @classmethod - def from_network(cls, ID, network, facilities=()): - """ - Create a System object from a network. - - Parameters - ---------- - ID : str - Name of system. - network : Network - Network that defines the simulation path. - facilities : Iterable[Facility] - Offsite facilities that are simulated only after - completing the path simulation. - - """ - facilities = Facility.ordered_facilities(facilities) - isa = isinstance - path = tuple([(cls.from_network('', i) if isa(i, Network) else i) - for i in network.path]) - self = cls.__new__(cls) - self.units = network.units - self.streams = streams = network.streams - self.feeds = feeds = network.feeds - self.products = products = network.products - self._numerical_specification = None - self._reset_errors() - self._set_path(path) - self._set_facilities(facilities) - self._set_recycle(network.recycle) - self._register(ID) - if facilities: - f_streams = streams_from_path(facilities) - f_feeds = feeds_from_streams(f_streams) - f_products = products_from_streams(f_streams) - streams.update(f_streams) - feeds.update(f_feeds) - products.update(f_products) - self._finalize_streams() - return self - - def __init__(self, ID, path, recycle=None, facilities=()): - self._numerical_specification = None - self._load_flowsheet() - self._reset_errors() - self._set_path(path) - self._load_units() - self._set_facilities(facilities) - self._load_streams() - self._finalize_streams() - self._set_recycle(recycle) - self._register(ID) - - numerical_specification = Unit.numerical_specification - save_report = save_report - - def _load_flowsheet(self): - self.flowsheet = flowsheet_module.main_flowsheet.get_flowsheet() - - def _set_recycle(self, recycle): - if recycle is None: - self._converge = self._run - else: - assert isinstance(recycle, Stream), ( - "recycle must be a Stream instance or None, not " - f"{type(recycle).__name__}") - self._recycle = recycle - - def _set_path(self, path): - #: tuple[Unit, function and/or System] A path that is run element - #: by element until the recycle converges. - self.path = path - - #: set[System] All subsystems in the system - self.subsystems = subsystems = set() - - #: list[Unit] Network of only unit operations - self._unit_path = unit_path = [] - - isa = isinstance - for i in path: - if i in unit_path: continue - if isa(i, Unit): - unit_path.append(i) - elif isa(i, System): - unit_path.extend(i._unit_path) - subsystems.add(i) - - #: set[Unit] All units in the path that have costs - self._path_costunits = costunits = {i for i in unit_path - if i._design or i._cost} - - #: set[Unit] All units that have costs. - self._costunits = costunits = costunits.copy() - - def _load_units(self): - #: set[Unit] All units within the system - self.units = set(self._unit_path) - - def _set_facilities(self, facilities): - #: tuple[Unit, function, and/or System] Offsite facilities that are simulated only after completing the path simulation. - self.facilities = facilities = tuple(facilities) - subsystems = self.subsystems - costunits = self._costunits - units = self.units - isa = isinstance - for i in facilities: - if isa(i, Unit): - i._load_stream_links() - units.add(i) - if i._cost: costunits.add(i) - if isa(i, Facility): i._system = self - elif isa(i, System): - units.update(i.units) - subsystems.add(i) - costunits.update(i._costunits) - - def _load_streams(self): - #: set[:class:`~thermosteam.Stream`] All streams within the system - self.streams = streams = set() - - for u in self.units: - streams.update(u._ins + u._outs) - for sys in self.subsystems: - streams.update(sys.streams) - - #: set[:class:`~thermosteam.Stream`] All feed streams in the system. - self.feeds = feeds_from_streams(streams) - - #: set[:class:`~thermosteam.Stream`] All product streams in the system. - self.products = products_from_streams(streams) - - def _load_stream_links(self): - for u in self._unit_path: u._load_stream_links() - - def _filter_out_missing_streams(self): - for stream_set in (self.streams, self.feeds, self.products): - filter_out_missing_streams(stream_set) - - def _finalize_streams(self): - self._load_stream_links() - self._filter_out_missing_streams() - - @property - def TEA(self): - """[TEA] Object for Techno-Economic Analysis.""" - try: return self._TEA - except AttributeError: return None - - @property - def recycle(self): - """[:class:`~thermosteam.Stream`] A tear stream for the recycle loop""" - return self._recycle - - @property - def converge_method(self): - """Iterative convergence method ('wegstein', 'aitken', or 'fixed point').""" - return self._converge.__name__[1:] - - @converge_method.setter - def converge_method(self, method): - if self.recycle is None: - raise ValueError( - "cannot set converge method when no recyle is specified") - method = method.lower().replace('-', '').replace(' ', '') - if 'wegstein' == method: - self._converge = self._wegstein - elif 'fixedpoint' == method: - self._converge = self._fixed_point - elif 'aitken' == method: - self._converge = self._aitken - else: - raise ValueError( - f"only 'wegstein', 'aitken', and 'fixed point' methods " - f"are valid, not '{method}'") - - - def _downstream_path(self, unit): - """Return a list composed of the `unit` and everything downstream.""" - if unit not in self.units: return [] - elif self._recycle: return self.path - unit_found = False - downstream_units = unit._downstream_units - path = [] - isa = isinstance - for i in self.path: - if unit_found: - if isa(i, System): - for u in i.units: - if u in downstream_units: - path.append(i) - break - elif i in downstream_units: - path.append(i) - elif (not isa(i, Unit) - or i.line == 'Balance'): - path.append(i) - else: - if unit is i: - unit_found = True - path.append(unit) - elif isa(i, System) and unit in i.units: - unit_found = True - path.append(i) - return path - - def _downstream_system(self, unit): - """Return a system with a path composed of the `unit` and - everything downstream (facilities included).""" - if unit is self.path[0]: return self - system = self._cached_downstream_systems.get((self, unit)) - if system: return system - path = self._downstream_path(unit) - if path: - downstream_facilities = self.facilities - else: - unit_found = False - isa = isinstance - for pos, i in enumerate(self.facilities): - if unit is i or (isa(i, System) and unit in i.units): - downstream_facilities = self.facilities[pos:] - unit_found = True - break - assert unit_found, f'{unit} not found in system' - system = System(None, path, - facilities=downstream_facilities) - system._ID = f'{type(unit).__name__}-{unit} and downstream' - self._cached_downstream_systems[unit] = system - return system - - def _minimal_digraph(self, **graph_attrs): - """Return digraph of the path as a box.""" - return minimal_digraph(self.ID, self.units, self.streams, **graph_attrs) - - def _surface_digraph(self, **graph_attrs): - return surface_digraph(self.path) - - def _thorough_digraph(self, **graph_attrs): - return digraph_from_units_and_streams(self.units, self.streams, - **graph_attrs) - - def diagram(self, kind='surface', file=None, format='png', **graph_attrs): - """Display a `Graphviz `__ diagram of the system. - - Parameters - ---------- - kind='surface' : {'thorough', 'surface', 'minimal'}: - * **'thorough':** Display every unit within the path. - * **'surface':** Display only elements listed in the path. - * **'minimal':** Display path as a box. - file=None : str, display in console by default - File name to save diagram. - format='png' : str - File format (e.g. "png", "svg"). - - """ - if kind == 'thorough': - f = self._thorough_digraph(format=format, **graph_attrs) - elif kind == 'surface': - f = self._surface_digraph(format=format, **graph_attrs) - elif kind == 'minimal': - f = self._minimal_digraph(format=format, **graph_attrs) - else: - raise ValueError(f"kind must be either 'thorough', 'surface', or 'minimal'") - finalize_digraph(f, file, format) - - # Methods for running one iteration of a loop - def _iter_run(self, mol): - """ - Run the system at specified recycle molar flow rate. - - Parameters - ---------- - mol : numpy.ndarray - Recycle molar flow rates. - - Returns - ------- - rmol : numpy.ndarray - New recycle molar flow rates. - unconverged : bool - True if recycle has not converged. - - """ - recycle = self.recycle - rmol = recycle.mol - rmol[:] = mol - T = recycle.T - self._run() - mol_error = abs(mol - recycle.mol).sum() - T_error = abs(T - recycle.T) - self._iter += 1 - if mol_error < self.molar_tolerance and T_error < self.T_tolerance: - unconverged = False - elif self._iter > self.maxiter: - raise SolverError(f'{repr(self)} could not converge' + self._error_info()) - else: - unconverged = True - self._T_error = T_error - self._mol_error = mol_error - return rmol.copy(), unconverged - - - def _setup(self): - """Setup each element of the system.""" - isa = isinstance - for a in self.path: - if isa(a, (Unit, System)): a._setup() - else: pass # Assume it is a function - - def _run(self): - """Rigorous run each element of the system.""" - isa = isinstance - for i in self.path: - if isa(i, Unit): - run_unit_in_path(i) - elif isa(i, System): - converge_system_in_path(i) - else: i() # Assume it is a function - - # Methods for convering the recycle stream - def _fixed_point(self): - """Converge system recycle using inner and outer loops with fixed-point iteration.""" - r = self.recycle - rmol = r.mol - while True: - mol = rmol.copy() - T = r.T - self._run() - self._mol_error = abs(mol - rmol).sum() - self._T_error = abs(T - r.T) - self._iter += 1 - if (self._mol_error < self.molar_tolerance - and self._T_error < self.T_tolerance): break - if self._iter > self.maxiter: - raise SolverError(f'{repr(self)} could not converge' + self._error_info()) - - def _wegstein(self): - """Converge the system recycle iteratively using wegstein's method.""" - conditional_wegstein(self._iter_run, self.recycle.mol.copy()) - - def _aitken(self): - """Converge the system recycle iteratively using Aitken's method.""" - conditional_aitken(self._iter_run, self.recycle.mol.copy()) - - # Default converge method - _converge = _aitken - - def _reset_iter(self): - self._iter = 0 - for system in self.subsystems: system._reset_iter() - - def reset_names(self, unit_format=None, stream_format=None): - """Reset names of all streams and units according to the path order.""" - Unit._default_ID = unit_format if unit_format else ['U', 0] - Stream._default_ID = stream_format if stream_format else ['d', 0] - streams = set() - units = set() - for i in self._unit_path: - if i in units: continue - try: i.ID = '' - except: continue - for s in (i._ins + i._outs): - if (s and s._sink and s._source - and s not in streams): - s.ID = '' - streams.add(s) - units.add(i) - - def _reset_errors(self): - #: Molar flow rate error (kmol/hr) - self._mol_error = 0 - - #: Temperature error (K) - self._T_error = 0 - - #: Specification error - self._spec_error = 0 - - #: Number of iterations - self._iter = 0 - - def reset_flows(self): - """Reset all process streams to zero flow.""" - self._reset_errors() - feeds = self.feeds - for stream in self.streams: - if stream not in feeds: stream.empty() - - def simulate(self): - """Converge the path and simulate all units.""" - self._reset_iter() - self._setup() - self._converge() - for i in self._path_costunits: - try_method_with_object_stamp(i, i._summary) - isa = isinstance - for i in self.facilities: - if isa(i, Unit): - simulate_unit_in_path(i) - elif isa(i, System): - simulate_system_in_path(i) - else: - i() # Assume it is a function - - # Debugging - def _debug_on(self): - """Turn on debug mode.""" - self._run = _notify_run_wrapper(self, self._run) - self.path = path = list(self.path) - for i, item in enumerate(path): - if isinstance(item, Unit): - item._run = _method_debug(item, item._run) - elif isinstance(item, System): - item._converge = _method_debug(item, item._converge) - elif callable(item): - path[i] = _method_debug(item, item) - - def _debug_off(self): - """Turn off debug mode.""" - self._run = self._run._original - path = self.path - for i, item in enumerate(path): - if isinstance(item, Unit): - item._run = item._run._original - elif isinstance(item, System): - item._converge = item._converge._original - elif callable(item): - path[i] = item._original - self.path = tuple(path) - - def debug(self): - """Converge in debug mode. Just try it!""" - self._debug_on() - try: self._converge() - finally: self._debug_off() - end = self._error_info() - if end: - print(f'\nFinished debugging{end}') - else: - print(f'\n Finished debugging') - - # Representation - def __str__(self): - if self.ID: return self.ID - else: return type(self).__name__ - - def __repr__(self): - if self.ID: return f'<{type(self).__name__}: {self.ID}>' - else: return f'<{type(self).__name__}>' - - def show(self): - """Prints information on unit.""" - print(self._info()) - - def to_network(self): - isa = isinstance - path = [(i.to_network() if isa(i, System) else i) for i in self.path] - network = Network.__new__(Network) - network.path = path - network.recycle = self.recycle - network.units = self.units - network.subnetworks = [i for i in path if isa(i, Network)] - network.feeds = self.feeds - network.products = self.products - return network - - def _ipython_display_(self): - try: self.diagram('minimal') - except: pass - self.show() - - def _error_info(self): - """Return information on convergence.""" - if self.recycle: - return (f"\n convergence error: Flow rate {self._mol_error:.2e} kmol/hr" - f"\n Temperature {self._T_error:.2e} K" - f"\n iterations: {self._iter}") - else: - return "" - - def _info(self): - """Return string with all specifications.""" - if self.recycle is None: - recycle = '' - else: - recycle = f"\n recycle: {self.recycle}" - error = self._error_info() - path = strtuple(self.path) - i = 1; last_i = 0 - while True: - i += 2 - i = path.find(', ', i) - i_next = path.find(', ', i+2) - if (i_next-last_i) > 35: - path = (path[:i] + '%' + path[i:]) - last_i = i - elif i == -1: break - path = path.replace('%, ', ',\n' + ' '*8) - - if self.facilities: - facilities = strtuple(self.facilities) - i = 1; last_i = 0 - while True: - i += 2 - i = facilities.find(', ', i) - if (i - last_i) > 35: - facilities = (facilities[:i] + '%' + facilities[i:]) - last_i = i - elif i == -1: break - facilities = facilities.replace('%, ', ',\n'+' '*14) - facilities = f"\n facilities: {facilities}" - else: - facilities = '' - - return (f"System: {self.ID}" - + recycle - + f"\n path: {path}" - + facilities - + error) - - -from biosteam import _flowsheet as flowsheet_module \ No newline at end of file diff --git a/build/lib/biosteam/_tea.py b/build/lib/biosteam/_tea.py deleted file mode 100644 index 119cdd8ea..000000000 --- a/build/lib/biosteam/_tea.py +++ /dev/null @@ -1,867 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon Feb 4 19:38:37 2019 - -@author: Guest Group -""" - -import pandas as pd -import numpy as np -from flexsolve import wegstein_secant, aitken_secant, secant -from copy import copy as copy_ -from thermosteam.utils import njitable - -__all__ = ('TEA', 'CombinedTEA') - - -# TODO: Add 'SL', 'DB', 'DDB', 'SYD', 'ACRS' and 'MACRS' functions to generate depreciation data - -# %% Depreciation data - -_MACRS = {'MACRS5': np.array([.2000, .3200, .1920, - .1152, .1152, .0576]), - - 'MACRS7': np.array([.1429, .2449, .1749, - .1249, .0893, .0892, - .0893, .0446]), - - 'MACRS10': np.array([.1000, .1800, .1440, - .1152, .0922, .0737, - .0655, .0655, .0656, - .0655, .0328]), - - 'MACRS15': np.array([.0500, .0950, .0855, - .0770, .0693, .0623, - .0590, .0590, .0591, - .0590, .0591, .0590, - .0591, .0590, .0591, - .0295]), - - 'MACRS20': np.array([0.03750, 0.07219, 0.06677, - 0.06177, 0.05713, 0.05285, - 0.04888, 0.04522, 0.4462, - 0.04461, 0.04462, 0.04461, - 0.04462, 0.04461, 0.04462, - 0.04461, 0.04462, 0.04461, - 0.04462, 0.04461, 0.02231])} - - -# %% Utilities - -def initial_loan_principal(loan, interest): - principal = 0 - k = 1. + interest - for loan_i in loan: - principal += loan_i - principal *= k - return principal - -@njitable -def final_loan_principal(payment, principal, interest, years): - for iter in range(years): - principal += principal * interest - payment - return principal - -def solve_payment(payment, loan, interest, years): - principal = initial_loan_principal(loan, interest) - return wegstein_secant(final_loan_principal, - payment, payment+10., 1e-4, 1e-4, - args=(principal, interest, years)) - -@njitable -def net_earnings(D, C, S, start, - FCI, TDC, VOC, FOC, sales, - income_tax, - depreciation, - startup_time, - startup_VOCfrac, - startup_FOCfrac, - startup_salesfrac): - end = start+len(depreciation) - D[start:end] = TDC*depreciation - w0 = startup_time - w1 = 1. - w0 - C[start] = (w0*startup_VOCfrac*VOC + w1*VOC - + w0*startup_FOCfrac*FOC + w1*FOC) - S[start] = w0*startup_salesfrac*sales + w1*sales - start1 = start + 1 - C[start1:] = VOC + FOC - S[start1:] = sales - return (S - C - D)*(1. - income_tax) - - -# %% Techno-Economic Analysis - -class TEA: - """ - Abstract TEA class for cash flow analysis. - - **Abstract methods** - - _TDC(DPI) -> TDC - Should take direct permanent investment as an argument - and return total depreciable capital. - _FCI(TDC) -> FCI - Should take total depreciable capital as an argument and return - fixed capital investment. - _FOC(FCI) -> FOC - Should take fixed capital investment as an arguments and return - fixed operating cost without depreciation. - - Parameters - ---------- - system : System - Should contain feed and product streams. - IRR : float - Internal rate of return (fraction). - duration : tuple[int, int] - Start and end year of venture (e.g. (2018, 2038)). - depreciation : str - 'MACRS' + number of years (e.g. 'MACRS7'). - operating_days : float - Number of operating days per year. - income_tax : float - Combined federal and state income tax rate (fraction). - lang_factor : float - Lang factor for getting fixed capital investment - from total purchase cost. If no lang factor, estimate - capital investment using bare module factors. - construction_schedule : tuple[float] - Construction investment fractions per year (e.g. (0.5, 0.5) for 50% - capital investment in the first year and 50% investment in the second). - startup_months : float - Startup time in months. - startup_FOCfrac : float - Fraction of fixed operating costs incurred during startup. - startup_VOCfrac : float - Fraction of variable operating costs incurred during startup. - startup_salesfrac : float - Fraction of sales achieved during startup. - WC_over_FCI : float - Working capital as a fraction of fixed capital investment. - finanace_interest : float - Yearly interest of capital cost financing as a fraction. - finance_years : int - Number of years the loan is paid for. - finance_fraction : float - Fraction of capital cost that needs to be financed. - - Examples - -------- - :doc:`tutorial/Techno-economic_analysis` - - """ - - __slots__ = ('system', 'income_tax', 'lang_factor', 'WC_over_FCI', - 'finance_interest', 'finance_years', 'finance_fraction', - '_construction_schedule', '_startup_time', - 'startup_FOCfrac', 'startup_VOCfrac', 'startup_salesfrac', - 'units', '_startup_schedule', '_operating_days', - '_annual_factor', '_duration', '_duration_array', - '_depreciation_array', '_depreciation', '_years', - '_duration', '_start', 'IRR', '_IRR', '_sales') - - def __init_subclass__(self, isabstract=False): - if isabstract: return - for method in ('_TDC', '_FCI', '_FOC'): - if not hasattr(self, method): - raise NotImplementedError(f"subclass must implement a '{method}' method unless the 'isabstract' keyword argument is True") - - @staticmethod - def like(system, other): - """Create a TEA object from `system` with the same settings as `other`.""" - self = copy_(other) - self.units = sorted(system._costunits, key=lambda x: x.line) - self.system = system - return self - - def __init__(self, system, IRR, duration, depreciation, income_tax, - operating_days, lang_factor, construction_schedule, - startup_months, startup_FOCfrac, startup_VOCfrac, - startup_salesfrac, WC_over_FCI, finance_interest, - finance_years, finance_fraction): - self.duration = duration - self.depreciation = depreciation - self.construction_schedule = construction_schedule - self.startup_months = startup_months - self.operating_days = operating_days - - #: [float] Internal rate of return (fraction). - self.IRR = IRR - - #: [float] Combined federal and state income tax rate (fraction). - self.income_tax = income_tax - - #: [float] Lang factor for getting fixed capital investment from total purchase cost. If no lang factor, estimate capital investment using bare module factors. - self.lang_factor = lang_factor - - #: [float] Fraction of fixed operating costs incurred during startup. - self.startup_FOCfrac = startup_FOCfrac - - #: [float] Fraction of variable operating costs incurred during startup. - self.startup_VOCfrac = startup_VOCfrac - - #: [float] Fraction of sales achieved during startup. - self.startup_salesfrac = startup_salesfrac - - #: [float] Working capital as a fraction of fixed capital investment. - self.WC_over_FCI = WC_over_FCI - - #: [float] Yearly interest of capital cost financing as a fraction. - self.finance_interest = finance_interest - - #: [int] Number of years the loan is paid for. - self.finance_years = finance_years - - #: [float] Fraction of capital cost that needs to be financed. - self.finance_fraction = finance_fraction - - #: Guess IRR for solve_IRR method - self._IRR = IRR - - #: Guess cost for solve_price method - self._sales = 0 - - #: list[Unit] All unit operations considered - self.units = sorted(system._costunits, key=lambda x: x.line) - - #: [System] Should contain feed and product streams. - self.system = system - system._TEA = self - - @property - def operating_days(self): - """[float] Number of operating days per year.""" - return self._operating_days - @operating_days.setter - def operating_days(self, days): - """[float] Number of operating days per year.""" - self._operating_days = days - self._annual_factor = days*24 - - @property - def duration(self): - """tuple[int, int] Start and end year of venture.""" - return self._duration - @duration.setter - def duration(self, duration): - self._duration = duration - self._years = duration[1] - duration[0] - - @property - def depreciation(self): - """[str] 'MACRS' + number of years (e.g. 'MACRS7').""" - return self._depreciation - @depreciation.setter - def depreciation(self, depreciation): - try: - self._depreciation_array = _MACRS[depreciation] - except KeyError: - raise ValueError(f"depreciation must be either 'MACRS5', 'MACRS7', 'MACRS10' or 'MACRS15 (not {repr(depreciation)})") - self._depreciation = depreciation - - @property - def construction_schedule(self): - """tuple[float] Construction investment fractions per year, starting from year 0. For example, for 50% capital investment in year 0 and 50% investment in year 1: (0.5, 0.5).""" - return tuple(self._construction_schedule) - @construction_schedule.setter - def construction_schedule(self, schedule): - self._construction_schedule = np.array(schedule, dtype=float) - self._start = len(schedule) - - @property - def startup_months(self): - return self._startup_time * 12. - @startup_months.setter - def startup_months(self, months): - assert months <= 12., "startup time must be less than a year" - self._startup_time = months/12. - - @property - def utility_cost(self): - """Total utility cost (USD/yr).""" - return sum([u.utility_cost for u in self.units]) * self._annual_factor - @property - def purchase_cost(self): - """Total purchase cost (USD).""" - return sum([u.purchase_cost for u in self.units]) - @property - def installation_cost(self): - """Total installation cost (USD).""" - return sum([u.installation_cost for u in self.units]) - @property - def DPI(self): - """Direct permanent investment.""" - return self.purchase_cost * self.lang_factor if self.lang_factor else self.installation_cost - @property - def TDC(self): - """Total depreciable capital.""" - return self._TDC(self.DPI) - @property - def FCI(self): - """Fixed capital investment.""" - return self._FCI(self.TDC) - @property - def TCI(self): - """Total capital investment.""" - return (1. + self.WC_over_FCI)*self.FCI - @property - def FOC(self): - """Fixed operating costs (USD/yr).""" - return self._FOC(self.FCI) - @property - def VOC(self): - """Variable operating costs (USD/yr).""" - return self.material_cost + self.utility_cost - @property - def AOC(self): - """Annual operating cost excluding depreciation (USD/yr).""" - return self.FOC + self.VOC - @property - def working_capital(self): - return self.WC_over_FCI * self.TDC - @property - def material_cost(self): - """Annual material cost.""" - return sum([s.cost for s in self.system.feeds if s.price]) * self._annual_factor - @property - def annual_depreciation(self): - """Depreciation (USD/yr) equivalent to FCI dived by the the duration of the venture.""" - return self.TDC/(self.duration[1]-self.duration[0]) - @property - def sales(self): - """Annual sales revenue.""" - return sum([s.cost for s in self.system.products if s.price]) * self._annual_factor - @property - def ROI(self): - """Return on investment (1/yr) without accounting for annualized depreciation.""" - FCI = self.FCI - net_earnings = (1-self.income_tax)*(self.sales-self._AOC(FCI)) - TCI = FCI*(1.+self.WC_over_FCI) - return net_earnings/TCI - @property - def net_earnings(self): - """Net earnings without accounting for annualized depreciation.""" - return (1-self.income_tax)*(self.sales-self.AOC) - @property - def PBP(self): - """Pay back period (yr) without accounting for annualized depreciation.""" - FCI = self.FCI - net_earnings = (1-self.income_tax)*(self.sales-self._AOC(FCI)) - return FCI/net_earnings - - def get_cashflow_table(self): - """Return DataFrame of the cash flow analysis.""" - # Cash flow data and parameters - # index: Year since construction until end of venture - # C_D: Depreciable capital - # C_FC: Fixed capital - # C_WC: Working capital - # D: Depreciation - # L: Loan revenue - # LI: Loan interest payment - # LP: Loan payment - # LPl: Loan principal - # C: Annual operating cost (excluding depreciation) - # S: Sales - # NE: Net earnings - # CF: Cash flow - # DF: Discount factor - # NPV: Net present value - # CNPV: Cumulative NPV - TDC = self.TDC - FCI = self._FCI(TDC) - start = self._start - years = self._years - FOC = self._FOC(FCI) - VOC = self.VOC - sales = self.sales - self._duration_array = np.arange(-start+1, years+1, dtype=float) - length = start+years - C_D, C_FC, C_WC, D, L, LI, LP, LPl, C, S, NE, CF, DF, NPV, CNPV = data = np.zeros((15, length)) - depreciation = self._depreciation_array - D[start:start+len(depreciation)] = TDC*depreciation - w0 = self._startup_time - w1 = 1. - w0 - C[start] = (w0*self.startup_VOCfrac*VOC + w1*VOC - + w0*self.startup_FOCfrac*FOC + w1*FOC) - S[start] = w0*self.startup_salesfrac*sales + w1*sales - start1 = start + 1 - C[start1:] = VOC + FOC - S[start1:] = sales - NE[:] = (S - C - D)*(1. - self.income_tax) - WC = self.WC_over_FCI * FCI - C_D[:start] = TDC*self._construction_schedule - C_FC[:start] = FCI*self._construction_schedule - C_WC[start-1] = WC - C_WC[-1] = -WC - if self.finance_interest: - interest = self.finance_interest - years = self.finance_years - end = start+years - L[:start] = loan = self.finance_fraction*(C_FC[:start]+C_WC[:start]) - f_interest = (1.+interest) - LP[start:end] = solve_payment(loan.sum()/years * f_interest, - loan, interest, years) - loan_principal = 0 - for i in range(end): - LI[i] = li = (loan_principal + L[i]) * interest - LPl[i] = loan_principal = loan_principal - LP[i] + li + L[i] - CF[:] = NE + D + L - C_FC - C_WC - LP - else: - CF[:] = NE + D - C_FC - C_WC - DF[:] = 1/(1.+self.IRR)**self._duration_array - NPV[:] = CF*DF - CNPV[:] = NPV.cumsum() - return pd.DataFrame(data.transpose(), - index=np.arange(self._duration[0]-start, self._duration[1]), - columns=('Depreciable capital', - 'Fixed capital investment', - 'Working capital', - 'Depreciation', - 'Loan', - 'Loan interest payment', - 'Loan payment', - 'Loan principal', - 'Annual operating cost (excluding depreciation)', - 'Sales', - 'Net earnings', - 'Cash flow', - 'Discount factor', - 'Net present value (NPV)', - 'Cumulative NPV')) - @property - def NPV(self): - """Net present value.""" - return self._NPV_at_IRR(self.IRR, self.cashflow) - - def _AOC(self, FCI): - """Return AOC at given FCI""" - return self._FOC(FCI) + self.VOC - - def production_cost(self, products, with_annual_depreciation=True): - """Return production cost of products [USD/yr]. - - Parameters - ---------- - products : Iterable[:class:`~thermosteam.Stream`] - Main products of the system - with_annual_depreciation=True : bool, optional - - Notes - ----- - If there is more than one main product, The production cost is - proportionally allocated to each of the main products with respect to - their marketing values. The marketing value of each product is - determined by the annual production multiplied by its selling price. - """ - market_values = np.array([i.cost for i in products]) - total_market_value = market_values.sum() - weights = market_values/total_market_value - return weights * self.total_production_cost(products, with_annual_depreciation) - - def total_production_cost(self, products, with_annual_depreciation): - """Return total production cost of products [USD/yr]. - - Parameters - ---------- - products : Iterable[:class:`~thermosteam.Stream`] - Main products of the system - with_annual_depreciation=True : bool, optional - - """ - coproducts = self.system.products.difference(products) - coproduct_sales = sum([s.cost for s in coproducts if s.price]) * self._annual_factor - if with_annual_depreciation: - TDC = self.TDC - annual_depreciation = TDC/(self.duration[1]-self.duration[0]) - AOC = self._AOC(self._FCI(TDC)) - return AOC + coproduct_sales + annual_depreciation - else: - return self.AOC + coproduct_sales - - @property - def cashflow(self): - # Cash flow data and parameters - # C_FC: Fixed capital - # C_WC: Working capital - # Loan: Money gained from loan - # LP: Loan payment - # D: Depreciation - # C: Annual operating cost (excluding depreciation) - # S: Sales - # NE: Net earnings - # CF: Cash flow - TDC = self.TDC - FCI = self._FCI(TDC) - start = self._start - years = self._years - FOC = self._FOC(FCI) - VOC = self.VOC - self._duration_array = np.arange(-start+1, years+1, dtype=float) - D, C_FC, C_WC, Loan, LP, C, S = np.zeros((7, start+years)) - NE = net_earnings(D, C, S, start, - FCI, TDC, VOC, FOC, self.sales, - self.income_tax, - self._depreciation_array, - self._startup_time, - self.startup_VOCfrac, - self.startup_FOCfrac, - self.startup_salesfrac) - WC = self.WC_over_FCI * FCI - C_FC[:start] = FCI*self._construction_schedule - C_WC[start-1] = WC - C_WC[-1] = -WC - if self.finance_interest: - interest = self.finance_interest - years = self.finance_years - Loan[:start] = loan = self.finance_fraction*(C_FC[:start]+C_WC[:start]) - LP[start:start+years] = solve_payment(loan.sum()/years * (1. + interest), - loan, interest, years) - return NE + D + Loan - C_FC - C_WC - LP - else: - return NE + D - C_FC - C_WC - - def _NPV_at_IRR(self, IRR, cashflow): - """Return NPV at given IRR and cashflow data.""" - return (cashflow/(1.+IRR)**self._duration_array).sum() - - def _NPV_with_sales(self, sales, NPV, coefficients, discount_factors): - """Return NPV with an additional annualized sales.""" - return NPV + (sales*coefficients/discount_factors).sum() - - def solve_IRR(self): - """Return the IRR at the break even point (NPV = 0) through cash flow analysis.""" - try: - self._IRR = wegstein_secant(self._NPV_at_IRR, - self._IRR, self._IRR+1e-6, - xtol=1e-6, maxiter=200, - args=(self.cashflow,)) - except: - self._IRR = secant(self._NPV_at_IRR, - 0.15, 0.15001, - xtol=1e-4, maxiter=200, - args=(self.cashflow,)) - return self._IRR - - def _price2cost(self, stream): - """Get factor to convert stream price to cost for cashflow in solve_price method.""" - return stream.F_mass*self._annual_factor*(1-self.income_tax) - - def solve_price(self, stream): - """Return the price (USD/kg) of stream at the break even point (NPV = 0) through cash flow analysis. - - Parameters - ---------- - stream : :class:`~thermosteam.Stream` - Stream with variable selling price. - - """ - price2cost = self._price2cost(stream) - discount_factors = (1 + self.IRR)**self._duration_array - cashflow = self.cashflow - NPV = (cashflow/discount_factors).sum() - coefficients = np.ones_like(discount_factors) - start = self._start - coefficients[:start] = 0 - w0 = self._startup_time - coefficients[self._start] = w0*self.startup_VOCfrac + (1-w0) - try: - self._sales = wegstein_secant(self._NPV_with_sales, - self._sales, self._sales+1e-6, - xtol=1e-6, maxiter=200, - args=(NPV, coefficients, discount_factors)) - except: - self._sales = secant(self._NPV_with_sales, - 0, 1e-6, - xtol=1e-6, maxiter=200, - args=(NPV, coefficients, discount_factors)) - if stream.sink: - return stream.price - self._sales/price2cost - elif stream.source: - return stream.price + self._sales/price2cost - else: - raise ValueError(f"stream must be either a feed or a product") - - def __repr__(self): - return f'<{type(self).__name__}: {self.system.ID}>' - - def _info(self): - return (f'{type(self).__name__}: {self.system.ID}\n' - f' NPV: {self.NPV:.3g} USD at {self.IRR:.1%} IRR\n' - f' ROI: {self.ROI:.3g} 1/yr\n' - f' PBP: {self.PBP:.3g} yr') - - def show(self): - """Prints information on unit.""" - print(self._info()) - _ipython_display_ = show - - -class CombinedTEA(TEA): - """ - Create a CombinedTEA object that performs techno-economic analysis by - using data from many TEA objects that correspond to different areas - of a biorefinery. A CombinedTEA object serves to accomodate for areas of - a biorefinery with different assumptions. For example, an area of a - biorefinery may not operate the same days a year, may have different - depreciation schedules, and may even be taxed differently than other - areas. Ultimately, a CombinedTEA object is an aggregation of TEA objects - that work together to conduct techno-economic analysis of a whole biorefinery. - - Parameters - ---------- - TEAs : Iterable[TEA] - TEA objects used to conduct techno-economic analysis. - - """ - _TDC = _FCI = _FOC = NotImplemented - - __slots__ = ('TEAs',) - - def __init__(self, TEAs, IRR): - #: Iterable[TEA] All TEA objects for cashflow calculation - self.TEAs = TEAs - - #: [float] Internal rate of return (fraction) - self.IRR = IRR - - #: Guess IRR for solve_IRR method - self._IRR = IRR - - #: Guess sales for solve_price method - self._sales = 0 - - @property - def operating_days(self): - v_all = [i.operating_days for i in self.TEAs] - v0, *vs = v_all - if all([v0 == v for v in vs]): return v0 - else: return tuple(v_all) - @operating_days.setter - def operating_days(self, operating_days): - vector = np.zeros(len(self.TEAs)) - vector[:] = operating_days - for i, j in zip(self.TEAs, vector): i.operating_days = j - @property - def startup_months(self): - v_all = [i.startup_months for i in self.TEAs] - v0, *vs = v_all - if all([v0 == v for v in vs]): return v0 - else: return tuple(v_all) - @startup_months.setter - def startup_months(self, startup_months): - vector = np.zeros(len(self.TEAs)) - vector[:] = startup_months - for i, j in zip(self.TEAs, vector): i.startup_months = j - @property - def income_tax(self): - v_all = [i.income_tax for i in self.TEAs] - v0, *vs = v_all - if all([v0 == v for v in vs]): return v0 - else: return tuple(v_all) - @income_tax.setter - def income_tax(self, income_tax): - vector = np.zeros(len(self.TEAs)) - vector[:] = income_tax - for i, j in zip(self.TEAs, vector): i.income_tax = j - @property - def cashflow(self): - return sum([i.cashflow for i in self.TEAs]) - @property - def utility_cost(self): - """Total utility cost (USD/yr).""" - return sum([i.utility_cost for i in self.TEAs]) - @property - def purchase_cost(self): - """Total purchase cost (USD).""" - return sum([i.purchase_cost for i in self.TEAs]) - @property - def installation_cost(self): - """Total installation cost (USD).""" - return sum([i.installation_cost for i in self.TEAs]) - @property - def NPV(self): - return sum([i.NPV for i in self.TEAs]) - @property - def DPI(self): - """Direct permanent investment.""" - return sum([i.DPI for i in self.TEAs]) - @property - def TDC(self): - """Total depreciable capital.""" - return sum([i.TDC for i in self.TEAs]) - @property - def FCI(self): - """Fixed capital investment.""" - return sum([i.FCI for i in self.TEAs]) - @property - def TCI(self): - """Total capital investment.""" - return sum([i.TCI for i in self.TEAs]) - @property - def FOC(self): - """Fixed operating costs (USD/yr).""" - return sum([i.FOC for i in self.TEAs]) - @property - def VOC(self): - """Variable operating costs (USD/yr).""" - return self.material_cost + self.utility_cost - @property - def AOC(self): - """Annual operating cost excluding depreciation (USD/yr).""" - return self.FOC + self.VOC - @property - def working_capital(self): - return sum([i.working_capital for i in self.TEAs]) - @property - def material_cost(self): - """Annual material cost.""" - return sum([i.material_cost for i in self.TEAs]) - @property - def annual_depreciation(self): - """Depreciation (USD/yr) equivalent to FCI dived by the the duration of the venture.""" - return sum([i.annual_depreciation for i in self.TEAs]) - @property - def sales(self): - """Annual sales revenue.""" - return sum([i.sales for i in self.TEAs]) - @property - def net_earnings(self): - """Net earnings without accounting for annualized depreciation.""" - return sum([i.net_earnings for i in self.TEAs]) - @property - def ROI(self): - """Return on investment (1/yr) without accounting for annualized depreciation.""" - return sum([i.ROI for i in self.TEAs]) - @property - def PBP(self): - """Pay back period (yr) without accounting for annualized depreciation.""" - return self.FCI/self.net_earnings - - def get_cashflow_table(self): - """Return DataFrame of the cash flow analysis.""" - TEA, *TEAs = self.TEAs - table = TEA.get_cashflow_table() - DF = table['Discount factor'] - DF_data = np.array(DF) - for i in TEAs: - i_table = i.get_cashflow_table() - if (i_table.index != table.index).any(): - raise NotImplementedError('cannot yet create cashflow table from TEAs with different venture years') - table[:] += np.asarray(i_table) - DF[:] = DF_data - return table - - def _NPV_at_IRR(self, IRR, TEA_cashflows): - """Return NPV at given IRR and cashflow data.""" - return sum([i._NPV_at_IRR(IRR, j) for i, j in TEA_cashflows]) - - def _NPV_with_sales(self, sales, NPV, TEA, coefficients, discount_factors): - """Return NPV with additional sales.""" - return TEA._NPV_with_sales(sales, NPV, coefficients, discount_factors) - - def production_cost(self, products, with_annual_depreciation=True): - """Return production cost of products [USD/yr]. - - Parameters - ---------- - products : Iterable[:class:`~thermosteam.Stream`] - Main products of the system - with_annual_depreciation=True : bool, optional - - Notes - ----- - If there is more than one main product, The production cost is - proportionally allocated to each of the main products with respect to - their marketing values. The marketing value of each product is - determined by the annual production multiplied by its selling price. - """ - market_values = np.array([i.cost for i in products]) - total_market_value = market_values.sum() - weights = market_values/total_market_value - total_production_cost = 0 - for TEA in self.TEAs: - total_production_cost += TEA.total_production_cost(products, with_annual_depreciation) - return weights * total_production_cost - - def solve_IRR(self): - """Return the IRR at the break even point (NPV = 0) through cash flow analysis.""" - try: - self._IRR = aitken_secant(self._NPV_at_IRR, - self._IRR, self._IRR+1e-6, - xtol=1e-8, maxiter=200, - args=([(i, i.cashflow) - for i in self.TEAs],)) - except: - self._IRR = secant(self._NPV_at_IRR, - 0.15, 0.15001, - xtol=5e-8, maxiter=200, - args=([(i, i.cashflow) - for i in self.TEAs],)) - return self._IRR - - def solve_price(self, stream, TEA=None): - """Return the price (USD/kg) of stream at the break even point (NPV = 0) through cash flow analysis. - - Parameters - ---------- - stream : :class:`~thermosteam.Stream` - Stream with variable selling price. - TEA : TEA, optional - Stream should belong here. - - """ - if not TEA: - for TEA in self.TEAs: - if stream in TEA.system.feeds or stream in TEA.system.products: break - price2cost = TEA._price2cost(stream) - IRR = self.IRR - NPV = sum([i._NPV_at_IRR(IRR, i.cashflow) for i in self.TEAs]) - discount_factors = (1.+IRR)**TEA._duration_array - coefficients = np.ones_like(discount_factors) - start = TEA._start - coefficients[:start] = 0 - w0 = TEA._startup_time - coefficients[TEA._start] = w0*TEA.startup_VOCfrac + (1-w0) - try: - self._sales = aitken_secant(self._NPV_with_sales, - self._sales, self._sales+1e-6, - xtol=5e-8, maxiter=200, - args=(NPV, TEA, - coefficients, - discount_factors)) - except: - self._sales = secant(self._NPV_with_sales, - 0, 1e-6, - xtol=5e-8, maxiter=200, - args=(NPV, TEA, - coefficients, - discount_factors)) - if stream.sink: - return stream.price - self._sales/price2cost - elif stream.source: - return stream.price + self._sales/price2cost - else: - raise ValueError(f"stream must be either a feed or a product") - - def __repr__(self): - return f'<{type(self).__name__}: {", ".join([i.system.ID for i in self.TEAs])}>' - - def _info(self): - return (f'{type(self).__name__}: {", ".join([i.system.ID for i in self.TEAs])}\n' - f' NPV: {self.NPV:.3g} USD at {self.IRR:.1%} IRR\n' - f' ROI: {self.ROI:.3g} 1/yr\n' - f' PBP: {self.PBP:.3g} yr') - - def show(self): - """Prints information on unit.""" - print(self._info()) - _ipython_display_ = show - - - -# def update_loan_principal(loan_principal, loan, loan_payment, interest): -# principal = 0 -# for i, loan_i in enumerate(loan): -# loan_principal[i] = principal = loan_i + principal * interest - loan_payment[i] \ No newline at end of file diff --git a/build/lib/biosteam/_unit.py b/build/lib/biosteam/_unit.py deleted file mode 100644 index 06ebef9f7..000000000 --- a/build/lib/biosteam/_unit.py +++ /dev/null @@ -1,750 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed Dec 18 07:18:50 2019 - -@author: yoelr -""" - -import numpy as np -import pandas as pd -from graphviz import Digraph -from ._graphics import UnitGraphics, box_graphics -from thermosteam import Stream -from ._heat_utility import HeatUtility -from .utils import Ins, Outs, NotImplementedMethod, \ - format_unit_line, static -from ._power_utility import PowerUtility -from ._digraph import finalize_digraph -from thermosteam.utils import thermo_user, registered -from thermosteam.base import convert -import biosteam as bst - -__all__ = ('Unit',) - -# %% Unit Operation - -@thermo_user -@registered(ticket_name='U') -class Unit: - """Abstract parent class for Unit objects. Child objects must contain `_run`, `_design` and `_cost` methods to estimate stream outputs of a Unit and find design and cost information. - - **Abstract class methods** - - _setup() - Set stream conditions and constant data. - _run() - Run simulation and update output streams. - _design() - Add design requirements to the `design_results` dictionary. - _cost() - Add itemized purchse costs to the `purchase_costs` dictionary. - - **Abstract class attributes** - - **line=None** - [str] Name denoting the type of Unit class. Defaults to the class - name of the first child class. - **BM=None** - [float] Bare module factor (installation factor). - **_units={}** - [dict] Units of measure for `design_results` dictionary. - **_N_ins=1** - [int] Expected number of input streams. - **_N_outs=2** - [int] Expected number of output streams. - **_ins_size_is_fixed=True** - [bool] Whether the number of streams in ins is fixed. - **_outs_size_is_fixed=True** - [bool] Whether the number of streams in outs is fixed. - **_N_heat_utilities=0** - [int] Number of heat utilities created with each instance - **_graphics** - [biosteam.Graphics, abstract, optional] Settings for diagram - representation. Defaults to a box with the same number of input - and output edges as `_N_ins` and `_N_outs` - - Parameters - ---------- - ID='' : str, defaults to a unique ID - A unique identification. If ID is None, unit will not be - registered in flowsheet. - ins=None : Iterable[:class:`~thermosteam.Stream`, or str], :class:`~thermosteam.Stream`, or str - Inlet streams or IDs to initialize input streams. - If empty, default IDs will be given. If None, defaults to missing streams. - outs=() : Iterable[:class:`~thermosteam.Stream`, or str], :class:`~thermosteam.Stream`, or str - Outlet streams or IDs to initialize output streams. - If empty, default IDs will be given. - If None, leave streams missing. - thermo=None : Thermo - Thermo object to initialize inlet and outlet streams. Defaults to - `biosteam.settings.get_thermo()`. - - Attributes - ---------- - ins : Ins[:class:`~thermosteam.Stream`] - Input streams. - outs : Outs[:class:`~thermosteam.Stream`] - Output streams. - power_utility : PowerUtility - Electricity rate requirements are stored here. - heat_utilities : tuple[HeatUtility] - Cooling and heating requirements are stored here. - design_results : dict - All design requirements. - purchase_costs : dict - Itemized purchase costs. - thermo : Thermo - The thermodynamic property package used by the unit. - - - Examples - -------- - :doc:`tutorial/Creating_a_Unit` - - :doc:`tutorial/Using_-pipe-_notation` - - :doc:`tutorial/Inheriting_from_Unit` - - :doc:`tutorial/Unit_decorators` - - """ - # Settings - IPYTHON_DISPLAY_UNIT_OPERATIONS = True - - - def __init_subclass__(cls, - isabstract=False, - new_graphics=True): - dct = cls.__dict__ - if 'line' not in dct: - cls.line = format_unit_line(cls.__name__) - if '_graphics' not in dct and new_graphics: - # Set new graphics for specified line - cls._graphics = UnitGraphics.box(cls._N_ins, cls._N_outs) - if not (isabstract or cls._run): static(cls) - - ### Abstract Attributes ### - - # [float] Bare module factor (installation factor). - BM = 1. - - # [dict] Units for construction and design results - _units = {} - - # [int] Expected number of input streams - _N_ins = 1 - - # [int] Expected number of output streams - _N_outs = 2 - - # [bool] Whether the number of streams in ins is fixed - _ins_size_is_fixed = True - - # [bool] Whether the number of streams in outs is fixed - _outs_size_is_fixed = True - - # [int] number of heat utilities - _N_heat_utilities = 0 - - # [StreamLinkOptions] Options of linking streams - _stream_link_options = None - - # [biosteam Graphics] a Graphics object for diagram representation. - _graphics = box_graphics - - # [str] The general type of unit, regardless of class - line = 'Unit' - - ### Other defaults ### - - def __init__(self, ID='', ins=None, outs=(), thermo=None): - self._numerical_specification = None - self._load_thermo(thermo) - self._init_ins(ins) - self._init_outs(outs) - self._init_utils() - self._init_results() - self._assert_compatible_property_package() - self._register(ID) - - def _init_ins(self, ins): - # Ins[:class:`~thermosteam.Stream`] Input streams - self._ins = Ins(self, self._N_ins, ins, self._thermo, self._ins_size_is_fixed) - - def _init_outs(self, outs): - # Outs[:class:`~thermosteam.Stream`] Output streams - self._outs = Outs(self, self._N_outs, outs, self._thermo, self._outs_size_is_fixed) - - def _init_utils(self): - # tuple[HeatUtility] All heat utilities associated to unit - self.heat_utilities = tuple([HeatUtility() for i in - range(self._N_heat_utilities)]) - - # [PowerUtility] Electric utility associated to unit - self.power_utility = PowerUtility() - - def _init_results(self): - # [dict] All purchase cost results in USD. - self.purchase_costs = {} - - # [dict] All design results. - self.design_results = {} - - # [dict] Greenhouse gas emissions - self._GHGs = {} - - def _assert_compatible_property_package(self): - chemical_IDs = self.chemicals.IDs - streams = self._ins + self._outs - assert all([s.chemicals.IDs == chemical_IDs for s in streams if s]), ( - "unit operation chemicals are incompatible with inlet and outlet streams; " - "try using the `thermo` keyword argument to initialize the unit operation " - "with a compatible thermodynamic property package") - - def disconnect(self): - self._ins[:] = () - self._outs[:] = () - - def get_node(self): - """Return unit node attributes for graphviz.""" - try: self._load_stream_links() - except: pass - return self._graphics.get_node_taylored_to_unit(self) - - def get_design_result(self, key, units): - return convert(self.design_results[key], self._units[key], units) - - def take_place_of(self, other): - """Replace inlets and outlets from this unit with that of another unit.""" - self.ins[:] = other.ins - self.outs[:] = other.outs - - # Forward pipping - def __sub__(self, other): - """Source streams.""" - isa = isinstance - int_types = (int, np.int) - if isa(other, Unit): - other._ins[:] = self._outs - return other - elif isa(other, int_types): - return self.outs[other] - elif isa(other, Stream): - self._outs[:] = (other,) - return self - elif isa(other, (tuple, list, np.ndarray)): - if all(isa(i, (int, np.int)) for i in other): - return [self._outs[i] for i in other] - else: - if isa(other, Unit): - self._outs[:] = other._ins - return other - else: - self._outs[:] = other - return self - else: - return other.__rsub__(self) - - def __rsub__(self, other): - """Sink streams.""" - isa = isinstance - int_types = (int, np.int) - if isa(other, int_types): - return self._ins[other] - elif isa(other, Stream): - self._ins[:] = (other,) - return self - elif isa(other, (tuple, list, np.ndarray)): - if all(isa(i, int_types) for i in other): - return [self._ins[i] for i in other] - else: - if isa(other, Unit): - self._ins[:] = other._outs - return other - else: - self._ins[:] = other - return self - else: - raise ValueError(f"cannot pipe '{type(other).__name__}' object into unit") - - # Backwards pipping - __pow__ = __sub__ - __rpow__ = __rsub__ - - # Abstract methods - _run = NotImplementedMethod - _setup = NotImplementedMethod - _design = NotImplementedMethod - _cost = NotImplementedMethod - - def _get_design_info(self): - return () - - def _load_stream_links(self): - options = self._stream_link_options - if options: - s_in = self._ins[0] - s_out = self._outs[0] - s_out.link_with(s_in, options.flow, options.phase, options.TP) - - def _summary(self): - """Calculate all results from unit run.""" - self._design() - self._cost() - - @property - def numerical_specification(self): - """Numerical design or process specification.""" - return self._numerical_specification - @numerical_specification.setter - def numerical_specification(self, numerical_specification): - if numerical_specification: - if not callable(numerical_specification): - raise ValueError("numerical specification must a callable or None.") - self._numerical_specification = numerical_specification - - @property - def purchase_cost(self): - """Total purchase cost [USD].""" - return sum(self.purchase_costs.values()) - - @property - def installation_cost(self): - """Installation cost [USD].""" - return self.BM * sum(self.purchase_costs.values()) - - @property - def utility_cost(self): - """Total utility cost [USD/hr].""" - return sum([i.cost for i in self.heat_utilities]) + self.power_utility.cost - - def simulate(self): - """Run rigourous simulation and determine all design requirements. No design specifications are solved.""" - self._load_stream_links() - self._setup() - self._run() - self._summary() - - def results(self, with_units=True, include_utilities=True, - include_total_cost=True): - """Return key results from simulation as a DataFrame if `with_units` is True or as a Series otherwise.""" - # TODO: Divide this into functions - keys = []; addkey = keys.append - vals = []; addval = vals.append - if with_units: - if include_utilities: - power_utility = self.power_utility - if power_utility: - addkey(('Power', 'Rate')) - addkey(('Power', 'Cost')) - addval(('kW', power_utility.rate)) - addval(('USD/hr', power_utility.cost)) - for heat_utility in self.heat_utilities: - if heat_utility: - ID = heat_utility.ID.replace('_', ' ').capitalize() - addkey((ID, 'Duty')) - addkey((ID, 'Flow')) - addkey((ID, 'Cost')) - addval(('kJ/hr', heat_utility.duty)) - addval(('kmol/hr', heat_utility.flow)) - addval(('USD/hr', heat_utility.cost)) - units = self._units - Cost = self.purchase_costs - for ki, vi in self.design_results.items(): - addkey(('Design', ki)) - addval((units.get(ki, ''), vi)) - for ki, vi, ui in self._get_design_info(): - addkey(('Design', ki)) - addval((ui, vi)) - for ki, vi in Cost.items(): - addkey(('Purchase cost', ki)) - addval(('USD', vi)) - if include_total_cost: - addkey(('Total purchase cost', '')) - addval(('USD', self.purchase_cost)) - addkey(('Utility cost', '')) - addval(('USD/hr', self.utility_cost)) - if self._GHGs: - a, b = self._totalGHG - GHG_units = self._GHG_units - for ko, vo in self._GHGs.items(): - for ki, vi in vo.items(): - addkey((ko, ki)) - addval((GHG_units.get(ko, ''), vi)) - a_key, b_key = GHG_units.keys() - a_unit, b_unit = GHG_units.values() - addkey(('Total ' + a_key, '')) - addval((a_unit, a)) - addkey(('Total ' + b_key, '')) - addval((b_unit, b)) - if not keys: return None - df = pd.DataFrame(vals, - pd.MultiIndex.from_tuples(keys), - ('Units', self.ID)) - df.columns.name = self.line - return df - else: - if include_utilities: - power_utility = self.power_utility - if power_utility: - addkey(('Power', 'Rate')) - addkey(('Power', 'Cost')) - addval(power_utility.rate) - addval(power_utility.cost) - for heat_utility in self.heat_utilities: - if heat_utility: - if heat_utility: - ID = heat_utility.ID.replace('_', ' ').capitalize() - addkey((ID, 'Duty')) - addkey((ID, 'Flow')) - addkey((ID, 'Cost')) - addval(heat_utility.duty) - addval(heat_utility.flow) - addval(heat_utility.cost) - for ki, vi in self.design_results.items(): - addkey(('Design', ki)) - addval(vi) - for ki, vi, ui in self._get_design_info(): - addkey(('Design', ki)) - addval((ui, vi)) - for ki, vi in self.purchase_costs.items(): - addkey(('Purchase cost', ki)) - addval(vi) - if self._GHGs: - GHG_units = self._GHG_units - for ko, vo in self._GHGs.items(): - for ki, vi in vo.items(): - addkey((ko, ki)) - addval(vi) - a, b = self._totalGHG - a_key, b_key = GHG_units.keys() - addkey(('Total ' + a_key, '')) - addval(a) - addkey(('Total ' + b_key, '')) - addval(b) - if include_total_cost: - addkey(('Total purchase cost', '')) - addval(self.purchase_cost) - addkey(('Utility cost', '')) - addval(self.utility_cost) - if not keys: return None - series = pd.Series(vals, pd.MultiIndex.from_tuples(keys)) - series.name = self.ID - return series - - @property - def thermo(self): - """Thermodynamic property package""" - return self._thermo - @property - def ins(self): - """All input streams.""" - return self._ins - @property - def outs(self): - """All output streams.""" - return self._outs - - def _add_upstream_neighbors_to_set(self, set): - """Add upsteam neighboring units to set.""" - for s in self._ins: - u_source = s._source - if u_source: set.add(u_source) - - def _add_downstream_neighbors_to_set(self, set): - """Add downstream neighboring units to set.""" - for s in self._outs: - u_sink = s._sink - if u_sink: set.add(u_sink) - - @property - def _downstream_units(self): - """Return a set of all units downstream.""" - downstream_units = set() - outer_periphery = set() - self._add_downstream_neighbors_to_set(outer_periphery) - inner_periphery = None - old_length = -1 - new_length = 0 - while new_length != old_length: - old_length = new_length - inner_periphery = outer_periphery - downstream_units.update(inner_periphery) - outer_periphery = set() - for unit in inner_periphery: - unit._add_downstream_neighbors_to_set(outer_periphery) - new_length = len(downstream_units) - return downstream_units - - def _neighborhood(self, radius=1, upstream=True, downstream=True): - """Return a set of all neighboring units within given radius. - - Parameters - ---------- - radius : int - Maxium number streams between neighbors. - downstream=True : bool, optional - Whether to include downstream operations - upstream=True : bool, optional - Whether to include upstream operations - - """ - radius -= 1 - neighborhood = set() - if radius < 0: return neighborhood - if upstream:self._add_upstream_neighbors_to_set(neighborhood) - if downstream: self._add_downstream_neighbors_to_set(neighborhood) - direct_neighborhood = neighborhood - for i in range(radius): - neighbors = set() - for unit in direct_neighborhood: - if upstream: unit._add_upstream_neighbors_to_set(neighbors) - if downstream: unit._add_downstream_neighbors_to_set(neighbors) - if neighbors == direct_neighborhood: break - direct_neighborhood = neighbors - neighborhood.update(direct_neighborhood) - return neighborhood - - def get_digraph(self, format='png', **graph_attrs): - ins = self.ins - outs = self.outs - graphics = self._graphics - - # Make a Digraph handle - f = Digraph(name='unit', filename='unit', format=format) - f.attr('graph', ratio='0.5', splines='normal', outputorder='edgesfirst', - nodesep='1.1', ranksep='0.8', maxiter='1000') # Specifications - f.attr(rankdir='LR', **graph_attrs) # Left to right - - # If many streams, keep streams close - if (len(ins) >= 3) or (len(outs) >= 3): - f.attr('graph', nodesep='0.4') - - # Initialize node arguments based on unit and make node - node = graphics.get_node_taylored_to_unit(self) - f.node(**node) - - # Set stream node attributes - f.attr('node', shape='rarrow', fillcolor='#79dae8', - style='filled', orientation='0', width='0.6', - height='0.6', color='black', peripheries='1') - - # Make nodes and edges for input streams - di = 0 # Destination position of stream - for stream in ins: - if not stream: continue - f.node(stream.ID) - edge_in = graphics.edge_in - if di >= len(edge_in): di = 0 - f.attr('edge', arrowtail='none', arrowhead='none', - tailport='e', **edge_in[di]) - f.edge(stream.ID, node['name']) - di += 1 - - # Make nodes and edges for output streams - oi = 0 # Origin position of stream - for stream in outs: - if not stream: continue - f.node(stream.ID) - edge_out = graphics.edge_out - if oi >= len(edge_out): oi = 0 - f.attr('edge', arrowtail='none', arrowhead='none', - headport='w', **edge_out[oi]) - f.edge(node['name'], stream.ID) - oi += 1 - return f - - def diagram(self, radius=0, upstream=True, downstream=True, - file=None, format='png', **graph_attrs): - """ - Display a `Graphviz `__ diagram - of the unit and all neighboring units within given radius. - - Parameters - ---------- - radius : int - Maxium number streams between neighbors. - downstream=True : bool, optional - Whether to show downstream operations - upstream=True : bool, optional - Whether to show upstream operations - file : Must be one of the following: - * [str] File name to save diagram. - * [None] Display diagram in console. - format : str - Format of file. - - """ - if radius > 0: - neighborhood = self._neighborhood(radius, upstream, downstream) - neighborhood.add(self) - sys = bst.System('', neighborhood) - return sys.diagram('thorough', file, format, **graph_attrs) - f = self.get_digraph(format, **graph_attrs) - finalize_digraph(f, file, format) - - ### Net input and output flows ### - - # Molar flow rates - @property - def mol_in(self): - """Molar flows going in [kmol/hr].""" - return sum([s.mol for s in self._ins if s]) - @property - def mol_out(self): - """Molar flows going out [kmol/hr].""" - return sum([s.mol for s in self._outs if s]) - - @property - def z_mol_in(self): - """Molar fractions going in [kmol/hr].""" - return self._mol_in/self.F_mol_in - @property - def z_mol_out(self): - """Molar fractions going in.""" - return self._mol_out/self.F_mol_out - - @property - def F_mol_in(self): - """Net molar flow going in [kmol/hr].""" - return sum([s.F_mol for s in self._ins if s]) - @property - def F_mol_out(self): - """Net molar flow going out [kmol/hr].""" - return sum([s.F_mol for s in self._outs if s]) - - # Mass flow rates - @property - def mass_in(self): - """Mass flows going in [kg/hr].""" - return sum([s.mol for s in self._ins if s]) * self._thermo.chemicals.MW - @property - def mass_out(self): - """Mass flows going out [kg/hr].""" - return sum([s.mol for s in self._outs if s]) * self._thermo.chemicals.MW - - @property - def z_mass_in(self): - """Mass fractions going in.""" - return self.mass_in/self.F_mass_in - @property - def z_mass_out(self): - """Mass fractions going out.""" - return self.mass_out/self.F_mass_out - - @property - def F_mass_in(self): - """Net mass flow going in [kg/hr].""" - return self.mass_in.sum() - @property - def F_mass_out(self): - """Net mass flow going out [kg/hr].""" - return self.mass_out.sum() - - # Volumetric flow rates - @property - def vol_in(self): - """Volumetric flows going in [m3/hr].""" - return sum([s.vol for s in self._ins if s]) - @property - def F_vol_in(self): - """Net volumetric flow going in [m3/hr].""" - return sum(self.vol_in) - - @property - def z_vol_in(self): - """Volumetric fractions going in.""" - return self.vol_in/self.F_vol_in - @property - def vol_out(self): - """Volumetric flows going out [m3/hr].""" - return sum([s.vol for s in self._outs if s]) - - @property - def F_vol_out(self): - """Net volumetric flow going out [m3/hr].""" - return sum(self.vol_out) - @property - def z_vol_out(self): - """Volumetric fractions going out.""" - return self.vol_out/self.F_vol_out - - # Enthalpy flow rates - @property - def H_in(self): - """Enthalpy flow going in [kJ/hr].""" - return sum([s.H for s in self._ins if s]) - - @property - def H_out(self): - """Enthalpy flow going out [kJ/hr].""" - return sum([s.H for s in self._outs if s]) - - @property - def Hf_in(self): - """Enthalpy of formation flow going in [kJ/hr].""" - return sum([s.Hf for s in self._ins if s]) - - @property - def Hf_out(self): - """Enthalpy of formation flow going out [kJ/hr].""" - return sum([s.Hf for s in self._outs if s]) - - @property - def Hnet(self): - """Net enthalpy flow, including enthalpies of formation [kJ/hr].""" - return self.H_out - self.H_in + self.Hf_out - self.Hf_in - - # Representation - def _info(self, T, P, flow, composition, N): - """Information on unit.""" - if self.ID: - info = f'{type(self).__name__}: {self.ID}\n' - else: - info = f'{type(self).__name__}\n' - info += f'ins...\n' - i = 0 - for stream in self.ins: - if not stream: - info += f'[{i}] {stream}\n' - i += 1 - continue - stream_info = stream._info(T, P, flow, composition, N) - unit = stream._source - index = stream_info.index('\n') - source_info = f' from {type(unit).__name__}-{unit}\n' if unit else '\n' - info += f'[{i}] {stream.ID}' + source_info + stream_info[index+1:] + '\n' - i += 1 - info += f'outs...\n' - i = 0 - for stream in self.outs: - if not stream: - info += f'[{i}] {stream}\n' - i += 1 - continue - stream_info = stream._info(T, P, flow, composition, N) - unit = stream._sink - index = stream_info.index('\n') - sink_info = f' to {type(unit).__name__}-{unit}\n' if unit else '\n' - info += f'[{i}] {stream.ID}' + sink_info + stream_info[index+1:] + '\n' - i += 1 - info = info.replace('\n ', '\n ') - return info[:-1] - - def show(self, T=None, P=None, flow=None, composition=False, N=None): - """Prints information on unit.""" - print(self._info(T, P, flow, composition, N)) - - def _ipython_display_(self): - if self.IPYTHON_DISPLAY_UNIT_OPERATIONS: - try: self.diagram() - except: pass - self.show() - - def __repr__(self): - if self.ID: - return f'<{type(self).__name__}: {self.ID}>' - else: - return f'<{type(self).__name__}>' - -del thermo_user, registered \ No newline at end of file diff --git a/build/lib/biosteam/evaluation/__init__.py b/build/lib/biosteam/evaluation/__init__.py deleted file mode 100644 index 9c65d677d..000000000 --- a/build/lib/biosteam/evaluation/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed Mar 6 16:53:04 2019 - -@author: yoelr -""" -from . import (_parameter, _block, _state, _model, - _metric, evaluation_tools, _variable) -from ._variable import * -from ._parameter import * -from ._block import * -from ._state import * -from ._model import * -from ._metric import * - -__all__ = ('evaluation_tools', - *_variable.__all__, - *_parameter.__all__, - *_metric.__all__, - *_block.__all__, - *_state.__all__, - *_model.__all__) \ No newline at end of file diff --git a/build/lib/biosteam/evaluation/_block.py b/build/lib/biosteam/evaluation/_block.py deleted file mode 100644 index d7c3b6d62..000000000 --- a/build/lib/biosteam/evaluation/_block.py +++ /dev/null @@ -1,98 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed Mar 6 15:22:37 2019 - -@author: yoelr -""" - -from .. import Unit -from thermosteam import Stream -from inspect import signature -from ._parameter import Parameter - -__all__ = ('Block',) - -def do_nothing(): pass - -# %% Simulation blocks - -class Block: - """ - Create a Block object that can simulate the element and the system - downstream. The block can also generate Parameter objects that can - update the system state. - - Parameters - ---------- - element : Unit or :class:`~thermosteam.Stream` - Element in the system. - system=None : System, optional - System affected by element. In other words, element should affect - downstream operations in the system. - - """ - - _blocks = {} - __slots__ = ('_system', '_simulate', '_element') - - def __new__(cls, element, system=None): - block = cls._blocks.get((system, element)) - if block: - self = block - else: - self = object.__new__(cls) - self.__init__(element, system) - return self - - def __init__(self, element, system=None): - inst = isinstance - if inst(element, Stream): unit = element.sink - elif inst(element, Unit): unit = element - if system and element: - subsys = system._downstream_system(unit) - simulate = subsys.simulate - else: - subsys = system - simulate = unit.simulate if inst(element, Unit) else do_nothing - self._system = subsys - self._simulate = simulate - self._element = element - self._blocks[system, element] = self - - def parameter(self, setter, simulate=None, name=None, distribution=None, - units=None, baseline=None) -> Parameter: - """Return a Parameter object.""" - if simulate is None: simulate = self._simulate - if not name: name, = signature(setter).parameters.keys() - return Parameter(name, setter, simulate, self._element, - self._system, distribution, units, baseline) - - @property - def system(self): - """System that the block simulates.""" - return self._system - - @property - def element(self): - """Starting element of the system (if any).""" - return self._element - - @property - def simulate(self): - """Simulate block.""" - return self._simulate - - def __repr__(self): - if self._system: - return f'<{type(self).__name__}: {self._system}>' - elif self._element: - if isinstance(self._element, type): - return f"<{type(self).__name__}: {self._element.__name__}>" - return f"<{type(self).__name__}: {type(self._element).__name__}-{self._element}>" - else: - return f"<{type(self).__name__}>" - - - - - \ No newline at end of file diff --git a/build/lib/biosteam/evaluation/_metric.py b/build/lib/biosteam/evaluation/_metric.py deleted file mode 100644 index 247b14682..000000000 --- a/build/lib/biosteam/evaluation/_metric.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon Sep 2 02:52:51 2019 - -@author: yoelr -""" -from ._variable import Variable -__all__ = ('Metric',) - -class Metric(Variable): - """Create a Metric object that serves as an argument for Model objects. - - Parameters - ---------- - name : str - Name of metric. - units : str - Metric units of measure. - getter : function - Should take no arguments and return the metric value. - element : str - Element corresponding to metric - - """ - __slots__ = ('name', 'units', 'getter', 'element') - distribution = None - def __init__(self, name, getter, units=None, element='Biorefinery'): - self.name = name - self.units = units - self.getter = getter - self.element = element - - def __call__(self): - return self.getter() - \ No newline at end of file diff --git a/build/lib/biosteam/evaluation/_model.py b/build/lib/biosteam/evaluation/_model.py deleted file mode 100644 index beb3624a0..000000000 --- a/build/lib/biosteam/evaluation/_model.py +++ /dev/null @@ -1,264 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Mar 7 23:17:38 2019 - -@author: yoelr -""" -import numpy as np -import pandas as pd -from ._state import State -from ._metric import Metric -from biosteam.utils import TicToc -from scipy.stats import spearmanr -import multiprocessing as mp - -__all__ = ('Model',) - -# %% Functions - -varindices = lambda vars: [var.index for var in vars] -varcolumns = lambda vars: pd.MultiIndex.from_tuples( - varindices(vars), - names=('Element', 'Variable')) - - -# %% Grid of simulation blocks - -class Model(State): - """ - Create a Model object that allows for evaluation over a sample space. - - Parameters - ---------- - system : System - Should reflect the model state. - metrics : tuple[Metric] - Metrics to be evaluated by model. - skip=False : bool - If True, skip simulation for repeated states. - - Examples - -------- - :doc:`../tutorial/Monte_Carlo` - - """ - __slots__ = ('_table', # [DataFrame] All arguments and results - '_metrics', # tuple[Metric] Metrics to be evaluated by model - '_index', # list[int] Order of sample evaluation for performance - '_samples', # [array] Argument sample space - '_setters') # list[function] Cached parameter setters - - def __init__(self, system, metrics, skip=False): - super().__init__(system, skip) - self.metrics = metrics - self._setters = self._samples = self._table = None - - def copy(self): - """Return copy.""" - copy = super().copy() - copy._metrics = self._metrics - if self._update: - copy._setters = self._setters - copy._table = self._table.copy() - else: - copy._setters = copy._samples = copy._table = None - return copy - - def _erase(self): - """Erase cached data.""" - self._setters = self._update = self._table = self._samples = None - - @property - def metrics(self): - """tuple[Metric] Metrics to be evaluated by model.""" - return self._metrics - @metrics.setter - def metrics(self, metrics): - for i in metrics: - if not isinstance(i, Metric): - raise ValueError(f"metrics must be '{Metric.__name__}' " - f"objects, not '{type(i).__name__}'") - self._metrics = tuple(metrics) - - @property - def table(self): - """[DataFrame] Table of the sample space with results in the final column.""" - return self._table - - def load_samples(self, samples): - """Load samples for evaluation - - Parameters - ---------- - samples : numpy.ndarray, dim=2 - - """ - if not self._update: self._loadparams() - params = self._params - paramlen = len(params) - if not isinstance(samples, np.ndarray): - raise TypeError(f'samples must be an ndarray, not a {type(samples).__name__} object') - elif samples.ndim != 2: - raise ValueError('samples must be 2 dimensional') - elif samples.shape[1] != paramlen: - raise ValueError(f'number of parameters in samples ({samples.shape[1]}) must be equal to the number of parameters ({len(params)})') - key = lambda x: samples[x][i] - N_samples = len(samples) - index = list(range(N_samples)) - for i in range(paramlen-1, -1, -1): - if not params[i].system: break - index.sort(key=key) - self._index = index - empty_metric_data = np.zeros((N_samples, len(self.metrics))) - self._table = pd.DataFrame(np.hstack((samples, empty_metric_data)), - columns=varcolumns(tuple(params)+self.metrics)) - self._samples = samples - - def evaluate(self, thorough=True): - """Evaluate metric over the argument sample space and save values to ``table``. - - Parameters - ---------- - thorough : bool - If True, simulate the whole system with each sample. - If False, simulate only the affected parts of the system. - - """ - # Setup before simulation - funcs = [i.getter for i in self._metrics] - samples = self._samples - if samples is None: - raise ValueError('must load samples or distribution before evaluating') - index = self._index - values = [None] * len(index) - if thorough: - if self._setters: - setters = self._setters - else: - self._setters = setters = [p.setter for p in self._params] - simulate = self._system.simulate - zip_ = zip - for i in index: - for f, s in zip_(setters, samples[i]): f(s) - simulate() - values[i] = [i() for i in funcs] - else: - update = self._update - for i in index: - update(samples[i]) - values[i] = [i() for i in funcs] - - cols = varindices(self._metrics) - for k, v in zip_(cols, zip_(*values)): self.table[k] = v - - def evaluate_across_coordinate(self, name, f_coordinate, coordinate, - *, xlfile=None, notify=True, - multi_coordinate=False): - """Evaluate across coordinate and save sample metrics. - - Parameters - ---------- - name : str or tuple[str] - Name of coordinate - f_coordinate : function - Should change state of system given the coordinate. - coordinte : array - Coordinate values. - xlfile : str, optional - Name of file to save. File must end with ".xlsx" - rule='L' : str - Sampling rule. - notify=True : bool, optional - If True, notify elapsed time after each coordinate evaluation. - - """ - table = self.table - N_samples, N_parameters = table.shape - N_points = len(coordinate) - - # Initialize data containers - metric_data = {} - def new_data(key, dct=metric_data): - data = np.zeros([N_samples, N_points]) - dct[key] = data - return data - for i in self.metrics: new_data(i.index) - - # Initialize timer - if notify: - timer = TicToc() - timer.tic() - def evaluate(): - self.evaluate() - print(f"[{n}] Elapsed time: {timer.elapsed_time:.0f} sec") - else: - evaluate = self.evaluate - - for n, x in enumerate(coordinate): - f_coordinate(*x) if multi_coordinate else f_coordinate(x) - evaluate() - for metric in metric_data: - metric_data[metric][:, n] = table[metric] - - if xlfile: - if multi_coordinate: - columns = pd.MultiIndex.from_tuples(coordinate, - names=name) - else: - columns = pd.Index(coordinate, name=name) - - # Save data to excel - data = pd.DataFrame(data=np.zeros([N_samples, N_points]), - columns=columns) - - with pd.ExcelWriter(xlfile) as writer: - for i, metric in zip(self.metrics, metric_data): - data[:] = metric_data[metric] - data.to_excel(writer, sheet_name=i.name) - return metric_data - - def spearman(self, metrics=()): - """Return DataFrame of Spearman's rank correlation for metrics vs parameters. - - Parameters - ---------- - metrics=() : Iterable[Metric], defaults to all metrics - Metrics to be correlated with parameters. - """ - data = self.table - param_cols = list(data) - all_metric_names = varindices(self.metrics) - if not metrics: - metric_names = all_metric_names - else: - metric_names = varindices(metrics) - params = list(self._params) - - for i in all_metric_names: param_cols.remove(i) - param_descriptions = [i.describe() for i in params] - allrhos = [] - for name in metric_names: - rhos = [] - metric = data[name] - for col in param_cols: - rho, p = spearmanr(data[col], metric) - rhos.append(rho) - allrhos.append(rhos) - allrhos = np.array(allrhos).transpose() - return pd.DataFrame(allrhos, index=param_descriptions, - columns=metric_names) - - def __call__(self, sample): - """Return list of metric values.""" - super().__call__(sample) - return [i.getter() for i in self._metrics] - - def _repr(self): - clsname = type(self).__name__ - newline = "\n" + " "*(len(clsname)+2) - return f'{clsname}: {newline.join([i.describe() for i in self.metrics])}' - - def __repr__(self): - return f'<{type(self).__name__}: {", ".join([i.name for i in self.metrics])}>' - - \ No newline at end of file diff --git a/build/lib/biosteam/evaluation/_name.py b/build/lib/biosteam/evaluation/_name.py deleted file mode 100644 index e040e92a5..000000000 --- a/build/lib/biosteam/evaluation/_name.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Tue May 14 14:35:55 2019 - -@author: yoelr -""" -import re -__all__ = ('element_name',) - -def element_name(element): - if element: - if isinstance(element, type): - return re.sub(r"\B([A-Z])", r" \1", element.__name__.replace('_', ' ')).capitalize() - elif isinstance(element, str): - return element.replace('_', ' ') - else: - return element.line + '-' + element.ID.replace('_', ' ') - else: - return '' \ No newline at end of file diff --git a/build/lib/biosteam/evaluation/_parameter.py b/build/lib/biosteam/evaluation/_parameter.py deleted file mode 100644 index a71ca1cc6..000000000 --- a/build/lib/biosteam/evaluation/_parameter.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Tue May 14 14:20:53 2019 - -@author: yoelr -""" -__all__ = ('Parameter',) -from ._variable import Variable - -class Parameter(Variable): - """ - Create a Parameter object that, when called, runs the setter and - the simulate functions. - - Parameters - ---------- - name : str - Name of parameter. - setter : function - Should set the parameter. - simulate : function - Should simulate parameter effects. - element : object - Element associated to parameter. - system : System - System associated to parameter. - distribution : chaospy.Dist - Parameter distribution. - units : str - Units of parameter. - baseline : float - Baseline value of parameter. - - """ - __slots__ = ('name', 'setter', 'simulate', 'element', - 'system', 'distribution', 'units', 'baseline') - - def __init__(self, name, setter, simulate, - element, system, distribution, - units, baseline): - self.name = name.replace('_', ' ').capitalize() - self.setter = setter - self.simulate = simulate - self.element = element - self.system = system - self.distribution = distribution - self.units = units - self.baseline = baseline - - def __call__(self, value): - self.setter(value) - self.simulate() - - def _info(self): - return (f'{type(self).__name__}: {self.name}\n' - + (f' element: {self.element_name}' if self.element else '') - + (f' system: {self.system}\n' if self.system else '') - + (f' units: {self.units}\n' if self.units else '') - + (f' distribution: {self.distribution}\n' if self.distribution else '')) - - - - \ No newline at end of file diff --git a/build/lib/biosteam/evaluation/_state.py b/build/lib/biosteam/evaluation/_state.py deleted file mode 100644 index 2bf7ceac5..000000000 --- a/build/lib/biosteam/evaluation/_state.py +++ /dev/null @@ -1,279 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu May 9 13:38:57 2019 - -@author: Guest Group -""" -from ._block import Block -from .. import Unit -from thermosteam import Stream -import numpy as np -import pandas as pd -from chaospy import J -from .evaluation_tools import load_default_parameters - -__all__ = ('State',) - -# %% functions - -def param_unit(param): - element = param.element - if isinstance(element, Unit): return element - elif isinstance(element, Stream): return element._sink - -def parameter(system, element, setter, kind, name, distribution, units, baseline): - if kind == 'coupled': - return Block(element, system).parameter(setter, name=name, - distribution=distribution, units=units, baseline=baseline) - elif kind == 'isolated': - return Block(element, None).parameter(setter, name=name, - distribution=distribution, units=units, baseline=baseline) - elif kind == 'design': - return Block(element, None).parameter(setter, element._summary, - name, distribution=distribution, units=units, baseline=baseline) - elif kind == 'cost': - simulate = element._cost - return Block(element, None).parameter(setter, simulate, name, - distribution=distribution, units=units, baseline=baseline) - raise ValueError(f"kind must be either 'coupled', 'isolated', 'design', or 'cost' (not {kind}).") - -class UpdateWithSkipping: - __slots__ = ('cache', 'params') - def __init__(self, params): - self.params = tuple(params) - self.cache = None - def __call__(self, sample): - try: - sim = None - for p, x, same in zip(self.params, sample, self.cache==sample): - if same: continue - p.setter(x) - if sim: continue - if p.system: sim = p.simulate - else: p.simulate() - if sim: sim() - self.cache = sample.copy() - except Exception as Error: - self.cache = None - raise Error - -class UpdateWithoutSkipping: - __slots__ = ('params',) - def __init__(self, params): - self.params = tuple(params) - def __call__(self, sample): - sim = None - for p, x in zip(self.params, sample): - p.setter(x) - if sim: continue - if p.system: sim = p.simulate - else: p.simulate() - if sim: sim() - - -# %% - -class State: - """ - Create a State object that can update the `system` state given - a sample of parameter states. - - Parameters - ---------- - system : System - skip=False : bool - If True, skip simulation for repeated states - - """ - __slots__ = ('_system', # [System] - '_params', # list[Parameter] All parameters. - '_update', # [function] Updates system state. - '_skip') # [bool] If True, skip simulation for repeated states - - load_default_parameters = load_default_parameters - - def __init__(self, system, skip=False): - self._system = system - self._params = [] - self._update = None - self._skip = skip - - def __len__(self): - return len(self._params) - - def copy(self): - """Return copy.""" - copy = self.__new__(type(self)) - copy._params = list(self._params) - copy._system = self._system - copy._update = self._update - copy._skip = self._skip - return copy - - def get_baseline_sample(self): - return np.array([i.baseline for i in self.get_parameters()]) - - def get_parameters(self): - """Return parameters.""" - if not self._update: self._loadparams() - return tuple(self._params) - - def get_distribution_summary(self): - """Return dictionary of shape name-DataFrame pairs.""" - params = self.get_parameters() - if not params: return None - params_by_shape = {} - shape_keys = {} - for p in params: - distribution = p.distribution - if not distribution: continue - shape = type(distribution).__name__ - if shape in params_by_shape: - params_by_shape[shape].append(p) - else: - params_by_shape[shape] = [p] - shape_keys[shape] = tuple(distribution._repr.keys()) - tables_by_shape = {} - for shape, params in params_by_shape.items(): - data = [] - columns = ('Element', 'Name', 'Units', 'Shape', *shape_keys[shape]) - for p in params: - distribution = p.distribution - element = p.element_name - name = p.name.replace(element, '') - units = p.units or '' - values = distribution._repr.values() - data.append((element, name, units, shape, *values)) - tables_by_shape[shape] = pd.DataFrame(data, columns=columns) - return tables_by_shape - - def parameter(self, setter=None, element=None, kind='isolated', - name=None, distribution=None, units=None, baseline=None): - """Define and register parameter. - - Parameters - ---------- - setter : function - Should set parameter in the element. - element : Unit or :class:`~thermosteam.Stream` - Element in the system being altered. - kind : {'coupled', 'isolated', 'design', 'cost'} - * 'coupled': parameter is coupled to the system. - * 'isolated': parameter does not affect the system but does affect the element (if any). - * 'design': parameter only affects design and cost of the element. - * 'cost': parameter only affects cost of the element. - name : str - Name of parameter. If None, default to argument name of setter. - distribution : chaospy.Dist - Parameter distribution. - units : str - Parameter units of measure - baseline : float - Baseline value of parameter. - - Notes - ----- - - If kind is 'coupled', account for downstream operations. Otherwise, only account for given element. If kind is 'design' or 'cost', element must be a Unit object. - - """ - if not setter: - return lambda setter: self.parameter(setter, element, kind, name, - distribution, units, baseline) - param = parameter(self._system, element, setter, kind, - name, distribution, units, baseline) - self._params.append(param) - self._erase() - return setter - - def sample(self, N, rule): - """Return N samples from parameter distribution at given rule. - - Parameters - ---------- - N : int - Number of samples. - rule : str - Sampling rule. - - Notes - ----- - Use the following ``rule`` flag for sampling: - - +-------+-------------------------------------------------+ - | key | Description | - +=======+=================================================+ - | ``C`` | Roots of the first order Chebyshev polynomials. | - +-------+-------------------------------------------------+ - | ``NC``| Chebyshev nodes adjusted to ensure nested. | - +-------+-------------------------------------------------+ - | ``K`` | Korobov lattice. | - +-------+-------------------------------------------------+ - | ``R`` | Classical (Pseudo-)Random samples. | - +-------+-------------------------------------------------+ - | ``RG``| Regular spaced grid. | - +-------+-------------------------------------------------+ - | ``NG``| Nested regular spaced grid. | - +-------+-------------------------------------------------+ - | ``L`` | Latin hypercube samples. | - +-------+-------------------------------------------------+ - | ``S`` | Sobol low-discrepancy sequence. | - +-------+-------------------------------------------------+ - | ``H`` | Halton low-discrepancy sequence. | - +-------+-------------------------------------------------+ - | ``M`` | Hammersley low-discrepancy sequence. | - +-------+-------------------------------------------------+ - - """ - if not self._update: self._loadparams() - return J(*[i.distribution for i in self._params]).sample(N, rule).transpose() - - def _erase(self): - """Erase cached data.""" - self._update = None - - def _loadparams(self): - """Load parameters.""" - length = len(self._system._unit_path) - index = self._system._unit_path.index - self._params.sort(key=lambda x: index(param_unit(x)) if x.system else length) - self._update = (UpdateWithSkipping if self._skip - else UpdateWithoutSkipping)(self._params) - - def __call__(self, sample): - """Update state given sample of parameters.""" - if not self._update: self._loadparams() - self._update(np.asarray(sample, dtype=float)) - - def _repr(self): - return f'{type(self).__name__}: {self._system}' - - def __repr__(self): - return '<' + self._repr() + '>' - - def _info(self): - if not self._update: self._loadparams() - if not self._params: return f'{self._repr()}\n (No parameters)' - lines = [] - lenghts_block = [] - lastblk = None - for i in self._params: - blk = i.element_name - element = len(blk)*' ' if blk==lastblk else blk - lines.append(f" {element}${i.name}\n") - lastblk = blk - lenghts_block.append(len(blk)) - maxlen_block = max(lenghts_block) - out = f'{self._repr()}\n' - maxlen_block = max(maxlen_block, 7) - out += ' Element:' + (maxlen_block - 7)*' ' + ' Parameter:\n' - for newline, len_ in zip(lines, lenghts_block): - newline = newline.replace('$', ' '*(maxlen_block-len_) + ' ') - out += newline - return out.rstrip('\n ') - - def show(self): - """Return information on metric parameters.""" - print(self._info()) - _ipython_display_ = show - \ No newline at end of file diff --git a/build/lib/biosteam/evaluation/_variable.py b/build/lib/biosteam/evaluation/_variable.py deleted file mode 100644 index 267de9a18..000000000 --- a/build/lib/biosteam/evaluation/_variable.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Sep 26 06:15:40 2019 - -@author: yoelr -""" -__all__ = ('Variable',) -from ._name import element_name - - -class Variable: - """ - Abstract class for a variable in BioSTEAM. - - Attributes - ---------- - name : str - Name of variable - element : object - Element corresponding to variable - units : str - Units of measure - distribution : chaospy.Dist, optional - Distribution of variable - - """ - __slots__ = () - include_units_in_index = True - @property - def element_name(self): - return element_name(self.element) - - @property - def index(self): - name = self.name - if self.include_units_in_index: - units = self.units - if units: name += f" [{units}]" - return (self.element_name, name) - - def describe(self, number_format='.3g') -> str: - """Return description of variable.""" - name = self.name - if not name.isupper(): - name = name.casefold() - if self.element: - name = self.element_name + ' ' + name - if self.units: - units = (' (' + str(self.units) + ')') - else: - units = '' - if self.distribution: - distribution_values = self.distribution._repr.values() - distribution = ', '.join([format(j, number_format) - for j in distribution_values]) - distribution = ' [' + distribution + ']' - else: - distribution = '' - return name + units + distribution - - def __repr__(self): - units = f" ({self.units})" if self.units else "" - element = f" [{self.element_name}]" if self.element else "" - return f'<{type(self).__name__}:{element} {self.name}{units}>' - - def show(self): - print(self._info()) \ No newline at end of file diff --git a/build/lib/biosteam/evaluation/evaluation_tools/__init__.py b/build/lib/biosteam/evaluation/evaluation_tools/__init__.py deleted file mode 100644 index bcb773397..000000000 --- a/build/lib/biosteam/evaluation/evaluation_tools/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon Sep 2 04:58:34 2019 - -@author: yoelr -""" - -from lazypkg import LazyPkg - -LazyPkg(__name__, ['parameter', 'plot', 'in_parallel']) \ No newline at end of file diff --git a/build/lib/biosteam/evaluation/evaluation_tools/in_parallel.py b/build/lib/biosteam/evaluation/evaluation_tools/in_parallel.py deleted file mode 100644 index ae4a96890..000000000 --- a/build/lib/biosteam/evaluation/evaluation_tools/in_parallel.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Jan 23 20:03:24 2020 - -@author: yoelr -""" -import numpy as np -import multiprocessing as mp -import pandas as pd - -__all__ = ('evaluate_coordinate_in_parallel',) - -def evaluate_coordinate_in_parallel(f_evaluate_at_coordinate, coordinate, - metrics, multi_coordinate=False, - name=None, names=None, xlfile=None): - # In parallel - pool = mp.Pool(mp.cpu_count()) - map = pool.starmap if multi_coordinate else pool.map - tables = map(f_evaluate_at_coordinate, coordinate) - pool.close() - - # Initialize data containers - table = tables[0] - N_samples, N_parameters = table.shape - N_coordinate = len(coordinate) - metric_data = {i.index: np.zeros([N_samples, N_coordinate]) - for i in metrics} - try: - for n, table in enumerate(tables): - for metric in metric_data: - metric_data[metric][:, n] = table[metric] - except: - return tables, metric_data - - if xlfile: - if multi_coordinate: - columns = pd.MultiIndex.from_tuples(coordinate, - names=names or name) - else: - columns = pd.Index(coordinate, name=name or names) - - # Save data to excel - data = pd.DataFrame(data=np.zeros([N_samples, N_coordinate]), - columns=columns) - - with pd.ExcelWriter(xlfile) as writer: - for i, metric in zip(metrics, metric_data): - data[:] = metric_data[metric] - data.to_excel(writer, sheet_name=i.name) - - return metric_data - \ No newline at end of file diff --git a/build/lib/biosteam/evaluation/evaluation_tools/parameter.py b/build/lib/biosteam/evaluation/evaluation_tools/parameter.py deleted file mode 100644 index 7a27d9691..000000000 --- a/build/lib/biosteam/evaluation/evaluation_tools/parameter.py +++ /dev/null @@ -1,230 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sun May 26 11:21:31 2019 - -@author: yoelr -""" -from chaospy import distributions as shape -import biosteam as bst - -__all__ = ('load_default_parameters', - 'add_all_cost_item_params', - 'add_flow_rate_param', - 'add_all_stream_price_params', - 'add_stream_price_param', - 'add_power_utility_price_param', - 'add_basic_TEA_params', - 'triang', - 'bounded_triang',) - -# %% Predefined shapes - -def triang(mid, proportion=0.1, addition=0): - return shape.Triangle((1.-proportion)*mid - addition, - mid, - (1.+proportion)*mid + addition) - -def bounded_triang(mid, lb=0, ub=1, proportion=0, addition=0.1): - lower = (1.-proportion)*mid - addition - upper = (1.+proportion)*mid + addition - if lower < lb: lower = lb - if upper > ub: upper = ub - return shape.Triangle(lower, mid, upper) - -# %% Fill model with parameters - -def load_default_parameters(self, feedstock, shape=triang, - bounded_shape=bounded_triang, - operating_days=False, include_feedstock_price=True): - """ - Load all default parameters, including coefficients of cost items, - stream prices, electricity price, heat utility prices, feedstock - flow rate, and number of operating days. - - Parameters - ---------- - feedstock : Stream - Main feedstock of process. - shape : function, optional - Should take in baseline value and return a chaospy.Dist object or - None. Default function returns a chaospy.Triangle object with bounds - at +- 10% of baseline value. The distribution is applied to all - parameters except exponential factor "n" of cost items. - bounded_shape : function, optional - Should take in baseline value and return a chaospy.Dist object or - None. Default function returns a chaospy.Triangle object with bounds - at +- 0.1 of baseline value or minimum 0 and maximum 1. The - distribution is applied to exponential factor "n" of cost items. - operating_days : bool, optional - If True, include operating days - - """ - bounded_shape = bounded_shape or shape - add_all_cost_item_params(self, shape, bounded_shape) - add_all_stream_price_params(self, shape, feedstock, include_feedstock_price) - add_power_utility_price_param(self, shape) - add_heat_utility_price_params(self, shape) - add_flow_rate_param(self, feedstock, shape) - add_basic_TEA_params(self, shape, operating_days) - -def add_all_cost_item_params(model, shape, exp_shape): - system = model._system - costunits = system._costunits - - # Get all cost options (without repetition) - all_unit_lines = [] - all_cost_items = [] - for i in costunits: - if hasattr(i, 'cost_items'): - if any([i.cost_items is j for j in all_cost_items]): continue - all_unit_lines.append(i.line) - all_cost_items.append(i.cost_items) - - # Add cost exponents, base cost, and electricity cost as parameters - # (for each column in each dataframe of the list) - for cost_items, line in zip(all_cost_items, all_unit_lines): - for ID, item in cost_items.items(): - _cost(model, ID, item, line, shape) - _exp(model, ID, item, line, exp_shape) - _kW(model, ID, item, line, shape) - -def add_all_stream_price_params(model, shape, feedstock, include_feedstock_price): - # Add stream prices as parameters - system = model._system - feeds = system.feeds - products = system.products - if not include_feedstock_price: - feeds = feeds.copy() - feeds.discard(feedstock) - for s in feeds: add_stream_price_param(model, s, shape) - for s in products: add_stream_price_param(model, s, shape) - -def set_price(price): - bst.PowerUtility.price = price - -def add_power_utility_price_param(model, shape): - if bst.PowerUtility.price: - baseline = bst.PowerUtility.price - model.parameter(set_price, element='Electricity', units='USD/kWhr', - distribution=shape(baseline), - baseline=baseline) - -def add_heat_utility_price_params(model, shape): - agents = (*bst.HeatUtility.cooling_agents, - *bst.HeatUtility.heating_agents) - for agent in agents: - add_agent_price_params(model, agent.ID, agent, shape) - -class Setter: - __slots__ = ('obj', 'attr') - def __init__(self, obj, attr): - self.obj = obj - self.attr = attr - def __call__(self, value): - setattr(self.obj, self.attr, value) - -def add_agent_price_params(model, name, agent, shape): - if agent.heat_transfer_price: - baseline = agent.heat_transfer_price - model.parameter(Setter(agent, 'heat_transfer_price'), element=name, units='USD/kJ', - name='Price', distribution=shape(baseline), baseline=baseline) - elif agent.regeneration_price: - baseline = agent.regeneration_price - model.parameter(Setter(agent, 'regeneration_price'), - element=name, units='USD/kmol', - name='Price', distribution=shape(baseline), - baseline=baseline) - -def add_flow_rate_param(model, feed, shape): - baseline = feed.F_mass - model.parameter(Setter(feed, 'F_mass'), element=feed, units='kg/hr', - distribution=shape(baseline), kind='coupled', - name='Flow rate', baseline=baseline) - -def add_basic_TEA_params(model, shape, operating_days): - param = model.parameter - TEA = model._system.TEA - - if operating_days: - baseline = TEA.operating_days - param(Setter(TEA, 'operating_days'), element='TEA', - distribution=shape(baseline), - baseline=baseline, - name='Operating days') - - baseline = TEA.income_tax - param(Setter(TEA, 'income_tax'), - element='TEA', - distribution=shape(baseline), - baseline=baseline, - name='Income tax') - - if TEA.startup_months: - baseline = TEA.startup_months - param(Setter(TEA, 'startup_months'), element='TEA', - baseline=baseline, - distribution=shape(baseline), name='startup_months') - - - -# %% Parameter creators for cost_items - -def add_stream_price_param(model, stream, shape): - mid = stream.price - if not mid: return - model.parameter(Setter(stream, 'price'), - element=stream, units='USD/kg', - distribution=shape(mid), - baseline=mid, - name='price') - -def _cost(model, ID, item, line, shape): - key = 'cost' - mid = float(item[key]) - if not mid: return None - distribution = shape(mid) - name = 'base cost' - if ID!=line: ID = ID + ' ' + name - else: ID = name - _cost_option(model, ID, item, key, line, distribution, 'USD', mid) - -def _exp(model, ID, item, line, shape): - key = 'n' - mid = float(item[key]) - if not mid: return None - distribution = shape(mid) - name = 'exponent' - if ID!=line: ID = ID + ' ' + name - else: ID = name - _cost_option(model, ID, item, key, line, distribution, None, mid) - -class CostItemSetter: - __slots__ = ('item', 'key', 'size') - def __init__(self, item, key, size=1): - self.item = item - self.key = key - self.size = size - def __call__(self, value): - self.item[self.key] = value*self.size - -def _kW(model, ID, item, line, shape): - key = 'kW' - mid = float(item[key]) - if not mid: return None - units = item.units - size = item.S - mid = mid/size - distribution = shape(mid) - units = 'kW / ' + units - name = 'electricity rate' - if ID!=line: ID = ID + ' ' + name - else: ID = name - model.parameter(CostItemSetter(item, key, size), element=line, - baseline=mid, - units=units, distribution=distribution, name=ID) - -def _cost_option(model, ID, item, key, line, distribution, units, mid): - model.parameter(CostItemSetter(item, key), element=line, units=units, - baseline=mid, - distribution=distribution, name=ID) - \ No newline at end of file diff --git a/build/lib/biosteam/evaluation/evaluation_tools/plot.py b/build/lib/biosteam/evaluation/evaluation_tools/plot.py deleted file mode 100644 index a202dbd17..000000000 --- a/build/lib/biosteam/evaluation/evaluation_tools/plot.py +++ /dev/null @@ -1,263 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon Sep 2 05:01:11 2019 - -@author: yoelr -""" -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt -from biosteam.utils import colors - -__all__ = ('plot_montecarlo', 'plot_montecarlo_across_coordinate', - 'annotate_line', 'plot_single_points', 'plot_spearman', - 'plot_horizontal_line') - -def plot_spearman(rhos, top=None, name=None): - """Display Spearman's rank correlation plot. - - Parameters - ---------- - rhos : pandas.Series - Spearman's rank correlation coefficients to be plotted. - top=None : float, optional - Number of parameters to plot (from highest values). - - Returns - ------- - fig : matplotlib Figure - ax : matplotlib AxesSubplot - """ - # Sort parameters for plot - abs_ = abs - if not name: name = rhos.name - rhos, index = zip(*sorted(zip(rhos, rhos.index), - key=lambda x: abs_(x[0]))) - if top: - rhos = rhos[-top:] - index = index[-top:] - - xranges = [(0, i) for i in rhos] - yranges = [(i, 0.65) for i in range(len(rhos))] - - # Plot bars one by one - fig, ax = plt.subplots() - for x, y in zip(xranges, yranges): - ax.broken_barh([x], y, facecolors=colors.blue_tint.RGBn, - edgecolors=colors.blue_dark.RGBn) - - # Plot central line - plot_vertical_line(0, color=colors.neutral_shade.RGBn, lw=1) - - xticks = [-1, -0.5, 0, 0.5, 1] - yticks = [i[0]+i[1]/2 for i in yranges] - ax.set_xlim(-1, 1) - ax.set_xlabel(f"Spearman's correlation with {name}") - ax.set_xticks(xticks) - ax.set_yticks(yticks) - ax.tick_params(axis='y', right=False, direction="inout", length=4) - ax.tick_params(axis='x', direction="inout", length=4) - ax.set_yticklabels(index) - ax.grid(False) - ylim = plt.ylim() - - ax2 = ax.twinx() - plt.sca(ax2) - plt.yticks(yticks, []) - plt.ylim(*ylim) - ax2.zorder = 1000 - ax2.tick_params(direction="in") - - ax3 = ax.twiny() - plt.sca(ax3) - plt.xticks(xticks) - ax3.zorder = 1000 - ax3.tick_params(direction="in", labeltop=False) - - return fig, ax - - -# %% Plot metrics vs coordinate - -light_color = colors.brown_tint.RGBn -dark_color = colors.brown_shade.RGBn - -def plot_horizontal_line(y, color='grey', **kwargs): - """Plot horizontal line.""" - plt.axhline(y=y, color=color, **kwargs) - -def plot_vertical_line(x, color='grey', **kwargs): - """Plot vertical line.""" - plt.axvline(x=x, color=color, **kwargs) - -def plot_single_points(xs, ys, color=dark_color, s=50, zorder=1e6, edgecolor='black', **kwargs): - """Plot single points and return patch artist.""" - if xs is None: - xs = tuple(range(len(ys))) - return plt.scatter(xs, ys, marker='o', s=s, color=color, zorder=zorder, edgecolor=edgecolor, **kwargs) - -def plot_bars(scenarios, ys, colors, edgecolors, labels, positions=None): - barwidth = 0.50 - N_scenarios = len(scenarios) - N_labels = len(labels) - if positions is None: positions = N_labels * np.arange(N_scenarios, dtype=float) - data = (ys, colors, edgecolors, labels) - for y, color, edgecolor, label in zip(*data): - plt.bar(positions, y, barwidth, - align='center', label=label, - color=color, edgecolor=edgecolor) - positions += barwidth - - plt.xticks(positions-barwidth*(N_labels+1)/2, scenarios) - plt.tight_layout() - plt.legend() - -def plot_montecarlo(data, - light_color=light_color, - dark_color=dark_color, - positions=None, - transpose=False, - **kwargs): - """Return box plot of Monte Carlo evaluation. - - Parameters - ---------- - data : numpy.ndarray or pandas.DataFrame - Metric values with uncertainty. - light_colors : Iterable(numpy.ndarray) - RGB normalized to 1. Defaults to brown. - dark_colors : Iterable(numpy.ndarray) - RGB normalized to 1. Defaults to brown. - **kwargs : - Additional arguments for pyplot.boxplot - - Returns - ------- - bx : Patch - - """ - if isinstance(data, pd.DataFrame): - if 'labels' not in kwargs: - kwargs['labels'] = data.columns - if transpose: data = data.transpose() - if not positions: - if data.ndim == 1: - positions = (0,) - else: - positions = tuple(range(data.shape[0])) - bx = plt.boxplot(x=data, positions=positions, patch_artist=True, - widths=0.8, whis=[5, 95], - boxprops={'facecolor':light_color, - 'edgecolor':dark_color}, - medianprops={'color':dark_color, - 'linewidth':1.5}, - flierprops = {'marker':'D', - 'markerfacecolor': light_color, - 'markeredgecolor': dark_color, - 'markersize':6}) - - return bx - -def plot_montecarlo_across_coordinate(xs, ys, - light_color=light_color, - dark_color=dark_color): - """ - Plot Monte Carlo evaluation across a coordinate. - - Parameters - ---------- - xs : numpy.ndarray(ndim=1) - Coordinate values for each column in ``ys``. - ys : numpy.ndarray(ndim=2) - Metric values with uncertainty. - light_color : numpy.ndarray - RGB normalized to 1. Defaults to brown. - dark_color : numpy.ndarray - RGB normalized to 1. Defaults to brown. - - Returns - ------- - percentiles : numpy.ndarray(ndim=2) - 5, 25, 50, 75 and 95th percentiles by row (5 rows total). - - """ - q05, q25, q50, q75, q95 = percentiles = np.percentile(ys, [5,25,50,75,95], axis=0) - - plt.plot(xs, q50, '-', - color=dark_color, - linewidth=1.5) # Median - plt.fill_between(xs, q25, q50, - color=light_color, - linewidth=1.0) # Lower quartile - plt.fill_between(xs, q75, q50, - color=light_color, - linewidth=1.0) # Upper quartile - plt.plot(xs, q05, '-.', - color=dark_color, - linewidth=1.0) # Lower whisker - plt.plot(xs, q95, '-.', - color=dark_color, - linewidth=1.0) # Upper whisker - - return percentiles - -# def estimate_ylim(lower, upper, ylim=()): -# if ylim: -# ylim[0] = min([ylim[0], min(lower)]) -# ylim[1] = max([ylim[1], max(upper)]) -# else: -# ylim = (0.95*min(lower), 1.05*max(upper)) -# return ylim - -def expand(lower, upper, lb, ub): - dx = (upper - lower)/12 - return max(lower-dx, lb), min(upper+dx, ub) - -def closest_index(x, xs): - if xs[0] < xs[-1]: - for i, xi in enumerate(xs): - if x < xi: break - else: - for i, xi in enumerate(xs): - if x > xi: break - return i - -def annotate_line(text, x, xs, ys, dy=0.2, dy_text=0.22, position='under', - color=colors.brown_shade.RGBn): - """Annotate line with text and arrow pointing to text. - - Parameters - ---------- - text : str - x : float - Arrow position - xs : numpy.ndarray(dim=1) - ys : numpy.ndarray(dim=1) - dy : float - Length of arrow to y-position. - dy_text : float - Distance of text to arrow. - position : {'under', 'over'} - Relative position of text to line. - color : numpy.ndarray - RGB normalized to 1. Defaults to brown. - - """ - index = closest_index(x, xs) - x = xs[index] - y = ys[index] - if position == 'under': - y *= 0.998 - y_text = y - dy - dy_text - elif position == 'over': - y *= 1.002 - y_text = y + dy + dy_text - else: - raise ValueError(f"position must be either 'over' or 'under', not '{position}'") - dx = 0 - color = 0.60*color - plt.arrow(x, y, dx, dy, linestyle='-', alpha=0.8, color=color, linewidth=1) - plt.text(x, y_text, text, color=0.75*color, horizontalalignment='center', fontsize=12) - - - diff --git a/build/lib/biosteam/exceptions.py b/build/lib/biosteam/exceptions.py deleted file mode 100644 index 7951168d1..000000000 --- a/build/lib/biosteam/exceptions.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- -from thermosteam import exceptions -from thermosteam.exceptions import * - -__all__ = ('DesignError', *exceptions.__all__) -del exceptions - -# %% Biosteam errors - -class DesignError(RuntimeError): - """RuntimeError regarding unit design.""" - diff --git a/build/lib/biosteam/tests/__init__.py b/build/lib/biosteam/tests/__init__.py deleted file mode 100644 index 65e742762..000000000 --- a/build/lib/biosteam/tests/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sun Dec 8 01:40:16 2019 - -@author: yoelr -""" - -from . import biorefinery_tests -from . import unit_operation_tests -from . import utility_tests - -__all__ = (*biorefinery_tests.__all__, - "test_biosteam") - -from .biorefinery_tests import * -from .unit_operation_tests import * -from .utility_tests import * - -def test_biosteam(): - utility_tests.test_utilities() - unit_operation_tests.test_unit_operations() - biorefinery_tests.test_biorefineries() \ No newline at end of file diff --git a/build/lib/biosteam/tests/biorefinery_tests.py b/build/lib/biosteam/tests/biorefinery_tests.py deleted file mode 100644 index f981e4535..000000000 --- a/build/lib/biosteam/tests/biorefinery_tests.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sun Dec 8 02:01:41 2019 - -@author: yoelr -""" - -__all__ = ('test_biorefineries', 'test_lipidcane', 'test_cornstover') - -def test_biorefineries(): - test_lipidcane() - test_cornstover() - -def test_lipidcane(): - from biorefineries.lipidcane import system - IRR = system.lipidcane_tea.IRR - assert 0.175 < IRR < 0.185, ("IRR of the lipid-cane biorefinery changed significantly " - f"from 0.181 to {IRR}") - -def test_cornstover(): - from biorefineries.cornstover import system - MESP = system.cornstover_tea.solve_price(system.ethanol) - assert 0.69 < MESP < 0.73, ("MESP of the corn stover biorefinery changed significantly " - f"from 0.700 to {MESP} USD/kg") - -if __name__ == '__main__': - test_biorefineries() - \ No newline at end of file diff --git a/build/lib/biosteam/tests/unit_operation_tests.py b/build/lib/biosteam/tests/unit_operation_tests.py deleted file mode 100644 index cf7c177a8..000000000 --- a/build/lib/biosteam/tests/unit_operation_tests.py +++ /dev/null @@ -1,79 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sun Dec 8 02:13:07 2019 - -@author: yoelr -""" -from biosteam import units -import doctest - -__all__ = ('test_binary_distillation', - 'test_mixer', - 'test_splitter', - 'test_liquids_centrifuge', - 'test_fermentation', - 'test_heat_exchanger', - 'test_pump', - 'test_tank', - 'test_molecular_sieve', - 'test_flash', - 'test_mass_balance', - 'test_process_specification', - 'test_unit_operations') - -def test_binary_distillation(): - doctest.testmod(units._binary_distillation) - -def test_shortcut_column(): - doctest.testmod(units._shortcut_column) - -def test_mixer(): - doctest.testmod(units._mixer) - -def test_splitter(): - doctest.testmod(units._splitter) - -def test_liquids_centrifuge(): - doctest.testmod(units._liquids_centrifuge) - -def test_tank(): - doctest.testmod(units._tank) - -def test_fermentation(): - doctest.testmod(units._fermentation) - -def test_heat_exchanger(): - doctest.testmod(units._hx) - -def test_pump(): - doctest.testmod(units._pump) - -def test_molecular_sieve(): - doctest.testmod(units._molecular_sieve) - -def test_flash(): - doctest.testmod(units._flash) - -def test_mass_balance(): - doctest.testmod(units._balance) - -def test_process_specification(): - doctest.testmod(units._process_specification) - -def test_unit_operations(): - test_binary_distillation() - test_shortcut_column() - test_mixer() - test_splitter() - test_liquids_centrifuge() - test_tank() - test_fermentation() - test_heat_exchanger() - test_pump() - test_molecular_sieve() - test_flash() - test_mass_balance() - test_process_specification() - -if __name__ == '__main__': - test_unit_operations() \ No newline at end of file diff --git a/build/lib/biosteam/tests/utility_tests.py b/build/lib/biosteam/tests/utility_tests.py deleted file mode 100644 index 25154fd1f..000000000 --- a/build/lib/biosteam/tests/utility_tests.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Feb 6 06:45:34 2020 - -@author: yoelr -""" -import biosteam as bst -import doctest - -def test_heat_utility(): - from biosteam import _heat_utility - doctest.testmod(_heat_utility) - -def test_power_utility(): - from biosteam import _power_utility - doctest.testmod(_power_utility) - -def test_utilities(): - test_heat_utility() - test_power_utility() - -if __name__ == '__main__': - test_utilities() \ No newline at end of file diff --git a/build/lib/biosteam/units/_CAS.py b/build/lib/biosteam/units/_CAS.py deleted file mode 100644 index c918ccda8..000000000 --- a/build/lib/biosteam/units/_CAS.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sat Jan 4 05:59:46 2020 - -@author: yoelr -""" - -__all__ = ('H2O_CAS',) - -H2O_CAS = '7732-18-5' \ No newline at end of file diff --git a/build/lib/biosteam/units/__init__.py b/build/lib/biosteam/units/__init__.py deleted file mode 100644 index 1ce956829..000000000 --- a/build/lib/biosteam/units/__init__.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Sun Apr 15 20:39:46 2018 - -@author: Yoel Rene Cortes-Pena -""" -from . import ( - _flash, _liquids_centrifuge, _mixer, _splitter, - _pump, _hx, _multi_effect_evaporator, _shortcut_column, - _binary_distillation, _tank, _magnetic_separator, - _molecular_sieve, _conveying_belt, _vent_scrubber, - _vibrating_screen, _junction, _solids_separator, - _transesterification, _fermentation, - _enzyme_treatment, _clarifier, _rvf, - _solids_centrifuge, _crushing_mill, - _balance, _shredder, _screw_feeder, - decorators, design_tools, facilities, - _process_specification, _duplicator, - _diagram_only_units, -) -__all__ = (*_diagram_only_units.__all__, - *_flash.__all__, - *_liquids_centrifuge.__all__, - *_shortcut_column.__all__, - *_mixer.__all__, - *_splitter.__all__, - *_pump.__all__, - *_hx.__all__, - *_multi_effect_evaporator.__all__, - *_liquids_centrifuge.__all__, - *_binary_distillation.__all__, - *_tank.__all__, - *_molecular_sieve.__all__, - *_conveying_belt.__all__, - *_vent_scrubber.__all__, - *_vibrating_screen.__all__, - *_junction.__all__, - *_solids_separator.__all__, - *_transesterification.__all__, - *_fermentation.__all__, - *_enzyme_treatment.__all__, - *_clarifier.__all__, - *_rvf.__all__, - *_solids_centrifuge.__all__, - *_crushing_mill.__all__, - *_balance.__all__, - *_shredder.__all__, - *_screw_feeder.__all__, - *_magnetic_separator.__all__, - *facilities.__all__, - *_process_specification.__all__, - *_duplicator.__all__, - 'facilities', - 'decorators', - 'design_tools', -) - -from ._shortcut_column import * -from ._duplicator import * -from ._process_specification import * -from ._flash import * -from ._liquids_centrifuge import * -from ._mixer import * -from ._splitter import * -from ._pump import * -from ._hx import * -from ._magnetic_separator import * -from ._multi_effect_evaporator import * -from ._liquids_centrifuge import * -from ._binary_distillation import * -from ._tank import * -from ._molecular_sieve import * -from ._conveying_belt import * -from ._vent_scrubber import * -from ._vibrating_screen import * -from ._junction import * -from ._solids_separator import * -from ._transesterification import * -from ._fermentation import * -from ._enzyme_treatment import * -from ._clarifier import * -from ._rvf import * -from ._solids_centrifuge import * -from ._crushing_mill import * -from ._balance import * -from ._shredder import * -from ._screw_feeder import * -from ._diagram_only_units import * -from .facilities import * - diff --git a/build/lib/biosteam/units/_balance.py b/build/lib/biosteam/units/_balance.py deleted file mode 100644 index fcc6fcf0a..000000000 --- a/build/lib/biosteam/units/_balance.py +++ /dev/null @@ -1,256 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sat Sep 1 17:35:28 2018 - -@author: yoelr -""" -import numpy as np -from .. import Unit -from ._process_specification import ProcessSpecification -from ..utils import static - -__all__ = ('MassBalance',) - -# %% Mass Balance Unit - -@static -class MassBalance(Unit): - """ - Create a Unit object that changes net input flow rates to satisfy output - flow rates. This calculation is based on mass balance equations for - specified IDs. - - Parameters - ---------- - ins : stream - Inlet stream. Doesn't actually affect mass balance. It's just to - show the position in the process. - outs : stream - Outlet stream. Doesn't actually affect mass balance. It's just to - show the position in the process. - chemical_IDs : tuple[str] - Chemicals that will be used to solve mass balance linear equations. - The number of chemicals must be same as the number of input streams varied. - variable_inlets : Iterable[Stream] - Inlet streams that can vary in net flow rate to accomodate for the - mass balance. - constant_inlets: Iterable[Stream], optional - Inlet streams that cannot vary in flow rates. - constant_outlets: Iterable[Stream], optional - Outlet streams that cannot vary in flow rates. - is_exact=True : bool, optional - True if exact flow rate solution is required for the specified IDs. - balance='flow' : {'flow', 'composition'}, optional - * 'flow': Satisfy output flow rates - * 'composition': Satisfy net output molar composition - - Examples - -------- - MassBalance are Unit objects that serve to alter flow rates of selected - chemicals and input streams to satisfy the mass balance. - The example below uses the MassBalance object to satisfy the target - flow rate feeding the mixer M1: - - >>> from biosteam import System, Stream, settings, main_flowsheet - >>> from biosteam.units import (Mixer, Splitter, StorageTank, Pump, - ... Flash, MassBalance) - >>> main_flowsheet.set_flowsheet('mass_balance_example') - >>> settings.set_thermo(['Water', 'Ethanol']) - >>> water = Stream('water', - ... Water=40, - ... units='lb/s', - ... T=350, P=101325) - >>> ethanol = Stream('ethanol', - ... Ethanol=190, Water=30, - ... T=300, P=101325) - >>> target = Stream('target', - ... Ethanol=500, Water=500) - >>> T1 = StorageTank('T1', outs='s1') - >>> T2 = StorageTank('T2', outs='s2') - >>> P1 = Pump('P1', P=101325, outs='s3') - >>> P2 = Pump('P2', P=101325, outs='s4') - >>> M1 = Mixer('M1', outs='s5') - >>> S1 = Splitter('S1', outs=('s6', 's7'), split=0.5) - >>> F1 = Flash('F1', outs=('s8', 's9'), V=0.5, P =101325) - >>> MB1 = MassBalance('MB1', outs='s6_2', - ... variable_inlets=[water, ethanol], - ... constant_inlets=[S1-0], - ... constant_outlets=[target], - ... chemical_IDs=('Ethanol', 'Water'), - ... description='Adjust flow rate of feed to mixer') - >>> # Connect units - >>> water-T1-P1 - - >>> ethanol-T2-P2 - - >>> [P1-0, P2-0, MB1-0]-M1-F1-1-S1-0-MB1 - - >>> sys = main_flowsheet.create_system('sys') - >>> # Make diagram to view system - >>> # sys.diagram() - >>> sys.simulate(); - >>> target.show() - Stream: target - phase: 'l', T: 298.15 K, P: 101325 Pa - flow (kmol/hr): Water 500 - Ethanol 500 - - """ - _graphics = ProcessSpecification._graphics - power_utility = None - heat_utilities = () - results = None - _N_ins = _N_outs = 1 - - def __init__(self, ID='', ins=None, outs=(), thermo=None, - chemical_IDs=None, variable_inlets=(), - constant_outlets=(), constant_inlets=(), - is_exact=True, balance='flow', - description=""): - self._numerical_specification = None - self._load_thermo(thermo) - self._init_ins(ins) - self._init_outs(outs) - self._assert_compatible_property_package() - self._register(ID) - self.variable_inlets = variable_inlets - self.constant_inlets = constant_inlets - self.constant_outlets = constant_outlets - self.chemical_IDs = tuple(chemical_IDs) - self.is_exact = is_exact - self.balance = balance - self.description = description - - def _run(self): - """Solve mass balance by iteration.""" - # SOLVING BY ITERATION TAKES 15 LOOPS FOR 2 STREAMS - # SOLVING BY LEAST-SQUARES TAKES 40 LOOPS - balance = self.balance - solver = np.linalg.solve if self.is_exact else np.linalg.lstsq - - # Set up constant and variable streams - vary = self.variable_inlets # Streams to vary in mass balance - constant = self.constant_inlets # Constant streams - index = self.chemicals.get_index(self.chemical_IDs) - mol_out = sum([s.mol for s in self.constant_outlets]) - - if balance == 'flow': - # Perform the following calculation: Ax = b = f - g - # Where: - # A = flow rate array - # x = factors - # b = target flow rates - # f = output flow rates - # g = constant input flow rates - - # Solve linear equations for mass balance - A = np.array([s.mol for s in vary]).transpose()[index, :] - f = mol_out[index] - g = sum([s.mol[index] for s in constant]) - b = f - g - x = solver(A, b) - - # Set flow rates for input streams - for factor, s in zip(x, vary): - s.mol[:] = s.mol * factor - - elif balance == 'composition': - # Perform the following calculation: - # Ax = b - # = sum( A_ * x_guess + g_ )f - g - # = A_ * x_guess * f - O - # O = sum(g_)*f - g - # Where: - # A_ is flow array for all species - # g_ is constant flows for all species - # Same variable definitions as in 'flow' - - # Set all variables - A_ = np.array([s.mol for s in vary]).transpose() - A = np.array([s.mol for s in vary]).transpose()[index, :] - F_mol_out = mol_out.sum() - z_mol_out = mol_out / F_mol_out if F_mol_out else mol_out - f = z_mol_out[index] - g_ = sum([s.mol for s in constant]) - g = g_[index] - O = sum(g_) * f - g - - # Solve by iteration - x_guess = np.ones_like(index) - not_converged = True - while not_converged: - # Solve linear equations for mass balance - b = (A_ * x_guess).sum()*f + O - x_new = solver(A, b) - not_converged = sum(((x_new - x_guess)/x_new)**2) > 0.0001 - x_guess = x_new - - # Set flow rates for input streams - for factor, s in zip(x_new, vary): - s.mol = s.mol * factor - - else: - raise ValueError( "balance type must be one of the following: 'flow', 'composition'") - - -# %% Energy Balance Unit - -# class EnergyBalance(Unit): -# """Create a Unit object that changes a stream's temperature, flow rate, or vapor fraction to satisfy energy balance. - -# **Parameters** - -# **index:** [int] Index of stream that can vary in temperature, flow rate, or vapor fraction. - -# **Type:** [str] Should be one of the following -# * 'T': Vary temperature of output stream -# * 'F': Vary flow rate of input/output stream -# * 'V': Vary vapor fraction of output stream - -# **Qin:** *[float]* Additional energy input. - -# .. Note:: This is not a mixer, input streams and output streams should match flow rates. - -# """ -# _kwargs = {'index': None, -# 'Type': 'T', -# 'Qin': 0} -# line = 'Balance' -# _has_cost = False -# _graphics = MassBalance._graphics -# _init_ins = MassBalance._init_ins -# _init_outs = MassBalance._init_outs - -# def _run(self): # Get arguments -# ins = self.ins.copy() -# outs = self.outs.copy() -# kwargs = self._kwargs -# index = kwargs['index'] -# Type = kwargs['Type'] -# Qin = kwargs['Qin'] - -# # Pop out required streams -# if Type == 'F': -# s_in = ins.pop(index) -# s_out = outs.pop(index) -# else: -# s = outs.pop(index) - -# # Find required enthalpy -# H_in = sum(i.H for i in ins) + Qin -# H_out = sum(o.H for o in outs) -# H_s = H_out - H_in - -# # Set enthalpy -# if Type == 'T': -# s.H = -H_s -# elif Type == 'V': -# s.enable_phases() -# s.VLE(Qin=s.H - H_s) -# elif Type == 'F': -# s.mol *= (s_out.H - s_in.H)/H_s -# else: -# raise ValueError(f"Type must be 'T', 'V' or 'F', not '{Type}'") - - - \ No newline at end of file diff --git a/build/lib/biosteam/units/_binary_distillation.py b/build/lib/biosteam/units/_binary_distillation.py deleted file mode 100644 index 54e828c16..000000000 --- a/build/lib/biosteam/units/_binary_distillation.py +++ /dev/null @@ -1,1033 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 23 19:33:20 2018 - -@author: yoelr -""" -import numpy as np -from math import ceil -import thermosteam as tmo -from .design_tools.specification_factors import ( - distillation_column_material_factors, - tray_material_factor_functions, - distillation_tray_type_factor, - material_densities_lb_per_in3) -from .design_tools.column_design import ( - compute_tower_height, - compute_tower_diameter, - compute_tower_weight, - compute_tower_wall_thickness, - compute_flow_parameter, - compute_max_vapor_velocity, - compute_max_capacity_parameter, - compute_downcomer_area_fraction, - compute_murphree_stage_efficiency, - compute_purchase_cost_of_trays, - compute_purchase_cost_of_tower) -from .. import Unit -from .._graphics import vertical_column_graphics -from scipy.optimize import brentq -from ._hx import HXutility -import matplotlib.pyplot as plt - -__all__ = ('BinaryDistillation',) - -def compute_stages_McCabeThiele(P, operating_line, - x_stages, y_stages, T_stages, - x_limit, solve_Ty): - """ - Use the McCabe-Thiele method to find the specifications at every stage of - the operating line before the maximum liquid molar fraction, `x_limit`. - Append the light key liquid molar fraction, light key vapor molar - fraction, and stage temperatures to `x_stages`, `y_stages` and `T_stages` - respectively. - - Parameters - ---------- - P : float - Pressure [Pa]. - operating_line : function - Should return the liquid molar fraction of the light - key given its vapor molar fraction. - x_stages : list - Liquid molar compositions at each stage. Last element - should be the starting point for the next stage. - y_stages : list - Vapor molar compositions at each stage. Last element - should be the starting point for the next stage. - x_limit : float - Maximum value of liquid composition before algorithm stops. - T_stages : list - Temperature at each stage. - solve_Ty : function - Should return T and y given x. - - """ - i = 0 - yi = y_stages[-1] - xi = x_stages[-1] - while xi < x_limit: - if i > 100: - raise RuntimeError('cannot meet specifications! stages > 100') - i += 1 - # Go Up - x = np.array((xi, 1-xi)) - T, y = solve_Ty(x, P) - yi = y[0] - y_stages.append(yi) - T_stages.append(T) - # Go Right - xi = operating_line(yi) - if xi > x_limit: - xi = x_limit - x_stages.append(xi) - - -# %% Distillation - -class BinaryDistillation(Unit): - r""" - Create a binary distillation column that assumes all light and heavy non keys - separate to the top and bottoms product respectively. McCabe-Thiele - analysis is used to find both the number of stages and the reflux ratio - given a ratio of actual reflux to minimum reflux [1]_. This assumption - is good for both binary distillation of highly polar compounds and - ternary distillation assuming complete separation of light non-keys - and heavy non-keys with large differences in boiling points. Preliminary - analysis showed that the theoretical number of stages using this method - on Methanol/Glycerol/Water systems is off by less than +-1 stage. Other - methods, such as the Fenske-Underwood-Gilliland method, are more suitable - for hydrocarbons. The Murphree efficiency is based on the modified - O'Connell correlation [2]_. The diameter is based on tray separation - and flooding velocity [1]_ [3]_. Purchase costs are based on correlations - compiled by Warren et. al. [4]_. - - Parameters - ---------- - ins : streams - Inlet fluids to be mixed into the feed stage. - outs : stream sequence - * [0] Distillate - * [1] Bottoms product - LHK : tuple[str] - Light and heavy keys. - y_top : float - Molar fraction of light key to the light and heavy keys in the - distillate. - x_bot : float - Molar fraction of light key to the light and heavy keys in the bottoms - product. - Lr : float - Recovery of the light key in the distillate. - Hr : float - Recovery of the heavy key in the bottoms product. - k : float - Ratio of reflux to minimum reflux. - specification="Composition" : "Composition" or "Recovery" - If composition is used, `y_top` and `x_bot` must be specified. - If recovery is used, `Lr` and `Hr` must be specified. - P=101325 : float - Operating pressure [Pa]. - vessel_material : str, optional - Vessel construction material. Defaults to 'Carbon steel'. - tray_material : str, optional - Tray construction material. Defaults to 'Carbon steel'. - tray_type='Sieve' : 'Sieve', 'Valve', or 'Bubble cap' - Tray type. - tray_spacing=450 : float - Typically between 152 to 915 mm. - stage_efficiency=None : - User enforced stage efficiency. If None, stage efficiency is - calculated by the O'Connell correlation [2]_. - velocity_fraction=0.8 : float - Fraction of actual velocity to maximum velocity allowable before - flooding. - foaming_factor=1.0 : float - Must be between 0 to 1. - open_tray_area_fraction=0.1 : float - Fraction of open area to active area of a tray. - downcomer_area_fraction=None : float - Enforced fraction of downcomer area to net (total) area of a tray. - If None, estimate ratio based on Oliver's estimation [1]_. - is_divided=False : bool - True if the stripper and rectifier are two separate columns. - - References - ---------- - .. [1] J.D. Seader, E.J. Henley, D.K. Roper. (2011) - Separation Process Principles 3rd Edition. John Wiley & Sons, Inc. - - .. [2] M. Duss, R. Taylor. (2018) - Predict Distillation Tray Efficiency. AICHE - - .. [3] Green, D. W. Distillation. In Perry’s Chemical Engineers’ - Handbook, 9 ed.; McGraw-Hill Education, 2018. - - .. [4] Seider, W. D., Lewin, D. R., Seader, J. D., Widagdo, S., Gani, R., - & Ng, M. K. (2017). Product and Process Design Principles. Wiley. - Cost Accounting and Capital Cost Estimation (Chapter 16) - - Examples - -------- - Binary distillation assuming 100% separation on non-keys: - - >>> from biosteam.units import BinaryDistillation - >>> from biosteam import Stream, settings - >>> settings.set_thermo(['Water', 'Methanol', 'Glycerol']) - >>> feed = Stream('feed', flow=(80, 100, 25)) - >>> bp = feed.bubble_point_at_P() - >>> feed.T = bp.T # Feed at bubble point T - >>> D1 = BinaryDistillation('D1', ins=feed, - ... outs=('distillate', 'bottoms_product'), - ... LHK=('Methanol', 'Water'), - ... y_top=0.99, x_bot=0.01, k=2, - ... is_divided=True) - >>> D1.simulate() - >>> # See all results - >>> D1.show(T='degC', P='atm', composition=True) - BinaryDistillation: D1 - ins... - [0] feed - phase: 'l', T: 76.129 degC, P: 1 atm - composition: Water 0.39 - Methanol 0.488 - Glycerol 0.122 - -------- 205 kmol/hr - outs... - [0] distillate - phase: 'g', T: 64.91 degC, P: 1 atm - composition: Water 0.01 - Methanol 0.99 - -------- 100 kmol/hr - [1] bottoms_product - phase: 'l', T: 100.06 degC, P: 1 atm - composition: Water 0.754 - Methanol 0.00761 - Glycerol 0.239 - -------- 105 kmol/hr - >>> D1.results() - Distillation Units D1 - Cooling water Duty kJ/hr -5.11e+06 - Flow kmol/hr 3.49e+03 - Cost USD/hr 1.71 - Low pressure steam Duty kJ/hr 9.49e+06 - Flow kmol/hr 244 - Cost USD/hr 58.1 - Design Theoretical feed stage 9 - Theoretical stages 13 - Minimum reflux Ratio 0.687 - Reflux Ratio 1.37 - Rectifier stages 15 - Stripper stages 13 - Rectifier height ft 34.7 - Stripper height ft 31.7 - Rectifier diameter ft 3.93 - Stripper diameter ft 3.6 - Rectifier wall thickness in 0.25 - Stripper wall thickness in 0.25 - Rectifier weight lb 4.8e+03 - Stripper weight lb 4.03e+03 - Purchase cost Rectifier trays USD 1.5e+04 - Stripper trays USD 1.33e+04 - Rectifier tower USD 7.63e+04 - Stripper tower USD 6.82e+04 - Condenser USD 2.02e+04 - Boiler USD 4.27e+03 - Total purchase cost USD 1.97e+05 - Utility cost USD/hr 59.8 - - """ - line = 'Distillation' - _graphics = vertical_column_graphics - _N_heat_utilities = 0 - _ins_size_is_fixed = False - _N_ins = 1 - _N_outs = 2 - _units = {'Minimum reflux': 'Ratio', - 'Reflux': 'Ratio', - 'Rectifier height': 'ft', - 'Rectifier diameter': 'ft', - 'Rectifier wall thickness': 'in', - 'Rectifier weight': 'lb', - 'Stripper height': 'ft', - 'Stripper diameter': 'ft', - 'Stripper wall thickness': 'in', - 'Stripper weight': 'lb', - 'Height': 'ft', - 'Diameter': 'ft', - 'Wall thickness': 'in', - 'Weight': 'lb'} - # Bare module factor - BM = 4.3 - - # [dict] Bounds for results - _bounds = {'Diameter': (3., 24.), - 'Height': (27., 170.), - 'Weight': (9000., 2.5e6)} - - def __init__(self, ID='', ins=None, outs=(), thermo=None, - P=101325, *, LHK, k, - Lr=None, - Hr=None, - y_top=None, - x_bot=None, - product_specification_format='Composition', - vessel_material='Carbon steel', - tray_material='Carbon steel', - tray_type='Sieve', - tray_spacing=450, - stage_efficiency=None, - velocity_fraction=0.8, - foaming_factor=1.0, - open_tray_area_fraction=0.1, - downcomer_area_fraction=None, - is_divided=False): - Unit.__init__(self, ID, ins, outs, thermo) - - # Operation specifications - self.k = k - self.P = P - self.LHK = LHK - self._set_distillation_product_specifications(product_specification_format, - x_bot, y_top, Lr, Hr) - - # Construction specifications - self.vessel_material = vessel_material - self.tray_type = tray_type - self.tray_material = tray_material - self.tray_spacing = tray_spacing - self.stage_efficiency = stage_efficiency - self.velocity_fraction = velocity_fraction - self.foaming_factor = foaming_factor - self.open_tray_area_fraction = open_tray_area_fraction - self.downcomer_area_fraction = downcomer_area_fraction - self.is_divided = is_divided - - # Setup components - thermo = self.thermo - - #: [MultiStream] Overall feed to the distillation column - self.feed = tmo.MultiStream(None, thermo=thermo) - - #: [HXutility] Condenser. - self.condenser = HXutility(None, - ins=tmo.Stream(None, phase='g', thermo=thermo), - outs=tmo.MultiStream(None, thermo=thermo), - thermo=thermo) - #: [HXutility] Boiler. - self.boiler = HXutility(None, - ins=tmo.Stream(None, thermo=thermo), - outs=tmo.MultiStream(None, thermo=thermo), - thermo=thermo) - self.heat_utilities = self.condenser.heat_utilities + self.boiler.heat_utilities - self._setup_cache() - - def _setup_cache(self): - self._McCabeThiele_args = np.zeros(6) - - @property - def product_specification_format(self): - return self._product_specification_format - @product_specification_format.setter - def product_specification_format(self, spec): - assert spec in ('Composition', 'Recovery'), ( - "product specification format must be either 'Composition' or 'Recovery'") - self._product_specification_format = spec - - @property - def condensate(self): - return self.condenser.outs[0]['l'] - @property - def boilup(self): - return self.boiler.outs[0]['g'] - - @property - def LHK(self): - """tuple[str, str] Light and heavy keys.""" - return self._LHK - @LHK.setter - def LHK(self, LHK): - # Set light non-key and heavy non-key indices - self._LHK = LHK = tuple(LHK) - intermediate_volatile_chemicals = [] - chemicals = self.chemicals - LHK_chemicals = LK_chemical, HK_chemical = self.chemicals.retrieve(LHK) - Tb_light = LK_chemical.Tb - Tb_heavy = HK_chemical.Tb - LNK = [] - HNK = [] - gases = [] - solids = [] - if Tb_light > Tb_heavy: - LK, HK = LHK - raise ValueError(f"light key must be lighter than heavy key " - f"(i.e. {LK}.Tb must be lower than {HK}.Tb)") - for chemical in chemicals: - ID = chemical.ID - Tb = chemical.Tb - if not Tb or chemical.locked_state in ('l', 's'): - solids.append(ID) - elif chemical.locked_state == 'g': - gases.append(ID) - elif Tb < Tb_light: - LNK.append(ID) - elif Tb > Tb_heavy: - HNK.append(ID) - elif chemical not in LHK_chemicals: - intermediate_volatile_chemicals.append(chemical.ID) - self._LNK = LNK = tuple(LNK) - self._HNK = HNK = tuple(HNK) - self._gases = gases = tuple(gases) - self._solids = solids = tuple(solids) - get_index = self.chemicals.get_index - self._LHK_index = get_index(LHK) - self._LNK_index = get_index(LNK) - self._HNK_index = get_index(HNK) - self._gases_index = get_index(gases) - self._solids_index = get_index(solids) - self._intermediate_volatile_chemicals = tuple(intermediate_volatile_chemicals) - - @property - def y_top(self): - """Light key composition of at the distillate.""" - return self._y_top - @y_top.setter - def y_top(self, y_top): - assert self.product_specification_format == "Composition", ( - "product specification format must be 'Composition' " - "to set distillate composition") - assert 0 < y_top < 1, "light key composition in the distillate must be a fraction" - self._y_top = y_top - self._y = np.array([y_top, 1-y_top]) - - @property - def x_bot(self): - """Light key composition at the bottoms product.""" - return self._x_bot - @x_bot.setter - def x_bot(self, x_bot): - assert self.product_specification_format == "Composition", ( - "product specification format must be 'Composition' to set bottoms " - "product composition") - assert 0 < x_bot < 1, "heavy key composition in the bottoms product must be a fraction" - self._x_bot = x_bot - self._x = np.array([x_bot, 1-x_bot]) - - @property - def Lr(self): - """Light key recovery in the distillate.""" - return self._Lr - @Lr.setter - def Lr(self, Lr): - assert self.product_specification_format == "Recovery", ( - "product specification format must be 'Recovery' " - "to set light key recovery") - assert 0 < Lr < 1, "light key recovery in the distillate must be a fraction" - self._Lr = Lr - - @property - def Hr(self): - """Heavy key recovery in the bottoms product.""" - return self._Hr - @Hr.setter - def Hr(self, Hr): - assert self.product_specification_format == "Recovery", ( - "product specification format must be 'Recovery' " - "to set heavy key recovery") - assert 0 < Hr < 1, "heavy key recovery in the bottoms product must be a fraction" - self._Hr = Hr - - @property - def tray_spacing(self): - return self._TS - @tray_spacing.setter - def tray_spacing(self, TS): - """Tray spacing (225-600 mm).""" - self._TS = TS - - @property - def stage_efficiency(self): - """Enforced user defined stage efficiency.""" - return self._E_eff - @stage_efficiency.setter - def stage_efficiency(self, E_eff): - self._E_eff = E_eff - - @property - def velocity_fraction(self): - """Fraction of actual velocity to maximum velocity allowable before flooding.""" - return self._f - @velocity_fraction.setter - def velocity_fraction(self, f): - self._f = f - - @property - def foaming_factor(self): - """Foaming factor (0 to 1).""" - return self._F_F - @foaming_factor.setter - def foaming_factor(self, F_F): - if not 0 <= F_F <= 1: - raise ValueError(f"foaming_factor must be between 0 and 1, ({F_F} given).") - self._F_F = F_F - - @property - def open_tray_area_fraction(self): - """Fraction of open area, A_h, to active area, A_a.""" - return self._A_ha - @open_tray_area_fraction.setter - def open_tray_area_fraction(self, A_ha): - self._A_ha = A_ha - - @property - def downcomer_area_fraction(self): - """Enforced fraction of downcomer area to net (total) area. - If None, the fraction is estimated based on heuristics.""" - return self._A_dn - @downcomer_area_fraction.setter - def downcomer_area_fraction(self, A_dn): - self._A_dn = A_dn - - @property - def tray_type(self): - """Default 'Sieve'""" - return self._tray_type - @tray_type.setter - def tray_type(self, tray_type): - if tray_type in distillation_tray_type_factor: - self._tray_type = tray_type - self._F_TT = distillation_tray_type_factor[tray_type] - else: - raise ValueError("tray type must be one of the following: " - f"{', '.join(distillation_tray_type_factor)}") - - @property - def tray_material(self): - """Default 'Carbon steel'""" - return self._tray_material - @tray_material.setter - def tray_material(self, tray_material): - if tray_material in tray_material_factor_functions: - self._tray_material = tray_material - self._F_TM_function = tray_material_factor_functions[tray_material] - else: - raise ValueError("tray material must be one of the following: " - f"{', '.join(tray_material_factor_functions)}") - - @property - def vessel_material(self): - """Default 'Carbon steel'""" - return self._vessel_material - @vessel_material.setter - def vessel_material(self, vessel_material): - if vessel_material in distillation_column_material_factors: - self._vessel_material = vessel_material - self._F_VM = distillation_column_material_factors[vessel_material] - else: - raise ValueError("vessel material must be one of the following: " - f"{', '.join(distillation_column_material_factors)}") - - @property - def is_divided(self): - """[bool] True if the stripper and rectifier are two separate columns.""" - return self._is_divided - @is_divided.setter - def is_divided(self, is_divided): - self._is_divided = is_divided - - def _set_distillation_product_specifications(self, - product_specification_format, - x_bot, y_top, Lr, Hr): - self._product_specification_format = product_specification_format - if product_specification_format == 'Composition': - self.y_top = y_top - self.x_bot = x_bot - elif product_specification_format == 'Recovery': - self.Lr = Lr - self.Hr = Hr - else: - raise ValueError("product specification format must be either 'Composition' or 'Recovery'") - - def _get_y_top_and_x_bot(self): - if self.product_specification_format == 'Composition': - y_top = self.y_top - x_bot = self.x_bot - else: - distillate, bottoms_product = self.outs - LHK = self._LHK - y_top, _ = distillate.get_normalized_mol(LHK) - x_bot, _ = bottoms_product.get_normalized_mol(LHK) - return y_top, x_bot - - def _check_mass_balance(self): - LHK = self._LHK - intermediate_chemicals = self._intermediate_volatile_chemicals - intemediates_index = self.chemicals.get_index(intermediate_chemicals) - intermediate_flows = self.mol_in[intemediates_index] - for flow, chemical in zip(intermediate_flows, intermediate_chemicals): - assert flow==0, ("intermediate volatile chemical," - f"'{chemical}', between light and heavy " - f"key, {', '.join(LHK)}") - distillate, bottoms_product = self.outs - LHK = self._LHK - LHK_index = self._LHK_index - LK_distillate, HK_distillate = distillate.mol[LHK_index] - LK_bottoms, HK_bottoms = bottoms_product.mol[LHK_index] - assert LK_distillate >= 0 and LK_bottoms >= 0, ("Light key composition is infeasible") - assert HK_distillate >= 0 and HK_bottoms >= 0, ("heavy key composition is infeasible") - - def _run_binary_distillation_mass_balance(self): - # Get all important flow rates (both light and heavy keys and non-keys) - feed = self.feed - feed.mix_from(self.ins) - mol = feed.mol - LHK_index = self._LHK_index - LNK_index = self._LNK_index - HNK_index = self._HNK_index - gases_index = self._gases_index - solids_index = self._solids_index - intermediate_chemicals = self._intermediate_volatile_chemicals - intemediates_index = self.chemicals.get_index(intermediate_chemicals) - LHK_mol = mol[LHK_index] - LNK_mol = mol[LNK_index] - HNK_mol = mol[HNK_index] - gases_mol = mol[gases_index] - solids_mol = mol[solids_index] - - # Mass balance for non-keys - distillate, bottoms_product = self.outs - distillate.mol[LNK_index] = LNK_mol - distillate.mol[gases_index] = gases_mol - bottoms_product.mol[HNK_index] = HNK_mol - bottoms_product.mol[solids_index] = solids_mol - - # Mass balance for keys - spec = self.product_specification_format - if spec == 'Composition': - # Use lever rule - light, heavy = LHK_mol - F_mol_LHK = light + heavy - zf = light/F_mol_LHK - distillate_fraction = (zf-self.x_bot)/(self.y_top-self.x_bot) - F_mol_LHK_distillate = F_mol_LHK * distillate_fraction - distillate_LHK_mol = F_mol_LHK_distillate * self._y - elif spec == 'Recovery': - distillate_LHK_mol = LHK_mol * [self.Lr, (1 - self.Hr)] - else: - raise ValueError("invalid specification '{spec}'") - distillate.mol[LHK_index] = distillate_LHK_mol - bottoms_product.mol[LHK_index] = LHK_mol - distillate_LHK_mol - distillate.mol[intemediates_index] = \ - bottoms_product.mol[intemediates_index] = mol[intemediates_index] / 2 - if tmo.settings.debug: - self._check_mass_balance() - - def _update_distillate_and_bottoms_temperature(self): - distillate, bottoms_product = self.outs - self._condensate_dew_point = dp = distillate.dew_point_at_P() - self._boilup_bubble_point = bp = bottoms_product.bubble_point_at_P() - bottoms_product.T = bp.T - distillate.T = dp.T - - def _setup(self): - distillate, bottoms_product = self.outs - distillate.P = bottoms_product.P = self.P - distillate.phase = 'g' - bottoms_product.phase = 'l' - - def _run(self): - self._run_binary_distillation_mass_balance() - self._update_distillate_and_bottoms_temperature() - - def _get_feed_quality(self): - feed = self.feed - feed = feed.copy() - H_feed = feed.H - try: dp = feed.dew_point_at_P() - except: pass - else: feed.T = dp.T - feed.phase = 'g' - H_vap = feed.H - try: bp = feed.bubble_point_at_P() - except: pass - else: feed.T = bp.T - feed.phase = 'l' - H_liq = feed.H - q = (H_vap - H_feed) / (H_vap - H_liq) - return q - - def _run_McCabeThiele(self): - distillate, bottoms = self.outs - chemicals = self.chemicals - LHK = self._LHK - LHK_index = chemicals.get_index(LHK) - - # Feed light key mol fraction - feed = self.feed - liq_mol = feed.imol['l'] - vap_mol = feed.imol['g'] - LHK_mol = liq_mol[LHK_index] + vap_mol[LHK_index] - F_mol_LHK = LHK_mol.sum() - zf = LHK_mol[0]/F_mol_LHK - q = self._get_feed_quality() - - # Main arguments - P = self.P - k = self.k - y_top, x_bot = self._get_y_top_and_x_bot() - - # Cache - old_args = self._McCabeThiele_args - args = np.array([P, k, y_top, x_bot, q, zf]) - tol = np.array([50, 1e-5, 1e-6, 1e-6, 1e-2, 1e-6], float) - if (abs(old_args - args) < tol).all(): return - self._McCabeThiele_args = args - - # Get R_min and the q_line - if abs(q - 1) < 1e-6: - q = 1 - 1e-5 - q_line = lambda x: q*x/(q-1) - zf/(q-1) - self._q_line_args = dict(q=q, zf=zf) - - solve_Ty = bottoms.get_bubble_point(LHK).solve_Ty - Rmin_intersection = lambda x: q_line(x) - solve_Ty(np.array((x, 1-x)), P)[1][0] - x_Rmin = brentq(Rmin_intersection, 0, 1) - y_Rmin = q_line(x_Rmin) - m = (y_Rmin-y_top)/(x_Rmin-y_top) - Rmin = m/(1-m) - if Rmin <= 0.05: - Rmin = 0.05 - R = Rmin*k - - # Rectifying section: Inntersects q_line with slope given by R/(R+1) - m1 = R/(R+1) - b1 = y_top-m1*y_top - rs = lambda y: (y - b1)/m1 # -> x - - # y_m is the solution to lambda y: y - q_line(rs(y)) - self._y_m = y_m = (q*b1 + m1*zf)/(q - m1*(q-1)) - self._x_m = x_m = rs(y_m) - - # Stripping section: Intersects Rectifying section and q_line and beggins at bottoms liquid composition - m2 = (x_bot-y_m)/(x_bot-x_m) - b2 = y_m-m2*x_m - ss = lambda y: (y-b2)/m2 # -> x - - # Data for staircase - self._x_stages = x_stages = [x_bot] - self._y_stages = y_stages = [x_bot] - self._T_stages = T_stages = [] - compute_stages_McCabeThiele(P, ss, x_stages, y_stages, T_stages, x_m, solve_Ty) - yi = y_stages[-1] - xi = rs(yi) - x_stages[-1] = xi if xi < 1 else 0.99999 - compute_stages_McCabeThiele(P, rs, x_stages, y_stages, T_stages, y_top, solve_Ty) - - # Find feed stage - N_stages = len(x_stages) - feed_stage = ceil(N_stages/2) - for i in range(len(y_stages)-1): - if y_stages[i] < y_m < y_stages[i+1]: - feed_stage = i+1 - - # Results - Design = self.design_results - Design['Theoretical feed stage'] = N_stages - feed_stage - Design['Theoretical stages'] = N_stages - Design['Minimum reflux'] = Rmin - Design['Reflux'] = R - - def _run_condenser_and_boiler(self): - feed = self.feed - distillate, bottoms_product = self.outs - condenser = self.condenser - boiler = self.boiler - R = self.design_results['Reflux'] - - # Set condenser conditions - condenser.outs[0].imol['g'] = distillate.mol - self._F_mol_distillate = F_mol_distillate = distillate.F_mol - self._F_mol_condensate = F_mol_condensate = R * F_mol_distillate - dp = self._condensate_dew_point - condensate_x_mol = dp.x - condensate = self.condensate - condensate.empty() - condensate.imol[dp.IDs] = condensate_x_mol * F_mol_condensate - condensate.T = dp.T - condensate.P = dp.P - vap = condenser.ins[0] - vap.mol = distillate.mol + condensate.mol - vap.T = distillate.T - vap.P = distillate.P - - # Set boiler conditions - boiler.outs[0].imol['l'] = bottoms_product.mol - F_vap_feed = feed.imol['g'].sum() - self._F_mol_boilup = F_mol_boilup = (R+1)*F_mol_distillate - F_vap_feed - bp = self._boilup_bubble_point - boilup_flow = bp.y * F_mol_boilup - boilup = self.boilup - boilup.T = bp.T - boilup.P = bp.P - boilup.imol[bp.IDs] = boilup_flow - liq = boiler.ins[0] - liq.mol = bottoms_product.mol + boilup.mol - - def _simulate_condenser(self): - condenser = self.condenser - condenser._design() - condenser._cost() - - def _simulate_boiler(self): - boiler = self.boiler - boiler._design(self.H_out - self.H_in - self.condenser.Q) - boiler._cost() - - def _simulate_components(self): - # Cost condenser - self._simulate_condenser() - self.purchase_costs['Condenser'] = self.condenser.purchase_costs['Heat exchanger'] - - # Cost boiler - self._simulate_boiler() - self.purchase_costs['Boiler'] = self.boiler.purchase_costs['Heat exchanger'] - - def _get_relative_volatilities_LHK(self): - x_stages = self._x_stages - y_stages = self._y_stages - - K_light = y_stages[-1]/x_stages[-1] - K_heavy = (1-y_stages[-1])/(1-x_stages[-1]) - alpha_LHK_distillate = K_light/K_heavy - - K_light = y_stages[0]/x_stages[0] - K_heavy = (1-y_stages[0])/(1-x_stages[0] ) - alpha_LHK_bottoms = K_light/K_heavy - - return alpha_LHK_distillate, alpha_LHK_bottoms - - def _compute_N_stages(self): - """Return a tuple with the actual number of stages for the rectifier and the stripper.""" - feed = self.feed - vap, liq = self.outs - Design = self.design_results - R = Design['Reflux'] - N_stages = Design['Theoretical stages'] - feed_stage = Design['Theoretical feed stage'] - E_eff = self.stage_efficiency - if E_eff: - E_rectifier = E_stripper = E_eff - else: - # Calculate Murphree Efficiency for rectifying section - condensate = self.condensate - mu = condensate.get_property('mu', 'mPa*s') - alpha_LHK_distillate, alpha_LHK_bottoms = self._get_relative_volatilities_LHK() - F_mol_distillate = self._F_mol_distillate - L_Rmol = self._F_mol_condensate - V_Rmol = (R+1) * F_mol_distillate - E_rectifier = compute_murphree_stage_efficiency(mu, - alpha_LHK_distillate, - L_Rmol, V_Rmol) - - # Calculate Murphree Efficiency for stripping section - mu = liq.get_property('mu', 'mPa*s') - V_Smol = self._F_mol_boilup - L_Smol = R*F_mol_distillate + feed.imol['g'].sum() - E_stripper = compute_murphree_stage_efficiency(mu, - alpha_LHK_bottoms, - L_Smol, V_Smol) - - # Calculate actual number of stages - mid_stage = feed_stage - 0.5 - N_rectifier = np.ceil(mid_stage/E_rectifier) - N_stripper = np.ceil((N_stages-mid_stage)/E_stripper) - return N_rectifier, N_stripper - - def _design(self): - self._run_McCabeThiele() - self._run_condenser_and_boiler() - self._complete_distillation_column_design() - - def _complete_distillation_column_design(self): - distillate, bottoms_product = self.outs - Design = self.design_results - R = Design['Reflux'] - Rstages, Sstages = self._compute_N_stages() - is_divided = self.is_divided - TS = self._TS - - ### Get diameter of rectifying section based on top plate ### - - condensate = self.condensate - rho_L = condensate.rho - sigma = condensate.get_property('sigma', 'dyn/cm') - L = condensate.F_mass - V = L*(R+1)/R - vap = self.condenser.ins[0] - V_vol = vap.get_total_flow('m^3/s') - rho_V = distillate.rho - F_LV = compute_flow_parameter(L, V, rho_V, rho_L) - C_sbf = compute_max_capacity_parameter(TS, F_LV) - F_F = self._F_F - A_ha = self._A_ha - U_f = compute_max_vapor_velocity(C_sbf, sigma, rho_L, rho_V, F_F, A_ha) - A_dn = self._A_dn - if A_dn is None: - self._A_dn = A_dn = compute_downcomer_area_fraction(F_LV) - f = self._f - R_diameter = compute_tower_diameter(V_vol, U_f, f, A_dn) * 3.28 - - ### Get diameter of stripping section based on feed plate ### - rho_L = bottoms_product.rho - boilup = self.boilup - V = boilup.F_mass - V_vol = boilup.get_total_flow('m^3/s') - rho_V = boilup.rho - L = bottoms_product.F_mass # To get liquid going down - F_LV = compute_flow_parameter(L, V, rho_V, rho_L) - C_sbf = compute_max_capacity_parameter(TS, F_LV) - sigma = condensate.get_property('sigma', 'dyn/cm') - U_f = compute_max_vapor_velocity(C_sbf, sigma, rho_L, rho_V, F_F, A_ha) - A_dn = self._A_dn - if A_dn is None: - A_dn = compute_downcomer_area_fraction(F_LV) - S_diameter = compute_tower_diameter(V_vol, U_f, f, A_dn) * 3.28 - Po = self.P * 0.000145078 # to psi - rho_M = material_densities_lb_per_in3[self.vessel_material] - - if is_divided: - Design['Rectifier stages'] = Rstages - Design['Stripper stages'] = Sstages - Design['Rectifier height'] = H_R = compute_tower_height(TS, Rstages-1) * 3.28 - Design['Stripper height'] = H_S = compute_tower_height(TS, Sstages-1) * 3.28 - Design['Rectifier diameter'] = R_diameter - Design['Stripper diameter'] = S_diameter - Design['Rectifier wall thickness'] = tv = compute_tower_wall_thickness(Po, R_diameter, H_R) - Design['Stripper wall thickness'] = tv = compute_tower_wall_thickness(Po, S_diameter, H_S) - Design['Rectifier weight'] = compute_tower_weight(R_diameter, H_R, tv, rho_M) - Design['Stripper weight'] = compute_tower_weight(S_diameter, H_S, tv, rho_M) - else: - Design['Actual stages'] = Rstages + Sstages - Design['Height'] = H = compute_tower_height(TS, Rstages+Sstages-2) * 3.28 - Design['Diameter'] = Di = max((R_diameter, S_diameter)) - Design['Wall thickness'] = tv = compute_tower_wall_thickness(Po, Di, H) - Design['Weight'] = compute_tower_weight(Di, H, tv, rho_M) - - def _cost(self): - Design = self.design_results - Cost = self.purchase_costs - F_TT = self._F_TT - F_VM = self._F_VM - if self.is_divided: - # Number of trays assuming a partial condenser - N_RT = Design['Rectifier stages'] - 1 - Di_R = Design['Rectifier diameter'] - F_TM = self._F_TM_function(Di_R) - Cost['Rectifier trays'] = compute_purchase_cost_of_trays(N_RT, Di_R, F_TT, F_TM) - N_ST = Design['Stripper stages'] - 1 - Di_S = Design['Stripper diameter'] - F_TM = self._F_TM_function(Di_R) - Cost['Stripper trays'] = compute_purchase_cost_of_trays(N_ST, Di_S, F_TT, F_TM) - - # Cost vessel assuming T < 800 F - W_R = Design['Rectifier weight'] # in lb - H_R = Design['Rectifier height']*3.28 # in ft - Cost['Rectifier tower'] = compute_purchase_cost_of_tower(Di_R, H_R, W_R, F_VM) - W_S = Design['Stripper weight'] # in lb - H_S = Design['Stripper height']*3.28 # in ft - Cost['Stripper tower'] = compute_purchase_cost_of_tower(Di_S, H_S, W_S, F_VM) - else: - # Cost trays assuming a partial condenser - N_T = Design['Actual stages'] - 1 - Di = Design['Diameter'] - F_TM = self._F_TM_function(Di) - Cost['Trays'] = compute_purchase_cost_of_trays(N_T, Di, F_TT, F_TM) - - # Cost vessel assuming T < 800 F - W = Design['Weight'] # in lb - L = Design['Height']*3.28 # in ft - Cost['Tower'] = compute_purchase_cost_of_tower(Di, L, W, F_VM) - self._simulate_components() - - def _plot_stages(self): - """Plot stages, graphical aid line, and equilibrium curve. The plot does not include operating lines nor a legend.""" - vap, liq = self.outs - if not hasattr(self, '_x_stages'): - raise RuntimeError('cannot plot stages without running McCabe Thiele binary distillation') - x_stages = self._x_stages - y_stages = self._y_stages - LHK = self.LHK - LK = self.LHK[0] - P = self.P - - # Equilibrium data - x_eq = np.linspace(0, 1, 100) - y_eq = np.zeros(100) - T = np.zeros(100) - n = 0 - - bp = vap.get_bubble_point(IDs=LHK) - solve_Ty = bp.solve_Ty - for xi in x_eq: - T[n], y = solve_Ty(np.array([xi, 1-xi]), P) - y_eq[n] = y[0] - n += 1 - - # Set-up graph - plt.figure() - plt.xticks(np.arange(0, 1.1, 0.1), fontsize=12) - plt.yticks(fontsize=12) - plt.xlabel('x (' + LK + ')', fontsize=16) - plt.ylabel('y (' + LK + ')', fontsize=16) - plt.xlim([0, 1]) - - # Plot stages - x_stairs = [] - for x in x_stages: - x_stairs.append(x) - x_stairs.append(x) - - y_stairs = [] - for y in y_stages: - y_stairs.append(y) - y_stairs.append(y) - x_stairs.pop(-1) - x_stairs.insert(0, y_stairs[0]) - plt.plot(x_stairs, y_stairs, '--') - - # Graphical aid line - plt.plot([0, 1], [0, 1]) - - # Vapor equilibrium graph - plt.plot(x_eq, y_eq, lw=2) - - def plot_stages(self): - """Plot the McCabe Thiele Diagram.""" - # Plot stages, graphical aid and equilibrium curve - self._plot_stages() - vap, liq = self.outs - Design = self.design_results - if not hasattr(self, '_x_stages'): self._design() - q_args = self._q_line_args - zf = q_args['zf'] - q = q_args['q'] - q_line = lambda x: q*x/(q-1) - zf/(q-1) - y_top, x_bot = self._get_y_top_and_x_bot() - stages = Design['Theoretical stages'] - Rmin = Design['Minimum reflux'] - R = Design['Reflux'] - feed_stage = Design['Theoretical feed stage'] - - # q_line - intersect2 = lambda x: x - q_line(x) - x_m2 = brentq(intersect2, 0, 1) - - # Graph q-line, Rectifying and Stripping section - plt.plot([self._x_m, x_m2], [self._y_m, x_m2]) - plt.plot([self._x_m, y_top], [self._y_m, y_top]) - plt.plot([x_bot, self._x_m], [x_bot, self._y_m]) - plt.legend([f'Stages: {stages}, Feed: {feed_stage}', 'Graphical aid', 'eq-line', 'q-line', 'ROL', 'SOL'], fontsize=12) - plt.title(f'McCabe Thiele Diagram (Rmin = {Rmin:.2f}, R = {R:.2f})') - plt.show() - return plt diff --git a/build/lib/biosteam/units/_clarifier.py b/build/lib/biosteam/units/_clarifier.py deleted file mode 100644 index a00636c1d..000000000 --- a/build/lib/biosteam/units/_clarifier.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 23 22:18:16 2018 - -@author: yoelr -""" - -from ._splitter import Splitter -from ..exceptions import DesignError -from .decorators import cost - -__all__ = ('Clarifier',) - -_iswithin = lambda x, bounds: bounds[0] < x < bounds[1] -# Electricity: 16 hp / 200 ft diameter - -@cost('Settling area', cost=2720, CE=567, n=0.58, kW=0.0005) -class Clarifier(Splitter): - _units = {'Settling area': 'ft^2'} - # Height of the clarifier tank from other designs, estimate (ft) - height = 10 - - # Setting the working bounds for different materials - _bounds = {'Area steel' : (80, 8000), 'Area concrete': (8000, 125000)} - - def _design(self): - # Heuristic settling area estimation - # Settling area in ft^2 = overflow in gpm - Design = self.design_results - Design['Settling area'] = SetArea = self.outs[0].F_vol * 4.4028 - # Checking to see which cost equation/material to use - Steel_bounds, Concrete_bounds = self._bounds.values() - if _iswithin(SetArea, Steel_bounds): Design['Material'] = 'Steel' - elif _iswithin(SetArea, Concrete_bounds): Design['Material'] = 'Concrete' - else: raise DesignError('Volumetric flow rate is out of working range.') - - -_cost = Clarifier._cost -def _extended_cost(self): - _cost(self) - self.purchase_costs['Clarifier'] *= 1.4 if self.design_results['Material']=='Steel' else 1 -Clarifier._cost = _extended_cost - - - \ No newline at end of file diff --git a/build/lib/biosteam/units/_conveying_belt.py b/build/lib/biosteam/units/_conveying_belt.py deleted file mode 100644 index b2955f8a3..000000000 --- a/build/lib/biosteam/units/_conveying_belt.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon Mar 4 11:10:49 2019 - -@author: yoelr -""" -from .decorators import cost -from .._unit import Unit - -__all__ = ('ConveyingBelt',) - -@cost('Flow rate', CE=567, cost=813, ub=2000, n=0.38, N='Number of conveyors') -class ConveyingBelt(Unit): - length = 40 #: ft - height = 20 #: ft - _N_outs = 1 - _has_power_utility = True - _minimum_flow = 120 - _units = {'Flow rate': 'ft^3/hr'} - - def _design(self): - feed = self.ins[0] - self.design_results['Flow rate'] = F_vol = feed.F_vol*35.315 # ft3/hr - if F_vol < self._minimum_flow: - self._lb_warning('Flow rate', F_vol, self._minimum_flow) - F_mass = feed.F_mass*0.0006124 #lb/s - self.power_utility(0.00058*F_mass**0.82*self.length - + self.height*0.00182*F_mass * 0.7457) # kW - - - diff --git a/build/lib/biosteam/units/_crushing_mill.py b/build/lib/biosteam/units/_crushing_mill.py deleted file mode 100644 index 634d4ba7b..000000000 --- a/build/lib/biosteam/units/_crushing_mill.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 23 22:14:01 2018 - -@author: yoelr -""" -from ._solids_separator import SolidsSeparator -from .decorators import cost - -__all__ = ('CrushingMill',) - -@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 CrushingMill object. - - Parameters - ---------- - ins : stream sequence - * [0] Shredded sugar cane - * [1] Recycle water - outs : stream sequence - * [0] Bagasse - * [1] Juice - moisture_content : float - Fraction of water in Baggasse. - - """ - diff --git a/build/lib/biosteam/units/_diagram_only_units.py b/build/lib/biosteam/units/_diagram_only_units.py deleted file mode 100644 index ed29d8995..000000000 --- a/build/lib/biosteam/units/_diagram_only_units.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon Mar 30 23:25:42 2020 - -@author: yoelr -""" -from .._unit import Unit -from .._graphics import system_unit, stream_unit - -__all__ = ('DiagramOnlyUnit', - 'DiagramOnlySystemUnit', - 'DiagramOnlyStreamUnit') - -class DiagramOnlyUnit(Unit, isabstract=True): - _ID = ID = None - _N_ins = _N_outs = 1 - _ins_size_is_fixed = _outs_size_is_fixed = False - - def __init__(self, ID='', ins=None, outs=(), thermo=None): - self._load_thermo(thermo) - self._init_ins(ins) - self._init_outs(outs) - self._register(ID) - - def _register(self, ID): - self.ID = self._ID = ID - -class DiagramOnlySystemUnit(DiagramOnlyUnit, isabstract=True): - """Dummy unit for displaying a system as a unit.""" - line = 'System' - _graphics = system_unit - -class DiagramOnlyStreamUnit(DiagramOnlyUnit, isabstract=True): - """Dummy unit for displaying a streams as a unit.""" - line = '' - _graphics = stream_unit \ No newline at end of file diff --git a/build/lib/biosteam/units/_duplicator.py b/build/lib/biosteam/units/_duplicator.py deleted file mode 100644 index fd3ebf61b..000000000 --- a/build/lib/biosteam/units/_duplicator.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -Created on Thu January 23 16:27:00 2020 - -@author: brent -""" - -from .._unit import Unit - -__all__ = ('Duplicator',) - - -class Duplicator(Unit, isabstract=True): - """ - Create a Duplicator object that takes in one inlet stream - and duplicates it to all outlet streams. - - Parameters - ---------- - ins : stream - Inlet stream. - outs : stream sequence - Duplicated outlet streams. - - Examples - -------- - Create a Duplicator object with an ID and any number of outlet streams: - - .. code-block:: python - - >>> from biosteam import settings, Stream, units - >>> settings.set_thermo(['Water', 'Ethanol']) - >>> feed = Stream('feed', Water=20, Ethanol=10, T=340) - >>> D1 = units.Duplicator('S1', ins=feed, outs=('out_a', 'out_b', 'out_c')) - >>> D1.simulate() - >>> D1.show() - Duplicator: D1 - ins... - [0] feed - phase: 'l', T: 340 K, P: 101325 Pa - flow (kmol/hr): Water 20 - Ethanol 10 - outs... - [0] out_a - phase: 'l', T: 340 K, P: 101325 Pa - flow (kmol/hr): Water 20 - Ethanol 10 - [1] out_b - phase: 'l', T: 340 K, P: 101325 Pa - flow (kmol/hr): Water 20 - Ethanol 10 - [2] out_c - phase: 'l', T: 340 K, P: 101325 Pa - flow (kmol/hr): Water 20 - Ethanol 10 - - """ - _N_outs = 2 - _outs_size_is_fixed = False - - def _load_stream_links(self): - feed, = self.ins - duplicated_outs = self.outs - for stream in duplicated_outs: - stream.link_with(feed) diff --git a/build/lib/biosteam/units/_enzyme_treatment.py b/build/lib/biosteam/units/_enzyme_treatment.py deleted file mode 100644 index b37b7d250..000000000 --- a/build/lib/biosteam/units/_enzyme_treatment.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 23 22:07:01 2018 - -@author: yoelr -""" -from ._tank import MixTank -from ._hx import HXutility - -__all__ = ('EnzymeTreatment',) - -class EnzymeTreatment(MixTank): - """Create an EnzymeTreatment unit that is cost as a MixTank with a heat exchanger.""" - _N_outs = 1 - - def __init__(self, ID='', ins=None, outs=(), thermo=None, *, - vessel_type='Conventional', tau=1.0, V_wf=0.9, - vessel_material='Stainless steel', T): - MixTank.__init__(self, ID, ins, outs, thermo, vessel_type=vessel_type, - tau=tau, V_wf=V_wf, vessel_material=vessel_material) - self.T = T #: Operating temperature - self.heat_exchanger = hx = HXutility(None, None, None, T=T) - self.heat_utilities = hx.heat_utilities - - def _run(self): - feed = self.ins[0] - out = self.outs[0] - out.mol[:] = self.mol_in - out.phase = feed.phase - out.P = feed.P - out.T = self.T - - def _design(self): - super()._design() - self.heat_exchanger.simulate_as_auxiliary_exchanger(self.Hnet, self.outs[0]) - - def _cost(self): - super()._cost() - self.purchase_costs['Heat exchanger'] = self.heat_exchanger.purchase_costs['Heat exchanger'] - \ No newline at end of file diff --git a/build/lib/biosteam/units/_fermentation.py b/build/lib/biosteam/units/_fermentation.py deleted file mode 100644 index 9426b018b..000000000 --- a/build/lib/biosteam/units/_fermentation.py +++ /dev/null @@ -1,320 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 23 22:45:47 2018 - -@author: yoelr -""" -import numpy as np -from ._hx import HXutility -from .. import Unit -from scipy.integrate import odeint -from .decorators import cost -from .design_tools import size_batch -from thermosteam.reaction import Reaction - -__all__ = ('Fermentation',) - -@cost('Reactor volume', 'Cleaning in place', CE=521.9, - cost=421e3, S=3785, n=0.6, BM=1.8, N='N') -@cost('Reactor volume', 'Agitators', CE=521.9, cost=52500, - S=3785, n=0.5, kW=22.371, BM=1.5, N='N') -@cost('Reactor volume', 'Reactors', CE=521.9, cost=844000, - S=3785, n=0.5, BM=1.5, N='N') -class Fermentation(Unit): - """ - Create a Fermentation object which models large-scale batch fermentation - for the production of 1st generation ethanol using yeast - [1]_ [2]_ [3]_ [4]_. A compound with CAS 'Yeast' must be present. - Only sucrose and glucose are taken into account for conversion. - Conversion is based on reaction time, `tau`. Cleaning and unloading time, - `tau_0`, fraction of working volume, `V_wf`, and number of reactors, - `N_reactors`, are attributes that can be changed. Cost of a reactor - is based on the NREL batch fermentation tank cost assuming volumetric - scaling with a 6/10th exponent [5]_. - - Parameters - ---------- - ins : streams - Inlet fluids to be mixed into the fermentor. - outs : stream sequence - * [0] Vent - * [1] Effluent - tau : float - Reaction time. - efficiency=0.9 : float, optional - User enforced efficiency. - iskinetic=False: bool, optional - If True, `Fermenation.kinetic_model` will be used. - N : int - Number of batch reactors - T=305.15 : float - Temperature of reactor [K]. - - Examples - -------- - Simulate a Fermentation object which models batch fermentation for the - production of 1st generation ethanol using yeast. - - >>> from biorefineries.lipidcane.chemicals import ethanol_chemicals - >>> from biosteam.units import Fermentation - >>> from biosteam import Stream, settings - >>> settings.set_thermo(ethanol_chemicals) - >>> feed = Stream('feed', - ... Water=1.20e+05, - ... Glucose=1.89e+03, - ... Sucrose=2.14e+04, - ... DryYeast=1.03e+04, - ... units='kg/hr', - ... T=32+273.15) - >>> F1 = Fermentation('F1', - ... ins=feed, outs=('CO2', 'product'), - ... tau=8, efficiency=0.90, N=8) - >>> F1.simulate() - >>> F1.show() - Fermentation: F1 - ins... - [0] feed - phase: 'l', T: 305.15 K, P: 101325 Pa - flow (kmol/hr): Water 6.66e+03 - Glucose 10.5 - Sucrose 62.5 - DryYeast 1.03e+04 - [1] missing stream - outs... - [0] CO2 - phase: 'g', T: 305.15 K, P: 101325 Pa - flow (kmol/hr): Water 2.5 - CO2 244 - Ethanol 0.582 - [1] product - phase: 'l', T: 305.15 K, P: 101325 Pa - flow (kmol/hr): Water 6.6e+03 - Ethanol 243 - Glucose 13.6 - DryYeast 1.03e+04 - >>> F1.results() - Fermentation Units F1 - Power Rate kW 11.7 - Cost USD/hr 0.912 - Chilled water Duty kJ/hr -2.91e+06 - Flow kmol/hr 1.95e+03 - Cost USD/hr 14.5 - Design Reactor volume m3 247 - Batch time hr 12.6 - Loading time hr 1.57 - Cleaning and unloading time hr 3 - Working volume fraction 0.9 - Number of reactors 8 - Purchase cost Coolers USD 3.7e+04 - Reactors USD 1.87e+06 - Agitators USD 1.17e+05 - Cleaning in place USD 7.12e+05 - Total purchase cost USD 2.74e+06 - Utility cost USD/hr 15.4 - - References - ---------- - .. [1] Oliveira, Samuel C., et al. "Discrimination between ethanol inhibition models in a continuous alcoholic fermentation process using flocculating yeast." Applied biochemistry and biotechnology 74.3 (1998): 161-172. - - .. [2] Oliveira, Samuel C., et al. "Continuous ethanol fermentation in a tower reactor with flocculating yeast recycle: scale-up effects on process performance, kinetic parameters and model predictions." Bioprocess Engineering 20.6 (1999): 525-530. - - .. [3] Oliveira, Samuel C., et al. "Mathematical modeling of a continuous alcoholic fermentation process in a two-stage tower reactor cascade with flocculating yeast recycle." Bioprocess and biosystems engineering 38.3 (2015): 469-479. - - .. [4] Oliveira, Samuel C., et al. "Kinetic Modeling of 1‐G Ethanol Fermentations." Fermentation Processes. InTech, 2017. - - .. [5] D. Humbird, R. Davis, L. Tao, C. Kinchin, D. Hsu, and A. Aden National. Renewable Energy Laboratory Golden, Colorado. P. Schoen, J. Lukas, B. Olthof, M. Worley, D. Sexton, and D. Dudgeon. Harris Group Inc. Seattle, Washington and Atlanta, Georgia. Process Design and Economics for Biochemical Conversion of Lignocellulosic Biomass to Ethanol Dilute-Acid Pretreatment and Enzymatic Hydrolysis of Corn Stover. May 2011. Technical Report NREL/TP-5100-47764 - - """ - _units = {'Reactor volume': 'm3', - 'Cycle time': 'hr', - 'Batch time': 'hr', - 'Loading time': 'hr', - 'Total dead time': 'hr'} - _N_ins = _N_outs = 2 - _N_heat_utilities = 1 - _has_power_utility = True - line = 'Fermentation' - - #: [bool] If True, number of reactors (N) is chosen as to minimize installation cost in every simulation. Otherwise, N remains constant. - autoselect_N = False - - #: [float] Cleaning and unloading time (hr) - tau_0 = 3 - - #: [float] Fraction of filled tank to total tank volume - V_wf = 0.9 - - #: tuple[float] Kinetic parameters for the kinetic model. Default constants are fitted for Oliveria's model (mu_m1, mu_m2, Ks1, Ks2, Pm1, Pm2, Xm, Y_PS, a) - kinetic_constants = (0.31, # mu_m1 - 1.01, # mu_m2 - 1.88, # Ks1 - 2.81, # Ks2 - 82.8, # Pm1 - 108.2, # Pm2 - 113.4, # Xm - 0.45, # Y_PS - 0.18) # a - - def _get_design_info(self): - return (('Cleaning and unloading time', self.tau_0, 'hr'), - ('Working volume fraction', self.V_wf, ''), - ('Number of reactors', self.N, '')) - - def __init__(self, ID='', ins=None, outs=(), thermo=None, *, - tau, N, efficiency=0.9, iskinetic=False, T=305.15): - Unit.__init__(self, ID, ins, outs, thermo) - self.hydrolysis = Reaction('Sucrose + Water -> 2Glucose', 'Sucrose', 1.00) - self.fermentation = Reaction('Glucose -> 2Ethanol + 2CO2', 'Glucose', efficiency) - self.iskinetic = iskinetic - self.efficiency = efficiency - self.tau = tau - self.N = N - self.T = T - self.cooler = HXutility(None) - - def _setup(self): - vent, effluent = self.outs - vent.phase = 'g' - self.cooler._outs[0].T = effluent.T = vent.T = self.T - - def _calc_efficiency(self, feed, tau): - # Get initial concentrations - y, e, s, w = feed.indices(['Yeast', - '64-17-5', - '492-61-5', - '7732-18-5']) - mass = feed.mass - F_vol = feed.F_vol - concentration_in = mass/F_vol - X0, P0, S0 = (concentration_in[i] for i in (y, e, s)) - - # Integrate to get final concentration - t = np.linspace(0, tau, 1000) - C_t = odeint(self.kinetic_model, (X0, P0, S0), t, - args=self.kinetic_constants) - # Cache data - self._X = C_t[:, 0] - self._P = C_t[:, 1] - self._S = S = C_t[:, 2] - - # Calculate efficiency - Sf = S[-1] - Sf = Sf if Sf > 0 else 0 - Y_PS = self.kinetic_constants[-2] - eff = (S0 - Sf)/S0 * Y_PS/0.511 - return eff - - @staticmethod - def kinetic_model(z, t, *kinetic_constants): - """ - Return change of yeast, ethanol, and substrate concentration in kg/m3. - - Parameters - ---------- - z : Iterable with (X, E, S) [-]: - * X: Yeast concentration (kg/m3) - * P: Ethanol concentration (kg/m3) - * S: Substrate concentration (kg/m3) - - t : float - Time point - - *kinetic_constants - * mu_m1: Maximum specific growth rate (1/hr) - * mu_m2: Maximum specific ethanol production rate (g-product/g-cell-hr) - * Ks1: Sugar saturation constant for growth (g/L) - * Ks2: Sugar saturation constant for product (g/L) - * Pm1: Maximum product concentration at zero growth [mu_m1=0] (g/L) - * Pm2: Maximum product concentration [mu_m2=0] (g/L) - * Xm: Maximum cell concentration [mu_m1=0] (g/L) - * Y_PS: Ethanol yield based on sugar consumed - * a: Toxic power - - """ - mu_m1, mu_m2, Ks1, Ks2, Pm1, Pm2, Xm, Y_PS, a = kinetic_constants - - # Current yeast, ethanol, and glucose concentration (kg/m3) - X, P, S = z - - # Compute coefficients - mu_X = mu_m1 * (S/(Ks1 + S)) * (1 - P/Pm1)**a*((1-X/Xm)) - mu_P = mu_m2 * (S/(Ks2 + S)) * (1 - P/Pm2) - mu_S = mu_P/0.45 - - # Compute derivatives - dXdt = mu_X * X - dPdt = (mu_P * X) - dSdt = - mu_S * X - return (dXdt, dPdt, dSdt) - - @property - def N(self): - """[int] Number of reactors""" - return self._N - @N.setter - def N(self, N): - if N <= 1: - raise ValueError(f"number of reactors must be greater than 1, value {N} is infeasible") - self._N = N - - @property - def efficiency(self): - return self.fermentation.X - @efficiency.setter - def efficiency(self, efficiency): - self.fermentation.X = efficiency - - @property - def tau(self): - return self._tau - @tau.setter - def tau(self, tau): - self._tau = tau - - def _run(self): - vent, effluent = self.outs - effluent.mix_from(self.ins) - effluent_mol = effluent.mol - self.hydrolysis(effluent_mol) - if self.iskinetic: - self.fermentation.X = self._calc_efficiency(effluent, self._tau) - self.fermentation(effluent_mol) - vent.receive_vent(effluent) - - @property - def N_at_minimum_capital_cost(self): - cost_old = np.inf - self.autoselect_N = False - self._N, N = 2, self._N - cost_new = self.purchase_cost - self._summary() - while cost_new < cost_old: - self._N += 1 - self._summary() - cost_old = cost_new - cost_new = self.purchase_cost - self._N, N = N, self._N - self.autoselect_N = True - return N - 1 - - def _design(self): - effluent = self.outs[1] - v_0 = effluent.F_vol - tau = self._tau - tau_0 = self.tau_0 - Design = self.design_results - if self.autoselect_N: - self._N = self.N_at_minimum_capital_cost - N = self._N - Design.update(size_batch(v_0, tau, tau_0, N, self.V_wf)) - hx_effluent = effluent.copy() - hx_effluent.mol[:] /= N - cooler = self.cooler - cooler.simulate_as_auxiliary_exchanger(self.Hnet/N, hx_effluent) - hu_fermentation, = self.heat_utilities - hu_cooler, = cooler.heat_utilities - hu_fermentation.copy_like(hu_cooler) - hu_fermentation.scale(N) - self.purchase_costs['Coolers'] = self.cooler.purchase_costs['Heat exchanger'] * N - - \ No newline at end of file diff --git a/build/lib/biosteam/units/_flash.py b/build/lib/biosteam/units/_flash.py deleted file mode 100644 index d2eca46c9..000000000 --- a/build/lib/biosteam/units/_flash.py +++ /dev/null @@ -1,787 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 23 16:21:56 2018 - -@author: yoelr -""" -from .. import Unit, PowerUtility -from thermosteam import MultiStream -from math import pi, ceil -import numpy as np -from .design_tools import (compute_vacuum_system_power_and_cost, - HNATable, ceil_half_step, - compute_vertical_vessel_purchase_cost, - compute_horizontal_vessel_purchase_cost, - compute_vessel_weight_and_wall_thickness, - compute_Stokes_law_York_Demister_K_value, - pressure_vessel_material_factors, - material_densities_lb_per_ft3,) -from ._splitter import Splitter -from ._hx import HX, HXutility -from .._graphics import vertical_vessel_graphics -from ..utils import bounds_warning -from ._CAS import H2O_CAS - -exp = np.exp -ln = np.log - -# USDA Biodiesel: -#V = np.pi*D**2/4*Ht*0.3048**3 -#if not (0.1 < V < 70): -# raise DesignError(f"Volume is out of bounds for costing") -#lambda V, CE: CE*13*V**0.62 # V (m3) -__all__ = ('Flash', 'SplitFlash', 'RatioFlash') - - -# %% Flash - -class Flash(Unit): - """ - Create an equlibrium based flash drum with the option of having light - non-keys and heavy non-keys completly separate into their respective - phases. Design procedure is based on heuristics by Wayne D. Monnery & - William Y. Svrcek [1]_. Purchase costs are based on correlations by - Mulet et al. [2]_ [3]_ as compiled by Warren et. al. [4]_. - - Parameters - ---------- - ins : stream - Inlet fluid. - outs : stream sequence - * [0] Vapor product - * [1] Liquid product - P=None : float - Operating pressure [Pa]. - Q=None : float - Duty [kJ/hr]. - T=None : float - Operating temperature [K]. - V=None : float - Molar vapor fraction. - x=None : float - Molar composition of liquid (for binary mixtures). - y=None : float - Molar composition of vapor (for binary mixtures). - vessel_material : str, optional - Vessel construction material. Defaults to 'Carbon steel'. - vacuum_system_preference : 'Liquid-ring pump', 'Steam-jet ejector', or 'Dry-vacuum pump' - If a vacuum system is needed, it will choose one according to this - preference. Defaults to 'Liquid-ring pump'. - has_glycol_groups=False : bool - True if glycol groups are present in the mixture. - has_amine_groups=False : bool - True if amine groups are present in the mixture. - vessel_type='Default' : 'Horizontal', 'Vertical', or 'Default' - Vessel separation type. If 'Default', the vessel type will be chosen - according to heuristics. - holdup_time=15.0 : float - Time it takes to raise liquid to half full [min]. - surge_time=7.5 : float - Time it takes to reach from normal to maximum liquied level [min]. - has_mist_eliminator : bool - True if using a mist eliminator pad. - - Notes - ----- - You may only specify two of the following parameters: P, Q, T, V, x, and y. - Additionally, If x or y is specified, the other parameter must be either - P or T (e.g., x and V is invalid). - - Examples - -------- - >>> from biosteam.units import Flash - >>> from biosteam import Stream, settings - >>> settings.set_thermo(['Water', 'Glycerol']) - >>> feed = Stream('feed', Glycerol=300, Water=1000) - >>> bp = feed.bubble_point_at_P() # Feed at bubble point T - >>> feed.T = bp.T - >>> F1 = Flash('F1', - ... ins=feed, - ... outs=('vapor', 'crude_glycerin'), - ... P=101325, # Pa - ... T=410.15) # K - >>> F1.simulate() - >>> F1.show(T='degC', P='atm') - Flash: F1 - ins... - [0] feed - phase: 'l', T: 100.7 degC, P: 1 atm - flow (kmol/hr): Water 1e+03 - Glycerol 300 - outs... - [0] vapor - phase: 'g', T: 137 degC, P: 1 atm - flow (kmol/hr): Water 958 - Glycerol 2.32 - [1] crude_glycerin - phase: 'l', T: 137 degC, P: 1 atm - flow (kmol/hr): Water 42.4 - Glycerol 298 - >>> F1.results() - Flash Units F1 - Medium pressure steam Duty kJ/hr 5.05e+07 - Flow kmol/hr 1.39e+03 - Cost USD/hr 384 - Design Vessel type Vertical - Length ft 26.5 - Diameter ft 6.5 - Weight lb 9.38e+03 - Wall thickness in 0.375 - Material Carbon steel - Purchase cost Flash USD 6.24e+04 - Heat exchanger USD 4.04e+04 - Total purchase cost USD 1.03e+05 - Utility cost USD/hr 384 - - - References - ---------- - .. [1] "Design Two-Phase Separators Within the Right Limits", Chemical - Engineering Progress Oct, 1993. - - .. [2] Mulet, A., A. B. Corripio, and L. B. Evans, “Estimate Costs of - Pressure Vessels via Correlations,” Chem. Eng., 88(20), 145–150 (1981a). - - .. [3] Mulet, A., A.B. Corripio, and L.B.Evans, “Estimate Costs of - Distillation and Absorption Towers via Correlations,” Chem. Eng., 88(26), 77–82 (1981b). - - .. [4] Seider, W. D., Lewin, D. R., Seader, J. D., Widagdo, S., Gani, - R., & Ng, M. K. (2017). Product and Process Design Principles. Wiley. - Cost Accounting and Capital Cost Estimation (Chapter 16) - - """ - _units = {'Vertical vessel weight': 'lb', - 'Horizontal vessel weight': 'lb', - 'Length': 'ft', - 'Diameter': 'ft', - 'Weight': 'lb', - 'Wall thickness': 'in'} - _graphics = vertical_vessel_graphics - _N_outs = 2 - _N_heat_utilities = 0 - - # Bare module factor - BM_horizontal = 3.05 - BM_vertical = 4.16 - @property - def BM(self): - vessel_type = self.vessel_type - if vessel_type == 'Vertical': - return self.BM_vertical - elif vessel_type == 'Horizontal': - return self.BM_horizontal - elif vessel_type == 'Default': - return self.BM_vertical if self._isVertical else self.BM_horizontal - else: - raise AttributeError('vessel_type not defined') - - _bounds = {'Vertical vessel weight': (4200, 1e6), - 'Horizontal vessel weight': (1e3, 9.2e5), - 'Diameter': (3, 21), - 'Vertical vessel length': (12, 40)} - - @property - def vessel_material(self): - """Vessel construction material.""" - return self._vessel_material - @vessel_material.setter - def vessel_material(self, material): - try: self._F_M = pressure_vessel_material_factors[material] - except KeyError: - raise ValueError(f"no material factor available for '{material}'; " - "only the following materials are available: " - f"{', '.join(pressure_vessel_material_factors)}") - self._vessel_material = material - - def __init__(self, ID='', ins=None, outs=(), thermo=None, *, - V=None, T=None, Q=None, P=None, y=None, x=None, - vessel_material='Carbon steel', - vacuum_system_preference='Liquid-ring pump', - has_glycol_groups=False, - has_amine_groups=False, - vessel_type='Default', - holdup_time=15, - surge_time=7.5, - has_mist_eliminator=False): - Unit.__init__(self, ID, ins, outs, thermo) - self._multistream = MultiStream(None) - self.heat_exchanger = None - - #: Enforced molar vapor fraction - self.V = V - - #: Enforced operating temperature (K) - self.T = T - - #: [array_like] Molar composition of vapor (for binary mixture) - self.y = y - - #: [array_like] Molar composition of liquid (for binary mixture) - self.x = x - - #: Duty (kJ/hr) - self.Q = Q - - #: Operating pressure (Pa) - self.P = P - - #: [str] Vessel construction material - self.vessel_material = vessel_material - - #: [str] If a vacuum system is needed, it will choose one according to this preference. - self.vacuum_system_preference = vacuum_system_preference - - #: [bool] True if glycol groups are present in the mixture - self.has_glycol_groups = has_glycol_groups - - #: [bool] True if amine groups are present in the mixture - self.has_amine_groups = has_amine_groups - - #: [str] 'Horizontal', 'Vertical', or 'Default' - self.vessel_type = vessel_type - - #: [float] Time it takes to raise liquid to half full (min) - self.holdup_time = holdup_time - - #: [float] Time it takes to reach from normal to maximum liquied level (min) - self.surge_time = surge_time - - #: [bool] True if using a mist eliminator pad - self.has_mist_eliminator = has_mist_eliminator - - @property - def P(self): - """Operating pressure (Pa).""" - return self._P - @P.setter - def P(self, P): - if P < 101325 and not self.power_utility: - self.power_utility = PowerUtility() - self._P = P - - @property - def Q(self): - """Enforced duty (kJ/hr).""" - return self._Q - @Q.setter - def Q(self, Q): - if Q == 0: - self.heat_exchanger = None - elif not self.heat_exchanger: - self.heat_exchanger = he = HXutility(None, outs=None) - self.heat_utilities = he.heat_utilities - he._ins = self._ins - he._outs[0] = self._multistream - self._Q = Q - - def _run(self): - # Unpack - vap, liq = self.outs - feed = self.ins[0] - - # Vapor Liquid Equilibrium - ms = self._multistream - ms.empty() - ms.imol['l'] = feed.mol - ms.T = feed.T - Q = self.Q - H = feed.H + Q if Q is not None else None - ms.vle(P=self.P, H=H, T=self.T, V=self.V, x=self.x, y=self.y) - - # Set Values - vap.phase = 'g' - liq.phase = 'l' - vap.mol[:] = ms.imol['g'] - liq.mol[:] = ms.imol['l'] - vap.T = liq.T = ms.T - vap.P = liq.P = ms.P - - def _design(self): - # Set horizontal or vertical vessel - vessel_type = self.vessel_type - if vessel_type == 'Default': - vap, liq = self.outs - isVertical = vap.F_mass/liq.F_mass > 0.2 - elif vessel_type == 'Vertical': - isVertical = True - elif vessel_type == 'Horizontal': - isVertical = False - else: - raise ValueError( f"vessel_type must be either 'Default', 'Horizontal', 'Vertical', not '{self.vessel_type}'") - self._isVertical = isVertical - - # Run vertical or horizontal design - if isVertical: self._design_vertical_vessel() - else: self._design_horizontal_vessel() - if self.heat_exchanger: self.heat_exchanger._design() - self.design_results['Material'] = self._vessel_material - - def _cost(self): - Design = self.design_results - W = Design['Weight'] - D = Design['Diameter'] - L = Design['Length'] - F_M = self._F_M - if self._isVertical: - Cp = compute_vertical_vessel_purchase_cost(W, D, L, F_M) - else: - Cp = compute_horizontal_vessel_purchase_cost(W, D, F_M) - self.purchase_costs['Flash'] = Cp - if self.heat_exchanger: - hx = self.heat_exchanger - hx._cost() - self.purchase_costs.update(hx.purchase_costs) - self._cost_vacuum() - - def _cost_vacuum(self): - P = self.P - if not P or P > 101320: return - - Design = self.design_results - volume = 0.02832 * np.pi * Design['Length'] * (Design['Diameter']/2)**2 - - # If vapor is condensed, assume vacuum system is after condenser - vapor = self.outs[0] - hx = vapor.sink - if isinstance(hx, HX): - index = hx.ins.index(vapor) - stream = hx.outs[index] - if isinstance(stream, MultiStream): - vapor = stream['g'] - F_mass = vapor.F_mass - F_vol = vapor.F_vol - else: - if stream.phase == 'g': - F_mass = stream.F_mass - F_vol = stream.F_vol - else: - F_mass = 0 - F_vol = 0 - else: - F_mass = vapor.F_mass - F_vol = vapor.F_vol - - power, cost = compute_vacuum_system_power_and_cost( - F_mass, F_vol, P, volume, self.vacuum_system_preference) - self.purchase_costs['Liquid-ring pump'] = cost - self.power_utility(power) - - def _design_parameters(self): - # Retrieve run_args and properties - vap, liq = self._outs - rhov = vap.get_property('rho', 'lb/ft3') - rhol = liq.get_property('rho', 'lb/ft3') - P = liq.get_property('P', 'psi') # Pressure (psi) - - vessel_type = self.vessel_type - Th = self.holdup_time - Ts = self.surge_time - has_mist_eliminator = self.has_mist_eliminator - - # Calculate the volumetric flowrate - Qv = vap.get_total_flow('ft^3 / s') - Qll = liq.get_total_flow('ft^3 / min') - - # Calculate Ut and set Uv - K = compute_Stokes_law_York_Demister_K_value(P) - - # Adjust K value - if not has_mist_eliminator and vessel_type == 'Vertical': K /= 2 - - # Adjust for amine or glycol groups: - if self.has_glycol_groups: K *= 0.6 - elif self.has_amine_groups: K *= 0.8 - - Ut = K*((rhol - rhov) / rhov)**0.5 - Uv = 0.75*Ut - - # Calculate Holdup and Surge volume - Vh = Th*Qll - Vs = Ts*Qll - return rhov, rhol, P, Th, Ts, has_mist_eliminator, Qv, Qll, Ut, Uv, Vh, Vs - - def _design_vertical_vessel(self): - rhov, rhol, P, Th, Ts, has_mist_eliminator, Qv, Qll, Ut, Uv, Vh, Vs = self._design_parameters() - - # Calculate internal diameter, Dvd - Dvd = (4.0*Qv/(pi*Uv))**0.5 - if has_mist_eliminator: - D = ceil_half_step(Dvd + 0.4) - else: - D = ceil_half_step(Dvd) - - # Obtaining low liquid level height, Hlll - Hlll = 0.5 - if P < 300: - Hlll = 1.25 - - # Calculate the height from Hlll to Normal liq level, Hnll - Hh = Vh/(pi/4.0*Dvd**2) - if Hh < 1.0: Hh = 1.0 - - # Calculate the height from Hnll to High liq level, Hhll - Hs = Vs/(pi/4.0*Dvd**2) - if Hs < 0.5: Hs = 0.5 - - # Calculate dN - Qm = Qll + Qv - lamda = Qll/Qm - rhoM = rhol*lamda + rhov*(1-lamda) - dN = (4*Qm/(pi*60.0/(rhoM**0.5)))**0.5 - dN = ceil_half_step(dN) - - # Calculate Hlin, assume with inlet diverter - Hlin = 1.0 + dN - - # Calculate the vapor disengagement height - Hv = 0.5*Dvd - Hv2 = (2.0 if has_mist_eliminator else 3.0) + dN/2.0 - if Hv2 < Hv: Hv = Hv2 - Hv = Hv - - # Calculate total height, Ht - Hme = 1.5 if has_mist_eliminator else 0.0 - Ht = Hlll + Hh + Hs + Hlin + Hv + Hme - Ht = ceil_half_step(Ht) - - # Calculate Vessel weight and wall thickness - rho_M = material_densities_lb_per_ft3[self._vessel_material] - VW, VWT = compute_vessel_weight_and_wall_thickness(P, D, Ht, rho_M) - - # Find maximum and normal liquid level - # Hhll = Hs + Hh + Hlll - # Hnll = Hh + Hlll - - Design = self.design_results - bounds_warning(self, 'Vertical vessel weight', VW, 'lb', - self._bounds['Vertical vessel weight'], - 'cost') - bounds_warning(self, 'Vertical vessel length', Ht, 'ft', - self._bounds['Vertical vessel length'], - 'cost') - Design['Vessel type'] = 'Vertical' - Design['Length'] = Ht # ft - Design['Diameter'] = D # ft - Design['Weight'] = VW # lb - Design['Wall thickness'] = VWT # in - - def _design_horizontal_vessel(self): - rhov, rhol, P, Th, Ts, has_mist_eliminator, Qv, Qll, Ut, Uv, Vh, Vs = self._design_parameters() - - # Initialize LD - if P > 0 and P <= 264.7: - LD = 1.5/250.0*(P-14.7)+1.5 - elif P > 264.7 and P <= 514.7: - LD = 1.0/250.0*(P-14.7)+2.0 - elif P > 514.7: - LD = 5.0 - - D = (4.0*(Vh+Vs)/(0.6*pi*LD))**(1.0/3.0) - if D <= 4.0: - D = 4.0 - else: - D = ceil_half_step(D) - - for outerIter in range(50): - At = pi*(D**2)/4.0 # Total area - - # Calculate Lower Liquid Area - Hlll = round(0.5*D + 7.0) - Hlll = Hlll/12.0 # D is in ft but Hlll is in inches - X = Hlll/D - Y = HNATable(1, X) - Alll = Y*At - - # Calculate the Vapor disengagement area, Av - Hv = 0.2*D - if has_mist_eliminator and Hv <= 2.0: Hv = 2.0 - elif Hv <= 1.0: Hv = 1.0 - else: Hv = ceil_half_step(Hv) - Av = HNATable(1, Hv/D)*At - - # Calculate minimum length for surge and holdup - L = (Vh + Vs)/(At - Av - Alll) - # Calculate liquid dropout - Phi = Hv/Uv - # Calculate actual vapor velocity - Uva = Qv/Av - # Calculate minimum length for vapor disengagement - Lmin = Uva*Phi - Li = L - - for innerIter in range(50): - if L < 0.8*Lmin: Hv += 0.5 - elif L > 1.2*Lmin: - if has_mist_eliminator and Hv <= 2.0: Hv = 2.0 - elif not has_mist_eliminator and Hv <= 1.0: Hv = 1.0 - else: Hv -= 0.5 - else: break - Av = HNATable(1, Hv/D)*At - Alll = HNATable(1, Hlll/D)*At - Li = (Vh + Vs)/(At - Av - Alll) - Phi = Hv/Uv - Uva = Qv/Av - Lmin = Uva*Phi - - L = Li - LD = L/D - # Check LD - if LD < 1.2: - if D <= 4.0: break - else: D -= 0.5 - - if LD > 7.2: - D += 0.5 - else: break - - # Recalculate LD so it lies between 1.5 - 6.0 - while True: - LD = L / D - if (LD < 1.5) and D <= 4.0: L += 0.5 - elif LD < 1.5: D -= 0.5 - elif (LD > 6.0): D += 0.5 - else: break - - # Calculate vessel weight and wall thickness - rho_M = material_densities_lb_per_ft3[self._vessel_material] - VW, VWT = compute_vessel_weight_and_wall_thickness(P, D, L, rho_M) - - # # To check minimum Hv value - # if int(has_mist_eliminator) == 1 and Hv <= 2.0: - # Hv = 2.0 - # if int(has_mist_eliminator) == 0 and Hv <= 1.0: - # Hv = 1.0 - - # Calculate normal liquid level and High liquid level - # Hhll = D - Hv - # if (Hhll < 0.0): - # Hhll = 0.0 - # Anll = Alll + Vh/L - # X = Anll/At - # Y = HNATable(2, X) - # Hnll = Y*D - - Design = self.design_results - bounds_warning(self, 'Horizontal vessel weight', VW, 'lb', - self._bounds['Horizontal vessel weight'], 'cost') - Design['Vessel type'] = 'Horizontal' - Design['Length'] = L # ft - Design['Diameter'] = D # ft - Design['Weight'] = VW # lb - Design['Wall thickness'] = VWT # in - - def _end_decorated_cost_(self): - if self.heat_utilities: self.heat_utilities[0](self.Hnet, self.T) - - -# %% Special - -class SplitFlash(Flash): - line = 'Flash' - - def __init__(self, ID='', ins=None, outs=(), *, split, - order=None, T=None, P=None, Q=None, - vessel_material='Carbon steel', - vacuum_system_preference='Liquid-ring pump', - has_glycol_groups=False, - has_amine_groups=False, - vessel_type='Default', - holdup_time=15, - surge_time=7.5, - has_mist_eliminator=False): - Splitter.__init__(self, ID, ins, outs, split=split, order=order) - self._multistream = MultiStream(None) - - #: [HXutility] Heat exchanger if needed. - self.heat_exchanger = None - self.T = T #: Operating temperature (K) - self.P = P #: Operating pressure (Pa) - self.Q = Q #: Duty (kJ/hr) - - #: [str] Vessel construction material - self.vessel_material = vessel_material - - #: [str] If a vacuum system is needed, it will choose one according to this preference. - self.vacuum_system_preference = vacuum_system_preference - - #: [bool] True if glycol groups are present in the mixture - self.has_glycol_groups = has_glycol_groups - - #: [bool] True if amine groups are present in the mixture - self.has_amine_groups = has_amine_groups - - #: [str] 'Horizontal', 'Vertical', or 'Default' - self.vessel_type = vessel_type - - #: [float] Time it takes to raise liquid to half full (min) - self.holdup_time = holdup_time - - #: [float] Time it takes to reach from normal to maximum liquied level (min) - self.surge_time = surge_time - - #: [bool] True if using a mist eliminator pad - self.has_mist_eliminator = has_mist_eliminator - - split = Splitter.split - V = None - - def _run(self): - top, bot = self.outs - feed, = self.ins - feed_mol = feed.mol - top.mol[:] = top_mol = feed_mol * self.split - bot.mol[:] = feed_mol - top_mol - top.phase = 'g' - bot.phase = 'l' - bot.T = top.T = self.T - bot.P = top.P = self.P - - def _design(self): - if self.heat_exchanger: - self.heat_exchanger.outs[0] = ms = self._multistream - ms.mix_from(self.outs) - super()._design() - - -class RatioFlash(Flash): - _N_heat_utilities = 1 - - def __init__(self, ID='', ins=None, outs=(), *, - K_chemicals, Ks, top_solvents=(), top_split=(), - bot_solvents=(), bot_split=()): - Unit.__init__(self, ID, ins, outs) - self.K_chemicals = K_chemicals - self.Ks = Ks - self.top_solvents = top_solvents - self.top_split = top_split - self.bot_solvents = bot_solvents - self.bot_split = bot_split - - def _run(self): - feed = self.ins[0] - top, bot = self.outs - indices = self.chemicals.get_index - K_index = indices(self.K_chemicals) - top_index = indices(self.top_solvents) - bot_index = indices(self.bot_solvents) - top_mol = top.mol; bot_mol = bot.mol; feed_mol = feed.mol - top_mol[top_index] = feed_mol[top_index]*self.top_split - bot_mol[top_index] = feed_mol[top_index]-top_mol[top_index] - bot_mol[bot_index] = feed_mol[bot_index]*self.bot_split - top_mol[bot_index] = feed_mol[bot_index]-bot_mol[bot_index] - topnet = top_mol[top_index].sum() - botnet = bot_mol[bot_index].sum() - molnet = topnet+botnet - top_mol[K_index] = self.Ks * topnet * feed_mol[K_index] / molnet # solvent * mol ratio - bot_mol[K_index] = feed_mol[K_index] - top_mol[K_index] - top.T, top.P = feed.T, feed.P - bot.T, bot.P = feed.T, feed.P - - -# %% Single Component - -class Evaporator_PQ(Unit): - _N_ins = 2 - _N_outs = 3 - @property - def P(self): - return self._P - @P.setter - def P(self, P): - water = getattr(self.chemicals, H2O_CAS) - self._T = T = water.Tsat(P) - self._Hvap = water.Hvap(T) - self._P = P - @property - def T(self): - return self._T - @T.setter - def T(self, T): - water = getattr(self.chemicals, H2O_CAS) - self._P = water.Psat(T) - self._Hvap = water.Hvap(T) - self._T = T - @property - def V(self): - return self._V - - def __init__(self, ID='', ins=None, outs=(), *, Q=0, P=101325): - super().__init__(ID, ins, outs) - self.Q = Q - self.P = P - self._V = None - - def _run(self): - feed, utility_vapor = self.ins - vapor, liquid, utility_liquid = self.outs - - # Optional if Q also comes from condensing a side stream - Q = self.Q - if not utility_vapor.isempty(): - utility_liquid.copy_like(utility_vapor) - utility_liquid.phase = 'l' - Q += utility_vapor.Hvap - feed_H = feed.H - - # Set exit conditions - vapor.T = liquid.T = self.T - vapor.P = liquid.P = self.P - liquid.phase = 'l' - vapor.phase = 'g' - liquid.copy_flow(feed, IDs=H2O_CAS, exclude=True) - - # Energy balance to find vapor fraction - f = feed.imol[H2O_CAS] - H = feed_H + Q - liquid.H - if f: - V = H/(f * self._Hvap) - if V < 0: - V = 0 - elif V > 1: - V = 1 - else: - V = 0 - evaporated = f * V - vapor.imol[H2O_CAS] = evaporated - liquid.imol[H2O_CAS] = (1-V)*f - self._Q = Q - self._V = V - - -class Evaporator_PV(Unit): - _N_heat_utilities = 1 - - @property - def P(self): - return self._P - @P.setter - def P(self, P): - water = getattr(self.chemicals, H2O_CAS) - self._T = water.Tsat(P) - self._P = P - @property - def T(self): - return self._T - @T.setter - def T(self, T): - water = getattr(self.chemicals, H2O_CAS) - self._P = water.Psat(T) - self._T = T - - def __init__(self, ID='', ins=None, outs=(), *, V=0.5, P=101325): - super().__init__(ID, ins, outs) - self.V = V - self.P = P - - def _run(self): - feed = self.ins[0] - vapor, liquid = self.outs - vapor.T = liquid.T = self.T - H2O_index = self.chemicals.index(H2O_CAS) - water_mol = feed.mol[H2O_index] - vapor.mol[H2O_index] = self.V * water_mol - liquid_mol = liquid.mol - liquid_mol[:] = feed.mol - liquid_mol[H2O_index] = (1-self.V) * water_mol - - \ No newline at end of file diff --git a/build/lib/biosteam/units/_hx.py b/build/lib/biosteam/units/_hx.py deleted file mode 100644 index 8af124413..000000000 --- a/build/lib/biosteam/units/_hx.py +++ /dev/null @@ -1,704 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 23 14:38:34 2018 - -@author: yoelr -""" -from .. import Unit -from .._graphics import utility_heat_exchanger_graphics, process_heat_exchanger_graphics -from .design_tools.specification_factors import ( - shell_and_tube_material_factor_coefficients, - compute_shell_and_tube_material_factor) -from thermosteam import Stream -from fluids import nearest_pipe -from .design_tools import heat_transfer as ht -import numpy as np -import biosteam as bst -from math import exp, log as ln - -__all__ = ('HXutility', 'HXprocess') - -# Lenght factor -x = np.array((8, 13, 16, 20)) -y = np.array((1.25, 1.12,1.05,1)) -p2 = np.polyfit(x, y, 2) - -# %% Purchase price - -def compute_floating_head_purchase_price(A, CE): - return exp(12.0310 - 0.8709*ln(A) + 0.09005 * ln(A)**2)*CE/567 - -def compute_fixed_head_purchase_price(A, CE): - return exp(11.4185 - 0.9228*ln(A) + 0.09861 * ln(A)**2)*CE/567 - -def compute_u_tube_purchase_price(A, CE): - return exp(11.5510 - 0.9186*ln(A) + 0.09790 * ln(A)**2)*CE/567 - -def compute_kettle_vaporizer_purchase_price(A, CE): - return exp(12.3310 - 0.8709*ln(A) + 0.09005 * ln(A)**2)*CE/567 - -def compute_double_pipe_purchase_price(A, CE): - return exp( 7.2718 + 0.16*ln(A))*CE/567 - -# Purchase price -Cb_dict = {'Floating head': compute_floating_head_purchase_price, - 'Fixed head': compute_fixed_head_purchase_price, - 'U tube': compute_u_tube_purchase_price, - 'Kettle vaporizer': compute_kettle_vaporizer_purchase_price, - 'Double pipe': compute_double_pipe_purchase_price} - -# %% Classes - -class HX(Unit, isabstract=True): - """Abstract class for counter current heat exchanger. - - **Abstract methods** - - get_streams() - Should return two inlet streams and two outlet streams that exchange heat. - - """ - line = 'Heat Exchanger' - _units = {'Area': 'ft^2', - 'Overall heat transfer coefficient': 'kW/m^2/K', - 'Tube side pressure drop': 'psi', - 'Shell side pressure drop': 'psi', - 'Operating pressure': 'psi', - 'Total tube length': 'ft'} - _N_ins = 1 - _N_outs = 1 - _N_heat_utilities = 1 - BM_douple_pipe = 1.80 - BM_shell_and_tube = 3.17 - - @property - def BM(self): - if self._shell_and_tube_type == "Double pipe": - return self.BM_douple_pipe - else: - return self.BM_shell_and_tube - - @property - def material(self): - """Default 'Carbon steel/carbon steel'""" - return self.material - @material.setter - def material(self, material): - try: - self._F_Mab = shell_and_tube_material_factor_coefficients[material] - except KeyError: - raise ValueError("material must be one of the following: " - f"{', '.join(shell_and_tube_material_factor_coefficients)}") - self._material = material - - @property - def shell_and_tube_type(self): - """[str] Shell and tube type. Purchase cost depends on this selection.""" - return self._shell_and_tube_type - @shell_and_tube_type.setter - def shell_and_tube_type(self, shell_and_tube_type): - try: - self._Cb_func = Cb_dict[shell_and_tube_type] - except KeyError: - raise ValueError("heat exchange type must be one of the following: " - f"{', '.join(Cb_dict)}") - self._shell_and_tube_type = shell_and_tube_type - - def _design(self): - ### Use LMTD correction factor method ### - Design = self.design_results - - # Get cold and hot inlet and outlet streams - ci, hi, co, ho = ht.order_streams(*self.get_streams()) - - # Get log mean temperature difference - Tci = ci.T - Thi = hi.T - Tco = co.T - Tho = ho.T - LMTD = ht.compute_LMTD(Thi, Tho, Tci, Tco) - - # Get correction factor - ft = self.ft - if not ft: - N_shells = self.N_shells - ft = ht.compute_Fahkeri_LMTD_correction_factor(Tci, Thi, Tco, Tho, N_shells) - - # Get duty (kW) - Q = self.Q / 3600 - - # Get overall heat transfer coefficient - U = self.U or ht.heuristic_overall_heat_transfer_coefficient(ci, hi, co, ho) - dP_tube, dP_shell = ht.heuristic_tubeside_and_shellside_pressure_drops(ci, hi, co, ho) - - # TODO: Complete design of heat exchanger to find L - # For now assume lenght is 20 ft - L = 20 - - # Design pressure - P = max((ci.P, hi.P)) - Design['Area'] = 10.763 * ht.compute_heat_transfer_area(abs(LMTD), U, abs(Q), ft) - Design['Overall heat transfer coefficient'] = U - Design['Fouling correction factor'] = ft - Design['Tube side pressure drop'] = dP_tube - Design['Shell side pressure drop'] = dP_shell - Design['Operating pressure'] = P * 14.7/101325 # psi - Design['Total tube length'] = L - - def _cost(self): - Design = self.design_results - A = Design['Area'] - L = Design['Total tube length'] - P = Design['Operating pressure'] - - if A < 150: # Double pipe - P = P/600 - F_p = 0.8510 + 0.1292*P + 0.0198*P**2 - # Assume outer pipe carbon steel, inner pipe stainless steel - F_m = 2 - A_min = 2.1718 - if A < A_min: - F_l = A/A_min - A = A_min - else: - F_l = 1 - self.shell_and_tube_type = 'Double pipe' - else: # Shell and tube - a, b = self._F_Mab - F_m = a + (A/100)**b - F_l = 1 if L > 20 else np.polyval(p2, L) - P = P/100 - F_p = 0.9803 + 0.018*P + 0.0017*P**2 - - C_b_func = self._Cb_func - C_b = C_b_func(A, bst.CE) - - # Free on board purchase prize - self.purchase_costs['Heat exchanger'] = F_p * F_l * F_m * C_b - - -class HXutility(HX): - """ - Create a heat exchanger that changes temperature of the - outlet stream using a heat utility. - - Parameters - ---------- - ins : stream - Inlet. - outs : stream - Outlet. - T=None : float - Temperature of outlet stream [K]. - V=None : float - Vapor fraction of outlet stream. - rigorous=False : bool - If true, calculate vapor liquid equilibrium - U=None : float, optional - Enforced overall heat transfer coefficent [kW/m^2/K]. - shell_and_tube_type : str, optional - Heat exchanger type. Defaults to "Floating head". - N_shells=2 : int - Number of shells. - ft=None : float - User imposed correction factor. - - Notes - ----- - Must specify either `T` or `V` when creating a HXutility object. - - Examples - -------- - Run heat exchanger by temperature: - - >>> from biosteam.units import HXutility - >>> from biosteam import Stream, settings - >>> settings.set_thermo(['Water', 'Ethanol']) - >>> feed = Stream('feed', Water=200, Ethanol=200) - >>> hx = HXutility('hx', ins=feed, outs='product', T=50+273.15, - ... rigorous=False) # Ignore VLE - >>> hx.simulate() - >>> hx.show() - HXutility: hx - ins... - [0] feed - phase: 'l', T: 298.15 K, P: 101325 Pa - flow (kmol/hr): Water 200 - Ethanol 200 - outs... - [0] product - phase: 'l', T: 323.15 K, P: 101325 Pa - flow (kmol/hr): Water 200 - Ethanol 200 - >>> hx.results() - Heat Exchanger Units hx - Low pressure steam Duty kJ/hr 1.01e+06 - Flow kmol/hr 26.1 - Cost USD/hr 6.21 - Design Area ft^2 320 - Overall heat transfer coefficient kW/m^2/K 0.5 - Fouling correction factor 1 - Tube side pressure drop psi 1.5 - Shell side pressure drop psi 5 - Operating pressure psi 50 - Total tube length ft 20 - Purchase cost Heat exchanger USD 2.19e+04 - Total purchase cost USD 2.19e+04 - Utility cost USD/hr 6.21 - - Run heat exchanger by vapor fraction: - - >>> feed = Stream('feed', Water=200, Ethanol=200) - >>> hx = HXutility('hx', ins=feed, outs='product', V=1, - ... rigorous=True) # Include VLE - >>> hx.simulate() - >>> hx.show() - HXutility: hx - ins... - [0] feed - phase: 'l', T: 298.15 K, P: 101325 Pa - flow (kmol/hr): Water 200 - Ethanol 200 - outs... - [0] product - phases: ('g', 'l'), T: 357.45 K, P: 101325 Pa - flow (kmol/hr): (g) Water 200 - Ethanol 200 - >>> hx.results() - Heat Exchanger Units hx - Low pressure steam Duty kJ/hr 2.07e+07 - Flow kmol/hr 532 - Cost USD/hr 126 - Design Area ft^2 297 - Overall heat transfer coefficient kW/m^2/K 1 - Fouling correction factor 1 - Tube side pressure drop psi 1.5 - Shell side pressure drop psi 1.5 - Operating pressure psi 50 - Total tube length ft 20 - Purchase cost Heat exchanger USD 2.16e+04 - Total purchase cost USD 2.16e+04 - Utility cost USD/hr 126 - - """ - line = 'Heat Exchanger' - _graphics = utility_heat_exchanger_graphics - - def __init__(self, ID='', ins=None, outs=(), thermo=None, *, - T=None, V=None, rigorous=False, U=None, - shell_and_tube_type="Floating head", - material="Carbon steel/carbon steel", - N_shells=2, - ft=None): - super().__init__(ID, ins, outs, thermo) - self.T = T #: Temperature of outlet stream (K). - self.V = V #: Vapor fraction of outlet stream. - - #: [bool] If true, calculate vapor liquid equilibrium - self.rigorous = rigorous - - #: [float] Enforced overall heat transfer coefficent (kW/m^2/K) - self.U = U - - #: [float] Total heat transfered. - self.Q = None - - #: Number of shells for LMTD correction factor method. - self.N_shells = N_shells - - #: User imposed correction factor. - self.ft = ft - - self.material = material - self.shell_and_tube_type = shell_and_tube_type - - def simulate_as_auxiliary_exchanger(self, duty, stream): - self.outs[0] = stream.proxy() - self.ins[0] = stream.proxy() - self.heat_utilities[0](duty, stream.T) - self.Q = duty - super()._design() - self._cost() - - def _run(self): - feed = self.ins[0] - s = self.outs[0] - s.copy_like(feed) - T = self.T - V = self.V - V_given = V is not None - if self.rigorous: - if T and V_given: - raise ValueError("may only define either temperature, 'T', or vapor fraction 'V', in a rigorous simulation") - if V_given: - s.vle(V=V, P=s.P) - else: - s.vle(T=T, P=s.P) - elif (T or V_given): - if V_given: - if V == 0: - s.phase = 'l' - elif V == 1: - s.phase = 'g' - else: - s.phases = ('g', 'l') - mol = s.mol - s.imol['g'] = vap_mol = V * mol - s.imol['l'] = mol - vap_mol - if T: - s.T = T - else: - raise ValueError("must define at least one of the following: 'T', 'V'") - - def get_streams(self): - """Return inlet and outlet streams. - - Returns - ------- - in_a : Stream - Inlet a. - in_b : Stream - Inlet b. - out_a : Stream - Outlet a. - out_b : Stream - Outlet b. - - """ - in_a = self.ins[0] - out_a = self.outs[0] - hu = self.heat_utilities[0] - in_b = hu.inlet_utility_stream - out_b = hu.outlet_utility_stream - return in_a, in_b, out_a, out_b - - def _design(self, duty=None): - # Set duty and run heat utility - if duty is None: - duty = self.H_out - self.H_in - self.heat_utilities[0](duty, self.ins[0].T, self.outs[0].T) - self.Q = duty - super()._design() - - -class HXprocess(HX): - """ - Counter current heat exchanger for process fluids. Condenses/boils latent fluid until sensible fluid reaches pinch temperature. - - Parameters - ---------- - ins : stream sequence - * [0] Inlet process fluid a - * [1] Inlet process fluid b - outs : stream sequence - * [0] Outlet process fluid a - * [1] Outlet process fluid b - U=None : float, optional - Enforced overall heat transfer coefficent [kW/m^2/K]. - fluid_type : None, 'ss', 'll', 'ls' - * **None:** Rigorously transfers heat until pinch temperature (not implemented yet). - * **'ss':** Sensible-sensible fluids. Heat is exchanged until the pinch temperature is reached. - * **'ll':** Latent-latent fluids. Heat is exchanged until one fluid completely changes phase. - * **'ls':** Latent-sensible fluids. Heat is exchanged until either the pinch temperature is reached or the latent fluid completely changes phase. - shell_and_tube_type : str, optional - Heat exchanger type. Defaults to 'Floating head'. - N_shells=2 : int - Number of shells. - ft=None : float - User imposed correction factor. - - Examples - -------- - Sensible fluids case: - - >>> from biosteam.units import HXprocess - >>> from biosteam import Stream, settings - >>> settings.set_thermo(['Water', 'Ethanol']) - >>> in_a = Stream('in_a', Water=200, T=350) - >>> in_b = Stream('in_b', Ethanol=200) - >>> hx = HXprocess('hx', ins=(in_a, in_b), outs=('out_a', 'out_b'), - ... fluid_type='ss') - >>> hx.simulate() - >>> hx.show() - HXprocess: hx - ins... - [0] in_a - phase: 'l', T: 350 K, P: 101325 Pa - flow (kmol/hr): Water 200 - [1] in_b - phase: 'l', T: 298.15 K, P: 101325 Pa - flow (kmol/hr): Ethanol 200 - outs... - [0] out_a - phase: 'l', T: 303.15 K, P: 101325 Pa - flow (kmol/hr): Water 200 - [1] out_b - phase: 'l', T: 329.64 K, P: 101325 Pa - flow (kmol/hr): Ethanol 200 - >>> hx.results() - Heat Exchanger Units hx - Design Area ft^2 207 - Overall heat transfer coefficient kW/m^2/K 0.5 - Fouling correction factor 1 - Tube side pressure drop psi 5 - Shell side pressure drop psi 5 - Operating pressure psi 14.7 - Total tube length ft 20 - Purchase cost Heat exchanger USD 2.05e+04 - Total purchase cost USD 2.05e+04 - Utility cost USD/hr 0 - - One latent fluid and one sensible fluid case: - - >>> in_a = Stream('in_a', Ethanol=50, T=351.43, phase='g') - >>> in_b = Stream('in_b', Water=200) - >>> hx = HXprocess('hx', ins=(in_a, in_b), outs=('out_a', 'out_b'), - ... fluid_type='ls') - >>> hx.simulate() - >>> hx.show() - HXprocess: hx - ins... - [0] in_a - phase: 'g', T: 351.43 K, P: 101325 Pa - flow (kmol/hr): Ethanol 50 - [1] in_b - phase: 'l', T: 298.15 K, P: 101325 Pa - flow (kmol/hr): Water 200 - outs... - [0] out_a - phases: ('g', 'l'), T: 351.39 K, P: 101325 Pa - flow (kmol/hr): (g) Ethanol 32.94 - (l) Ethanol 17.06 - [1] out_b - phase: 'l', T: 346.43 K, P: 101325 Pa - flow (kmol/hr): Water 200 - >>> hx.results() - Heat Exchanger Units hx - Design Area ft^2 228 - Overall heat transfer coefficient kW/m^2/K 0.5 - Fouling correction factor 1 - Tube side pressure drop psi 1.5 - Shell side pressure drop psi 5 - Operating pressure psi 14.7 - Total tube length ft 20 - Purchase cost Heat exchanger USD 2.07e+04 - Total purchase cost USD 2.07e+04 - Utility cost USD/hr 0 - - Latent fluids case: - - >>> in_a = Stream('in_a', Ethanol=50, T=351.43, phase='l') - >>> in_b = Stream('in_b', Water=200, T=373.15, phase='g') - >>> hx = HXprocess('hx', ins=(in_a, in_b), outs=('out_a', 'out_b'), - ... fluid_type='ll') - >>> hx.simulate() - >>> hx.show() - HXprocess: hx - ins... - [0] in_a - phase: 'l', T: 351.43 K, P: 101325 Pa - flow (kmol/hr): Ethanol 50 - [1] in_b - phase: 'g', T: 373.15 K, P: 101325 Pa - flow (kmol/hr): Water 200 - outs... - [0] out_a - phase: 'g', T: 351.43 K, P: 101325 Pa - flow (kmol/hr): Ethanol 50 - [1] out_b - phases: ('g', 'l'), T: 373.12 K, P: 101325 Pa - flow (kmol/hr): (g) Water 150.8 - (l) Water 49.19 - >>> hx.results() - Heat Exchanger Units hx - Design Area ft^2 293 - Overall heat transfer coefficient kW/m^2/K 1 - Fouling correction factor 1 - Tube side pressure drop psi 1.5 - Shell side pressure drop psi 1.5 - Operating pressure psi 14.7 - Total tube length ft 20 - Purchase cost Heat exchanger USD 2.14e+04 - Total purchase cost USD 2.14e+04 - Utility cost USD/hr 0 - - """ - line = 'Heat Exchanger' - _graphics = process_heat_exchanger_graphics - _N_heat_utilities = 0 - _N_ins = 2 - _N_outs = 2 - dT = 5 #: [float] Pinch temperature difference. - - def __init__(self, ID='', ins=None, outs=(), thermo=None, *, - U=None, fluid_type='ss', - material="Carbon steel/carbon steel", - shell_and_tube_type="Floating head", - N_shells=2, - ft=None): - super().__init__(ID, ins, outs, thermo) - - #: [float] Enforced overall heat transfer coefficent (kW/m^2/K) - self.U = U - - #: [float] Total heat transfered. - self.Q = None - - #: Number of shells for LMTD correction factor method. - self.N_shells = N_shells - - #: User imposed correction factor. - self.ft = ft - - self.fluid_type = fluid_type - self.material = material - self.shell_and_tube_type = shell_and_tube_type - - @property - def fluid_type(self): - """ - [None, 'ss', 'll', or 'ls'] - * **None** Rigorously transfers heat until pinch temperature (not implemented yet). - * **'ss'** Sensible-sensible fluids. Heat is exchanged until the pinch temperature is reached. - * **'ll'** Latent-latent fluids. Heat is exchanged until one fluid completely changes phase. - * **'ls'** Latent-sensible fluids. Heat is exchanged until either the pinch temperature is reached or the latent fluid completely changes phase. - """ - return self._fluid_type - @fluid_type.setter - def fluid_type(self, fluid_type): - if fluid_type not in ('ss', 'ls', 'll'): - raise ValueError(f"fluid type must be either 'ss', 'ls', or 'll', not {repr(fluid_type)}") - self._fluid_type = fluid_type - - def get_streams(self): - s_in_a, s_in_b = self.ins - s_out_a, s_out_b = self.outs - return s_in_a, s_in_b, s_out_a, s_out_b - - def _run(self): - so0, so1 = self._outs - si0, si1 = self._ins - s0_F_mol = si0.F_mol - s1_F_mol = si1.F_mol - if not s0_F_mol: - so1.copy_like(si1) - elif not s1_F_mol: - so0.copy_like(si0) - else: - fluid_type = self._fluid_type - so0.copy_like(si0) - so1.copy_like(si1) - if fluid_type == 'ss': - self._run_ss() - elif fluid_type == 'ls': - self._run_ls() - elif fluid_type == 'll': - self._run_ll() - - def _run_ss(self): - dT = self.dT - s1f, s2f = self.outs - s1, s2 = self.ins - s1_hot = s1.T > s2.T # s2 energy will increase - if s1.C < s2.C: - s1f.T = (s2.T + dT) if s1_hot else (s2.T - dT) - Q = s1.H - s1f.H - s2f.T += Q/s2.C - else: - s2f.T = (s1.T - dT) if s1_hot else (s1.T + dT) - Q = s2.H - s2f.H - s1f.T += Q/s1.C - self.Q = Q - - def _run_ls(self): - s1_in, s2_in = self.ins - s1_out, s2_out = self.outs - dT = self.dT - - if s2_out.T > s1_in.T: - # Stream s1 is boiling - boiling = True - s1_out.phase = 'g' - dp = s1_out.dew_point_at_P() - s1_out.T = dp.T - T_pinch = s1_in.T + dT # Minimum - else: - # Stream s1 is condensing - boiling = False - s1_out.phase = 'l' - bp = s1_out.bubble_point_at_P() - s1_out.T = bp.T - T_pinch = s1_in.T - dT # Maximum - - # Calculate maximum latent heat and new temperature of sensible stream - Q = s1_in.H - s1_out.H - T_s2_new = s2_out.T + Q/s2_out.C - s1_out.copy_like(s1_in) - - if boiling and T_s2_new < T_pinch: - # Partial boiling if temperature falls below pinch - H0 = s2_in.H - s2_out.T = T_pinch - delH1 = H0 - s2_out.H - s1_out.vle(P=s1_out.P, H=s1_in.H + delH1) - elif not boiling and T_s2_new > T_pinch: - # Partial condensation if temperature goes above pinch - H0 = s2_in.H - s2_out.T = T_pinch - delH1 = H0 - s2_out.H - s1_out.vle(P=s1_out.P, H=s1_in.H + delH1) - elif boiling: - s1_out.phase ='g' - s2_out.T = T_s2_new - elif not boiling: - s1_out.phase = 'l' - s2_out.T = T_s2_new - self.Q = Q - - def _run_ll(self): - s1_in, s2_in = self.ins - s1_out, s2_out = self.outs - - if s1_in.T > s2_in.T and ('g' in s1_in.phase) and ('l' in s2_in.phase): - for s in self.outs: s.phases = ('g', 'l') - # s2 boiling, s1 condensing - boiling = s2_out - delH1 = s1_out['g'].Hvap - delH2 = s2_out['l'].Hvap - elif s1_in.T < s2_in.T and ('l' in s1_in.phase) and ('g' in s2_in.phase): - for s in self.outs: s.phases = ('g', 'l') - # s1 boiling, s2 condensing - boiling = s1_out - delH1 = s1_out['l'].Hvap - delH2 = s2_out['g'].Hvap - else: - raise ValueError(f"no latent heat streams available for heat exchange with shell_and_tube_type='ll'") - - # sc: Stream that completely changes phase - # sp: Stream that partialy changes phase - if delH1 > delH2: - sc_in = s2_in - sc_out = s2_out - sp_in = s1_in - sp_out = s1_out - else: - sc_in = s1_in - sc_out = s1_out - sp_in = s2_in - sp_out = s2_out - - if sc_out is boiling: - sc_out.phase = 'g' - dp = sc_out.dew_point_at_P() - sc_out.T = dp.T - else: - sc_out.phase = 'l' - bp = sc_out.bubble_point_at_P() - sc_out.T = bp.T - - # VLE - Q = (sc_in.H - sc_out.H) - sp_out.vle(P=sp_out.P, H=sp_in.H + Q) - self.Q = abs(Q) - - diff --git a/build/lib/biosteam/units/_junction.py b/build/lib/biosteam/units/_junction.py deleted file mode 100644 index edc1cb587..000000000 --- a/build/lib/biosteam/units/_junction.py +++ /dev/null @@ -1,114 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sat Jun 8 23:29:37 2019 - -@author: yoelr -""" -from .._unit import Unit -from .._graphics import junction_graphics -from .._power_utility import PowerUtility -from thermosteam import MultiStream -from ..utils.piping import Ins, Outs - -__all__ = ('Junction',) - - -# %% Connect between different flowsheets - -chemicals_in_common = lambda upstream, downstream: \ - tuple(set(upstream.chemicals.IDs).intersection(downstream.chemicals.IDs)) - -class Junction(Unit): - """Create a Junction object that copies specifications from `upstream` - to `downstream`. This serves to connect streams with different - Species object. - - Parameters - ---------- - upstream=None : Stream or str, defaults to missing stream - Stream that will be copied to `downstream`. - downstream="" : Stream or str, defaults to new stream - Flow rate, T, P, and phase information - will be copied from `upstream` to this stream. - If None, stream will be missing. - species=None : list[str], defaults to all species in common - IDs of species to be passed down. - - Examples - -------- - Create a Junction object and connect streams with different Species objects: - - >>> from biosteam import * - >>> settings.thermo = Thermo(['Water']) - >>> s1 = Stream('s1', Water=20) - >>> settings.thermo = Thermo(['Ethanol', 'Water']) - >>> s2 = Stream('s2') # Note that s2 and s1 have different chemicals defined - >>> J1 = units.Junction('J1', s1, s2) - >>> J1.simulate() - >>> J1.show() - Junction: J1 - ins... - [0] s1 - phase: 'l', T: 298.15 K, P: 101325 Pa - flow (kmol/hr): Water 20 - outs... - [0] s2 - phase: 'l', T: 298.15 K, P: 101325 Pa - flow (kmol/hr): Water 20 - - """ - _graphics = junction_graphics - _N_ins = _N_outs = 1 - heat_utilities = () - power_utility = PowerUtility() - def __init__(self, ID="", upstream=None, downstream=None, thermo=None): - thermo = self._load_thermo(thermo) - self._numerical_specification = None - self._chemicals_in_common = self._past_streams = () - self._ins = Ins(self, self._N_ins, upstream, thermo) - self._outs = Outs(self, self._N_outs, downstream, thermo) - self._register(ID) - - def set_spec(self, *args, **kwargs): - raise TypeError("{type(self).__name__}' does not support design specifications") - - def get_spec(self): - return None - - def _get_chemicals_in_common(self, upstream, downstream): - if (upstream, downstream) == self._past_streams: - IDs = self._chemicals_in_common - else: - self._chemicals_in_common = IDs = chemicals_in_common(upstream, downstream) - return IDs - - def _get_streams(self): - try: - upstream, = self._ins - downstream, = self._outs - except ValueError as error: - N_ins = self._ins.size - N_outs = self._outs.size - if N_ins != 1: - raise RuntimeError(f'a Junction object must have 1 input stream, not {N_ins}') - elif N_outs != 1: - raise RuntimeError(f'a Junction object must have 1 output stream, not {N_outs}') - else: - raise error - return upstream, downstream - - def _run(self): - upstream, downstream = self._get_streams() - IDs = self._get_chemicals_in_common(upstream, downstream) - if isinstance(upstream, MultiStream): - downstream.phases = upstream.phases - downstream.imol[..., IDs] = upstream.imol[..., IDs] - else: - downstream.phase = upstream.phase - downstream.imol[IDs] = upstream.imol[IDs] - downstream.T = upstream.T - downstream.P = upstream.P - simulate = _run - - - diff --git a/build/lib/biosteam/units/_liquids_centrifuge.py b/build/lib/biosteam/units/_liquids_centrifuge.py deleted file mode 100644 index c887ca289..000000000 --- a/build/lib/biosteam/units/_liquids_centrifuge.py +++ /dev/null @@ -1,202 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 23 21:23:56 2018 - -@author: yoelr -""" -from .. import Unit -from ._flash import RatioFlash -from ._splitter import Splitter -from .decorators import cost -import thermosteam as tmo - -__all__ = ('LiquidsCentrifuge', - 'LiquidsSplitCentrifuge', - 'LiquidsRatioCentrifuge', - 'LLECentrifuge') - -# electricity kW/(m3/hr) from USDA biosdiesel Super Pro model -# Possibly 1.4 kW/(m3/hr) -# https://www.sciencedirect.com/topics/engineering/disc-stack-centrifuge -# Microalgal fatty acids—From harvesting until extraction H.M. Amaro, et. al., -# in Microalgae-Based Biofuels and Bioproducts, 2017 - -@cost('Flow rate', units='m^3/hr', CE=525.4, cost=28100, - n=0.574, kW=3.66, ub=100, BM=2.03, N='Number of centrifuges') -class LiquidsCentrifuge(Unit, isabstract=True): - r""" - Abstract class for liquid centrifuges. - - Parameters - ---------- - ins : stream - Inlet fluid. - outs : stream sequence - * [0] 'liquid' phase fluid - * [1] 'LIQUID' phase fluid - - Notes - ----- - The f.o.b purchase cost is given by [1]_: - - :math:`C_{f.o.b}^{2007} = 28100 Q^{0.574} (Q < 100 \frac{m^3}{h})` - - References - ---------- - .. [1] Apostolakou, A. A.; Kookos, I. K.; Marazioti, C.; Angelopoulos, - K. C. Techno-Economic Analysis of a Biodiesel Production Process - from Vegetable Oils. Fuel Process. Technol. 2009, 90, 1023−1031 - - """ - _N_outs = 2 - line = 'Liquids centrifuge' - - -class LiquidsRatioCentrifuge(LiquidsCentrifuge): - _N_heat_utilities = 0 - line = 'Liquids centrifuge' - __init__ = RatioFlash.__init__ - _run = RatioFlash._run - - -class LiquidsSplitCentrifuge(LiquidsCentrifuge): - r""" - Create a liquids centrifuge simulated by component splits. - - Parameters - ---------- - ins : stream - Inlet fluid. - outs : stream sequence - * [0] 'liquid' phase fluid - * [1] 'LIQUID' phase fluid - 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. - - Notes - ----- - The f.o.b purchase cost is given by [1]_: - - :math:`C_{f.o.b}^{2007} = 28100 Q^{0.574} (Q < 100 \frac{m^3}{h})` - - References - ---------- - .. [1] Apostolakou, A. A.; Kookos, I. K.; Marazioti, C.; Angelopoulos, - K. C. Techno-Economic Analysis of a Biodiesel Production Process - from Vegetable Oils. Fuel Process. Technol. 2009, 90, 1023−1031 - - """ - line = 'Liquids centrifuge' - __init__ = Splitter.__init__ - _run = Splitter._run - split = Splitter.split - - -class LLECentrifuge(LiquidsCentrifuge): - r""" - Create a liquids centrifuge simulated by liquid-liquid equilibrium. - - Parameters - ---------- - ins : stream - Inlet fluid. - outs : stream sequence - * [0] 'liquid' phase fluid - * [1] 'LIQUID' phase fluid - top_chemical : str, optional - Identifier of chemical that will be favored in the "liquid" phase. - If none given, the "liquid" phase will the lightest and the "LIQUID" - phase will be the heaviest. - - Notes - ----- - The f.o.b purchase cost is given by [1]_: - - :math:`C_{f.o.b}^{2007} = 28100 Q^{0.574} (Q < 100 \frac{m^3}{h})` - - References - ---------- - .. [1] Apostolakou, A. A.; Kookos, I. K.; Marazioti, C.; Angelopoulos, - K. C. Techno-Economic Analysis of a Biodiesel Production Process - from Vegetable Oils. Fuel Process. Technol. 2009, 90, 1023−1031 - - Examples - -------- - >>> from biorefineries.lipidcane.chemicals import lipidcane_chemicals - >>> from biosteam import units, settings, Stream - >>> settings.set_thermo(lipidcane_chemicals) - >>> feed = Stream('feed', T=333.15, - ... Lipid=0.996, Biodiesel=26.9, - ... Methanol=32.9, Glycerol=8.97) - >>> C1 = units.LLECentrifuge('C1', ins=feed, outs=('light', 'heavy')) - >>> C1.simulate() - >>> C1.show() - LLECentrifuge: C1 - ins... - [0] feed - phase: 'l', T: 333.15 K, P: 101325 Pa - flow (kmol/hr): Methanol 32.9 - Glycerol 8.97 - Biodiesel 26.9 - Lipid 0.996 - outs... - [0] light - phase: 'l', T: 333.15 K, P: 101325 Pa - flow (kmol/hr): Methanol 24.1 - Glycerol 8.96 - Biodiesel 0.00772 - Lipid 5.22e-15 - [1] heavy - phase: 'l', T: 333.15 K, P: 101325 Pa - flow (kmol/hr): Methanol 8.75 - Glycerol 0.013 - Biodiesel 26.9 - Lipid 0.996 - >>> C1.results() - Liquids centrifuge Units C1 - Power Rate kW 46.8 - Cost USD/hr 3.66 - Design Flow rate m^3/hr 12.8 - Number of centrifuges 1 - Purchase cost Liquids centrifuge USD 1.31e+05 - Total purchase cost USD 1.31e+05 - Utility cost USD/hr 3.66 - - """ - line = 'Liquids centrifuge' - - def __init__(self, ID='', ins=None, outs=(), thermo=None, top_chemical=None): - super().__init__(ID, ins, outs, thermo) - self._multistream = tmo.MultiStream(None, phases=('L', 'l'), - thermo=thermo) - self.top_chemical = top_chemical - - def _run(self): - ms = self._multistream - feed = self.ins[0] - top, bottom = self.outs - ms.imol['l'] = feed.mol - ms.lle(feed.T) - top_chemical = self.top_chemical - if top_chemical: - F_l = ms.imol['l', top_chemical] - F_L = ms.imol['L', top_chemical] - top_l = F_l > F_L - else: - rho_l = ms['l'].rho - rho_L = ms['L'].rho - top_l = rho_l < rho_L - if top_l: - top_phase = 'l' - bottom_phase = 'L' - else: - top_phase = 'L' - bottom_phase = 'l' - top.mol[:] = ms.imol[top_phase] - bottom.mol[:] = ms.imol[bottom_phase] - top.T = bottom.T = feed.T - top.P = bottom.P = feed.P \ No newline at end of file diff --git a/build/lib/biosteam/units/_magnetic_separator.py b/build/lib/biosteam/units/_magnetic_separator.py deleted file mode 100644 index 9a2efc19f..000000000 --- a/build/lib/biosteam/units/_magnetic_separator.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon Mar 4 11:32:01 2019 - -@author: yoelr -""" - -from .._unit import Unit -from .decorators import cost - -__all__ = ('MagneticSeparator',) - -@cost('Flow rate', units='kg/hr', CE=576, cost=533471, S=333333, n=0.6) -class MagneticSeparator(Unit): pass - \ No newline at end of file diff --git a/build/lib/biosteam/units/_mixer.py b/build/lib/biosteam/units/_mixer.py deleted file mode 100644 index 118dcd3bc..000000000 --- a/build/lib/biosteam/units/_mixer.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 23 14:26:41 2018 - -@author: yoelr -""" -from .. import Unit -from .._graphics import mixer_graphics - -__all__ = ('Mixer',) - -class Mixer(Unit): - """ - Create a mixer that mixes any number of streams together. - - Parameters - ---------- - ins : streams - Inlet fluids to be mixed. - outs : stream - Mixed outlet fluid. - Examples - -------- - Mix two streams: - - >>> from biosteam import units, settings, Stream - >>> settings.set_thermo(['Ethanol', 'Water']) - >>> 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.11 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 _run(self): - s_out, = self.outs - s_out.mix_from(self.ins) diff --git a/build/lib/biosteam/units/_molecular_sieve.py b/build/lib/biosteam/units/_molecular_sieve.py deleted file mode 100644 index 69032ba74..000000000 --- a/build/lib/biosteam/units/_molecular_sieve.py +++ /dev/null @@ -1,110 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 23 22:17:05 2018 - -@author: yoelr -""" -from ._splitter import Splitter -from .decorators import cost - -__all__ = ('MolecularSieve',) - - -# @cost('Flow rate', 'Pressure filter drying (2)', -# cost=405000, CE=521.9, S=22687, n=0.6, kW=1044) -# @cost('Flow rate', 'Pressure filter pressing', -# cost=75200, CE=521.9, S=22687, n=0.6, kW=112) -@cost('Flow rate', 'Column', kW=151, BM=1.8, - cost=2601000, CE=521.9, S=22687, n=0.6) -class MolecularSieve(Splitter): - """ - Create an ethanol/water molecular sieve for bioethanol plants. - The molecular sieve is modeled as a component wise separator. Costing - is based on scaling by the 6/10ths rule from an NREL TEA report [1]_. - - Parameters - ---------- - ins : stream - * [0] Feed (gas) - outs : stream sequence - * [0] Split stream (gas) - * [1] Remainder stream (gas) - split : array_like - Componentwise split to the 0th output stream - - Examples - -------- - >>> from biosteam import Stream, settings - >>> from biosteam.units import MolecularSieve - >>> settings.set_thermo(['Water', 'Ethanol']) - >>> feed = Stream('feed', flow=(75.7, 286), phase='g') - >>> bp = feed.bubble_point_at_T() - >>> feed.T = bp.T - >>> MS1 = MolecularSieve('MS1', ins=feed, - ... outs=('ethanol_rich', 'water_rich'), - ... split=dict(Water=0.160, - ... Ethanol=0.925)) - >>> MS1.simulate() - >>> MS1.show(T='degC', P='atm', composition= True) - MolecularSieve: MS1 - ins... - [0] feed - phase: 'g', T: 25 degC, P: 1 atm - composition: Water 0.209 - Ethanol 0.791 - ------- 362 kmol/hr - outs... - [0] ethanol_rich - phase: 'g', T: 25 degC, P: 1 atm - composition: Water 0.0438 - Ethanol 0.956 - ------- 277 kmol/hr - [1] water_rich - phase: 'g', T: 25 degC, P: 1 atm - composition: Water 0.748 - Ethanol 0.252 - ------- 85 kmol/hr - - >>> MS1.results() - Molecular sieve Units MS1 - Power Rate kW 14.2 - Cost USD/hr 1.11 - Low pressure steam Duty kJ/hr 3.21e+06 - Flow kmol/hr 82.6 - Cost USD/hr 19.7 - Chilled water Duty kJ/hr -1.18e+05 - Flow kmol/hr 123 - Cost USD/hr 0.592 - Design Flow rate kg/hr 2.13e+03 - Purchase cost Column USD 6.85e+05 - Total purchase cost USD 6.85e+05 - Utility cost USD/hr 21.4 - - - References - ---------- - .. [1] Process Design and Economics for Biochemical Conversion of - Lignocellulosic Biomass to Ethanol Dilute-Acid Pretreatment and - Enzymatic Hydrolysis of Corn Stover. D. Humbird, R. Davis, L. - Tao, C. Kinchin, D. Hsu, and A. Aden (National Renewable Energy - Laboratory Golden, Colorado). P. Schoen, J. Lukas, B. Olthof, - M. Worley, D. Sexton, and D. Dudgeon (Harris Group Inc. Seattle, - Washington and Atlanta, Georgia) - - """ - _N_heat_utilities = 2 - _units = {'Flow rate': 'kg/hr'} - def __init__(self, ID='', ins=None, outs=(), *, order=None, split): - Splitter.__init__(self, ID, ins, outs, order=order, split=split) - - def _setup(self): - s1_out, s2_out = self.outs - s1_out.phase = s2_out.phase = 'g' - - def _design(self): - self.design_results['Flow rate'] = flow = self._outs[1].F_mass - T = self.ins[0].T - hu1, hu2 = self.heat_utilities - hu1(1429.65*flow, T) - hu2(-55.51*flow, T) - diff --git a/build/lib/biosteam/units/_multi_effect_evaporator.py b/build/lib/biosteam/units/_multi_effect_evaporator.py deleted file mode 100644 index f55c15ca2..000000000 --- a/build/lib/biosteam/units/_multi_effect_evaporator.py +++ /dev/null @@ -1,242 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 23 21:43:13 2018 - -@author: yoelr -""" -import numpy as np -import biosteam as bst -from .. import Unit -from ._mixer import Mixer -from ._hx import HXutility -from ._flash import Evaporator_PV, Evaporator_PQ -from .design_tools import ( - compute_vacuum_system_power_and_cost, - compute_heat_transfer_area -) -from thermosteam import Stream, settings -from flexsolve import IQ_interpolation -from warnings import warn -import ht - -__all__ = ('MultiEffectEvaporator',) - -log = np.log -exp = np.exp - -# Table 22.32 Product process and design (pg 592) -# Name: ('Area range (m2)', 'Cost(A) (USD)', 'U (kJ/(hr*m2*K)))', 'Material') -evaporators = {'Horizontal tube': - ((9.29, 743.224), - lambda A, CE: CE*2.304*A**0.53, - 4906.02, - 'Carbon steel'), - 'Long-tube vertical': - ((9.29, 743.224), - lambda A, CE: CE*3.086*A**0.55, - 8176.699, - 'Carbon steel'), - 'Forced circulation': - ((13.935, 8000), - lambda A, CE: CE/500*exp(8.2986 + 0.5329*log(A*0.0929)-0.000196*log(A*0.0929)**2), - 10731.918, - 'Carbon steel'), - 'Falling film': - ((13.935, 371.612), - lambda A, CE: CE*7.416*A**0.55, - 10220.874, - 'Stainless steel tubes/Carbon steel shell')} - - -class MultiEffectEvaporator(Unit): - """ - Creates evaporatorators with pressures given by P (a list of pressures). - Adjusts first evaporator vapor fraction to satisfy an overall fraction - evaporated. All evaporators after the first have zero duty. Condenses - the vapor coming out of the last evaporator. Pumps all liquid streams - to prevent back flow in later parts. All liquid evaporated is ultimately - recondensed. Cost is based on required heat transfer area. Vacuum system - is based on air leakage. Air leakage is based on volume, as given by - residence time `tau` and flow rate to each evaporator. - - Parameters - ---------- - ins : stream - Inlet. - outs : stream sequence - * [0] Solid-rich stream. - * [1] Condensate stream. - component : str - Component being evaporated. - P : tuple[float] - Pressures describing each evaporator (Pa). - V : float - Overall molar fraction of component evaporated. - P_liq : tuple - Liquid pressure after pumping (Pa). - - """ - _units = {'Area': 'm^2', - 'Volume': 'm^3'} - _N_outs = 2 - _N_heat_utilities = 2 - BM = 2.45 - line = 'Multi-Effect Evaporator' - - #: Residence time (hr) - tau = 0.30 - - # Evaporator type - _Type = 'Forced circulation' - - # Data for simmulation and costing - _evap_data = evaporators[_Type] - - @property - def Type(self): - """Evaporation type.""" - return self._Type - @Type.setter - def Type(self, evap_type): - try: - self._evap_data = evaporators[evap_type] - except KeyError: - dummy = str(evaporators.keys())[11:-2] - raise ValueError(f"Type must be one of the following: {dummy}") - self._Type = evap_type - - def __init__(self, ID='', ins=None, outs=(), thermo=None, *, P, V): - Unit.__init__(self, ID, ins, outs, thermo) - # Unpack - out_wt_solids, liq = self.outs - self.V = V #: [float] Overall molar fraction of component evaporated. - self._V1 = V/2. - - # Create components - self._N_evap = n = len(P) # Number of evaporators - first_evaporator = Evaporator_PV(None, outs=(None, None), P=P[0]) - - # Put liquid first, then vapor side stream - evaporators = [first_evaporator] - for i in range(1, n): - evap = Evaporator_PQ(None, outs=(None, None, None), P=P[i], Q=0) - evaporators.append(evap) - - condenser = HXutility(None, outs=Stream(None), V=0) - self.heat_utilities = (first_evaporator.heat_utilities[0], - condenser.heat_utilities[0]) - mixer = Mixer(None, outs=Stream(None)) - - self.components = {'evaporators': evaporators, - 'condenser': condenser, - 'mixer': mixer} - - - def _run(self): - out_wt_solids, liq = self.outs - ins = self.ins - - n = self._N_evap # Number of evaporators - - # Set-up components - components = self.components - evaporators = components['evaporators'] - first_evaporator, *other_evaporators = evaporators - first_evaporator.ins[:] = [i.copy() for i in ins] - condenser = components['condenser'] - mixer = components['mixer'] - - # Put liquid first, then vapor side stream - ins = [first_evaporator.outs[1], first_evaporator.outs[0]] - for evap in other_evaporators: - evap.ins[:] = ins - ins = [evap.outs[1], evap.outs[0]] - - def compute_overall_vapor_fraction(v1): - v_overall = v1 - first_evaporator.V = v1 - first_evaporator._run() - for evap in other_evaporators: - evap._run() - v_overall += (1-v_overall) * evap.V - return v_overall - - x0 = 0.0001 - x1 = 0.9990 - y0 = compute_overall_vapor_fraction(x0) - y1 = compute_overall_vapor_fraction(x1) - self._V1 = IQ_interpolation(compute_overall_vapor_fraction, - x0, x1, y0, y1, self._V1, self.V, - xtol=0.0001, ytol=0.001) - # Condensing vapor from last effector - outs_vap = evaporators[-1].outs[0] - condenser.ins[:] = [outs_vap] - condenser._run() - outs_liq = [condenser.outs[0]] # list containing all output liquids - - # Unpack other output streams - out_wt_solids.copy_like(evaporators[-1].outs[1]) - for i in range(1, n): - evap = evaporators[i] - outs_liq.append(evap.outs[2]) - - # Mix liquid streams - mixer.ins[:] = outs_liq - mixer._run() - liq.copy_like(mixer.outs[0]) - - def _design(self): - # This functions also finds the cost - A_range, C_func, U, _ = self._evap_data - components = self.components - evaporators = components['evaporators'] - Design = self.design_results - Cost = self.purchase_costs - CE = bst.CE - - first_evaporator = evaporators[0] - hu = first_evaporator.heat_utilities[0] - duty = first_evaporator.H_out - first_evaporator.H_in - Q = abs(duty) - Tci = first_evaporator.ins[0].T - Tco = first_evaporator.outs[0].T - hu(duty, Tci, Tco) - Th = hu.inlet_utility_stream.T - LMTD = ht.LMTD(Th, Th, Tci, Tco) - ft = 1 - A = abs(compute_heat_transfer_area(LMTD, U, Q, ft)) - self._evap_costs = evap_costs = [C_func(A, CE)] - - # Find condenser requirements - condenser = components['condenser'] - condenser._design() - condenser._cost() - Cost['Condenser'] = condenser.purchase_costs['Heat exchanger'] - - # Find area and cost of evaporators - As = [A] - A_min, A_max = A_range - for evap in evaporators[1:]: - Q = evap._Q - Tc = evap.outs[0].T - Th = evap.outs[2].T - LMTD = Th - Tc - A = compute_heat_transfer_area(LMTD, U, Q, ft) - As.append(A) - if settings.debug and not A_min < A < A_max: - warn(f'area requirement ({A}) is out of range, {A_range}') - evap_costs.append(C_func(A, CE)) - self._As = As - Design['Area'] = A = sum(As) - Design['Volume'] = total_volume = self._N_evap * self.tau * self.ins[0].F_vol - Cost['Evaporators'] = sum(evap_costs) - - # Calculate power - power, cost = compute_vacuum_system_power_and_cost( - F_mass=0, F_vol=0, P_suction=evap.outs[0].P, - vessel_volume=total_volume, - vacuum_system_preference='Liquid-ring pump') - Cost['Vacuum liquid-ring pump'] = cost - self.power_utility(power) - - diff --git a/build/lib/biosteam/units/_process_specification.py b/build/lib/biosteam/units/_process_specification.py deleted file mode 100644 index 22acb4a2d..000000000 --- a/build/lib/biosteam/units/_process_specification.py +++ /dev/null @@ -1,86 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sat Jul 13 02:24:35 2019 - -@author: yoelr -""" -from .._unit import Unit -from .._graphics import process_specification_graphics -from ..utils import format_unit_line - -__all__ = ('ProcessSpecification',) - -class ProcessSpecification(Unit): - """ - Create a ProcessSpecification object that runs a function when simulated - to set a process specification. - - Parameters - ---------- - ins : stream, optional - Inlet stream. - outs : stream, optional - Outlet stream. - specification : callable - Called during simulation to set a process specification. - description : - Description of process specification. Defaults to name of the `specification` function. - - Examples - -------- - Use a ProcessSpecifcation object to define the amount of denaturant to - add according to the flow of bioethanol. The final bioethanol product - must be 2 wt. % denaturant: - - >>> from biosteam import settings, Stream, units, main_flowsheet - >>> main_flowsheet.set_flowsheet('mix_ethanol_with_denaturant') - >>> settings.set_thermo(['Water', 'Ethanol', 'Octane']) - >>> ethanol = Stream('ethanol', T=340, Water=200, Ethanol=22500, units='kg/hr') - >>> denaturant = Stream('denaturant', Octane=1) - >>> def adjust_denaturant(): - ... denaturant_over_ethanol_flow = 0.02 / 0.98 # A mass ratio - ... denaturant.imass['Octane'] = denaturant_over_ethanol_flow * ethanol.F_mass - >>> PS1 = units.ProcessSpecification('PS1', - ... ins=ethanol, outs='ethanol_', - ... specification=adjust_denaturant) - >>> M1 = units.Mixer('M1', ins=(PS1-0, denaturant), outs='denatured_ethanol') - >>> system = main_flowsheet.create_system('mix_ethanol_with_denaturant_sys') - >>> system.show() - System: mix_ethanol_with_denaturant_sys - path: (PS1, M1) - >>> system.simulate() - >>> M1.outs[0].show(composition=True, flow='kg/hr') - Stream: denatured_ethanol from - phase: 'l', T: 339.32 K, P: 101325 Pa - composition: Water 0.00863 - Ethanol 0.971 - Octane 0.02 - ------- 2.32e+04 kg/hr - - """ - _graphics = process_specification_graphics - _N_ins = _N_outs = 1 - power_utility = None - results = None - heat_utilities = () - - def __init__(self, ID='', ins=None, outs=(), thermo=None, *, - specification, description=None): - self._numerical_specification = None - self._load_thermo(thermo) - self._init_ins(ins) - self._init_outs(outs) - self._assert_compatible_property_package() - self._register(ID) - self.specification = specification - self.description = format_unit_line(description or specification.__name__) - - @property - def specification(self): - return self._specification - @specification.setter - def specification(self, specification): - assert callable(specification), "specification must be a function" - self._run = self._specification = specification - - diff --git a/build/lib/biosteam/units/_pump.py b/build/lib/biosteam/units/_pump.py deleted file mode 100644 index dd7cf5e46..000000000 --- a/build/lib/biosteam/units/_pump.py +++ /dev/null @@ -1,288 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 23 15:53:14 2018 - -@author: yoelr -""" -import numpy as np -from fluids.pump import nema_sizes_hp -from .design_tools.mechanical import calculate_NPSH, pump_efficiency, nearest_NEMA_motor_size -from .design_tools.specification_factors import ( - pump_material_factors, - pump_centrifugal_factors) -from .._unit import Unit -from ..utils import static_flow_and_phase -import biosteam as bst - -__all__ = ('Pump',) - -ln = np.log -exp = np.exp - -# %% Data - -max_hp = nema_sizes_hp[-1] -pump_types = ('Default', 'CentrifugalSingle', 'CentrifugalDouble', 'Gear') - - -# %% Classes - -# TODO: Fix pump selection to include NPSH available and required. -@static_flow_and_phase -class Pump(Unit): - """ - Create a pump that sets the pressure of the outlet. - - Parameters - ---------- - ins : stream - Inlet. - outs : stream - Outlet. - P=101325 : float, optional - Pressure of output stream (Pa). If the pressure of the outlet is - the same as the inlet, pump is design to increase of pressure - to `P_design`. - pump_type='Default' : str - If 'Default', a pump type is selected based on heuristics. - material : str, optional - Contruction material of pump. Defaults to 'Cast iron'. - P_design=405300 : float - Design pressure for pump. - ignore_NPSH=True : bool - Whether to take into consideration NPSH in the selection of a - pump type. - - Notes - ----- - Default pump selection and design and cost algorithms are based on [0]_. - - Examples - -------- - Simulate Pump for pressure increase: - - >>> from biosteam import Stream, settings - >>> from biosteam.units import Pump - >>> settings.set_thermo(['Water', 'Ethanol']) - >>> feed = Stream('feed', Water=200, T=350) - >>> P1 = Pump('P1', ins=feed, outs='out', P=2e5) - >>> P1.simulate() - >>> P1.show() - Pump: P1 - ins... - [0] feed - phase: 'l', T: 350 K, P: 101325 Pa - flow (kmol/hr): Water 200 - outs... - [0] out - phase: 'l', T: 350 K, P: 200000 Pa - flow (kmol/hr): Water 200 - >>> P1.results() - Pump Units P1 - Power Rate kW 0.29 - Cost USD/hr 0.0227 - Design Ideal power hp 0.137 - Flow rate gpm 16.4 - Efficiency 0.352 - Actual power 0.389 - Pump power 0.5 - N 1 - Head ft 97 - Type Centrifugal - Purchase cost Pump USD 4.37e+03 - Motor USD 311 - Total purchase cost USD 4.68e+03 - Utility cost USD/hr 0.0227 - - References - ---------- - .. [0] Seider, Warren D., et al. (2017). "Cost Accounting and Capital Cost - Estimation". In Product and Process Design Principles: Synthesis, - Analysis, and Evaluation (pp. 450-455). New York: Wiley. - - """ - _units = {'Ideal power': 'hp', - 'Power': 'hp', - 'Head': 'ft', - 'NPSH': 'ft', - 'Flow rate': 'gpm'} - BM = 3.3 - - @property - def pump_type(self): - """Pump type""" - return self._pump_type - @pump_type.setter - def pump_type(self, pump_type): - if pump_type not in pump_types: - raise ValueError('Type must be one of the following: {", ".join(pump_types)}') - self._pump_type = pump_type - - @property - def material(self): - """Pump material""" - return self._material - @material.setter - def material(self, material): - try: - self._F_M = pump_material_factors[material] - except KeyError: - raise ValueError("material must be one of the following: " - f"{', '.join(pump_material_factors)}") - self._F_Mstr = material - - def __init__(self, ID='', ins=None, outs=(), thermo=None, *, - P=101325, - pump_type='Default', - material='Cast iron', - P_design=405300, - ignore_NPSH=True): - Unit.__init__(self, ID, ins, outs, thermo) - self.P = P - self.pump_type = pump_type - self.material = material - self.P_design = P_design - self.ignore_NPSH = ignore_NPSH - - def _setup(self): - s_in, = self.ins - s_out, = self.outs - s_out.P = self.P - - def _run(self): - s_in, = self.ins - s_out, = self.outs - s_out.T = s_in.T - - def _design(self): - Design = self.design_results - si, = self.ins - so, = self.outs - Pi = si.P - Po = so.P - Qi = si.F_vol - mass = si.F_mass - nu = si.nu - dP = Po - Pi - if dP < 1: dP = self.P_design - Pi - Design['Ideal power'] = power_ideal = Qi*dP*3.725e-7 # hp - Design['Flow rate'] = q = Qi*4.403 # gpm - if power_ideal <= max_hp: - Design['Efficiency'] = efficiency = pump_efficiency(q, power_ideal) - Design['Actual power'] = power = power_ideal/efficiency - Design['Pump power'] = nearest_NEMA_motor_size(power) - Design['N'] = N = 1 - Design['Head'] = head = power/mass*897806 # ft - # Note that: - # head = power / (mass * gravity) - # head [ft] = power[hp]/mass[kg/hr]/9.81[m/s^2] * conversion_factor - # and 897806 = (conversion_factor/9.81) - else: - power_ideal /= 2 - q /= 2 - if power_ideal <= max_hp: - Design['Efficiency'] = efficiency = pump_efficiency(q, power_ideal) - Design['Actual power'] = power = power_ideal/efficiency - Design['Pump power'] = nearest_NEMA_motor_size(power) - Design['N'] = N = 2 - Design['Head'] = head = power/mass*897806 # ft - else: - raise NotImplementedError('more than 2 pump required, but not yet implemented') - - if self.ignore_NPSH: - NPSH_satisfied = True - else: - Design['NPSH'] = NPSH = calculate_NPSH(Pi, si.P_vapor, si.rho) - NPSH_satisfied = NPSH > 1.52 - - # Get type - pump_type = self.pump_type - if pump_type == 'Default': - if (0.00278 < q < 5000 - and 15.24 < head < 3200 - and nu < 0.00002 - and NPSH_satisfied): - pump_type = 'Centrifugal' - elif (q < 1500 - and head < 914.4 - and 0.00001 < nu < 0.252): - pump_type = 'Gear' - elif (head < 20000 - and q < 500 - and power < 200 - and nu < 0.01): - pump_type = 'MeteringPlunger' - else: - NPSH = calculate_NPSH(Pi, si.P_vapor, si.rho) - raise NotImplementedError( - f'no pump type available at current power ' - f'({power:.3g} hp), flow rate ({q:.3g} gpm), and head ' - f'({head:.3g} ft), kinematic viscosity ({nu:.3g} m2/s), ' - f'and NPSH ({NPSH:.3g} ft)') - - Design['Type'] = pump_type - self.power_utility(power/N/1.341) # Set power in kW - - def _cost(self): - # Parameters - Design = self.design_results - Cost = self.purchase_costs - pump_type = Design['Type'] - q = Design['Flow rate'] - h = Design['Head'] - p = Design['Pump power'] - F_M = self._F_M - I = bst.CE/567 - lnp = ln(p) - - # TODO: Add cost equation for small pumps - # Head and flow rate is too small, so make conservative estimate on cost - if q < 50: q = 50 - if h < 50: h = 50 - - # Cost pump - if 'Centrifugal' in pump_type: - # Find pump factor - F_Tdict = pump_centrifugal_factors - F_T = 1 # Assumption - if p < 75 and 50 <= q <= 900 and 50 <= h <= 400: - F_T = F_Tdict['VSC3600'] - elif p < 200 and 50 <= q <= 3500 and 50 <= h <= 2000: - F_T = F_Tdict['VSC1800'] - elif p < 150 and 100 <= q <= 1500 and 100 <= h <= 450: - F_T = F_Tdict['HSC3600'] - elif p < 250 and 250 <= q <= 5000 and 50 <= h <= 500: - F_T = F_Tdict['HSC1800'] - elif p < 250 and 50 <= q <= 1100 and 300 <= h <= 1100: - F_T = F_Tdict['2HSC3600'] - elif p < 1450 and 100 <= q <= 1500 and 650 <= h <= 3200: - F_T = F_Tdict['2+HSC3600'] - else: - raise NotImplementedError(f'no centrifugal pump available at current power ({p:.3g} hp), flow rate ({q:.3g} gpm), and head ({h:.3g} ft)') - S = q*h**0.5 # Size factor - S_new = S if S > 400 else 400 - lnS = ln(S_new) - Cb = exp(12.1656-1.1448*lnS+0.0862*lnS**2) - Cb *= S/S_new - Cost['Pump'] = F_M*F_T*Cb*I - elif pump_type == 'Gear': - q_new = q if q > 50 else 50 - lnq = ln(q_new) - Cb = exp(8.2816 - 0.2918*lnq + 0.0743*lnq**2) - Cb *= q/q_new - Cost['Pump'] = F_M*Cb*I - elif pump_type == 'MeteringPlunger': - Cb = exp(7.9361 + 0.26986*lnp + 0.06718*lnp**2) - Cost['Pump'] = F_M*Cb*I - - # Cost electric motor - lnp2 = lnp**2 - lnp3 = lnp2*lnp - lnp4 = lnp3*lnp - Cost['Motor'] = exp(5.9332 + 0.16829*lnp - - 0.110056*lnp2 + 0.071413*lnp3 - - 0.0063788*lnp4)*I - - - - \ No newline at end of file diff --git a/build/lib/biosteam/units/_rvf.py b/build/lib/biosteam/units/_rvf.py deleted file mode 100644 index 3b6321bac..000000000 --- a/build/lib/biosteam/units/_rvf.py +++ /dev/null @@ -1,109 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 23 22:15:20 2018 - -@author: yoelr -""" -from ._solids_separator import SolidsSeparator -from .design_tools import compute_vacuum_system_power_and_cost -import numpy as np -import biosteam as bst - -__all__ = ('RotaryVacuumFilter', 'RVF') - -class RotaryVacuumFilter(SolidsSeparator): - """ - Create a RotaryVacuumFilter object. - - Parameters - ---------- - ins : stream sequence - * [0] Feed - * [1] Wash water - outs : stream sequence - * [0] Retentate - * [1] Permeate - moisture_content : float - Fraction of water in retentate. - - """ - _has_power_utility = True - BM = 2.32 - - #: Revolutions per second - rps = 20/3600 - - #: Radius of the vessel (m) - radius = 1 - - #: Suction pressure (Pa) - P_suction = 105 - - #: 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'} - - #: Efficiency of the vacuum pump - power_efficiency = 0.9 - - 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) - self._power(Area, N_vessels) - iArea = Area/N_vessels # individual vessel - Design['# RVF'] = N_vessels - Design['Individual area'] = iArea - logArea = np.log(iArea) - Cost = np.exp(11.796-0.1905*logArea+0.0554*logArea**2) - self.purchase_costs['Cost of vessels'] = N_vessels*Cost*bst.CE/567 - - def _power(self, area, N_vessels): - s_cake, s_vacuumed = self.outs - - # # Weight of empty plate - # mass_plates = 10*N_vessels - - # # Revolutions per s - # rps = self.rps - - # # Cake volumetric flow meter per sec - # Volcake = s_cake.volnet / 3600 - - # # Thickness of cake layer, assumed uniform and constant - # thCake = Volcake / rps; - - # # Mass of Cake - # mass_cake = thCake * s_cake.rho - radius = self.radius - # cent_a = (2*np.pi*rps)**2*radius - # cent_F = (mass_cake + mass_plates)*cent_a - # work_rot = rps*2*np.pi*radius*cent_F - Area = self.design_results['Area'] - vessel_volume = radius*Area*0.0929/2 # m3 - - # Assume same volume of air comes in as volume of liquid - F_vol = s_vacuumed.F_vol - F_mass = F_vol * 1.2041 # multiply by density of air kg/m3 - work_vacuum, self.purchase_costs['Liquid-ring pump'] = compute_vacuum_system_power_and_cost( - F_mass, F_vol, self.P_suction, vessel_volume) - #power = work_rot/self.power_efficiency/1000 + work_vacuum # kW - self.power_utility(work_vacuum) - - @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 - - diff --git a/build/lib/biosteam/units/_screw_feeder.py b/build/lib/biosteam/units/_screw_feeder.py deleted file mode 100644 index 49e7d8718..000000000 --- a/build/lib/biosteam/units/_screw_feeder.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon Mar 4 16:40:50 2019 - -@author: yoelr -""" -from .decorators import cost -from ..utils.unit_warnings import lb_warning -from .._unit import Unit - -__all__ = ('ScrewFeeder',) - -@cost('Flow rate', ub=10e4, CE=567, cost=1096, n=0.22) -class ScrewFeeder(Unit): - length = 30 #: ft - _N_outs = 1 - _has_power_utility = True - _minimum_flow = 400 - _units = {'Flow rate': 'ft^3/hr'} - - def _design(self): - feed = self.ins[0] - r = self.results - Design = r['Design'] - F_vol = feed.F_vol*35.315 # ft3/hr - if F_vol < self._minimum_flow: - lb_warning('Flow rate', F_vol, self._minimum_flow) - Design['Flow rate'] = F_vol - F_mass = feed.F_mass*0.0006124 #lb/s - self._power_utility(0.0146*F_mass**0.85*self.length*0.7457) \ No newline at end of file diff --git a/build/lib/biosteam/units/_shortcut_column.py b/build/lib/biosteam/units/_shortcut_column.py deleted file mode 100644 index 8df20e1af..000000000 --- a/build/lib/biosteam/units/_shortcut_column.py +++ /dev/null @@ -1,388 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Mar 19 09:22:08 2020 - -@author: yoelr -""" -from math import log10, ceil, exp, floor -from ._binary_distillation import BinaryDistillation -from flexsolve import wegstein, find_bracket, bounded_wegstein -from thermosteam.equilibrium import DewPoint, BubblePoint - -__all__ = ('ShortcutColumn',) - -# %% Functions - -def geometric_mean(a, b): - return (a * b) ** (0.5) - -def compute_mean_volatilities_relative_to_heavy_key(K_distillate, K_bottoms, HK_index): - alpha_distillate = K_distillate / K_distillate[HK_index] - alpha_bottoms = K_bottoms / K_bottoms[HK_index] - alpha_mean = geometric_mean(alpha_distillate, - alpha_bottoms) - return alpha_mean - -def compute_partition_coefficients(y, x): - x[(y == 0) & (x == 0)] = 1 - return y / x - -def compute_distillate_recoveries_Hengsteback_and_Gaddes(d_Lr, b_Hr, - alpha_mean, - LHK_index): - LK_index = LHK_index[0] - alpha_LK = alpha_mean[LK_index] - A_dummy = (1 - b_Hr) / b_Hr - A = log10(A_dummy) - B = log10(d_Lr / (1 - d_Lr) / A_dummy) / log10(alpha_LK) - dummy = 10**A * alpha_mean**B - distillate_recoveries = dummy / (1 + dummy) - distillate_recoveries[LHK_index] = [d_Lr, 1 - b_Hr] - distillate_recoveries[distillate_recoveries < 1e-12] = 0. - return distillate_recoveries - -def compute_minimum_theoretical_stages_Fenske(LHK_distillate, LHK_bottoms, alpha_LK): - LK, HK = LHK_distillate - LHK_ratio_distillate = LK / HK - LK, HK = LHK_bottoms - HLK_ratio_bottoms = HK / LK - N = log10(LHK_ratio_distillate * HLK_ratio_bottoms) / log10(alpha_LK) - return N - -def objective_function_Underwood_constant(theta, q, z_f, - alpha_mean): - return (alpha_mean * z_f / (alpha_mean - theta)).sum() - 1 + q - -def compute_minimum_reflux_ratio_Underwood(alpha_mean, z_d, theta): - Rm = (alpha_mean * z_d / (alpha_mean - theta)).sum() - 1 - return Rm - -def compute_theoretical_stages_Gilliland(Nm, Rm, R): - X = (R - Rm) / (R + 1) - Y = 1 - exp((1 + 54.4*X) / (11 + 117.2*X) * (X - 1) / X**0.5) - N = (Y + Nm) / (1 - Y) - return ceil(N) - -def compute_feed_stage_Kirkbride(N, B, D, - feed_HK_over_LK, - z_LK_bottoms, - z_HK_distillate): - m_over_p = (B/D * feed_HK_over_LK * (z_LK_bottoms / z_HK_distillate)**2) ** 0.206 - return floor(N / (m_over_p + 1)) - - -# %% - -class ShortcutColumn(BinaryDistillation, - new_graphics=False): - r""" - Create a multicomponent distillation column that relies on the - Fenske-Underwood-Gilliland method to solve for the theoretical design - of the distillation column and the separation of non-keys [1]_.The Murphree - efficiency (i.e. column efficiency) is based on the modified O'Connell - correlation [2]_. The diameter is based on tray separation and flooding - velocity [1]_ [3]_. Purchase costs are based on correlations compiled by - Warren et. al. [4]_. - - Parameters - ---------- - ins : streams - Inlet fluids to be mixed into the feed stage. - outs : stream sequence - * [0] Distillate - * [1] Bottoms product - LHK : tuple[str] - Light and heavy keys. - y_top : float - Molar fraction of light key to the light and heavy keys in the - distillate. - x_bot : float - Molar fraction of light key to the light and heavy keys in the bottoms - product. - Lr : float - Recovery of the light key in the distillate. - Hr : float - Recovery of the heavy key in the bottoms product. - k : float - Ratio of reflux to minimum reflux. - specification="Composition" : "Composition" or "Recovery" - If composition is used, `y_top` and `x_bot` must be specified. - If recovery is used, `Lr` and `Hr` must be specified. - P=101325 : float - Operating pressure [Pa]. - vessel_material : str, optional - Vessel construction material. Defaults to 'Carbon steel'. - tray_material : str, optional - Tray construction material. Defaults to 'Carbon steel'. - tray_type='Sieve' : 'Sieve', 'Valve', or 'Bubble cap' - Tray type. - tray_spacing=450 : float - Typically between 152 to 915 mm. - stage_efficiency=None : - User enforced stage efficiency. If None, stage efficiency is - calculated by the O'Connell correlation [2]_. - velocity_fraction=0.8 : float - Fraction of actual velocity to maximum velocity allowable before - flooding. - foaming_factor=1.0 : float - Must be between 0 to 1. - open_tray_area_fraction=0.1 : float - Fraction of open area to active area of a tray. - downcomer_area_fraction=None : float - Enforced fraction of downcomer area to net (total) area of a tray. - If None, estimate ratio based on Oliver's estimation [1]_. - is_divided=False : bool - True if the stripper and rectifier are two separate columns. - - References - ---------- - .. [1] J.D. Seader, E.J. Henley, D.K. Roper. (2011) - Separation Process Principles 3rd Edition. John Wiley & Sons, Inc. - - .. [2] M. Duss, R. Taylor. (2018) - Predict Distillation Tray Efficiency. AICHE - - .. [3] Green, D. W. Distillation. In Perry’s Chemical Engineers’ - Handbook, 9 ed.; McGraw-Hill Education, 2018. - - .. [4] Seider, W. D., Lewin, D. R., Seader, J. D., Widagdo, S., Gani, R., - & Ng, M. K. (2017). Product and Process Design Principles. Wiley. - Cost Accounting and Capital Cost Estimation (Chapter 16) - - Examples - -------- - >>> from biosteam.units import ShortcutColumn - >>> from biosteam import Stream, settings - >>> settings.set_thermo(['Water', 'Methanol', 'Glycerol']) - >>> feed = Stream('feed', flow=(80, 100, 25)) - >>> bp = feed.bubble_point_at_P() - >>> feed.T = bp.T # Feed at bubble point T - >>> D1 = ShortcutColumn('D1', ins=feed, - ... outs=('distillate', 'bottoms_product'), - ... LHK=('Methanol', 'Water'), - ... y_top=0.99, x_bot=0.01, k=2, - ... is_divided=True) - >>> D1.simulate() - >>> # See all results - >>> D1.show(T='degC', P='atm', composition=True) - ShortcutColumn: D1 - ins... - [0] feed - phase: 'l', T: 76.129 degC, P: 1 atm - composition: Water 0.39 - Methanol 0.488 - Glycerol 0.122 - -------- 205 kmol/hr - outs... - [0] distillate - phase: 'g', T: 64.91 degC, P: 1 atm - composition: Water 0.01 - Methanol 0.99 - -------- 100 kmol/hr - [1] bottoms_product - phase: 'l', T: 100.06 degC, P: 1 atm - composition: Water 0.754 - Methanol 0.00761 - Glycerol 0.239 - -------- 105 kmol/hr - >>> D1.results() - Distillation Units D1 - Cooling water Duty kJ/hr -7.9e+06 - Flow kmol/hr 5.4e+03 - Cost USD/hr 2.63 - Low pressure steam Duty kJ/hr 1.24e+07 - Flow kmol/hr 320 - Cost USD/hr 76 - Design Theoretical feed stage 8 - Theoretical stages 16 - Minimum reflux Ratio 1.06 - Reflux Ratio 2.12 - Rectifier stages 13 - Stripper stages 26 - Rectifier height ft 31.7 - Stripper height ft 50.9 - Rectifier diameter ft 4.53 - Stripper diameter ft 4.11 - Rectifier wall thickness in 0.312 - Stripper wall thickness in 0.312 - Rectifier weight lb 6.46e+03 - Stripper weight lb 9e+03 - Purchase cost Rectifier trays USD 1.52e+04 - Stripper trays USD 2.15e+04 - Rectifier tower USD 8.45e+04 - Stripper tower USD 1.09e+05 - Condenser USD 2.09e+04 - Boiler USD 4.45e+03 - Total purchase cost USD 2.55e+05 - Utility cost USD/hr 78.6 - """ - line = 'Distillation' - _ins_size_is_fixed = False - _N_ins = 1 - _N_outs = 2 - - def _run(self): - # Set starting point for solving column - self._run_binary_distillation_mass_balance() - self._add_trace_heavy_and_light_non_keys_in_products() - - # Initialize objects to calculate bubble and dew points - vle_chemicals = self.feed.vle_chemicals - self._dew_point = DewPoint(vle_chemicals, self.thermo) - self._bubble_point = BubblePoint(vle_chemicals, self.thermo) - self._IDs_vle = IDs = self._dew_point.IDs - self._LHK_vle_index = [IDs.index(i) for i in self.LHK] - - # Solve for new recoveries - distillate_recoveries = self._solve_distillate_recoveries() - self._update_distillate_recoveries(distillate_recoveries) - self._update_distillate_and_bottoms_temperature() - - def _setup_cache(self): - pass - - def plot_stages(self): - raise TypeError('cannot plot stages for shortcut column') - - def _design(self): - self._run_FenskeUnderwoodGilliland() - self._run_condenser_and_boiler() - self._complete_distillation_column_design() - - def _run_FenskeUnderwoodGilliland(self): - LHK_index = self._LHK_index - alpha_mean = self._estimate_mean_volatilities_relative_to_heavy_key() - LK_index = self._LHK_vle_index[0] - alpha_LK = alpha_mean[LK_index] - feed, = self.ins - distillate, bottoms = self.outs - Nm = compute_minimum_theoretical_stages_Fenske(distillate.mol[LHK_index], - bottoms.mol[LHK_index], - alpha_LK) - theta = self._solve_Underwood_constant(alpha_mean, alpha_LK) - IDs = self._IDs_vle - z_d = distillate.get_normalized_mol(IDs) - Rm = compute_minimum_reflux_ratio_Underwood(alpha_mean, z_d, theta) - if Rm < 0.1: Rm = 0.1 - R = self.k * Rm - N = compute_theoretical_stages_Gilliland(Nm, Rm, R) - feed_HK, feed_LK = feed.mol[LHK_index] - feed_HK_over_LK = feed_HK / feed_LK - Bs = bottoms.imol[IDs] - Ds = distillate.imol[IDs] - B = Bs.sum() - D = Ds.sum() - LK_index, HK_index = LHK_index - z_LK_bottoms = bottoms.mol[LK_index] / B - z_HK_distillate = distillate.mol[HK_index] / D - feed_stage = compute_feed_stage_Kirkbride(N, B, D, - feed_HK_over_LK, - z_LK_bottoms, - z_HK_distillate) - design = self.design_results - design['Theoretical feed stage'] = N - feed_stage - design['Theoretical stages'] = N - design['Minimum reflux'] = Rm - design['Reflux'] = R - - def _get_relative_volatilities_LHK(self): - distillate, bottoms = self.outs - LHK = self.LHK - condensate = self.condensate - K_light, K_heavy = distillate.get_molar_composition(LHK) / condensate.get_molar_composition(LHK) - alpha_LHK_distillate = K_light/K_heavy - - boilup = self.boilup - K_light, K_heavy = boilup.get_molar_composition(LHK) / bottoms.get_molar_composition(LHK) - alpha_LHK_distillate = K_light/K_heavy - alpha_LHK_bottoms = K_light/K_heavy - - return alpha_LHK_distillate, alpha_LHK_bottoms - - def _get_feed_quality(self): - feed = self.feed - feed = feed.copy() - H_feed = feed.H - try: dp = feed.dew_point_at_P() - except: pass - else: feed.T = dp.T - feed.phase = 'g' - H_vap = feed.H - try: bp = feed.bubble_point_at_P() - except: pass - else: feed.T = bp.T - feed.phase = 'l' - H_liq = feed.H - q = (H_vap - H_feed) / (H_vap - H_liq) - return q - - def _solve_Underwood_constant(self, alpha_mean, alpha_LK): - q = self._get_feed_quality() - z_f = self.ins[0].get_normalized_mol(self._IDs_vle) - args = (q, z_f, alpha_mean) - bracket = find_bracket(objective_function_Underwood_constant, - 1, alpha_LK, args=args) - theta = bounded_wegstein(objective_function_Underwood_constant, - *bracket, args=args) - return theta - - def _add_trace_heavy_and_light_non_keys_in_products(self): - distillate, bottoms = self.outs - LNK_index = self._LNK_index - HNK_index = self._HNK_index - bottoms.mol[LNK_index] = LNK_trace = 0.001 * distillate.mol[LNK_index] - distillate.mol[LNK_index] -= LNK_trace - distillate.mol[HNK_index] = HNK_trace = 0.001 * bottoms.mol[HNK_index] - bottoms.mol[HNK_index] -= HNK_trace - - def _estimate_mean_volatilities_relative_to_heavy_key(self): - # Mean volatilities taken at distillate and bottoms product - distillate, bottoms = self.outs - dew_point = self._dew_point - bubble_point = self._bubble_point - IDs = self._IDs_vle - z_distillate = distillate.get_normalized_mol(IDs) - z_bottoms = bottoms.get_normalized_mol(IDs) - dp = dew_point(z_distillate, P=self.P) - bp = bubble_point(z_bottoms, P=self.P) - K_distillate = compute_partition_coefficients(dp.z, dp.x) - K_bottoms = compute_partition_coefficients(bp.y, bp.z) - HK_index = self._LHK_vle_index[1] - alpha_mean = compute_mean_volatilities_relative_to_heavy_key(K_distillate, - K_bottoms, - HK_index) - return alpha_mean - - def _estimate_distillate_recoveries(self): - # Use Hengsteback and Geddes equations - alpha_mean = self._estimate_mean_volatilities_relative_to_heavy_key() - feed = self.feed - distillate, bottoms = self.outs - LHK_index = self._LHK_index - LK_index, HK_index = LHK_index - d_Lr = distillate.mol[LK_index] / feed.mol[LK_index] - b_Hr = bottoms.mol[HK_index] / feed.mol[HK_index] - LHK_vle_index = self._LHK_vle_index - return compute_distillate_recoveries_Hengsteback_and_Gaddes(d_Lr, b_Hr, - alpha_mean, - LHK_vle_index) - - def _update_distillate_recoveries(self, distillate_recoveries): - feed = self.feed - distillate, bottoms = self.outs - IDs = self._IDs_vle - feed_mol = feed.imol[IDs] - distillate.imol[IDs] = distillate_mol = distillate_recoveries * feed_mol - bottoms.imol[IDs] = feed_mol - distillate_mol - - def _solve_distillate_recoveries(self): - distillate_recoveries = self._estimate_distillate_recoveries() - return wegstein(self._recompute_distillate_recovery, - distillate_recoveries, 1e-4) - - def _recompute_distillate_recovery(self, distillate_recoveries): - self._update_distillate_recoveries(distillate_recoveries) - return self._estimate_distillate_recoveries() - - - \ No newline at end of file diff --git a/build/lib/biosteam/units/_shredder.py b/build/lib/biosteam/units/_shredder.py deleted file mode 100644 index afe06e66d..000000000 --- a/build/lib/biosteam/units/_shredder.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon Mar 4 10:57:12 2019 - -@author: yoelr -""" -from .._unit import Unit -from .decorators import cost - -__all__ = ('Shredder',) - -@cost('Flow rate', units='kg/hr', cost=2.5e6, - CE=567.3, n=0.6, S=500e3, kW=3000, BM=1.39) -class Shredder(Unit): pass \ No newline at end of file diff --git a/build/lib/biosteam/units/_solids_centrifuge.py b/build/lib/biosteam/units/_solids_centrifuge.py deleted file mode 100644 index 93c8e5248..000000000 --- a/build/lib/biosteam/units/_solids_centrifuge.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 23 22:18:36 2018 - -@author: yoelr -""" -import numpy as np -from ..utils.unit_warnings import lb_warning -from .decorators import cost -from ._splitter import Splitter - -__all__ = ('SolidsCentrifuge',) - -@cost('Solids loading', cost=68040, CE=567, n=0.50, ub=40, BM=2.03, - N='Number of centrifuges') -class SolidsCentrifuge(Splitter): - """ - Create a solids centrifuge that separates out solids according to - user defined split. Assume a continuous scroll solid bowl. - - Parameters - ---------- - ins : stream - Inlet fluid with solids. - outs : stream sequence - * [0] Liquid-rich stream. - * [1] Solids-rich stream. - split: array_like - Component splits to 0th output stream - order=None : Iterable[str] - Species order of split. Defaults to Stream.species.IDs. - solids : tuple[str] - IDs of solids. - - References - ---------- - .. [0] 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. - - """ - _units = {'Solids loading': 'tonne/hr'} - _minimum_solids_loading = 2 - - def __init__(self, ID='', ins=None, outs=(), thermo=None, *, - split, order=None, solids=None): - super().__init__(ID, ins, outs, thermo, split=split, order=order) - self.solids = solids - - @property - def solids(self): - return self._solids - @solids.setter - def solids(self, solids): - self._solids = tuple(solids) - - def _design(self): - mass_solids = 0 - solids = self._solids - for s in self.ins: - mass_solids += s.imass[solids] - ts = np.asarray(mass_solids).sum() # Total solids - ts *= 0.0011023 # To short tons (2000 lbs/hr) - self.design_results['Solids loading'] = ts - lb = self._minimum_solids_loading - if ts < lb: - lb_warning('Solids loading', ts, 'tonn/hr', lb, 3, self) - diff --git a/build/lib/biosteam/units/_solids_separator.py b/build/lib/biosteam/units/_solids_separator.py deleted file mode 100644 index 696860bd4..000000000 --- a/build/lib/biosteam/units/_solids_separator.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 23 22:14:01 2018 - -@author: yoelr -""" -from ._splitter import Splitter, run_split_with_mixing -from ._CAS import H2O_CAS - -__all__ = ('SolidsSeparator',) - -class SolidsSeparator(Splitter): - """Create SolidsSeparator object. - - Parameters - ---------- - ins : stream - Inlet fluid with solids. - outs : stream sequnece - * [0] Retentate. - * [1] Permeate. - split : array_like - Component splits to 0th output stream - moisture_content : float - Fraction of water in solids - - """ - _N_ins = 2 - - def __init__(self, ID='', ins=None, outs=(), thermo=None, *, - order=None, split, moisture_content): - Splitter.__init__(self, ID, ins, outs, thermo, order=order, split=split) - #: Moisture content of retentate - self.mositure_content = moisture_content - assert self.isplit[H2O_CAS] == 0, 'cannot define water split, only moisture content' - - def _run(self): - run_split_with_mixing(self) - retentate, permeate = self.outs - solids = retentate.F_mass - mc = self.mositure_content - retentate.imol[H2O_CAS] = water = (solids * mc/(1-mc))/18.01528 - permeate.imol[H2O_CAS] -= water - if permeate.imol[H2O_CAS] < water: - raise ValueError(f'not enough water for {repr(self)}') - diff --git a/build/lib/biosteam/units/_splitter.py b/build/lib/biosteam/units/_splitter.py deleted file mode 100644 index aa45b13ff..000000000 --- a/build/lib/biosteam/units/_splitter.py +++ /dev/null @@ -1,242 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon May 20 22:04:02 2019 - -@author: yoelr -""" -from .. import Unit -from .._graphics import splitter_graphics -import numpy as np -from thermosteam.indexer import ChemicalIndexer -from ._process_specification import ProcessSpecification - -__all__ = ('run_split', 'run_split_with_mixing', 'Splitter', 'FakeSplitter', - 'ReversedSplitter', 'ReversedSplit') - - -def run_split(self): - """Splitter mass and energy balance function based on one input stream.""" - top, bot = self.outs - feed = self.ins[0] - top_mol = top.mol - feed_mol = feed.mol - top_mol[:] = feed_mol * self.split - bot.mol[:] = feed_mol - top_mol - bot.T = top.T = feed.T - bot.P = top.P = feed.P - -def run_split_with_mixing(self): - """Splitter mass and energy balance function with mixing all input streams.""" - top, bot = self.outs - ins = self.ins - top.mix_from(ins) - bot.copy_like(top) - top_mol = top.mol - top_mol[:] *= self.split - bot.mol[:] -= top_mol - - -class Splitter(Unit): - """ - Create a splitter that separates mixed streams based on splits. - - Parameters - ---------- - ins : stream - Inlet fluid to be split. - outs : stream sequence - * [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: - - .. code-block:: python - - >>> from biosteam import units, settings, Stream - >>> settings.set_thermo(['Water', 'Ethanol']) - >>> 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: - - .. code-block:: python - - >>> 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: - - .. code-block:: python - - >>> 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 - - """ - _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): - """[Array] Componentwise split of feed to 0th outlet stream.""" - return self._isplit._data - - def __init__(self, ID='', ins=None, outs=(), thermo=None, *, split, order=None): - Unit.__init__(self, ID, ins, outs, thermo) - chemicals = self.thermo.chemicals - if isinstance(split, dict): - assert not order, "cannot pass 'other' key word argument when split is a dictionary" - order, split = zip(*split.items()) - - if order: - isplit = chemicals.iarray(order, split) - elif hasattr(split, '__len__'): - isplit = ChemicalIndexer.from_data(np.asarray(split), - phase=None, - chemicals=chemicals) - else: - split = split * np.ones(chemicals.size) - isplit = ChemicalIndexer.from_data(split, - phase=None, - chemicals=chemicals) - self._isplit = isplit - - def _run(self): - top, bot = self.outs - feed, = self.ins - feed_mol = feed.mol - bot.T = top.T = feed.T - bot.P = top.P = feed.P - bot.phase = top.phase = feed.phase - top.mol[:] = top_mol = feed_mol * self.split - bot.mol[:] = feed_mol - top_mol - - -class FakeSplitter(Unit): - """ - Create a FakeSplitter object that does nothing when simulated. - """ - _graphics = Splitter._graphics - _N_ins = 1 - _N_outs = 2 - _outs_size_is_fixed = False - - def _run(self): pass - - def create_reversed_splitter_process_specification(self, ID='', ins=None, outs=(), - description=None): - return ProcessSpecification(ID, ins, outs, self.thermo, - specification=ReversedSplit(self), - description=description or 'Reverse split') - -FakeSplitter.line = 'Splitter' - -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 become the same temperature, pressure and phase as the input.""" - _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) - -class ReversedSplit: - """Create a splitter that, when called, sets the inlet stream based on outlet streams. Must have only one input stream. The outlet streams will become the same temperature, pressure and phase as the input.""" - __slots__ = ('splitter',) - - def __init__(self, splitter): - self.splitter = splitter - - @property - def __name__(self): - return 'ReversedSplit' - - def __call__(self): - splitter = self.splitter - inlet, = splitter.ins - outlets = splitter.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 - - - \ No newline at end of file diff --git a/build/lib/biosteam/units/_tank.py b/build/lib/biosteam/units/_tank.py deleted file mode 100644 index da27eb33c..000000000 --- a/build/lib/biosteam/units/_tank.py +++ /dev/null @@ -1,296 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 23 15:47:26 2018 - -@author: yoelr -""" -from .design_tools.specification_factors import vessel_material_factors -from .design_tools.tank_design import ( - compute_number_of_tanks_and_total_purchase_cost, - storage_tank_purchase_cost_algorithms, - mix_tank_purchase_cost_algorithms) -from ..utils import ExponentialFunctor -from .._unit import Unit -from ._mixer import Mixer -import biosteam as bst - -from warnings import warn - -__all__ = ('Tank', 'MixTank', 'StorageTank') - -# %% - -class Tank(Unit, isabstract=True): - r""" - Abstract class for tanks. - - Attributes - ---------- - vessel_type : str - Vessel type. - tau : float - Residence time [hr]. - V_wf : float - Fraction of working volume over total volume. - vessel_material : str - Vessel material. - - Notes - ----- - The total volume [:math:`m^3`] is given by: - - :math:`V_{total} = \frac{\tau \cdot Q }{V_{wf}}` - - Where :math:`\tau` is the residence time [:math:`hr`], - :math:`Q` is the flow rate [:math:`\frac{m^3}{hr}`], - and :math:`V_{wf}` is the fraction of working volume over total volume. - - The number of tanks is given by: - - :math:`N = Ceil \left( \frac{V_{total}}{V_{max}} \right)` - - Where :math:`V_{max}` is the maximum volume of a tank depends on the - vessel type. - - The volume [:math:`m^3`] of each tank is given by: - - :math:`V = \frac{V_{total}}{N}` - - The purchase cost will depend on the vessel type. - - Child classes should implement the following class attributes and methods: - - purchase_cost_algorithms : dict[str: VesselPurchaseCostAlgorithm] - All purchase cost algorithms options for selected vessel types. - - """ - _units = {'Total volume': 'm^3'} - _N_outs = 1 - - def __init_subclass__(cls, isabstract=False): - if not isabstract: - if not hasattr(cls, 'purchase_cost_algorithms'): - raise NotImplementedError("Tank subclass must implement " - "a 'purchase_cost_algorithms' dictionary") - super().__init_subclass__(isabstract) - - @property - def vessel_material(self): - return self._vessel_material - @vessel_material.setter - def vessel_material(self, material): - self._vessel_material = material - default_material = self.purchase_cost_algorithm.material - if material == default_material: - self._F_M = 1.0 - else: - try: - F_M_new = vessel_material_factors[material] - except: - raise ValueError("no material factor available for " - f"vessel construction material '{material}';" - "only the following materials are " - f"available: {', '.join(vessel_material_factors)}") - try: - F_M_old = vessel_material_factors[default_material] - except KeyError: - raise ValueError(f"only '{default_material}' is a valid " - "material for given vessel type") - self._F_M = F_M_new / F_M_old - - @property - def vessel_type(self): - return self._vessel_type - @vessel_type.setter - def vessel_type(self, vessel_type): - if vessel_type in self.purchase_cost_algorithms: - self._vessel_type = vessel_type - self.purchase_cost_algorithm = self.purchase_cost_algorithms[vessel_type] - else: - raise ValueError(f"vessel type '{vessel_type}'" - "is not avaiable; only the following vessel " - "types are available: " - f"{', '.join(self.purchase_cost_algorithms)}") - - def _design(self): - design_results = self.design_results - design_results['Residence time'] = tau = self.tau - design_results['Total volume'] = tau * self.F_vol_out / self.V_wf - - def _cost(self): - N, Cp = compute_number_of_tanks_and_total_purchase_cost( - self.design_results['Total volume'], - self.purchase_cost_algorithm, - self._F_M) - self.design_results['Number of tanks'] = N - self.purchase_costs['Tanks'] = Cp - - -# %% Storage tank purchase costs - - -class StorageTank(Tank): - r""" - Create a storage tank with a purchase cost based on volume as given by residence time. - - Parameters - ---------- - ins : stream - Inlet. - outs : stream - Outlet. - vessel_type : str, optional - Vessel type. Defaults to 'Field erected'. - tau=672 : float - Residence time [hr]. - V_wf=1 : float - Fraction of working volume over total volume. - vessel_material : str, optional - Vessel material. Defaults to 'Stainless steel'. - - Notes - ----- - For a detailed discussion on the design and cost algorithm, - please read the :doc:`Tank` documentation. - - References to the purchase cost algorithms for each available - vessel type are as follows: - - * Field erected: [1]_ - * Floating roof: [2]_ - * Cone roof: [2]_ - - Examples - -------- - Create a carbon steel, floating roof storage tank for the storage of bioethanol: - - >>> from biosteam import units, settings, Stream - >>> settings.set_thermo(['Ethanol']) - >>> feed = Stream('feed', Ethanol=23e3, units='kg/hr') - >>> effluent = Stream('effluent') - >>> T1 = units.StorageTank('T1', ins=feed, outs=effluent, - ... tau=7*24, # In hours - ... vessel_type='Floating roof', - ... vessel_material='Carbon steel') - >>> T1.simulate() - >>> T1.show(flow='kg/hr') - StorageTank: T1 - ins... - [0] feed - phase: 'l', T: 298.15 K, P: 101325 Pa - flow (kg/hr): Ethanol 2.3e+04 - outs... - [0] effluent - phase: 'l', T: 298.15 K, P: 101325 Pa - flow (kg/hr): Ethanol 2.3e+04 - >>> T1.results() - Storage tank Units T1 - Design Residence time 168 - Total volume m^3 5.19e+03 - Number of tanks 2 - Purchase cost Tanks USD 8.65e+05 - Total purchase cost USD 8.65e+05 - Utility cost USD/hr 0 - - - References - ---------- - .. [1] Apostolakou, A. A., Kookos, I. K., Marazioti, C., Angelopoulos, K. C. - (2009). Techno-economic analysis of a biodiesel production process from - vegetable oils. Fuel Processing Technology, 90(7–8), 1023–1031. - https://doi.org/10.1016/j.fuproc.2009.04.017 - .. [2] Seider, W. D.; Lewin, D. R.; Seader, J. D.; Widagdo, S.; Gani, R.; - Ng, M. K. Cost Accounting and Capital Cost Estimation. - In Product and Process Design Principles; Wiley, 2017; pp 426–485. - - """ - def __init__(self, ID='', ins=None, outs=(), thermo=None, *, - vessel_type='Field erected', tau=4*7*24, - V_wf=1.0, vessel_material='Stainless steel'): - Unit.__init__(self, ID, ins, outs, thermo) - - # [str] Vessel type. - self.vessel_type = vessel_type - - #: [float] Residence time in hours. - self.tau = tau - - #: [float] Fraction of working volume to total volume. - self.V_wf = V_wf - - #: [str] Vessel construction material. - self.vessel_material = vessel_material - - #: dict[str: VesselPurchaseCostAlgorithm] All cost algorithms available for vessel types. - purchase_cost_algorithms = storage_tank_purchase_cost_algorithms - - -class MixTank(Tank): - """ - Create a mixing tank with volume based on residence time. - - Parameters - ---------- - ins : streams - Inlet fluids to be mixed. - outs : stream - Outlet. - vessel_type : str - Vessel type. - tau=1 : float - Residence time [hr]. - V_wf=0.8 : float - Fraction of working volume over total volume. - vessel_material : str, optional - Vessel material. Defaults to 'Stainless steel'. - kW_per_m3=0.0985 : float - Electricity requirement per unit volume [kW/m^3]. - - Notes - ----- - For a detailed discussion on the design and cost algorithm, - please read the :doc:`Tank` documentation. - - The purchase cost algorithm is based on [1]_. - - References - ---------- - .. [1] Apostolakou, A. A., Kookos, I. K., Marazioti, C., Angelopoulos, K. C. - (2009). Techno-economic analysis of a biodiesel production process from - vegetable oils. Fuel Processing Technology, 90(7–8), 1023–1031. - https://doi.org/10.1016/j.fuproc.2009.04.017 - - """ - _N_ins = 2 - _run = Mixer._run - - def __init__(self, ID='', ins=None, outs=(), thermo=None, *, - vessel_type="Conventional", tau=1, - V_wf=0.8, vessel_material='Stainless steel', - kW_per_m3=0.0985): - Unit.__init__(self, ID, ins, outs, thermo) - - # [float] Electricity requirement per unit volume [kW/m^3]. - self.kW_per_m3 = kW_per_m3 - - # [str] Vessel type. - self.vessel_type = vessel_type - - #: [float] Residence time in hours. - self.tau = tau - - #: [float] Fraction of working volume to total volume. - self.V_wf = V_wf - - #: [str] Vessel construction material. - self.vessel_material = vessel_material - - #: dict[str: VesselPurchaseCostAlgorithm] All cost algorithms available for vessel types. - purchase_cost_algorithms = mix_tank_purchase_cost_algorithms - - def _cost(self): - super()._cost() - self.power_utility(self.kW_per_m3 * self.design_results['Total volume']) - -MixTank._graphics.edge_in *= 3 -MixTank._graphics.edge_out *= 3 diff --git a/build/lib/biosteam/units/_transesterification.py b/build/lib/biosteam/units/_transesterification.py deleted file mode 100644 index c26d8c9e4..000000000 --- a/build/lib/biosteam/units/_transesterification.py +++ /dev/null @@ -1,120 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 23 22:49:58 2018 - -@author: yoelr -""" -from .. import Unit -from .decorators import cost -from thermosteam.reaction import Reaction, ParallelReaction - -__all__ = ('Transesterification',) - -@cost('Volume', 'Reactor', - CE=525.4, cost=15000, n=0.55, kW=1.5, BM=4.3,) -class Transesterification(Unit): - """ - Create a transesterification reactor that converts 'Lipid' and 'Methanol' - to 'Biodiesel' and 'Glycerol'. Finds the amount of catalyst 'NaOCH3' - required and consumes it to produce 'NaOH' and 'Methanol'. - - Parameters - ---------- - ins : stream sequence - * [0] Lipid feed - * [1] Methanol feed (includes catalyst) - outs : stream - Reactor effluent. - efficiency : float - Efficiency of conversion (on a 'Lipid' basis). - methanol2lipid : float - Methanol feed to lipid molar ratio. - catalyst_molfrac : float - Catalyst to methanol molar ratio. - T : float - Operating temperature [K]. - - """ - _bounds = {'Volume': (0.1, 20)} - _units = {'Volume': 'm^3'} - _tau = 1 - _N_ins = 2 - _N_outs = 1 - _N_heat_utilities = 1 - - def _more_design_specs(self): - return (('Residence time', self.tau, 'hr'), - ('Conversion efficiency', self.efficiency, ''), - ('Working volume fraction', 0.8, '')) - - def __init__(self, ID='', ins=None, outs=(), *, - efficiency, methanol2lipid, T, catalyst_molfrac): - Unit.__init__(self, ID, ins, outs) - #: [:class:`~thermosteam.ParallelReaction`] Transesterification and catalyst consumption reaction - self.reaction = ParallelReaction([ - Reaction('Lipid + 3Methanol -> 3Biodiesel + Glycerol', - reactant='Lipid', X=efficiency), - Reaction('NaOCH3 -> NaOH + Methanol', - reactant='NaOCH3', X=1)]) - chemicals = self.chemicals - self._methanol_composition = chemicals.kwarray( - dict(Methanol=1-catalyst_molfrac, - NaOCH3=catalyst_molfrac)) - self._lipid_index, self._methanol_index, self._catalyst_index = \ - chemicals.get_index(('Lipid', 'Methanol', 'NaOCH3')) - self._methanol2lipid = methanol2lipid - self.T = T #: Operating temperature (K). - - @property - def tau(self): - """Residence time (hr).""" - return self._tau - @tau.setter - def tau(self, tau): - self._tau = tau - - @property - def efficiency(self): - """Transesterification efficiency.""" - return self.reaction.X[0] - @efficiency.setter - def efficiency(self, efficiency): - self.reaction.X[0] = efficiency - - @property - def methanol2lipid(self): - """Methanol feed to lipid molar ratio.""" - return self._methanol2lipid - @methanol2lipid.setter - def methanol2lipid(self, ratio): - self._methanol2lipid = ratio - - @property - def catalyst_molfrac(self): - """Catalyst molar fraction in methanol feed.""" - return self._methanol_composition[self._catalyst_index] - @catalyst_molfrac.setter - def catalyst_molfrac(self, molfrac): - meoh = self._methanol_composition - meoh[self._catalyst_index] = molfrac - meoh[self._methanol_index] = 1-molfrac - - def _run(self): - feed, methanol = self.ins - product, = self.outs - methanol.mol[:] = (self._methanol_composition - * feed.mol[self._lipid_index] - * self._methanol2lipid) - product.mol[:] = feed.mol + methanol.mol - self.reaction(product.mol) - product.T = self.T - - def _design(self): - effluent = self._outs[0] - self.design_results['Volume'] = self._tau * effluent.F_vol / 0.8 - self.heat_utilities[0](self.Hnet, effluent.T) - - - - - diff --git a/build/lib/biosteam/units/_vent_scrubber.py b/build/lib/biosteam/units/_vent_scrubber.py deleted file mode 100644 index 650a8296a..000000000 --- a/build/lib/biosteam/units/_vent_scrubber.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed Jul 17 01:11:46 2019 - -@author: yoelr -""" -from .decorators import cost -from .. import Unit - -__all__ = ('VentScrubber',) - -@cost('Flow rate', units='kg/hr', - S=22608, CE=522, cost=215e3, n=0.6, BM=2.4) -class VentScrubber(Unit): - _N_ins = _N_outs = 2 - _units = {'Flow rate': 'kg/hr'} - def __init__(self, ID='', ins=None, outs=(), thermo=None, *, gas): - Unit.__init__(self, ID, ins, outs, thermo) - self.gas = gas - - def _run(self): - water, vent_entry = self.ins - vent_exit, bottoms = self.outs - vent_exit.copy_like(vent_entry) - bottoms.copy_flow(vent_exit, self.gas, - remove=True, exclude=True) - bottoms.mol[:] += water.mol - - def _design(self): - self.design_results['Flow rate'] = self._outs[0].F_mass \ No newline at end of file diff --git a/build/lib/biosteam/units/_vibrating_screen.py b/build/lib/biosteam/units/_vibrating_screen.py deleted file mode 100644 index e906ba3ad..000000000 --- a/build/lib/biosteam/units/_vibrating_screen.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon Mar 4 20:51:22 2019 - -@author: yoelr -""" -from .decorators import cost -from ._splitter import Splitter - -__all__ = ('VibratingScreen',) - -@cost('Area', units='ft^2', ub=200, CE=567, cost=1010, n=0.91, BM=1.73, N='Number of screens', - fsize=lambda self, _: self.ins[0].F_mass/(self.capacity*self.mesh_opening)) -class VibratingScreen(Splitter): - # Assume 3-deck vibrating screen - - #: Flow rate per area of screen per apareture (kg∕ft2-hr-mm) - capacity = 0.2*907.18474 - - #: Mesh opening (mm) - mesh_opening = 8 \ No newline at end of file diff --git a/build/lib/biosteam/units/decorators/__init__.py b/build/lib/biosteam/units/decorators/__init__.py deleted file mode 100644 index 72c823c88..000000000 --- a/build/lib/biosteam/units/decorators/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed May 1 19:05:53 2019 - -@author: yoelr -""" -__all__ = [] - -from ._cost import * -from ._design import * - -from . import _cost -from . import _design - -__all__.extend(_cost.__all__) -__all__.extend(_design.__all__) \ No newline at end of file diff --git a/build/lib/biosteam/units/decorators/_cost.py b/build/lib/biosteam/units/decorators/_cost.py deleted file mode 100644 index 6b939584d..000000000 --- a/build/lib/biosteam/units/decorators/_cost.py +++ /dev/null @@ -1,194 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed May 1 19:05:53 2019 - -@author: yoelr -""" -import biosteam as bst -import copy -from ._design import design -from math import ceil - -__all__ = ('cost', 'add_cost', 'CostItem') - -class CostItem: - """ - Create a CostItem object which defines exponential scaling factors for an item's purchase cost. - - Parameters - ---------- - basis : str - Name of size parameter used for scaling. - units : str - Units of measure. - S : float - Size. - ub : float - Size limit. - CE : float - Chemical engineering plant cost index. - cost : float - Purchase cost of item. - n : float - Exponential factor. - kW : float - Electricity rate. - BM : float - Bare module factor (installation factor). - N : str - Attribute name for number of parallel units. - - """ - __slots__ = ('_basis', '_units', 'S', 'ub', 'CE', - 'cost', 'n', 'kW', 'BM', 'N') - def __init__(self, basis, units, S, ub, CE, cost, n, kW, BM, N): - if ub and not N: N='#' - self._basis = basis - self._units = units - self.S = S - self.ub = ub - self.CE = CE - self.cost = cost - self.n = n - self.kW = kW - self.BM = BM - self.N = N - - __getitem__ = object.__getattribute__ - __setitem__ = object.__setattr__ - - @property - def basis(self): - return self._basis - @property - def units(self): - return self._units - - def __repr__(self): - return f"<{type(self).__name__}: {self._basis} ({self._units})>" - - def _ipython_display_(self): - print(f"{type(self).__name__}: {self._basis} ({self._units})\n" - +f" S {self.S:.3g}\n" - +f" ub {self.ub:.3g}\n" - +f" CE {self.CE:.3g}\n" - +f" cost {self.cost:.3g}\n" - +f" n {self.n:.3g}\n" - +f" kW {self.kW:.3g}\n" - +f" BM {self.BM:.3g}\n" - +(f" N '{self.N}'" if self.N else "")) - show = _ipython_display_ - -def _cost(self): - D = self.design_results - C = self.purchase_costs - kW = 0 - for i, x in self.cost_items.items(): - S = D[x._basis] - if x.ub: - D[x.N or '#' + i] = N = ceil(S/x.ub) - q = S/x.S - F = q/N - C[i] = N*bst.CE/x.CE*x.cost*F**x.n - kW += x.kW*q - elif x.N: - N = getattr(self, x.N) - F = S/x.S - C[i] = N*bst.CE/x.CE*x.cost*F**x.n - kW += N*x.kW*F - else: - F = S/x.S - C[i] = bst.CE/x.CE*x.cost*F**x.n - kW += x.kW*F - if kW: self.power_utility(kW) - -def _extended_cost(self): - _cost(self) - self._end_decorated_cost_() - -@property -def BM(self): - raise AttributeError("can only get installation factor 'BM' through 'cost_items'") -@BM.setter -def BM(self, BM): - raise AttributeError("can only set installation factor 'BM' through 'cost_items'") -BM_property = BM -del BM - -@property -def installation_cost(self): - C = self.purchase_costs - return sum([C[i]*j.BM for i,j in self.cost_items.items()]) - -def cost(basis, ID=None, *, CE, cost, n, S=1, ub=0, kW=0, BM=1, units=None, fsize=None, N=None): - r""" - Add item (free-on-board) purchase cost based on exponential scale up: - - Parameters - ---------- - basis : str - Name of size parameter used for scaling. - ID : str - Name of purchase item. - CE : float - Chemical engineering plant cost index. - cost : float - Purchase cost of item. - n : float - Exponential factor. - S = 1 : float - Size. - ub : float - Size limit. - kW : float - Electricity rate. - BM = 1: float - Bare module factor (installation factor). - units = None : str, optional - Units of measure. - fsize = None : function, optional - Accepts a Unit object argument and returns the size parameter. If None, defaults to function predefined for given name and units of measure. - N : str - Attribute name for number of parallel units. - - Examples - -------- - :doc:`../../tutorial/Unit_decorators` - - """ - - return lambda cls: add_cost(cls, ID, basis, units, S, ub, CE, cost, n, kW, BM, fsize, N) - -def add_cost(cls, ID, basis, units, S, ub, CE, cost, n, kW, BM, fsize, N): - if kW: cls._has_power_utility = True - if basis in cls._units: - if fsize: - raise RuntimeError(f"cost basis '{basis}' already defined in class, cannot pass 'fsize' argument") - elif not units: - units = cls._units[basis] - elif units != cls._units[basis]: - raise RuntimeError(f"cost basis '{basis}' already defined in class with units '{cls._units[basis]}'") - elif units: - design._add_design2cls(cls, basis, units, fsize) - else: - raise RuntimeError(f"units of cost basis '{basis}' not available in '{cls.__name__}._units' dictionary, must pass units or define in class") - if hasattr(cls, 'cost_items'): - if 'cost_items' not in cls.__dict__: - cls.cost_items = copy.deepcopy(cls.cost_items) - if not ID: - raise ValueError("must pass an 'ID' for purchase cost item") - if ID in cls.cost_items: - raise ValueError(f"ID '{ID}' already in use") - cls.cost_items[ID] = CostItem(basis, units, S, ub, CE, cost, n, kW, BM, N) - else: - ID = ID or cls.line - cls.cost_items = {ID: CostItem(basis, units, S, ub, CE, cost, n, kW, BM, N)} - if '_cost' not in cls.__dict__: - if hasattr(cls, '_end_decorated_cost_'): - cls._cost = _extended_cost - else: - cls._cost = _cost - cls.BM = BM_property - cls.installation_cost = installation_cost - return cls - diff --git a/build/lib/biosteam/units/decorators/_design.py b/build/lib/biosteam/units/decorators/_design.py deleted file mode 100644 index 983273f94..000000000 --- a/build/lib/biosteam/units/decorators/_design.py +++ /dev/null @@ -1,157 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon May 6 17:19:41 2019 - -@author: yoelr -""" -__all__ = ('design', 'add_design') - - -# -*- coding: utf-8 -*- -""" -Created on Mon Jun 17 21:18:50 2019 - -@author: yoelr -""" -from thermosteam.base import stream_units_of_measure - -__all__ = ('design',) - -# %% Design Center class - -def _design(self): - D = self.design_results - U = self._units - for i, j in self._design_basis_: D[i] = j(self, U[i]) - -class DesignCenter: - """Create a DesignCenter object that manages all design basis functions. When called, it returns a Unit class decorator that adds a design item to the given Unit class.""" - __slots__ = ('design_basis_functions',) - - def __init__(self): - self.design_basis_functions = {} - - def define(self, design_basis): - """Define a new design basis. - - Parameters - ---------- - - design_basis : function - Should accept the unit_object and the units_of_measure and return design basis value. - - .. Note:: - - Design basis is registered with the name of the design basis function. - - """ - name = design_basis.__name__.replace('_', ' ').capitalize() - functions = self.design_basis_functions - if name in functions: raise ValueError(f"design basis '{name}' already implemented") - functions[name] = design_basis - return design_basis - - def __call__(self, name, units, fsize=None): - """ - Return a Unit class decorator that adds a size/design requirement to the class. - - Parameters - ---------- - name : str - Name of design item - units : str - Units of measure of design item. - fsize : function - Should return design item given the Unit object. If None, defaults to function predefined for given name and units. - - """ - return lambda cls: self._add_design2cls(cls, name, units, fsize) - - def _add_design2cls(self, cls, name, units, fsize): - """ - Add size/design requirement to class. - - Parameters - ---------- - cls : Unit class. - name : str - Name of design item. - units : str - Units of measure of design item. - fsize : function - Should return design item given the Unit object. If None, defaults to function predefined for given name and units. - - Examples - -------- - - :doc:`Unit decorators` - - """ - f = fsize or self.design_basis_functions[name.capitalize()] - - # Make sure new _units dictionary is defined - if not cls._units: - cls._units = {} - elif '_units' not in cls.__dict__: - cls._units = cls._units.copy() - - # Make sure design basis is not defined - if name in cls._units: - raise RuntimeError(f"design basis '{name}' already defined in class") - else: - cls._units[name] = units - - # Add design basis - if cls._design is _design: - cls._design_basis_.append((name, f)) - elif '_design' in cls.__dict__: - raise RuntimeError("'_design' method already implemented") - else: - cls._design_basis_ = [(name, f)] - cls._design = _design - - return cls - - def __contains__(self, basis): - return basis in self.design_basis_functions - - def __iter__(self): - yield from self.design_basis_functions - - def __repr__(self): - return f"<{type(self).__name__}: {', '.join(self)}>" - -# %% Design factories - -design = DesignCenter() #: Used to decorate classes with new design item - -@design.define -def flow_rate(self, units): - if self._N_ins == 1: - return self._ins[0].get_total_flow(units) - elif self._N_outs == 1: - return self._outs[0].get_total_flow(units) - elif self._N_ins < self._N_outs: - return sum([i.get_total_flow(units) for i in self._ins]) - else: - return sum([i.get_total_flow(units) for i in self._outs]) - -H_units = stream_units_of_measure['H'] - -@design.define -def duty(self, units): - self._duty = duty = self.H_out - self.H_in - self.heat_utilities[0](duty, self.ins[0].T, self.outs[0].T) - return H_units.conversion_factor(units) * duty - -@design.define -def dry_flow_rate(self, units): - ins = self._ins - flow_in = sum([i.get_total_flow(units) for i in ins]) - moisture = sum([i.get_flow(units, '7732-18-5') for i in ins]) - return flow_in - moisture - -del flow_rate, duty, dry_flow_rate - - - diff --git a/build/lib/biosteam/units/design_tools/__init__.py b/build/lib/biosteam/units/design_tools/__init__.py deleted file mode 100644 index aaed1b0ed..000000000 --- a/build/lib/biosteam/units/design_tools/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed Mar 6 16:46:04 2019 - -@author: yoelr -""" - - -from . import vacuum -from . import flash_vessel_design -from . import cost_index -from . import batch -from . import specification_factors -from . import column_design -from . import heat_transfer -from . import tank_design - -__all__ = (*cost_index.__all__, - *vacuum.__all__, - *flash_vessel_design.__all__, - *batch.__all__, - *specification_factors.__all__, - *column_design.__all__, - *heat_transfer.__all__, - *tank_design.__all__, -) - -from .specification_factors import * -from .column_design import * -from .vacuum import * -from .flash_vessel_design import * -from .cost_index import * -from .batch import * -from .heat_transfer import * -from .tank_design import * \ No newline at end of file diff --git a/build/lib/biosteam/units/design_tools/batch.py b/build/lib/biosteam/units/design_tools/batch.py deleted file mode 100644 index 0f02dcfb0..000000000 --- a/build/lib/biosteam/units/design_tools/batch.py +++ /dev/null @@ -1,57 +0,0 @@ -# -*- coding: utf-8 -*- -""" -General functional algorithms for batch design. -""" - -__all__ = ('size_batch',) - -def size_batch(F_vol, tau_reaction, tau_cleaning, N_reactors, V_wf) -> dict: - """ - Solve for batch reactor volume, cycle time, and loading time. - - Parameters - ---------- - F_vol : float - Volumetric flow rate. - tau_reaction : float - Reaction time. - tau_cleaning : float - Cleaning in place time. - N_reactors : int - Number of reactors. - V_wf : float - Fraction of working volume. - - Returns - ------- - dict - * 'Reactor volume': float - * 'Batch time': float - * 'Loading time': float - - Notes - ----- - Units of measure may vary so long as they are consistent. The loading time - can be considered the cycle time. - - """ - # Total volume of all reactors, assuming no downtime - V_T = F_vol * (tau_reaction + tau_cleaning) / (1 - 1 / N_reactors) - - # Volume of an individual reactor - V_i = V_T/N_reactors - - # Time required to load a reactor - tau_loading = V_i/F_vol - - # Total batch time - tau_batch = tau_reaction + tau_cleaning + tau_loading - - # Account for excess volume - V_i /= V_wf - - return {'Reactor volume': V_i, - 'Batch time': tau_batch, - 'Loading time': tau_loading} - - \ No newline at end of file diff --git a/build/lib/biosteam/units/design_tools/column_design.py b/build/lib/biosteam/units/design_tools/column_design.py deleted file mode 100644 index b340ff06f..000000000 --- a/build/lib/biosteam/units/design_tools/column_design.py +++ /dev/null @@ -1,441 +0,0 @@ -# -*- coding: utf-8 -*- -""" -General functional algorithms for the design and purchase cost estimation -of distillation columns. - -References ----------- -.. [1] Seider, W. D., Lewin, D. R., Seader, J. D., Widagdo, S., Gani, R., - & Ng, M. K. (2017). Product and Process Design Principles. Wiley. - Cost Accounting and Capital Cost Estimation (Chapter 16) -.. [2] M. Duss, R. Taylor. (2018) - Predict Distillation Tray Efficiency. AICHE -.. [3] Green, D. W. Distillation. In Perry’s Chemical Engineers’ - 674 Handbook, 9 ed.; McGraw-Hill Education, 2018. - -""" -import numpy as np -from ...utils import approx2step -import biosteam as bst - -__all__ = ('compute_purchase_cost_of_trays', - 'compute_purchase_cost_of_tower', - 'compute_empty_tower_cost', - 'compute_plaform_ladder_cost', - 'compute_tower_weight', - 'compute_tower_wall_thickness', - 'compute_tray_base_purchase_cost', - 'compute_n_trays_factor', - 'compute_murphree_stage_efficiency', - 'compute_flow_parameter', - 'compute_max_capacity_parameter', - 'compute_max_vapor_velocity', - 'compute_downcomer_area_fraction', - 'compute_tower_diameter', - 'compute_tower_height') - -# Minimum thickness data -x = np.array((4, 6, 8, 10, 12)) -y = np.array((1/4, 5/16, 3/8, 7/16, 1/2)) -ts_min_p = np.polyfit(x,y,1) - -def compute_purchase_cost_of_trays(N_T, Di, F_TT, F_TM): - """ - Return total cost of all trays at BioSTEAM's CEPCI. - - Parameters - ---------- - N_T : int - Number of trays. - Di : float - Inner diameter [ft]. - F_TT : float - Tray type factor. - F_TM : float - Tray material factor. - - Notes - ----- - The purchase cost is given by [1]_. See source code for details. - The purchase cost is scaled according to BioSTEAM's Chemical - Plant Cost Index, `biosteam.CE`. - - """ - F_CE = bst.CE/500 - C_BT = compute_tray_base_purchase_cost(Di) - F_NT = compute_n_trays_factor(N_T) - return N_T * F_CE * F_NT * F_TT * F_TM * C_BT - -def compute_purchase_cost_of_tower(Di, L, W, F_VM): - """ - Return cost of tower at BioSTEAM's CEPCI. - - Parameters - ---------- - Di : float - Inner diameter [ft] - L : float - length [ft] - W : float - weight [lb]. - F_VM : float - Tower material factor. - - Notes - ----- - The purchase cost is given by [1]_. See source code for details. - The purchase cost is scaled according to BioSTEAM's Chemical - Plant Cost Index, `biosteam.CE`. - - """ - F_CE = bst.CE/500 - C_V = compute_empty_tower_cost(W) - C_PL = compute_plaform_ladder_cost(Di, L) - return F_CE * (F_VM * C_V + C_PL) - -def compute_empty_tower_cost(W): - """ - Return the cost [C_V; in USD] of an empty tower vessel assuming a CE of 500. - - Parameters - ---------- - W : float - Weight [lb]. - - - Notes - ----- - The purchase cost is given by [1]_. See source code for details. - - """ - return np.exp(7.2756 + 0.18255*np.log(W) + 0.02297*np.log(W)**2) - -def compute_plaform_ladder_cost(Di, L): - """ - Return the cost [C_PL; in USD] of platforms and ladders assuming a CE of 500. - - Parameters - ---------- - Di: float - Inner diameter [ft]. - L: float - Legnth [ft]. - - Notes - ----- - The purchase cost is given by [1]_. See source code for details. - - """ - return 300.9*Di**0.63316*L**0.80161 - -def compute_tower_weight(Di, L, tv, rho_M): - """ - Return the weight [W; in lb] of the tower assuming 2:1 elliptical head. - - Parameters - ---------- - Di : float - Inner diameter [ft]. - L : float - Legnth [ft]. - tv : float - Shell thickness [in]. - rho_M: floa - Density of material [lb/in^3]. - - Notes - ----- - The tower weight is given by [1]_. See source code for details. - - """ - Di = Di*12 - L = L*12 - return np.pi*(Di+tv)*(L+0.8*Di)*tv*rho_M - -def compute_tower_wall_thickness(Po, Di, L, S=15000, E=None, M=29.5): - """ - Return the wall thinkness [tv; in inches] designed to withstand the - internal pressure and the wind/earthquake load at the bottom. - - Parameters - ---------- - Po : float - Operating internal pressure [psi]. - Di : float - Internal diameter [ft]. - L : float - Height [ft]. - S : float - Maximum stress [psi]. - E : float - Fractional weld efficiency - M : float - Elasticity [psi]. - - Notes - ----- - The wall thickness is given by [1]_. See source code for details. - - """ - # TODO: Incorporate temperature for choosing S and M - Di = Di*12 # ft to in - L = L*12 - - E_check = E is None - if E_check: - # Assume carbon steel with thickness more than 1.25 in - E = 1.0 - - # Get design pressure, which should be higher than operating pressure. - Po_gauge = Po - 14.69 - if Po_gauge < 0: - # TODO: Double check vacuum calculation - Pd = -Po_gauge - tE = 1.3*Di*(Pd*L/M/Di)**0.4 - tEC = L*(0.18*Di - 2.2)*10**-5 - 0.19 - tv = tE + tEC - return tv - elif Po_gauge < 5: - Pd = 10 - elif Po_gauge < 1000: - Pd = np.exp(0.60608 + 0.91615*np.log(Po)) + 0.0015655*np.log(Po)**2 - else: - Pd = 1.1*Po_gauge - - # Calculate thinkess according to ASME pressure-vessel code. - ts = Pd*Di/(2*S*E-1.2*Pd) - - if E_check: - # Weld efficiency of 0.85 for low thickness carbon steel - if ts < 1.25: - E = 0.85 - ts = Pd*Di/(2*S*E-1.2*Pd) - - # Add corrosion allowence - ts += 1/8 - - # Minimum thickness for vessel rigidity may be larger - Di_ft = Di/12 - ts_min = np.polyval(ts_min_p, Di/12) if Di_ft < 4 else 0.25 - if ts < ts_min: - ts = ts_min - - # Calculate thickness to withstand wind/earthquake load - Do = Di + ts - tw = 0.22*(Do + 18)*L**2/(S*Do**2) - tv = max(tw, ts) - - # Vessels are fabricated from metal plates with small increments - if tv < 0.5: - tv = approx2step(tv, 3/16, 1/16) - elif tv < 2: - tv = approx2step(tv, 0.5, 1/8) - elif tv < 3: - tv = approx2step(tv, 2, 1/4) - return tv - -def compute_tray_base_purchase_cost(Di): - """Return the base cost of a tray [C_BT; USD] at a CE of 500. - - Parameters - ---------- - Di : float - Inner diameter [ft]. - - Notes - ----- - The purchase cost is given by [1]_. See source code for details. - - """ - return 412.6985 * np.exp(0.1482*Di) - -def compute_n_trays_factor(N_T): - """ - Return the cost factor for number of trays, F_NT. - - Parameters - ---------- - N_T: Number of trays - - Notes - ----- - The cost factor is given by [1]_. See source code for details. - - """ - if N_T < 20: - F_NT = 2.25/1.0414**N_T - else: - F_NT = 1 - return F_NT - -def compute_murphree_stage_efficiency(mu, alpha, L, V): - """ - Return the sectional murphree efficiency, E_mv. - - Parameters - ---------- - mu: float - Viscosity [mPa*s] - alpha: float - Relative volatility. - L: float - Liquid flow rate by mol. - V: float - Vapor flow rate by mol. - - Notes - ----- - The efficiency is given by [2]_. See source code for details. - - """ - S = alpha*V/L # Stripping factor - e = 0.503*mu**(-0.226)*(S if S > 1 else 1/S)**(-0.08 ) - if e < 1: return e - else: return 1 - -def compute_flow_parameter(L, V, rho_V, rho_L): - """ - Return the flow parameter, F_LV. - - Parameters - ---------- - L : float - Liquid flow rate by mass. - V : float - Vapor flow rate by mass. - rho_V : float - Vapor density. - rho_L : float - Liquid density. - - Notes - ----- - The flow parameter is given by [3]_. See source code for details. - - """ - return L/V*(rho_V/rho_L)**0.5 - -def compute_max_capacity_parameter(TS, F_LV): - """Return the maximum capacity parameter before flooding [C_sbf; in m/s]. - - Parameters - ---------- - TS : float - Tray spacing [mm]. - F_LV : float - Flow parameter. - - Notes - ----- - The max capacity parameter is given by [3]_. See source code for details. - - """ - return 0.0105 + 8.127e-4*TS**0.755*np.exp(-1.463*F_LV**0.842) - -def compute_max_vapor_velocity(C_sbf, sigma, rho_L, rho_V, F_F, A_ha): - """ - Return the maximum allowable vapor velocity - through the net area of flow before flooding [U_f; in m/s]. - - Parameters - ---------- - C_sbf : - Maximum Capacity Parameter (m/s) - sigma : - Liquid surface tension (dyn/cm) - rho_L : - Liquid density - rho_V : - Vapor density - F_F : - Foaming factor - A_ha : - Ratio of open area, A_h, to active area, A_a - - Notes - ----- - The max vapor velocity is given by [3]_. See source code for details. - - """ - F_ST = (sigma/20)**0.2 # Surface tension factor - - # Working area factor - if A_ha >= 0.1 and A_ha <= 1: - F_HA = 1 - elif A_ha >= 0.06: - F_HA = 5*A_ha + 0.5 - else: - raise ValueError(f"ratio of open to active area, 'A', must be between 0.06 and 1 ({A_ha} given)") - - return C_sbf * F_HA * F_ST * ((rho_L-rho_V)/rho_V)**0.5 - -def compute_downcomer_area_fraction(F_LV): - """ - Return the ratio of downcomer area to net (total) area, `A_dn`. - - Parameters - ---------- - F_LV : float - Flow parameter. - - Notes - ----- - The fraction of downcomer area is given by [3]_. See source code for details. - - """ - if F_LV < 0.1: - A_dn = 0.1 - elif F_LV < 1: - A_dn = 0.1 + (F_LV-0.1)/9 - else: - A_dn = 0.2 - return A_dn - -def compute_tower_diameter(V_vol, U_f, f, A_dn): - """Return tower diameter [D_T; in meter]. - - Parameters - ---------- - V_vol : float - Vapor volumetric flow rate [m^3/s]. - U_f : float - Maximum vapor velocity before flooding [m/s]. - f : float - Ratio of actual velocity to `U_f`. - A_dn : float - Ratio of downcomer area to net (total) area. - - Notes - ----- - The tower diameter is given by [3]_. See source code for details. - - """ - Di = (4*V_vol/(f*U_f*np.pi*(1-A_dn)))**0.5 - if Di < 0.914: - # Make sure diameter is not too small - Di = 0.914 - return Di - -def compute_tower_height(TS, N_stages: int, top=True, bot=True): - """ - Return the height of a tower [H; in meter]. - - Parameters - ---------- - TS : float - Tray spacing [mm]. - N_stages : float - Number of stages. - - Notes - ----- - The tower height is given by [3]_. See source code for details. - - """ - # 3 m bottoms surge capacity, 1.25 m above top tray to remove entrained liquid - H = TS*N_stages/1000 - if top: - H += 1.2672 - if bot: - H += 3 - return H \ No newline at end of file diff --git a/build/lib/biosteam/units/design_tools/cost_index.py b/build/lib/biosteam/units/design_tools/cost_index.py deleted file mode 100644 index 87d372551..000000000 --- a/build/lib/biosteam/units/design_tools/cost_index.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Cost indices. -""" - -__all__ = ('CEPCI_by_year',) - -#: Chemical Engineering Plant Cost Index by year -CEPCI_by_year = {1980: 261, - 1981: 297, - 1982: 314, - 1983: 317, - 1984: 323, - 1985: 325, - 1986: 318, - 1987: 324, - 1988: 343, - 1989: 355, - 1990: 358, - 1991: 361, - 1992: 358, - 1993: 359, - 1994: 368, - 1995: 381, - 1996: 382, - 1997: 387, - 1998: 390, - 1999: 391, - 2000: 394, - 2001: 394, - 2002: 396, - 2003: 402, - 2004: 444, - 2005: 468, - 2006: 500, - 2007: 525, - 2008: 575, - 2009: 522, - 2010: 551, - 2011: 586, - 2012: 585, - 2013: 567, - 2014: 576, - 2015: 542, - 2016: 568, - 2018: 603} \ No newline at end of file diff --git a/build/lib/biosteam/units/design_tools/flash_vessel_design.py b/build/lib/biosteam/units/design_tools/flash_vessel_design.py deleted file mode 100644 index 5fbaf6fae..000000000 --- a/build/lib/biosteam/units/design_tools/flash_vessel_design.py +++ /dev/null @@ -1,379 +0,0 @@ -""" -General functional algorithms for the design and purchase cost estimation -of flash vessels. - -References ----------- -.. [1] Seider, W. D., Lewin, D. R., Seader, J. D., Widagdo, S., Gani, R., - & Ng, M. K. (2017). Product and Process Design Principles. Wiley. - Cost Accounting and Capital Cost Estimation (Chapter 16) -.. [2] "Design Two-Phase Separators Within the Right Limits", Chemical - Engineering Progress Oct, 1993. - -""" -from math import log as ln, pi, exp -import biosteam as bst -__all__ = ('compute_horizontal_vessel_purchase_cost', - 'compute_vertical_vessel_purchase_cost', - 'GTable', 'HNATable', 'ceil_half_step', - 'compute_vessel_weight_and_wall_thickness', - 'compute_Stokes_law_York_Demister_K_value') - -def compute_horizontal_vessel_purchase_cost(W, D, F_M): - """ - Return the purchase cost [Cp; in USD] of a horizontal vessel, - including thes cost of platforms and ladders. - - Parameters - ---------- - W : float - Weight [lb]. - D : float - Diameter [ft]. - F_M : float - Vessel material factor. - - Notes - ----- - The purchase cost is given by [1]_. See source code for details. - The purchase cost is scaled according to BioSTEAM's Chemical - Plant Cost Index, `biosteam.CE`. - - """ - # C_v: Vessel cost - # C_pl: Platforms and ladders cost - C_v = exp(5.6336 - 0.4599*ln(W) + 0.00582*ln(W)**2) - C_pl = 2275*D**0.20294 - return bst.CE/567 * (F_M * C_v + C_pl) - -def compute_vertical_vessel_purchase_cost(W, D, L, F_M): - """ - Return the purchase cost [Cp; in USD] of a horizontal vessel, - including thes cost of platforms and ladders. - - Parameters - ---------- - W : float - Weight [lb]. - D : float - Diameter [ft]. - L : float - Length [ft]. - F_M : float - Vessel material factor. - - Notes - ----- - The purchase cost is given by [1]_. See source code for details. - The purchase cost is scaled according to BioSTEAM's Chemical - Plant Cost Index, `biosteam.CE`. - - """ - # C_v: Vessel cost - # C_pl: Platforms and ladders cost - C_v = exp(7.1390 + 0.18255*ln(W) + 0.02297*ln(W)**2) - C_pl = 410*D**0.7396*L**0.70684 - return bst.CE/567 * (F_M * C_v + C_pl) - -def GTable(DRho, Hlr): - """ - Return the allowable downflow (baffle liquid load) in gph/ft2, usually used for vertical vessel. - - Parameters - ---------- - DRho : float - Density difference between light liquid and vapor [lb/ft^3 ?] - Hlr : float - Height of liquid level above the interphase of light liquid and heavy liquid [ft] - - Notes - ----- - This function is not currently in use, nor has it been tested. - - """ - - A = {} - B = {} - C = {} - D = {} - - # TODO: Add errors when outside the range! - if Hlr > 30.0: - Hlr = 30 - - if Hlr < 18.0: - Hlr = 18 - - if DRho > 50.0: - DRho = 50 - - if DRho < 10.0: - DRho = 10 - - Hlr = round(Hlr, 0) - - A[18] = -9000.0 - B[18] = 1275.4 - C[18] = -31.3571 - D[18] = 0.255556 - - A[19] = -4690.0 - B[19] = 900.117 - C[19] = -20.5252 - D[19] = 0.157254 - - A[20] = -9980.0 - B[20] = 1367.91 - C[20] = -33.0163 - D[20] = 0.26476 - - A[21] = -8120.0 - B[21] = 1147.46 - C[21] = -25.3 - D[21] = 0.184444 - - A[22] = -16800.0 - B[22] = 1964.89 - C[22] = -48.8627 - D[22] = 0.399498 - - A[23] = -7900.0 - B[23] = 1255.35 - C[23] = -29.9142 - D[23] = 0.235632 - - A[24] = -11200.0 - B[24] = 1561.48 - C[24] = -38.7335 - D[24] = 0.318511 - - A[25] = -11100.0 - B[25] = 1554.66 - C[25] = -38.0313 - D[25] = 0.308026 - - A[26] = -7410.0 - B[26] = 1274.0 - C[26] = -30.8013 - D[26] = 0.246585 - - A[27] = -12700.0 - B[27] = 1709.78 - C[27] = -42.1048 - D[27] = 0.342222 - - A[28] = -10200.0 - B[28] = 1507.78 - C[28] = -36.422 - D[28] = 0.291221 - - A[29] = -10700.0 - B[29] = 1553.51 - C[29] = -37.5721 - D[29] = 0.300279 - - A[30] = -9830.0 - B[30] = 1513.11 - C[30] = -37.1907 - D[30] = 0.30379 - - G = A[Hlr] + (B[Hlr] * DRho) + (C[Hlr] * DRho ** 2) + (D[Hlr] * DRho ** 3) - - return round(G, 2) - - -def HNATable(Type, X): - """ - Table for cylindrical height and area conversions. - - Parameters - ---------- - Type: int - 1 if given H/D and find A/At, 2 if given A/At and find H/D. - X: float - H/D or A/At. - - Notes - ----- - Equations are given by [2]_. See source code for details. - - """ - # Type = 1 is where H/D is known, find A/At, Type = 2 is where A/At is known, find H/D - if (Type == 1): - a = -0.0000475593 - b = 3.924091 - c = 0.174875 - d = -6.358805 - e = 5.668973 - f = 4.018448 - g = -4.916411 - h = -1.801705 - i = -0.145348 - Y = (a + c * X + e * X ** 2 + g * X ** 3 + i * X ** 4) / \ - (1.0 + b * X + d * X ** 2 + f * X ** 3 + h * X ** 4) - else: - a = 0.00153756 - b = 26.787101 - c = 3.299201 - d = -22.923932 - e = 24.353518 - f = -14.844824 - g = -36.999376 - h = 10.529572 - i = 9.892851 - Y = (a + c * X + e * X ** 2 + g * X ** 3 + i * X ** 4) / \ - (1.0 + b * X + d * X ** 2 + f * X ** 3 + h * X ** 4) - - return Y - - -def compute_vessel_weight_and_wall_thickness(P, D, L, rho_M, Je=0.85): - """Return vessel weight and wall thickness. - - Parameters - ---------- - P : float - Pressure [psia]. - D : float - Diameter [ft]. - L: float - Vessel length [ft]. - rho_M: float - Density of Material [lb/ft^3]. - Je: float - Joint efficiency (1.0 for X-Rayed joints, 0.85 for thin carbon steel), - - Notes - ----- - Equations are given by [2]_. See source code for details. - - """ - S = 15000.0 # Vessel material stress value (assume carbon-steel) - Ca = 1.0/8.0 # Corrosion Allowance in inches - - P_gauge = abs(P - 14.7) - P1 = P_gauge + 30.0 - P2 = 1.1 * P_gauge - if P1 > P2: - PT = P1 - else: - PT = P2 - - # Calculate the wall thickness and surface area - # Shell - SWT = (PT * D*12.0) / (2.0 * S * Je - 1.2 * PT) + Ca - SSA = pi * D * L - if D < 15.0 and PT > (100 - 14.7): - # Elliptical Heads - HWT = (PT * D*12.0) / (2.0 * S * Je - 0.2 * PT) + Ca - HSA = 1.09 * D ** 2 - elif D > 15.0: - # Hemispherical Heads - HWT = (PT * D*12.0) / (4.0 * S * Je - 0.4 * PT) + Ca - HSA = 1.571 * D ** 2 - else: - # Dished Heads - HWT = 0.885 * (PT * D*12.0) / (S * Je - 0.1 * PT) + Ca - HSA = 0.842 * D ** 2 - - # Approximate the vessel wall thickness, whichever is larger - if SWT > HWT: - ts = SWT - else: - ts = HWT - - # Minimum thickness for vessel rigidity may be larger - if D < 4: - ts_min = 1/4 - elif D < 6: - ts_min = 5/16 - elif D < 8: - ts_min = 3/8 - elif D < 10: - ts_min = 7/16 - elif D < 12: - ts_min = 1/2 - else: - ts_min = ts - if ts < ts_min: - ts = ts_min - VW = rho_M * ts/12 * (SSA + 2.0 * HSA) # in lb - VW = round(VW, 2) - return VW, ts - - -def compute_low_liq_level_height(Type, P, D): - """ - Return the height of the lowest liquid level [Hlll; in ft] - for two-phase separators. - - Parameters - ---------- - Type : int - 1 for vertical, 2 for horizontal. - P : float - Pressure [psia]. - D: float - Diameter [ft]. - - Notes - ----- - Equations are given by [2]_. See source code for details. - - """ - if Type == 1: - Hlll = 0.5 - if P < 300: - Hlll = 1.25 - - elif Type == 2: - if D <= 4.0: - Hlll = 9.0/12.0 - elif D > 4.0 and D <= 7.0: - Hlll = 10.0/12.0 - elif D > 7.0 and D <= 9.0: - Hlll = 11.0/12.0 - elif D > 9.0 and D <= 11.0: - Hlll = 1.0 - elif D > 11.0 and D <= 15.0: - Hlll = 13.0/12.0 - else: - Hlll = 15.0/12.0 - - return Hlll # in ft - - -def compute_Stokes_law_York_Demister_K_value(P): - """ - Return K-constant in Stoke's Law using the York-Demister equation. - - Parameters - ---------- - P : float - Pressure [psia]. - - Notes - ----- - Equations are given by [2]_. See source code for details. - - """ - if P >= 0 and P <= 15.0: - K = 0.1821+(0.0029*P)+(0.046*ln(P)) - elif P > 15.0 and P <= 40.0: - K = 0.35 - elif P > 40.0 and P <= 5500.0: - K = 0.43 - 0.023*ln(P) - else: - raise ValueError(f'invalid Pressure {P} psia') - return K - - -def ceil_half_step(value): - """Return value to the next/highest 0.5 units""" - intval = round(value) - if value > intval: - return intval + 0.5 - elif value == intval: - return value - else: - return intval - 0.5 diff --git a/build/lib/biosteam/units/design_tools/heat_transfer.py b/build/lib/biosteam/units/design_tools/heat_transfer.py deleted file mode 100644 index c05bcd8e0..000000000 --- a/build/lib/biosteam/units/design_tools/heat_transfer.py +++ /dev/null @@ -1,325 +0,0 @@ -# -*- coding: utf-8 -*- -""" -General functional algorithms for the design of heat exchangers. - -""" -from math import log as ln - -__all__ = ('heuristic_overall_heat_transfer_coefficient', - 'heuristic_pressure_drop', - 'heuristic_tubeside_and_shellside_pressure_drops', - 'order_streams', - 'compute_Fahkeri_LMTD_correction_factor', - 'compute_heat_transfer_area', - 'compute_LMTD') - -def heuristic_overall_heat_transfer_coefficient(ci, hi, co, ho): - """ - Return a heuristic estimate of the overall heat transfer coefficient - [U; in kW/m^2/K]. Assume `U` is 0.5 kW/m^2/K if heat exchange is sensible - and 1.0 kW/m^2/K otherwise. - - Parameters - ---------- - ci : Stream - Cold inlet stream. - hi : Stream - Hot inlet stream. - co : Stream - Cold outlet stream. - ho : Stream - Hot outlet stream. - - Returns - ------- - U : float - overall heat transfer coefficient [kW/m^2/K]. - - """ - # TODO: Base U on Table 18.5, Warren D. Seider et. al. Product and Process Design Principles. (2016) - cip, hip, cop, hop = ci.phase, hi.phase, co.phase, ho.phase - phases = cip + hip + cop + hop - if 'g' in phases: - if ('g' in hip and 'l' in hop) and ('l' in cip and 'g' in cop): - return 1.0 - else: - return 0.5 - else: - return 0.5 - -def heuristic_pressure_drop(inlet_phase, outlet_phase): - """ - Return a heuristic estimate of the pressure drop [dP; in psi]. If the fluid - changes phase, `dP` is 1.5 psi. If the fluid remains a liquid, `dP` is 5 psi. - If the fluid remains a gas, `dP` is 3 psi. - - Parameters - ---------- - inlet_phase: str - outlet_phase : str - - Returns - ------- - dP : float - Pressure drop [psi]. - - """ - if ('l' in inlet_phase and 'g' in outlet_phase) or ('g' in inlet_phase and 'l' in outlet_phase): - # Latent fluid (boiling or condensing) - dP = 1.5 - elif inlet_phase == 'l': - # Sensible liquid - dP = 5 - elif outlet_phase == 'g': - # Sensible vapor - dP = 3 - return dP - -def heuristic_tubeside_and_shellside_pressure_drops(ci, hi, co, ho, - tubeside_iscooling=True): - """ - Return an estimate of tubeside and shellside pressure drops. - - Parameters - ---------- - ci : Stream - Cold inlet stream. - hi : Stream - Hot inlet stream. - co : Stream - Cold outlet stream. - ho : Stream - Hot outlet stream. - tubeside_iscooling : bool - True of tubeside fluid is cooling. - - Returns - ------- - dP_tube : float - Tubeside pressure drop (psi) - dP_shell : float - Shellside pressure drop (psi) - - """ - dP_c = heuristic_pressure_drop(ci.phase, co.phase) - dP_h = heuristic_pressure_drop(hi.phase, ho.phase) - if tubeside_iscooling: - dP_tube = dP_h - dP_shell = dP_c - else: - dP_tube = dP_c - dP_shell = dP_h - return dP_tube, dP_shell - -def order_streams(in_a, in_b, out_a, out_b): - """ - Return cold and hot inlet and outlet streams. - - Parameters - ---------- - in_a : Stream - Inlet a. - in_b : Stream - Inlet b. - out_a : Stream - Outlet a. - out_b : Stream - Outlet b. - - Returns - ------- - ci : Stream - Cold inlet. - hi : Stream - Hot inlet. - co : Stream - Cold outlet. - ho : Stream - Hot outlet. - - """ - if in_a.T < in_b.T: - return in_a, in_b, out_a, out_b - else: - return in_b, in_a, out_b, out_a - -def compute_fallback_Fahkeri_LMTD_correction_factor(P, N_shells): - """Return LMTF correction factor using the fallback equation for - `compute_Fahkeri_LMTD_correction_factor` when logarithms cannot be computed.""" - # A, J, and K are dummy variables - A = N_shells - N_shells*P - W = A/(A + P) - if 0.999 < W < 1.001: - Ft = 1 - else: - J = W/(1. - W) - K = (J + 2**-0.5)/(J - 2**-0.5) - if K <= 1: - Ft = 1 - else: - Ft = (2**0.5*J)/ln(K) - return Ft - -def compute_Fahkeri_LMTD_correction_factor(Tci, Thi, Tco, Tho, N_shells): - r""" - Return the log-mean temperature difference correction factor `Ft` - for a shell-and-tube heat exchanger with one or an even number of tube - passes, and a given number of shell passes, with the expression given in - [1]_ and also shown in [2]_. - - .. math:: - F_t=\frac{S\ln W}{\ln \frac{1+W-S+SW}{1+W+S-SW}} - - S = \frac{\sqrt{R^2+1}}{R-1} - - W = \left(\frac{1-PR}{1-P}\right)^{1/N} - - R = \frac{T_{in}-T_{out}}{t_{out}-t_{in}} - - P = \frac{t_{out}-t_{in}}{T_{in}-t_{in}} - - If R = 1 and logarithms cannot be evaluated: - - .. math:: - W' = \frac{N-NP}{N-NP+P} - - F_t = \frac{\sqrt{2}\frac{1-W'}{W'}}{\ln\frac{\frac{W'}{1-W'}+\frac{1} - {\sqrt{2}}}{\frac{W'}{1-W'}-\frac{1}{\sqrt{2}}}} - - Parameters - ---------- - Tci : float - Inlet temperature of cold fluid, [K] - Thi : float - Inlet temperature of hot fluid, [K] - Tco : float - Outlet temperature of cold fluid, [K] - Tho : float - Outlet temperature of hot fluid, [K] - shells : int, optional - Number of shell-side passes, [-] - - Returns - ------- - Ft : float - Log-mean temperature difference correction factor, [-] - - Notes - ----- - This expression is symmetric - the same result is calculated if the cold - side values are swapped with the hot side values. It also does not - depend on the units of the temperature given. - - Examples - -------- - compute_Fahkeri_LMTD_correction_factor(Tci=15, Tco=85, Thi=130, Tho=110, N_shells=1) - 0.9438358829645933 - - References - ---------- - .. [1] Fakheri, Ahmad. "A General Expression for the Determination of the - Log Mean Temperature Correction Factor for Shell and Tube Heat - Exchangers." Journal of Heat Transfer 125, no. 3 (May 20, 2003): 527-30. - doi:10.1115/1.1571078. - .. [2] Hall, Stephen. Rules of Thumb for Chemical Engineers, Fifth Edition. - Oxford; Waltham, MA: Butterworth-Heinemann, 2012. - - """ - if (Tco - Tci) < 0.01: - R = 1 - else: - R = (Thi - Tho)/(Tco - Tci) - P = (Tco - Tci)/(Thi - Tci) - if 0.999 < R < 1.001: - Ft = compute_fallback_Fahkeri_LMTD_correction_factor(P, N_shells) - else: - W = ((1. - P*R)/(1. - P))**(1./N_shells) - S = (R*R + 1.)**0.5/(R - 1.) - K = (1. + W - S + S*W)/(1. + W + S - S*W) - if K <= 0.001 or 0.999 < K < 1.001: - Ft = compute_fallback_Fahkeri_LMTD_correction_factor(P, N_shells) - else: - Ft = S*ln(W)/ln(K) - if Ft > 1.0: - Ft = 1.0 - elif Ft < 0.5: - # Bad design, probably a heat exchanger network with operating - # too close to the pinch. Fahkeri may not be valid, so give - # a conservative estimate of the correction factor. - Ft = 0.5 - return Ft - -def compute_heat_transfer_area(LMTD, U, Q, ft): - """ - Return required heat transfer area by LMTD correction factor method. - - Parameters - ---------- - LMTD : float - Log mean temperature difference - U : float - Heat transfer coefficient - Q : float - Duty - - """ - return Q/(U*LMTD*ft) - -def compute_LMTD(Thi, Tho, Tci, Tco, counterflow=True): - r''' - Return the log-mean temperature difference of an ideal counterflow - or co-current heat exchanger. - - .. math:: - \Delta T_{LMTD}=\frac{\Delta T_1-\Delta T_2}{\ln(\Delta T_1/\Delta T_2)} - - \text{For countercurrent: } \\ - \Delta T_1=T_{h,i}-T_{c,o}\\ - \Delta T_2=T_{h,o}-T_{c,i} - - \text{Parallel Flow Only:} \\ - {\Delta T_1=T_{h,i}-T_{c,i}}\\ - {\Delta T_2=T_{h,o}-T_{c,o}} - - Parameters - ---------- - Thi : float - Inlet temperature of hot fluid [K]. - Tho : float - Outlet temperature of hot fluid [K]. - Tci : float - Inlet temperature of cold fluid [K]. - Tco : float - Outlet temperature of cold fluid [K]. - counterflow : bool, optional - Whether the exchanger is counterflow or co-current. - - Returns - ------- - LMTD : float - Log-mean temperature difference [K] - - Notes - ----- - Any consistent set of units produces a consistent output. - - Examples - -------- - >>> LMTD(100., 60., 30., 40.2) - 43.200409294131525 - >>> LMTD(100., 60., 30., 40.2, counterflow=False) - 39.75251118049003 - - ''' - if counterflow: - dTF1 = Thi-Tco - dTF2 = Tho-Tci - else: - dTF1 = Thi-Tci - dTF2 = Tho-Tco - log_factor = ln(dTF2/dTF1) - if log_factor < 0.0001: - LMTD = dTF1 - else: - LMTD = (dTF2 - dTF1)/ln(log_factor) - return LMTD \ No newline at end of file diff --git a/build/lib/biosteam/units/design_tools/mechanical.py b/build/lib/biosteam/units/design_tools/mechanical.py deleted file mode 100644 index 9fcee5da2..000000000 --- a/build/lib/biosteam/units/design_tools/mechanical.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -""" -General functional algorithms for the design of pumps and motors. - -""" -from math import log as ln -from fluids.pump import nema_sizes_hp - -__all__ = ('brake_efficiency', 'motor_efficiency', 'pump_efficiency', - 'nearest_NEMA_motor_size') - -def brake_efficiency(q): - """Return brake efficiency given flow rate in gpm.""" - if q < 50: q = 50 - elif q > 5000: q = 5000 - return -0.316 + 0.24015*ln(q) - 0.01199*ln(q)**2 - -def motor_efficiency(Pb): - """Return motor efficiency given brake power in hp.""" - if Pb < 1: Pb = 1 - elif Pb > 1500: Pb = 1500 - return 0.8 + 0.0319*ln(Pb) - 0.00182*ln(Pb)**2 - -def pump_efficiency(q, p): - """Return pump efficiency. - - Parameters - ---------- - q : float - Volumetric flow rate in gpm. - p : float - Power in hp. - """ - mup = brake_efficiency(q) - mum = motor_efficiency(p/mup) - return mup*mum - -def nearest_NEMA_motor_size(power): - for nearest_power in nema_sizes_hp: - if nearest_power >= power: return nearest_power - raise ValueError(f'no NEMA motor size bigger than {power} hp') - -def calculate_NPSH(P_suction, P_vapor, rho_liq): - """Return NPSH in ft given suction and vapor pressure in Pa and density in kg/m^3.""" - # Note: NPSH = (P_suction - P_vapor)/(rho_liq*gravity) - # Taking into account units, NPSH will be equal to return value - return 0.334438*(P_suction - P_vapor)/rho_liq \ No newline at end of file diff --git a/build/lib/biosteam/units/design_tools/specification_factors.py b/build/lib/biosteam/units/design_tools/specification_factors.py deleted file mode 100644 index 3c921c824..000000000 --- a/build/lib/biosteam/units/design_tools/specification_factors.py +++ /dev/null @@ -1,181 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Material and specification factors as compiled by [1]_. - -References ----------- - -.. [1] Seider, W. D.; Lewin, D. R.; Seader, J. D.; Widagdo, S.; Gani, R.; - Ng, M. K. Cost Accounting and Capital Cost Estimation. - In Product and Process Design Principles; Wiley, 2017; pp 426–485. - -""" -__all__ = ('vessel_material_factors', - 'pressure_vessel_material_factors', - 'material_densities_lb_per_ft3', - 'material_densities_lb_per_in3', - 'pump_material_factors', - 'pump_gear_factors', - 'pump_centrifugal_factors', - 'distillation_tray_type_factor', - 'tray_material_factor_functions', - 'distillation_column_material_factors', - 'shell_and_tube_material_factor_coefficients', -) - -# %% Vessels - -#: Material factors for pressure vessels -pressure_vessel_material_factors = { - 'Carbon steel': 1.0, - 'Low-alloy steel': 1.2, - 'Stainless steel 304': 1.7, - 'Stainless steel 316': 2.1, - 'Carpenter 20CB-3': 3.2, - 'Nickel-200': 5.4, - 'Monel-400': 3.6, - 'Inconel-600': 3.9, - 'Incoloy-825': 3.7, - 'Titanium': 7.7} - -#: Material factors for ordinary vessels -vessel_material_factors = { - 'Carbon steel': 1.0, - 'Copper': 1.2, - 'Stainless steel': 2.0, - 'Nickel': 2.5, - 'Titanium clad': 3.0, - 'Titanium': 6.0} - -#: Material densities in lb/ft^3 -material_densities_lb_per_ft3 = { - 'Carbon steel': 490, - 'Low-alloy steel': None, - 'Stainless steel 304': 499.4, - 'Stainless steel 316': 499.4, - 'Carpenter 20CB-3': None, - 'Nickel-200': None, - 'Monel-400': None, - 'Inconel-600': None, - 'Incoloy-825': None, - 'Titanium': None} - -#: Material densities in lb/in^3 -material_densities_lb_per_in3 = { - 'Carbon steel': 0.284 , - 'Low-alloy steel': None, - 'Stainless steel 304': 0.289, - 'Stainless steel 316': 0.289, - 'Carpenter 20CB-3': None, - 'Nickel-200': None, - 'Monel-400': None, - 'Inconel-600': None, - 'Incoloy-825': None, - 'Titanium': None} - -# %% Pumps - -#: Material factors for pumps -pump_material_factors = { - 'Cast iron': 1, - 'Ductile iron': 1.15, - 'Cast steel': 1.35, - 'Bronze': 1.9, - 'Stainless steel': 2, - 'Hastelloy C': 2.95, - 'Monel': 3.3, - 'Nickel': 3.5, - 'Titanium': 9.7} - -#: Gear-type cost factors for pumps -pump_gear_factors = { - 'OpenDripProof': 1, - 'EnclosedFanCooled': 1.4, - 'ExplosionProofEnclosure': 1.8} - -#: Centrifugal-type cost factors for pumps. -#: Keys are case-split orientation and shaft rpm. -pump_centrifugal_factors = { - 'VSC3600': 1, - 'VSC1800': 1.5, - 'HSC3600': 1.7, - 'HSC1800': 2, - '2HSC3600': 2.7, - '2+HSC3600': 8.9} - - -# %% Distillation - -#: Tray-type cost factors for distillation columns. -distillation_tray_type_factor = { - 'Sieve': 1, - 'Valve': 1.18, - 'Bubble cap': 1.87} - -# Tray material factors (inner diameter, Di, in ft) -def compute_carbon_steel_material_factor(Di): - return 1 - -def compute_stainless_steel_304_material_factor(Di): - return 1.189 + 0.058*Di - -def compute_stainless_steel_316_material_factor(Di): - return 1.401 + 0.073*Di - -def compute_carpenter_20CB3_material_factor(Di): - return 1.525 + 0.079*Di - -def compute_monel_material_factor(Di): - return 2.306 + 0.112*Di - -#: Material cost factors for distillation column trays. -tray_material_factor_functions = { - 'Carbon steel': compute_carbon_steel_material_factor, - 'Stainless steel 304': compute_stainless_steel_304_material_factor, - 'Stainless steel 316': compute_stainless_steel_316_material_factor, - 'Carpenter 20CB-3': compute_carpenter_20CB3_material_factor, - 'Monel': compute_monel_material_factor} - -#: Material cost factors for distillation columns. -distillation_column_material_factors = { - 'Carbon steel': 1.0, - 'Low-alloy steel': 1.2, - 'Stainless steel 304': 1.7, - 'Stainless steel 316': 2.1, - 'Carpenter 20CB-3': 3.2, - 'Nickel-200': 5.4, - 'Monel-400': 3.6, - 'Inconel-600': 3.9, - 'Incoloy-825': 3.7, - 'Titanium': 7.7} - -# %% Shell & tube heat exchangers - -#: dict[str, tuple[float, float]] Material factor coefficients of shell and -#: tube heat exchangers. Keys are materials of construction of -#: shell/tube and coefficients are `a` and `b` in Eq. (16.44). -shell_and_tube_material_factor_coefficients = { - 'Carbon steel/carbon steel': (0, 0), - 'Carbon steel/brass': (1.08, 0.05), - 'Carbon steel/stainles steel': (1.75, 0.13), - 'Carbon steel/Monel': (2.1, 0.13), - 'Carbon steel/titanium': (5.2, 0.16), - 'Carbon steel/Cr-Mo steel': (1.55, 0.05), - 'Cr-Mo steel/Cr-Mo steel': (1.7, 0.07), - 'Stainless steel/stainless steel': (2.7, 0.07), - 'Monel/Monel': (3.3, 0.08), - 'Titanium/titanium': (9.6, 0.06)} - -def compute_shell_and_tube_material_factor(A, a, b): - r""" - Return the material factor for shell and tubes given the area [A; ft^3] - and material-dependent coefficients `a` and `b`. - - Notes - ----- - Material factors are computed using Eq. 16.44 in [1]_: - - :math:`F_M = a + \left(\frac{A}{100.0}\right)^b` - - """ - return a + (A/100.0)**b diff --git a/build/lib/biosteam/units/design_tools/tank_design.py b/build/lib/biosteam/units/design_tools/tank_design.py deleted file mode 100644 index 9a9dd3aed..000000000 --- a/build/lib/biosteam/units/design_tools/tank_design.py +++ /dev/null @@ -1,188 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Design and cost algorithms from ordinary vessels. - -References ----------- -.. [1] Apostolakou, A. A., Kookos, I. K., Marazioti, C., Angelopoulos, K. C. - (2009). Techno-economic analysis of a biodiesel production process from - vegetable oils. Fuel Processing Technology, 90(7–8), 1023–1031. - https://doi.org/10.1016/j.fuproc.2009.04.017 -.. [2] Seider, W. D.; Lewin, D. R.; Seader, J. D.; Widagdo, S.; Gani, R.; - Ng, M. K. Cost Accounting and Capital Cost Estimation. - In Product and Process Design Principles; Wiley, 2017; pp 426–485. - -""" -import biosteam as bst -from math import ceil -from thermosteam import settings -from thermosteam.base import AbsoluteUnitsOfMeasure -from ...utils import ExponentialFunctor - -__all__ = ('TankPurchaseCostAlgorithm', - 'field_erected_tank_purchase_cost', - 'compute_number_of_tanks_and_total_purchase_cost', - 'storage_tank_purchase_cost_algorithms', - 'mix_tank_purchase_cost_algorithms') - -class TankPurchaseCostAlgorithm: - r""" - Create a TankPurchaseCostAlgorithm for vessel costing. - - Parameters - ---------- - f_Cp : function - Should return the purchase cost given the volume. - V_min : float - Minimum volume at which cost is considered accurate. - V_max : float - Maximum volume of a vessel. - V_units : str - Units of measure for volume. - - Attributes - ---------- - f_Cp : function - Returns the purchase cost given the volume. - V_min : float - Minimum volume at which cost is considered accurate. - V_max : float - Maximum volume of a vessel. - V_units : AbsoluteUnitsOfMeasure - Units of measure for volume. - - Examples - -------- - Find the number of mixing tanks and the total purchase cost - at a volume of 1 m^3 using the purchase cost equation from [1]_: - - >>> from biosteam.units._tank import TankPurchaseCostAlgorithm - >>> TankPurchaseCostAlgorithm(lambda V: 12080 * V **0.525, - ... V_min=0.1, V_max=30, V_units='m^3', - ... CE=525.4, material='Stainless steel') - TankPurchaseCostAlgorithm(f_Cp=, V_min=0.1, V_max=30, CE=525.4, material=Stainless steel, V_units=m^3) - - - """ - __slots__ = ('f_Cp', 'V_min', 'V_max', - 'CE', 'material', 'V_units') - - def __init__(self, f_Cp, V_min, V_max, V_units, CE, material): - self.f_Cp = f_Cp - self.V_min = V_min - self.V_max = V_max - self.V_units = AbsoluteUnitsOfMeasure(V_units) - self.CE = CE - self.material = material - - def __repr__(self): - return f"{type(self).__name__}(f_Cp={self.f_Cp.__name__}, V_min={self.V_min}, V_max={self.V_max}, CE={self.CE}, material={self.material}, V_units={self.V_units})" - - -def compute_number_of_tanks_and_total_purchase_cost(total_volume, - purchase_cost_algorithm, - material_factor): - """ - Return number of tanks and total purchase cost of all tanks. - - Parameters - ---------- - total_volume : float - Total volume required [m^3]. - purchase_cost_algorithm : TankPurchaseCostAlgorithm - All costing options. - material_factor : float - Material purchase cost factor. - - """ - V_total = total_volume - F_M = material_factor - V_units = purchase_cost_algorithm.V_units - V_total /= V_units.conversion_factor('m^3') - V_min = purchase_cost_algorithm.V_min - if settings.debug and V_total < V_min: - raise RuntimeError( - f"volume ({V_total:.5g} {V_units}) is below " - f"the lower bound ({V_min:.5g} {V_units}) for purchase " - "cost estimation") - N = ceil(V_total / purchase_cost_algorithm.V_max) - V = V_total / N - F_CE = bst.CE / purchase_cost_algorithm.CE - Cp = N * F_M * F_CE * purchase_cost_algorithm.f_Cp(V) - return N, Cp - -def field_erected_tank_purchase_cost(V): - r""" - Return the purchase cost [USD] of a single, field-erected vessel assuming - stainless steel construction material. - - Parameters - ---------- - V : float - Volume of tank [m^3]. - - Returns - ------- - Cp : float - Purchase cost [USD]. - - Notes - ----- - The purchase cost is given by [1]_. - - If :math:`V < 2 \cdot 10^3`: - - :math:`C_p^{2007} = 32500.0 + 79.35 V` - - Otherwise: - - :math:`C_p^{2007} = 125000.0 + 47.1 V` - - Examples - -------- - >>> from biosteam.units._tank import field_erected_vessel_purchase_cost - >>> field_erected_vessel_purchase_cost(300) - 112610.0 - - """ - if V < 2e3: - Cp = 65000.0 + 158.7 * V - else: - Cp = 250000.0 + 94.2 * V - return Cp - -#: Cost algorithms for storage tank vessel types as in [1]_ [2]_. -storage_tank_purchase_cost_algorithms = { -"Field erected": TankPurchaseCostAlgorithm( - field_erected_tank_purchase_cost, - V_min=0, V_max=50e3, V_units='m^3', - CE=525.4, material='Stainless steel'), -"Floating roof": TankPurchaseCostAlgorithm( - ExponentialFunctor(A=475, n=0.507), - V_min=3e4, V_max=1e6, V_units='gal', - CE=567, material='Carbon steel'), -"Cone roof": TankPurchaseCostAlgorithm( - ExponentialFunctor(A=265, n=0.513), - V_min=1e4, V_max=1e6, V_units='gal', - CE=567, material='Carbon steel'), -"Spherical; 0-30 psig": TankPurchaseCostAlgorithm( - ExponentialFunctor(68, 0.72 ), - V_min=1e4, V_max=1e6, V_units='gal', - CE=567, material='Carbon steel'), -"Spherical; 30–200 psig": TankPurchaseCostAlgorithm( - ExponentialFunctor(53, 0.78), - V_min=1e4, V_max=7.5e5, V_units='gal', - CE=567, material='Carbon steel'), -"Gas holder": TankPurchaseCostAlgorithm( - ExponentialFunctor(3595, 0.43), - V_min=4e3, V_max=4e5, V_units='ft^3', - CE=567, material='Carbon steel') -} - -#: Cost algorithms for mix tank vessel types as in [2]_. -mix_tank_purchase_cost_algorithms = { -"Conventional": TankPurchaseCostAlgorithm( - ExponentialFunctor(A=12080, n=0.525), - V_min=0.1, V_max=30, V_units='m^3', - CE=525.4, material='Stainless steel') -} \ No newline at end of file diff --git a/build/lib/biosteam/units/design_tools/vacuum.py b/build/lib/biosteam/units/design_tools/vacuum.py deleted file mode 100644 index 7ef193b64..000000000 --- a/build/lib/biosteam/units/design_tools/vacuum.py +++ /dev/null @@ -1,231 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Functional algorithms for the design and purchase cost estimation of -vacuum systems. - -References ----------- -.. [1] Seider, W. D.; Lewin, D. R.; Seader, J. D.; Widagdo, S.; Gani, R.; - Ng, M. K. Cost Accounting and Capital Cost Estimation. - In Product and Process Design Principles; Wiley, 2017; pp 426–485. - - -""" -from math import log as ln -from .mechanical import motor_efficiency -from biosteam.utils import checkbounds -from biosteam.exceptions import DesignError -import biosteam as bst - -__all__ = ('compute_vacuum_system_power_and_cost',) - - -# %% Data - -# System types of vacuum systems -# Volumetric flowrate ranges, (cfm) and lower limit of suction (torr) -_steamjet_ejectors = { - 'One stage': ((10, 1000000), 100), - 'Two stage': ((10, 1000000), 15), - 'Three stage': ((10, 1000000), 2)} -_liquid_ring = { - 'One stage water seal': (( 3, 18000), 50), - 'Two stage water seal': (( 3, 18000), 25), - 'Oil seal': (( 3, 18000), 10)} -_dry_vacuum = { - 'Three-stage rotary lobe': ((60, 240), 1.5), - 'Three-stage claw': ((60, 270), 0.3), - 'Screw compressor': ((50, 1400), 0.1)} - -_default_vacuum_systems = {'Liquid-ring pump': _liquid_ring, - 'Steam-jet ejector': _steamjet_ejectors, - 'Dry-vacuum pump': _dry_vacuum} - -_air_density = 1.2041 # kg/m3 dry air - -# %% Calculate vacuum system requirements - -def compute_vacuum_system_power_and_cost( - F_mass, F_vol, P_suction, vessel_volume, - vacuum_system_preference=None): - """ - Return power (kW) and cost (USD) of vacuum system. - - Parameters - ---------- - F_mass : float - Vapor mass flow rate entering vacuum system from vessel in kg/hr (not including inleakage). - F_vol : float - Vapor volumetric flow rate entering vacuum system from vessel in m3/hr (not including inleakage). - P_suction : float - Suction pressure in Pa - vessel_volume : float - Vacuum volume in m3 - vacuum_system_preference : 'Liquid-ring pump', 'Steam-jet ejector', or 'Dry-vacuum pump' - Name(s) of preferred vacuum systems - - """ - P_suction *= 7.5006e-3 # to torr - if vessel_volume: - vessel_volume *= 35.315 # to ft3 - F_mass_air = calculate_air_inleakage(vessel_volume, P_suction) # lb/hr - F_vol_air = 0.26697*F_mass_air/_air_density # cfm - else: - F_vol_air = F_mass_air = 0 - F_vol_cfm = 0.5886*F_vol + F_vol_air - if F_vol_cfm < 3.01: - factor = 3.01/F_vol_cfm - F_vol_cfm = 3.01 - else: - factor = 1 - F_mass_kgph = (F_mass + 0.4536*F_mass_air)*factor # kg/hr - F_mass_lbph = 2.205 * F_mass_kgph - power = calculate_vacuum_power(F_mass_kgph, P_suction) - vacuum_systems = get_prefered_vacuum_systems(vacuum_system_preference) - vacuum_sys, grade = select_vacuum_system(vacuum_systems, F_vol_cfm, P_suction) - base_cost = calculate_vacuum_cost(vacuum_sys, grade, F_mass_lbph, F_vol_cfm, P_suction) - cost = bst.CE / 567 * base_cost - return power, cost - - -# %% Supporting functions - -def get_prefered_vacuum_systems(preference): - if preference is None: - vacuum_systems = _default_vacuum_systems.values() - else: - defaults = _default_vacuum_systems.keys() - if isinstance(preference, str): - if preference not in defaults: - raise ValueError(f"preference have at least one of the following: {defaults}") - preference = (preference,) - else: - for name in preference: - if name not in defaults: - raise ValueError(f"preference have at least one of the following: {defaults}") - - vacuum_systems = [_default_vacuum_systems[name] for name in preference] - return vacuum_systems - - -def get_available_vacuum_systems(F_vol_cfm, P_suction): - """ - Return available vacuum type and grade - - Parameters - ---------- - F_vol_cfm : float - Vapor volumetric flow rate entering vacuum system from vessel in cfm (including inleakage). - P_suction : float - Suction pressure in Torr - """ - types = [] - for vacuumtype, vacuum_sys in _default_vacuum_systems.items(): - for grade, flowrange_minsuction in vacuum_sys.items(): - flowrange, minsuction = flowrange_minsuction - if checkbounds(F_vol_cfm, flowrange) and P_suction > minsuction: - types.append((vacuumtype, grade)) - return types - -def select_vacuum_system(vacuum_systems, F_vol_cfm, P_suction): - """ - Return a heuristic vacuum type and grade - - Parameters - ---------- - F_vol_cfm : float - Vapor volumetric flow rate entering vacuum system from vessel in cfm (including inleakage). - P_suction : float - Suction pressure in Torr - """ - for vacuum_sys in vacuum_systems: - for grade, flowrange_minsuction in vacuum_sys.items(): - flowrange, minsuction = flowrange_minsuction - if checkbounds(F_vol_cfm, flowrange) and P_suction > minsuction: - return (vacuum_sys, grade) - raise DesignError('no vacuum system available at current flow and suction pressure') - -def calculate_heuristic_air_inleakage(V, P): - """ - Return air in-leakage in kg/hr through a heuristic calculation. - - Parameters - ---------- - V : float - Vacuum volume in m3 - P : float - Suction pressure in Pa - - """ - if P > 11999.013: k = 0.2 - elif P > 4132.993: k = 0.15 - elif P > 2799.77: k = 0.10 - elif P > 133.322: k = 0.051 - else: raise ValueError('cannot calculate air inleakage at pressures lower than 133.32 Pascal') - return k*V**0.667 - -def calculate_air_inleakage(V, P): - """ - Return air in-leakage in kg/hr. - - Parameters - ---------- - V : float - Vacuum volume in m3 - P : float - Suction pressure in Torr - """ - return 5 + (0.0298 + 0.03088*ln(P) - 5.733e-4*ln(P)**2)*V**0.66 - -def calculate_vacuum_power(F_mass, P_suction): - """ - Return vacuum power (after accounting for motor efficiency) in kW. - - Parameters - ---------- - F_mass : float - Total mass flow rate entering vacuum system in kg/hr (including inleakage). - P_suction : float - Suction pressure in Torr - - """ - SF = F_mass/P_suction # Size factor - if SF < 0.2: SF = 0.2 - elif SF > 16: SF = 16 - Pb = 21.4*SF**0.924 # Break power assuming Liquid-ring (NASH) - mu_m = motor_efficiency(Pb) - return Pb/mu_m - -def calculate_vacuum_cost(vacuum_sys, grade, F_mass_lbph, F_vol_cfm, P_suction): - # Costing for different vacuums ASSUMING THAT ALL ARE CARBON STEEL!!!! - if vacuum_sys is _steamjet_ejectors: - S = F_mass_lbph/P_suction - Cp = 1915*(S**(0.41)) - if grade == 'One stage': - Cs = 1 - elif grade == 'Two stage': - Cs = 1.8 - elif grade == 'Three stage': - Cs = 2.1 - Cost = Cp*Cs - elif vacuum_sys is _liquid_ring: - S = F_vol_cfm - Cp = 8250*(S**(0.37)) - if grade == 'One stage water seal': - Cs = 1 - elif grade == 'Two stage water seal': - Cs = 1.8 - elif grade == 'Oil seal': - Cs = 2.1 - Cost = Cp*Cs - elif vacuum_sys is _dry_vacuum: - S = F_vol_cfm - if grade == 'Three-stage rotary lobe': - Cp = 8075*(S**(0.41)) - elif grade == 'Three-stage claw': - Cp = 9785*(S**(0.36)) - elif grade == 'Screw compressor': - Cp = 10875*(S**(0.38)) - Cost = Cp - return Cost - diff --git a/build/lib/biosteam/units/facilities/__init__.py b/build/lib/biosteam/units/facilities/__init__.py deleted file mode 100644 index 3a8dd13e3..000000000 --- a/build/lib/biosteam/units/facilities/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Tue May 28 11:32:33 2019 - -@author: yoelr -""" - -__all__ = ['Facility'] - -from ..._facility import Facility -from ._boiler_turbogenerator import * -from ._cooling_tower import * -from ._chilled_water_package import * -from ._process_water_center import * -from ._air_distribution_package import * - -from . import _boiler_turbogenerator -from . import _cooling_tower -from . import _chilled_water_package -from . import _process_water_center -from . import _air_distribution_package - -__all__.extend(_process_water_center.__all__) -__all__.extend(_chilled_water_package.__all__) -__all__.extend(_boiler_turbogenerator.__all__) -__all__.extend(_cooling_tower.__all__) -__all__.extend(_air_distribution_package.__all__) \ No newline at end of file diff --git a/build/lib/biosteam/units/facilities/_air_distribution_package.py b/build/lib/biosteam/units/facilities/_air_distribution_package.py deleted file mode 100644 index a895a10e9..000000000 --- a/build/lib/biosteam/units/facilities/_air_distribution_package.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon Jul 29 21:35:10 2019 - -@author: yoelr -""" -from ..._facility import Facility -from ..decorators import cost - -__all__ = ('AirDistributionPackage',) - -@cost('Flow rate', 'Plant air reciever', - cost=16e3, CE=522, S=83333, n=0.6, BM=3.1) -@cost('Flow rate', 'Instrument air dryer', - cost=15e3, CE=522, S=83333, n=0.6, BM=1.8) -@cost('Flow rate', 'Plant air compressor', units='kg/hr', - cost=28e3, CE=551, S=83333, n=0.6, BM=1.6, kW=150*0.7457) -class AirDistributionPackage(Facility): - """ - Create a AirDistributionPackage object that accounts for the capital cost - and power of air distribution based on flow rate correlations from [1]_. - - Parameters - ---------- - ID : str, optional - Unit ID. - ins : stream - Air flow to be distributed. - outs : stream - Distributed air flow. - - References - ---------- - .. [1] 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 - """ - network_priority = 0 \ No newline at end of file diff --git a/build/lib/biosteam/units/facilities/_boiler_turbogenerator.py b/build/lib/biosteam/units/facilities/_boiler_turbogenerator.py deleted file mode 100644 index c67c5aba9..000000000 --- a/build/lib/biosteam/units/facilities/_boiler_turbogenerator.py +++ /dev/null @@ -1,172 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sun Feb 17 16:36:47 2019 - -@author: yoelr -""" -from ... import HeatUtility, Unit -from . import Facility -from ..decorators import cost - -__all__ = ('BoilerTurbogenerator',) - -#: TODO add reference of NREL - -@cost('Work', 'Turbogenerator', - CE=551, S=42200, kW=0, cost=9500e3, n=0.60, BM=1.8) -@cost('Flow rate', 'Hot process water softener system', - CE=551, cost=78e3, S=235803, n=0.6, BM=1.8) -@cost('Flow rate', 'Amine addition pkg', - CE=551, cost=40e3, S=235803, n=0.6, BM=1.8) -@cost('Flow rate', 'Deaerator', - CE=551, cost=305e3, S=235803, n=0.6, BM=3.0) -@cost('Flow rate', 'Boiler', - CE=551, cost=28550e3, kW=1000, S=238686, n=0.6, BM=1.8) -class BoilerTurbogenerator(Facility): - """ - Create a BoilerTurbogenerator object that will calculate electricity - generation from burning the feed. It also takes into account how much - steam is being produced, and the required cooling utility of the turbo - generator. No emissions or mass balances are taken into account. All - capital cost correlations are based on [1]_. - - Parameters - ---------- - ins : stream sequence - [0] Liquid/solid feed that will be burned. - - [1] Gas feed that will be burned. - - [2] Make-up water. - outs : stream sequence - [0] Total emissions produced. - - [1] Blowdown water. - boiler_efficiency : float - Fraction of heat transfered to steam. - turbo_generator_efficiency : float - Fraction of steam heat converted to electricity. - agent : UtilityAgent - Steam produced. - - References - ---------- - .. [1] 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 - - """ - network_priority = 0 - - # TODO: Make this a whole system instead of approximating duty per mol values - duty_over_mol = 35000 - - boiler_blowdown = 0.03 - RO_rejection = 0 - _N_ins = 3 - _N_outs = 2 - _N_heat_utilities = 2 - _units = {'Flow rate': 'kg/hr', - 'Work': 'kW'} - - def __init__(self, ID='', ins=None, outs=(), *, - boiler_efficiency=0.80, - turbogenerator_efficiency=0.85, - side_steam=None, - agent=HeatUtility.get_heating_agent('low_pressure_steam')): - Unit.__init__(self, ID, ins, outs) - self.agent = agent - self.makeup_water = makeup_water = agent.to_stream('boiler_makeup_water') - loss = makeup_water.flow_proxy() - loss.ID = 'rejected_water_and_blowdown' - self.ins[-1] = makeup_water - self.outs[-1] = loss - self.boiler_efficiency = boiler_efficiency - self.turbogenerator_efficiency = turbogenerator_efficiency - self.steam_utilities = set() - self.steam_demand = agent.to_stream() - self.side_steam = side_steam - - def _run(self): pass - - def _load_utility_agents(self): - steam_utilities = self.steam_utilities - steam_utilities.clear() - agent = self.agent - for u in self.system.units: - if u is self: continue - for hu in u.heat_utilities: - if hu.agent is agent: - steam_utilities.add(hu) - - def _design(self): - B_eff = self.boiler_efficiency - TG_eff = self.turbogenerator_efficiency - steam = self.steam_demand - self._load_utility_agents() - steam.imol['7732-18-5'] = steam_mol = sum([i.flow for i in self.steam_utilities]) - duty_over_mol = self.duty_over_mol - feed_solids, feed_gas, _ = self.ins - emissions, _ = self.outs - hu_cooling, hu_steam = self.heat_utilities - H_steam = steam_mol * duty_over_mol - if self.side_steam: - H_steam += self.side_steam.H - LHV_feed = 0 - for feed in (feed_solids, feed_gas): - if not feed.isempty(): LHV_feed -= feed.LHV - - # This is simply for the mass balance (no special purpose) - # TODO: In reality, this should be CO2 - emissions_mol = emissions.mol - emissions_mol[:] = 0 - if feed_solids: - emissions_mol += feed_solids.mol - if feed_gas: - emissions_mol += feed_gas.mol - combustion_rxns = self.chemicals.get_combustion_reactions() - combustion_rxns(emissions_mol) - emissions.imol['O2'] = 0 - emissions.T = 373.15 - emissions.P = 101325 - emissions.phase = 'g' - H_content = LHV_feed*B_eff - emissions.H - - #: [float] Total steam produced by the boiler (kmol/hr) - self.total_steam = H_content / duty_over_mol - - self.makeup_water.imol['7732-18-5'] = ( - self.total_steam * self.boiler_blowdown * 1/(1-self.RO_rejection) - ) - # Heat available for the turbogenerator - H_electricity = H_content - H_steam - - Design = self.design_results - Design['Flow rate'] = self.total_steam * 18.01528 - - if H_electricity < 0: - H_steam = H_content - cooling = electricity = 0 - else: - electricity = H_electricity * TG_eff - cooling = electricity - H_electricity - hu_cooling(cooling, steam.T) - hu_steam.mix_from(self.steam_utilities) - hu_steam.reverse() - Design['Work'] = electricity/3600 - - def _end_decorated_cost_(self): - self.power_utility(self.power_utility.rate - self.design_results['Work']) - -# Simulation of ethanol production from sugarcane -# in Brazil: economic study of an autonomous -# distillery -# Marina O.S. Diasa,b, Marcelo P. Cunhaa, Charles D.F. Jesusa, Mirna I.G. -# Scandiffioa, Carlos E.V. Rossella,b, Rubens Maciel Filhob, Antonio Bonomia -# a CTBE – Bioethanol Science and Technology National Laboratory, PO Box 6170 – -# CEP 13083-970, Campinas – SP, Brazil, marina.dias@bioetanol.org.br -# b School of Chemical Engineering, University of Campinas, PO Box 6066 – CEP -# 13083-970, Campinas – SP, Brazil -# LHV = 7565 # Bagasse lower heating value (kJ/kg) \ No newline at end of file diff --git a/build/lib/biosteam/units/facilities/_chilled_water_package.py b/build/lib/biosteam/units/facilities/_chilled_water_package.py deleted file mode 100644 index 60c8f7f55..000000000 --- a/build/lib/biosteam/units/facilities/_chilled_water_package.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon Apr 29 18:28:55 2019 - -@author: yoelr -""" -from . import Facility -from ..decorators import cost -from ... import HeatUtility -import numpy as np - -__all__ = ('ChilledWaterPackage',) - -@cost('Duty', S=-14*4184000, kW=3400*0.7457, cost=1375e3, CE=551, n=0.7, BM=1.5) -class ChilledWaterPackage(Facility): - """ - Create a chilled water package with capital cost and power based on the flow rate - of chilled water as in [1]_. - - - Parameters - ---------- - ID : str, optional - Unit ID. - - References - ---------- - .. [1] 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 - - """ - network_priority = 0 - _N_heat_utilities = 1 - _units = {'Duty': 'kJ/hr'} - def __init__(self, ID=''): - chilled_water = HeatUtility.get_cooling_agent('chilled_water') - super().__init__(ID, - ins='recirculated_chilled_water', - outs=chilled_water.to_stream(), - thermo=chilled_water.thermo) - self.chilled_water_utilities = set() - - def _design(self): - cwu = self.chilled_water_utilities - if not cwu: - for u in self.system.units: - if u is self: continue - for hu in u.heat_utilities: - if hu.ID == 'chilled_water': cwu.add(hu) - self.design_results['Duty'] = duty = sum([i.duty for i in cwu]) - hu = self.heat_utilities[0] - hu(duty, 330) - used = self.ins[0] - used.mol[0] = sum([i.flow for i in cwu]) - used.T = np.array([i.outlet_utility_stream.T for i in cwu]).mean() - \ No newline at end of file diff --git a/build/lib/biosteam/units/facilities/_cooling_tower.py b/build/lib/biosteam/units/facilities/_cooling_tower.py deleted file mode 100644 index 032e03437..000000000 --- a/build/lib/biosteam/units/facilities/_cooling_tower.py +++ /dev/null @@ -1,96 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon Apr 29 18:28:55 2019 - -@author: yoelr -""" -from . import Facility -from ..decorators import cost -from ... import HeatUtility -# from copy import copy - -__all__ = ('CoolingTower',) #'CoolingTowerWithPowerDemand') - - -# %% - -@cost('Flow rate', 'Cooling water pump', - S=557183, kW=1021, cost=283671, CE=551, n=0.8, BM=3.1) -@cost('Flow rate', 'Cooling tower', - S=557183, kW=1598, cost=1375e3, CE=551, n=0.7, BM=1.5) -class CoolingTower(Facility): - """ - Create a cooling tower with capital cost and power based on the flow rate - of cooling water as in [1]_. - - Parameters - ---------- - ID : str, optional - Unit ID. - - References - ---------- - .. [1] 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 - - """ - network_priority = 1 - _units = {'Flow rate': 'kmol/hr'} - _N_heat_utilities = 1 - _N_outs = _N_ins = 2 - evaporation = 0.01 - blowdown = 0.001 - def __init__(self, ID=''): - cooling_water = HeatUtility.get_cooling_agent('cooling_water') - self.makeup_water = makeup_water = cooling_water.to_stream('cooling_tower_makeup_water') - loss = makeup_water.flow_proxy() - loss.ID = 'evaporation_and_blowdown' - super().__init__(ID, ('return_cooling_water', makeup_water), - (cooling_water.to_stream(), loss), thermo=cooling_water.thermo) - self.cooling_water_utilities = set() - self.agent = cooling_water - - def _load_utility_agents(self): - cwu = self.cooling_water_utilities - agent = self.agent - cwu.clear() - for u in self.system.units: - if u is self: continue - for hu in u.heat_utilities: - if hu.agent is agent: - cwu.add(hu) - - def _design(self): - cwu = self.cooling_water_utilities - if not cwu: self._load_utility_agents() - used = self._ins[0] - hu = self.heat_utilities[0] - self._load_utility_agents() - hu.mix_from(cwu) - - used.imol['7732-18-5'] = \ - self.design_results['Flow rate'] = \ - self.cooling_water = hu.flow - - self._outs[0].T = hu.inlet_utility_stream.T - self.makeup_water.mol[0] = self.cooling_water * (self.evaporation + self.blowdown) - hu.reverse() - -CoolingTower._N_outs = CoolingTower._N_ins = 2 - -# class CoolingTowerWithPowerDemand(CoolingTower): -# _has_power_utility = True -# _N_heat_utilities = 1 -# cost_options = copy(CoolingTower.cost_items) -# cost_options['Cooling tower'].kW = 0.1 -# def _cost(self): -# super()._cost() -# q = self._molar_flow # kmol/hr -# hu = self.heat_utilities[0] -# cw = hu.cooling_agents['Cooling water'] -# hu.ID = 'Cooling water' -# hu.flow = -q -# hu.cost = -q*cw.price_kmol \ No newline at end of file diff --git a/build/lib/biosteam/units/facilities/_process_water_center.py b/build/lib/biosteam/units/facilities/_process_water_center.py deleted file mode 100644 index 9646da9f0..000000000 --- a/build/lib/biosteam/units/facilities/_process_water_center.py +++ /dev/null @@ -1,92 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed Jul 17 18:43:39 2019 - -@author: yoelr -""" -from . import Facility -from ..decorators import cost - -__all__ = ('ProcessWaterCenter',) - -@cost('Makeup water flow rate', 'Makeup water pump', - CE=551, kW=20*0.7457, cost=6864, S=155564, n=0.8, BM=3.1) -@cost('Process water flow rate', 'Process water pump', - CE=551, kW=75*0.7457, cost=15292, S=518924, n=0.8, BM=3.1) -@cost('Process water flow rate', 'Tank', - CE=522, cost=250e3, S=451555, n=0.7, BM=1.7) -class ProcessWaterCenter(Facility): - """ - Create a ProcessWaterCenter object that takes care of balancing the amount - of make-up water required for the process. The capital cost and power - are based on the flow rate of process and make-up water as in [1]_. - - Parameters - ---------- - ins : stream sequence - [0] Recycle water. - - [1] Make-up water. - outs : stream - [0] Process water. - - [1] Waste. - makeup_water_streams : streams, optional - All fresh makeup water streams (must be a subset of `process_water_streams`). - process_water_streams : streams, optional - All process water streams (including makeup water streams). - - References - ---------- - .. [1] 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 - - """ - network_priority = 2 - _N_ins = 2 - _N_outs = 2 - _units = {'Makeup water flow rate': 'kg/hr', - 'Process water flow rate': 'kg/hr'} - def __init__(self, ID='', ins=None, outs=(), - makeup_water_streams=None, - process_water_streams=None): - Facility.__init__(self, ID, ins, outs) - self.makeup_water_streams = makeup_water_streams - self.process_water_streams = process_water_streams - - def _assert_compatible_property_package(self): pass - - def update_process_water(self): - process_water_streams = self.process_water_streams - s_process, _ = self.outs - process_water = sum([stream.imol['7732-18-5'] - for stream in process_water_streams]) - s_process.imol['7732-18-5'] = process_water - - def update_makeup_water(self): - makeup_water_streams = self.makeup_water_streams - _, s_makeup = self.ins - s_makeup.imol['7732-18-5'] = sum([stream.imol['7732-18-5'] - for stream in makeup_water_streams]) - - def _run(self): - self.update_process_water() - self.update_makeup_water() - s_recycle, s_makeup = self._ins - s_process, s_waste = self.outs - makeup_water = s_makeup.F_mol - recycle_water = s_recycle.F_mol - process_water = s_process.F_mol - waste_water = recycle_water + makeup_water - process_water - if waste_water < 0: - s_makeup.imol['7732-18-5'] -= waste_water - waste_water = 0 - s_waste.imol['7732-18-5'] = waste_water - Design = self.design_results - Design['Process water flow rate'] = (process_water + waste_water) * 18.015 - Design['Makeup water flow rate'] = makeup_water * 18.015 - - \ No newline at end of file diff --git a/build/lib/biosteam/units/factories.py b/build/lib/biosteam/units/factories.py deleted file mode 100644 index b6faded0f..000000000 --- a/build/lib/biosteam/units/factories.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Sat Jun 15 00:46:41 2019 - -@author: yoelr -""" -from .. import Unit, units -from ..utils import format_unit_name, static_flow_and_phase -from . import decorators -import pandas as pd -import numpy as np - -_add_cost = decorators.add_cost -_index = np.array(('Basis', - 'Units', - 'Size', - 'Upper bound', - 'CEPCI', - 'Cost (USD)', - 'Exponent', - 'Electricity (kW)', - 'Installation factor', - 'Number')) - - -__all__ = ('df2unit', 'xl2dct', 'xl2mod') - -def df2unit(clsname, cost_items, *, supercls=None, metacls=None): - """Return Unit subclass from cost_options DataFrame.""" - superclasses = (supercls,) if supercls else (Unit,) - if not metacls: metacls = type(supercls) - cls = metacls.__new__(metacls, clsname, superclasses, {}) - for ID in cost_items: - _add_cost(cls, ID, *cost_items[ID], None) - return cls - -def df2dct(df): - dct = {} - for name_sim in df.columns.levels[0]: - cost_items = df[name_sim] - if '-' in name_sim: - sim, name = name_sim.split('-') - is_static = False - else: - name = name_sim - sim = 'Unit' - is_static = True - name = format_unit_name(name) - try: - if is_static: - supercls = Unit - else: - supercls = getattr(units, sim) - except: - supername = ''.join([i.capitalize() for i in sim.split(' ')]) - try: supercls = getattr(units, supername) - except AttributeError: - raise ValueError(f"invalid simulation option '{sim}'") - dct[name] = df2unit(name, cost_items, supercls=supercls) - return dct - -def xl2dct(file, sheet_name=0): - """Return dictionary of unit subclasses from excel file.""" - return df2dct(pd.read_excel(file, header=[0, 1])) - -def xl2mod(file, module, sheet_name=0): - dct = xl2dct(file, sheet_name) - for i, j in dct.items(): - setattr(module, i, j) - j.__module__ = module.__name__ - if not hasattr(module, '__all__'): - module.__all__ = tuple(dct) - - - - - - \ No newline at end of file diff --git a/build/lib/biosteam/utils/__init__.py b/build/lib/biosteam/utils/__init__.py deleted file mode 100644 index 909550e90..000000000 --- a/build/lib/biosteam/utils/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Mon Apr 16 11:49:52 2018 - -@author: Yoel Rene Cortes-Pena -""" -from thermosteam.utils import colors -from . import (misc, - plotting, - tictoc, - not_implemented_method, - piping, - stream_link_options, - unit_warnings, - functors, - bounded_numerical_specification) - -__all__ = ('colors', - 'misc', - 'plotting', - 'tictoc', - 'not_implemented_method', - 'functors', - *plotting.__all__, - *not_implemented_method.__all__, - *misc.__all__, - *tictoc.__all__, - *piping.__all__, - *stream_link_options.__all__, - *unit_warnings.__all__, - *functors.__all__, - *bounded_numerical_specification.__all__) - -from .bounded_numerical_specification import * -from .not_implemented_method import * -from .misc import * -from .plotting import * -from .tictoc import * -from .piping import * -from .stream_link_options import * -from .unit_warnings import * -from .functors import * \ No newline at end of file diff --git a/build/lib/biosteam/utils/bounded_numerical_specification.py b/build/lib/biosteam/utils/bounded_numerical_specification.py deleted file mode 100644 index 038e6d515..000000000 --- a/build/lib/biosteam/utils/bounded_numerical_specification.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Fri Mar 13 12:43:44 2020 - -@author: yoelr -""" -from scipy.optimize import brentq - -__all__ = ('BoundedNumericalSpecification',) - -class BoundedNumericalSpecification: - __slots__ = ('f', 'a', 'b', 'kwargs') - - def __init__(self, f, a, b, **kwargs): - self.f = f - self.a = a - self.b = b - self.kwargs = kwargs - - def __call__(self): - return brentq(self.f, self.a, self.b, **self.kwargs) - - def __repr__(self): - return f"{type(self).__name__}(f={self.f}, a={self.a}, b={self.b}, kwargs={self.kwargs}" - \ No newline at end of file diff --git a/build/lib/biosteam/utils/functors.py b/build/lib/biosteam/utils/functors.py deleted file mode 100644 index 9ae85d557..000000000 --- a/build/lib/biosteam/utils/functors.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Tue Feb 11 02:11:08 2020 - -@author: yoelr -""" -__all__ = ('ExponentialFunctor',) - -class ExponentialFunctor: - """ - Create an ExponentialFunctor object for computing equations of the - form :math:`f(S) = A \cdot S^n`. - - Parameters - ---------- - A : float - Linear coefficient. - n : float - Exponential coefficient. - - Attributes - ---------- - A : float - Linear coefficient. - n : float - Exponential coefficient. - - Examples - -------- - >>> from biosteam.utils import ExponentialFunctor - >>> f_exp = ExponentialFunctor(A=10.0, n=2.0) - >>> f_exp(2.0) - 40.0 - """ - __slots__ = ('A', 'n') - - def __init__(self, A, n): - self.A = A - self.n = n - - def __call__(self, S): - return self.A * S ** self.n - - def __repr__(self): - return f"{type(self).__name__}(A={self.A}, n={self.n})" - diff --git a/build/lib/biosteam/utils/misc.py b/build/lib/biosteam/utils/misc.py deleted file mode 100644 index a7411f153..000000000 --- a/build/lib/biosteam/utils/misc.py +++ /dev/null @@ -1,79 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed Dec 5 17:24:01 2018 - -This module includes arbitrary classes and functions. - -@author: Guest Group -""" -import biosteam as bst - -__all__ = ('factor', 'checkbounds', 'approx2step', 'strtuple', - 'format_unit_line', 'format_unit_name') - -# %% Number functions - -def factor(base_units, new_units): - if base_units == new_units: return 1 - else: return bst._Q(1, base_units).to(new_units).magnitude - -def checkbounds(x, bounds): - return bounds[0] < x < bounds[1] - -def approx2step(val, x0, dx): - """Approximate value, val, to closest increment/step, dx, starting from x0.""" - while True: - if x0 > val: break - x0 += dx - return x0 - - -# %% String functions - -def strtuple(iterable): - """Return string of all items in the tuple""" - string = '' - function = type(strtuple) - for i in iterable: - if isinstance(i , function): - string += i.__name__ + ', ' - else: - string += str(i) + ', ' - string = string.rstrip(', ') - string = '(' + string + ')' - return string - -def format_unit_line(line): - line = line.replace('_', ' ') - words = [] - word = '' - for i in line: - if i.isupper(): - words.append(word) - word = i - else: - word += i - words.append(word) - line = '' - for word in words: - N_letters = len(word) - if N_letters > 1: - line += word + ' ' - else: - line += word - line = line.strip(' ') - first_word, *rest = line.split(' ') - words = [first_word[0].capitalize() + first_word[1:]] - for word in rest: - if not all([i.isupper() for i in word]): - word = word.lower() - words.append(word) - return ' '.join(words) - -def format_unit_name(name): - words = name.split(' ') - new_words = [] - for i in words: - new_words.append(i[0].capitalize() + i[1:]) - return ''.join(new_words) - \ No newline at end of file diff --git a/build/lib/biosteam/utils/not_implemented_method.py b/build/lib/biosteam/utils/not_implemented_method.py deleted file mode 100644 index f6ed3d700..000000000 --- a/build/lib/biosteam/utils/not_implemented_method.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Tue Jul 9 00:35:01 2019 - -@author: yoelr -""" -__all__ = ('NotImplementedMethod',) - -class NotImplementedMethodType: - __slots__ = () - - @property - def __name__(self): return "NotImplementedMethod" - @property - def __doc__(self): return None - def __new__(self): return NotImplementedMethod - def __call__(self): pass - def __bool__(self): return False - def __repr__(self): return "NotImplementedMethod" - -NotImplementedMethod = object.__new__(NotImplementedMethodType) \ No newline at end of file diff --git a/build/lib/biosteam/utils/piping.py b/build/lib/biosteam/utils/piping.py deleted file mode 100644 index f02feef7b..000000000 --- a/build/lib/biosteam/utils/piping.py +++ /dev/null @@ -1,482 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed Dec 5 16:47:33 2018 - -This module includes classes and functions relating Stream objects. - -@author: Yoel Cortes-Pena -""" -from thermosteam import Stream, MultiStream - -__all__ = ('MissingStream', 'Ins', 'Outs', 'Sink', 'Source', - 'as_stream', 'as_upstream', 'as_downstream', - 'materialize_connections') - -isa = isinstance - -# %% Utilities - -def pipe_info(source, sink): - """Return stream information header.""" - # First line - if source is None: - source = '' - else: - source = f' from {repr(source)}' - if sink is None: - sink = '' - else: - sink = f' to {repr(sink)}' - return f"{source}{sink}" - -def as_stream(stream): - if isa(stream, Stream): - return stream - elif isa(stream, str): - return Stream(stream) - elif stream is None: - return MissingStream(None, None) - -def as_upstream(stream, sink): - stream = as_stream(stream) - stream._sink = sink - return stream - -def as_downstream(stream, source): - stream = as_stream(stream) - stream._source = source - return stream - -def materialize_connections(streams): - for stream in streams: - if not stream: stream.materialize_connection() - -# %% Dummy Stream object - -class MissingStream: - """ - Create a MissingStream object that acts as a dummy in Ins and Outs - objects until replaced by an actual Stream object. - """ - __slots__ = ('_source', '_sink') - - def __init__(self, source, sink): - self._source = source - self._sink = sink - - def materialize_connection(self, ID=""): - """ - Disconnect this missing stream from any unit operations and - replace it with a material stream. - """ - source = self._source - sink = self._sink - assert source and sink, ( - "both a source and a sink is required to materialize connection") - material_stream = Stream(ID, thermo=source.thermo) - source._outs.replace(self, material_stream) - sink._ins.replace(self, material_stream) - - def isempty(self): - return True - - @property - def source(self): - self._source - - @property - def sink(self): - self._sink - - def __bool__(self): - return False - - def __repr__(self): - return f'MissingStream(source={self.source}, sink={self.sink})' - - def __str__(self): - return 'missing stream' - - -# %% Utilities - -def n_missing(ub, N): - assert ub >= N, f"size of streams exceeds {ub}" - return ub - N - -# %% List objects for input and output streams - -class StreamSequence: - """ - Abstract class for a sequence of streams for a Unit object. - - Abstract methods: - * _dock(self, stream) -> Stream - * _redock(self, stream) -> Stream - * _undock(self) -> None - * _load_missing_stream(self) - - """ - __slots__ = ('_size', '_streams', '_fixed_size') - - def __init__(self, size, streams, thermo, fixed_size): - self._size = size - self._fixed_size = fixed_size - dock = self._dock - redock = self._redock - if streams == (): - self._streams = [dock(Stream(thermo=thermo)) for i in range(size)] - else: - if fixed_size: - self._initialize_missing_streams() - if streams: - if isa(streams, str): - self._streams[0] = dock(Stream(streams, thermo=thermo)) - elif isa(streams, (Stream, MultiStream)): - self._streams[0] = redock(streams) - else: - N = len(streams) - n_missing(size, N) # Assert size is not too big - self._streams[:N] = [redock(i) if isa(i, Stream) - else dock(Stream(i, thermo=thermo)) for i in streams] - else: - if streams: - if isa(streams, str): - self._streams = [dock(Stream(streams, thermo=thermo))] - elif isa(streams, (Stream, MultiStream)): - self._streams = [redock(streams)] - else: - self._streams = [redock(i) if isa(i, Stream) - else dock(Stream(i, thermo=thermo)) - for i in streams] - else: - self._initialize_missing_streams() - - def _create_missing_stream(self): - return MissingStream(None, None) - - def _create_N_missing_streams(self, N): - return [self._create_missing_stream() for i in range(N)] - - def _initialize_missing_streams(self): - #: All input streams - self._streams = self._create_N_missing_streams(self._size) - - def __add__(self, other): - return self._streams + other - def __radd__(self, other): - return other + self._streams - - def _dock(self, stream): return stream - def _redock(self, stream): return stream - def _undock(self, stream): pass - - def _set_streams(self, slice, streams): - all_streams = self._streams - for stream in all_streams[slice]: self._undock(stream) - all_streams[slice] = streams - for stream in all_streams: - self._redock(stream) - if self._fixed_size: - size = self._size - N_streams = len(all_streams) - if N_streams < size: - N_missing = n_missing(size, N_streams) - if N_missing: - all_streams[N_streams: size] = self._create_N_missing_streams(N_missing) - - @property - def size(self): - return self._streams.__len__() - - def __len__(self): - return self._streams.__len__() - - def _set_stream(self, int, stream): - self._undock(self._streams[int]) - self._redock(stream) - self._streams[int] = stream - - def replace(self, stream, other_stream): - index = self.index(stream) - self[index] = other_stream - - def index(self, stream): - return self._streams.index(stream) - - def pop(self, index): - streams = self._streams - if self._fixed_size: - stream = streams[index] - missing_stream = self._create_missing_stream() - self.replace(stream, missing_stream) - else: - stream = streams.pop(index) - return stream - - def remove(self, stream): - streams = self._streams - self._undock(stream) - if self._fixed_size: - missing_stream = self._create_missing_stream() - self.replace(stream, missing_stream) - else: - streams.remove(stream) - - def clear(self): - if self._fixed_size: - self._initialize_missing_streams() - else: - self._streams.clear() - - def __iter__(self): - yield from self._streams - - def __getitem__(self, index): - return self._streams[index] - - def __setitem__(self, index, item): - if isa(index, int): - if item: - assert isa(item, Stream), ( - f"'{type(self).__name__}' object can only contain " - f"'Stream' objects; not '{type(item).__name__}'") - else: - item = self._create_missing_stream() - self._set_stream(index, item) - elif isa(index, slice): - streams = [] - for stream in item: - if stream: - assert isa(stream, Stream), ( - f"'{type(self).__name__}' object can only contain " - f"'Stream' objects; not '{type(stream).__name__}'") - else: - stream = self._create_missing_stream() - streams.append(stream) - self._set_streams(index, item) - else: - raise TypeError("Only intergers and slices are valid " - f"indices for '{type(self).__name__}' objects") - - def __repr__(self): - return repr(self._streams) - - -class Ins(StreamSequence): - """Create an Ins object which serves as input streams for a Unit object.""" - __slots__ = ('_sink', '_fixed_size') - - def __init__(self, sink, size, streams, thermo, fixed_size=True): - self._sink = sink - super().__init__(size, streams, thermo, fixed_size) - - @property - def sink(self): - return self._sink - - def _create_missing_stream(self): - return MissingStream(None, self._sink) - - def _dock(self, stream): - stream._sink = self._sink - return stream - - def _redock(self, stream): - sink = stream._sink - if sink: - ins = sink._ins - if ins is not self: - ins.remove(stream) - stream._sink = self._sink - else: - stream._sink = self._sink - return stream - - def _undock(self, stream): - stream._sink = None - - -class Outs(StreamSequence): - """Create an Outs object which serves as output streams for a Unit object.""" - __slots__ = ('_source',) - - def __init__(self, source, size, streams, thermo, fixed_size=True): - self._source = source - super().__init__(size, streams, thermo, fixed_size) - - @property - def source(self): - return self._source - - def _create_missing_stream(self): - return MissingStream(self._source, None) - - def _dock(self, stream): - stream._source = self._source - return stream - - def _redock(self, stream): - source = stream._source - if source: - outs = source._outs - if outs is not self: - # Remove from source - outs.remove(stream) - stream._source = self._source - else: - stream._source = self._source - return stream - - def _undock(self, stream): - stream._source = None - - - -# %% Sink and Source object for piping notation - -class Sink: - """ - Create a Sink object that connects a stream to a unit using piping - notation: - - Parameters - ---------- - stream : Stream - index : int - - Examples - -------- - First create a stream and a Mixer: - - .. code-block:: python - - >>> stream = Stream('s1') - >>> unit = Mixer('M1') - - Sink objects are created using -pipe- notation: - - .. code-block:: python - - >>> stream-1 - - - Use pipe notation to create a sink and connect the stream: - - .. code-block:: python - - >>> stream-1-unit - >>> M1.show() - - Mixer: M1 - ins... - [0] Missing stream - [1] s1 - phase: 'l', T: 298.15 K, P: 101325 Pa - flow: 0 - outs... - [0] d27 - phase: 'l', T: 298.15 K, P: 101325 Pa - flow: 0 - - """ - __slots__ = ('stream', 'index') - def __init__(self, stream, index): - self.stream = stream - self.index = index - - # Forward pipping - def __sub__(self, unit): - unit.ins[self.index] = self.stream - return unit - - # Backward pipping - __pow__ = __sub__ - - def __repr__(self): - return '<' + type(self).__name__ + ': ' + self.stream.ID + '-' + str(self.index) + '>' - - -class Source: - """ - Create a Source object that connects a stream to a unit using piping - notation: - - Parameters - ---------- - stream : Stream - index : int - - Examples - -------- - First create a stream and a Mixer: - - .. code-block:: python - - >>> stream = Stream('s1') - >>> unit = Mixer('M1') - - Source objects are created using -pipe- notation: - - .. code-block:: python - - >>> 1**stream - - - Use -pipe- notation to create a source and connect the stream: - - .. code-block:: python - - >>> unit**0**stream - >>> M1.show() - - Mixer: M1 - ins... - [0] Missing stream - [1] Missing stream - outs... - [0] s1 - phase: 'l', T: 298.15 K, P: 101325 Pa - flow: 0 - - """ - __slots__ = ('stream', 'index') - def __init__(self, stream, index): - self.stream = stream - self.index = index - - # Forward pipping - def __rsub__(self, unit): - unit.outs[self.index] = self.stream - return unit - - # Backward pipping - __rpow__ = __rsub__ - - def __repr__(self): - return '<' + type(self).__name__ + ': ' + str(self.index) + '-' + self.stream.ID + '>' - -# %% Pipping - -def __sub__(self, index): - if isinstance(index, int): - return Sink(self, index) - elif isinstance(index, Stream): - raise TypeError("unsupported operand type(s) for -: " - f"'{type(self)}' and '{type(index)}'") - return index.__rsub__(self) - -def __rsub__(self, index): - if isinstance(index, int): - return Source(self, index) - elif isinstance(index, Stream): - raise TypeError("unsupported operand type(s) for -: " - "'{type(self)}' and '{type(index)}'") - return index.__sub__(self) - -Stream.__pow__ = Stream.__sub__ = __sub__ # Forward pipping -Stream.__rpow__ = Stream.__rsub__ = __rsub__ # Backward pipping -Stream.sink = property(lambda self: self._sink) -Stream.source = property(lambda self: self._source) -Stream._basic_info = lambda self: (f"{type(self).__name__}: {self.ID or ''}" - f"{pipe_info(self._source, self._sink)}\n") diff --git a/build/lib/biosteam/utils/plotting.py b/build/lib/biosteam/utils/plotting.py deleted file mode 100644 index 2583c66fb..000000000 --- a/build/lib/biosteam/utils/plotting.py +++ /dev/null @@ -1,172 +0,0 @@ -import matplotlib.pyplot as plt -import matplotlib.patches as patches -from thermosteam.utils import colors -import numpy as np - -__all__ = ('DoubleColorCircle', 'DoubleColorLegend') - -# %% Legend handlers - -def make_patch(pos, width, height, Patch, handlepatch, kwargs): - if issubclass(Patch , patches.Circle): - patch = Patch(pos+[width/2,height/2], height/2, - transform=handlepatch.get_transform(), **kwargs) - elif issubclass(Patch, patches.Rectangle): - patch = Patch(pos, width, height, - transform=handlepatch.get_transform(), **kwargs) - else: - raise NotImplementedError(f"patch '{Patch}' not implemented") - handlepatch.add_artist(patch) - return patch - -class DoubleColor: - __slots__ = ('leftpatches', 'rightpatches', 'Patch', 'patches', 'both') - - def __init__(self, leftpatches, rightpatches, morepatches=None, - shape='Rectangle', both={}): - self.leftpatches = leftpatches - self.rightpatches = rightpatches - self.Patch = getattr(patches, shape) - self.patches = morepatches or [] - self.both = both - - def legend_artist(self, legend, handle, fontsize, patch): - x0, y0 = patch.xdescent, patch.ydescent - width, height = patch.width/2, patch.height - leftpos = np.array([x0, y0]) - rightpos = np.array([x0+width, y0]) - Patch = self.Patch - for l in self.leftpatches: - lpatch = make_patch(leftpos, width, height, Patch, patch, l) - for r in self.rightpatches: - rpatch = make_patch(rightpos, width, height, Patch, patch, r) - patch.add_artist(rpatch) - for patch in self.patches: - patch.legend_artist(legend, handle, fontsize, patch) - both = self.both - if both: - if 'fill' not in both: - both['fill'] = False - if 'edgecolor' not in both: - both['edgecolor'] = 'k' - patch.add_artist(patches.Rectangle(leftpos, width*2, height, - transform=patch.get_transform(), **both)) - return lpatch - - -class DoubleColorCircle: - __slots__ = ('left', 'right', 'both') - - def __init__(self, left, right, both): - self.left = left - self.right = right - self.both = both - - def legend_artist(self, legend, handle, fontsize, patch): - x0, y0 = patch.xdescent, patch.ydescent - width, height = patch.width, patch.height - pos = np.array([x0+width/2, y0+height/2]) - leftpatch = patches.Arc(pos, width/2, width/2, - angle=.0, theta1= 90.0, theta2=-90.0, - **self.left, hatch = 20*'o', - transform=patch.get_transform()) - rightpatch = patches.Arc(pos, width/2, width/2, - angle=.0, theta1=-90.0, theta2= 90.0, - **self.right, hatch = 20*'o', - transform=patch.get_transform()) - patch.add_artist(leftpatch) - patch.add_artist(rightpatch) - both = self.both - if both: - if 'fill' not in both: - both['fill'] = False - if 'edgecolor' not in both: - both['edgecolor'] = 'k' - patch.add_artist(patches.Arc(pos, width/2, width/2, - transform=patch.get_transform(), - **both)) - return leftpatch - - -class TwoColorArrow: - __slots__ = ('left', 'right', 'pos') - - def __init__(self, left, right, pos='mid'): - self.left = left - self.right = right - self.pos = pos - - def legend_artist(self, legend, handle, fontsize, patch): - x0, y0 = patch.xdescent, patch.ydescent - width = patch.width - height = patch.height - pos = self.pos - if pos=='mid': - y0 += height/2 - elif pos=='bot': - pass - elif pos=='top': - y0 += height - else: - ValueError('invalid pos in ArrowPatch object') - leftpatch = patches.Arrow(x0, y0, width/2, 0, - **self.left) - rightpatch = patches.Arrow(x0+width/2, y0, width/2, 0, - **self.right) - patch.add_artist(leftpatch) - patch.add_artist(rightpatch) - return leftpatch - - -class Legend: - __slots__ = ('handler_map') - - def __init__(self, handler_map=None): - self.handler_map = handler_map or {} - - def legend(self, loc='upper right', **kwargs): - hmap = self.handler_map - names = hmap.keys() - plt.legend(names, names, handler_map=self.handler_map, prop=kwargs, - loc=loc) - - -class DoubleColorLegend(Legend): - __slots__ = () - - def __init__(self, key_leftrightpatches=None, handler_map=None): - self.handler_map = handler_map or {} - if key_leftrightpatches: - for key, left_right in key_leftrightpatches.items(): - handler_map[key] = DoubleColor(*left_right) - - def add_box(self, name, - leftcolor=colors.orange_tint.RGBn, - rightcolor=colors.blue_tint.RGBn, - both={}): - self.handler_map[name] = DoubleColor([{'facecolor': leftcolor}], - [{'facecolor': rightcolor}], - None, 'Rectangle', both=both) - - def add_circle(self, name, - leftcolor=colors.orange_shade.RGBn, - rightcolor=colors.blue_shade.RGBn, - both={}): - self.handler_map[name] = DoubleColorCircle( - {'color': leftcolor, - 'edgecolor': 'black'}, - {'color': rightcolor, - 'edgecolor': 'black'}, - both=both) - - # def add2key(self, key, patch, side='left'): - # db = self.handler_map[key] - # sidepatches = getattr(db, side+'patches') - # sidepatches.append(patch) - - # def newkey(self, key, leftpatches, rightpatches, patches=None, shape='Rectangle', **kwargs): - # if isinstance(leftpatches, dict): - # leftpatches = [leftpatches] - # if isinstance(rightpatches, dict): - # rightpatches = [rightpatches] - # self.handler_map[key] = DoubleColor(leftpatches, rightpatches, patches, shape, **kwargs) \ No newline at end of file diff --git a/build/lib/biosteam/utils/stream_link_options.py b/build/lib/biosteam/utils/stream_link_options.py deleted file mode 100644 index 59ced43a4..000000000 --- a/build/lib/biosteam/utils/stream_link_options.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Fri Dec 27 00:45:47 2019 - -@author: yoelr -""" -from collections import namedtuple - -__all__ = ('StreamLinkOptions', - 'static', - 'static_flow', - 'static_flow_and_phase', - 'static_link_options', - 'static_flow_link_options', - 'static_flow_and_phase_options') - -# %% Linking options - -StreamLinkOptions = namedtuple('StreamLinkOptions', ('flow', 'phase', 'TP'), module=__name__) -static_link_options = StreamLinkOptions(flow=True, TP=True, phase=True) -static_flow_and_phase_options = StreamLinkOptions(flow=True, TP=False, phase=True) -static_flow_link_options = StreamLinkOptions(flow=True, TP=False, phase=False) - -def static(cls): - cls._stream_link_options = static_link_options - cls._N_ins = cls._N_outs = 1 - return cls - -def static_flow(cls): - cls._stream_link_options = static_flow_link_options - cls._N_ins = cls._N_outs = 1 - return cls - -def static_flow_and_phase(cls): - cls._stream_link_options = static_flow_and_phase_options - cls._N_ins = cls._N_outs = 1 - return cls - -del namedtuple \ No newline at end of file diff --git a/build/lib/biosteam/utils/tictoc.py b/build/lib/biosteam/utils/tictoc.py deleted file mode 100644 index 3cb7ce54b..000000000 --- a/build/lib/biosteam/utils/tictoc.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon Mar 18 11:28:33 2019 - -@author: yoelr -""" -import time -import numpy as np - -__all__ = ('TicToc',) - -class TicToc: - """Create a TicToc class with tic toc functions that measure elapsed time.""" - __slots__ = ['ID', 'record', '_start'] - - def __init__(self, ID=None): - self.ID = ID - self.record = [] #: [list] elapsed times from tic toc functions - self._start = None - - def tic(self): - """Start timer.""" - # Marks the beginning of a time interval - self._start = time.clock() - - def toc(self): - """Record time interval since last 'tic'.""" - # Appends time difference - try: self.record.append(self.elapsed_time) - except TypeError: - if self._start is None: - raise RuntimeError("Must run 'tic' before 'toc'.") - - @property - def elapsed_time(self): - return time.clock() - self._start - - @property - def average(self): - """The mean value of elapsed time""" - return np.mean(self.record) - - def __repr__(self): - ID = ': ' + self.ID if self.ID else '' - return (f"<{type(self).__name__}{ID}, average={self.average:.2g} seconds>") - - def _info(self): - ID = ': ' + self.ID if self.ID else '' - record = np.array(self.record) - average = self.average - return (f"{type(self).__name__}{ID}\n" + - f" average: {average:.2g} second\n" - f" record: {record}\n" ) - def show(self): - print(self._info()) - \ No newline at end of file diff --git a/build/lib/biosteam/utils/unit_warnings.py b/build/lib/biosteam/utils/unit_warnings.py deleted file mode 100644 index fcc6893d3..000000000 --- a/build/lib/biosteam/utils/unit_warnings.py +++ /dev/null @@ -1,86 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed Dec 18 07:19:49 2019 - -@author: yoelr -""" -from warnings import warn -from ..exceptions import message_with_object_stamp - -__all__ = ('UnitWarning', - 'DesignWarning', - 'CostWarning', - 'lb_warning', - 'ub_warning', - 'bounds_warning') - -class UnitWarning(Warning): - """Warning regarding unit operations.""" - - @classmethod - def from_source(cls, source, msg): - """Return a DesignWarning object with source description.""" - msg= message_with_object_stamp(source, msg) - return cls(msg) - -class DesignWarning(UnitWarning): - """Warning regarding design constraints.""" - -class CostWarning(UnitWarning): - """Warning regarding design constraints.""" - -# %% Bounds checking - -def design_warning_with_source(source, msg): - """Return a DesignWarning object with source description.""" - msg= message_with_object_stamp(source, msg) - return DesignWarning(msg) - -def lb_warning(key, value, units, lb, stacklevel, source): - units = ' ' + units if units else '' - try: - msg = f"{key} ({value:.4g}{units}) is out of bounds (minimum {lb:.4g}{units})." - except: # Handle format errors - msg = f"{key} ({value:.4g}{units}) is out of bounds (minimum {lb}{units})." - - warn(DesignWarning.from_source(source, msg), stacklevel=stacklevel) - -def ub_warning(key, value, units, ub, stacklevel, source): - units = ' ' + units if units else '' - try: - msg = f"{key} ({value:.4g}{units}) is out of bounds (maximum {ub:.4g}{units})." - except: # Handle format errors - msg = f"{key} ({value:.4g}{units}) is out of bounds (maximum {ub}{units})." - - warn(DesignWarning.from_source(source, msg), stacklevel=stacklevel) - -def bounds_warning(source, key, value, units, bounds, kind='design'): - """Issue a warning if value is out of bounds. - - Parameters - ---------- - key : str - Name of value. - value : float - units : str - Units of value - bounds : iterable[float, float] - Upper and lower bounds. - - """ - # Warn when value is out of bounds - lb, ub = bounds - if not (lb <= value <= ub): - units = ' ' + units if units else '' - try: - msg = f"{key} ({value:.4g}{units}) is out of bounds ({lb:.4g} to {ub:.4g}{units})." - except: # Handle format errors - msg = f"{key} ({value:.4g}{units}) is out of bounds ({lb} to {ub}{units})." - if kind == 'design': - Warning = DesignWarning - elif kind == 'cost': - Warning = CostWarning - else: - raise ValueError(f"kind must be either 'design' or 'cost', not {repr(kind)}") - warning = Warning.from_source(source, msg) - warn(warning, stacklevel=3) \ No newline at end of file diff --git a/dist/biosteam-2.11.4-py3-none-any.whl b/dist/biosteam-2.11.4-py3-none-any.whl deleted file mode 100644 index ace87d554..000000000 Binary files a/dist/biosteam-2.11.4-py3-none-any.whl and /dev/null differ diff --git a/dist/biosteam-2.11.4.tar.gz b/dist/biosteam-2.11.4.tar.gz deleted file mode 100644 index 687ebeb1e..000000000 Binary files a/dist/biosteam-2.11.4.tar.gz and /dev/null differ diff --git a/dist/biosteam-2.11.5-py3-none-any.whl b/dist/biosteam-2.11.5-py3-none-any.whl deleted file mode 100644 index 479aa0fb5..000000000 Binary files a/dist/biosteam-2.11.5-py3-none-any.whl and /dev/null differ diff --git a/dist/biosteam-2.11.5.tar.gz b/dist/biosteam-2.11.5.tar.gz deleted file mode 100644 index ec6eeacf2..000000000 Binary files a/dist/biosteam-2.11.5.tar.gz and /dev/null differ diff --git a/setup.py b/setup.py index e6455e8c2..d4905708d 100644 --- a/setup.py +++ b/setup.py @@ -12,12 +12,12 @@ name='biosteam', packages=['biosteam'], license='MIT', - version='2.11.5', + version='2.11.7', description='The Biorefinery Simulation and Techno-Economic Analysis Modules', long_description=open('README.rst').read(), author='Yoel Cortes-Pena', install_requires=['IPython>=7.9.0', 'biorefineries>=2.8.1', - 'thermosteam>=0.11.3', 'graphviz>=0.8.3', + 'thermosteam>=0.11.4', 'graphviz>=0.8.3', 'chaospy>=3.0.11', 'pyqt5>=5.12'], python_requires=">=3.6", package_data=