# -*- coding: utf-8 -*-
# BioSTEAM: The Biorefinery Simulation and Techno-Economic Analysis Modules
# Copyright (C) 2020-2023, Yoel Cortes-Pena <yoelcortes@gmail.com>
#
# This module is under the UIUC open-source license. See
# github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/LICENSE.txt
# for license details.
"""
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 __future__ import annotations
from typing import Optional, Iterable
import biosteam as bst
from thermosteam.utils import Registry
from thermosteam import AbstractStream
from ._unit import AbstractUnit
from ._system import System
__all__ = ('main_flowsheet', 'Flowsheet', 'F')
# %% Flowsheet search
class FlowsheetRegistry:
__getitem__ = object.__getattribute__
def clear(self):
self.__dict__.clear()
main_flowsheet.set_flowsheet('default')
def __contains__(self, obj):
dct = self.__dict__
if isinstance(obj, str):
return obj in dct
elif isinstance(obj, Flowsheet):
return obj.ID in dct and dct[obj.ID] is obj
else:
return False
def __setattr__(self, key, value):
raise TypeError(f"'{type(self).__name__}' object does not support attribute assignment")
__setitem__ = __setattr__
def __iter__(self):
return self.__dict__.values().__iter__()
def __delattr__(self, key):
if key == main_flowsheet.ID:
raise AttributeError('cannot delete main flowsheet')
else:
super().__delattr__(key)
def __repr__(self):
return f"<{type(self).__name__}: {', '.join([str(i) for i in self])}>"
def _ipython_display_(self): # pragma: no cover
print(f'{type(self).__name__}:\n ' + '\n '.join([str(i) for i in self]))
[docs]
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: str = "Flowsheet"
#: All flowsheets.
flowsheet: FlowsheetRegistry = FlowsheetRegistry()
def __new__(cls, ID=None):
self = super().__new__(cls)
if ID is None: ID = ''
#: Contains all System objects as attributes.
self.system: Registry = Registry()
#: Contains all Unit objects as attributes.
self.unit: Registry = Registry()
#: Contains all Stream objects as attributes.
self.stream: Registry = Registry()
#: ID of flowsheet.
self._ID: str = ID
#: Temporary flowsheet stack.
self._temporary_stack = []
self.flowsheet.__dict__[ID] = self
return self
def __enter__(self):
"""
Temporarily register all objects in this flowsheet instead of
the main flowsheet.
Examples
--------
>>> import biosteam as bst
>>> bst.settings.set_thermo(['Water'], cache=True)
>>> f = bst.Flowsheet('f')
>>> with f:
... M1 = bst.Mixer('M1')
>>> M1 in bst.main_flowsheet.unit
False
>>> M1 in f.unit
True
"""
self._temporary_stack.append(main_flowsheet.get_flowsheet())
main_flowsheet.set_flowsheet(self)
return self
def __exit__(self, type, exception, traceback):
main_flowsheet.set_flowsheet(self._temporary_stack.pop())
if exception: raise exception
def temporary(self): return self # for backwards compatibility
def __reduce__(self):
return self.from_registries, self.registries
def __getattr__(self, name):
obj = (self.stream.search(name)
or self.unit.search(name)
or self.system.search(name))
if not obj: raise AttributeError(f"no registered item '{name}'")
return obj
def __setattr__(self, key, value):
if self in self.flowsheet.__dict__:
raise AttributeError("cannot register object through flowsheet")
else:
super().__setattr__(key, value)
@property
def ID(self):
return self._ID
@classmethod
def from_registries(cls, ID, stream, unit, system):
flowsheet = super().__new__(cls)
flowsheet.stream = stream
flowsheet.unit = unit
flowsheet.system = system
flowsheet._ID = ID
flowsheet._temporary_stack = []
flowsheet.flowsheet.__dict__[ID] = flowsheet
return flowsheet
@property
def registries(self):
return (self.stream, self.unit, self.system)
def clear(self, reset_ticket_numbers=True):
for registry in self.registries: registry.clear()
if reset_ticket_numbers:
for i in (AbstractStream, AbstractUnit, System): i.ticket_numbers.clear()
def discard(self, ID):
for registry in self.registries: registry.discard(ID)
def remove_unit_and_associated_streams(self, ID):
stream_registry = self.stream
unit = self.unit.pop(ID)
for inlet in unit._ins:
if inlet.source: continue
stream_registry.discard(inlet)
for outlet in unit._outs:
if outlet._sink: continue
stream_registry.discard(outlet)
def update(self, flowsheet):
for registry, other_registry in zip(self.registries, flowsheet.registries):
registry.data.update(other_registry.data)
def to_dict(self):
return {**self.stream.data,
**self.unit.data,
**self.system.data}
[docs]
@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
[docs]
def diagram(self, kind: Optional[int|str]=None, file: Optional[str]=None,
format: Optional[str]=None, display: Optional[bool]=True,
number: Optional[bool]=None, profile: Optional[bool]=None,
label: Optional[bool]=None, title: Optional[str]=None,
**graph_attrs):
"""
Display a `Graphviz <https://pypi.org/project/graphviz/>`__ diagram of
all unit operations.
Parameters
----------
kind :
* 0 or 'cluster': Display all units clustered by system.
* 1 or 'thorough': Display every unit within the path.
* 2 or 'surface': Display only elements listed in the path.
* 3 or 'minimal': Display a single box representing all units.
file :
File name to save diagram.
format:
File format (e.g. "png", "svg"). Defaults to 'png'
display :
Whether to display diagram in console or to return the graphviz
object.
number :
Whether to number unit operations according to their
order in the system path.
profile :
Whether to clock the simulation time of unit operations.
label :
Whether to label the ID of streams with sources and sinks.
"""
if title is None: title = ''
return self.create_system(None).diagram(kind or 'thorough', file, format,
display, number, profile, label,
title, **graph_attrs)
[docs]
def create_system(self, ID: Optional[str]="",
ends: Optional[Iterable[AbstractStream]]=None,
facility_recycle: Optional[AbstractStream]=None,
operating_hours: Optional[float]=None,
**kwargs):
"""
Create a System object from all units and streams defined in the flowsheet.
Parameters
----------
ID :
Name of system.
ends :
End streams of the system which are not products. Specify this
argument if only a section of the complete system is wanted, or if
recycle streams should be ignored.
facility_recycle :
Recycle stream between facilities and system path. This argument
defaults to the outlet of a BlowdownMixer facility (if any).
operating_hours :
Number of operating hours in a year. This parameter is used to
compute annualized properties such as utility cost and material cost
on a per year basis.
"""
return System.from_units(ID, self.unit, ends, facility_recycle,
operating_hours, **kwargs)
[docs]
def __call__(self, ID: str|type[AbstractUnit], strict: Optional[bool]=False):
"""
Return requested biosteam item or a list of all matching items.
Parameters
----------
ID :
ID of the requested item or Unit subclass.
strict :
Whether an exact match is required.
"""
isa = isinstance
if isa(ID, str):
ID = ID.replace(' ', '_')
obj = (self.stream.search(ID)
or self.unit.search(ID)
or self.system.search(ID))
if not obj:
if strict:
raise LookupError(f"no registered item '{ID}'")
else:
obj = [i for i in self.unit if ID in ' '.join([i.__class__.__name__, i.ID])]
N = len(obj)
if N == 0:
raise LookupError(f"no registered item '{ID}'")
elif N == 1:
obj = obj[0]
return obj
elif issubclass(ID, bst.Unit):
cls = ID
obj = [i for i in self.unit if isa(i, cls)]
N = len(obj)
if N == 0:
raise LookupError(f"no registered item '{ID}'")
elif N == 1:
obj = obj[0]
else:
obj = sorted(obj, key=lambda x: x.ID)
return obj
else:
raise TypeError('ID must be either a string or a Unit subclass')
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`.
"""
__slots__ = ()
line = "Main flowsheet"
def set_flowsheet(self, flowsheet, new=False):
"""Set main flowsheet that is updated with new biosteam objects."""
if isinstance(flowsheet, Flowsheet):
dct = flowsheet.__dict__
elif isinstance(flowsheet, str):
if not new and 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')
AbstractStream.registry = dct['stream']
System.registry = dct['system']
AbstractUnit.registry = dct['unit']
object.__setattr__(self, '__dict__', dct)
def get_flowsheet(self):
return self.flowsheet[self.ID]
def __new__(cls, ID):
main_flowsheet.set_flowsheet(ID)
return main_flowsheet
def __repr__(self):
return f'<{type(self).__name__}: {self.ID}>'
#: Main flowsheet where objects are registered by ID.
#: Use the `set_flowsheet` to change the main flowsheet.
F = main_flowsheet = object.__new__(MainFlowsheet)
main_flowsheet.set_flowsheet(
Flowsheet.from_registries(
'default', AbstractStream.registry, AbstractUnit.registry, System.registry
)
)