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)
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
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)
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)
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
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
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
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}
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
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
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}
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