diff --git a/Bioindustrial-Park b/Bioindustrial-Park index 9b65e3ae..40669360 160000 --- a/Bioindustrial-Park +++ b/Bioindustrial-Park @@ -1 +1 @@ -Subproject commit 9b65e3ae6814e285ba05be5debba61d6809ad114 +Subproject commit 4066936085ae314850deda40ff8fae42a76a189b diff --git a/biosteam/__init__.py b/biosteam/__init__.py index b955e550..12c4f09d 100644 --- a/biosteam/__init__.py +++ b/biosteam/__init__.py @@ -13,7 +13,7 @@ """ from __future__ import annotations -__version__ = '2.49.4' +__version__ = '2.50.0' #: Chemical engineering plant cost index (defaults to 567.5 at 2017). CE: float = 567.5 diff --git a/biosteam/process_tools/process_model.py b/biosteam/process_tools/process_model.py index b553f278..127074ec 100644 --- a/biosteam/process_tools/process_model.py +++ b/biosteam/process_tools/process_model.py @@ -2,9 +2,17 @@ """ """ from dataclasses import dataclass -__all__ = ('ProcessModel', 'scenario') +from thermosteam.utils import AbstractMethod +import biosteam as bst -def display_scenario(scenario): +__all__ = ('ProcessModel', 'ScenarioComparison') + +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): slots = scenario.__slots__ units_of_measure = scenario.units_of_measure arguments = [] @@ -27,7 +35,7 @@ def display_scenario(scenario): N_spaces = len(clsname) +1 if N_spaces > 4: arguments = '\n '.join(arguments) - print( + return ( f"{clsname}(" f"\n {arguments}\n" ")" @@ -36,29 +44,119 @@ def display_scenario(scenario): else: spaces = N_spaces * ' ' arguments = f'\n{spaces}'.join(arguments) - print( + return ( f"{clsname}({arguments})" ) else: - print( + return ( f"{type(scenario).__name__}()" ) -def scenario(cls=None, **units_of_measure): - if cls is None: return lambda cls: scenario(cls, **units_of_measure) - cls = dataclass(cls, - init=True, repr=True, eq=True, unsafe_hash=False, frozen=True, - match_args=True, slots=True, - ) - cls.units_of_measure = units_of_measure - cls._ipython_display_ = cls.show = display_scenario - return cls +def display_scenario(scenario): + print(scenario_info(scenario)) + +def iterate_scenario_data(scenario): + for i in scenario.__slots__: + yield (i, getattr(scenario, i)) + +def scenario_comparison(left, right): + return ScenarioComparison(left, right) + +@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 + class ProcessModel: """Abstract class for setting up an all-in-one handle with easy access to all objects related to a process model, including chemicals, streams, units, systems, and model parameters and metrics.""" + default_scenario = AbstractMethod + create_system = AbstractMethod + create_model = AbstractMethod + as_scenario = 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`') + elif 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 '__new__' in cls.__dict__: return + if not hasattr(cls, 'Scenario'): + raise NotImplementedError( + "ProcessModel sublcass missing a 'Scenario' attribute" + ) + if 'Scenario' in cls.__dict__: + Scenario = cls.Scenario + Scenario.units_of_measure = units_of_measure = {} + for i, j in tuple(Scenario.__dict__.items()): + if i.startswith('__'): continue + if isinstance(j, str): + if j[0] == '[' and j[-1] == ']': + delattr(Scenario, i) + units_of_measure[i] = j + if isinstance(j, tuple) and len(j) == 2 and isinstance(j[-1], str): + value, units = j + if units[0] == '[' and units[-1] == ']': + setattr(Scenario, i, value) + units_of_measure[i] = units + + # TODO: get defaults and separate units of measure + cls.Scenario = Scenario = dataclass(cls.Scenario, + init=True, repr=True, eq=True, unsafe_hash=False, frozen=True, + match_args=True, slots=True, + ) + Scenario.items = iterate_scenario_data + Scenario._ipython_display_ = Scenario.show = display_scenario + Scenario.copy = copy + Scenario.__sub__ = scenario_comparison + + + def __new__(cls, *, simulate=True, scenario=None, **kwargs): + scenario = cls.scenario_hook(scenario, kwargs) + if scenario in cls.cache: return cls.cache[scenario] + self = super().__new__(cls) + self.scenario = scenario + self.flowsheet = bst.Flowsheet(repr(self)) + bst.main_flowsheet.set_flowsheet(self.flowsheet) + system = self.create_system() + self.load_system(system) + model = self.create_model() + self.load_model(model) + if simulate: system.simulate() + cls.cache[scenario] = self + return self def baseline(self): sample = self.model.get_baseline_sample() @@ -76,9 +174,10 @@ 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) for i in model.metrics: setattr(self, i.getter.__name__, i) - + @property def parameters(self): return self.model._parameters @@ -87,19 +186,21 @@ def parameters(self): def metrics(self): return self.model._metrics - def _repr(self, m): - 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__}: {len(self.parameters)}-parameters, {len(self.metrics)}-metrics>' + scenario = self.scenario + scenario_name = type(scenario).__name__ + process_name = type(self).__name__ + N = len(scenario_name) + return process_name + repr(self.scenario)[N:] - def _info(self, p, m): - return 'Process' + self.model._info(p, m) + def show(self): + """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) + print(process_name + info[N:]) - def show(self, p=None, m=None): - """Return information on p-parameters and m-metrics.""" - print(self._info(p, m)) _ipython_display_ = show \ No newline at end of file diff --git a/setup.py b/setup.py index 3d0a68d6..730933dd 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ name='biosteam', packages=['biosteam'], license='MIT', - version='2.49.4', + version='2.50.0', description='The Biorefinery Simulation and Techno-Economic Analysis Modules', long_description=open('README.rst', encoding='utf-8').read(), author='Yoel Cortes-Pena', diff --git a/thermosteam b/thermosteam index 58c83cf6..de983847 160000 --- a/thermosteam +++ b/thermosteam @@ -1 +1 @@ -Subproject commit 58c83cf669f0dead8448a4e027ae8b31befbe126 +Subproject commit de983847e4b661c9339e64abafd2322d0620efed