# -*- coding: utf-8 -*-
"""
"""
from dataclasses import dataclass
from thermosteam.utils import AbstractMethod, AbstractClassMethod
from colorpalette import Color
import biosteam as bst
__all__ = ('ProcessModel', 'ScenarioComparison', 'scenario')
def copy(scenario, **kwargs):
for i in scenario.__slots__:
if i not in kwargs: kwargs[i] = getattr(scenario, i)
return scenario.__class__(**kwargs)
def scenario_info(scenario, add_metadata):
slots = scenario.__slots__
if add_metadata:
grey = Color(fg='#878787')
metadata = scenario.metadata
arguments = []
for i in slots:
j = getattr(scenario, i)
if isinstance(j, (bool, str)):
arg = f"{i}={j!r},"
else:
try:
arg = f"{i}={j:.3g},"
except:
arg = f"{i}={j},"
if add_metadata and i in metadata:
comment = '# ' + metadata[i]
comment = grey(comment)
arguments.append(comment)
arguments.append(arg)
continue
arguments.append(
arg
)
if arguments:
clsname = type(scenario).__name__
N_spaces = len(clsname) +1
if N_spaces > 4:
arguments = '\n '.join(arguments)
return (
f"{clsname}("
f"\n {arguments}\n"
")"
)
else:
spaces = N_spaces * ' '
arguments = f'\n{spaces}'.join(arguments)
return (
f"{clsname}({arguments})"
)
else:
return (
f"{type(scenario).__name__}()"
)
def display_scenario(scenario, metadata=True):
print(scenario_info(scenario, metadata))
def iterate_scenario_data(scenario):
for i in scenario.__slots__:
yield (i, getattr(scenario, i))
def scenario_comparison(left, right):
return ScenarioComparison(left, right)
def scenario(cls):
if hasattr(cls, 'metadata'): return cls
cls.metadata = metadata = {}
for i, j in tuple(cls.__dict__.items()):
if i.startswith('__'): continue
if isinstance(j, str):
if j[0] == '#':
delattr(cls, i)
metadata[i] = j.lstrip('# ')
if isinstance(j, tuple) and len(j) == 2 and isinstance(j[-1], str):
value, j = j
if j[0] == '#':
setattr(cls, i, value)
metadata[i] = j.lstrip('# ')
# TODO: get defaults and separate units of measure
cls = dataclass(cls,
init=True, repr=True, eq=True, unsafe_hash=False, frozen=True,
match_args=True, slots=True,
)
cls.__iter__ = iterate_scenario_data
cls._ipython_display_ = cls.show = display_scenario
cls.copy = copy
cls.__sub__ = scenario_comparison
return cls
@dataclass(
init=True, repr=True, eq=True,
unsafe_hash=False, frozen=True,
match_args=True, slots=True,
)
class ScenarioComparison:
left: object
right: object
def show(self):
parameters = ',\n'.join(['left=' + scenario_info(self.left), 'right=' + scenario_info(self.right)])
parameters = parameters.replace('\n', '\n ')
return print(
f'{type(self).__name__}(\n'
f' {parameters}\n'
f')'
)
_ipython_display_ = show
[docs]
class ProcessModel:
"""
ProcessModel objects allow us to write code for many related configurations
with ease. It streamlines the process of creating a model, including:
* Defining **scenarios** to compare.
* Creating the **thermodynamic property package**.
* Forming the **system** from unit operations.
* Setting up the evaluation **model**.
Additionally, all objects created within the process model
(e.g., chemicals, streams, units, systems) can be easily accessed as attributes.
The first step is to inherit from the ProcessModel abstract class.
An abstract class has missing (or "abstract") attributes/methods which
the user must define to complete the class. The user must define a
`Scenario` dataclass, and `as_scenario`, `create_thermo`, `create_system`,
`create_model` methods for the process model to initialize its key components.
It may help to look at how ProcessModel objects are created (approximately):
.. code-block:: python
def __new__(cls, simulate=None, scenario=None, load=True, save=True, **kwargs):
if scenario is None:
self.scenario = cls.Scenario(**kwargs)
else:
# The Scenario object can be initialized through the `as_scenario` class method.
self.scenario = cls.as_scenario(scenario)
# No need to recreate a process model for repeated scenarios.
if load and scenario in cls.cache: return cls.cache[scenario]
self = super().__new__()
# The thermodynamic property package is given by the `create_thermo` method.
self.load_thermo(self.create_thermo())
# If no system is returned by the `create_system` method, a new system is created from flowsheet units.
self.flowsheet = bst.Flowsheet()
system = self.create_system()
if system is None: system = self.flowsheet.create_system()
# This saves the system as self.system and all units/streams as attributes by ID.
# For example, Stream('feedstock') will be stored as self.feestock.
self.load_system(system)
# A Model object is loaded from the `create_model` method.
# The model will be stored as self.model and all parameters and indicators as attributes by function name.
# For example:
#
# @model.indicator
# def MSP(): return self.tea.solve_price(self.product)
#
# ^ This becomes self.MSP.
self.load_model(self.create_model())
if simulate: self.system.simulate()
if save: self.cache[scenario] = self
return self
"""
#: **class-attribute** Class which defines arguments to the process model using
#: the layout of a python dataclass: https://docs.python.org/3/library/dataclasses.html
Scenario: type
#: This method allows the process model to default the scenario.
#: It should return a Scenario object.
default_scenario = AbstractMethod
#: This method allows the process model to interpret objects
#: (e.g., strings, numbers) as a Scenario.
as_scenario = AbstractClassMethod
#: This method should return a model object.
#: The model will be saved as a self.model attribute.
#: All parameters and indicators of the model object will also be saved as
#: attributes by their function names.
initialize = AbstractMethod
#: This method should return a chemicals or thermo object.
#: BioSTEAM will automatically set it as the thermodynmic property package.
create_thermo = AbstractMethod
#: This method should create unit operations and connect them.
#: It can return a system object, optionally. Otherwise, BioSTEAM will
#: take care of creating the system from the units and saves
#: it as the self.system attribute.
#: All streams and unit operations are also saved as attributes by their ID.
create_system = AbstractMethod
#: This method should return a model object.
#: The model will be saved as a self.model attribute.
#: All parameters and indicators of the model object will also be saved as
#: attributes by their function names.
create_model = AbstractMethod
@classmethod
def scenario_hook(cls, scenario, kwargs):
if scenario is None:
if cls.default_scenario:
scenario = cls.default_scenario()
else:
try:
return cls.Scenario(**kwargs)
except:
raise NotImplementedError('missing class method `default_scenario`')
if not isinstance(scenario, cls.Scenario):
if cls.as_scenario:
scenario = cls.as_scenario(scenario)
else:
raise NotImplementedError('missing class method `as_scenario`')
if kwargs: scenario = scenario.copy(**kwargs)
return scenario
def __init_subclass__(cls):
cls.cache = {}
if not hasattr(cls, 'Scenario'):
cls.Scenario = type('Scenario', (), {})
if 'Scenario' in cls.__dict__:
cls.Scenario = scenario(cls.Scenario)
def __new__(cls, scenario=None, *, simulate=True, load=True, save=True, **kwargs):
scenario = cls.scenario_hook(scenario, kwargs)
if load and scenario in cls.cache:
process_model = cls.cache[scenario]
if simulate:
system = process_model.system
if all([i.isempty() for i in system.products]): system.simulate()
return process_model
self = super().__new__(cls)
self.scenario = scenario
self.flowsheet = bst.Flowsheet(repr(self))
thermo = self.create_thermo()
if thermo is NotImplemented:
raise NotImplementedError(f"{cls.__name__!r} is missing the `create_thermo` method")
self.load_thermo(thermo)
bst.main_flowsheet.set_flowsheet(self.flowsheet)
unit_registry = self.flowsheet.unit
unit_registry.open_context_level()
system = self.create_system()
if system is NotImplemented:
raise NotImplementedError(f"{cls.__name__!r} is missing the `create_system` method")
else:
units = unit_registry.close_context_level()
if system is None: system = bst.System.from_units(units=units)
if self.flowsheet is not system.flowsheet: self.flowsheet.update(system.flowsheet)
self.load_system(system)
model = self.create_model()
if model is NotImplemented:
raise NotImplementedError(f"{cls.__name__!r} is missing the `create_model` method")
elif model is None:
raise RuntimeError('`create_model` must return a biosteam.Model object')
self.load_model(model)
if simulate: system.simulate()
if save: cls.cache[scenario] = self
return self
def load_thermo(self, thermo):
bst.settings.set_thermo(thermo)
thermo = bst.settings.get_thermo()
self.chemicals = thermo.chemicals
self.thermo = thermo
def baseline(self):
sample = self.model.get_baseline_scenario()
return sample, self.model(sample)
def load_system(self, system):
self.system = system
self.__dict__.update(self.flowsheet.to_dict())
for i in system.subsystems: self.__dict__[i.ID] = i
for i in system.units: self.__dict__[i.ID] = i
for i in system.streams: self.__dict__[i.ID] = i
def load_model(self, model):
self.model = model
for i in model.parameters:
setattr(self, i.setter.__name__, i)
if i.baseline is not None:
i.setter(i.baseline)
i.last_value = i.baseline
for i in model.optimized_parameters:
setattr(self, i.setter.__name__, i)
for i in model.indicators:
setattr(self, i.getter.__name__, i)
@property
def parameters(self):
return self.model._parameters
@property
def indicators(self):
return self.model._indicators
metrics = indicators
def __repr__(self):
scenario = self.scenario
scenario_name = type(scenario).__name__
process_name = type(self).__name__
N = len(scenario_name)
return process_name + repr(self.scenario)[N:]
[docs]
def show(self, metadata=True):
"""Print representation of process model."""
scenario = self.scenario
scenario_name = type(scenario).__name__
process_name = type(self).__name__
N = len(scenario_name)
info = scenario_info(scenario, metadata)
print(process_name + info[N:])
_ipython_display_ = show