diff --git a/biosteam/evaluation/_model.py b/biosteam/evaluation/_model.py index 25e702ed..1a68a807 100644 --- a/biosteam/evaluation/_model.py +++ b/biosteam/evaluation/_model.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # BioSTEAM: The Biorefinery Simulation and Techno-Economic Analysis Modules -# Copyright (C) 2020-2023, Yoel Cortes-Pena , -# Yalin Li +# Copyright (C) 2020-, Yoel Cortes-Pena , +# Yalin Li , +# Sarang Bhagwat # # This module implements a filtering feature from the stats module of the QSDsan library: # QSDsan: Quantitative Sustainable Design for sanitation and resource recovery systems @@ -10,10 +11,12 @@ # This module is under the UIUC open-source license. See # github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/LICENSE.txt # for license details. + from scipy.spatial.distance import cdist from scipy.optimize import shgo, differential_evolution import numpy as np import pandas as pd +from chaospy import distributions as shape from ._metric import Metric from ._feature import MockFeature from ._utils import var_indices, var_columns, indices_to_multiindex @@ -27,13 +30,34 @@ from .evaluation_tools import load_default_parameters import pickle -__all__ = ('Model',) +__all__ = ('Model', 'EasyInputModel') def replace_nones(values, replacement): for i, j in enumerate(values): if j is None: values[i] = replacement return values +def codify(statement): + statement = replace_apostrophes(statement) + statement = replace_newline(statement) + return statement + +def replace_newline(statement): + statement = statement.replace('\n', ';') + return statement + +def replace_apostrophes(statement): + statement = statement.replace('’', "'").replace('‘', "'").replace('“', '"').replace('”', '"') + return statement + +def create_function(code, namespace): + def wrapper_fn(statement): + def f(x): + namespace['x'] = x + exec(codify(statement), namespace) + return f + function = wrapper_fn(code) + return function # %% Fix compatibility with new chaospy version @@ -170,6 +194,84 @@ def set_parameters(self, parameters): assert isa(i, Parameter), 'all elements must be Parameter objects' Parameter.check_indices_unique(self.features) + def parameters_from_df(self, df_or_filename, namespace=None): + """ + Load a list (from a DataFrame or spreadsheet) of distributions and statements + to load values for user-selected parameters. + + Parameters + ---------- + df_or_filename : pandas.DataFrame or file path to a spreadsheet of the following format: + Column titles (these must be included, but others may be added for convenience): + 'Parameter name': String + Name of the parameter. + 'Element': String, optional + 'Kind': String, optional + 'Units': String, optional + 'Baseline': float or int + The baseline value of the parameter. + 'Shape': String, one of ['Uniform', 'Triangular'] + The shape of the parameter distribution. + 'Lower': float or int + The lower value defining the shape of the parameter distribution. + 'Midpoint': float or int + The midpoint value defining the shape of a 'Triangular' parameter distribution. + 'Upper': float or int + The upper value defining the shape of the parameter distribution. + 'Load statement': String + A statement executed to load the value of the parameter. The value is stored in + the variable x. A namespace defined in the namespace during EasyInputModel + initialization may be accessed. + E.g., to load a value into an example distillation unit D101's light key recovery, + ensure 'D101' is a key pointing to the D101 unit object in namespace, then + simply include the load statement: 'D101.Lr = x'. New lines in the statement + may be represented by '\n' or ';'. + + namespace : dict, optional + Dictionary used to update the namespace accessed when executing + statements to load values into model parameters. Defaults to the + system's flowsheet dict. + + """ + + df = df_or_filename + if type(df) is not pd.DataFrame: + try: + df = pd.read_excel(df_or_filename) + except: + df = pd.read_csv(df_or_filename) + + if namespace is None: namespace = {} + namespace = self.system.flowsheet.to_dict() | namespace + + param = self.parameter + + for i, row in df.iterrows(): + name = row['Parameter name'] + element = row['Element'] # currently only compatible with String elements + kind = row['Kind'] + units = row['Units'] + baseline = row['Baseline'] + shape_data = row['Shape'] + lower, midpoint, upper = row['Lower'], row['Midpoint'], row['Upper'] + load_statements = row['Load statement'] + + D = None + if shape_data.lower() in ['triangular', 'triangle',]: + D = shape.Triangle(lower, midpoint, upper) + elif shape_data.lower() in ['uniform',]: + if not str(midpoint)=='nan': + raise ValueError(f"The parameter distribution for {name} ({element}) is 'Uniform' but was associated with a given midpoint value.") + D = shape.Uniform(lower, upper) + + param(name=name, + setter=create_function(load_statements, namespace), + element=element, + kind=kind, + units=units, + baseline=baseline, + distribution=D) + def get_parameters(self): """Return parameters.""" return tuple(self._parameters) @@ -1268,4 +1370,7 @@ def _info(self, p, m): 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 + _ipython_display_ = show + +EasyInputModel = Model +Model.load_parameter_distributions = Model.parameters_from_df diff --git a/tests/test_evaluation.py b/tests/test_evaluation.py index 960af67f..8231f307 100644 --- a/tests/test_evaluation.py +++ b/tests/test_evaluation.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # BioSTEAM: The Biorefinery Simulation and Techno-Economic Analysis Modules -# Copyright (C) 2020-2023, Yoel Cortes-Pena , Yalin Li +# Copyright (C) 2020-, Yoel Cortes-Pena , +# Yalin Li , +# Sarang Bhagwat # # This module is under the UIUC open-source license. See # github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/LICENSE.txt @@ -76,6 +78,60 @@ def non_correlated_metric(): model.evaluate() cache[0] = model return model + +def test_parameters_from_df(): + import biosteam as bst + from pandas import DataFrame + from chaospy.distributions import Uniform, Triangle + bst.settings.set_thermo(['Water'], cache=True) + + U101 = bst.Unit('U101') + U102 = bst.Unit('U102') + U103 = bst.Unit('U103') + U101.example_param = 5 + U102.example_param = 8 + U102.test_checker = 0 + U103.example_param = 40 + U103.test_checker = 0 + + sys = bst.System.from_units(units=[U101]) + model = bst.Model(sys) + + example_namespace_var1 = 2 + example_namespace_var2 = 3 + + df_dict = {'Parameter name': ['U101 example parameter', + 'U102 example parameter', + 'U103 example parameter'], + 'Element': ['TEA', 'Fermentation', 'LCA'], + 'Kind': ['isolated', 'coupled', 'isolated'], + 'Units': ['g/g', 'g/L', '%theoretical'], + 'Baseline': [10, 20, 50], + 'Shape': ['Uniform', 'Triangular', 'Triangular'], + 'Lower': [5, 8, 26], + 'Midpoint': [None, 22, 52], + 'Upper': [25, 35, 75], + 'Load statement': ['U101.example_param = x', + 'U102.example_param = x; U102.test_checker=example_namespace_var1', + 'U103.example_param = x\nU103.test_checker=example_namespace_var2'], + } + + model.parameters_from_df(DataFrame.from_dict(df_dict), + namespace={'example_namespace_var1':example_namespace_var1, + 'example_namespace_var2':example_namespace_var2}) + + assert model.parameters[1].baseline == 20 + assert isinstance(model.parameters[0].distribution, Uniform) + assert isinstance(model.parameters[2].distribution, Triangle) + assert model.parameters[0].distribution.lower == 5 + assert model.parameters[0].distribution.upper == 25 + + model.metrics_at_baseline() + assert U101.example_param == 10 + assert U102.example_param == 20 + assert U103.example_param == 50 + assert U102.test_checker == example_namespace_var1 + assert U103.test_checker == example_namespace_var2 def test_pearson_r(): model = create_evaluation_model() @@ -127,7 +183,7 @@ def test_kendall_tau(): def test_model_index(): import biosteam as bst - bst.settings.set_thermo(['Water']) + bst.settings.set_thermo(['Water'], cache=True) with bst.System() as sys: H1 = bst.HXutility('H1', ins=bst.Stream('feed', Water=1000), T=310) @@ -292,4 +348,5 @@ def set_M2_tau(i): test_model_sample() test_copy() test_model_exception_hook() + test_parameters_from_df() test_kolmogorov_smirnov_d() \ No newline at end of file