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/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/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)
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/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/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/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/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/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/utils.py
def unpack_piecewise(x):
    if isinstance(x, symengine.Piecewise):
        return float(x.args[0])
    else:
        return float(x)
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


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
    implemented in emcee [2]

    Attributes
    ----------
    scheduler : mappable
        An object implementing a `map` function
    save_interval : int
        Interval of iterations to save the tracefile and probfile.
    tracefile : str
        Filename to store the trace with NumPy.save. Array has shape
        (chains, iterations, parameters). Defaults to None.
    probfile : str
        filename to store the log probability with NumPy.save. Has shape (chains, iterations)

    References
    ----------
    [1] Goodman and Weare, Ensemble Samplers with Affine Invariance. Commun. Appl. Math. Comput. Sci. 5, 65-80 (2010).
    [2] Foreman-Mackey, Hogg, Lang, Goodman, emcee: The MCMC Hammer. Publ. Astron. Soc. Pac. 125, 306-312 (2013).
    """
    def __init__(self, dbf, phase_models=None, scheduler=None):
        super(EmceeOptimizer, self).__init__(dbf)
        self.phase_models = phase_models
        self.scheduler = scheduler
        self.save_interval = 1
        # These are set by the _fit method
        self.sampler = None
        self.tracefile = None
        self.probfile = None

    @staticmethod
    def initialize_new_chains(params, chains_per_parameter, std_deviation, deterministic=True):
        """
        Return an array of num_samples from a Gaussian distribution about each parameter.

        Parameters
        ----------
        params : ndarray
            1D array of initial parameters that will be the mean of the distribution.
        num_samples : int
            Number of chains to initialize.
        chains_per_parameter : int
            number of chains for each parameter. Must be an even integer greater or
            equal to 2. Defaults to 2.
        std_deviation : float
            Fractional standard deviation of the parameters to use for initialization.
        deterministic : bool
            True if the parameters should be generated deterministically.

        Returns
        -------
        ndarray

        Notes
        -----
        Parameters are sampled from ``normal(loc=param, scale=param*std_deviation)``.
        A parameter of zero will produce a standard deviation of zero and
        therefore only zeros will be sampled. This will break emcee's
        StretchMove for this parameter and only zeros will be selected.

        """
        _log.trace('Initial parameters: %s', params)
        params = np.array(params)
        num_zero_params = np.nonzero(params == 0)[0].size
        if num_zero_params > 0:
            _log.warning(
                "%s initial parameters are initialized to zero. The ensemble of chains "
                "for zero parameters will be all initialized to zero and all proposed "
                "values for these parameter will be zero. If possible, it's better to "
                "make a good guess at a reasonable parameter value to start with. "
                "Alternatively, you can start with a small value near zero and let the "
                "ensemble search parameter space.", num_zero_params)
        nchains = params.size * chains_per_parameter
        _log.info('Initializing %s chains with %s chains per parameter.', nchains, chains_per_parameter)
        if deterministic:
            rng = np.random.RandomState(1769)
        else:
            rng = np.random.RandomState()
        # apply a Gaussian random to each parameter with std dev of std_deviation*parameter
        tiled_parameters = np.tile(params, (nchains, 1))
        chains = rng.normal(tiled_parameters, np.abs(tiled_parameters * std_deviation))
        return chains

    @staticmethod
    def initialize_chains_from_trace(restart_trace):
        tr = restart_trace
        walkers = tr[np.nonzero(tr)].reshape((tr.shape[0], -1, tr.shape[2]))[:, -1, :]
        nchains = walkers.shape[0]
        ndim = walkers.shape[1]
        initial_parameters = walkers.mean(axis=0)
        _log.info('Restarting from previous calculation with %s chains (%s per parameter).', nchains, nchains / ndim)
        _log.trace('Means of restarting parameters are %s', initial_parameters)
        _log.trace('Standard deviations of restarting parameters are %s', walkers.std(axis=0))
        return walkers

    @staticmethod
    def get_priors(prior, symbols, params):
        """
        Build priors for a particular set of fitting symbols and initial parameters.
        Returns a dict that should be used to update the context.

        Parameters
        ----------
        prior : dict or PriorSpec or None
            Prior to initialize. See the docs on
        symbols : list of str
            List of symbols that will be fit
        params : list of float
            List of parameter values corresponding to the symbols. These should
            be the initial parameters that the priors will be based off of.

        Returns
        -------

        """
        if isinstance(prior, dict):
            _log.info('Initializing a %s prior for the parameters.', prior['name'])
        elif isinstance(prior, PriorSpec):
            _log.info('Initializing a %s prior for the parameters.', prior.name)
        elif prior is None:
            prior = {'name': 'zero'}

Answer:         prior_specs = build_prior_specs(prior, params)