Source code for despasito.parameter_fitting

"""
Fit Parameters
--------------

This package uses functions from ``input_output``, ``equations_of_state``, and
``thermodynamics`` to fit parameters to experimental data.

input.json files have a different dictionary structure that is processed by
:func:`~despasito.input_output.read_input.process_param_fit_inputs`.
"""

import os
import numpy as np
from importlib import import_module
import logging

from . import fit_functions as ff
from . import data_classes

logger = logging.getLogger(__name__)


[docs] def fit( optimization_parameters=None, exp_data=None, global_opts={}, minimizer_opts=None, MultiprocessingObject=None, **kwargs, ): r""" Fit parameters for an equation of state object with given experimental data. Each set of experimental data is converted to an object with the built in ability to evaluate its part of objective function. To add another type of supported experimental data, add a class to the fit_classes.py file. Parameters ---------- optimization_parameters : dict, Optional, default=None Parameters used in global fitting algorithm. - fit_bead (str) - Name of bead whose parameters are being fit. Should be within bead_configuration. - fit_parameter_names (list[str]) - This list contains the name of the parameter being fit (e.g. epsilon). See EOS documentation for supported parameter names. Cross interaction parameter names should be composed of a parameter name followed by the interacting bead's name, separated by an underscore (e.g. epsilon_CO2). - parameters_guess (list[float]), Optional - Initial guess for all parameters being fit. If this is not provided, the Eos object provides a guesses based on the parameter types. - \*_bounds (list[float]), Optional - This list contains the minimum and maximum of the parameter from a parameter listed in fit_parameter_names, represented in place of the asterisk. See , :ref:`startfitting-label`, for more information. exp_data : dict, Optional, default=None This dictionary is made up of a dictionary for each data set that the parameters are fit to. Each dictionary is converted into an object and saved back to this structure before parameter fitting begins. Each key is an arbitrary string used to identify the data set and used later in reporting objective function values during the fitting process. See data type objects for more details. - data_class_type (str) - One of the supported data type objects to fit parameters, :ref:`data-types`. - eos_obj (obj) - Equation of state output that writes pressure, max density, fugacity coefficient, updates parameters, and evaluates parameter fitting objective function. See equation of state documentation for more details. global_opts : dict, Optional, default={} Method and keyword arguments used in global optimization method. See :func:`~despasito.parameter_fitting.fit_functions.global_minimization`. - method (str), Optional - default='differential_evolution', Global optimization method used to fit parameters. See :func:`~despasito.parameter_fitting.fit_functions.global_minimization`. - Additional options, specific to the global optimization method minimizer_opts : dict, Optional, default=None Dictionary used to define minimization type and the associated options. - method (str) - Method available to ``scipy.optimize.minimize`` - options (dict) - This dictionary contains the keyword arguments available to the chosen method MultiprocessingObject : obj, Optional Multiprocessing object, :class:`~despasito.utils.parallelization.MultiprocessingJob` kwargs : Other keywords of instructions for thermodynamic calculations and parameter fitting. Returns ------- output : dict Results from parameter optimization - parameters_final (numpy.ndarray) - Array of the same length as `fit_parameter_names` containing the parameters resulting from the chosen global optimization method. - objective_value (float) - Objective value resulting from `parameters_final` """ # Extract relevant quantities from kwargs dicts = {} # Extract inputs if optimization_parameters is None: raise ValueError("Required input, optimization_parameters, is missing.") dicts["global_opts"] = global_opts if minimizer_opts is not None: dicts["minimizer_opts"] = minimizer_opts if exp_data is None: raise ValueError("Required input, exp_data, is missing.") # Add multiprocessing object to exp_data objects and global_optss if MultiprocessingObject is not None: for k2 in list(exp_data.keys()): exp_data[k2]["MultiprocessingObject"] = MultiprocessingObject dicts["global_opts"]["MultiprocessingObject"] = MultiprocessingObject # Thermodynamic options and optimization options are added to data object for k2 in list(exp_data.keys()): for key, value in kwargs.items(): if key not in exp_data[k2]: exp_data[k2][key] = value if not isinstance(optimization_parameters["fit_parameter_names"], list): if isinstance(optimization_parameters["fit_parameter_names"], str): optimization_parameters["fit_parameter_names"] = [optimization_parameters["fit_parameter_names"]] else: raise ValueError( f"'fit_parameter_names' must be a list not: {optimization_parameters['fit_parameter_names']}" ) # Generate initial guess and bounds for parameters if none was given optimization_parameters = ff.consolidate_bounds(optimization_parameters).copy() if "bounds" in optimization_parameters: bounds = optimization_parameters["bounds"] del optimization_parameters["bounds"] else: bounds = np.zeros((len(optimization_parameters["fit_parameter_names"]), 2)) Eos = exp_data[list(exp_data.keys())[0]][ "eos_obj" ] # since all exp data sets use the same Eos, it doesn't really matter bounds = ff.check_parameter_bounds(optimization_parameters, Eos, bounds) if "parameters_guess" in optimization_parameters: parameters_guess = optimization_parameters["parameters_guess"] if len(parameters_guess) != len(optimization_parameters["fit_parameter_names"]): raise ValueError("The number of initial parameters given isn't the same number of " "parameters to be fit.") else: parameters_guess = ff.initial_guess(optimization_parameters, Eos) logger.info("Initial guess in parameters: {}".format(parameters_guess)) # _________________________________________________________ # Reformat exp. data into formatted dictionary exp_dict = {} pkgpath = os.path.dirname(data_classes.__file__) type_list = [f for f in os.listdir(pkgpath) if f.endswith(".py")] type_list.remove("__init__.py") for key, data_dict in exp_data.items(): fittype = data_dict["data_class_type"] try: exp_module = import_module("." + fittype, package="despasito.parameter_fitting.data_classes") data_class = getattr(exp_module, "Data") except Exception: if not type_list: raise ImportError("No fit types") elif len(type_list) == 1: tmp = type_list[0] else: tmp = ", ".join(type_list) raise ImportError( "The experimental data type, '{}', was not found\nThe following " "calculation types are supported: {}".format(fittype, tmp) ) try: instance = data_class(data_dict) exp_dict[key] = instance logger.info("Initiated exp. data object: {}".format(instance.name)) except Exception: raise AttributeError("Data set, {}, did not properly initiate object".format(key)) # Check global optimization method if "method" in dicts["global_opts"]: global_method = dicts["global_opts"]["method"] del dicts["global_opts"]["method"] else: global_method = "differential_evolution" # Run Parameter Fitting try: result = ff.global_minimization( global_method, parameters_guess, bounds, optimization_parameters["fit_bead"], optimization_parameters["fit_parameter_names"], exp_dict, **dicts, ) logger.info("Fitting terminated:\n{}".format(result.message)) logger.info("Best Fit Parameters") logger.info(" Obj. Value: {}".format(result.fun)) for i in range(len(optimization_parameters["fit_parameter_names"])): logger.info( " {} {}: {}".format( optimization_parameters["fit_bead"], optimization_parameters["fit_parameter_names"][i], result.x[i], ) ) except Exception: raise TypeError("The parameter fitting failed") return { "fit_bead": optimization_parameters["fit_bead"], "fit_parameter_names": optimization_parameters["fit_parameter_names"], "parameters_final": result.x, "objective_value": result.fun, }