示例#1
0
文件: model.py 项目: tkphd/pycalphad
    def __init__(self, dbe, comps, phase, parameters=None):
        # Constrain possible components to those within phase's d.o.f
        possible_comps = set([x.upper() for x in comps])
        self.components = set()
        self.constituents = []
        self.phase_name = phase.upper()
        self.site_ratios = dbe.phases[phase.upper()].sublattices
        for sublattice in dbe.phases[phase.upper()].constituents:
            self.components |= set(sublattice).intersection(possible_comps)
        logger.debug("Model of %s has components %s", phase, self.components)
        # Verify that this phase is still possible to build
        for sublattice in dbe.phases[phase.upper()].constituents:
            if len(set(sublattice).intersection(self.components)) == 0:
                # None of the components in a sublattice are active
                # We cannot build a model of this phase
                raise DofError(
                    "{0}: Sublattice {1} of {2} has no components in {3}".format(
                        phase.upper(), sublattice, dbe.phases[phase.upper()].constituents, self.components
                    )
                )
            self.constituents.append(set(sublattice).intersection(self.components))

        # Convert string symbol names to sympy Symbol objects
        # This makes xreplace work with the symbols dict
        symbols = dict([(Symbol(s), val) for s, val in dbe.symbols.items()])

        if parameters is not None:
            symbols.update([(Symbol(s), val) for s, val in parameters.items()])

        self.models = dict()
        self.build_phase(dbe, phase.upper(), symbols, dbe.search)
        self.site_fractions = sorted(self.ast.atoms(v.SiteFraction), key=str)

        # Need to do more substitutions to catch symbols that are functions
        # of other symbols
        for name, value in self.models.items():
            try:
                for iteration in range(_MAX_PARAM_NESTING):
                    self.models[name] = self.models[name].xreplace(symbols)
                    undefs = self.models[name].atoms(Symbol) - self.models[name].atoms(v.StateVariable)
                    if len(undefs) == 0:
                        break
            except AttributeError:
                # Can't use xreplace on a float
                pass
示例#2
0
文件: model.py 项目: broshe/pycalphad
    def __init__(self, dbe, comps, phase, parameters=None):
        # Constrain possible components to those within phase's d.o.f
        possible_comps = set([x.upper() for x in comps])
        self.components = set()
        for sublattice in dbe.phases[phase.upper()].constituents:
            self.components |= set(sublattice).intersection(possible_comps)
        logger.debug('Model of %s has components %s', phase, self.components)
        # Verify that this phase is still possible to build
        for sublattice in dbe.phases[phase.upper()].constituents:
            if len(set(sublattice).intersection(self.components)) == 0:
                # None of the components in a sublattice are active
                # We cannot build a model of this phase
                raise DofError(
                    '{0}: Sublattice {1} of {2} has no components in {3}' \
                    .format(phase.upper(), sublattice,
                            dbe.phases[phase.upper()].constituents,
                            self.components))

        # Convert string symbol names to sympy Symbol objects
        # This makes xreplace work with the symbols dict
        symbols = dict([(Symbol(s), val) for s, val in dbe.symbols.items()])
        if parameters is not None:
            symbols.update([(Symbol(s), val) for s, val in parameters.items()])
        # Need to do more substitutions to catch symbols that are functions
        # of other symbols
        for name, value in symbols.items():
            try:
                symbols[name] = value.xreplace(symbols)
            except AttributeError:
                # Can't use xreplace on a float
                pass
        for name, value in symbols.items():
            try:
                symbols[name] = value.xreplace(symbols)
            except AttributeError:
                # Can't use xreplace on a float
                pass

        # Build the abstract syntax tree
        self.ast = self.build_phase(dbe, phase.upper(), symbols, dbe.search)
        self.ast = self.ast.xreplace(symbols)
        self.variables = self.ast.atoms(v.StateVariable)
示例#3
0
    def __init__(self, dbe, comps, phase, parameters=None):
        # Constrain possible components to those within phase's d.o.f
        possible_comps = set([x.upper() for x in comps])
        self.components = set()
        for sublattice in dbe.phases[phase.upper()].constituents:
            self.components |= set(sublattice).intersection(possible_comps)
        logger.debug('Model of %s has components %s', phase, self.components)
        # Verify that this phase is still possible to build
        for sublattice in dbe.phases[phase.upper()].constituents:
            if len(set(sublattice).intersection(self.components)) == 0:
                # None of the components in a sublattice are active
                # We cannot build a model of this phase
                raise DofError(
                    '{0}: Sublattice {1} of {2} has no components in {3}' \
                    .format(phase.upper(), sublattice,
                            dbe.phases[phase.upper()].constituents,
                            self.components))

        # Convert string symbol names to sympy Symbol objects
        # This makes xreplace work with the symbols dict
        symbols = dict([(Symbol(s), val) for s, val in dbe.symbols.items()])

        if parameters is not None:
            symbols.update([(Symbol(s), val) for s, val in parameters.items()])

        self.models = dict()
        self.build_phase(dbe, phase.upper(), symbols, dbe.search)

        # Need to do more substitutions to catch symbols that are functions
        # of other symbols
        for name, value in self.models.items():
            try:
                for iteration in range(_MAX_PARAM_NESTING):
                    self.models[name] = self.models[name].xreplace(symbols)
                    undefs = self.models[name].atoms(
                        Symbol) - self.models[name].atoms(v.StateVariable)
                    if len(undefs) == 0:
                        break
            except AttributeError:
                # Can't use xreplace on a float
                pass
示例#4
0
    def __init__(self, dbf, comps, phases, conditions, **kwargs):
        self.conditions = conditions
        self.components = set(comps)
        self.phases = dict()
        self.statevars = dict()
        self.data = pd.DataFrame()

        self._phases = dict([[name, dbf.phases[name]] for name in phases])
        self._phase_callables = dict()
        self._gradient_callables = dict()
        self._molefrac_callables = dict()
        self._molefrac_jac_callables = dict()
        self._variables = dict()
        self._sublattice_dof = dict()
        self.statevars = dict()
        for key in ['T', 'P']:
            try:
                self.statevars[v.StateVariable(key)] = kwargs[key]
            except KeyError:
                pass

        # Construct models for each phase; prioritize user models
        self._models = unpack_kwarg(kwargs.pop('model', Model), \
            default_arg=Model)
        for name in phases:
            mod = self._models[name]
            if isinstance(mod, type):
                # Initialize the model
                self._models[name] = mod(dbf, self.components, name)

        self._build_objective_functions()

        self.data = energy_surf(dbf, comps, phases, model=self._models, \
            **kwargs)

        # self.data now contains energy surface information for the system
        # find simplex for a starting point; refine with optimization
        estimates = self.get_starting_simplex()
        logger.debug(estimates)
        self.result = self.minimize(estimates[0], estimates[1])
示例#5
0
    def __init__(self, dbe, comps, phase_name, parameters=None):
        # Constrain possible components to those within phase's d.o.f
        possible_comps = set([x.upper() for x in comps])
        self.components = set()
        self.constituents = []
        self.phase_name = phase_name.upper()
        phase = dbe.phases[self.phase_name]
        self.site_ratios = phase.sublattices
        for sublattice in phase.constituents:
            self.components |= set(sublattice).intersection(possible_comps)
        logger.debug('Model of %s has components %s', self.phase_name,
                     self.components)
        # Verify that this phase is still possible to build
        for sublattice in phase.constituents:
            if len(set(sublattice).intersection(self.components)) == 0:
                # None of the components in a sublattice are active
                # We cannot build a model of this phase
                raise DofError(
                    '{0}: Sublattice {1} of {2} has no components in {3}' \
                    .format(self.phase_name, sublattice,
                            phase.constituents,
                            self.components))
            self.constituents.append(
                set(sublattice).intersection(self.components))

        # Convert string symbol names to sympy Symbol objects
        # This makes xreplace work with the symbols dict
        symbols = dict([(Symbol(s), val) for s, val in dbe.symbols.items()])

        if parameters is not None:
            symbols.update([(Symbol(s), val) for s, val in parameters.items()])

        self.models = OrderedDict()
        self.build_phase(dbe)
        self.site_fractions = sorted(self.ast.atoms(v.SiteFraction), key=str)

        for name, value in self.models.items():
            self.models[name] = self.symbol_replace(value, symbols)
示例#6
0
    def __init__(self, dbe, comps, phase_name, parameters=None):
        # Constrain possible components to those within phase's d.o.f
        possible_comps = set([x.upper() for x in comps])
        self.components = set()
        self.constituents = []
        self.phase_name = phase_name.upper()
        phase = dbe.phases[self.phase_name]
        self.site_ratios = phase.sublattices
        for sublattice in phase.constituents:
            self.components |= set(sublattice).intersection(possible_comps)
        logger.debug("Model of %s has components %s", self.phase_name, self.components)
        # Verify that this phase is still possible to build
        for sublattice in phase.constituents:
            if len(set(sublattice).intersection(self.components)) == 0:
                # None of the components in a sublattice are active
                # We cannot build a model of this phase
                raise DofError(
                    "{0}: Sublattice {1} of {2} has no components in {3}".format(
                        self.phase_name, sublattice, phase.constituents, self.components
                    )
                )
            self.constituents.append(set(sublattice).intersection(self.components))

        # Convert string symbol names to sympy Symbol objects
        # This makes xreplace work with the symbols dict
        symbols = dict([(Symbol(s), val) for s, val in dbe.symbols.items()])

        if parameters is not None:
            symbols.update([(Symbol(s), val) for s, val in parameters.items()])

        self.models = OrderedDict()
        self.build_phase(dbe)
        self.site_fractions = sorted(self.ast.atoms(v.SiteFraction), key=str)

        for name, value in self.models.items():
            self.models[name] = self.symbol_replace(value, symbols)
示例#7
0
 def __init__(self, dbe, comps, phase, parameters=None):
     possible_comps = set([x.upper() for x in comps])
     print possible_comps
     self.components = set()
     for sublattice in dbe.phases[phase.upper()].constituents:
         self.components |= set(sublattice).intersection(possible_comps)
     logger.debug('Model of %s has components %s', phase, self.components)
     print self.components
     # Verify that this phase is still possible to build
     for sublattice in dbe.phases[phase.upper()].constituents:
         if len(set(sublattice).intersection(self.components)) == 0:
             # None of the components in a sublattice are active
             # We cannot build a model of this phase
             raise DofError(
                 '{0}: Sublattice {1} of {2} has no components in {3}' \
                 .format(phase.upper(), sublattice,
                         dbe.phases[phase.upper()].constituents,
                         self.components))
     symbols = dict([(Symbol(s), val) for s, val in dbe.symbols.items()])
     if parameters is not None:
         symbols.update([(Symbol(s), val) for s, val in parameters.items()])
     # Need to do more substitutions to catch symbols that are functions
     # of other symbols
     for name, value in symbols.items():
         try:
             symbols[name] = value.xreplace(symbols)
         except AttributeError:
             # Can't use xreplace on a float
             pass
     for name, value in symbols.items():
         try:
             symbols[name] = value.xreplace(symbols)
         except AttributeError:
             # Can't use xreplace on a float
             pass
     self.ast = self.build_phase(dbe, phase.upper(), symbols, dbe.search)
示例#8
0
 def get_starting_simplex(self):
     """
     Calculate convex hull and find a suitable starting point.
     Returns (DataFrame of phase compositions, ndarray of phase fractions)
     """
     phase_compositions, phase_fracs, pots = \
         lower_convex_hull(self.data, self.components, self.conditions)
     if phase_compositions is None:
         logger.error('Unable to find starting point for calculation')
         raise EquilibriumError('Unable to find starting point for calculation')
     logger.debug(self.data.iloc[phase_compositions])
     independent_indices = \
         check_degenerate_phases(self.data.iloc[phase_compositions],
                                 mindist=0.1)
     logger.debug('phase_fracs: %s', phase_fracs)
     logger.debug('independent_indices: %s', independent_indices)
     # renormalize phase fractions to 1 after eliminating redundant phases
     phase_fracs = phase_fracs[independent_indices]
     phase_fracs /= np.sum(phase_fracs)
     return [self.data.iloc[phase_compositions[independent_indices]],
             phase_fracs]
示例#9
0
def energy_surf(dbf, comps, phases, mode=None, **kwargs):
    """
    Sample the energy surface of a system containing the specified
    components and phases. Model parameters are taken from 'dbf' and any
    state variables (T, P, etc.) can be specified as keyword arguments.

    Parameters
    ----------
    dbf : Database
        Thermodynamic database containing the relevant parameters.
    comps : list
        Names of components to consider in the calculation.
    phases : list
        Names of phases to consider in the calculation.
    pdens : int, a dict of phase names to int, or a list of both, optional
        Number of points to sample per degree of freedom.

    Returns
    -------
    DataFrame of the energy as a function of composition, temperature, etc.

    Examples
    --------
    None yet.
    """
    # Here we check for any keyword arguments that are special, i.e.,
    # there may be keyword arguments that aren't state variables
    pdens_dict = unpack_kwarg(kwargs.pop('pdens', 2000), default_arg=2000)
    model_dict = unpack_kwarg(kwargs.pop('model', Model), default_arg=Model)

    # Convert keyword strings to proper state variable objects
    # If we don't do this, sympy will get confused during substitution
    statevar_dict = \
        dict((v.StateVariable(key), value) \
             for (key, value) in kwargs.items())

    # Generate all combinations of state variables for 'map' calculation
    # Wrap single values of state variables in lists
    # Use 'kwargs' because we want state variable names to be stringified
    statevar_values = [_listify(val) for val in kwargs.values()]
    statevars_to_map = [dict(zip(kwargs.keys(), prod)) \
        for prod in itertools.product(*statevar_values)]

    # Consider only the active phases
    active_phases = dict((name.upper(), dbf.phases[name.upper()]) \
        for name in phases)
    comp_sets = {}
    # Construct a list to hold all the data
    all_phase_data = []
    for phase_name, phase_obj in sorted(active_phases.items()):
        # Build the symbolic representation of the energy
        mod = model_dict[phase_name]
        # if this is an object type, we need to construct it
        if isinstance(mod, type):
            try:
                mod = mod(dbf, comps, phase_name)
            except DofError:
                # we can't build the specified phase because the
                # specified components aren't found in every sublattice
                # we'll just skip it
                logger.warning("""Suspending specified phase %s due to
                some sublattices containing only unspecified components""",
                               phase_name)
                continue
        # As a last resort, treat undefined symbols as zero
        # But warn the user when we do this
        # This is consistent with TC's behavior
        undefs = list(mod.ast.atoms(Symbol) - mod.ast.atoms(v.StateVariable))
        for undef in undefs:
            mod.ast = mod.ast.xreplace({undef: float(0)})
            logger.warning('Setting undefined symbol %s for phase %s to zero',
                           undef, phase_name)
        # Construct an ordered list of the variables
        variables, sublattice_dof = generate_dof(phase_obj, mod.components)

        # Build the "fast" representation of that model
        comp_sets[phase_name] = make_callable(mod.ast, \
            list(statevar_dict.keys()) + variables, mode=mode)

        # Get the site ratios in each sublattice
        site_ratios = list(phase_obj.sublattices)

        # Eliminate pure vacancy endmembers from the calculation
        vacancy_indices = list()
        for idx, sublattice in enumerate(phase_obj.constituents):
            if 'VA' in sorted(sublattice) and 'VA' in sorted(comps):
                vacancy_indices.append(sorted(sublattice).index('VA'))
        if len(vacancy_indices) != len(phase_obj.constituents):
            vacancy_indices = None
        logger.debug('vacancy_indices: %s', vacancy_indices)
        # Add all endmembers to guarantee their presence
        points = endmember_matrix(sublattice_dof,
                                  vacancy_indices=vacancy_indices)

        # Sample composition space for more points
        if sum(sublattice_dof) > len(sublattice_dof):
            points = np.concatenate((points,
                                     point_sample(sublattice_dof,
                                                  pdof=pdens_dict[phase_name])
                                    ))



        # If there are nontrivial sublattices with vacancies in them,
        # generate a set of points where their fraction is zero and renormalize
        for idx, sublattice in enumerate(phase_obj.constituents):
            if 'VA' in set(sublattice) and len(sublattice) > 1:
                var_idx = variables.index(v.SiteFraction(phase_name, idx, 'VA'))
                addtl_pts = np.copy(points)
                # set vacancy fraction to log-spaced between 1e-10 and 1e-6
                addtl_pts[:, var_idx] = np.power(10.0, -10.0*(1.0 - addtl_pts[:, var_idx]))
                # renormalize site fractions
                cur_idx = 0
                for ctx in sublattice_dof:
                    end_idx = cur_idx + ctx
                    addtl_pts[:, cur_idx:end_idx] /= \
                        addtl_pts[:, cur_idx:end_idx].sum(axis=1)[:, None]
                    cur_idx = end_idx
                # add to points matrix
                points = np.concatenate((points, addtl_pts), axis=0)

        data_dict = {'Phase': phase_name}
        # Generate input d.o.f matrix for all state variable combinations
        for statevars in statevars_to_map:
            # Prefill the state variable arguments to the energy function
            energy_func = \
                lambda *args: comp_sets[phase_name](
                    *itertools.chain(list(statevars.values()),
                                     args))
            # Get the stable points and energies for this configuration
            # Set max refinements equal to the number of independent dof
            mxr = sum(phase_obj.sublattices) - len(phase_obj.sublattices)
            refined_points, energies = \
                refine_energy_surf(points, None, phase_obj, comps,
                                   variables, energy_func, max_iterations=-1)
            try:
                data_dict['GM'].extend(energies)
                for statevar in kwargs.keys():
                    data_dict[statevar].extend(
                        list(np.repeat(list(statevars.values()),
                                       len(refined_points))))
            except KeyError:
                data_dict['GM'] = list(energies)
                for statevar in kwargs.keys():
                    data_dict[statevar] = \
                        list(np.repeat(list(statevars.values()),
                                       len(refined_points)))

            # Map the internal degrees of freedom to global coordinates

            # Normalize site ratios
            # Normalize by the sum of site ratios times a factor
            # related to the site fraction of vacancies
            site_ratio_normalization = np.zeros(len(refined_points))
            for idx, sublattice in enumerate(phase_obj.constituents):
                vacancy_column = np.ones(len(refined_points))
                if 'VA' in set(sublattice):
                    var_idx = variables.index(v.SiteFraction(phase_name, idx, 'VA'))
                    vacancy_column -= refined_points[:, var_idx]
                site_ratio_normalization += site_ratios[idx] * vacancy_column

            for comp in sorted(comps):
                if comp == 'VA':
                    continue
                avector = [float(cur_var.species == comp) * \
                    site_ratios[cur_var.sublattice_index] for cur_var in variables]
                try:
                    data_dict['X('+comp+')'].extend(list(np.divide(np.dot(
                        refined_points[:, :], avector), site_ratio_normalization)))
                except KeyError:
                    data_dict['X('+comp+')'] = list(np.divide(np.dot(
                        refined_points[:, :], avector), site_ratio_normalization))

            # Copy coordinate information into data_dict
            # TODO: Is there a more memory-efficient way to deal with this?
            # Perhaps with hierarchical indexing...
            try:
                for column_idx, data in enumerate(refined_points.T):
                    data_dict[str(variables[column_idx])].extend(list(data))
            except KeyError:
                for column_idx, data in enumerate(refined_points.T):
                    data_dict[str(variables[column_idx])] = list(data)

        all_phase_data.append(pd.DataFrame(data_dict))

    # all_phases_data now contains energy surface information for the system
    return pd.concat(all_phase_data, axis=0, join='outer', \
                            ignore_index=True, verify_integrity=False)
示例#10
0
def lower_convex_hull(global_grid, result_array, verbose=False):
    """
    Find the simplices on the lower convex hull satisfying the specified
    conditions in the result array.

    Parameters
    ----------
    global_grid : Dataset
        A sample of the energy surface of the system.
    result_array : Dataset
        This object will be modified!
        Coordinates correspond to conditions axes.
    verbose : bool
        Display details to stdout. Useful for debugging.

    Returns
    -------
    None. Results are written to result_array.

    Notes
    -----
    This routine will not check if any simplex is degenerate.
    Degenerate simplices will manifest with duplicate or NaN indices.

    Examples
    --------
    None yet.
    """
    conditions = [x for x in result_array.coords.keys() if x not in ['vertex',
                                                                     'component']]
    indep_conds = sorted([x for x in sorted(result_array.coords.keys()) if x in ['T', 'P']])
    indep_shape = tuple(len(result_array.coords[x]) for x in indep_conds)
    comp_conds = sorted([x for x in sorted(result_array.coords.keys()) if x.startswith('X_')])
    comp_shape = tuple(len(result_array.coords[x]) for x in comp_conds)
    pot_conds = sorted([x for x in sorted(result_array.coords.keys()) if x.startswith('MU_')])
    # force conditions to have particular ordering
    conditions = indep_conds + pot_conds + comp_conds
    trial_shape = (len(result_array.coords['component']),)
    trial_points = None
    _initialize_array(global_grid, result_array)

    # Enforce ordering of shape if this is the first iteration
    if result_array.attrs['hull_iterations'] == 1:
        result_array['points'] = result_array['points'].transpose(*(conditions + ['vertex']))
        result_array['GM'] = result_array['GM'].transpose(*conditions)
        result_array['NP'] = result_array['NP'].transpose(*(conditions + ['vertex']))

    # Determine starting combinations of chemical potentials and compositions
    # TODO: Check Gibbs phase rule compliance

    if len(pot_conds) > 0:
        raise NotImplementedError('Chemical potential conditions are not yet supported')

    # FIRST CASE: Only composition conditions specified
    # We only need to compute the dependent composition value directly
    # Initialize trial points as lowest energy point in the system
    if (len(comp_conds) > 0) and (len(pot_conds) == 0):
        trial_points = np.empty(result_array['GM'].T.shape)
        trial_points.fill(np.inf)
        trial_points[...] = global_grid['GM'].argmin(dim='points').values.T
        trial_points = trial_points.T
        comp_values = cartesian([result_array.coords[cond] for cond in comp_conds])
        # Insert dependent composition value
        # TODO: Handle W(comp) as well as X(comp) here
        specified_components = set([x[2:] for x in comp_conds])
        dependent_component = set(result_array.coords['component'].values) - specified_components
        dependent_component = list(dependent_component)
        if len(dependent_component) != 1:
            raise ValueError('Number of dependent components is different from one')
        insert_idx = sorted(result_array.coords['component'].values).index(dependent_component[0])
        comp_values = np.concatenate((comp_values[..., :insert_idx],
                                      1 - np.sum(comp_values, keepdims=True, axis=-1),
                                      comp_values[..., insert_idx:]),
                                     axis=-1)
        # Prevent compositions near an edge from going negative
        comp_values[np.nonzero(comp_values < MIN_SITE_FRACTION)] = MIN_SITE_FRACTION*10
        # TODO: Assumes N=1
        comp_values /= comp_values.sum(axis=-1, keepdims=True)
        #print(comp_values)

    # SECOND CASE: Only chemical potential conditions specified
    # TODO: Implementation of chemical potential

    # THIRD CASE: Mixture of composition and chemical potential conditions
    # TODO: Implementation of mixed conditions

    if trial_points is None:
        raise ValueError('Invalid conditions')

    driving_forces = np.zeros(result_array.GM.values.shape + (len(global_grid.points),),
                                   dtype=np.float)

    max_iterations = 200
    iterations = 0
    while iterations < max_iterations:
        iterations += 1

        trial_simplices = np.empty(result_array['points'].values.shape + \
                                   (result_array['points'].values.shape[-1],), dtype=np.int)
        # Initialize trial simplices with values from best guess simplices
        trial_simplices[..., :, :] = result_array['points'].values[..., np.newaxis, :]
        # Trial simplices will be the current simplex with each vertex
        #     replaced by the trial point
        # Exactly one of those simplices will contain a given test point,
        #     excepting edge cases
        trial_simplices.T[np.diag_indices(trial_shape[0])] = trial_points.T
        #print('trial_simplices.shape', trial_simplices.shape)
        #print('global_grid.X.values.shape', global_grid.X.values.shape)
        flat_statevar_indices = np.unravel_index(np.arange(np.multiply.reduce(result_array.MU.values.shape)),
                                                 result_array.MU.values.shape)[:len(indep_conds)]
        #print('flat_statevar_indices', flat_statevar_indices)
        trial_matrix = global_grid.X.values[np.index_exp[flat_statevar_indices +
                                                         (trial_simplices.reshape(-1, trial_simplices.shape[-1]).T,)]]
        trial_matrix = np.rollaxis(trial_matrix, 0, -1)
        #print('trial_matrix', trial_matrix)
        # Partially ravel the array to make indexing operations easier
        trial_matrix.shape = (-1,) + trial_matrix.shape[-2:]

        # We have to filter out degenerate simplices before
        #     phase fraction computation
        # This is because even one degenerate simplex causes the entire tensor
        #     to be singular
        nondegenerate_indices = np.all(np.linalg.svd(trial_matrix,
                                                     compute_uv=False) > 1e-09,
                                       axis=-1, keepdims=True)
        #('NONDEGENERATE INDICES', nondegenerate_indices)
        # Determine how many trial simplices remain for each target point.
        # In principle this would always be one simplex per point, but once
        # some target values reach equilibrium, trial_points starts
        # to contain points already on our best guess simplex.
        # This causes trial_simplices to create degenerate simplices.
        # We can safely filter them out since those target values are
        # already at equilibrium.
        #sum_array = np.sum(nondegenerate_indices, axis=-1, dtype=np.int)
        #index_array = np.repeat(np.arange(trial_matrix.shape[0], dtype=np.int),
        #                        sum_array)
        index_array = np.arange(trial_matrix.shape[0], dtype=np.int)
        comp_shape = trial_simplices.shape[:len(indep_conds)+len(pot_conds)] + \
                     (comp_values.shape[0], trial_simplices.shape[-2])

        comp_indices = np.unravel_index(index_array, comp_shape)[len(indep_conds)+len(pot_conds)]
        fractions = np.full(result_array['points'].values.shape + \
                            (result_array['points'].values.shape[-1],), -1.)
        fractions[np.unravel_index(index_array, fractions.shape[:-1])] = \
            stacked_lstsq(np.swapaxes(trial_matrix[index_array], -2, -1),
                          comp_values[comp_indices])
        fractions /= fractions.sum(axis=-1, keepdims=True)
        #print('fractions', fractions)
        # A simplex only contains a point if its barycentric coordinates
        # (phase fractions) are non-negative.
        bounding_indices = np.all(fractions >= -MIN_SITE_FRACTION*100, axis=-1)
        #print('BOUNDING INDICES', bounding_indices)
        if ~np.any(bounding_indices):
            raise ValueError('Desired composition is not inside any candidate simplex. This is a bug.')
        multiple_success_trials = np.sum(bounding_indices, axis=-1, dtype=np.int, keepdims=False) != 1
        #print('MULTIPLE SUCCESS TRIALS SHAPE', np.nonzero(multiple_success_trials))
        if np.any(multiple_success_trials):
            saved_trial = np.zeros_like(multiple_success_trials, dtype=np.int)
            # Case of only degenerate simplices (zero bounding)
            # Choose trial with "least negative" fraction
            zero_success_indices = np.logical_and(~nondegenerate_indices.reshape(bounding_indices.shape),
                                                  multiple_success_trials[..., np.newaxis])
            saved_trial[np.nonzero(zero_success_indices.any(axis=-1))] = \
                np.argmax(fractions[np.nonzero(zero_success_indices.any(axis=-1))].min(axis=-1), axis=-1)
            # Case of multiple bounding non-degenerate simplices
            # Choose the first one. This addresses gh-28.
            multiple_bounding_indices = \
                np.logical_and(np.logical_and(bounding_indices, nondegenerate_indices.reshape(bounding_indices.shape)),
                               multiple_success_trials[..., np.newaxis])
            #print('MULTIPLE SUCCESS TRIALS.shape', multiple_success_trials.shape)
            #print('BOUNDING INDICES.shape', bounding_indices.shape)
            #print('MULTIPLE_BOUNDING_INDICES.shape', multiple_bounding_indices.shape)
            saved_trial[np.nonzero(multiple_bounding_indices.any(axis=-1))] = \
                np.argmax(multiple_bounding_indices[np.nonzero(multiple_bounding_indices.any(axis=-1))], axis=-1)
            #print('SAVED TRIAL.shape', saved_trial.shape)
            #print('BOUNDING INDICES BEFORE', bounding_indices)
            bounding_indices[np.nonzero(multiple_success_trials)] = False
            #print('BOUNDING INDICES FALSE', bounding_indices)
            bounding_indices[np.nonzero(multiple_success_trials) + \
                             (saved_trial[np.nonzero(multiple_success_trials)],)] = True
            #print('BOUNDING INDICES AFTER', bounding_indices)
        fractions.shape = (-1, fractions.shape[-1])
        bounding_indices.shape = (-1,)
        index_array = np.arange(trial_matrix.shape[0], dtype=np.int)[bounding_indices]

        raveled_simplices = trial_simplices.reshape((-1,) + trial_simplices.shape[-1:])
        candidate_simplices = raveled_simplices[index_array, :]
        #print('candidate_simplices', candidate_simplices)

        # We need to convert the flat index arrays into multi-index tuples.
        # These tuples will tell us which state variable combinations are relevant
        # for the calculation. We can drop the last dimension, 'trial'.
        #print('trial_simplices.shape[:-1]', trial_simplices.shape[:-1])
        statevar_indices = np.unravel_index(index_array, trial_simplices.shape[:-1]
                                            )[:len(indep_conds)+len(pot_conds)]
        aligned_energies = global_grid.GM.values[statevar_indices + (candidate_simplices.T,)].T
        statevar_indices = tuple(x[..., np.newaxis] for x in statevar_indices)
        #print('statevar_indices', statevar_indices)
        aligned_compositions = global_grid.X.values[np.index_exp[statevar_indices + (candidate_simplices,)]]
        #print('aligned_compositions', aligned_compositions)
        #print('aligned_energies', aligned_energies)
        candidate_potentials = stacked_lstsq(aligned_compositions.astype(np.float, copy=False),
                                             aligned_energies.astype(np.float, copy=False))
        #print('candidate_potentials', candidate_potentials)
        logger.debug('candidate_simplices: %s', candidate_simplices)
        comp_indices = np.unravel_index(index_array, comp_shape)[len(indep_conds)+len(pot_conds)]
        #print('comp_values[comp_indices]', comp_values[comp_indices])
        candidate_energies = np.multiply(candidate_potentials,
                                         comp_values[comp_indices]).sum(axis=-1)
        #print('candidate_energies', candidate_energies)

        # Generate a matrix of energies comparing our calculations for this iteration
        # to each other.
        # 'conditions' axis followed by a 'trial' axis
        # Empty values are filled in with infinity
        comparison_matrix = np.empty([int(trial_matrix.shape[0] / trial_shape[0]),
                                      trial_shape[0]])
        if comparison_matrix.shape[0] != aligned_compositions.shape[0]:
            raise ValueError('Arrays have become misaligned. This is a bug. Try perturbing your composition conditions '
                             'by a small amount (1e-4). If you would like, you can report this issue to the development'
                             ' team and they will fix it for future versions.')
        comparison_matrix.fill(np.inf)
        comparison_matrix[np.divide(index_array, trial_shape[0]).astype(np.int),
                          np.mod(index_array, trial_shape[0])] = candidate_energies
        #print('comparison_matrix', comparison_matrix)

        # If a condition point is all infinities, it means we did not calculate it
        # We should filter those out from any comparisons
        calculated_indices = ~np.all(comparison_matrix == np.inf, axis=-1)
        # Extract indices for trials with the lowest energy for each target point
        lowest_energy_indices = np.argmin(comparison_matrix[calculated_indices],
                                          axis=-1)
        # Filter conditions down to only those calculated this iteration
        calculated_conditions_indices = np.arange(comparison_matrix.shape[0])[calculated_indices]

        #print('comparison_matrix[calculated_conditions_indices,lowest_energy_indices]',comparison_matrix[calculated_conditions_indices,
        #                                    lowest_energy_indices])
        # This has to be greater-than-or-equal because, in the case where
        # the desired condition is right on top of a simplex vertex (gh-28), there
        # will be no change in energy changing a "_FAKE_" vertex to a real one.
        is_lower_energy = comparison_matrix[calculated_conditions_indices,
                                            lowest_energy_indices] <= \
            result_array['GM'].values.flat[calculated_conditions_indices]
        #print('is_lower_energy', is_lower_energy)

        # These are the conditions we will update this iteration
        final_indices = calculated_conditions_indices[is_lower_energy]
        #print('final_indices', final_indices)
        # Convert to multi-index form so we can index the result array
        final_multi_indices = np.unravel_index(final_indices,
                                               result_array['GM'].values.shape)

        updated_potentials = candidate_potentials[is_lower_energy]
        result_array['points'].values[final_multi_indices] = candidate_simplices[is_lower_energy]
        result_array['GM'].values[final_multi_indices] = candidate_energies[is_lower_energy]
        result_array['MU'].values[final_multi_indices] = updated_potentials
        result_array['NP'].values[final_multi_indices] = \
            fractions[np.nonzero(bounding_indices)][is_lower_energy]
        #print('result_array.GM.values', result_array.GM.values)

        # By profiling, it's faster to recompute all driving forces in-place
        # versus doing fancy indexing to only update "changed" driving forces
        # This avoids the creation of expensive temporaries
        np.einsum('...i,...i',
                  result_array.MU.values[..., np.newaxis, :],
                  global_grid.X.values[np.index_exp[...] + ((np.newaxis,) * len(comp_conds)) + np.index_exp[:, :]],
                  out=driving_forces)
        np.subtract(driving_forces,
                    global_grid.GM.values[np.index_exp[...] + ((np.newaxis,) * len(comp_conds)) + np.index_exp[:]],
                    out=driving_forces)


        # Update trial points to choose points with largest remaining driving force
        trial_points = np.argmax(driving_forces, axis=-1)
        #print('trial_points', trial_points)
        logger.debug('trial_points: %s', trial_points)

        # If all driving force (within some tolerance) is consumed, we found equilibrium
        if np.all(driving_forces <= DRIVING_FORCE_TOLERANCE):
            return
    if verbose:
        print('Max hull iterations exceeded. Remaining driving force: ', driving_forces.max())
示例#11
0
    def minimize(self, simplex, phase_fractions=None):
        """
        Accept a list of simplex vertices and return the values of the
        variables that minimize the energy under the constraints.
        """
        # Generate phase fraction variables
        # Track the multiplicity of phases with a Counter object
        composition_sets = Counter()
        all_variables = []
        # starting point
        x_0 = []
        # scaling factor -- set to minimum energy of starting simplex
        # Scaling the objective to be of order '10' seems to result in
        # sufficient precision (at least 5 significant figures).
        scaling_factor = abs(simplex['GM'].min()) / 10.0
        # a list of tuples for where each phase's variable indices
        # start and end
        index_ranges = []
        #print(list(enumerate(simplex.iterrows())))
        #print((simplex.iterrows()))
        #print('END')
        for m_idx, vertex in enumerate(simplex.iterrows()):
            vertex = vertex[1]
            # increase multiplicity by one
            composition_sets[vertex['Phase']] += 1
            # create new phase fraction variable
            all_variables.append(
                v.PhaseFraction(vertex['Phase'],
                                composition_sets[vertex['Phase']])
                )
            start = len(x_0)
            # default position is centroid of the simplex
            if phase_fractions is None:
                x_0.append(1.0/len(list(simplex.iterrows())))
            else:
                # use the provided guess for the phase fraction
                x_0.append(phase_fractions[m_idx])

            # add site fraction variables
            all_variables.extend(self._variables[vertex['Phase']])
            # add starting point for variable
            for varname in self._variables[vertex['Phase']]:
                x_0.append(vertex[str(varname)])
            index_ranges.append([start, len(x_0)])

        # Create master objective function
        def obj(input_x):
            "Objective function. Takes x vector as input. Returns scalar."
            objective = 0.0
            for idx, vertex in enumerate(simplex.iterrows()):
                vertex = vertex[1]
                cur_x = input_x[index_ranges[idx][0]+1:index_ranges[idx][1]]
                #print('Phase: '+vertex['Phase']+' '+str(cur_x))
                # phase fraction times value of objective for that phase
                objective += input_x[index_ranges[idx][0]] * \
                    self._phase_callables[vertex['Phase']](
                        *list(cur_x))
            return objective / scaling_factor

        # Create master gradient function
        def gradient(input_x):
            "Accepts input vector and returns gradient vector."
            gradient = np.zeros(len(input_x))
            for idx, vertex in enumerate(simplex.iterrows()):
                vertex = vertex[1]
                cur_x = input_x[index_ranges[idx][0]+1:index_ranges[idx][1]]
                #print('grad cur_x: '+str(cur_x))
                # phase fraction derivative is just the phase energy
                gradient[index_ranges[idx][0]] = \
                    self._phase_callables[vertex['Phase']](
                        *list(cur_x))
                # gradient for particular phase's variables
                # NOTE: We assume all phase d.o.f are independent here,
                # and we handle any coupling through the constraints
                for g_idx, grad in \
                    enumerate(self._gradient_callables[vertex['Phase']]):
                    gradient[index_ranges[idx][0]+1+g_idx] = \
                        input_x[index_ranges[idx][0]] * \
                            grad(*list(cur_x))
            #print('grad: '+str(gradient / scaling_factor))
            return gradient / scaling_factor

        # Generate constraint sequence
        constraints = []

        # phase fraction constraint
        def phasefrac_cons(input_x):
            "Accepts input vector and returns phase fraction constraint."
            output = 1.0 - sum([input_x[i[0]] for i in index_ranges])#** 2
            return output
        def phasefrac_jac(input_x):
            "Accepts input vector and returns Jacobian of constraint."
            output_x = np.zeros(len(input_x))
            for idx_range in index_ranges:
                output_x[idx_range[0]] = -1.0 #\
                #    -2.0*sum([input_x[i[0]] for i in index_ranges])
            return output_x
        phasefrac_dict = dict()
        phasefrac_dict['type'] = 'eq'
        phasefrac_dict['fun'] = phasefrac_cons
        phasefrac_dict['jac'] = phasefrac_jac
        constraints.append(phasefrac_dict)

        # bounds constraint
        def nonneg_cons(input_x, idx):
            "Accepts input vector and returns non-negativity constraint."
            output = input_x[idx]
            #print('nonneg_cons: '+str(output))
            return output

        def nonneg_jac(input_x, idx):
            "Accepts input vector and returns Jacobian of constraint."
            output_x = np.zeros(len(input_x))
            output_x[idx] = 1.0
            return output_x

        for idx in range(len(all_variables)):
            nonneg_dict = dict()
            nonneg_dict['type'] = 'ineq'
            nonneg_dict['fun'] = nonneg_cons
            nonneg_dict['jac'] = nonneg_jac
            nonneg_dict['args'] = [idx]
            constraints.append(nonneg_dict)

        # Generate all site fraction constraints
        for idx_range in index_ranges:
            # need to generate constraint for each sublattice
            dofs = self._sublattice_dof[all_variables[idx_range[0]].phase_name]
            cur_idx = idx_range[0]+1
            for dof in dofs:
                sitefrac_dict = dict()
                sitefrac_dict['type'] = 'eq'
                sitefrac_dict['fun'] = sitefrac_cons
                sitefrac_dict['jac'] = sitefrac_jac
                sitefrac_dict['args'] = [[cur_idx, cur_idx+dof]]
                cur_idx += dof
                if dof > 0:
                    constraints.append(sitefrac_dict)

        # All other constraints, e.g., mass balance
        def molefrac_cons(input_x, species, fix_val, all_variables, phases):
            """
            Accept input vector, species and fixed value.
            Returns constraint.
            """
            output = -fix_val
            for idx, vertex in enumerate(simplex.iterrows()):
                vertex = vertex[1]
                cur_x = input_x[index_ranges[idx][0]+1:index_ranges[idx][1]]
                res = self._molefrac_callables[vertex['Phase']][species](*cur_x)
                output += input_x[index_ranges[idx][0]] * res
            #print('molefrac_cons: '+str(output))
            return output
        def molefrac_jac(input_x, species, fix_val, all_variables, phases):
            "Accepts input vector and returns Jacobian vector."
            output_x = np.zeros(len(input_x))
            for idx, vertex in enumerate(simplex.iterrows()):
                vertex = vertex[1]
                cur_x = input_x[index_ranges[idx][0]+1:index_ranges[idx][1]]
                output_x[index_ranges[idx][0]] = \
                    self._molefrac_callables[vertex['Phase']][species](*cur_x)
                for g_idx, grad in \
                    enumerate(self._molefrac_jac_callables[vertex['Phase']][species]):
                    output_x[index_ranges[idx][0]+1+g_idx] = \
                        input_x[index_ranges[idx][0]] * \
                            grad(*list(cur_x))
            #print('molefrac_jac '+str(output_x))
            return output_x

        eqs = len([x for x in constraints if x['type'] == 'eq'])
        if eqs < len(x_0):
            for condition, value in self.conditions.items():
                if isinstance(condition, v.Composition):
                    # mass balance constraint for mole fraction
                    molefrac_dict = dict()
                    molefrac_dict['type'] = 'eq'
                    molefrac_dict['fun'] = molefrac_cons
                    molefrac_dict['jac'] = molefrac_jac
                    molefrac_dict['args'] = \
                        [condition.species, value, all_variables,
                         self._phases]
                    constraints.append(molefrac_dict)
        else:
            logger.warning("""Dropping mass balance constraints due to
                zero internal degrees of freedom""")

        # Run optimization
        res = scipy.optimize.minimize(obj, x_0, method='slsqp', jac=gradient,\
            constraints=constraints, options={'maxiter': 1000})
        # rescale final values back to original
        res['raw_fun'] = copy.deepcopy(res['fun'])
        res['raw_jac'] = copy.deepcopy(res['jac'])
        res['raw_x'] = copy.deepcopy(res['x'])
        res['fun'] *= scaling_factor
        res['jac'] *= scaling_factor
        # force tiny numerical values to be positive
        res['x'] = np.maximum(res['x'], np.zeros(1, dtype=np.float64))
        logger.debug(res)
        if not res['success']:
            logger.error('Energy minimization failed')
            return None

        # Build result object
        eq_res = EquilibriumResult(self._phases, self.components,
                                   self.statevars, res['fun'],
                                   zip(all_variables, res['x']))
        return eq_res
示例#12
0
def lower_convex_hull(global_grid, result_array):
    """
    Find the simplices on the lower convex hull satisfying the specified
    conditions in the result array.

    Parameters
    ----------
    global_grid : Dataset
        A sample of the energy surface of the system.
    result_array : Dataset
        This object will be modified!
        Coordinates correspond to conditions axes.

    Returns
    -------
    None. Results are written to result_array.

    Notes
    -----
    This routine will not check if any simplex is degenerate.
    Degenerate simplices will manifest with duplicate or NaN indices.

    Examples
    --------
    None yet.
    """
    conditions = [
        x for x in result_array.coords.keys()
        if x not in ['vertex', 'component']
    ]
    indep_conds = sorted(
        [x for x in sorted(result_array.coords.keys()) if x in ['T', 'P']])
    indep_shape = tuple(len(result_array.coords[x]) for x in indep_conds)
    comp_conds = sorted(
        [x for x in sorted(result_array.coords.keys()) if x.startswith('X_')])
    comp_shape = tuple(len(result_array.coords[x]) for x in comp_conds)
    pot_conds = sorted(
        [x for x in sorted(result_array.coords.keys()) if x.startswith('MU_')])
    # force conditions to have particular ordering
    conditions = indep_conds + pot_conds + comp_conds
    trial_shape = (len(result_array.coords['component']), )
    trial_points = None
    _initialize_array(global_grid, result_array)

    # Enforce ordering of shape
    result_array['points'] = result_array['points'].transpose(*(conditions +
                                                                ['vertex']))
    result_array['GM'] = result_array['GM'].transpose(*(conditions))
    result_array['NP'] = result_array['NP'].transpose(*(conditions +
                                                        ['vertex']))

    # Determine starting combinations of chemical potentials and compositions
    # TODO: Check Gibbs phase rule compliance

    if len(pot_conds) > 0:
        raise NotImplementedError(
            'Chemical potential conditions are not yet supported')

    # FIRST CASE: Only composition conditions specified
    # We only need to compute the dependent composition value directly
    # Initialize trial points as lowest energy point in the system
    if (len(comp_conds) > 0) and (len(pot_conds) == 0):
        trial_points = np.empty(result_array['GM'].T.shape)
        trial_points.fill(np.inf)
        trial_points[...] = global_grid['GM'].argmin(dim='points').values.T
        trial_points = trial_points.T
        comp_values = cartesian(
            [result_array.coords[cond] for cond in comp_conds])
        # Insert dependent composition value
        # TODO: Handle W(comp) as well as X(comp) here
        specified_components = set([x[2:] for x in comp_conds])
        dependent_component = set(
            result_array.coords['component'].values) - specified_components
        dependent_component = list(dependent_component)
        if len(dependent_component) != 1:
            raise ValueError(
                'Number of dependent components is different from one')
        insert_idx = sorted(result_array.coords['component'].values).index(
            dependent_component[0])
        comp_values = np.concatenate(
            (comp_values[..., :insert_idx],
             1 - np.sum(comp_values, keepdims=True, axis=-1),
             comp_values[..., insert_idx:]),
            axis=-1)

    # SECOND CASE: Only chemical potential conditions specified
    # TODO: Implementation of chemical potential

    # THIRD CASE: Mixture of composition and chemical potential conditions
    # TODO: Implementation of mixed conditions

    if trial_points is None:
        raise ValueError('Invalid conditions')

    driving_forces = np.zeros(result_array.GM.values.shape +
                              (len(global_grid.points), ),
                              dtype=np.float)

    max_iterations = 50
    iterations = 0
    while iterations < max_iterations:
        iterations += 1

        trial_simplices = np.empty(result_array['points'].values.shape + \
                                   (result_array['points'].values.shape[-1],), dtype=np.int)
        # Initialize trial simplices with values from best guess simplices
        trial_simplices[..., :, :] = result_array['points'].values[
            ..., np.newaxis, :]
        # Trial simplices will be the current simplex with each vertex
        #     replaced by the trial point
        # Exactly one of those simplices will contain a given test point,
        #     excepting edge cases
        trial_simplices.T[np.diag_indices(trial_shape[0])] = trial_points.T
        #print('trial_simplices.shape', trial_simplices.shape)
        #print('global_grid.X.values.shape', global_grid.X.values.shape)
        flat_statevar_indices = np.unravel_index(
            np.arange(np.multiply.reduce(result_array.MU.values.shape)),
            result_array.MU.values.shape)[:len(indep_conds)]
        #print('flat_statevar_indices', flat_statevar_indices)
        trial_matrix = global_grid.X.values[np.index_exp[
            flat_statevar_indices +
            (trial_simplices.reshape(-1, trial_simplices.shape[-1]).T, )]]
        trial_matrix = np.rollaxis(trial_matrix, 0, -1)
        #print('trial_matrix', trial_matrix)
        # Partially ravel the array to make indexing operations easier
        trial_matrix.shape = (-1, ) + trial_matrix.shape[-2:]

        # We have to filter out degenerate simplices before
        #     phase fraction computation
        # This is because even one degenerate simplex causes the entire tensor
        #     to be singular
        nondegenerate_indices = np.all(
            np.linalg.svd(trial_matrix, compute_uv=False) > 1e-12,
            axis=-1,
            keepdims=True)
        # Determine how many trial simplices remain for each target point.
        # In principle this would always be one simplex per point, but once
        # some target values reach equilibrium, trial_points starts
        # to contain points already on our best guess simplex.
        # This causes trial_simplices to create degenerate simplices.
        # We can safely filter them out since those target values are
        # already at equilibrium.
        sum_array = np.sum(nondegenerate_indices, axis=-1, dtype=np.int)
        index_array = np.repeat(np.arange(trial_matrix.shape[0], dtype=np.int),
                                sum_array)
        comp_shape = trial_simplices.shape[:len(indep_conds)+len(pot_conds)] + \
                     (comp_values.shape[0], trial_simplices.shape[-2])

        comp_indices = np.unravel_index(
            index_array, comp_shape)[len(indep_conds) + len(pot_conds)]

        fractions = np.linalg.solve(
            np.swapaxes(trial_matrix[index_array], -2, -1),
            comp_values[comp_indices])

        # A simplex only contains a point if its barycentric coordinates
        # (phase fractions) are positive.
        bounding_indices = np.all(fractions >= 0, axis=-1)
        index_array = np.atleast_1d(index_array[bounding_indices])

        raveled_simplices = trial_simplices.reshape((-1, ) +
                                                    trial_simplices.shape[-1:])
        candidate_simplices = raveled_simplices[index_array, :]
        #print('candidate_simplices', candidate_simplices)

        # We need to convert the flat index arrays into multi-index tuples.
        # These tuples will tell us which state variable combinations are relevant
        # for the calculation. We can drop the last dimension, 'trial'.
        #print('trial_simplices.shape[:-1]', trial_simplices.shape[:-1])
        statevar_indices = np.unravel_index(
            index_array,
            trial_simplices.shape[:-1])[:len(indep_conds) + len(pot_conds)]
        aligned_energies = global_grid.GM.values[statevar_indices +
                                                 (candidate_simplices.T, )].T
        statevar_indices = tuple(x[..., np.newaxis] for x in statevar_indices)
        #print('statevar_indices', statevar_indices)
        aligned_compositions = global_grid.X.values[np.index_exp[
            statevar_indices + (candidate_simplices, )]]
        #print('aligned_compositions', aligned_compositions)
        #print('aligned_energies', aligned_energies)
        candidate_potentials = np.linalg.solve(
            aligned_compositions.astype(np.float, copy=False),
            aligned_energies.astype(np.float, copy=False))
        #print('candidate_potentials', candidate_potentials)
        logger.debug('candidate_simplices: %s', candidate_simplices)
        comp_indices = np.unravel_index(
            index_array, comp_shape)[len(indep_conds) + len(pot_conds)]
        #print('comp_values[comp_indices]', comp_values[comp_indices])
        candidate_energies = np.multiply(
            candidate_potentials, comp_values[comp_indices]).sum(axis=-1)
        #print('candidate_energies', candidate_energies)

        # Generate a matrix of energies comparing our calculations for this iteration
        # to each other.
        # 'conditions' axis followed by a 'trial' axis
        # Empty values are filled in with infinity
        comparison_matrix = np.empty(
            [trial_matrix.shape[0] / trial_shape[0], trial_shape[0]])
        assert comparison_matrix.shape[0] == aligned_compositions.shape[0]
        comparison_matrix.fill(np.inf)
        comparison_matrix[
            np.divide(index_array, trial_shape[0]).astype(np.int),
            np.mod(index_array, trial_shape[0])] = candidate_energies
        #print('comparison_matrix', comparison_matrix)

        # If a condition point is all infinities, it means we did not calculate it
        # We should filter those out from any comparisons
        calculated_indices = ~np.all(comparison_matrix == np.inf, axis=-1)
        # Extract indices for trials with the lowest energy for each target point
        lowest_energy_indices = np.argmin(
            comparison_matrix[calculated_indices], axis=-1)
        # Filter conditions down to only those calculated this iteration
        calculated_conditions_indices = np.arange(
            comparison_matrix.shape[0])[calculated_indices]

        #print('comparison_matrix[calculated_conditions_indices,lowest_energy_indices]',comparison_matrix[calculated_conditions_indices,
        #                                    lowest_energy_indices])
        is_lower_energy = comparison_matrix[calculated_conditions_indices,
                                            lowest_energy_indices] < \
            result_array['GM'].values.flat[calculated_conditions_indices]
        #print('is_lower_energy', is_lower_energy)

        # These are the conditions we will update this iteration
        final_indices = calculated_conditions_indices[is_lower_energy]
        #print('final_indices', final_indices)
        # Convert to multi-index form so we can index the result array
        final_multi_indices = np.unravel_index(final_indices,
                                               result_array['GM'].values.shape)

        updated_potentials = candidate_potentials[is_lower_energy]
        result_array['points'].values[
            final_multi_indices] = candidate_simplices[is_lower_energy]
        result_array['GM'].values[final_multi_indices] = candidate_energies[
            is_lower_energy]
        result_array['MU'].values[final_multi_indices] = updated_potentials
        result_array['NP'].values[final_multi_indices] = \
            fractions[bounding_indices][is_lower_energy]
        #print('result_array.GM.values', result_array.GM.values)

        # By profiling, it's faster to recompute all driving forces in-place
        # versus doing fancy indexing to only update "changed" driving forces
        # This avoids the creation of expensive temporaries
        np.einsum('...i,...i',
                  result_array.MU.values[..., np.newaxis, :],
                  global_grid.X.values[np.index_exp[...] +
                                       ((np.newaxis, ) * len(comp_conds)) +
                                       np.index_exp[:, :]],
                  out=driving_forces)
        np.subtract(driving_forces,
                    global_grid.GM.values[np.index_exp[...] +
                                          ((np.newaxis, ) * len(comp_conds)) +
                                          np.index_exp[:]],
                    out=driving_forces)

        # Update trial points to choose points with largest remaining driving force
        trial_points = np.argmax(driving_forces, axis=-1)
        #print('trial_points', trial_points)
        logger.debug('trial_points: %s', trial_points)

        # If all driving force (within some tolerance) is consumed, we found equilibrium
        if np.all(driving_forces <= DRIVING_FORCE_TOLERANCE):
            return
    #raise ValueError
    print('Iterations exceeded. Remaining driving force: ',
          driving_forces.max())
    logger.error('Iterations exceeded')
示例#13
0
def lower_convex_hull(global_grid, result_array):
    """
    Find the simplices on the lower convex hull satisfying the specified
    conditions in the result array.

    Parameters
    ----------
    global_grid : Dataset
        A sample of the energy surface of the system.
    result_array : Dataset
        This object will be modified!
        Coordinates correspond to conditions axes.

    Returns
    -------
    None. Results are written to result_array.

    Notes
    -----
    This routine will not check if any simplex is degenerate.
    Degenerate simplices will manifest with duplicate or NaN indices.

    Examples
    --------
    None yet.
    """
    conditions = [x for x in result_array.coords.keys() if x not in ['vertex',
                                                                     'component']]
    indep_conds = sorted([x for x in sorted(result_array.coords.keys()) if x in ['T', 'P']])
    indep_shape = tuple(len(result_array.coords[x]) for x in indep_conds)
    comp_conds = sorted([x for x in sorted(result_array.coords.keys()) if x.startswith('X_')])
    comp_shape = tuple(len(result_array.coords[x]) for x in comp_conds)
    pot_conds = sorted([x for x in sorted(result_array.coords.keys()) if x.startswith('MU_')])
    # force conditions to have particular ordering
    conditions = indep_conds + pot_conds + comp_conds
    trial_shape = (len(result_array.coords['component']),)
    trial_points = None
    _initialize_array(global_grid, result_array)

    # Enforce ordering of shape
    result_array['points'] = result_array['points'].transpose(*(conditions + ['vertex']))
    result_array['GM'] = result_array['GM'].transpose(*(conditions))
    result_array['NP'] = result_array['NP'].transpose(*(conditions + ['vertex']))

    # Determine starting combinations of chemical potentials and compositions
    # TODO: Check Gibbs phase rule compliance

    if len(pot_conds) > 0:
        raise NotImplementedError('Chemical potential conditions are not yet supported')

    # FIRST CASE: Only composition conditions specified
    # We only need to compute the dependent composition value directly
    # Initialize trial points as lowest energy point in the system
    if (len(comp_conds) > 0) and (len(pot_conds) == 0):
        trial_points = np.empty(result_array['GM'].T.shape)
        trial_points.fill(np.inf)
        trial_points[...] = global_grid['GM'].argmin(dim='points').values.T
        trial_points = trial_points.T
        comp_values = cartesian([result_array.coords[cond] for cond in comp_conds])
        # Insert dependent composition value
        # TODO: Handle W(comp) as well as X(comp) here
        specified_components = set([x[2:] for x in comp_conds])
        dependent_component = set(result_array.coords['component'].values) - specified_components
        dependent_component = list(dependent_component)
        if len(dependent_component) != 1:
            raise ValueError('Number of dependent components is different from one')
        insert_idx = sorted(result_array.coords['component'].values).index(dependent_component[0])
        comp_values = np.concatenate((comp_values[..., :insert_idx],
                                      1 - np.sum(comp_values, keepdims=True, axis=-1),
                                      comp_values[..., insert_idx:]),
                                     axis=-1)

    # SECOND CASE: Only chemical potential conditions specified
    # TODO: Implementation of chemical potential

    # THIRD CASE: Mixture of composition and chemical potential conditions
    # TODO: Implementation of mixed conditions

    if trial_points is None:
        raise ValueError('Invalid conditions')

    driving_forces = np.zeros(result_array.GM.values.shape + (len(global_grid.points),),
                                   dtype=np.float)

    max_iterations = 50
    iterations = 0
    while iterations < max_iterations:
        iterations += 1

        trial_simplices = np.empty(result_array['points'].values.shape + \
                                   (result_array['points'].values.shape[-1],), dtype=np.int)
        # Initialize trial simplices with values from best guess simplices
        trial_simplices[..., :, :] = result_array['points'].values[..., np.newaxis, :]
        # Trial simplices will be the current simplex with each vertex
        #     replaced by the trial point
        # Exactly one of those simplices will contain a given test point,
        #     excepting edge cases
        trial_simplices.T[np.diag_indices(trial_shape[0])] = trial_points.T
        #print('trial_simplices.shape', trial_simplices.shape)
        #print('global_grid.X.values.shape', global_grid.X.values.shape)
        flat_statevar_indices = np.unravel_index(np.arange(np.multiply.reduce(result_array.MU.values.shape)),
                                                 result_array.MU.values.shape)[:len(indep_conds)]
        #print('flat_statevar_indices', flat_statevar_indices)
        trial_matrix = global_grid.X.values[np.index_exp[flat_statevar_indices +
                                                         (trial_simplices.reshape(-1, trial_simplices.shape[-1]).T,)]]
        trial_matrix = np.rollaxis(trial_matrix, 0, -1)
        #print('trial_matrix', trial_matrix)
        # Partially ravel the array to make indexing operations easier
        trial_matrix.shape = (-1,) + trial_matrix.shape[-2:]

        # We have to filter out degenerate simplices before
        #     phase fraction computation
        # This is because even one degenerate simplex causes the entire tensor
        #     to be singular
        nondegenerate_indices = np.all(np.linalg.svd(trial_matrix,
                                                     compute_uv=False) > 1e-12,
                                       axis=-1, keepdims=True)
        # Determine how many trial simplices remain for each target point.
        # In principle this would always be one simplex per point, but once
        # some target values reach equilibrium, trial_points starts
        # to contain points already on our best guess simplex.
        # This causes trial_simplices to create degenerate simplices.
        # We can safely filter them out since those target values are
        # already at equilibrium.
        sum_array = np.sum(nondegenerate_indices, axis=-1, dtype=np.int)
        index_array = np.repeat(np.arange(trial_matrix.shape[0], dtype=np.int),
                                sum_array)
        comp_shape = trial_simplices.shape[:len(indep_conds)+len(pot_conds)] + \
                     (comp_values.shape[0], trial_simplices.shape[-2])

        comp_indices = np.unravel_index(index_array, comp_shape)[len(indep_conds)+len(pot_conds)]

        fractions = np.linalg.solve(np.swapaxes(trial_matrix[index_array], -2, -1),
                                    comp_values[comp_indices])

        # A simplex only contains a point if its barycentric coordinates
        # (phase fractions) are positive.
        bounding_indices = np.all(fractions >= 0, axis=-1)
        index_array = np.atleast_1d(index_array[bounding_indices])

        raveled_simplices = trial_simplices.reshape((-1,) + trial_simplices.shape[-1:])
        candidate_simplices = raveled_simplices[index_array, :]
        #print('candidate_simplices', candidate_simplices)

        # We need to convert the flat index arrays into multi-index tuples.
        # These tuples will tell us which state variable combinations are relevant
        # for the calculation. We can drop the last dimension, 'trial'.
        #print('trial_simplices.shape[:-1]', trial_simplices.shape[:-1])
        statevar_indices = np.unravel_index(index_array, trial_simplices.shape[:-1]
                                            )[:len(indep_conds)+len(pot_conds)]
        aligned_energies = global_grid.GM.values[statevar_indices + (candidate_simplices.T,)].T
        statevar_indices = tuple(x[..., np.newaxis] for x in statevar_indices)
        #print('statevar_indices', statevar_indices)
        aligned_compositions = global_grid.X.values[np.index_exp[statevar_indices + (candidate_simplices,)]]
        #print('aligned_compositions', aligned_compositions)
        #print('aligned_energies', aligned_energies)
        candidate_potentials = np.linalg.solve(aligned_compositions.astype(np.float, copy=False),
                                               aligned_energies.astype(np.float, copy=False))
        #print('candidate_potentials', candidate_potentials)
        logger.debug('candidate_simplices: %s', candidate_simplices)
        comp_indices = np.unravel_index(index_array, comp_shape)[len(indep_conds)+len(pot_conds)]
        #print('comp_values[comp_indices]', comp_values[comp_indices])
        candidate_energies = np.multiply(candidate_potentials,
                                         comp_values[comp_indices]).sum(axis=-1)
        #print('candidate_energies', candidate_energies)

        # Generate a matrix of energies comparing our calculations for this iteration
        # to each other.
        # 'conditions' axis followed by a 'trial' axis
        # Empty values are filled in with infinity
        comparison_matrix = np.empty([trial_matrix.shape[0] / trial_shape[0],
                                      trial_shape[0]])
        assert comparison_matrix.shape[0] == aligned_compositions.shape[0]
        comparison_matrix.fill(np.inf)
        comparison_matrix[np.divide(index_array, trial_shape[0]).astype(np.int),
                          np.mod(index_array, trial_shape[0])] = candidate_energies
        #print('comparison_matrix', comparison_matrix)

        # If a condition point is all infinities, it means we did not calculate it
        # We should filter those out from any comparisons
        calculated_indices = ~np.all(comparison_matrix == np.inf, axis=-1)
        # Extract indices for trials with the lowest energy for each target point
        lowest_energy_indices = np.argmin(comparison_matrix[calculated_indices],
                                          axis=-1)
        # Filter conditions down to only those calculated this iteration
        calculated_conditions_indices = np.arange(comparison_matrix.shape[0])[calculated_indices]

        #print('comparison_matrix[calculated_conditions_indices,lowest_energy_indices]',comparison_matrix[calculated_conditions_indices,
        #                                    lowest_energy_indices])
        is_lower_energy = comparison_matrix[calculated_conditions_indices,
                                            lowest_energy_indices] < \
            result_array['GM'].values.flat[calculated_conditions_indices]
        #print('is_lower_energy', is_lower_energy)

        # These are the conditions we will update this iteration
        final_indices = calculated_conditions_indices[is_lower_energy]
        #print('final_indices', final_indices)
        # Convert to multi-index form so we can index the result array
        final_multi_indices = np.unravel_index(final_indices,
                                               result_array['GM'].values.shape)

        updated_potentials = candidate_potentials[is_lower_energy]
        result_array['points'].values[final_multi_indices] = candidate_simplices[is_lower_energy]
        result_array['GM'].values[final_multi_indices] = candidate_energies[is_lower_energy]
        result_array['MU'].values[final_multi_indices] = updated_potentials
        result_array['NP'].values[final_multi_indices] = \
            fractions[bounding_indices][is_lower_energy]
        #print('result_array.GM.values', result_array.GM.values)

        # By profiling, it's faster to recompute all driving forces in-place
        # versus doing fancy indexing to only update "changed" driving forces
        # This avoids the creation of expensive temporaries
        np.einsum('...i,...i',
                  result_array.MU.values[..., np.newaxis, :],
                  global_grid.X.values[np.index_exp[...] + ((np.newaxis,) * len(comp_conds)) + np.index_exp[:, :]],
                  out=driving_forces)
        np.subtract(driving_forces,
                    global_grid.GM.values[np.index_exp[...] + ((np.newaxis,) * len(comp_conds)) + np.index_exp[:]],
                    out=driving_forces)


        # Update trial points to choose points with largest remaining driving force
        trial_points = np.argmax(driving_forces, axis=-1)
        #print('trial_points', trial_points)
        logger.debug('trial_points: %s', trial_points)

        # If all driving force (within some tolerance) is consumed, we found equilibrium
        if np.all(driving_forces <= DRIVING_FORCE_TOLERANCE):
            return
    #raise ValueError
    print('Iterations exceeded. Remaining driving force: ', driving_forces.max())
    logger.error('Iterations exceeded')
示例#14
0
def energy_surf(dbf, comps, phases, mode=None, output='GM', **kwargs):
    """
    Sample the property surface of 'output' containing the specified
    components and phases. Model parameters are taken from 'dbf' and any
    state variables (T, P, etc.) can be specified as keyword arguments.

    Parameters
    ----------
    dbf : Database
        Thermodynamic database containing the relevant parameters.
    comps : list
        Names of components to consider in the calculation.
    phases : list
        Names of phases to consider in the calculation.
    mode : string, optional
        See 'make_callable' docstring for details.
    output : string, optional
        Model attribute to sample.
    pdens : int, a dict of phase names to int, or a list of both, optional
        Number of points to sample per degree of freedom.
    model : Model, a dict of phase names to Model, or a list of both, optional
        Model class to use for each phase.

    Returns
    -------
    DataFrame of the output as a function of composition, temperature, etc.

    Examples
    --------
    None yet.
    """
    warnings.warn('Use pycalphad.calculate() instead', DeprecationWarning, stacklevel=2)
    # Here we check for any keyword arguments that are special, i.e.,
    # there may be keyword arguments that aren't state variables
    pdens_dict = unpack_kwarg(kwargs.pop('pdens', 2000), default_arg=2000)
    model_dict = unpack_kwarg(kwargs.pop('model', Model), default_arg=Model)
    callable_dict = unpack_kwarg(kwargs.pop('callables', None), default_arg=None)

    # Convert keyword strings to proper state variable objects
    # If we don't do this, sympy will get confused during substitution
    statevar_dict = \
        collections.OrderedDict((v.StateVariable(key), value) \
                                for (key, value) in sorted(kwargs.items()))

    # Generate all combinations of state variables for 'map' calculation
    # Wrap single values of state variables in lists
    # Use 'kwargs' because we want state variable names to be stringified
    statevar_values = [_listify(val) for val in statevar_dict.values()]
    statevars_to_map = np.array(list(itertools.product(*statevar_values)))

    # Consider only the active phases
    active_phases = dict((name.upper(), dbf.phases[name.upper()]) \
        for name in phases)
    comp_sets = {}
    # Construct a list to hold all the data
    all_phase_data = []
    for phase_name, phase_obj in sorted(active_phases.items()):
        # Build the symbolic representation of the energy
        mod = model_dict[phase_name]
        # if this is an object type, we need to construct it
        if isinstance(mod, type):
            try:
                mod = mod(dbf, comps, phase_name)
            except DofError:
                # we can't build the specified phase because the
                # specified components aren't found in every sublattice
                # we'll just skip it
                logger.warning("""Suspending specified phase %s due to
                some sublattices containing only unspecified components""",
                               phase_name)
                continue
        try:
            out = getattr(mod, output)
        except AttributeError:
            raise AttributeError('Missing Model attribute {0} specified for {1}'
                                 .format(output, mod.__class__))

        # Construct an ordered list of the variables
        variables, sublattice_dof = generate_dof(phase_obj, mod.components)

        site_ratios = list(phase_obj.sublattices)
        # Build the "fast" representation of that model
        if callable_dict[phase_name] is None:
            # As a last resort, treat undefined symbols as zero
            # But warn the user when we do this
            # This is consistent with TC's behavior
            undefs = list(out.atoms(Symbol) - out.atoms(v.StateVariable))
            for undef in undefs:
                out = out.xreplace({undef: float(0)})
                logger.warning('Setting undefined symbol %s for phase %s to zero',
                               undef, phase_name)
            comp_sets[phase_name] = make_callable(out, \
                list(statevar_dict.keys()) + variables, mode=mode)
        else:
            comp_sets[phase_name] = callable_dict[phase_name]

        # Eliminate pure vacancy endmembers from the calculation
        vacancy_indices = list()
        for idx, sublattice in enumerate(phase_obj.constituents):
            if 'VA' in sorted(sublattice) and 'VA' in sorted(comps):
                vacancy_indices.append(sorted(sublattice).index('VA'))
        if len(vacancy_indices) != len(phase_obj.constituents):
            vacancy_indices = None
        logger.debug('vacancy_indices: %s', vacancy_indices)
        # Add all endmembers to guarantee their presence
        points = endmember_matrix(sublattice_dof,
                                  vacancy_indices=vacancy_indices)

        # Sample composition space for more points
        if sum(sublattice_dof) > len(sublattice_dof):
            points = np.concatenate((points,
                                     point_sample(sublattice_dof,
                                                  pdof=pdens_dict[phase_name])
                                    ))


        # If there are nontrivial sublattices with vacancies in them,
        # generate a set of points where their fraction is zero and renormalize
        for idx, sublattice in enumerate(phase_obj.constituents):
            if 'VA' in set(sublattice) and len(sublattice) > 1:
                var_idx = variables.index(v.SiteFraction(phase_name, idx, 'VA'))
                addtl_pts = np.copy(points)
                # set vacancy fraction to log-spaced between 1e-10 and 1e-6
                addtl_pts[:, var_idx] = np.power(10.0, -10.0*(1.0 - addtl_pts[:, var_idx]))
                # renormalize site fractions
                cur_idx = 0
                for ctx in sublattice_dof:
                    end_idx = cur_idx + ctx
                    addtl_pts[:, cur_idx:end_idx] /= \
                        addtl_pts[:, cur_idx:end_idx].sum(axis=1)[:, None]
                    cur_idx = end_idx
                # add to points matrix
                points = np.concatenate((points, addtl_pts), axis=0)

        data_dict = {'Phase': phase_name}
        # Broadcast compositions and state variables along orthogonal axes
        # This lets us eliminate an expensive Python loop
        data_dict[output] = \
            comp_sets[phase_name](*itertools.chain(
                np.transpose(statevars_to_map[:, :, np.newaxis], (1, 2, 0)),
                np.transpose(points[:, :, np.newaxis], (1, 0, 2)))).T.ravel()
        # Save state variables, with values indexed appropriately
        statevar_vals = np.repeat(statevars_to_map, len(points), axis=0).T
        data_dict.update({str(statevar): vals for statevar, vals \
            in zip(statevar_dict.keys(), statevar_vals)})

        # Map the internal degrees of freedom to global coordinates

        # Normalize site ratios by the sum of site ratios times a factor
        # related to the site fraction of vacancies
        site_ratio_normalization = np.zeros(len(points))
        for idx, sublattice in enumerate(phase_obj.constituents):
            vacancy_column = np.ones(len(points))
            if 'VA' in set(sublattice):
                var_idx = variables.index(v.SiteFraction(phase_name, idx, 'VA'))
                vacancy_column -= points[:, var_idx]
            site_ratio_normalization += site_ratios[idx] * vacancy_column

        for comp in sorted(comps):
            if comp == 'VA':
                continue
            avector = [float(vxx.species == comp) * \
                site_ratios[vxx.sublattice_index] for vxx in variables]
            data_dict['X('+comp+')'] = np.tile(np.divide(np.dot(
                points[:, :], avector), site_ratio_normalization),
                                               statevars_to_map.shape[0])

        # Copy coordinate information into data_dict
        # TODO: Is there a more memory-efficient way to deal with this?
        # Perhaps with hierarchical indexing...
        var_fmt = 'Y({0},{1},{2})'
        data_dict.update({var_fmt.format(vxx.phase_name, vxx.sublattice_index,
                                         vxx.species): \
            np.tile(vals, statevars_to_map.shape[0]) \
            for vxx, vals in zip(variables, points.T)})
        all_phase_data.append(pd.DataFrame(data_dict))

    # all_phases_data now contains energy surface information for the system
    return pd.concat(all_phase_data, axis=0, join='outer', \
                            ignore_index=True, verify_integrity=False)
示例#15
0
def lower_convex_hull(data, comps, conditions):
    """
    Find the simplex on the lower convex hull satisfying the specified
    conditions.

    Parameters
    ----------
    data : DataFrame
        A sample of the energy surface of the system.
    comps : list
        All the components in the system.
    conditions : dict
        StateVariables and their corresponding value.

    Returns
    -------
    A tuple containing:
    (1) A numpy array of indices corresponding to vertices of the simplex.
    (2) A numpy array corresponding to the phase fractions.
    (3) A numpy array of chemical potentials in sorted(comps) order (no 'VA')
    Note: This routine will not check if the simplex is degenerate.

    Examples
    --------
    None yet.
    """
    # determine column indices for degrees of freedom
    comps = sorted(list(comps))
    dof = ['X({0})'.format(c) for c in comps if c != 'VA']
    dof_values = np.zeros(len(dof))
    marked_dof_values = list(range(len(dof)))
    for cond, value in conditions.items():
        if not isinstance(cond, v.Composition):
            continue
        # ignore phase-specific composition conditions
        if cond.phase_name is not None:
            continue
        if cond.species == 'VA':
            continue
        dof_values[dof.index('X({0})'.format(cond.species))] = value
        marked_dof_values.remove(dof.index('X({0})'.format(cond.species)))

    dof.append('GM')

    if len(marked_dof_values) == 1:
        dof_values[marked_dof_values[0]] = 1-sum(dof_values)
    else:
        logger.error('Not enough composition conditions specified')
        raise ValueError('Not enough composition conditions specified.')

    # convert DataFrame of independent columns to ndarray
    dat = data[dof].values
    temperature = data.at[0, 'T']

    # Build a fictitious hyperplane which has an energy greater than the max
    # energy in the system
    # This guarantees our starting point is feasible but also makes it likely
    # it won't be part of the solution
    energy_ceiling = np.amax(dat[:, -1])
    if np.isnan(energy_ceiling):
        raise ValueError('Input energy surface contains one or more NaNs.')
    if energy_ceiling < 0:
        energy_ceiling *= 0.1
    else:
        energy_ceiling *= 10
    start_matrix = np.empty([len(dof)-1, len(dof)])
    start_matrix[:, :-1] = np.eye(len(dof)-1)
    start_matrix[:, -1] = energy_ceiling # set energy
    dat = np.concatenate([start_matrix, dat])

    max_iterations = min(100, dat.shape[0])
    # Need to choose a feasible starting point
    # initialize simplex as first n points of fictitious hyperplane
    candidate_simplex = np.array(range(len(dof)-1), dtype=np.int)
    # Calculate chemical potentials
    candidate_potentials = np.linalg.solve(dat[candidate_simplex, :-1],
                                           dat[candidate_simplex, -1])

    # Calculate driving forces for reducing our candidate potentials
    driving_forces = np.dot(dat[:, :-1], candidate_potentials) - dat[:, -1]
    # Mask points with negative (or nearly zero) driving force
    point_mask = driving_forces/(8.3145*temperature) < 1e-4
    #logger.debug(point_mask)
    #logger.debug(np.array(range(dat.shape[0]), dtype=np.int)[~point_mask])
    candidate_energy = np.dot(candidate_potentials, dof_values)
    fractions = np.empty(len(dof_values))
    iteration = 0
    found_solution = False
    index_array = np.array(range(dat.shape[0]), dtype=np.int)

    while (found_solution == False) and (iteration < max_iterations):
        iteration += 1
        for new_point in index_array[~point_mask]:
            found_point = False
            # Need to successively replace columns with the new point
            # The goal is to find positive phase fraction values
            new_simplex = np.empty(dat.shape[1] - 1, dtype=np.int)
            for col in range(dat.shape[1] - 1):
                #print(candidate_simplex)
                new_simplex[:] = candidate_simplex # [:] forces copy
                new_simplex[col] = new_point
                #print(new_simplex)
                logger.debug('trial matrix: %s', dat[new_simplex, :-1].T)
                try:
                    fractions = np.linalg.solve(dat[new_simplex, :-1].T,
                                                dof_values)
                except np.linalg.LinAlgError:
                    # singular matrix means the trial simplex is degenerate
                    # this usually happens due to collisions between points on
                    # the fictitious hyperplane and the endmembers
                    continue
                logger.debug('fractions: %s', fractions)
                if np.all(fractions > -1e-8):
                    # Positive phase fractions
                    # Do I reduce the energy with this solution?
                    # Recalculate chemical potentials and energy
                    #logger.debug('new matrix: {0}'.format(dat[new_simplex, :-1]))
                    #logger.debug('new energies: {0}'.format(dat[new_simplex, -1]))
                    new_potentials = np.linalg.solve(dat[new_simplex, :-1],
                                                     dat[new_simplex, -1])
                    #logger.debug('new_potentials: {0}'.format(new_potentials))
                    new_energy = np.dot(new_potentials, dof_values)
                    # differences of less than 1mJ/mol are irrelevant
                    new_energy = np.around(new_energy, decimals=3)
                    if new_energy <= candidate_energy:
                        #logger.debug('New simplex {2} reduces energy from \
                        #    {0} to {1}'.format(candidate_energy, new_energy, \
                        #    new_simplex))
                        # [:] notation forces a copy
                        candidate_simplex[:] = new_simplex
                        candidate_potentials[:] = new_potentials
                        # np.array() forces a copy
                        candidate_energy = np.array(new_energy)
                        # Recalculate driving forces with new potentials
                        driving_forces[:] = np.dot(dat[:, :-1], \
                            candidate_potentials) - dat[:, -1]
                        #logger.debug('driving_forces: %s', driving_forces)
                        point_mask = driving_forces/(8.3145*temperature) < 1e-4
                        # Don't test points on the fictitious hyperplane
                        point_mask[list(range(len(dof)-1))] = True
                        found_point = True
                        break
                    #else:
                    #    logger.debug('Trial simplex {2} increases energy from {0} to {1}'\
                    #                .format(candidate_energy, new_energy, new_simplex))
                    #    logger.debug('%s points with positive driving force remain',
                    #                 list(driving_forces >= 1e-4).count(True))
            if found_point:
                logger.debug('Found feasible simplex: moving to next iteration')
                #logger.debug('%s points with positive driving force remain',
                #             list(driving_forces >= 1e-4).count(True))
                break
        # If there is no positive driving force, we have the solution
        #print('Checking point mask')
        #print(point_mask)
        logger.debug('Iteration count: {0}'.format(iteration))
        if np.all(point_mask) == True:
            logger.debug('Unadjusted candidate_simplex: %s', candidate_simplex)
            logger.debug(dat[candidate_simplex])
            # Fix candidate simplex indices to remove fictitious points
            candidate_simplex = candidate_simplex - (len(dof)-1)
            logger.debug('Adjusted candidate_simplex: %s', candidate_simplex)
            # Remove fictitious points from the candidate simplex
            # These can inadvertently show up if we only calculate a phase with
            # limited solubility
            # Also remove points with very small estimated phase fractions
            candidate_simplex, fractions = zip(*[(c, f) for c, f in
                                                 zip(candidate_simplex,
                                                     fractions)
                                                 if c >= 0 and f >= 1e-12])
            candidate_simplex = np.array(candidate_simplex)
            fractions = np.array(fractions)
            fractions /= np.sum(fractions)
            logger.debug('Final candidate_simplex: %s', candidate_simplex)
            logger.debug('Final phase fractions: %s', fractions)
            found_solution = True
            logger.debug('Solution:')
            logger.debug(candidate_potentials)
            logger.debug(candidate_energy)
            return candidate_simplex, fractions, candidate_potentials

    logger.error('Iterations exceeded')
    logger.debug('Positive driving force still exists for these points')
    logger.debug(np.where(driving_forces/(8.3145*temperature) > 1e-4)[0])
    return None, None, None
示例#16
0
def calculate(dbf,
              comps,
              phases,
              mode=None,
              output='GM',
              fake_points=False,
              **kwargs):
    """
    Sample the property surface of 'output' containing the specified
    components and phases. Model parameters are taken from 'dbf' and any
    state variables (T, P, etc.) can be specified as keyword arguments.

    Parameters
    ----------
    dbf : Database
        Thermodynamic database containing the relevant parameters.
    comps : str or sequence
        Names of components to consider in the calculation.
    phases : str or sequence
        Names of phases to consider in the calculation.
    mode : string, optional
        See 'make_callable' docstring for details.
    output : string, optional
        Model attribute to sample.
    fake_points : bool, optional (Default: False)
        If True, the first few points of the output surface will be fictitious
        points used to define an equilibrium hyperplane guaranteed to be above
        all the other points. This is used for convex hull computations.
    points : ndarray or a dict of phase names to ndarray, optional
        Columns of ndarrays must be internal degrees of freedom (site fractions), sorted.
        If this is not specified, points will be generated automatically.
    pdens : int, a dict of phase names to int, or a seq of both, optional
        Number of points to sample per degree of freedom.
    model : Model, a dict of phase names to Model, or a seq of both, optional
        Model class to use for each phase.

    Returns
    -------
    xray.Dataset of the sampled attribute as a function of state variables

    Examples
    --------
    None yet.
    """
    # Here we check for any keyword arguments that are special, i.e.,
    # there may be keyword arguments that aren't state variables
    pdens_dict = unpack_kwarg(kwargs.pop('pdens', 2000), default_arg=2000)
    points_dict = unpack_kwarg(kwargs.pop('points', None), default_arg=None)
    model_dict = unpack_kwarg(kwargs.pop('model', Model), default_arg=Model)
    callable_dict = unpack_kwarg(kwargs.pop('callables', None),
                                 default_arg=None)
    if isinstance(phases, str):
        phases = [phases]
    if isinstance(comps, str):
        comps = [comps]
    components = [x for x in sorted(comps) if not x.startswith('VA')]

    # Convert keyword strings to proper state variable objects
    # If we don't do this, sympy will get confused during substitution
    statevar_dict = collections.OrderedDict((v.StateVariable(key), unpack_condition(value)) \
                                            for (key, value) in sorted(kwargs.items()))
    str_statevar_dict = collections.OrderedDict((str(key), unpack_condition(value)) \
                                                for (key, value) in statevar_dict.items())
    all_phase_data = []
    comp_sets = {}
    largest_energy = -np.inf
    maximum_internal_dof = 0

    # Consider only the active phases
    active_phases = dict((name.upper(), dbf.phases[name.upper()]) \
        for name in unpack_phases(phases))

    for phase_name, phase_obj in sorted(active_phases.items()):
        # Build the symbolic representation of the energy
        mod = model_dict[phase_name]
        # if this is an object type, we need to construct it
        if isinstance(mod, type):
            try:
                model_dict[phase_name] = mod = mod(dbf, comps, phase_name)
            except DofError:
                # we can't build the specified phase because the
                # specified components aren't found in every sublattice
                # we'll just skip it
                logger.warning(
                    """Suspending specified phase %s due to
                some sublattices containing only unspecified components""",
                    phase_name)
                continue
        if points_dict[phase_name] is None:
            try:
                out = getattr(mod, output)
                maximum_internal_dof = max(maximum_internal_dof,
                                           len(out.atoms(v.SiteFraction)))
            except AttributeError:
                raise AttributeError(
                    'Missing Model attribute {0} specified for {1}'.format(
                        output, mod.__class__))
        else:
            maximum_internal_dof = max(
                maximum_internal_dof,
                np.asarray(points_dict[phase_name]).shape[-1])

    for phase_name, phase_obj in sorted(active_phases.items()):
        try:
            mod = model_dict[phase_name]
        except KeyError:
            continue
        # Construct an ordered list of the variables
        variables, sublattice_dof = generate_dof(phase_obj, mod.components)

        # Build the "fast" representation of that model
        if callable_dict[phase_name] is None:
            out = getattr(mod, output)
            # As a last resort, treat undefined symbols as zero
            # But warn the user when we do this
            # This is consistent with TC's behavior
            undefs = list(out.atoms(Symbol) - out.atoms(v.StateVariable))
            for undef in undefs:
                out = out.xreplace({undef: float(0)})
                logger.warning(
                    'Setting undefined symbol %s for phase %s to zero', undef,
                    phase_name)
            comp_sets[phase_name] = make_callable(out, \
                list(statevar_dict.keys()) + variables, mode=mode)
        else:
            comp_sets[phase_name] = callable_dict[phase_name]

        points = points_dict[phase_name]
        if points is None:
            # Eliminate pure vacancy endmembers from the calculation
            vacancy_indices = list()
            for idx, sublattice in enumerate(phase_obj.constituents):
                active_in_subl = sorted(
                    set(phase_obj.constituents[idx]).intersection(comps))
                if 'VA' in active_in_subl and 'VA' in sorted(comps):
                    vacancy_indices.append(active_in_subl.index('VA'))
            if len(vacancy_indices) != len(phase_obj.constituents):
                vacancy_indices = None
            logger.debug('vacancy_indices: %s', vacancy_indices)
            # Add all endmembers to guarantee their presence
            points = endmember_matrix(sublattice_dof,
                                      vacancy_indices=vacancy_indices)

            # Sample composition space for more points
            if sum(sublattice_dof) > len(sublattice_dof):
                points = np.concatenate(
                    (points,
                     point_sample(sublattice_dof,
                                  pdof=pdens_dict[phase_name])))

            # If there are nontrivial sublattices with vacancies in them,
            # generate a set of points where their fraction is zero and renormalize
            for idx, sublattice in enumerate(phase_obj.constituents):
                if 'VA' in set(sublattice) and len(sublattice) > 1:
                    var_idx = variables.index(
                        v.SiteFraction(phase_name, idx, 'VA'))
                    addtl_pts = np.copy(points)
                    # set vacancy fraction to log-spaced between 1e-10 and 1e-6
                    addtl_pts[:, var_idx] = np.power(
                        10.0, -10.0 * (1.0 - addtl_pts[:, var_idx]))
                    # renormalize site fractions
                    cur_idx = 0
                    for ctx in sublattice_dof:
                        end_idx = cur_idx + ctx
                        addtl_pts[:, cur_idx:end_idx] /= \
                            addtl_pts[:, cur_idx:end_idx].sum(axis=1)[:, None]
                        cur_idx = end_idx
                    # add to points matrix
                    points = np.concatenate((points, addtl_pts), axis=0)
            # Filter out nan's that may have slipped in if we sampled too high a vacancy concentration
            # Issues with this appear to be platform-dependent
            points = points[~np.isnan(points).any(axis=-1)]
        # Ensure that points has the correct dimensions and dtype
        points = np.atleast_2d(np.asarray(points, dtype=np.float))

        phase_ds = _compute_phase_values(phase_obj, components, variables,
                                         str_statevar_dict, points,
                                         comp_sets[phase_name], output,
                                         maximum_internal_dof)
        # largest_energy is really only relevant if fake_points is set
        if fake_points:
            largest_energy = max(phase_ds[output].max(), largest_energy)
        all_phase_data.append(phase_ds)

    if fake_points:
        if output != 'GM':
            raise ValueError(
                'fake_points=True should only be used with output=\'GM\'')
        phase_ds = _generate_fake_points(components, statevar_dict,
                                         largest_energy, output,
                                         maximum_internal_dof)
        final_ds = xray.concat(itertools.chain([phase_ds], all_phase_data),
                               dim='points')
    else:
        # speedup for single-phase case (found by profiling)
        if len(all_phase_data) > 1:
            final_ds = xray.concat(all_phase_data, dim='points')
        else:
            final_ds = all_phase_data[0]

    if (not fake_points) and (len(all_phase_data) == 1):
        pass
    else:
        # Reset the points dimension to use a single global index
        final_ds['points'] = np.arange(len(final_ds.points))
    return final_ds
示例#17
0
def calculate(dbf, comps, phases, mode=None, output='GM', fake_points=False, broadcast=True, tmpman=None, **kwargs):
    """
    Sample the property surface of 'output' containing the specified
    components and phases. Model parameters are taken from 'dbf' and any
    state variables (T, P, etc.) can be specified as keyword arguments.

    Parameters
    ----------
    dbf : Database
        Thermodynamic database containing the relevant parameters.
    comps : str or sequence
        Names of components to consider in the calculation.
    phases : str or sequence
        Names of phases to consider in the calculation.
    mode : string, optional
        See 'make_callable' docstring for details.
    output : string, optional
        Model attribute to sample.
    fake_points : bool, optional (Default: False)
        If True, the first few points of the output surface will be fictitious
        points used to define an equilibrium hyperplane guaranteed to be above
        all the other points. This is used for convex hull computations.
    broadcast : bool, optional
        If True, broadcast given state variable lists against each other to create a grid.
        If False, assume state variables are given as equal-length lists.
    tmpman : TempfileManager, optional
        Context manager for temporary file creation during the calculation.
    points : ndarray or a dict of phase names to ndarray, optional
        Columns of ndarrays must be internal degrees of freedom (site fractions), sorted.
        If this is not specified, points will be generated automatically.
    pdens : int, a dict of phase names to int, or a seq of both, optional
        Number of points to sample per degree of freedom.
    model : Model, a dict of phase names to Model, or a seq of both, optional
        Model class to use for each phase.
    sampler : callable, a dict of phase names to callable, or a seq of both, optional
        Function to sample phase constitution space.
        Must have same signature as 'pycalphad.core.utils.point_sample'
    grid_points : bool, a dict of phase names to bool, or a seq of both, optional (Default: True)
        Whether to add evenly spaced points between end-members.
        The density of points is determined by 'pdens'

    Returns
    -------
    Dataset of the sampled attribute as a function of state variables

    Examples
    --------
    None yet.
    """
    # Here we check for any keyword arguments that are special, i.e.,
    # there may be keyword arguments that aren't state variables
    pdens_dict = unpack_kwarg(kwargs.pop('pdens', 2000), default_arg=2000)
    points_dict = unpack_kwarg(kwargs.pop('points', None), default_arg=None)
    model_dict = unpack_kwarg(kwargs.pop('model', Model), default_arg=Model)
    callable_dict = unpack_kwarg(kwargs.pop('callables', None), default_arg=None)
    sampler_dict = unpack_kwarg(kwargs.pop('sampler', None), default_arg=None)
    fixedgrid_dict = unpack_kwarg(kwargs.pop('grid_points', True), default_arg=True)
    if isinstance(phases, str):
        phases = [phases]
    if isinstance(comps, str):
        comps = [comps]
    if points_dict is None and broadcast is False:
        raise ValueError('The \'points\' keyword argument must be specified if broadcast=False is also given.')
    components = [x for x in sorted(comps) if not x.startswith('VA')]

    # Convert keyword strings to proper state variable objects
    # If we don't do this, sympy will get confused during substitution
    statevar_dict = collections.OrderedDict((v.StateVariable(key), unpack_condition(value)) \
                                            for (key, value) in sorted(kwargs.items()))
    str_statevar_dict = collections.OrderedDict((str(key), unpack_condition(value)) \
                                                for (key, value) in statevar_dict.items())
    all_phase_data = []
    comp_sets = {}
    largest_energy = -np.inf
    maximum_internal_dof = 0

    # Consider only the active phases
    active_phases = dict((name.upper(), dbf.phases[name.upper()]) \
        for name in unpack_phases(phases))

    for phase_name, phase_obj in sorted(active_phases.items()):
        # Build the symbolic representation of the energy
        mod = model_dict[phase_name]
        # if this is an object type, we need to construct it
        if isinstance(mod, type):
            try:
                model_dict[phase_name] = mod = mod(dbf, comps, phase_name)
            except DofError:
                # we can't build the specified phase because the
                # specified components aren't found in every sublattice
                # we'll just skip it
                logger.warning("""Suspending specified phase %s due to
                some sublattices containing only unspecified components""",
                               phase_name)
                continue
        if points_dict[phase_name] is None:
            try:
                out = getattr(mod, output)
                maximum_internal_dof = max(maximum_internal_dof, len(out.atoms(v.SiteFraction)))
            except AttributeError:
                raise AttributeError('Missing Model attribute {0} specified for {1}'
                                     .format(output, mod.__class__))
        else:
            maximum_internal_dof = max(maximum_internal_dof, np.asarray(points_dict[phase_name]).shape[-1])

    for phase_name, phase_obj in sorted(active_phases.items()):
        try:
            mod = model_dict[phase_name]
        except KeyError:
            continue
        # Construct an ordered list of the variables
        variables, sublattice_dof = generate_dof(phase_obj, mod.components)

        # Build the "fast" representation of that model
        if callable_dict[phase_name] is None:
            out = getattr(mod, output)
            # As a last resort, treat undefined symbols as zero
            # But warn the user when we do this
            # This is consistent with TC's behavior
            undefs = list(out.atoms(Symbol) - out.atoms(v.StateVariable))
            for undef in undefs:
                out = out.xreplace({undef: float(0)})
                logger.warning('Setting undefined symbol %s for phase %s to zero',
                               undef, phase_name)
            comp_sets[phase_name] = build_functions(out, list(statevar_dict.keys()) + variables, tmpman=tmpman,
                                                    include_obj=True, include_grad=False, include_hess=False)
        else:
            comp_sets[phase_name] = callable_dict[phase_name]

        points = points_dict[phase_name]
        if points is None:
            # Eliminate pure vacancy endmembers from the calculation
            vacancy_indices = list()
            for idx, sublattice in enumerate(phase_obj.constituents):
                active_in_subl = sorted(set(phase_obj.constituents[idx]).intersection(comps))
                if 'VA' in active_in_subl and 'VA' in sorted(comps):
                    vacancy_indices.append(active_in_subl.index('VA'))
            if len(vacancy_indices) != len(phase_obj.constituents):
                vacancy_indices = None
            logger.debug('vacancy_indices: %s', vacancy_indices)
            # Add all endmembers to guarantee their presence
            points = endmember_matrix(sublattice_dof,
                                      vacancy_indices=vacancy_indices)
            if fixedgrid_dict[phase_name] is True:
                # Sample along the edges of the endmembers
                # These constitution space edges are often the equilibrium points!
                em_pairs = list(itertools.combinations(points, 2))
                for first_em, second_em in em_pairs:
                    extra_points = first_em * np.linspace(0, 1, pdens_dict[phase_name])[np.newaxis].T + \
                                   second_em * np.linspace(0, 1, pdens_dict[phase_name])[::-1][np.newaxis].T
                    points = np.concatenate((points, extra_points))


            # Sample composition space for more points
            if sum(sublattice_dof) > len(sublattice_dof):
                sampler = sampler_dict[phase_name]
                if sampler is None:
                    sampler = point_sample
                points = np.concatenate((points,
                                         sampler(sublattice_dof,
                                                 pdof=pdens_dict[phase_name])
                                         ))

            # If there are nontrivial sublattices with vacancies in them,
            # generate a set of points where their fraction is zero and renormalize
            for idx, sublattice in enumerate(phase_obj.constituents):
                if 'VA' in set(sublattice) and len(sublattice) > 1:
                    var_idx = variables.index(v.SiteFraction(phase_name, idx, 'VA'))
                    addtl_pts = np.copy(points)
                    # set vacancy fraction to log-spaced between 1e-10 and 1e-6
                    addtl_pts[:, var_idx] = np.power(10.0, -10.0*(1.0 - addtl_pts[:, var_idx]))
                    # renormalize site fractions
                    cur_idx = 0
                    for ctx in sublattice_dof:
                        end_idx = cur_idx + ctx
                        addtl_pts[:, cur_idx:end_idx] /= \
                            addtl_pts[:, cur_idx:end_idx].sum(axis=1)[:, None]
                        cur_idx = end_idx
                    # add to points matrix
                    points = np.concatenate((points, addtl_pts), axis=0)
            # Filter out nan's that may have slipped in if we sampled too high a vacancy concentration
            # Issues with this appear to be platform-dependent
            points = points[~np.isnan(points).any(axis=-1)]
        # Ensure that points has the correct dimensions and dtype
        points = np.atleast_2d(np.asarray(points, dtype=np.float))

        phase_ds = _compute_phase_values(phase_obj, components, variables, str_statevar_dict,
                                         points, comp_sets[phase_name], output,
                                         maximum_internal_dof, broadcast=broadcast)
        # largest_energy is really only relevant if fake_points is set
        if fake_points:
            largest_energy = max(phase_ds[output].max(), largest_energy)
        all_phase_data.append(phase_ds)

    if fake_points:
        if output != 'GM':
            raise ValueError('fake_points=True should only be used with output=\'GM\'')
        phase_ds = _generate_fake_points(components, statevar_dict, largest_energy, output,
                                         maximum_internal_dof, broadcast)
        final_ds = concat(itertools.chain([phase_ds], all_phase_data),
                          dim='points')
    else:
        # speedup for single-phase case (found by profiling)
        if len(all_phase_data) > 1:
            final_ds = concat(all_phase_data, dim='points')
        else:
            final_ds = all_phase_data[0]

    if (not fake_points) and (len(all_phase_data) == 1):
        pass
    else:
        # Reset the points dimension to use a single global index
        final_ds['points'] = np.arange(len(final_ds.points))
    return final_ds