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 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 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, 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