コード例 #1
0
ファイル: fitting.py プロジェクト: manirm/pycalphad
def _map_internal_dof(input_database, components, phase_name, points):
    """
    Map matrix of internal degrees of freedom to global compositions.
    """
    # 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
    phase_obj = input_database.phases[phase_name]
    site_ratio_normalization = np.zeros(points.shape[:-1])
    phase_compositions = np.empty(points.shape[:-1] + (len(components), ))
    variables, sublattice_dof = generate_dof(phase_obj, components)
    for idx, sublattice in enumerate(phase_obj.constituents):
        vacancy_column = np.ones(points.shape[:-1])
        if 'VA' in set(sublattice):
            var_idx = variables.index(v.SiteFraction(phase_obj.name, idx,
                                                     'VA'))
            vacancy_column -= points[..., :, var_idx]
        site_ratio_normalization += phase_obj.sublattices[idx] * vacancy_column

    for col, comp in enumerate(components):
        avector = [
            float(vxx.species == comp) *
            phase_obj.sublattices[vxx.sublattice_index] for vxx in variables
        ]
        phase_compositions[..., :,
                           col] = np.divide(np.dot(points[..., :, :], avector),
                                            site_ratio_normalization)
    return phase_compositions
コード例 #2
0
ファイル: utils.py プロジェクト: subangstrom/scheil
def order_disorder_dict(dbf, comps, phases):
    """Return a dictionary with the sublattice degrees of freedom and equivalent
    sublattices for order/disorder phases

    Parameters
    ----------
    dbf : pycalphad.Database
    comps : list[str]
        List of active components to consider
    phases : list[str]
        List of active phases to consider

    Returns
    -------
    dict

    Notes
    -----
    Phases which should be checked for ordered/disordered configurations are
    determined heuristically for this script.

    The heuristic for a phase satisfies the following:
    1. The phase is the ordered part of an order-disorder model
    2. The equivalent sublattices have all the same number of elements
    """
    species = unpack_components(dbf, comps)
    ord_disord_phases = {}
    for phase_name in phases:
        phase_obj = dbf.phases[phase_name]
        if phase_name == phase_obj.model_hints.get('ordered_phase', ''):
            # This phase is active and modeled with an order/disorder model.
            dof = generate_dof(dbf.phases[phase_name], species)[1]
            # Define the symmetrically equivalent sublattices as any sublattices
            # that have the same site ratio. Create a {site_ratio: [subl idx]} dict
            site_ratio_idxs = defaultdict(lambda: [])
            for subl_idx, site_ratio in enumerate(phase_obj.sublattices):
                site_ratio_idxs[site_ratio].append(subl_idx)
            equiv_sublattices = list(site_ratio_idxs.values())
            ord_disord_phases[phase_name] = {
                'subl_dof': dof,
                'symmetric_subl_idx': equiv_sublattices,
                'disordered_phase': phase_obj.model_hints['disordered_phase']
            }
    return ord_disord_phases
コード例 #3
0
def sample_phase_points(dbf, comps, phase_name, conditions, calc_pdens, pdens):
    """Sample new points from a phase around the single phase equilibrium site fractions at the given conditions.

    Parameters
    ----------
    dbf :

    comps :

    phase_name :

    conditions :

    calc_pdens :
        The point density passed to calculate for the nominal points added.
    pdens : int
        The number of points to add in the local sampling at each set of equilibrium site fractions.

    Returns
    -------
    np.ndarray[:,:]

    """
    _, subl_dof = generate_dof(dbf.phases[phase_name],
                               unpack_components(dbf, comps))
    # subl_dof is number of species in each sublattice, e.g. (FE,NI,TI)(FE,NI)(FE,NI,TI) is [3, 2, 3]
    eqgrid = equilibrium(dbf, comps, [phase_name], conditions)
    all_eq_pts = eqgrid.Y.values[eqgrid.Phase.values == phase_name]
    # sample points locally
    additional_points = local_sample(all_eq_pts, subl_dof, pdens)
    # get the grid between endmembers and random point sampling from calculate
    pts_calc = calculate(dbf,
                         comps,
                         phase_name,
                         pdens=calc_pdens,
                         P=101325,
                         T=300,
                         N=1).Y.values.squeeze()
    return np.concatenate([additional_points, pts_calc], axis=0)
コード例 #4
0
ファイル: fitting.py プロジェクト: hastelloy/pycalphad
def _map_internal_dof(input_database, components, phase_name, points):
    """
    Map matrix of internal degrees of freedom to global compositions.
    """
    # 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
    phase_obj = input_database.phases[phase_name]
    site_ratio_normalization = np.zeros(points.shape[:-1])
    phase_compositions = np.empty(points.shape[:-1] + (len(components),))
    variables, sublattice_dof = generate_dof(phase_obj, components)
    for idx, sublattice in enumerate(phase_obj.constituents):
        vacancy_column = np.ones(points.shape[:-1])
        if 'VA' in set(sublattice):
            var_idx = variables.index(v.SiteFraction(phase_obj.name, idx, 'VA'))
            vacancy_column -= points[..., :, var_idx]
        site_ratio_normalization += phase_obj.sublattices[idx] * vacancy_column

    for col, comp in enumerate(components):
        avector = [float(vxx.species == comp) *
                   phase_obj.sublattices[vxx.sublattice_index] for vxx in variables]
        phase_compositions[..., :, col] = np.divide(np.dot(points[..., :, :], avector),
                                                    site_ratio_normalization)
    return phase_compositions
コード例 #5
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
コード例 #6
0
ファイル: calculate.py プロジェクト: zxj19910214/pycalphad
def calculate(dbf, comps, phases, mode=None, output='GM', fake_points=False, broadcast=True, parameters=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.
    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.
        Default: 2000; Default when called from equilibrium(): 500
    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'
    parameters : dict, optional
        Maps SymPy Symbol to numbers, for overriding the values of parameters in the Database.

    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)
    mass_dict = unpack_kwarg(kwargs.pop('massfuncs', 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)
    parameters = parameters or dict()
    if isinstance(parameters, dict):
        parameters = OrderedDict(sorted(parameters.items(), key=str))
    param_symbols = tuple(parameters.keys())
    param_values = np.atleast_1d(np.array(list(parameters.values()), dtype=np.float))
    if isinstance(phases, str):
        phases = [phases]
    if isinstance(comps, (str, v.Species)):
        comps = [comps]
    comps = sorted(unpack_components(dbf, 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.')
    nonvacant_components = [x for x in sorted(comps) if x.number_of_atoms > 0]

    # 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), unpack_condition(value)) for (key, value) in kwargs.items())
    # XXX: CompiledModel assumes P, T are the only state variables
    if statevar_dict.get(v.P, None) is None:
        statevar_dict[v.P] = 101325
    if statevar_dict.get(v.T, None) is None:
        statevar_dict[v.T] = 300
    # Sort after default state variable check to fix gh-116
    statevar_dict = collections.OrderedDict(sorted(statevar_dict.items(), key=lambda x: str(x[0])))
    str_statevar_dict = collections.OrderedDict((str(key), unpack_condition(value)) \
                                                for (key, value) in statevar_dict.items())
    all_phase_data = []
    comp_sets = {}
    largest_energy = 1e30
    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, parameters=parameters)
            except DofError:
                # we can't build the specified phase because the
                # specified components aren't found in every sublattice
                # we'll just skip it
                warnings.warn("""Suspending specified phase {} due to
                some sublattices containing only unspecified components""".format(phase_name))
                continue
        if points_dict[phase_name] is None:
            maximum_internal_dof = max(maximum_internal_dof, sum(len(x) for x in mod.constituents))
        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
        # this is a phase model we couldn't construct for whatever reason; skip it
        if isinstance(mod, type):
            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:
            try:
                out = getattr(mod, output)
            except AttributeError:
                raise AttributeError('Missing Model attribute {0} specified for {1}'
                                     .format(output, mod.__class__))
            # 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)})
                warnings.warn('Setting undefined symbol {0} for phase {1} to zero'.format(undef, phase_name))
            comp_sets[phase_name] = build_functions(out, list(statevar_dict.keys()) + variables,
                                                    include_obj=True, include_grad=False,
                                                    parameters=param_symbols)
        else:
            comp_sets[phase_name] = callable_dict[phase_name]
        if mass_dict[phase_name] is None:
            pure_elements = [spec for spec in nonvacant_components
                             if (len(spec.constituents.keys()) == 1 and
                                 list(spec.constituents.keys())[0] == spec.name)
                             ]
            # TODO: In principle, we should also check for undefs in mod.moles()
            mass_dict[phase_name] = [build_functions(mod.moles(el), list(statevar_dict.keys()) + variables,
                                                     include_obj=True, include_grad=False,
                                                     parameters=param_symbols)
                                     for el in pure_elements]
        phase_record = PhaseRecord_from_cython(comps, list(statevar_dict.keys()) + variables,
                                    np.array(dbf.phases[phase_name].sublattices, dtype=np.float),
                                    param_values, comp_sets[phase_name], None, None, mass_dict[phase_name], None)
        points = points_dict[phase_name]
        if points is None:
            points = _sample_phase_constitution(phase_name, phase_obj.constituents, sublattice_dof, comps,
                                                tuple(variables), sampler_dict[phase_name] or point_sample,
                                                fixedgrid_dict[phase_name], pdens_dict[phase_name])
        points = np.atleast_2d(points)

        fp = fake_points and (phase_name == sorted(active_phases.keys())[0])
        phase_ds = _compute_phase_values(nonvacant_components, str_statevar_dict,
                                         points, phase_record, output,
                                         maximum_internal_dof, broadcast=broadcast,
                                         largest_energy=float(largest_energy), fake_points=fp)
        all_phase_data.append(phase_ds)

    # speedup for single-phase case (found by profiling)
    if len(all_phase_data) > 1:
        final_ds = concat(all_phase_data, dim='points')
        final_ds['points'].values = np.arange(len(final_ds['points']))
        final_ds.coords['points'].values = np.arange(len(final_ds['points']))
    else:
        final_ds = all_phase_data[0]
    return final_ds
コード例 #7
0
ファイル: calculate.py プロジェクト: pycalphad/pycalphad
def calculate(dbf, comps, phases, mode=None, output='GM', fake_points=False, broadcast=True, parameters=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.
    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.
        Default: 2000; Default when called from equilibrium(): 500
    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'
    parameters : dict, optional
        Maps SymPy Symbol to numbers, for overriding the values of parameters in the Database.

    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)
    callables = kwargs.pop('callables', {})
    sampler_dict = unpack_kwarg(kwargs.pop('sampler', None), default_arg=None)
    fixedgrid_dict = unpack_kwarg(kwargs.pop('grid_points', True), default_arg=True)
    parameters = parameters or dict()
    if isinstance(parameters, dict):
        parameters = OrderedDict(sorted(parameters.items(), key=str))
    if isinstance(phases, str):
        phases = [phases]
    if isinstance(comps, (str, v.Species)):
        comps = [comps]
    comps = sorted(unpack_components(dbf, 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.')
    nonvacant_components = [x for x in sorted(comps) if x.number_of_atoms > 0]

    all_phase_data = []
    largest_energy = 1e10

    # Consider only the active phases
    list_of_possible_phases = filter_phases(dbf, comps)
    active_phases = sorted(set(list_of_possible_phases).intersection(set(phases)))
    active_phases = {name: dbf.phases[name] for name in active_phases}
    if len(list_of_possible_phases) == 0:
        raise ConditionError('There are no phases in the Database that can be active with components {0}'.format(comps))
    if len(active_phases) == 0:
        raise ConditionError('None of the passed phases ({0}) are active. List of possible phases: {1}.'
                             .format(phases, list_of_possible_phases))

    models = instantiate_models(dbf, comps, list(active_phases.keys()), model=kwargs.pop('model', None), parameters=parameters)

    if isinstance(output, (list, tuple, set)):
        raise NotImplementedError('Only one property can be specified in calculate() at a time')
    output = output if output is not None else 'GM'

    # Implicitly add 'N' state variable as a string to keyword arguements if it's not passed
    if kwargs.get('N') is None:
        kwargs['N'] = 1
    if np.any(np.array(kwargs['N']) != 1):
        raise ConditionError('N!=1 is not yet supported, got N={}'.format(kwargs['N']))

    # TODO: conditions dict of StateVariable instances should become part of the calculate API
    statevar_strings = [sv for sv in kwargs.keys() if getattr(v, sv) is not None]
    # If we don't do this, sympy will get confused during substitution
    statevar_dict = dict((v.StateVariable(key), unpack_condition(value)) for key, value in kwargs.items() if key in statevar_strings)
    # Sort after default state variable check to fix gh-116
    statevar_dict = collections.OrderedDict(sorted(statevar_dict.items(), key=lambda x: str(x[0])))
    phase_records = build_phase_records(dbf, comps, active_phases, statevar_dict,
                                   models=models, parameters=parameters,
                                   output=output, callables=callables,
                                   verbose=kwargs.pop('verbose', False))
    str_statevar_dict = collections.OrderedDict((str(key), unpack_condition(value)) \
                                                for (key, value) in statevar_dict.items())
    maximum_internal_dof = max(len(models[phase_name].site_fractions) for phase_name in active_phases)
    for phase_name, phase_obj in sorted(active_phases.items()):
        mod = models[phase_name]
        phase_record = phase_records[phase_name]
        points = points_dict[phase_name]
        variables, sublattice_dof = generate_dof(phase_obj, mod.components)
        if points is None:
            points = _sample_phase_constitution(phase_name, phase_obj.constituents, sublattice_dof, comps,
                                                tuple(variables), sampler_dict[phase_name] or point_sample,
                                                fixedgrid_dict[phase_name], pdens_dict[phase_name])
        points = np.atleast_2d(points)

        fp = fake_points and (phase_name == sorted(active_phases.keys())[0])
        phase_ds = _compute_phase_values(nonvacant_components, str_statevar_dict,
                                         points, phase_record, output,
                                         maximum_internal_dof, broadcast=broadcast,
                                         largest_energy=float(largest_energy), fake_points=fp)
        all_phase_data.append(phase_ds)

    # speedup for single-phase case (found by profiling)
    if len(all_phase_data) > 1:
        final_ds = concat(all_phase_data, dim='points')
        final_ds['points'].values = np.arange(len(final_ds['points']))
        final_ds.coords['points'].values = np.arange(len(final_ds['points']))
    else:
        final_ds = all_phase_data[0]
    return final_ds
コード例 #8
0
ファイル: calculate.py プロジェクト: jwsiegel2510/pycalphad
def calculate(dbf,
              comps,
              phases,
              mode=None,
              output='GM',
              fake_points=False,
              broadcast=True,
              parameters=None,
              to_xarray=True,
              **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.
    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.
        Default: 2000; Default when called from equilibrium(): 500
    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'
    parameters : dict, optional
        Maps SymPy Symbol to numbers, for overriding the values of parameters in the Database.

    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)
    callables = kwargs.pop('callables', {})
    sampler_dict = unpack_kwarg(kwargs.pop('sampler', None), default_arg=None)
    fixedgrid_dict = unpack_kwarg(kwargs.pop('grid_points', True),
                                  default_arg=True)
    parameters = parameters or dict()
    if isinstance(parameters, dict):
        parameters = OrderedDict(sorted(parameters.items(), key=str))
    if isinstance(phases, str):
        phases = [phases]
    if isinstance(comps, (str, v.Species)):
        comps = [comps]
    comps = sorted(unpack_components(dbf, 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.'
        )
    nonvacant_components = [x for x in sorted(comps) if x.number_of_atoms > 0]

    all_phase_data = []
    largest_energy = 1e10

    # Consider only the active phases
    list_of_possible_phases = filter_phases(dbf, comps)
    if len(list_of_possible_phases) == 0:
        raise ConditionError(
            'There are no phases in the Database that can be active with components {0}'
            .format(comps))
    active_phases = {
        name: dbf.phases[name]
        for name in filter_phases(dbf, comps, phases)
    }
    if len(active_phases) == 0:
        raise ConditionError(
            'None of the passed phases ({0}) are active. List of possible phases: {1}.'
            .format(phases, list_of_possible_phases))

    models = instantiate_models(dbf,
                                comps,
                                list(active_phases.keys()),
                                model=kwargs.pop('model', None),
                                parameters=parameters)

    if isinstance(output, (list, tuple, set)):
        raise NotImplementedError(
            'Only one property can be specified in calculate() at a time')
    output = output if output is not None else 'GM'

    # Implicitly add 'N' state variable as a string to keyword arguements if it's not passed
    if kwargs.get('N') is None:
        kwargs['N'] = 1
    if np.any(np.array(kwargs['N']) != 1):
        raise ConditionError('N!=1 is not yet supported, got N={}'.format(
            kwargs['N']))

    # TODO: conditions dict of StateVariable instances should become part of the calculate API
    statevar_strings = [
        sv for sv in kwargs.keys() if getattr(v, sv) is not None
    ]
    # If we don't do this, sympy will get confused during substitution
    statevar_dict = dict((v.StateVariable(key), unpack_condition(value))
                         for key, value in kwargs.items()
                         if key in statevar_strings)
    # Sort after default state variable check to fix gh-116
    statevar_dict = collections.OrderedDict(
        sorted(statevar_dict.items(), key=lambda x: str(x[0])))
    phase_records = build_phase_records(dbf,
                                        comps,
                                        active_phases,
                                        statevar_dict,
                                        models=models,
                                        parameters=parameters,
                                        output=output,
                                        callables=callables,
                                        build_gradients=False,
                                        build_hessians=False,
                                        verbose=kwargs.pop('verbose', False))
    str_statevar_dict = collections.OrderedDict((str(key), unpack_condition(value)) \
                                                for (key, value) in statevar_dict.items())
    maximum_internal_dof = max(
        len(models[phase_name].site_fractions) for phase_name in active_phases)
    for phase_name, phase_obj in sorted(active_phases.items()):
        mod = models[phase_name]
        phase_record = phase_records[phase_name]
        points = points_dict[phase_name]
        variables, sublattice_dof = generate_dof(phase_obj, mod.components)
        if points is None:
            points = _sample_phase_constitution(
                phase_name, phase_obj.constituents, sublattice_dof, comps,
                tuple(variables), sampler_dict[phase_name] or point_sample,
                fixedgrid_dict[phase_name], pdens_dict[phase_name])
        points = np.atleast_2d(points)

        fp = fake_points and (phase_name == sorted(active_phases.keys())[0])
        phase_ds = _compute_phase_values(nonvacant_components,
                                         str_statevar_dict,
                                         points,
                                         phase_record,
                                         output,
                                         maximum_internal_dof,
                                         broadcast=broadcast,
                                         largest_energy=float(largest_energy),
                                         fake_points=fp)
        all_phase_data.append(phase_ds)

    # speedup for single-phase case (found by profiling)
    if len(all_phase_data) > 1:
        concatenated_coords = all_phase_data[0].coords

        data_vars = all_phase_data[0].data_vars
        concatenated_data_vars = {}
        for var in data_vars.keys():
            data_coords = data_vars[var][0]
            points_idx = data_coords.index('points')  # concatenation axis
            arrs = []
            for phase_data in all_phase_data:
                arrs.append(getattr(phase_data, var))
            concat_data = np.concatenate(arrs, axis=points_idx)
            concatenated_data_vars[var] = (data_coords, concat_data)
        final_ds = LightDataset(data_vars=concatenated_data_vars,
                                coords=concatenated_coords)
    else:
        final_ds = all_phase_data[0]
    if to_xarray:
        return final_ds.get_dataset()
    else:
        return final_ds
コード例 #9
0
ファイル: zpf_error.py プロジェクト: jwsiegel2510/ESPEI
def calculate_driving_force(dbf,
                            data_comps,
                            phases,
                            current_statevars,
                            ph_cond_dict,
                            phase_models,
                            phase_dict,
                            parameters,
                            callables,
                            tol=0.001,
                            max_it=50):
    """
    Calculates driving force for a single data point.

    Parameters
    ----------
    dbf : pycalphad.Database
        Database to consider
    data_comps : list
        List of active component names
    phases : list
        List of phases to consider
    current_statevars : dict
        Dictionary of state variables, e.g. v.P and v.T, no compositions.
    ph_cond_dict : dict
        Dictionary mapping phases to the conditions at which they occurred in experiment.
    phase_models : dict
        Phase models to pass to pycalphad calculations
    parameters : dict
        Dictionary of symbols that will be overridden in pycalphad.equilibrium
    callables : dict
        Callables to pass to pycalphad
    tol: double
        The tolerance allowed for optimization over hyperplanes.
    max_it: int
        The maximum number of iterations allowed for optimization over hyperplanes.

    Notes
    ------
    Calculates the driving force by optimizing the driving force over the chemical potential.
    Allow calculation of the driving force even when both tie points are missing.
    """
    # TODO Refactor absurd unpacking which represents a significant overhead.
    species = list(map(v.Species, data_comps))
    conditions = current_statevars
    if conditions.get(v.N) is None:
        conditions[v.N] = 1.0
    if np.any(np.array(conditions[v.N]) != 1):
        raise ConditionError('N!=1 is not yet supported, got N={}'.format(
            conditions[v.N]))
    conds = conditions
    str_conds = OrderedDict([(str(key), conds[key])
                             for key in sorted(conds.keys(), key=str)])
    models = instantiate_models(dbf,
                                data_comps,
                                phases,
                                model=phase_models,
                                parameters=parameters)
    prxs = build_phase_records(dbf,
                               species,
                               phases,
                               conds,
                               models,
                               build_gradients=True,
                               build_hessians=True,
                               callables=callables,
                               parameters=parameters)
    # Collect data information in phase_dict.
    for phase in phases:
        phase_dict[phase]['data'] = False
    for ph, cond in ph_cond_dict:
        has_nones = False
        ph_conds = cond[0]
        phase_dict[ph]['data'] = True
        for key in ph_conds:
            if ph_conds[key] is None:
                has_nones = True
                phase_dict[ph]['phase_record'] = None
                phase_dict[ph]['str_conds'] = None
        if not has_nones:
            ph_conds.update(conditions)
            phase_records = build_phase_records(dbf,
                                                species, [ph],
                                                ph_conds,
                                                models,
                                                build_gradients=True,
                                                build_hessians=True,
                                                callables=callables,
                                                parameters=parameters)
            phase_dict[ph]['phase_record'] = phase_records[ph]
            phase_dict[ph]['str_conds'] = OrderedDict([
                (str(key), ph_conds[key])
                for key in sorted(ph_conds.keys(), key=str)
            ])
            phase_dict[ph]['min_energy'] = None
    # Collect sampling and equilibrium information in phase_dict.
    for phase in phases:
        # If sample points have not yet been calculated for this phase, calculate them.
        if not 'sample_points' in phase_dict[phase]:
            phase_obj = dbf.phases[phase]
            components = models[phase].components
            variables, sublattice_dof = generate_dof(phase_obj, components)
            sample_points = _sample_phase_constitution(
                phase, phase_obj.constituents, sublattice_dof, data_comps,
                tuple(variables), point_sample, True, 2000)
            phase_dict[phase]['sample_points'] = sample_points
        # If composition values have not yet been calculated for this phase, calculate them.
        if not 'composition_values' in phase_dict[phase]:
            composition_values = np.zeros(
                (sample_points.shape[0],
                 len([sp for sp in species if sp.__str__() != 'VA'])))
            temp_comp_set = CompositionSet(prxs[phase])
            current_state_variables = np.array(
                [str_conds[key] for key in sorted(str_conds.keys(), key=str)])
            for i in range(sample_points.shape[0]):
                temp_comp_set.py_update(sample_points[i, :], np.array([1.0]),
                                        current_state_variables, False)
                composition_values[i, :] = temp_comp_set.X
            phase_dict[phase]['composition_values'] = composition_values
        energies = calculate(dbf,
                             data_comps, [phase],
                             points=phase_dict[phase]['sample_points'],
                             to_xarray=False,
                             **str_conds)
        phase_dict[phase]['energy_values'] = np.array(energies['GM'][0][0][0])
    hyperplane = generate_random_hyperplane(species)
    result = calculate_driving_force_at_chem_potential(dbf,
                                                       hyperplane,
                                                       species,
                                                       phase_dict,
                                                       prxs,
                                                       str_conds,
                                                       approx=True)
    # Ignore entire data point if pointsolver fails to converge.
    if result is None:
        return 0
    # Optimize over the hyperplane.
    it = 0
    current_driving_force = result['driving_force']
    new_plane = result['new_plane']
    last_plane = new_plane
    while np.linalg.norm(hyperplane - last_plane) > tol and it < max_it:
        it += 1
        last_plane = hyperplane
        result = calculate_driving_force_at_chem_potential(dbf,
                                                           new_plane,
                                                           species,
                                                           phase_dict,
                                                           prxs,
                                                           str_conds,
                                                           approx=True)
        # If step results in objective decrease, accept the step.
        if result['driving_force'] < current_driving_force:
            current_driving_force = result['driving_force']
            hyperplane = new_plane
            new_plane = result['new_plane']
        else:
            step = 0.5
            temp_hyperplane = new_plane
            while result[
                    'driving_force'] > current_driving_force and np.linalg.norm(
                        hyperplane - temp_hyperplane) > tol:
                temp_hyperplane = (1.0 - step) * hyperplane + step * new_plane
                result = calculate_driving_force_at_chem_potential(
                    dbf,
                    temp_hyperplane,
                    species,
                    phase_dict,
                    prxs,
                    str_conds,
                    approx=True)
                step /= 2
            hyperplane = temp_hyperplane
            result = calculate_driving_force_at_chem_potential(dbf,
                                                               hyperplane,
                                                               species,
                                                               phase_dict,
                                                               prxs,
                                                               str_conds,
                                                               approx=True)
            new_plane = result['new_plane']
            current_driving_force = result['driving_force']
    final_result = calculate_driving_force_at_chem_potential(dbf,
                                                             hyperplane,
                                                             species,
                                                             phase_dict,
                                                             prxs,
                                                             str_conds,
                                                             approx=True)
    final_driving_force = final_result['driving_force']
    print(it, final_driving_force, hyperplane)
    return final_driving_force
コード例 #10
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
コード例 #11
0
ファイル: shadow_functions.py プロジェクト: zikuiliu/ESPEI
def calculate_(
    dbf: Database,
    species: Sequence[v.Species],
    phases: Sequence[str],
    str_statevar_dict: Dict[str, np.ndarray],
    models: Dict[str, Model],
    phase_records: Dict[str, PhaseRecord],
    output: Optional[str] = 'GM',
    points: Optional[Dict[str, np.ndarray]] = None,
    pdens: Optional[int] = 2000,
    broadcast: Optional[bool] = True,
    fake_points: Optional[bool] = False,
) -> LightDataset:
    """
    Quickly sample phase internal degree of freedom with virtually no overhead.
    """
    points_dict = unpack_kwarg(points, default_arg=None)
    pdens_dict = unpack_kwarg(pdens, default_arg=2000)
    nonvacant_components = [
        x for x in sorted(species) if x.number_of_atoms > 0
    ]
    maximum_internal_dof = max(prx.phase_dof for prx in phase_records.values())
    all_phase_data = []
    for phase_name in sorted(phases):
        phase_obj = dbf.phases[phase_name]
        mod = models[phase_name]
        phase_record = phase_records[phase_name]
        points = points_dict[phase_name]
        variables, sublattice_dof = generate_dof(phase_obj, mod.components)
        if points is None:
            points = _sample_phase_constitution(phase_name,
                                                phase_obj.constituents,
                                                sublattice_dof, species,
                                                tuple(variables), point_sample,
                                                True, pdens_dict[phase_name])
        points = np.atleast_2d(points)

        fp = fake_points and (phase_name == sorted(phases)[0])
        phase_ds = _compute_phase_values(nonvacant_components,
                                         str_statevar_dict,
                                         points,
                                         phase_record,
                                         output,
                                         maximum_internal_dof,
                                         broadcast=broadcast,
                                         largest_energy=float(1e10),
                                         fake_points=fp)
        all_phase_data.append(phase_ds)

    if len(all_phase_data) > 1:
        concatenated_coords = all_phase_data[0].coords

        data_vars = all_phase_data[0].data_vars
        concatenated_data_vars = {}
        for var in data_vars.keys():
            data_coords = data_vars[var][0]
            points_idx = data_coords.index('points')  # concatenation axis
            arrs = []
            for phase_data in all_phase_data:
                arrs.append(getattr(phase_data, var))
            concat_data = np.concatenate(arrs, axis=points_idx)
            concatenated_data_vars[var] = (data_coords, concat_data)
        final_ds = LightDataset(data_vars=concatenated_data_vars,
                                coords=concatenated_coords)
    else:
        final_ds = all_phase_data[0]
    return final_ds
コード例 #12
0
def simulate_scheil_solidification(dbf,
                                   comps,
                                   phases,
                                   composition,
                                   start_temperature,
                                   step_temperature=1.0,
                                   liquid_phase_name='LIQUID',
                                   eq_kwargs=None,
                                   stop=0.0001,
                                   verbose=False,
                                   adaptive=True):
    """Perform a Scheil-Gulliver solidification simulation.

    Parameters
    ----------
    dbf : pycalphad.Database
        Database object.
    comps : list
        List of components in the system.
    phases : list
        List of phases in the system.
    composition : Dict[v.X, float]
        Dictionary of independent `v.X` composition variables.
    start_temperature : float
        Starting temperature for simulation. Must be single phase liquid.
    step_temperature : Optional[float]
        Temperature step size. Defaults to 1.0.
    liquid_phase_name : Optional[str]
        Name of the phase treated as liquid (i.e. the phase with infinitely
        fast diffusion). Defaults to 'LIQUID'.
    eq_kwargs: Optional[Dict[str, Any]]
        Keyword arguments for equilibrium
    stop: Optional[float]
        Stop when the phase fraction of liquid is below this amount.
    adaptive: Optional[bool]
        Whether to add additional points near the equilibrium points at each
        step. Only takes effect if ``points`` is in the eq_kwargs dict.

    Returns
    -------
    SolidificationResult

    """
    eq_kwargs = eq_kwargs or dict()
    STEP_SCALE_FACTOR = 1.2  # How much to try to adapt the temperature step by
    MAXIMUM_STEP_SIZE_REDUCTION = 5.0
    T_STEP_ORIG = step_temperature
    phases = filter_phases(dbf, unpack_components(dbf, comps), phases)
    models = instantiate_models(dbf, comps, phases)
    if verbose:
        print('building callables... ', end='')
    cbs = build_callables(dbf,
                          comps,
                          phases,
                          models,
                          additional_statevars={v.P, v.T, v.N},
                          build_gradients=True,
                          build_hessians=True)
    if verbose:
        print('done')
    solid_phases = sorted(set(phases) - {liquid_phase_name})
    temp = start_temperature
    independent_comps = sorted([str(comp)[2:] for comp in composition.keys()])
    x_liquid = {comp: [composition[v.X(comp)]] for comp in independent_comps}
    fraction_solid = [0.0]
    temperatures = [temp]
    phase_amounts = {ph: [0.0] for ph in solid_phases}
    ord_disord_dict = order_disorder_dict(dbf, comps, phases)

    if adaptive and ('points' in eq_kwargs.get('calc_opts', {})):
        # Dynamically add points as the simulation runs
        species = unpack_components(dbf, comps)
        dof_dict = {
            ph: generate_dof(dbf.phases[ph], species)[1]
            for ph in phases
        }
    else:
        adaptive = False

    converged = False
    phases_seen = {liquid_phase_name, ''}
    liquid_comp = composition
    while fraction_solid[-1] < 1:
        conds = {v.T: temp, v.P: 101325.0, v.N: 1.0}
        comp_conds = liquid_comp
        fmt_comp_conds = ', '.join(
            [f'{c}={val:0.2f}' for c, val in comp_conds.items()])
        conds.update(comp_conds)
        eq = equilibrium(dbf,
                         comps,
                         phases,
                         conds,
                         callables=cbs,
                         model=models,
                         **eq_kwargs)
        if adaptive:
            # Update the points dictionary with local samples around the equilibrium site fractions
            points_dict = eq_kwargs['calc_opts']['points']
            for vtx in range(eq.vertex.size):
                masked = eq.isel(vertex=vtx)
                ph = str(masked.Phase.values.squeeze())
                pts = points_dict.get(ph)
                if pts is not None:
                    if verbose:
                        print(f'Adding points to {ph}. ', end='')
                    dof = dof_dict[ph]
                    points_dict[ph] = np.concatenate([
                        pts,
                        local_sample(
                            masked.Y.values.squeeze()[:sum(dof)].reshape(
                                1, -1),
                            dof,
                            pdens=20)
                    ],
                                                     axis=0)

        eq_phases = order_disorder_eq_phases(eq, ord_disord_dict)
        num_eq_phases = np.nansum(
            np.array([str(ph) for ph in eq_phases]) != '')
        new_phases_seen = set(eq_phases).difference(phases_seen)
        if len(new_phases_seen) > 0:
            if verbose:
                print(f'New phases seen: {new_phases_seen}. ', end='')
            phases_seen |= new_phases_seen
        if liquid_phase_name not in eq["Phase"].values.squeeze():
            found_ph = set(eq_phases) - {''}
            if verbose:
                print(
                    f'No liquid phase found at T={temp:0.3f}, {fmt_comp_conds}. (Found {found_ph}) ',
                    end='')
            if len(found_ph) == 0:
                # No phases found in equilibrium. Just continue on lowering the temperature without changing anything
                if verbose:
                    print(f'(Convergence failure) ', end='')
            if T_STEP_ORIG / step_temperature > MAXIMUM_STEP_SIZE_REDUCTION:
                # Only found solid phases and the step size has already been reduced. Stop running without converging.
                if verbose:
                    print('Maximum step size reduction exceeded. Stopping.')
                converged = False
                break
            else:
                # Only found solid phases. Try reducing the step size to zero-in on the correct phases
                if verbose:
                    print(f'Stepping back and reducing step size.')
                temp += step_temperature
                step_temperature /= STEP_SCALE_FACTOR
                continue
        # TODO: Will break if there is a liquid miscibility gap
        liquid_vertex = sorted(
            np.nonzero(
                eq["Phase"].values.squeeze().flat == liquid_phase_name))[0]
        liquid_comp = {}
        for comp in independent_comps:
            x = float(eq["X"].isel(vertex=liquid_vertex).squeeze().sel(
                component=comp).values)
            x_liquid[comp].append(x)
            liquid_comp[v.X(comp)] = x
        np_liq = np.nansum(
            eq.where(eq["Phase"] == liquid_phase_name).NP.values)
        current_fraction_solid = float(fraction_solid[-1])
        found_phase_amounts = [(liquid_phase_name, np_liq)
                               ]  # tuples of phase name, amount
        for solid_phase in solid_phases:
            if solid_phase not in eq_phases:
                phase_amounts[solid_phase].append(0.0)
                continue
            np_tieline = np.nansum(
                eq.isel(vertex=eq_phases.index(solid_phase))
                ["NP"].values.squeeze())
            found_phase_amounts.append((solid_phase, np_tieline))
            delta_fraction_solid = (1 - current_fraction_solid) * np_tieline
            current_fraction_solid += delta_fraction_solid
            phase_amounts[solid_phase].append(delta_fraction_solid)
        fraction_solid.append(current_fraction_solid)
        temperatures.append(temp)
        NL = 1 - fraction_solid[-1]
        if verbose:
            phase_amnts = ' '.join(
                [f'NP({ph})={amnt:0.3f}' for ph, amnt in found_phase_amounts])
            if NL < 1.0e-3:
                print(
                    f'T={temp:0.3f}, {fmt_comp_conds}, ΔT={step_temperature:0.3f}, NL: {NL:.2E}, {phase_amnts} ',
                    end='')
            else:
                print(
                    f'T={temp:0.3f}, {fmt_comp_conds}, ΔT={step_temperature:0.3f}, NL: {NL:0.3f}, {phase_amnts} ',
                    end='')
        if NL < stop:
            if verbose:
                print(
                    f'Liquid fraction below criterion {stop} . Stopping at {fmt_comp_conds}'
                )
            converged = True
            break
        if verbose:
            print()  # add line break
        temp -= step_temperature

    if fraction_solid[-1] < 1:
        for comp in independent_comps:
            x_liquid[comp].append(np.nan)
        fraction_solid.append(1.0)
        temperatures.append(temp)
        # set the final phase amount to the phase fractions in the eutectic
        # this method gives the sum total phase amounts of 1.0 by construction
        for solid_phase in solid_phases:
            if solid_phase in eq_phases:
                amount = np.nansum(
                    eq.isel(vertex=eq_phases.index(solid_phase))
                    ["NP"].values.squeeze())
                phase_amounts[solid_phase].append(
                    float(amount) * (1 - current_fraction_solid))
            else:
                phase_amounts[solid_phase].append(0.0)

    return SolidificationResult(x_liquid, fraction_solid, temperatures,
                                phase_amounts, converged, "scheil")
コード例 #13
0
def simulate_equilibrium_solidification(dbf,
                                        comps,
                                        phases,
                                        composition,
                                        start_temperature,
                                        step_temperature=1.0,
                                        liquid_phase_name='LIQUID',
                                        adaptive=True,
                                        eq_kwargs=None,
                                        binary_search_tol=0.1,
                                        verbose=False):
    """
    Compute the equilibrium solidification path.

    Decreases temperature until no liquid is found, performing a binary search to get the soildus temperature.

    dbf : pycalphad.Database
        Database object.
    comps : list
        List of components in the system.
    phases : list
        List of phases in the system.
    composition : Dict[v.X, float]
        Dictionary of independent `v.X` composition variables.
    start_temperature : float
        Starting temperature for simulation. Should be single phase liquid.
    step_temperature : Optional[float]
        Temperature step size. Defaults to 1.0.
    liquid_phase_name : Optional[str]
        Name of the phase treated as liquid (i.e. the phase with infinitely
        fast diffusion). Defaults to 'LIQUID'.
    eq_kwargs: Optional[Dict[str, Any]]
        Keyword arguments for equilibrium
    binary_search_tol : float
        Stop the binary search when the difference between temperatures is less than this amount.
    adaptive: Optional[bool]
        Whether to add additional points near the equilibrium points at each
        step. Only takes effect if ``points`` is in the eq_kwargs dict.

    """
    eq_kwargs = eq_kwargs or dict()
    phases = filter_phases(dbf, unpack_components(dbf, comps), phases)
    ord_disord_dict = order_disorder_dict(dbf, comps, phases)
    solid_phases = sorted(set(phases) - {liquid_phase_name})
    independent_comps = sorted([str(comp)[2:] for comp in composition.keys()])
    models = instantiate_models(dbf, comps, phases)
    if verbose:
        print('building callables... ', end='')
    cbs = build_callables(dbf,
                          comps,
                          phases,
                          models,
                          additional_statevars={v.P, v.T, v.N},
                          build_gradients=True,
                          build_hessians=True)
    if verbose:
        print('done')
    conds = {v.P: 101325, v.N: 1.0}
    conds.update(composition)

    if adaptive and ('points' in eq_kwargs.get('calc_opts', {})):
        # Dynamically add points as the simulation runs
        species = unpack_components(dbf, comps)
        dof_dict = {
            ph: generate_dof(dbf.phases[ph], species)[1]
            for ph in phases
        }
    else:
        adaptive = False

    temperatures = []
    x_liquid = {comp: [] for comp in independent_comps}
    fraction_solid = []
    phase_amounts = {ph: []
                     for ph in solid_phases}  # instantaneous phase amounts
    cum_phase_amounts = {ph: [] for ph in solid_phases}
    converged = False
    current_T = start_temperature
    if verbose:
        print('T=')
    while fraction_solid[-1] < 1 if len(fraction_solid) > 0 else True:
        sys.stdout.flush()
        conds[v.T] = current_T
        if verbose:
            print(f'{current_T} ', end='')
        eq = equilibrium(dbf,
                         comps,
                         phases,
                         conds,
                         callables=cbs,
                         model=models,
                         to_xarray=False,
                         **eq_kwargs)
        if not is_converged(eq):
            if verbose:
                comp_conds = {
                    cond: val
                    for cond, val in conds.items() if isinstance(cond, v.X)
                }
                print(f"Convergence failure at T={conds[v.T]} X={comp_conds} ")
        if adaptive:
            # Update the points dictionary with local samples around the equilibrium site fractions
            update_points(eq, eq_kwargs['calc_opts']['points'], dof_dict)
        if liquid_phase_name in eq.Phase:
            # Add the liquid phase composition
            # TODO: will break in a liquid miscibility gap
            liquid_vertex = np.nonzero(eq.Phase == liquid_phase_name)[-1][0]
            for comp in independent_comps:
                x_liquid[comp].append(
                    float(eq.X[..., liquid_vertex,
                               eq.component.index(comp)]))
            temperatures.append(current_T)
            current_T -= step_temperature
        else:
            # binary search to find the solidus
            T_high = current_T + step_temperature  # High temperature, liquid
            T_low = current_T  # Low temperature, solids only
            found_ph = set(eq.Phase[eq.Phase != ''].tolist())
            if verbose:
                print(
                    f'Found phases {found_ph}. Starting binary search between T={(T_low, T_high)} ',
                    end='')
            while (T_high - T_low) > binary_search_tol:
                bin_search_T = (T_high - T_low) * 0.5 + T_low
                conds[v.T] = bin_search_T
                eq = equilibrium(dbf,
                                 comps,
                                 phases,
                                 conds,
                                 callables=cbs,
                                 model=models,
                                 to_xarray=False,
                                 **eq_kwargs)
                if adaptive:
                    # Update the points dictionary with local samples around the equilibrium site fractions
                    update_points(eq, eq_kwargs['calc_opts']['points'],
                                  dof_dict)
                if not is_converged(eq):
                    if verbose:
                        comp_conds = {
                            cond: val
                            for cond, val in conds.items()
                            if isinstance(cond, v.X)
                        }
                        print(
                            f"Convergence failure at T={conds[v.T]} X={comp_conds} "
                        )
                if liquid_phase_name in eq.Phase:
                    T_high = bin_search_T
                else:
                    T_low = bin_search_T
            conds[v.T] = T_low
            temperatures.append(T_low)
            eq = equilibrium(dbf,
                             comps,
                             phases,
                             conds,
                             callables=cbs,
                             model=models,
                             to_xarray=False,
                             **eq_kwargs)
            if not is_converged(eq):
                if verbose:
                    comp_conds = {
                        cond: val
                        for cond, val in conds.items()
                        if isinstance(cond, v.X)
                    }
                    print(
                        f"Convergence failure at T={conds[v.T]} X={comp_conds} "
                    )
            if verbose:
                found_phases = set(eq.Phase[eq.Phase != ''].tolist())
                print(
                    f"Finshed binary search at T={conds[v.T]} with phases={found_phases} and NP={eq.NP.squeeze()[:len(found_phases)]}"
                )
            if adaptive:
                # Update the points dictionary with local samples around the equilibrium site fractions
                update_points(eq, eq_kwargs['calc_opts']['points'], dof_dict)
            # Set the liquid phase composition to NaN
            for comp in independent_comps:
                x_liquid[comp].append(float(np.nan))

        # Calculate fraction of solid and solid phase amounts
        current_fraction_solid = 0.0
        current_cum_phase_amnts = get_phase_amounts(
            order_disorder_eq_phases(eq.get_dataset(), ord_disord_dict),
            eq.NP.squeeze(), solid_phases)
        for solid_phase, amount in current_cum_phase_amnts.items():
            # Since the equilibrium calculations always give the "cumulative" phase amount,
            # we need to take the difference to get the instantaneous.
            cum_phase_amounts[solid_phase].append(amount)
            if len(phase_amounts[solid_phase]) == 0:
                phase_amounts[solid_phase].append(amount)
            else:
                phase_amounts[solid_phase].append(
                    amount - cum_phase_amounts[solid_phase][-2])
            current_fraction_solid += amount
        fraction_solid.append(current_fraction_solid)

    converged = True if np.isclose(fraction_solid[-1], 1.0) else False
    return SolidificationResult(x_liquid, fraction_solid, temperatures,
                                phase_amounts, converged, "equilibrium")
コード例 #14
0
ファイル: energy_surf.py プロジェクト: manirm/pycalphad
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)