Source code for pygaps.core.modelisotherm

"""Class representing a model of and isotherm."""

import typing as t

import numpy
import pandas

from pygaps import logger
from pygaps.core.baseisotherm import BaseIsotherm
from pygaps.modelling import _GUESS_MODELS
from pygaps.modelling import get_isotherm_model
from pygaps.modelling import is_model
from pygaps.modelling import is_model_class
from pygaps.units.converter_mode import c_loading
from pygaps.units.converter_mode import c_material
from pygaps.units.converter_mode import c_pressure
from pygaps.utilities.exceptions import CalculationError
from pygaps.utilities.exceptions import ParameterError


[docs]class ModelIsotherm(BaseIsotherm): """ Class to characterize pure-component isotherm data with an analytical model. Data fitting is done during instantiation. A `ModelIsotherm` class is instantiated by passing it the pure-component adsorption isotherm in the form of a Pandas DataFrame. Parameters ---------- pressure : list Create an isotherm directly from an array. Values for pressure. If the ``isotherm_data`` dataframe is specified, these values are ignored. loading : list Create an isotherm directly from an array. Values for loading. If the ``isotherm_data`` dataframe is specified, these values are ignored. isotherm_data : DataFrame Pure-component adsorption isotherm data. pressure_key : str Column of the pandas DataFrame where the pressure is stored. loading_key : str Column of the pandas DataFrame where the loading is stored. model : str or Model class The model to be used to describe the isotherm. param_guess : dict Starting guess for model parameters in the data fitting routine. param_bounds : dict Bounds for model parameters in the data fitting routine (applicable to some models). branch : ['ads', 'des'], optional The branch on which the model isotherm is based on. It is assumed to be the adsorption branch, as it is the most commonly modelled part, although may set to desorption as well. material : str Name of the material on which the isotherm is measured. adsorbate : str Isotherm adsorbate. temperature : float Isotherm temperature. Other Parameters ---------------- optimization_params : dict Dictionary to be passed to the minimization function to use in fitting model to data. See `here <https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.least_squares.html>`__. pressure_mode : str, optional The pressure mode, either 'absolute' pressure or 'relative' ('relative%') in the form of p/p0. pressure_unit : str, optional Unit of pressure, if applicable. loading_basis : str, optional Whether the adsorbed amount is in terms of either 'volume_gas' 'volume_liquid', 'molar', 'mass', or a fraction/percent basis. loading_unit : str, optional Unit in which the loading basis is expressed. material_basis : str, optional Whether the underlying material is in terms of 'per volume' 'per molar amount' or 'per mass' of material. material_unit : str, optional Unit in which the material basis is expressed. Notes ----- Models supported are found in :mod:modelling. Here, :math:`L` is the adsorbate uptake and :math:`P` is pressure (fugacity technically). """ _reserved_params = BaseIsotherm._reserved_params + [ 'model', ] ########################################################## # Instantiation and classmethods def __init__( self, pressure: t.List[float] = None, loading: t.List[float] = None, isotherm_data: pandas.DataFrame = None, pressure_key: str = None, loading_key: str = None, branch: str = 'ads', model: t.Union[str, t.List[str], t.Any] = None, param_guess: dict = None, param_bounds: dict = None, optimization_params: dict = None, verbose: bool = False, **other_properties ): """ Instantiation is done by passing the data to be fitted, model to be used and fitting method as well as the parameters required by parent class. """ # Run base class constructor super().__init__(**other_properties) # Checks if model is None: raise ParameterError( "Specify a model to fit to the pure-component" " isotherm data. e.g. model=\"Langmuir\"" ) if isotherm_data is not None: if None in [pressure_key, loading_key]: raise ParameterError( "Pass loading_key and pressure_key, the names of the loading and" " pressure columns in the DataFrame, to the constructor." ) data = isotherm_data.copy() # If branch column is already set if 'branch' not in isotherm_data.columns: data['branch'] = self._splitdata(data, pressure_key) if branch == 'ads': data = data.loc[data['branch'] == 0] elif branch == 'des': data = data.loc[data['branch'] == 1] else: raise ParameterError("ModelIsotherm branch must be singular: 'ads' or 'des'.") if data.empty: raise ParameterError("The required isotherm branch does not contain any points.") # Get just the pressure and loading columns pressure = data[pressure_key].values loading = data[loading_key].values process = True elif pressure is not None or loading is not None: if pressure is None or loading is None: raise ParameterError( "If you've chosen to pass loading and pressure directly as" " arrays, make sure both are specified!" ) if len(pressure) != len(loading): raise ParameterError("Pressure and loading arrays are not equal!") # Ensure we are dealing with numpy arrays pressure = numpy.asarray(pressure) loading = numpy.asarray(loading) process = True elif is_model_class(model): self.model = model self.branch = branch process = False else: raise ParameterError( "Pass isotherm data to fit in a pandas.DataFrame as ``isotherm_data``" " or directly ``pressure`` and ``loading`` as arrays." "Alternatively, pass an isotherm model instance." ) if process: # Branch the isotherm model is based on. self.branch = branch # Name of analytical model to fit to pure-component isotherm data # adsorption isotherm. self.model = get_isotherm_model( model, pressure_range=(min(pressure), max(pressure)), loading_range=(min(loading), max(loading)), param_bounds=param_bounds, ) # Pass odd parameters self.model.__init_parameters__(other_properties) # Dictionary of parameters as a starting point for data fitting. if param_guess: for param in param_guess.keys(): if param not in self.model.param_names: raise ParameterError( f"'{param}' is not a valid parameter" f" in the '{model}' model." ) else: param_guess = self.model.initial_guess(pressure, loading) # fit model to isotherm data self.model.fit( pressure, loading, param_guess, optimization_params, verbose, ) # Plot fit if verbose if verbose and other_properties.pop('plot_fit', True): from pygaps.graphing.model_graphs import plot_model_guesses plot_model_guesses([self], pressure, loading)
[docs] @classmethod def from_isotherm( cls, isotherm: BaseIsotherm, pressure: t.List[float] = None, loading: t.List[float] = None, isotherm_data: pandas.DataFrame = None, pressure_key: str = None, loading_key: str = None, branch: str = 'ads', model: t.Union[str, t.List[str], t.Any] = None, param_guess: dict = None, param_bounds: dict = None, optimization_params: dict = None, verbose: bool = False, ): """ Construct a ModelIsotherm using a parent isotherm as the template for all the parameters. Parameters ---------- isotherm : BaseIsotherm An instance of the BaseIsotherm parent class. pressure : list Create an isotherm directly from an array. Values for pressure. If the ``isotherm_data`` dataframe is specified, these values are ignored. loading : list Create an isotherm directly from an array. Values for loading. If the ``isotherm_data`` dataframe is specified, these values are ignored. isotherm_data : DataFrame Pure-component adsorption isotherm data. pressure_key : str Column of the pandas DataFrame where the pressure is stored. loading_key : str Column of the pandas DataFrame where the loading is stored. branch : ['ads', 'des'], optional The branch on which the model isotherm is based on. It is assumed to be the adsorption branch, as it is the most commonly modelled. model : str The model to be used to describe the isotherm. param_guess : dict Starting guess for model parameters in the data fitting routine. param_bounds : dict Bounds for model parameters in the data fitting routine. optimization_params : dict Dictionary to be passed to the minimization function to use in fitting model to data. See `here <https://docs.scipy.org/doc/scipy/reference/optimize.html#module-scipy.optimize>`__. Defaults to "Nelder-Mead". verbose : bool Prints out extra information about steps taken. """ # get isotherm parameters as a dictionary iso_params = isotherm.to_dict() # insert or update values iso_params['pressure'] = pressure iso_params['loading'] = loading iso_params['isotherm_data'] = isotherm_data iso_params['pressure_key'] = pressure_key iso_params['loading_key'] = loading_key iso_params['model'] = model iso_params['param_guess'] = param_guess iso_params['param_bounds'] = param_bounds iso_params['optimization_params'] = optimization_params iso_params['branch'] = branch iso_params['verbose'] = verbose return cls(**iso_params)
[docs] @classmethod def from_pointisotherm( cls, isotherm, branch: str = 'ads', model: t.Union[str, t.List[str], t.Any] = None, param_guess: dict = None, param_bounds: dict = None, optimization_params: dict = None, verbose: bool = False ): """ Constructs a ModelIsotherm using data from a PointIsotherm and all its parameters. Parameters ---------- isotherm : PointIsotherm An instance of the PointIsotherm parent class to model. branch : [None, 'ads', 'des'], optional Branch of isotherm to model. Defaults to adsorption branch. model : str, list, 'guess' The model to be used to describe the isotherm. Give a single model name (`"Langmuir"`) to fit it. Give a list of many model names to try them all and return the best fit (`[`Henry`, `Langmuir`]`). Specify `"guess"` to try all available models. param_guess : dict, optional Starting guess for model parameters in the data fitting routine. param_bounds : dict Bounds for model parameters in the data fitting routine. optimization_params : dict, optional Dictionary to be passed to the minimization function to use in fitting model to data. See `here <https://docs.scipy.org/doc/scipy/reference/optimize.html#module-scipy.optimize>`__. verbose : bool Prints out extra information about steps taken. """ if not model: raise ParameterError("Provide a model name (or a list of them) to fit.") # get isotherm parameters as a dictionary iso_params = isotherm.to_dict() iso_params['isotherm_data'] = isotherm.data(branch=branch) iso_params['pressure_key'] = isotherm.pressure_key iso_params['loading_key'] = isotherm.loading_key if isinstance(model, str): if model != 'guess': return cls( branch=branch, model=model, param_guess=param_guess, param_bounds=param_bounds, optimization_params=optimization_params, verbose=verbose, **iso_params ) return ModelIsotherm.guess( branch=branch, models=model, optimization_params=optimization_params, verbose=verbose, **iso_params )
[docs] @classmethod def guess( cls, pressure: t.List[float] = None, loading: t.List[float] = None, isotherm_data: pandas.DataFrame = None, pressure_key: str = None, loading_key: str = None, branch: str = 'ads', models='guess', optimization_params: dict = None, verbose: bool = False, **other_properties ): """ Attempt to model the data using supplied list of model names, then return the one with the best RMS fit. May take a long time depending on the number of datapoints. Parameters ---------- pressure : list Create an isotherm directly from an array. Values for pressure. If the ``isotherm_data`` dataframe is specified, these values are ignored. loading : list Create an isotherm directly from an array. Values for loading. If the ``isotherm_data`` dataframe is specified, these values are ignored. isotherm_data : DataFrame Pure-component adsorption isotherm data. pressure_key : str Column of the pandas DataFrame where the pressure is stored. loading_key : str Column of the pandas DataFrame where the loading is stored. models : 'guess', list of model names Attempt to guess which model best fits the isotherm data from the model name list supplied. If set to 'guess' A calculation of all models available will be performed, therefore it will take a longer time. optimization_params : dict Dictionary to be passed to the minimization function to use in fitting model to data. See `here <https://docs.scipy.org/doc/scipy/reference/optimize.html#module-scipy.optimize>`__. branch : ['ads', 'des'], optional The branch on which the model isotherm is based on. It is assumed to be the adsorption branch, as it is the most commonly modelled part, although may set to desorption as well. verbose : bool, optional Prints out extra information about steps taken. other_properties: Any other parameters of the isotherm which should be stored internally. """ attempts = [] if models == 'guess': guess_models = _GUESS_MODELS else: try: guess_models = [m for m in models if is_model(m)] except TypeError: raise ParameterError("Could not figure out the list of models. Is it a list?") if len(guess_models) != len(models): raise ParameterError('Not all models correspond to internal models.') for model in guess_models: try: isotherm = cls( pressure=pressure, loading=loading, isotherm_data=isotherm_data, pressure_key=pressure_key, loading_key=loading_key, model=model, param_guess=None, param_bounds=None, optimization_params=optimization_params, branch=branch, verbose=verbose, plot_fit=False, # we don't want to plot at this stage **other_properties ) attempts.append(isotherm) except CalculationError as err: logger.info(f"Modelling using {model} failed.") if verbose: logger.info(f"\n{err}") if not attempts: raise CalculationError("No model could be reliably fit on the isotherm.") errors = [x.model.rmse for x in attempts] best_fit = attempts[errors.index(min(errors))] if verbose: if loading is None: pressure = isotherm_data[pressure_key] loading = isotherm_data[loading_key] from pygaps.graphing.model_graphs import plot_model_guesses plot_model_guesses(attempts, pressure, loading) logger.info(f"Best model fit is {best_fit.model.name}.") return best_fit
########################################################### # Info function def __str__(self) -> str: """Print a short summary of all the isotherm parameters.""" return super().__str__() + self.model.__str__()
[docs] def print_info(self, **plot_iso_args): """ Print a short summary of the isotherm parameters and a graph. Parameters ---------- show : bool, optional Specifies if the graph is shown automatically or not. Other Parameters ---------------- plot_iso_args : dict options to be passed to pygaps.plot_iso() Returns ------- axes : matplotlib.axes.Axes or numpy.ndarray of them """ print(self) return self.plot(**plot_iso_args)
[docs] def plot(self, **plot_iso_args): """ Plot the isotherm using pygaps.plot_iso(). Parameters ---------- show : bool, optional Specifies if the graph is shown automatically or not. Other Parameters ---------------- plot_iso_args : dict options to be passed to pygaps.plot_iso() Returns ------- axes : matplotlib.axes.Axes or numpy.ndarray of them """ plot_dict = dict( material_basis=self.material_basis, material_unit=self.material_unit, loading_basis=self.loading_basis, loading_unit=self.loading_unit, pressure_unit=self.pressure_unit, pressure_mode=self.pressure_mode, ) plot_dict.update(plot_iso_args) from pygaps.graphing.isotherm_graphs import plot_iso return plot_iso(self, **plot_dict)
########################################################## # Methods
[docs] def has_branch(self, branch: str) -> bool: """ Check if the isotherm has an specific branch. Parameters ---------- branch : {None, 'ads', 'des'} The branch of the data to check for. Returns ------- bool Whether the data exists or not. """ return self.branch == branch
[docs] def pressure( self, points: int = 60, branch: str = None, pressure_unit: str = None, pressure_mode: str = None, limits: t.Tuple[float, float] = None, indexed: bool = False ): """ Return a numpy.linspace generated array with a fixed number of equidistant points within the pressure range the model was created. Parameters ---------- points : int The number of points to get. branch : {None, 'ads', 'des'} The branch of the pressure to return. If ``None``, returns the branch the isotherm is modelled on. pressure_unit : str, optional Unit in which the pressure should be returned. If None it defaults to which pressure unit the isotherm is currently in. pressure_mode : {None, 'absolute', 'relative', 'relative%'} The mode in which to return the pressure, if possible. If ``None``, returns mode the isotherm is currently in. limits : [float, float], optional Minimum and maximum pressure limits. Put None or -+np.inf for no limit. indexed : bool, optional If this is specified to true, then the function returns an indexed pandas.Series with the columns requested instead of an array. Returns ------- numpy.array or pandas.Series Pressure points in the model pressure range. """ if branch and branch not in [self.branch, 'all']: raise ParameterError( f"ModelIsotherm is based on an '{self.branch}' branch " f"(while parameter supplied was '{branch}')." ) # TODO: to calculate limits like this, better to put the limits directly to evaluate # Generate pressure points if self.model.calculates == 'loading': ret = numpy.linspace( self.model.pressure_range[0], self.model.pressure_range[1], points, ) # Convert if needed if pressure_mode or pressure_unit: if not pressure_mode: pressure_mode = self.pressure_mode if not pressure_unit: pressure_unit = self.pressure_unit ret = c_pressure( ret, mode_from=self.pressure_mode, mode_to=pressure_mode, unit_from=self.pressure_unit, unit_to=pressure_unit, adsorbate=self.adsorbate, temp=self.temperature ) elif self.model.calculates == 'pressure': ret = self.pressure_at( self.loading(points), pressure_mode=pressure_mode, pressure_unit=pressure_unit, ) # Select required points if limits and any(limits): ret = ret[((-numpy.inf if limits[0] is None else limits[0]) < ret) & (ret < (numpy.inf if limits[1] is None else limits[1]))] if indexed: return pandas.Series(ret) return ret
[docs] def loading( self, points: int = 60, branch: str = None, loading_unit: str = None, loading_basis: str = None, material_unit: str = None, material_basis: str = None, limits: t.Tuple[float, float] = None, indexed: bool = False ): """ Return the loading calculated at equidistant pressure points within the pressure range the model was created. Parameters ---------- points : int The number of points to get. branch : {None, 'ads', 'des'} The branch of the loading to return. If ``None``, returns entire dataset. loading_unit : str, optional Unit in which the loading should be returned. If None it defaults to which loading unit the isotherm is currently in. loading_basis : {None, 'mass', 'volume_gas', 'volume_liquid'} The basis on which to return the loading, if possible. If ``None``, returns on the basis the isotherm is currently in. material_unit : str, optional Unit in which the material should be returned. If None it defaults to which loading unit the isotherm is currently in. material_basis : {None, 'mass', 'volume'} The basis on which to return the material, if possible. If ``None``, returns on the basis the isotherm is currently in. limits : [float, float], optional Minimum and maximum loading limits. Put None or -+np.inf for no limit. indexed : bool, optional If this is specified to true, then the function returns an indexed pandas.Series with the columns requested instead of an array. Returns ------- numpy.array or pandas.Series Loading calculated at points the model pressure range. """ if branch and branch != self.branch: raise ParameterError( f"ModelIsotherm is based on an '{self.branch}' branch " f"(while parameter supplied was '{branch}')." ) if self.model.calculates == 'pressure': ret = numpy.linspace( self.model.loading_range[0], self.model.loading_range[1], points, ) # Convert if needed # First material is converted if material_basis or material_unit: if not material_basis: material_basis = self.material_basis ret = c_material( ret, basis_from=self.material_basis, basis_to=material_basis, unit_from=self.material_unit, unit_to=material_unit, material=self.material ) if loading_basis or loading_unit: if not loading_basis: loading_basis = self.loading_basis # These must be specified # in the case of fractional conversions if not material_basis: material_basis = self.material_basis if not material_unit: material_unit = self.material_unit ret = c_loading( ret, basis_from=self.loading_basis, basis_to=loading_basis, unit_from=self.loading_unit, unit_to=loading_unit, adsorbate=self.adsorbate, temp=self.temperature, basis_material=material_basis, unit_material=material_unit, ) else: ret = self.loading_at( self.pressure(points), loading_unit=loading_unit, loading_basis=loading_basis, material_unit=material_unit, material_basis=material_basis, ) # Select required points if limits and any(limits): ret = ret[((-numpy.inf if limits[0] is None else limits[0]) < ret) & (ret < (numpy.inf if limits[1] is None else limits[1]))] if indexed: return pandas.Series(ret) return ret
@property def other_keys(self): """ Return column names of any supplementary data points. """ return [] ########################################################## # Functions that calculate values of the isotherm data
[docs] def pressure_at( self, loading: t.Union[float, t.List[float]], branch: str = None, pressure_unit: str = None, pressure_mode: str = None, loading_unit: str = None, loading_basis: str = None, material_unit: str = None, material_basis: str = None, ): """ Compute pressure at loading L, given stored model parameters. Depending on the model, may be calculated directly or through a numerical minimisation. Parameters ---------- loading : float or array Loading at which to compute pressure. branch : {None, 'ads', 'des'} The branch the calculation is based on. pressure_unit : str Unit the pressure is returned in. If ``None``, it defaults to internal isotherm units. pressure_mode : str The mode the pressure is returned in. If ``None``, it defaults to internal isotherm mode. loading_unit : str Unit the loading is specified in. If ``None``, it defaults to internal isotherm units. loading_basis : {None, 'mass', 'volume_gas', 'volume_liquid'} The basis the loading is specified in. If ``None``, assumes the basis the isotherm is currently in. material_unit : str, optional Unit in which the material is passed in. If None it defaults to which loading unit the isotherm is currently in material_basis : str The basis the loading is passed in. If ``None``, it defaults to internal isotherm basis. Returns ------- float or array Predicted pressure at loading L using fitted model parameters. """ if branch and branch != self.branch: raise ParameterError( f"ModelIsotherm is based on an '{self.branch}' branch " f"(while parameter supplied was '{branch}')." ) # Convert to numpy array just in case loading = numpy.asarray(loading) # Ensure loading is in correct units and basis for the internal model if material_basis or material_unit: if not material_basis: material_basis = self.material_basis if not material_unit: raise ParameterError( "Must specify a material unit if the input" " is in another basis" ) loading = c_material( loading, basis_from=material_basis, basis_to=self.material_basis, unit_from=material_unit, unit_to=self.material_unit, material=self.material ) if loading_basis or loading_unit: if not loading_basis: loading_basis = self.loading_basis if not loading_unit: raise ParameterError( "Must specify a loading unit if the input" " is in another basis" ) loading = c_loading( loading, basis_from=loading_basis, basis_to=self.loading_basis, unit_from=loading_unit, unit_to=self.loading_unit, adsorbate=self.adsorbate, temp=self.temperature, basis_material=material_basis, unit_material=material_unit, ) # Calculate pressure using internal model pressure = self.model.pressure(loading) # Ensure pressure is in correct units and mode requested if pressure_mode or pressure_unit: if not pressure_mode: pressure_mode = self.pressure_mode if not pressure_unit: pressure_unit = self.pressure_unit pressure = c_pressure( pressure, mode_from=self.pressure_mode, mode_to=pressure_mode, unit_from=self.pressure_unit, unit_to=pressure_unit, adsorbate=self.adsorbate, temp=self.temperature ) return pressure
[docs] def loading_at( self, pressure: t.Union[float, t.List[float]], branch: str = None, pressure_unit: str = None, pressure_mode: str = None, loading_unit: str = None, loading_basis: str = None, material_unit: str = None, material_basis: str = None, ): """ Compute loading at pressure P, given stored model parameters. Depending on the model, may be calculated directly or through a numerical minimisation. Parameters ---------- pressure : float or array Pressure at which to compute loading. branch : {None, 'ads', 'des'} The branch the calculation is based on. pressure_unit : str Unit the pressure is specified in. If ``None``, it defaults to internal isotherm units. pressure_mode : str The mode the pressure is passed in. If ``None``, it defaults to internal isotherm mode. loading_unit : str, optional Unit in which the loading should be returned. If None it defaults to which loading unit the isotherm is currently in. loading_basis : {None, 'mass', 'volume_gas', 'volume_liquid'} The basis on which to return the loading, if possible. If ``None``, returns on the basis the isotherm is currently in. material_unit : str, optional Unit in which the material should be returned. If None it defaults to which loading unit the isotherm is currently in. material_basis : {None, 'mass', 'volume'} The basis on which to return the material, if possible. If ``None``, returns on the basis the isotherm is currently in. Returns ------- float or array Predicted loading at pressure P using fitted model parameters. """ if branch and branch != self.branch: raise ParameterError( f"ModelIsotherm is based on an '{self.branch}' branch " f"(while parameter supplied was '{branch}')." ) # Convert to numpy array just in case pressure = numpy.asarray(pressure) # Ensure pressure is in correct units and mode for the internal model if pressure_mode or pressure_unit: if not pressure_mode: pressure_mode = self.pressure_mode if pressure_mode == 'absolute' and not pressure_unit: raise ParameterError( "Must specify a pressure unit if the input" " is in an absolute mode" ) pressure = c_pressure( pressure, mode_from=pressure_mode, mode_to=self.pressure_mode, unit_from=pressure_unit, unit_to=self.pressure_unit, adsorbate=self.adsorbate, temp=self.temperature ) # Calculate loading using internal model loading = self.model.loading(pressure) # Ensure loading is in correct units and basis requested # First adsorbent is converted if material_basis or material_unit: if not material_basis: material_basis = self.material_basis loading = c_material( loading, basis_from=self.material_basis, basis_to=material_basis, unit_from=self.material_unit, unit_to=material_unit, material=self.material ) # Then loading if loading_basis or loading_unit: if not loading_basis: loading_basis = self.loading_basis # These must be specified # in the case of fractional conversions if not material_basis: material_basis = self.material_basis if not material_unit: material_unit = self.material_unit loading = c_loading( loading, basis_from=self.loading_basis, basis_to=loading_basis, unit_from=self.loading_unit, unit_to=loading_unit, adsorbate=self.adsorbate, temp=self.temperature, basis_material=material_basis, unit_material=material_unit, ) return loading
[docs] def spreading_pressure_at( self, pressure: t.Union[float, t.List[float]], branch: str = None, pressure_unit: str = None, pressure_mode: str = None, ): r""" Calculate reduced spreading pressure at a bulk gas pressure P. The reduced spreading pressure is an integral involving the isotherm :math:`L(P)`: .. math:: \Pi(p) = \int_0^p \frac{L(\hat{p})}{ \hat{p}} d\hat{p}, which is computed analytically or numerically, depending on the model used. Parameters ---------- pressure : float Pressure (in corresponding units as data in instantiation) branch : {'ads', 'des'} The branch of the use for calculation. Defaults to adsorption. pressure_unit : str Unit the pressure is returned in. If ``None``, it defaults to internal isotherm units. pressure_mode : str The mode the pressure is returned in. If ``None``, it defaults to internal isotherm mode. Returns ------- float Spreading pressure, :math:`\Pi`. """ if branch and branch != self.branch: raise ParameterError( f"ModelIsotherm is based on an '{self.branch}' branch " f"(while parameter supplied was '{branch}')." ) # Convert to numpy array just in case pressure = numpy.asarray(pressure) # Ensure pressure is in correct units and mode for the internal model if pressure_mode or pressure_unit: if not pressure_mode: pressure_mode = self.pressure_mode if not pressure_unit: pressure_unit = self.pressure_unit if not pressure_unit and self.pressure_mode.startswith('relative'): raise ParameterError( "Must specify a pressure unit if the input" " is in an absolute mode" ) pressure = c_pressure( pressure, mode_from=pressure_mode, mode_to=self.pressure_mode, unit_from=pressure_unit, unit_to=self.pressure_unit, adsorbate=self.adsorbate, temp=self.temperature ) # calculate based on model return self.model.spreading_pressure(pressure)