Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Easier input parameter distributions and load statements for Model objects #208

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 132 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 <[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
Expand All @@ -14,6 +15,8 @@
from scipy.optimize import shgo, differential_evolution
import numpy as np
import pandas as pd
from pandas import DataFrame, read_excel
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,7 +30,7 @@
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):
Expand Down Expand Up @@ -1268,4 +1271,129 @@ 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

#%% Easier input parameter distributions and load statements for Model objects

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_dict):
def wrapper_fn(statement):
def f(x):
namespace_dict['x'] = x
exec(codify(statement), namespace_dict)
return f
function = wrapper_fn(code)
return function

class EasyInputModel(Model):
"""
Create an EasyInputModel object that allows for evaluation over a sample space
using the Model class, with input parameter distributions and load statements
entered using a spreadsheet file or DataFrame object.

Parameters
----------
system : System
Should reflect the model state.
metrics : tuple[Metric]
Metrics to be evaluated by model.
specification=None : Function, optional
Loads specifications once all parameters are set. Specification should
simulate the system as well.
params=None : Iterable[Parameter], optional
Parameters to sample from.
exception_hook : callable(exception, sample)
Function called after a failed evaluation. The exception hook should
return either None or metric values given the exception and sample.
namespace_dict : dict, optional
Dictionary used to update the namespace accessed when executing
statements to load values into model parameters.

"""
def __init__(self, system, metrics=None, specification=None,
parameters=None, retry_evaluation=True, exception_hook='warn',
namespace_dict={}):
Model.__init__(self, system=system, metrics=metrics, specification=specification,
parameters=parameters, retry_evaluation=retry_evaluation, exception_hook=exception_hook)
self.namespace_dict = namespace_dict
# globals().update(namespace_dict)

def load_parameter_distributions(self, distributions,):
"""
Load a list of distributions and statements to load values for user-selected
parameters.

Parameters
----------
distributions : 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 Statements': 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_dict 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_dict, then
simply include the load statement: 'D101.Lr = x'. New lines in the statement
may be represented by '\n' or ';'.

"""

df = distributions
if type(df) is not DataFrame:
df = read_excel(distributions)

namespace_dict = self.namespace_dict
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 Statements']

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_dict),
element=element,
kind=kind,
units=units,
baseline=baseline,
distribution=D)
Loading