-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #209 from BioSTEAMDevelopmentGroup/easy_input_mode…
…l_stage Set Model parameter distributions and load statements using DataFrame or spreadhseet
- Loading branch information
Showing
2 changed files
with
168 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,8 @@ | ||
# -*- coding: utf-8 -*- | ||
# BioSTEAM: The Biorefinery Simulation and Techno-Economic Analysis Modules | ||
# Copyright (C) 2020-2023, Yoel Cortes-Pena <[email protected]>, | ||
# Yalin Li <[email protected]> | ||
# Copyright (C) 2020-, Yoel Cortes-Pena <[email protected]>, | ||
# Yalin Li <[email protected]>, | ||
# Sarang Bhagwat <[email protected]> | ||
# | ||
# 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 | ||
_ipython_display_ = show | ||
|
||
EasyInputModel = Model | ||
Model.load_parameter_distributions = Model.parameters_from_df |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,8 @@ | ||
# -*- coding: utf-8 -*- | ||
# BioSTEAM: The Biorefinery Simulation and Techno-Economic Analysis Modules | ||
# Copyright (C) 2020-2023, Yoel Cortes-Pena <[email protected]>, Yalin Li <[email protected]> | ||
# Copyright (C) 2020-, Yoel Cortes-Pena <[email protected]>, | ||
# Yalin Li <[email protected]>, | ||
# Sarang Bhagwat <[email protected]> | ||
# | ||
# 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() |