Skip to content

Commit

Permalink
Merge pull request #209 from BioSTEAMDevelopmentGroup/easy_input_mode…
Browse files Browse the repository at this point in the history
…l_stage

Set Model parameter distributions and load statements using DataFrame or spreadhseet
  • Loading branch information
sarangbhagwat authored Nov 1, 2024
2 parents 853ef21 + d181871 commit e2d3942
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 6 deletions.
113 changes: 109 additions & 4 deletions biosteam/evaluation/_model.py
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 <yoelcortes@gmail.com>,
# Yalin Li <mailto.yalin.li@gmail.com>
# Copyright (C) 2020-, Yoel Cortes-Pena <yoelcortes@gmail.com>,
# Yalin Li <mailto.yalin.li@gmail.com>,
# Sarang Bhagwat <sarangb2@gmail.com>
#
# This module implements a filtering feature from the stats module of the QSDsan library:
# QSDsan: Quantitative Sustainable Design for sanitation and resource recovery systems
Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
61 changes: 59 additions & 2 deletions tests/test_evaluation.py
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 <yoelcortes@gmail.com>, Yalin Li <zoe.yalin.li@gmail.com>
# Copyright (C) 2020-, Yoel Cortes-Pena <yoelcortes@gmail.com>,
# Yalin Li <zoe.yalin.li@gmail.com>,
# Sarang Bhagwat <sarangb2@gmail.com>
#
# This module is under the UIUC open-source license. See
# github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/LICENSE.txt
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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()

0 comments on commit e2d3942

Please sign in to comment.