From 942c34a96b1b4106bd1d82e070e697ad02ffcad3 Mon Sep 17 00:00:00 2001 From: Eric Taw Date: Sun, 11 Jul 2021 12:05:56 -0400 Subject: [PATCH 1/2] update gitignore --- .gitignore | 2 ++ tests/test_isotherm.py | 0 2 files changed, 2 insertions(+) create mode 100644 tests/test_isotherm.py diff --git a/.gitignore b/.gitignore index 5a51931..f982482 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *.egg-info test/*/*pdf test/*/*png +.idea/ +.pytest_cache \ No newline at end of file diff --git a/tests/test_isotherm.py b/tests/test_isotherm.py new file mode 100644 index 0000000..e69de29 From fd7de9a0e08c2d9086d56e50043dd7beb5c5f970 Mon Sep 17 00:00:00 2001 From: Eric Taw Date: Sun, 11 Jul 2021 12:07:00 -0400 Subject: [PATCH 2/2] added barebones unit tests, ModelIsotherm now takes params without fitting --- {test => manual_tests}/.gitignore | 0 {test => manual_tests}/IAST_validation.png | Bin {test => manual_tests}/IRMOF-1.cssr | 0 .../IRMOF-1_ethane_isotherm_298K.csv | 0 ...ane_ethane_mixture_isotherm_65bar_298K.csv | 0 .../IRMOF-1_methane_isotherm_298K.csv | 0 {test => manual_tests}/Isotherm tests.ipynb | 0 .../Methane and ethane test.ipynb | 0 .../Test IAST for Langmuir case.ipynb | 0 .../pure_component_isotherms.png | Bin .../python_scripts/Isotherm tests.py | 0 .../python_scripts/Methane and ethane test.py | 0 .../Test IAST for Langmuir case.py | 0 pyiast/isotherms.py | 83 +++++++++++------- requirements.txt | 1 + tests/test_isotherm.py | 34 +++++++ 16 files changed, 86 insertions(+), 32 deletions(-) rename {test => manual_tests}/.gitignore (100%) rename {test => manual_tests}/IAST_validation.png (100%) rename {test => manual_tests}/IRMOF-1.cssr (100%) rename {test => manual_tests}/IRMOF-1_ethane_isotherm_298K.csv (100%) rename {test => manual_tests}/IRMOF-1_methane_ethane_mixture_isotherm_65bar_298K.csv (100%) rename {test => manual_tests}/IRMOF-1_methane_isotherm_298K.csv (100%) rename {test => manual_tests}/Isotherm tests.ipynb (100%) rename {test => manual_tests}/Methane and ethane test.ipynb (100%) rename {test => manual_tests}/Test IAST for Langmuir case.ipynb (100%) rename {test => manual_tests}/pure_component_isotherms.png (100%) rename {test => manual_tests}/python_scripts/Isotherm tests.py (100%) rename {test => manual_tests}/python_scripts/Methane and ethane test.py (100%) rename {test => manual_tests}/python_scripts/Test IAST for Langmuir case.py (100%) diff --git a/test/.gitignore b/manual_tests/.gitignore similarity index 100% rename from test/.gitignore rename to manual_tests/.gitignore diff --git a/test/IAST_validation.png b/manual_tests/IAST_validation.png similarity index 100% rename from test/IAST_validation.png rename to manual_tests/IAST_validation.png diff --git a/test/IRMOF-1.cssr b/manual_tests/IRMOF-1.cssr similarity index 100% rename from test/IRMOF-1.cssr rename to manual_tests/IRMOF-1.cssr diff --git a/test/IRMOF-1_ethane_isotherm_298K.csv b/manual_tests/IRMOF-1_ethane_isotherm_298K.csv similarity index 100% rename from test/IRMOF-1_ethane_isotherm_298K.csv rename to manual_tests/IRMOF-1_ethane_isotherm_298K.csv diff --git a/test/IRMOF-1_methane_ethane_mixture_isotherm_65bar_298K.csv b/manual_tests/IRMOF-1_methane_ethane_mixture_isotherm_65bar_298K.csv similarity index 100% rename from test/IRMOF-1_methane_ethane_mixture_isotherm_65bar_298K.csv rename to manual_tests/IRMOF-1_methane_ethane_mixture_isotherm_65bar_298K.csv diff --git a/test/IRMOF-1_methane_isotherm_298K.csv b/manual_tests/IRMOF-1_methane_isotherm_298K.csv similarity index 100% rename from test/IRMOF-1_methane_isotherm_298K.csv rename to manual_tests/IRMOF-1_methane_isotherm_298K.csv diff --git a/test/Isotherm tests.ipynb b/manual_tests/Isotherm tests.ipynb similarity index 100% rename from test/Isotherm tests.ipynb rename to manual_tests/Isotherm tests.ipynb diff --git a/test/Methane and ethane test.ipynb b/manual_tests/Methane and ethane test.ipynb similarity index 100% rename from test/Methane and ethane test.ipynb rename to manual_tests/Methane and ethane test.ipynb diff --git a/test/Test IAST for Langmuir case.ipynb b/manual_tests/Test IAST for Langmuir case.ipynb similarity index 100% rename from test/Test IAST for Langmuir case.ipynb rename to manual_tests/Test IAST for Langmuir case.ipynb diff --git a/test/pure_component_isotherms.png b/manual_tests/pure_component_isotherms.png similarity index 100% rename from test/pure_component_isotherms.png rename to manual_tests/pure_component_isotherms.png diff --git a/test/python_scripts/Isotherm tests.py b/manual_tests/python_scripts/Isotherm tests.py similarity index 100% rename from test/python_scripts/Isotherm tests.py rename to manual_tests/python_scripts/Isotherm tests.py diff --git a/test/python_scripts/Methane and ethane test.py b/manual_tests/python_scripts/Methane and ethane test.py similarity index 100% rename from test/python_scripts/Methane and ethane test.py rename to manual_tests/python_scripts/Methane and ethane test.py diff --git a/test/python_scripts/Test IAST for Langmuir case.py b/manual_tests/python_scripts/Test IAST for Langmuir case.py similarity index 100% rename from test/python_scripts/Test IAST for Langmuir case.py rename to manual_tests/python_scripts/Test IAST for Langmuir case.py diff --git a/pyiast/isotherms.py b/pyiast/isotherms.py index d592e45..98a3bc4 100644 --- a/pyiast/isotherms.py +++ b/pyiast/isotherms.py @@ -176,22 +176,24 @@ class ModelIsotherm: """ def __init__(self, - df, + df=None, loading_key=None, pressure_key=None, model=None, - param_guess=None, + params=None, optimization_method="Nelder-Mead"): """ Instantiation. A `ModelIsotherm` class is instantiated by passing it the pure-component adsorption isotherm in the form of a Pandas DataFrame. The least squares data fitting is done here. - :param df: DataFrame pure-component adsorption isotherm data + :param df: DataFrame pure-component adsorption isotherm data. If not specified, + uses `params` directly without fitting :param loading_key: String key for loading column in df :param pressure_key: String key for pressure column in df - :param param_guess: Dict starting guess for model parameters in the - data fitting routine + :param params: Dict starting guess for model parameters in the + data fitting routine, if `df` is specified. If not, assumes these parameters + without fitting :param optimization_method: String method in SciPy minimization function to use in fitting model to data. See [here](http://docs.scipy.org/doc/scipy/reference/optimize.html#module-scipy.optimize). @@ -206,41 +208,47 @@ def __init__(self, raise Exception("Model %s not an option in pyIAST. See viable" "models with pyiast._MODELS" % model) + if df is None and params is None: + raise Exception("Must specify data to fit to, parameters for the isotherm model," + " or both.") + #: Name of analytical model to fit to pure-component isotherm data #: adsorption isotherm self.model = model + self.set_data(df, loading_key, pressure_key) - #: Pandas DataFrame on which isotherm was fit - self.df = df - if None in [loading_key, pressure_key]: - raise Exception( - "Pass loading_key and pressure_key, the names of the loading and" - " pressure columns in the DataFrame, to the constructor.") - #: name of column in `df` that contains loading - self.loading_key = loading_key - #: name of column in `df` that contains pressure - self.pressure_key = pressure_key + if df is not None: + + #: Pandas DataFrame on which isotherm was fit + if None in [loading_key, pressure_key]: + raise Exception( + "Pass loading_key and pressure_key, the names of the loading and" + " pressure columns in the DataFrame, to the constructor.") - # ! root mean square error in fit - self.rmse = np.nan + # ! root mean square error in fit + self.rmse = np.nan - # ! Dictionary of parameters as a starting point for data fitting - self.param_guess = get_default_guess_params(model, df, pressure_key, - loading_key) - # Override defaults if user provides param_guess dictionary - if param_guess is not None: - for param, guess_val in param_guess.items(): - if param not in list(self.param_guess.keys()): - raise Exception("%s is not a valid parameter" - " in the %s model." % (param, model)) - self.param_guess[param] = guess_val + # ! Dictionary of parameters as a starting point for data fitting + self.param_guess = get_default_guess_params(model, df, pressure_key, + loading_key) + # Override defaults if user provides param_guess dictionary + if params is not None: + for param, guess_val in params.items(): + if param not in list(self.param_guess.keys()): + raise Exception("%s is not a valid parameter" + " in the %s model." % (param, model)) + self.param_guess[param] = guess_val - # ! Dictionary of identified model parameters - # initialize params as nan - self.params = copy.deepcopy(_MODEL_PARAMS[model]) + # ! Dictionary of identified model parameters + # initialize params as nan + self.params = copy.deepcopy(_MODEL_PARAMS[model]) - # fit model to isotherm data in self.df - self._fit(optimization_method) + # fit model to isotherm data in self.df + self._fit(optimization_method) + + if df is None and params is not None: + self.df = None + self.params = params def loading(self, pressure): """ @@ -285,6 +293,14 @@ def loading(self, pressure): self.params["theta"] * langmuir_fractional_loading ** 2 * \ (langmuir_fractional_loading - 1)) + def set_data(self, df: pd.DataFrame, loading_key, pressure_key): + """ + Sets data to be fitted + """ + self.df = df + self.loading_key = loading_key + self.pressure_key = pressure_key + def _fit(self, optimization_method): """ Fit model to data using nonlinear optimization with least squares loss @@ -293,6 +309,9 @@ def _fit(self, optimization_method): :param K_guess: float guess Langmuir constant (units: 1/pressure) :param M_guess: float guess saturation loading (units: loading) """ + if self.df is None: + raise Exception("No fitting data specified. Set the data by calling" + " `isotherm.set_data(df, loading_key, pressure_key)`") # parameter names (cannot rely on order in Dict) param_names = [param for param in self.params.keys()] # guess diff --git a/requirements.txt b/requirements.txt index ff19780..5248e8f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ numpy>=1.11.1 matplotlib pandas>=0.18.1 scipy>=0.18.0 +pytest diff --git a/tests/test_isotherm.py b/tests/test_isotherm.py index e69de29..16b77e5 100644 --- a/tests/test_isotherm.py +++ b/tests/test_isotherm.py @@ -0,0 +1,34 @@ +import pytest +from pyiast import ModelIsotherm +import numpy as np +import pandas as pd + + +def test_isotherm_constructor(): + # make some mock data for a langmuir isotherm + p = np.linspace(0, 10) + true_K = 1e-1 + q = true_K * p / (1 + true_K * p) + df = pd.DataFrame( + data=np.stack((p, q), axis=1), + columns=['p', 'uptake'] + ) + + isotherm = ModelIsotherm(df, loading_key='uptake', pressure_key='p', model='Langmuir') + assert set(isotherm.params.keys()) == {'M', 'K'} + assert np.allclose( + np.sort(list(isotherm.params.values())), + [0.1, 1.0], + rtol=1e-4 + ) + + params = { + 'K': 1e-1, + 'M': 1.0 + } + isotherm = ModelIsotherm(params=params, model='Langmuir') + test_loadings = isotherm.loading(p) + assert np.allclose(q, test_loadings) + + with pytest.raises(Exception): + ModelIsotherm(df=None, params=None)