Esempio n. 1
0
def equilibrium_(phase_records: Dict[str, PhaseRecord],
                 conditions: Dict[v.StateVariable, np.ndarray], grid: LightDataset
                 ) -> LightDataset:
    """
    Perform a fast equilibrium calculation with virtually no overhead.
    """
    statevars = sorted(get_state_variables(conds=conditions), key=str)
    conditions = _adjust_conditions(conditions)
    str_conds = OrderedDict([(str(ky), conditions[ky]) for ky in sorted(conditions.keys(), key=str)])
    start_point = starting_point(conditions, statevars, phase_records, grid)
    return _solve_eq_at_conditions(start_point, phase_records, grid, str_conds, statevars, False)
Esempio n. 2
0
def constrained_equilibrium(phase_records: Dict[str, PhaseRecord],
                 conditions: Dict[v.StateVariable, np.ndarray], grid: LightDataset):
    """Perform an equilibrium calculation with just a single composition set that is constrained to the global composition condition"""
    statevars = get_state_variables(conds=conditions)
    conditions = _adjust_conditions(conditions)
    # Assume that all conditions keys are lists with exactly one element (point calculation)
    str_conds = OrderedDict([(str(ky), conditions[ky][0]) for ky in sorted(conditions.keys(), key=str)])
    compset = _single_phase_start_point(conditions, statevars, phase_records, grid)
    # modifies `compset` in place
    solver_result = solve_and_update([compset], str_conds, Solver())
    energy = compset.NP * compset.energy
    return solver_result.converged, energy
Esempio n. 3
0
def no_op_equilibrium_(phase_records: Dict[str, PhaseRecord],
                       conditions: Dict[v.StateVariable, np.ndarray],
                       grid: LightDataset,
                       ) -> LightDataset:
    """
    Perform a fast "equilibrium" calculation with virtually no overhead that
    doesn't refine the solution or do global minimization, but just returns
    the starting point.

    Notes
    -----
    Uses a placeholder first argument for the same signature as
    ``_equilibrium``, but ``species`` are not needed.

    """
    statevars = get_state_variables(conds=conditions)
    conditions = _adjust_conditions(conditions)
    return starting_point(conditions, statevars, phase_records, grid)
Esempio n. 4
0
def test_eq_build_callables_with_parameters():
    """
    Check build_callables() compatibility with the parameters kwarg.
    """
    comps = ["AL"]
    dbf = AL_PARAMETER_DBF
    phases = ['FCC_A1']
    conds = {v.P: 101325, v.T: 500, v.N: 1}
    conds_statevars = get_state_variables(conds=conds)
    models = {'FCC_A1': Model(dbf, comps, 'FCC_A1', parameters=['VV0000'])}
    # build callables with a parameter of 20000.0
    callables = build_callables(dbf, comps, phases, models=models, parameter_symbols=['VV0000'], additional_statevars=conds_statevars)

    # Check that passing callables should skip the build phase, but use the values from 'VV0000' as passed in parameters
    eq_res = equilibrium(dbf, comps, phases, conds, callables=callables, parameters={'VV0000': 10000})
    np.testing.assert_allclose(eq_res.GM.values.squeeze(), 10000.0)

    # Check that passing callables should skip the build phase,
    # but use the values from Symbol('VV0000') as passed in parameters
    eq_res = equilibrium(dbf, comps, phases, conds, callables=callables, parameters={Symbol('VV0000'): 10000})
    np.testing.assert_allclose(eq_res.GM.values.squeeze(), 10000.0)
Esempio n. 5
0
def map_binary(
    dbf,
    comps,
    phases,
    conds,
    eq_kwargs=None,
    calc_kwargs=None,
    boundary_sets=None,
    verbose=False,
    summary=False,
):
    """
    Map a binary T-X phase diagram

    Parameters
    ----------
    dbf : Database
    comps : list of str
    phases : list of str
        List of phases to consider in mapping
    conds : dict
        Dictionary of conditions
    eq_kwargs : dict
        Dictionary of keyword arguments to pass to equilibrium
    verbose : bool
        Print verbose output for mapping
    boundary_sets : ZPFBoundarySets
        Existing ZPFBoundarySets

    Returns
    -------
    ZPFBoundarySets

    Notes
    -----
    Assumes conditions in T and X.

    Simple algorithm to map a binary phase diagram in T-X. More or less follows
    the algorithm described in Figure 2 by Snider et al. [1] with the small
    algorithmic improvement of constructing a convex hull to find the next
    potential two phase region.

    For each temperature, proceed along increasing composition, skipping two
    over two phase regions, once calculated.
    [1] J. Snider, I. Griva, X. Sun, M. Emelianenko, Set based framework for
        Gibbs energy minimization, Calphad. 48 (2015) 18-26.
        doi: 10.1016/j.calphad.2014.09.005

    """

    eq_kwargs = eq_kwargs or {}
    calc_kwargs = calc_kwargs or {}
    # implicitly add v.N to conditions
    if v.N not in conds:
        conds[v.N] = [1.0]
    if 'pdens' not in calc_kwargs:
        calc_kwargs['pdens'] = 2000

    species = unpack_components(dbf, comps)
    phases = filter_phases(dbf, species, phases)
    parameters = eq_kwargs.get('parameters', {})
    models = eq_kwargs.get('model')
    statevars = get_state_variables(models=models, conds=conds)
    if models is None:
        models = instantiate_models(dbf,
                                    comps,
                                    phases,
                                    model=eq_kwargs.get('model'),
                                    parameters=parameters,
                                    symbols_only=True)
    prxs = build_phase_records(dbf,
                               species,
                               phases,
                               conds,
                               models,
                               output='GM',
                               parameters=parameters,
                               build_gradients=True,
                               build_hessians=True)

    indep_comp = [
        key for key, value in conds.items()
        if isinstance(key, v.MoleFraction) and len(np.atleast_1d(value)) > 1
    ]
    indep_pot = [
        key for key, value in conds.items()
        if (type(key) is v.StateVariable) and len(np.atleast_1d(value)) > 1
    ]
    if (len(indep_comp) != 1) or (len(indep_pot) != 1):
        raise ValueError(
            'Binary map requires exactly one composition and one potential coordinate'
        )
    if indep_pot[0] != v.T:
        raise ValueError(
            'Binary map requires that a temperature grid must be defined')

    # binary assumption, only one composition specified.
    comp_cond = [k for k in conds.keys() if isinstance(k, v.X)][0]
    indep_comp = comp_cond.name[2:]
    indep_comp_idx = sorted(get_pure_elements(dbf, comps)).index(indep_comp)
    composition_grid = unpack_condition(conds[comp_cond])
    dX = composition_grid[1] - composition_grid[0]
    Xmax = composition_grid.max()
    temperature_grid = unpack_condition(conds[v.T])
    dT = temperature_grid[1] - temperature_grid[0]

    boundary_sets = boundary_sets or ZPFBoundarySets(comps, comp_cond)

    equilibria_calculated = 0
    equilibrium_time = 0
    convex_hulls_calculated = 0
    convex_hull_time = 0
    curr_conds = {key: unpack_condition(val) for key, val in conds.items()}
    str_conds = sorted([str(k) for k in curr_conds.keys()])
    grid_conds = _adjust_conditions(curr_conds)
    for T_idx in range(temperature_grid.size):
        T = temperature_grid[T_idx]
        iter_equilibria = 0
        if verbose:
            print("=== T = {} ===".format(float(T)))
        curr_conds[v.T] = [float(T)]
        eq_conds = deepcopy(curr_conds)
        Xmax_visited = 0.0
        hull_time = time.time()
        grid = calculate(dbf,
                         comps,
                         phases,
                         fake_points=True,
                         output='GM',
                         T=T,
                         P=grid_conds[v.P],
                         N=1,
                         model=models,
                         parameters=parameters,
                         to_xarray=False,
                         **calc_kwargs)
        hull = starting_point(eq_conds, statevars, prxs, grid)
        convex_hull_time += time.time() - hull_time
        convex_hulls_calculated += 1
        while Xmax_visited < Xmax:
            hull_compsets = find_two_phase_region_compsets(
                hull,
                T,
                indep_comp,
                indep_comp_idx,
                minimum_composition=Xmax_visited,
                misc_gap_tol=2 * dX)
            if hull_compsets is None:
                if verbose:
                    print(
                        "== Convex hull: max visited = {} - no multiphase phase compsets found =="
                        .format(Xmax_visited, hull_compsets))
                break
            Xeq = hull_compsets.mean_composition
            eq_conds[comp_cond] = [float(Xeq)]
            eq_time = time.time()
            start_point = starting_point(eq_conds, statevars, prxs, grid)
            eq_ds = _solve_eq_at_conditions(species, start_point, prxs, grid,
                                            str_conds, statevars, False)
            equilibrium_time += time.time() - eq_time
            equilibria_calculated += 1
            iter_equilibria += 1
            # composition sets in the plane of the calculation:
            # even for isopleths, this should always be two.
            compsets = get_compsets(eq_ds, indep_comp, indep_comp_idx)
            if verbose:
                print(
                    "== Convex hull: max visited = {:0.4f} - hull compsets: {} equilibrium compsets: {} =="
                    .format(Xmax_visited, hull_compsets, compsets))
            if compsets is None:
                # equilibrium calculation, didn't find a valid multiphase composition set
                # we need to find the next feasible one from the convex hull.
                Xmax_visited += dX
                continue
            else:
                boundary_sets.add_compsets(compsets, Xtol=0.10, Ttol=2 * dT)
                if compsets.max_composition > Xmax_visited:
                    Xmax_visited = compsets.max_composition
            # this seems kind of sloppy, but captures the effect that we want to
            # keep doing equilibrium calculations, if possible.
            while Xmax_visited < Xmax and compsets is not None:
                eq_conds[comp_cond] = [float(Xmax_visited + dX)]
                eq_time = time.time()
                # TODO: starting point could be improved by basing it off the previous calculation
                start_point = starting_point(eq_conds, statevars, prxs, grid)
                eq_ds = _solve_eq_at_conditions(species, start_point, prxs,
                                                grid, str_conds, statevars,
                                                False)
                equilibrium_time += time.time() - eq_time
                equilibria_calculated += 1
                compsets = get_compsets(eq_ds, indep_comp, indep_comp_idx)
                if compsets is not None:
                    Xmax_visited = compsets.max_composition
                    boundary_sets.add_compsets(compsets,
                                               Xtol=0.10,
                                               Ttol=2 * dT)
                else:
                    Xmax_visited += dX
                if verbose:
                    print("Equilibrium: at X = {:0.4f}, found compsets {}".
                          format(Xmax_visited, compsets))
        if verbose:
            print(iter_equilibria, 'equilibria calculated in this iteration.')
    if verbose or summary:
        print("{} Convex hulls calculated ({:0.1f}s)".format(
            convex_hulls_calculated, convex_hull_time))
        print("{} Equilbria calculated ({:0.1f}s)".format(
            equilibria_calculated, equilibrium_time))
        print("{:0.0f}% of brute force calculations skipped".format(
            100 * (1 - equilibria_calculated /
                   (composition_grid.size * temperature_grid.size))))
    return boundary_sets
Esempio n. 6
0
def equilibrium(dbf, comps, phases, conditions, output=None, model=None,
                verbose=False, broadcast=True, calc_opts=None,
                scheduler='sync', parameters=None, solver=None, callables=None,
                **kwargs):
    """
    Calculate the equilibrium state of a system containing the specified
    components and phases, under the specified conditions.

    Parameters
    ----------
    dbf : Database
        Thermodynamic database containing the relevant parameters.
    comps : list
        Names of components to consider in the calculation.
    phases : list or dict
        Names of phases to consider in the calculation.
    conditions : dict or (list of dict)
        StateVariables and their corresponding value.
    output : str or list of str, optional
        Additional equilibrium model properties (e.g., CPM, HM, etc.) to compute.
        These must be defined as attributes in the Model class of each phase.
    model : Model, a dict of phase names to Model, or a seq of both, optional
        Model class to use for each phase.
    verbose : bool, optional
        Print details of calculations. Useful for debugging.
    broadcast : bool
        If True, broadcast conditions against each other. This will compute all combinations.
        If False, each condition should be an equal-length list (or single-valued).
        Disabling broadcasting is useful for calculating equilibrium at selected conditions,
        when those conditions don't comprise a grid.
    calc_opts : dict, optional
        Keyword arguments to pass to `calculate`, the energy/property calculation routine.
    scheduler : Dask scheduler, optional
        Job scheduler for performing the computation.
        If None, return a Dask graph of the computation instead of actually doing it.
    parameters : dict, optional
        Maps SymPy Symbol to numbers, for overriding the values of parameters in the Database.
    solver : pycalphad.core.solver.SolverBase
        Instance of a solver that is used to calculate local equilibria.
        Defaults to a pycalphad.core.solver.InteriorPointSolver.
    callables : dict, optional
        Pre-computed callable functions for equilibrium calculation.

    Returns
    -------
    Structured equilibrium calculation, or Dask graph if scheduler=None.

    Examples
    --------
    None yet.
    """
    if not broadcast:
        raise NotImplementedError('Broadcasting cannot yet be disabled')
    comps = sorted(unpack_components(dbf, comps))
    phases = unpack_phases(phases) or sorted(dbf.phases.keys())
    # remove phases that cannot be active
    list_of_possible_phases = filter_phases(dbf, comps)
    active_phases = sorted(set(list_of_possible_phases).intersection(set(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))
    if isinstance(comps, (str, v.Species)):
        comps = [comps]
    if len(set(comps) - set(dbf.species)) > 0:
        raise EquilibriumError('Components not found in database: {}'
                               .format(','.join([c.name for c in (set(comps) - set(dbf.species))])))
    calc_opts = calc_opts if calc_opts is not None else dict()
    solver = solver if solver is not None else InteriorPointSolver(verbose=verbose)
    parameters = parameters if parameters is not None else dict()
    if isinstance(parameters, dict):
        parameters = OrderedDict(sorted(parameters.items(), key=str))
    models = instantiate_models(dbf, comps, active_phases, model=model, parameters=parameters)
    # Temporary solution until constraint system improves
    if conditions.get(v.N) is None:
        conditions[v.N] = 1
    if np.any(np.array(conditions[v.N]) != 1):
        raise ConditionError('N!=1 is not yet supported, got N={}'.format(conditions[v.N]))
    # Modify conditions values to be within numerical limits, e.g., X(AL)=0
    # Also wrap single-valued conditions with lists
    conds = _adjust_conditions(conditions)

    for cond in conds.keys():
        if isinstance(cond, (v.Composition, v.ChemicalPotential)) and cond.species not in comps:
            raise ConditionError('{} refers to non-existent component'.format(cond))
    state_variables = sorted(get_state_variables(models=models, conds=conds), key=str)
    str_conds = OrderedDict((str(key), value) for key, value in conds.items())
    num_calcs = np.prod([len(i) for i in str_conds.values()])
    components = [x for x in sorted(comps)]
    desired_active_pure_elements = [list(x.constituents.keys()) for x in components]
    desired_active_pure_elements = [el.upper() for constituents in desired_active_pure_elements for el in constituents]
    pure_elements = sorted(set([x for x in desired_active_pure_elements if x != 'VA']))
    if verbose:
        print('Components:', ' '.join([str(x) for x in comps]))
        print('Phases:', end=' ')
    output = output if output is not None else 'GM'
    output = output if isinstance(output, (list, tuple, set)) else [output]
    output = set(output)
    output |= {'GM'}
    output = sorted(output)
    need_hessians = any(type(c) in v.CONDITIONS_REQUIRING_HESSIANS for c in conds.keys())
    phase_records = build_phase_records(dbf, comps, active_phases, conds, models,
                                        output='GM', callables=callables,
                                        parameters=parameters, verbose=verbose,
                                        build_gradients=True, build_hessians=need_hessians)
    if verbose:
        print('[done]', end='\n')

    # 'calculate' accepts conditions through its keyword arguments
    grid_opts = calc_opts.copy()
    statevar_strings = [str(x) for x in state_variables]
    grid_opts.update({key: value for key, value in str_conds.items() if key in statevar_strings})
    if 'pdens' not in grid_opts:
        grid_opts['pdens'] = 500
    grid = delayed(calculate, pure=False)(dbf, comps, active_phases,
                                          model=models, fake_points=True,
                                          callables=callables, output='GM',
                                          parameters=parameters, **grid_opts)
    coord_dict = str_conds.copy()
    coord_dict['vertex'] = np.arange(
        len(pure_elements) + 1)  # +1 is to accommodate the degenerate degree of freedom at the invariant reactions
    coord_dict['component'] = pure_elements
    grid_shape = tuple(len(x) for x in conds.values()) + (len(pure_elements)+1,)
    properties = delayed(starting_point, pure=False)(conds, state_variables, phase_records, grid)
    conditions_per_chunk_per_axis = 2
    if num_calcs > 1:
        # Generate slices of 'properties'
        slices = []
        for val in grid_shape[:-1]:
            idx_arr = list(range(val))
            num_chunks = int(np.floor(val/conditions_per_chunk_per_axis))
            if num_chunks > 0:
                cond_slices = [x for x in np.array_split(np.asarray(idx_arr), num_chunks) if len(x) > 0]
            else:
                cond_slices = [idx_arr]
            slices.append(cond_slices)
        chunk_dims = [len(slc) for slc in slices]
        chunk_grid = np.array(np.unravel_index(np.arange(np.prod(chunk_dims)), chunk_dims)).T
        res = []
        for chunk in chunk_grid:
            prop_slice = properties[OrderedDict(list(zip(str_conds.keys(),
                                                         [np.atleast_1d(sl)[ch] for ch, sl in zip(chunk, slices)])))]
            job = delayed(_solve_eq_at_conditions, pure=False)(comps, prop_slice, phase_records, grid,
                                                               list(str_conds.keys()), state_variables, verbose, solver=solver)
            res.append(job)
        properties = delayed(_merge_property_slices, pure=False)(properties, chunk_grid, slices, list(str_conds.keys()), res)
    else:
        # Single-process job; don't create child processes
        properties = delayed(_solve_eq_at_conditions, pure=False)(comps, properties, phase_records, grid,
                                                                  list(str_conds.keys()), state_variables, verbose, solver=solver)

    # Compute equilibrium values of any additional user-specified properties
    # We already computed these properties so don't recompute them
    output = sorted(set(output) - {'GM', 'MU'})
    for out in output:
        if (out is None) or (len(out) == 0):
            continue
        # TODO: How do we know if a specified property should be per_phase or not?
        # For now, we make a best guess
        if (out == 'degree_of_ordering') or (out == 'DOO'):
            per_phase = True
        else:
            per_phase = False
        eqcal = delayed(_eqcalculate, pure=False)(dbf, comps, active_phases, conditions, out,
                                                  data=properties, per_phase=per_phase,
                                                  callables=callables,
                                                  parameters=parameters,
                                                  model=models, **calc_opts)
        properties = delayed(properties.merge, pure=False)(eqcal, compat='equals')
    if scheduler is not None:
        properties = dask.compute(properties, scheduler=scheduler)[0]
    properties.attrs['created'] = datetime.utcnow().isoformat()
    if len(kwargs) > 0:
        warnings.warn('The following equilibrium keyword arguments were passed, but unused:\n{}'.format(kwargs))
    return properties
Esempio n. 7
0
def build_phase_records(dbf, comps, phases, conds, models, output='GM',
                        callables=None, parameters=None, verbose=False,
                        build_gradients=False, build_hessians=False
                        ):
    """
    Combine compiled callables and callables from conditions into PhaseRecords.

    Parameters
    ----------
    dbf : Database
        A Database object
    comps : list
        List of component names
    phases : list
        List of phase names
    conds : dict or None
        Conditions for calculation
    models : dict
        Dictionary of {'phase_name': Model()}
    parameters : dict, optional
        Maps SymPy Symbol to numbers, for overriding the values of parameters in the Database.
    callables : dict, optional
        Pre-computed callables. If None are passed, they will be built.
        Maps {'output' -> {'function' -> {'phase_name' -> AutowrapFunction()}}
    output : str
        Output property of the particular Model to sample
    verbose : bool, optional
        Print the name of the phase when its callables are built
    build_gradients : bool
        Whether or not to build gradient functions. Defaults to False. Only
        takes effect if callables are not passed.
    build_hessians : bool
        Whether or not to build Hessian functions. Defaults to False. Only
        takes effect if callables are not passed.

    Returns
    -------
    dict
        Dictionary mapping phase names to PhaseRecord instances.

    Notes
    -----
    If callables are passed, don't rebuild them. This means that the callables
    are not checked for incompatibility. Users of build_callables are
    responsible for ensuring that the state variables, parameters and models
    used to construct the callables are compatible with the ones used to
    build the constraints and phase records.

    """
    parameters = parameters if parameters is not None else {}
    callables = callables if callables is not None else {}
    _constraints = {
        'internal_cons': {},
        'internal_jac': {},
        'internal_cons_hess': {},
        'mp_cons': {},
        'mp_jac': {},
    }
    phase_records = {}
    state_variables = sorted(get_state_variables(models=models, conds=conds), key=str)
    param_symbols, param_values = extract_parameters(parameters)

    if callables.get(output) is None:
        callables = build_callables(dbf, comps, phases, models,
                                    parameter_symbols=parameters.keys(), output=output,
                                    additional_statevars=state_variables,
                                    build_gradients=build_gradients,
                                    build_hessians=build_hessians)

    for name in phases:
        mod = models[name]
        site_fracs = mod.site_fractions
        # build constraint functions
        cfuncs = build_constraints(mod, state_variables + site_fracs, conds, parameters=param_symbols)
        _constraints['internal_cons'][name] = cfuncs.internal_cons
        _constraints['internal_jac'][name] = cfuncs.internal_jac
        _constraints['internal_cons_hess'][name] = cfuncs.internal_cons_hess
        _constraints['mp_cons'][name] = cfuncs.multiphase_cons
        _constraints['mp_jac'][name] = cfuncs.multiphase_jac
        num_internal_cons = cfuncs.num_internal_cons
        num_multiphase_cons = cfuncs.num_multiphase_cons

        phase_records[name.upper()] = PhaseRecord(comps, state_variables, site_fracs, param_values,
                                                  callables[output]['callables'][name],
                                                  callables[output]['grad_callables'][name],
                                                  callables[output]['hess_callables'][name],
                                                  callables[output]['massfuncs'][name],
                                                  callables[output]['massgradfuncs'][name],
                                                  callables[output]['masshessfuncs'][name],
                                                  _constraints['internal_cons'][name],
                                                  _constraints['internal_jac'][name],
                                                  _constraints['internal_cons_hess'][name],
                                                  _constraints['mp_cons'][name],
                                                  _constraints['mp_jac'][name],
                                                  num_internal_cons,
                                                  num_multiphase_cons)

        if verbose:
            print(name + ' ')
    return phase_records
Esempio n. 8
0
def build_callables(dbf, comps, phases, models, parameter_symbols=None,
                    output='GM', build_gradients=True, build_hessians=False,
                    additional_statevars=None):
    """
    Create a compiled callables dictionary.

    Parameters
    ----------
    dbf : Database
        A Database object
    comps : list
        List of component names
    phases : list
        List of phase names
    models : dict
        Dictionary of {phase_name: Model subclass}
    parameter_symbols : list, optional
        List of string or SymPy Symbols that will be overridden in the callables.
    output : str, optional
        Output property of the particular Model to sample. Defaults to 'GM'
    build_gradients : bool, optional
        Whether or not to build gradient functions. Defaults to True.
    build_hessians : bool, optional
        Whether or not to build Hessian functions. Defaults to False.
    additional_statevars : set, optional
        State variables to include in the callables that may not be in the models (e.g. from conditions)
    verbose : bool, optional
        Print the name of the phase when its callables are built

    Returns
    -------
    callables : dict
        Dictionary of keyword argument callables to pass to equilibrium.
        Maps {'output' -> {'function' -> {'phase_name' -> AutowrapFunction()}}.

    Notes
    -----
    *All* the state variables used in calculations must be specified.
    If these are not specified as state variables of the models (e.g. often the
    case for v.N), then it must be supplied by the additional_statevars keyword
    argument.

    Examples
    --------
    >>> from pycalphad import Database, equilibrium, variables as v
    >>> from pycalphad.codegen.callables import build_callables
    >>> from pycalphad.core.utils import instantiate_models
    >>> dbf = Database('AL-NI.tdb')
    >>> comps = ['AL', 'NI', 'VA']
    >>> phases = ['LIQUID', 'AL3NI5', 'AL3NI2', 'AL3NI']
    >>> models = instantiate_models(dbf, comps, phases)
    >>> callables = build_callables(dbf, comps, phases, models, additional_statevars={v.P, v.T, v.N})
    >>> 'GM' in callables.keys()
    True
    >>> 'massfuncs' in callables['GM']
    True
    >>> conditions = {v.P: 101325, v.T: 2500, v.X('AL'): 0.2}
    >>> equilibrium(dbf, comps, phases, conditions, callables=callables)
    """
    additional_statevars = set(additional_statevars) if additional_statevars is not None else set()
    parameter_symbols = parameter_symbols if parameter_symbols is not None else []
    parameter_symbols = sorted([wrap_symbol(x) for x in parameter_symbols], key=str)
    comps = sorted(unpack_components(dbf, comps))
    pure_elements = get_pure_elements(dbf, comps)

    _callables = {
        'massfuncs': {},
        'massgradfuncs': {},
        'masshessfuncs': {},
        'callables': {},
        'grad_callables': {},
        'hess_callables': {},
        'internal_cons': {},
        'internal_jac': {},
        'internal_cons_hess': {},
        'mp_cons': {},
        'mp_jac': {},
    }

    state_variables = get_state_variables(models=models)
    state_variables |= additional_statevars
    if state_variables != {v.T, v.P, v.N}:
        warnings.warn("State variables in `build_callables` are not {{N, P, T}}, "
                      "but {}. Be sure you know what you are doing. "
                      "State variables can be added with the `additional_statevars` "
                      "argument.".format(state_variables))
    state_variables = sorted(state_variables, key=str)

    for name in phases:
        mod = models[name]
        site_fracs = mod.site_fractions
        try:
            out = getattr(mod, output)
        except AttributeError:
            raise AttributeError('Missing Model attribute {0} specified for {1}'
                                 .format(output, mod.__class__))

        # Build the callables of the output
        # Only force undefineds to zero if we're not overriding them
        undefs = {x for x in out.free_symbols if not isinstance(x, v.StateVariable)} - set(parameter_symbols)
        undef_vals = repeat(0., len(undefs))
        out = out.xreplace(dict(zip(undefs, undef_vals)))
        build_output = build_functions(out, tuple(state_variables + site_fracs), parameters=parameter_symbols,
                                       include_grad=build_gradients, include_hess=build_hessians)
        cf, gf, hf = build_output.func, build_output.grad, build_output.hess
        _callables['callables'][name] = cf
        _callables['grad_callables'][name] = gf
        _callables['hess_callables'][name] = hf

        # Build the callables for mass
        # TODO: In principle, we should also check for undefs in mod.moles()
        mcf, mgf, mhf = zip(*[build_functions(mod.moles(el), state_variables + site_fracs,
                                              include_obj=True,
                                              include_grad=build_gradients,
                                              include_hess=build_hessians,
                                              parameters=parameter_symbols)
                              for el in pure_elements])

        _callables['massfuncs'][name] = mcf
        _callables['massgradfuncs'][name] = mgf
        _callables['masshessfuncs'][name] = mhf
    return {output: _callables}
Esempio n. 9
0
def equilibrium(dbf,
                comps,
                phases,
                conditions,
                output=None,
                model=None,
                verbose=False,
                broadcast=True,
                calc_opts=None,
                to_xarray=True,
                scheduler='sync',
                parameters=None,
                solver=None,
                callables=None,
                **kwargs):
    """
    Calculate the equilibrium state of a system containing the specified
    components and phases, under the specified conditions.

    Parameters
    ----------
    dbf : Database
        Thermodynamic database containing the relevant parameters.
    comps : list
        Names of components to consider in the calculation.
    phases : list or dict
        Names of phases to consider in the calculation.
    conditions : dict or (list of dict)
        StateVariables and their corresponding value.
    output : str or list of str, optional
        Additional equilibrium model properties (e.g., CPM, HM, etc.) to compute.
        These must be defined as attributes in the Model class of each phase.
    model : Model, a dict of phase names to Model, or a seq of both, optional
        Model class to use for each phase.
    verbose : bool, optional
        Print details of calculations. Useful for debugging.
    broadcast : bool
        If True, broadcast conditions against each other. This will compute all combinations.
        If False, each condition should be an equal-length list (or single-valued).
        Disabling broadcasting is useful for calculating equilibrium at selected conditions,
        when those conditions don't comprise a grid.
    calc_opts : dict, optional
        Keyword arguments to pass to `calculate`, the energy/property calculation routine.
    to_xarray : bool
        Whether to return an xarray Dataset (True, default) or an EquilibriumResult.
    scheduler : Dask scheduler, optional
        Job scheduler for performing the computation.
        If None, return a Dask graph of the computation instead of actually doing it.
    parameters : dict, optional
        Maps SymPy Symbol to numbers, for overriding the values of parameters in the Database.
    solver : pycalphad.core.solver.SolverBase
        Instance of a solver that is used to calculate local equilibria.
        Defaults to a pycalphad.core.solver.InteriorPointSolver.
    callables : dict, optional
        Pre-computed callable functions for equilibrium calculation.

    Returns
    -------
    Structured equilibrium calculation, or Dask graph if scheduler=None.

    Examples
    --------
    None yet.
    """
    if not broadcast:
        raise NotImplementedError('Broadcasting cannot yet be disabled')
    comps = sorted(unpack_components(dbf, comps))
    phases = unpack_phases(phases) or sorted(dbf.phases.keys())
    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))
    if isinstance(comps, (str, v.Species)):
        comps = [comps]
    if len(set(comps) - set(dbf.species)) > 0:
        raise EquilibriumError('Components not found in database: {}'.format(
            ','.join([c.name for c in (set(comps) - set(dbf.species))])))
    calc_opts = calc_opts if calc_opts is not None else dict()
    solver = solver if solver is not None else InteriorPointSolver(
        verbose=verbose)
    parameters = parameters if parameters is not None else dict()
    if isinstance(parameters, dict):
        parameters = OrderedDict(sorted(parameters.items(), key=str))
    models = instantiate_models(dbf,
                                comps,
                                active_phases,
                                model=model,
                                parameters=parameters)
    # Temporary solution until constraint system improves
    if conditions.get(v.N) is None:
        conditions[v.N] = 1
    if np.any(np.array(conditions[v.N]) != 1):
        raise ConditionError('N!=1 is not yet supported, got N={}'.format(
            conditions[v.N]))
    # Modify conditions values to be within numerical limits, e.g., X(AL)=0
    # Also wrap single-valued conditions with lists
    conds = _adjust_conditions(conditions)

    for cond in conds.keys():
        if isinstance(cond,
                      (v.Composition,
                       v.ChemicalPotential)) and cond.species not in comps:
            raise ConditionError(
                '{} refers to non-existent component'.format(cond))
    state_variables = sorted(get_state_variables(models=models, conds=conds),
                             key=str)
    str_conds = OrderedDict((str(key), value) for key, value in conds.items())
    components = [x for x in sorted(comps)]
    desired_active_pure_elements = [
        list(x.constituents.keys()) for x in components
    ]
    desired_active_pure_elements = [
        el.upper() for constituents in desired_active_pure_elements
        for el in constituents
    ]
    pure_elements = sorted(
        set([x for x in desired_active_pure_elements if x != 'VA']))
    if verbose:
        print('Components:', ' '.join([str(x) for x in comps]))
        print('Phases:', end=' ')
    output = output if output is not None else 'GM'
    output = output if isinstance(output, (list, tuple, set)) else [output]
    output = set(output)
    output |= {'GM'}
    output = sorted(output)
    phase_records = build_phase_records(dbf,
                                        comps,
                                        active_phases,
                                        conds,
                                        models,
                                        output='GM',
                                        callables=callables,
                                        parameters=parameters,
                                        verbose=verbose,
                                        build_gradients=True,
                                        build_hessians=True)
    if verbose:
        print('[done]', end='\n')

    # 'calculate' accepts conditions through its keyword arguments
    grid_opts = calc_opts.copy()
    statevar_strings = [str(x) for x in state_variables]
    grid_opts.update({
        key: value
        for key, value in str_conds.items() if key in statevar_strings
    })
    if 'pdens' not in grid_opts:
        grid_opts['pdens'] = 500
    grid = calculate(dbf,
                     comps,
                     active_phases,
                     model=models,
                     fake_points=True,
                     callables=callables,
                     output='GM',
                     parameters=parameters,
                     to_xarray=False,
                     **grid_opts)
    coord_dict = str_conds.copy()
    coord_dict['vertex'] = np.arange(
        len(pure_elements) + 1
    )  # +1 is to accommodate the degenerate degree of freedom at the invariant reactions
    coord_dict['component'] = pure_elements
    properties = starting_point(conds, state_variables, phase_records, grid)
    properties = _solve_eq_at_conditions(comps,
                                         properties,
                                         phase_records,
                                         grid,
                                         list(str_conds.keys()),
                                         state_variables,
                                         verbose,
                                         solver=solver)

    # Compute equilibrium values of any additional user-specified properties
    # We already computed these properties so don't recompute them
    output = sorted(set(output) - {'GM', 'MU'})
    for out in output:
        if (out is None) or (len(out) == 0):
            continue
        # TODO: How do we know if a specified property should be per_phase or not?
        # For now, we make a best guess
        if (out == 'degree_of_ordering') or (out == 'DOO'):
            per_phase = True
        else:
            per_phase = False
        eqcal = _eqcalculate(dbf,
                             comps,
                             active_phases,
                             conditions,
                             out,
                             data=properties,
                             per_phase=per_phase,
                             model=models,
                             callables=callables,
                             parameters=parameters,
                             **calc_opts)
        properties = properties.merge(eqcal, inplace=True, compat='equals')
    if to_xarray:
        properties = properties.get_dataset()
    properties.attrs['created'] = datetime.utcnow().isoformat()
    if len(kwargs) > 0:
        warnings.warn(
            'The following equilibrium keyword arguments were passed, but unused:\n{}'
            .format(kwargs))
    return properties
Esempio n. 10
0
def build_phase_records(dbf,
                        comps,
                        phases,
                        conds,
                        models,
                        output='GM',
                        callables=None,
                        parameters=None,
                        verbose=False,
                        build_gradients=False,
                        build_hessians=False):
    """
    Combine compiled callables and callables from conditions into PhaseRecords.

    Parameters
    ----------
    dbf : Database
        A Database object
    comps : list
        List of component names
    phases : list
        List of phase names
    conds : dict or None
        Conditions for calculation
    models : dict
        Dictionary of {'phase_name': Model()}
    parameters : dict, optional
        Maps SymPy Symbol to numbers, for overriding the values of parameters in the Database.
    callables : dict, optional
        Pre-computed callables. If None are passed, they will be built.
        Maps {'output' -> {'function' -> {'phase_name' -> AutowrapFunction()}}
    output : str
        Output property of the particular Model to sample
    verbose : bool, optional
        Print the name of the phase when its callables are built
    build_gradients : bool
        Whether or not to build gradient functions. Defaults to False. Only
        takes effect if callables are not passed.
    build_hessians : bool
        Whether or not to build Hessian functions. Defaults to False. Only
        takes effect if callables are not passed.

    Returns
    -------
    dict
        Dictionary mapping phase names to PhaseRecord instances.

    Notes
    -----
    If callables are passed, don't rebuild them. This means that the callables
    are not checked for incompatibility. Users of build_callables are
    responsible for ensuring that the state variables, parameters and models
    used to construct the callables are compatible with the ones used to
    build the constraints and phase records.

    """
    parameters = parameters if parameters is not None else {}
    callables = callables if callables is not None else {}
    _constraints = {
        'internal_cons_func': {},
        'internal_cons_jac': {},
        'internal_cons_hess': {},
        'multiphase_cons_func': {},
        'multiphase_cons_jac': {},
        'multiphase_cons_hess': {}
    }
    phase_records = {}
    state_variables = sorted(get_state_variables(models=models, conds=conds),
                             key=str)
    param_symbols, param_values = extract_parameters(parameters)

    if callables.get(output) is None:
        callables = build_callables(dbf,
                                    comps,
                                    phases,
                                    models,
                                    parameter_symbols=parameters.keys(),
                                    output=output,
                                    additional_statevars=state_variables,
                                    build_gradients=build_gradients,
                                    build_hessians=build_hessians)

    for name in phases:
        mod = models[name]
        site_fracs = mod.site_fractions
        # build constraint functions
        cfuncs = build_constraints(mod,
                                   state_variables + site_fracs,
                                   conds,
                                   parameters=param_symbols)
        _constraints['internal_cons_func'][name] = cfuncs.internal_cons_func
        _constraints['internal_cons_jac'][name] = cfuncs.internal_cons_jac
        _constraints['internal_cons_hess'][name] = cfuncs.internal_cons_hess
        _constraints['multiphase_cons_func'][
            name] = cfuncs.multiphase_cons_func
        _constraints['multiphase_cons_jac'][name] = cfuncs.multiphase_cons_jac
        _constraints['multiphase_cons_hess'][
            name] = cfuncs.multiphase_cons_hess
        num_internal_cons = cfuncs.num_internal_cons
        num_multiphase_cons = cfuncs.num_multiphase_cons

        phase_records[name.upper()] = PhaseRecord(
            comps, state_variables, site_fracs, param_values,
            callables[output]['callables'][name],
            callables[output]['grad_callables'][name],
            callables[output]['hess_callables'][name],
            callables[output]['massfuncs'][name],
            callables[output]['massgradfuncs'][name],
            callables[output]['masshessfuncs'][name],
            _constraints['internal_cons_func'][name],
            _constraints['internal_cons_jac'][name],
            _constraints['internal_cons_hess'][name],
            _constraints['multiphase_cons_func'][name],
            _constraints['multiphase_cons_jac'][name],
            _constraints['multiphase_cons_hess'][name], num_internal_cons,
            num_multiphase_cons)

        if verbose:
            print(name + ' ')
    return phase_records
Esempio n. 11
0
def build_callables(dbf,
                    comps,
                    phases,
                    models,
                    parameter_symbols=None,
                    output='GM',
                    build_gradients=True,
                    build_hessians=False,
                    additional_statevars=None):
    """
    Create a compiled callables dictionary.

    Parameters
    ----------
    dbf : Database
        A Database object
    comps : list
        List of component names
    phases : list
        List of phase names
    models : dict
        Dictionary of {phase_name: Model subclass}
    parameter_symbols : list, optional
        List of string or SymPy Symbols that will be overridden in the callables.
    output : str, optional
        Output property of the particular Model to sample. Defaults to 'GM'
    build_gradients : bool, optional
        Whether or not to build gradient functions. Defaults to True.
    build_hessians : bool, optional
        Whether or not to build Hessian functions. Defaults to False.
    additional_statevars : set, optional
        State variables to include in the callables that may not be in the models (e.g. from conditions)
    verbose : bool, optional
        Print the name of the phase when its callables are built

    Returns
    -------
    callables : dict
        Dictionary of keyword argument callables to pass to equilibrium.
        Maps {'output' -> {'function' -> {'phase_name' -> AutowrapFunction()}}.

    Notes
    -----
    *All* the state variables used in calculations must be specified.
    If these are not specified as state variables of the models (e.g. often the
    case for v.N), then it must be supplied by the additional_statevars keyword
    argument.

    Examples
    --------
    >>> from pycalphad import Database, equilibrium, variables as v
    >>> from pycalphad.codegen.callables import build_callables
    >>> from pycalphad.core.utils import instantiate_models
    >>> dbf = Database('AL-NI.tdb')
    >>> comps = ['AL', 'NI', 'VA']
    >>> phases = ['LIQUID', 'AL3NI5', 'AL3NI2', 'AL3NI']
    >>> models = instantiate_models(dbf, comps, phases)
    >>> callables = build_callables(dbf, comps, phases, models, additional_statevars={v.P, v.T, v.N})
    >>> 'GM' in callables.keys()
    True
    >>> 'massfuncs' in callables['GM']
    True
    >>> conditions = {v.P: 101325, v.T: 2500, v.X('AL'): 0.2}
    >>> equilibrium(dbf, comps, phases, conditions, callables=callables)
    """
    additional_statevars = set(
        additional_statevars) if additional_statevars is not None else set()
    parameter_symbols = parameter_symbols if parameter_symbols is not None else []
    parameter_symbols = sorted([wrap_symbol(x) for x in parameter_symbols],
                               key=str)
    comps = sorted(unpack_components(dbf, comps))
    pure_elements = get_pure_elements(dbf, comps)

    _callables = {
        'massfuncs': {},
        'massgradfuncs': {},
        'masshessfuncs': {},
        'callables': {},
        'grad_callables': {},
        'hess_callables': {},
        'internal_cons_func': {},
        'internal_cons_jac': {},
        'internal_cons_hess': {},
        'multiphase_cons_func': {},
        'multiphase_cons_jac': {},
        'multiphase_cons_hess': {}
    }

    state_variables = get_state_variables(models=models)
    state_variables |= additional_statevars
    if state_variables != {v.T, v.P, v.N}:
        warnings.warn(
            "State variables in `build_callables` are not {{N, P, T}}, but {}. This can lead to incorrectly "
            "calculated values if the state variables used to call the generated functions do not match the "
            "state variables used to create them. State variables can be added with the "
            "`additional_statevars` argument.".format(state_variables))
    state_variables = sorted(state_variables, key=str)

    for name in phases:
        mod = models[name]
        site_fracs = mod.site_fractions
        try:
            out = getattr(mod, output)
        except AttributeError:
            raise AttributeError(
                'Missing Model attribute {0} specified for {1}'.format(
                    output, mod.__class__))

        # Build the callables of the output
        # Only force undefineds to zero if we're not overriding them
        undefs = {
            x
            for x in out.free_symbols if not isinstance(x, v.StateVariable)
        } - set(parameter_symbols)
        undef_vals = repeat(0., len(undefs))
        out = out.xreplace(dict(zip(undefs, undef_vals)))
        build_output = build_functions(out,
                                       tuple(state_variables + site_fracs),
                                       parameters=parameter_symbols,
                                       include_grad=build_gradients,
                                       include_hess=build_hessians)
        cf, gf, hf = build_output.func, build_output.grad, build_output.hess
        _callables['callables'][name] = cf
        _callables['grad_callables'][name] = gf
        _callables['hess_callables'][name] = hf

        # Build the callables for mass
        # TODO: In principle, we should also check for undefs in mod.moles()
        mcf, mgf, mhf = zip(*[
            build_functions(mod.moles(el),
                            state_variables + site_fracs,
                            include_obj=True,
                            include_grad=build_gradients,
                            include_hess=build_hessians,
                            parameters=parameter_symbols)
            for el in pure_elements
        ])

        _callables['massfuncs'][name] = mcf
        _callables['massgradfuncs'][name] = mgf
        _callables['masshessfuncs'][name] = mhf
    return {output: _callables}
Esempio n. 12
0
def build_phase_records(dbf,
                        comps,
                        phases,
                        state_variables,
                        models,
                        output='GM',
                        callables=None,
                        parameters=None,
                        verbose=False,
                        build_gradients=True,
                        build_hessians=True):
    """
    Combine compiled callables and callables from conditions into PhaseRecords.

    Parameters
    ----------
    dbf : Database
        A Database object
    comps : List[Union[str, v.Species]]
        List of active pure elements or species.
    phases : list
        List of phase names
    state_variables : Iterable[v.StateVariable]
        State variables used to produce the generated functions.
    models : Mapping[str, Model]
        Mapping of phase names to model instances
    parameters : dict, optional
        Maps SymEngine Symbol to numbers, for overriding the values of parameters in the Database.
    callables : dict, optional
        Pre-computed callables. If None are passed, they will be built.
        Maps {'output' -> {'function' -> {'phase_name' -> AutowrapFunction()}}
    output : str
        Output property of the particular Model to sample
    verbose : bool, optional
        Print the name of the phase when its callables are built
    build_gradients : bool
        Whether or not to build gradient functions. Defaults to False. Only
        takes effect if callables are not passed.
    build_hessians : bool
        Whether or not to build Hessian functions. Defaults to False. Only
        takes effect if callables are not passed.

    Returns
    -------
    dict
        Dictionary mapping phase names to PhaseRecord instances.

    Notes
    -----
    If callables are passed, don't rebuild them. This means that the callables
    are not checked for incompatibility. Users of build_callables are
    responsible for ensuring that the state variables, parameters and models
    used to construct the callables are compatible with the ones used to
    build the constraints and phase records.

    """
    comps = sorted(unpack_components(dbf, comps))
    parameters = parameters if parameters is not None else {}
    callables = callables if callables is not None else {}
    _constraints = {
        'internal_cons_func': {},
        'internal_cons_jac': {},
        'internal_cons_hess': {},
    }
    phase_records = {}
    state_variables = sorted(get_state_variables(models=models,
                                                 conds=state_variables),
                             key=str)
    param_symbols, param_values = extract_parameters(parameters)

    if callables.get(output) is None:
        callables = build_callables(dbf,
                                    comps,
                                    phases,
                                    models,
                                    parameter_symbols=parameters.keys(),
                                    output=output,
                                    additional_statevars=state_variables,
                                    build_gradients=False,
                                    build_hessians=False)
    # Temporary solution. PhaseRecord needs rework: https://github.com/pycalphad/pycalphad/pull/329#discussion_r634579356
    formulacallables = build_callables(dbf,
                                       comps,
                                       phases,
                                       models,
                                       parameter_symbols=parameters.keys(),
                                       output='G',
                                       additional_statevars=state_variables,
                                       build_gradients=build_gradients,
                                       build_hessians=build_hessians)

    # If a vector of parameters is specified, only pass the first row to the PhaseRecord
    # Future callers of PhaseRecord.obj_parameters_2d() can pass the full param_values array as an argument
    if len(param_values.shape) > 1:
        param_values = param_values[0]

    for name in phases:
        mod = models[name]
        site_fracs = mod.site_fractions
        # build constraint functions
        cfuncs = build_constraints(mod,
                                   state_variables + site_fracs,
                                   parameters=param_symbols)
        _constraints['internal_cons_func'][name] = cfuncs.internal_cons_func
        _constraints['internal_cons_jac'][name] = cfuncs.internal_cons_jac
        _constraints['internal_cons_hess'][name] = cfuncs.internal_cons_hess
        num_internal_cons = cfuncs.num_internal_cons

        phase_records[name.upper()] = PhaseRecord(
            comps, state_variables, site_fracs, param_values,
            callables[output]['callables'][name],
            formulacallables['G']['callables'][name],
            formulacallables['G']['grad_callables'][name],
            formulacallables['G']['hess_callables'][name],
            callables[output]['massfuncs'][name],
            formulacallables['G']['formulamolefuncs'][name],
            formulacallables['G']['formulamolegradfuncs'][name],
            formulacallables['G']['formulamolehessfuncs'][name],
            _constraints['internal_cons_func'][name],
            _constraints['internal_cons_jac'][name],
            _constraints['internal_cons_hess'][name], num_internal_cons)

        if verbose:
            print(name + ' ')
    return phase_records