espei/error_functions/activity_error.py
def calculate_activity_error(dbf, comps, phases, datasets, parameters=None, phase_models=None, callables=None, data_weight=1.0):
    """
    Return the sum of square error from activity data

    Parameters
    ----------
    dbf : pycalphad.Database
        Database to consider
    comps : list
        List of active component names
    phases : list
        List of phases to consider
    datasets : espei.utils.PickleableTinyDB
        Datasets that contain single phase data
    parameters : dict
        Dictionary of symbols that will be overridden in pycalphad.equilibrium
    phase_models : dict
        Phase models to pass to pycalphad calculations
    callables : dict
        Callables to pass to pycalphad
    data_weight : float
        Weight for standard deviation of activity measurements, dimensionless.
        Corresponds to the standard deviation of differences in chemical
        potential in typical measurements of activity, in J/mol.

    Returns
    -------
    float
        A single float of the sum of square errors

    Notes
    -----
    General procedure:
    1. Get the datasets
    2. For each dataset

        a. Calculate reference state equilibrium
        b. Calculate current chemical potentials
        c. Find the target chemical potentials
        d. Calculate error due to chemical potentials

    """
    std_dev = 500  # J/mol

    if parameters is None:
        parameters = {}

    activity_datasets = datasets.search(
        (tinydb.where('output').test(lambda x: 'ACR' in x)) &
        (tinydb.where('components').test(lambda x: set(x).issubset(comps))))

    error = 0
    if len(activity_datasets) == 0:
        return error

    for ds in activity_datasets:
        acr_component = ds['output'].split('_')[1]  # the component of interest
        # calculate the reference state equilibrium
        ref = ds['reference_state']
        # data_comps and data_phases ensures that we only do calculations on
        # the subsystem of the system defining the data.
        data_comps = ds['components']
        data_phases = filter_phases(dbf, unpack_components(dbf, data_comps), candidate_phases=phases)
        ref_conditions = {_map_coord_to_variable(coord): val for coord, val in ref['conditions'].items()}
        ref_result = equilibrium(dbf, data_comps, ref['phases'], ref_conditions,
                                 model=phase_models, parameters=parameters,
                                 callables=callables)

        # calculate current chemical potentials
        # get the conditions
        conditions = {}
        # first make sure the conditions are paired
        # only get the compositions, P and T are special cased
        conds_list = [(cond, value) for cond, value in ds['conditions'].items() if cond not in ('P', 'T')]
        # ravel the conditions
        # we will ravel each composition individually, since they all must have the same shape
        for comp_name, comp_x in conds_list:
            P, T, X = ravel_conditions(ds['values'], ds['conditions']['P'], ds['conditions']['T'], comp_x)
            conditions[v.P] = P
            conditions[v.T] = T
            conditions[_map_coord_to_variable(comp_name)] = X
        # do the calculations
        # we cannot currently turn broadcasting off, so we have to do equilibrium one by one
        # invert the conditions dicts to make a list of condition dicts rather than a condition dict of lists
        # assume now that the ravelled conditions all have the same size
        conditions_list = [{c: conditions[c][i] for c in conditions.keys()} for i in range(len(conditions[v.T]))]
        current_chempots = []
        for conds in conditions_list:
            sample_eq_res = equilibrium(dbf, data_comps, data_phases, conds,
                                        model=phase_models, parameters=parameters,
                                        callables=callables)
            current_chempots.append(sample_eq_res.MU.sel(component=acr_component).values.flatten()[0])
        current_chempots = np.array(current_chempots)

        # calculate target chempots
        samples = np.array(ds['values']).flatten()
        target_chempots = target_chempots_from_activity(acr_component, samples, conditions[v.T], ref_result)
        # calculate the error
        weight = ds.get('weight', 1.0)
        pe = chempot_error(current_chempots, target_chempots, std_dev=std_dev/data_weight/weight)
        error += np.sum(pe)
        _log.debug('Data: %s, chemical potential difference: %s, probability: %s, reference: %s', samples, current_chempots-target_chempots, pe, ds["reference"])

    # TODO: write a test for this
    if np.any(np.isnan(np.array([error], dtype=np.float64))):  # must coerce sympy.core.numbers.Float to float64
        return -np.inf
    return error
espei/error_functions/zpf_error.py
def calculate_zpf_error(zpf_data: Sequence[Dict[str, Any]],
                        parameters: np.ndarray = None,
                        data_weight: int = 1.0,
                        approximate_equilibrium: bool = False) -> float:
    """
    Calculate the likelihood due to phase equilibria data.

    For detailed documentation, see ``calculate_zpf_driving_forces``

    Returns
    -------
    float
        Log probability of ZPF driving forces

    """
    if len(zpf_data) == 0:
        return 0.0
    driving_forces, weights = calculate_zpf_driving_forces(zpf_data, parameters, approximate_equilibrium, short_circuit=True)
    # Driving forces and weights are 2D ragged arrays with the shape (len(zpf_data), len(zpf_data['values']))
    driving_forces = np.concatenate(driving_forces)
    weights = np.concatenate(weights)
    if np.any(np.logical_or(np.isinf(driving_forces), np.isnan(driving_forces))):
        return -np.inf
    log_probabilites = norm.logpdf(driving_forces, loc=0, scale=1000/data_weight/weights)
    _log.debug('Data weight: %s, driving forces: %s, weights: %s, probabilities: %s', data_weight, driving_forces, weights, log_probabilites)
    return np.sum(log_probabilites)
espei/priors.py
def build_prior_specs(prior_spec, parameters):
    """
    Get priors from given parameters

    Parameters
    ----------
    prior_spec : PriorSpec or dict
        Either a prior spec dict (to instantiate), a PriorSpec, or a list of either.
        If a list is passed, it must correspond to the parameters.
    parameters : list
        List of parameters that the priors will be instantiated by

    Returns
    -------
    [PriorSpec]

    Examples
    --------
    >>> s_norm = {'name': 'normal', 'scale_relative': 0.1, 'loc_identity': 1.0}
    >>> len(build_prior_specs(s_norm, [10, 100])) == 2
    True
    >>> s_tri = {'name': 'triangular', 'loc_shift_relative': -0.5, 'scale_shift_relative': 0.5, 'c': 0.5}
    >>> from espei.priors import PriorSpec
    >>> len(build_prior_specs([s_norm, PriorSpec(**s_tri)], [10, 100])) == 2
    True

    """
    # Check for single instance, otherwise assume some list/iterable matching the parameters
    if isinstance(prior_spec, (dict, PriorSpec)):
        prior_spec = [copy.deepcopy(prior_spec) for _ in parameters]

    prior_specs = []
    for spec, param in zip(prior_spec, parameters):
        if isinstance(spec, dict):
            prior_specs.append(PriorSpec(**spec))
        elif isinstance(spec, PriorSpec):
            prior_specs.append(spec)
        elif hasattr(spec, "logpdf"):
            # this is an instance of a prior we'll add it to the spec list and it should be handled later
            prior_spec.append(spec)
        else:
            raise ValueError("Unknown prior spec {}. Should be either a PriorSpec instance or a dict".format(spec))
    return prior_specs
espei/utils.py
def unpack_piecewise(x):
    if isinstance(x, symengine.Piecewise):
        return float(x.args[0])
    else:
        return float(x)
espei/error_functions/context.py
def setup_context(dbf, datasets, symbols_to_fit=None, data_weights=None, phase_models=None, make_callables=True):
    """
    Set up a context dictionary for calculating error.

    Parameters
    ----------
    dbf : Database
        A pycalphad Database that will be fit
    datasets : PickleableTinyDB
        A database of single- and multi-phase data to fit
    symbols_to_fit : list of str
        List of symbols in the Database that will be fit. If None (default) are
        passed, then all parameters prefixed with `VV` followed by a number,
        e.g. VV0001 will be fit.

    Returns
    -------

    Notes
    -----
    A copy of the Database is made and used in the context. To commit changes
    back to the original database, the dbf.symbols.update method should be used.
    """
    dbf = copy.deepcopy(dbf)
    if phase_models is not None:
        comps = sorted(phase_models['components'])
    else:
        comps = sorted([sp for sp in dbf.elements])
    if symbols_to_fit is None:
        symbols_to_fit = database_symbols_to_fit(dbf)
    else:
        symbols_to_fit = sorted(symbols_to_fit)
    data_weights = data_weights if data_weights is not None else {}

    if len(symbols_to_fit) == 0:
        raise ValueError('No degrees of freedom. Database must contain symbols starting with \'V\' or \'VV\', followed by a number.')
    else:
        _log.info('Fitting %s degrees of freedom.', len(symbols_to_fit))

    for x in symbols_to_fit:
        if isinstance(dbf.symbols[x], symengine.Piecewise):
            _log.debug('Replacing %s in database', x)
            dbf.symbols[x] = dbf.symbols[x].args[0]

    # construct the models for each phase, substituting in the SymEngine symbol to fit.
    if phase_models is not None:
        model_dict = get_model_dict(phase_models)
    else:
        model_dict = {}
    _log.trace('Building phase models (this may take some time)')
    import time
    t1 = time.time()
    phases = sorted(filter_phases(dbf, unpack_components(dbf, comps), dbf.phases.keys()))
    parameters = dict(zip(symbols_to_fit, [0]*len(symbols_to_fit)))
    models = instantiate_models(dbf, comps, phases, model=model_dict, parameters=parameters)
    if make_callables:
        eq_callables = build_callables(dbf, comps, phases, models, parameter_symbols=symbols_to_fit,
                            output='GM', build_gradients=True, build_hessians=True,
                            additional_statevars={v.N, v.P, v.T})
    else:
        eq_callables = None
    t2 = time.time()
    _log.trace('Finished building phase models (%0.2fs)', t2-t1)
    _log.trace('Getting non-equilibrium thermochemical data (this may take some time)')
    t1 = time.time()
    thermochemical_data = get_thermochemical_data(dbf, comps, phases, datasets, model=model_dict, weight_dict=data_weights, symbols_to_fit=symbols_to_fit)
    t2 = time.time()
    _log.trace('Finished getting non-equilibrium thermochemical data (%0.2fs)', t2-t1)
    _log.trace('Getting equilibrium thermochemical data (this may take some time)')
    t1 = time.time()
    eq_thermochemical_data = get_equilibrium_thermochemical_data(dbf, comps, phases, datasets, model=model_dict, parameters=parameters, data_weight_dict=data_weights)
    t2 = time.time()
    _log.trace('Finished getting equilibrium thermochemical data (%0.2fs)', t2-t1)
    _log.trace('Getting ZPF data (this may take some time)')
    t1 = time.time()
    zpf_data = get_zpf_data(dbf, comps, phases, datasets, model=model_dict, parameters=parameters)
    t2 = time.time()
    _log.trace('Finished getting ZPF data (%0.2fs)', t2-t1)

    # context for the log probability function
    # for all cases, parameters argument addressed in MCMC loop
    error_context = {
        'symbols_to_fit': symbols_to_fit,
        'zpf_kwargs': {
            'zpf_data': zpf_data,
            'data_weight': data_weights.get('ZPF', 1.0),
        },
        'equilibrium_thermochemical_kwargs': {
            'eq_thermochemical_data': eq_thermochemical_data,
        },
        'thermochemical_kwargs': {
            'thermochemical_data': thermochemical_data,
        },
        'activity_kwargs': {
            'dbf': dbf, 'comps': comps, 'phases': phases, 'datasets': datasets,
            'phase_models': models, 'callables': eq_callables,
            'data_weight': data_weights.get('ACR', 1.0),
        },
    }
    return error_context
espei/optimizers/graph.py
class OptNode:
    """
    Node as the result of an optimization.

    Attributes
    ----------
    parameters : dict
    datasets : PickleableTinyDB
    id : int
    parent : OptNode
    children : list of OptNode

    Notes
    -----
    OptNodes are individual nodes in the graph that correspond to the result of
    a call to fit - they represent optimized parameters given the parent state
    and some data (also part of the OptNode).

    Each OptNode can only be derived from one set of parameters, however one
    parameter state may be a branching point to many new parameter states, so an
    OptNode can have only one parent, but many children.

    """
    def __init__(self, parameters, datasets, node_id=None):
        self.parameters = copy.deepcopy(parameters)
        self.datasets = copy.deepcopy(datasets)
        self.id = node_id
        self.parent = None
        self.children = set()

    def __repr__(self):
        str_params = str(self.parameters)
        return "<OptNode({}, node_id={})>".format(str_params, self.id)

    def __str__(self):
        str_params = str(self.parameters)
        return "<OptNode({}, node_id={})>".format(str_params, self.id)

    def __hash__(self):
        return self.id

    def __eq__(self, other):
        return self.id == other.id
espei/utils.py
def optimal_parameters(trace_array, lnprob_array, kth=0):
    """
    Return the optimal parameters in the trace based on the highest likelihood.
    If kth is specified, return the kth set of *unique* optimal parameters.

    Parameters
    ----------
    trace_array : ndarray
        Array of shape (number of chains, iterations, number of parameters)
    lnprob_array : ndarray
        Array of shape (number of chains, iterations)
    kth : int
        Zero-indexed optimum. 0 (the default) is the most optimal solution. 1 is
        the second most optimal, etc.. Only *unique* solutions will be returned.

    Returns
    -------
    Array of optimal parameters

    Notes
    -----
    It is ok if the calculation did not finish and the arrays are padded with
    zeros. The number of chains and iterations in the trace and lnprob arrays
    must match.

    Examples
    --------
    >>> from espei.utils import optimal_parameters
    >>> trace = np.array([[[1, 0], [2, 0], [3, 0], [0, 0]], [[0, 2], [0, 4], [0, 6], [0, 0]]])  # 3 iterations of 4 allocated
    >>> lnprob = np.array([[-6, -4, -2, 0], [-3, -1, -2, 0]])
    >>> np.all(np.isclose(optimal_parameters(trace, lnprob), np.array([0, 4])))
    True

    """
    # indicies of chains + iterations that have non-zero parameters (that step has run)
    nz = np.nonzero(np.any(trace_array != 0, axis=-1))
    # chain + iteration index with the highest likelihood
    unique_params = np.zeros(trace_array.shape[-1])
    unique_params_found = -1
    # loop through all possible nonzero iterations
    for i in range(nz[-1][-1]):
        # find the next set of parameters parameters
        candidate_index = np.argpartition(-lnprob_array[nz], i)[i]
        candidate_params = trace_array[nz][candidate_index]
        # if the parameters are unique, make them the new unique parameters
        if np.any(candidate_params != unique_params):
            unique_params = candidate_params
            unique_params_found += 1
        # if we have found the kth set of unique parameters, stop
        if unique_params_found == kth:
            return unique_params
    return np.zeros(trace_array.shape[-1])
espei/error_functions/equilibrium_thermochemical_error.py
def calculate_equilibrium_thermochemical_probability(eq_thermochemical_data: Sequence[EqPropData],
                                                     parameters: np.ndarray,
                                                     approximate_equilibrium: Optional[bool] = False,
                                                     ) -> float:
    """
    Calculate the total equilibrium thermochemical probability for all EqPropData

    Parameters
    ----------
    eq_thermochemical_data : Sequence[EqPropData]
        List of equilibrium thermochemical data corresponding to the datasets.
    parameters : np.ndarray
        Values of parameters for this iteration to be updated in PhaseRecords.
    approximate_equilibrium : Optional[bool], optional

    eq_thermochemical_data : Sequence[EqPropData]

    Returns
    -------
    float
        Sum of log-probability for all thermochemical data.

    """
    if len(eq_thermochemical_data) == 0:
        return 0.0

    differences = []
    weights = []
    for eqpropdata in eq_thermochemical_data:
        diffs, wts = calc_prop_differences(eqpropdata, parameters, approximate_equilibrium)
        if np.any(np.isinf(diffs) | np.isnan(diffs)):
            # NaN or infinity are assumed calculation failures. If we are
            # calculating log-probability, just bail out and return -infinity.
            return -np.inf
        differences.append(diffs)
        weights.append(wts)

    differences = np.concatenate(differences, axis=0)
    weights = np.concatenate(weights, axis=0)
    probs = norm(loc=0.0, scale=weights).logpdf(differences)
    return np.sum(probs)
espei/priors.py
class PriorSpec(object):
    """
    Specification template for instantiating priors.
    """
    SUPPORTED_PRIORS = ('normal', 'uniform', 'triangular', 'zero')

    def __init__(self, name, **parameters):
        """

        Parameters
        ----------
        name : parameter name
        parameters : dict
            Parameters to be passed

        """
        if name not in self.SUPPORTED_PRIORS:
            raise ValueError("Selected prior is not ")
        self.name = name
        self.prior_generator = getattr(self, '_prior_'+name)

        # process parameters
        distribution_params = {}
        for k, v in parameters.items():
            # the key is the parameter type, e.g 'scale', 'scale_relative', 'loc_absolute'
            split_type = k.split('_')
            param = split_type[0]
            if len(split_type) > 1:
                param_type = '_'.join(split_type[1:])
            else:
                param_type = 'absolute'
            distribution_params[param] = DistributionParameter(v, param_type=param_type)

        self.parameters = distribution_params

    def get_prior(self, value):
        """Instantiate a prior as described in the spec

        Examples
        --------
        >>> import numpy as np
        >>> from espei.priors import PriorSpec
        >>> tri_spec = {'name': 'triangular', 'loc_shift_relative': -0.5, 'scale_shift_relative': 0.5, 'c': 0.5}
        >>> np.isneginf(PriorSpec(**tri_spec).get_prior(10).logpdf(5.1))
        False
        >>> np.isneginf(PriorSpec(**tri_spec).get_prior(10).logpdf(4.9))
        True

        """
        params = {k: dp.value(value) for k, dp in self.parameters.items()}
        return self.prior_generator(params)

    def _prior_normal(self, params):
        """Instantiate a normal prior"""
        # make the standard deviation is positive
        params['scale'] = np.abs(params.get('scale', 1.0))
        return norm(**params)

    def _prior_uniform(self, params):
        """Instantiate a uniform prior"""
        # make sure loc is min and scale is max
        params['scale'] = np.abs(params.get('scale', 1.0))
        return uniform(**params)

    def _prior_triangular(self, params):
        """Instantiate a triangular prior"""
        # make sure loc is min and scale is max
        params['scale'] = np.abs(params.get('scale', 1.0))
        # make sure c is set
        params['c'] = params.get('c', 0.5)
        return triang(**params)

    def _prior_zero(self, _):
        """Instantiate a zero prior"""
        return rv_zero()
espei/error_functions/non_equilibrium_thermochemical_error.py
def calculate_non_equilibrium_thermochemical_probability(thermochemical_data, parameters=None):
    """
    Calculate the weighted single phase error in the Database

    Parameters
    ----------
    thermochemical_data : list
        List of thermochemical data dicts
    parameters : np.ndarray
        Array of parameters to calculate the error with.

    Returns
    -------
    float
        A single float of the residual sum of square errors

    """
    if parameters is None:
        parameters = np.array([])

    prob_error = 0.0
    for data in thermochemical_data:
        phase_name = data['phase_name']
        output = data['output']
        phase_records = data['phase_records']
        sample_values = data['calculate_dict']['values']

        update_phase_record_parameters(phase_records, parameters)
        results = calculate_(data['species'], [phase_name],
                             data['str_statevar_dict'], data['model'],
                             phase_records, output=output, broadcast=False,
                             points=data['calculate_dict']['points'])[output]
        differences = results - sample_values
        probabilities = norm.logpdf(differences, loc=0, scale=data['weights'])
        prob_sum = np.sum(probabilities)
        _log.debug("%s(%s) - probability sum: %0.2f, data: %s, differences: %s, probabilities: %s, references: %s", data['prop'], phase_name, prob_sum, sample_values, differences, probabilities, data['calculate_dict']['references'])
        prob_error += prob_sum
    return prob_error
espei/optimizers/opt_base.py
class OptimizerBase(object):
    """Enable fitting and replaying fitting steps"""
    def __init__(self, dbf):
        self.orig_dbf = copy.deepcopy(dbf)
        self.dbf = copy.deepcopy(dbf)
        parameters = {sym: unpack_piecewise(dbf.symbols[sym]) for sym in database_symbols_to_fit(dbf)}
        ds = load_datasets([])  # empty TinyDB
        root = OptNode(parameters, ds)
        self.current_node = root
        self.staged_nodes = []
        self.graph = OptGraph(root)

    def _fit(self, symbols, datasets, *args, **kwargs):
        """
        Optimize a set of symbols to the passed datasets

        Parameters
        ----------
        symbols : list of str
        datasets : PickleableTinyDB

        Returns
        -------
        OptNode

        """
        raise NotImplementedError("The `_fit` method not implemented. Create a subclass of OptimizerBase with `_fit` overridden to use it")

    def fit(self, symbols, datasets, *args, **kwargs):
        node = self._fit(symbols, datasets, *args, **kwargs)
        self.staged_nodes.append(node)
        self.dbf.symbols.update(node.parameters)
        return self.dbf

    @staticmethod
    def predict(params, context):
        """Given a set of parameters and a context, return the resulting sum of square error.

        Parameters
        ----------
        params : list
            1 dimensional array of parameters
        context : dict
            Dictionary of arguments/keyword arguments to pass to functions

        Returns
        -------
        float
        """
        raise NotImplementedError("The `predict` method not implemented. Create a subclass of OptimizerBase with `predict` overridden to use it")

    def commit(self,):
        if len(self.staged_nodes) > 0:
            for staged in self.staged_nodes:
                self.graph.add_node(staged, self.current_node)
                self.current_node = staged
            self.staged_nodes = []
            self.reset_database()
        else:
            raise OptimizerError("Nothing to commit. Stage a commit by running the `fit` method.")

    def discard(self):
        """Discard all staged nodes"""
        self.staged_nodes = []
        self.reset_database()

    def reset_database(self):
        """Set the Database to the state of the current node"""
        trans_dict = self.graph.get_transformation_dict(self.current_node)
        self.dbf = copy.deepcopy(self.orig_dbf)
        self.dbf.symbols.update(trans_dict)


Question: import logging
import sys
import time
import numpy as np
import emcee
from espei.error_functions import calculate_zpf_error, calculate_activity_error, \
    calculate_non_equilibrium_thermochemical_probability, \
    calculate_equilibrium_thermochemical_probability
from espei.priors import PriorSpec, build_prior_specs
from espei.utils import unpack_piecewise, optimal_parameters
from espei.error_functions.context import setup_context
from .opt_base import OptimizerBase
from .graph import OptNode
            from espei.rstate import numpy_rstate
        Parameters
        ----------
        symbols : list of str
        ds : PickleableTinyDB
        prior : str
            Prior to use to generate priors. Defaults to 'zero', which keeps
            backwards compatibility. Can currently choose 'normal', 'uniform',
            'triangular', or 'zero'.
        iterations : int
            Number of iterations to calculate in MCMC. Default is 1000.
        chains_per_parameter : int
            number of chains for each parameter. Must be an even integer greater
            or equal to 2. Defaults to 2.
        chain_std_deviation : float
            Standard deviation of normal for parameter initialization as a
            fraction of each parameter. Must be greater than 0. Defaults to 0.1.
        deterministic : bool
            If True, the emcee sampler will be seeded to give deterministic sampling
            draws. This will ensure that the runs with the exact same database,
            chains_per_parameter, and chain_std_deviation (or restart_trace) will
            produce exactly the same results.
        restart_trace : np.ndarray
            ndarray of the previous trace. Should have shape (chains, iterations, parameters)
        tracefile : str
            filename to store the trace with NumPy.save. Array has shape
            (chains, iterations, parameters)
        probfile : str
            filename to store the log probability with NumPy.save. Has shape (chains, iterations)
        mcmc_data_weights : dict
            Dictionary of weights for each data type, e.g. {'ZPF': 20, 'HM': 2}

        Returns
        -------
        OptNode

        """
        # Set NumPy print options so logged arrays print on one line. Reset at the end.
        np.set_printoptions(linewidth=sys.maxsize)
        cbs = self.scheduler is None
        ctx = setup_context(self.dbf, ds, symbols, data_weights=mcmc_data_weights, phase_models=self.phase_models, make_callables=cbs)
        symbols_to_fit = ctx['symbols_to_fit']
        initial_guess = np.array([unpack_piecewise(self.dbf.symbols[s]) for s in symbols_to_fit])

        prior_dict = self.get_priors(prior, symbols_to_fit, initial_guess)
        ctx.update(prior_dict)
        ctx['zpf_kwargs']['approximate_equilibrium'] = approximate_equilibrium
        ctx['equilibrium_thermochemical_kwargs']['approximate_equilibrium'] = approximate_equilibrium
        # Run the initial parameters for guessing purposes:
        _log.trace("Probability for initial parameters")
        self.predict(initial_guess, **ctx)
        if restart_trace is not None:
            chains = self.initialize_chains_from_trace(restart_trace)
            # TODO: check that the shape is valid with the existing parameters
        else:
            chains = self.initialize_new_chains(initial_guess, chains_per_parameter, chain_std_deviation, deterministic)
        sampler = emcee.EnsembleSampler(chains.shape[0], initial_guess.size, self.predict, kwargs=ctx, pool=self.scheduler)
        if deterministic:
            sampler.random_state = numpy_rstate
            _log.info('Using a deterministic ensemble sampler.')
        self.sampler = sampler
        self.tracefile = tracefile
        self.probfile = probfile
        # Run the MCMC simulation
        self.do_sampling(chains, iterations)

        # Post process
        optimal_params = optimal_parameters(sampler.chain, sampler.lnprobability)
        _log.trace('Initial parameters: %s', initial_guess)
        _log.trace('Optimal parameters: %s', optimal_params)
        _log.trace('Change in parameters: %s', np.abs(initial_guess - optimal_params) / initial_guess)
        parameters = dict(zip(symbols_to_fit, optimal_params))
        np.set_printoptions(linewidth=75)
        return OptNode(parameters, ds)

    @staticmethod
    def predict(params, **ctx):
        """
        Calculate lnprob = lnlike + lnprior
        """
        _log.debug('Parameters - %s', params)

        # Important to coerce to floats here because the values _must_ be floats if
        # they are used to update PhaseRecords directly
        params = np.asarray(params, dtype=np.float_)

        # lnprior
        prior_rvs = ctx['prior_rvs']
        lnprior_multivariate = [rv.logpdf(theta) for rv, theta in zip(prior_rvs, params)]
        _log.debug('Priors: %s', lnprior_multivariate)
        lnprior = np.sum(lnprior_multivariate)
        if np.isneginf(lnprior):
            # It doesn't matter what the likelihood is. We can skip calculating it to save time.
            _log.trace('Proposal - lnprior: %0.4f, lnlike: %0.4f, lnprob: %0.4f', lnprior, np.nan, lnprior)
            return lnprior

        # lnlike
        parameters = {param_name: param for param_name, param in zip(ctx['symbols_to_fit'], params.tolist())}
        zpf_kwargs = ctx.get('zpf_kwargs')
        activity_kwargs = ctx.get('activity_kwargs')
        non_equilibrium_thermochemical_kwargs = ctx.get('thermochemical_kwargs')
        equilibrium_thermochemical_kwargs = ctx.get('equilibrium_thermochemical_kwargs')
        starttime = time.time()
        if zpf_kwargs is not None:
            try:
                multi_phase_error = calculate_zpf_error(parameters=params, **zpf_kwargs)
            except (ValueError, np.linalg.LinAlgError) as e:
                raise e
                print(e)
                multi_phase_error = -np.inf
        else:
            multi_phase_error = 0
        if equilibrium_thermochemical_kwargs is not None:
            eq_thermochemical_prob = calculate_equilibrium_thermochemical_probability(parameters=params, **equilibrium_thermochemical_kwargs)
        else:
            eq_thermochemical_prob = 0
        if activity_kwargs is not None:
            actvity_error = calculate_activity_error(parameters=parameters, **activity_kwargs)
        else:
            actvity_error = 0
        if non_equilibrium_thermochemical_kwargs is not None:

Answer:             non_eq_thermochemical_prob = calculate_non_equilibrium_thermochemical_probability(parameters=params, **non_equilibrium_thermochemical_kwargs)