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 get_thermochemical_data(dbf, comps, phases, datasets, weight_dict=None, symbols_to_fit=None): """ Parameters ---------- dbf : pycalphad.Database Database to consider comps : list List of active component names phases : list List of phases to consider datasets : espei.utils.PickleableTinyDB Datasets that contain single phase data weight_dict : dict Dictionary of weights for each data type, e.g. {'HM': 200, 'SM': 2} symbols_to_fit : list Parameters to fit. Used to build the models and PhaseRecords. Returns ------- list List of data dictionaries to iterate over """ # phase by phase, then property by property, then by model exclusions if weight_dict is None: weight_dict = {} if symbols_to_fit is not None: symbols_to_fit = sorted(symbols_to_fit) else: symbols_to_fit = database_symbols_to_fit(dbf) # estimated from NIST TRC uncertainties property_std_deviation = { 'HM': 500.0 / weight_dict.get('HM', 1.0), # J/mol 'SM': 0.2 / weight_dict.get('SM', 1.0), # J/K-mol 'CPM': 0.2 / weight_dict.get('CPM', 1.0), # J/K-mol } properties = [ 'HM_FORM', 'SM_FORM', 'CPM_FORM', 'HM_MIX', 'SM_MIX', 'CPM_MIX' ] ref_states = [] for el in get_pure_elements(dbf, comps): ref_state = ReferenceState(el, dbf.refstates[el]['phase']) ref_states.append(ref_state) all_data_dicts = [] for phase_name in phases: for prop in properties: desired_data = get_prop_data( comps, phase_name, prop, datasets, additional_query=(where('solver').exists())) if len(desired_data) == 0: continue unique_exclusions = set([ tuple(sorted(d.get('excluded_model_contributions', []))) for d in desired_data ]) for exclusion in unique_exclusions: data_dict = { 'phase_name': phase_name, 'prop': prop, # needs the following keys to be added: # species, calculate_dict, phase_records, model, output, weights } # get all the data with these model exclusions if exclusion == tuple([]): exc_search = ( ~where('excluded_model_contributions').exists()) & ( where('solver').exists()) else: exc_search = (where('excluded_model_contributions').test( lambda x: tuple(sorted(x)) == exclusion)) & ( where('solver').exists()) curr_data = get_prop_data(comps, phase_name, prop, datasets, additional_query=exc_search) calculate_dict = get_prop_samples(dbf, comps, phase_name, curr_data) mod = Model(dbf, comps, phase_name, parameters=symbols_to_fit) if prop.endswith('_FORM'): output = ''.join(prop.split('_')[:-1]) + 'R' mod.shift_reference_state( ref_states, dbf, contrib_mods={e: sympy.S.Zero for e in exclusion}) else: output = prop for contrib in exclusion: mod.models[contrib] = sympy.S.Zero mod.reference_model.models[contrib] = sympy.S.Zero species = sorted(unpack_components(dbf, comps), key=str) data_dict['species'] = species model = {phase_name: mod} statevar_dict = { getattr(v, c, None): vals for c, vals in calculate_dict.items() if isinstance(getattr(v, c, None), v.StateVariable) } statevar_dict = OrderedDict( sorted(statevar_dict.items(), key=lambda x: str(x[0]))) str_statevar_dict = OrderedDict( (str(k), vals) for k, vals in statevar_dict.items()) phase_records = build_phase_records( dbf, species, [phase_name], statevar_dict, model, output=output, parameters={s: 0 for s in symbols_to_fit}, build_gradients=False, build_hessians=False) data_dict['str_statevar_dict'] = str_statevar_dict data_dict['phase_records'] = phase_records data_dict['calculate_dict'] = calculate_dict data_dict['model'] = model data_dict['output'] = output data_dict['weights'] = np.array( property_std_deviation[prop.split('_')[0]]) / np.array( calculate_dict.pop('weights')) all_data_dicts.append(data_dict) return all_data_dicts
def shift_reference_state(self, reference_states, dbe, contrib_mods=None, output=('GM', 'HM', 'SM', 'CPM'), fmt_str="{}R"): """ Add new attributes for calculating properties w.r.t. an arbitrary pure element reference state. Parameters ---------- reference_states : Iterable of ReferenceState Pure element ReferenceState objects. Must include all the pure elements defined in the current model. dbe : Database Database containing the relevant parameters. output : Iterable, optional Parameters to subtract the ReferenceState from, defaults to ('GM', 'HM', 'SM', 'CPM'). contrib_mods : Mapping, optional Map of {model contribution: new value}. Used to adjust the pure reference model contributions at the time this is called, since the `models` attribute of the pure element references are effectively static after calling this method. fmt_str : str, optional String that will be formatted with the `output` parameter name. Defaults to "{}R", e.g. the transformation of 'GM' -> 'GMR' """ # Error checking # We ignore the case that the ref states are overspecified (same ref states can be used in different models w/ different active pure elements) model_pure_elements = set(get_pure_elements(dbe, self.components)) refstate_pure_elements_list = get_pure_elements(dbe, [r.species for r in reference_states]) refstate_pure_elements = set(refstate_pure_elements_list) if len(refstate_pure_elements_list) != len(refstate_pure_elements): raise DofError("Multiple ReferenceState objects exist for at least one pure element: {}".format(refstate_pure_elements_list)) if not refstate_pure_elements.issuperset(model_pure_elements): raise DofError("Non-existent ReferenceState for pure components {} in {} for {}".format(model_pure_elements.difference(refstate_pure_elements), self, self.phase_name)) contrib_mods = contrib_mods or {} def _pure_element_test(constituent_array): all_comps = set() for sublattice in constituent_array: if len(sublattice) != 1: return False all_comps.add(sublattice[0].name) pure_els = all_comps.intersection(model_pure_elements) return len(pure_els) == 1 # Remove interactions from a copy of the Database, avoids any element/VA interactions. endmember_only_dbe = copy.deepcopy(dbe) endmember_only_dbe._parameters.remove(~where('constituent_array').test(_pure_element_test)) reference_dict = {out: [] for out in output} # output: terms list for ref_state in reference_states: if ref_state.species not in self.components: continue mod_pure = self.__class__(endmember_only_dbe, [ref_state.species, v.Species('VA')], ref_state.phase_name, parameters=self._parameters_arg) # apply the modifications to the Models for contrib, new_val in contrib_mods.items(): mod_pure.models[contrib] = new_val # set all the free site fractions to one, this should effectively delete any mixing terms spuriously added, e.g. idmix site_frac_subs = {sf: 1 for sf in mod_pure.ast.free_symbols if isinstance(sf, v.SiteFraction)} for mod_key, mod_val in mod_pure.models.items(): mod_pure.models[mod_key] = mod_val.subs(site_frac_subs) moles = self.moles(ref_state.species) # get the output property of interest, substitute the fixed state variables (e.g. T=298.15) and add the pure element moles weighted term to the list of terms # substitution of fixed state variables has to happen after getting the attribute in case there are any derivatives involving that state variable for out in reference_dict.keys(): mod_out = getattr(mod_pure, out).subs(ref_state.fixed_statevars) reference_dict[out].append(mod_out*moles) # set the attribute on the class for out, terms in reference_dict.items(): reference_contrib = Add(*terms) referenced_value = getattr(self, out) - reference_contrib setattr(self, fmt_str.format(out), referenced_value)
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 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_callables(dbf, comps, phases, model=None, parameters=None, callables=None, output='GM', build_gradients=True, verbose=False): """ Create dictionaries of callable dictionaries and PhaseRecords. Parameters ---------- dbf : Database A Database object comps : list List of component names phases : list List of phase names model : dict or type Dictionary of {phase_name: Model subclass} or a type corresponding to a Model subclass. Defaults to ``Model``. parameters : dict, optional Maps SymPy Symbol to numbers, for overriding the values of parameters in the Database. callables : dict, optional Pre-computed callables output : str Output property of the particular Model to sample build_gradients : bool Whether or not to build gradient functions. Defaults to True. verbose : bool Print the name of the phase when its callables are built Returns ------- callables : dict Dictionary of keyword argument callables to pass to equilibrium. Example ------- >>> dbf = Database('AL-NI.tdb') >>> comps = ['AL', 'NI', 'VA'] >>> phases = ['FCC_L12', 'BCC_B2', 'LIQUID', 'AL3NI5', 'AL3NI2', 'AL3NI'] >>> callables = build_callables(dbf, comps, phases) >>> equilibrium(dbf, comps, phases, conditions, **callables) """ parameters = parameters if parameters is not None else {} if len(parameters) > 0: param_symbols, param_values = zip(*[(key, val) for key, val in sorted( parameters.items(), key=operator.itemgetter(0))]) param_values = np.asarray(param_values, dtype=np.float64) else: param_symbols = [] param_values = np.empty(0) comps = sorted(unpack_components(dbf, comps)) pure_elements = get_pure_elements(dbf, comps) callables = callables if callables is not None else {} _callables = { 'massfuncs': {}, 'massgradfuncs': {}, 'callables': {}, 'grad_callables': {} } models = unpack_kwarg(model, default_arg=Model) param_symbols = [wrap_symbol(sym) for sym in param_symbols] phase_records = {} # create models for name in phases: mod = models[name] if isinstance(mod, type): models[name] = mod = mod(dbf, comps, name, parameters=param_symbols) site_fracs = mod.site_fractions variables = sorted(site_fracs, key=str) try: out = getattr(mod, output) except AttributeError: raise AttributeError( 'Missing Model attribute {0} specified for {1}'.format( output, mod.__class__)) if callables.get('callables', {}).get(name, False) and \ ((not build_gradients) or callables.get('grad_callables',{}).get(name, False)): _callables['callables'][name] = callables['callables'][name] if build_gradients: _callables['grad_callables'][name] = callables[ 'grad_callables'][name] else: _callables['grad_callables'][name] = None else: # 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(param_symbols) undef_vals = repeat(0., len(undefs)) out = out.xreplace(dict(zip(undefs, undef_vals))) build_output = build_functions(out, tuple([v.P, v.T] + site_fracs), parameters=param_symbols, include_grad=build_gradients) if build_gradients: cf, gf = build_output else: cf = build_output gf = None _callables['callables'][name] = cf _callables['grad_callables'][name] = gf if callables.get('massfuncs', {}).get(name, False) and \ ((not build_gradients) or callables.get('massgradfuncs', {}).get(name, False)): _callables['massfuncs'][name] = callables['massfuncs'][name] if build_gradients: _callables['massgradfuncs'][name] = callables['massgradfuncs'][ name] else: _callables['massgradfuncs'][name] = None else: # Build the callables for mass # TODO: In principle, we should also check for undefs in mod.moles() if build_gradients: mcf, mgf = zip(*[ build_functions(mod.moles(el), [v.P, v.T] + variables, include_obj=True, include_grad=build_gradients, parameters=param_symbols) for el in pure_elements ]) else: mcf = tuple([ build_functions(mod.moles(el), [v.P, v.T] + variables, include_obj=True, include_grad=build_gradients, parameters=param_symbols) for el in pure_elements ]) mgf = None _callables['massfuncs'][name] = mcf _callables['massgradfuncs'][name] = mgf if not callables.get('phase_records', {}).get(name, False): pv = param_values else: # Copy parameter values from old PhaseRecord, if it exists pv = callables['phase_records'][name].parameters phase_records[name.upper()] = PhaseRecord_from_cython( comps, variables, np.array(dbf.phases[name].sublattices, dtype=np.float), pv, _callables['callables'][name], _callables['grad_callables'][name], _callables['massfuncs'][name], _callables['massgradfuncs'][name]) if verbose: print(name + ' ') # Update PhaseRecords with any user-specified parameter values, in case we skipped the build phase # We assume here that users know what they are doing, and pass compatible combinations of callables and parameters # See discussion in gh-192 for details if len(param_values) > 0: for prx_name in phase_records: if len(phase_records[prx_name].parameters) != len(param_values): raise ValueError( 'User-specified callables and parameters are incompatible') phase_records[prx_name].parameters = param_values # finally, add the models to the callables _callables['model'] = dict(models) _callables['phase_records'] = phase_records return _callables
def calculate(dbf, comps, phases, mode=None, output='GM', fake_points=False, broadcast=True, parameters=None, to_xarray=True, phase_records=None, **kwargs): """ Sample the property surface of 'output' containing the specified components and phases. Model parameters are taken from 'dbf' and any state variables (T, P, etc.) can be specified as keyword arguments. Parameters ---------- dbf : Database Thermodynamic database containing the relevant parameters. comps : str or sequence Names of components to consider in the calculation. phases : str or sequence Names of phases to consider in the calculation. mode : string, optional See 'make_callable' docstring for details. output : string, optional Model attribute to sample. fake_points : bool, optional (Default: False) If True, the first few points of the output surface will be fictitious points used to define an equilibrium hyperplane guaranteed to be above all the other points. This is used for convex hull computations. broadcast : bool, optional If True, broadcast given state variable lists against each other to create a grid. If False, assume state variables are given as equal-length lists. points : ndarray or a dict of phase names to ndarray, optional Columns of ndarrays must be internal degrees of freedom (site fractions), sorted. If this is not specified, points will be generated automatically. pdens : int, a dict of phase names to int, or a seq of both, optional Number of points to sample per degree of freedom. Default: 2000; Default when called from equilibrium(): 500 model : Model, a dict of phase names to Model, or a seq of both, optional Model class to use for each phase. sampler : callable, a dict of phase names to callable, or a seq of both, optional Function to sample phase constitution space. Must have same signature as 'pycalphad.core.utils.point_sample' grid_points : bool, a dict of phase names to bool, or a seq of both, optional (Default: True) Whether to add evenly spaced points between end-members. The density of points is determined by 'pdens' parameters : dict, optional Maps SymEngine Symbol to numbers, for overriding the values of parameters in the Database. phase_records : Optional[Mapping[str, PhaseRecord]] Mapping of phase names to PhaseRecord objects. Must include all active phases. The `model` argument must be a mapping of phase names to instances of Model objects. Callers must take care that the PhaseRecord objects were created with the same `output` as passed to `calculate`. Returns ------- Dataset of the sampled attribute as a function of state variables Examples -------- None yet. """ # Here we check for any keyword arguments that are special, i.e., # there may be keyword arguments that aren't state variables pdens_dict = unpack_kwarg(kwargs.pop('pdens', 2000), default_arg=2000) points_dict = unpack_kwarg(kwargs.pop('points', None), default_arg=None) callables = kwargs.pop('callables', {}) sampler_dict = unpack_kwarg(kwargs.pop('sampler', None), default_arg=None) fixedgrid_dict = unpack_kwarg(kwargs.pop('grid_points', True), default_arg=True) model = kwargs.pop('model', None) parameters = parameters or dict() if isinstance(parameters, dict): parameters = OrderedDict(sorted(parameters.items(), key=str)) if isinstance(phases, str): phases = [phases] if isinstance(comps, (str, v.Species)): comps = [comps] comps = sorted(unpack_components(dbf, comps)) if points_dict is None and broadcast is False: raise ValueError( 'The \'points\' keyword argument must be specified if broadcast=False is also given.' ) nonvacant_components = [x for x in sorted(comps) if x.number_of_atoms > 0] nonvacant_elements = get_pure_elements(dbf, comps) all_phase_data = [] largest_energy = 1e10 # Consider only the active phases list_of_possible_phases = filter_phases(dbf, comps) if len(list_of_possible_phases) == 0: raise ConditionError( 'There are no phases in the Database that can be active with components {0}' .format(comps)) active_phases = 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(output, (list, tuple, set)): raise NotImplementedError( 'Only one property can be specified in calculate() at a time') output = output if output is not None else 'GM' # Implicitly add 'N' state variable as a string to keyword arguements if it's not passed if kwargs.get('N') is None: kwargs['N'] = 1 if np.any(np.array(kwargs['N']) != 1): raise ConditionError('N!=1 is not yet supported, got N={}'.format( kwargs['N'])) # TODO: conditions dict of StateVariable instances should become part of the calculate API statevar_strings = [ sv for sv in kwargs.keys() if getattr(v, sv) is not None ] # If we don't do this, sympy will get confused during substitution statevar_dict = dict((v.StateVariable(key), unpack_condition(value)) for key, value in kwargs.items() if key in statevar_strings) # Sort after default state variable check to fix gh-116 statevar_dict = OrderedDict( sorted(statevar_dict.items(), key=lambda x: str(x[0]))) str_statevar_dict = OrderedDict((str(key), unpack_condition(value)) for (key, value) in statevar_dict.items()) # Build phase records if they weren't passed if phase_records is None: models = instantiate_models(dbf, comps, active_phases, model=model, parameters=parameters) phase_records = build_phase_records(dbf, comps, active_phases, statevar_dict, models=models, parameters=parameters, output=output, callables=callables, build_gradients=False, build_hessians=False, verbose=kwargs.pop( 'verbose', False)) else: # phase_records were provided, instantiated models must also be provided by the caller models = model if not isinstance(models, Mapping): raise ValueError( "A dictionary of instantiated models must be passed to `equilibrium` with the `model` argument if the `phase_records` argument is used." ) active_phases_without_models = [ name for name in active_phases if not isinstance(models.get(name), Model) ] active_phases_without_phase_records = [ name for name in active_phases if not isinstance(phase_records.get(name), PhaseRecord) ] if len(active_phases_without_phase_records) > 0: raise ValueError( f"phase_records must contain a PhaseRecord instance for every active phase. Missing PhaseRecord objects for {sorted(active_phases_without_phase_records)}" ) if len(active_phases_without_models) > 0: raise ValueError( f"model must contain a Model instance for every active phase. Missing Model objects for {sorted(active_phases_without_models)}" ) maximum_internal_dof = max( len(models[phase_name].site_fractions) for phase_name in active_phases) for phase_name in sorted(active_phases): mod = models[phase_name] phase_record = phase_records[phase_name] points = points_dict[phase_name] if points is None: points = _sample_phase_constitution( mod, sampler_dict[phase_name] or point_sample, fixedgrid_dict[phase_name], pdens_dict[phase_name]) points = np.atleast_2d(points) fp = fake_points and (phase_name == sorted(active_phases)[0]) phase_ds = _compute_phase_values(nonvacant_components, str_statevar_dict, points, phase_record, output, maximum_internal_dof, broadcast=broadcast, parameters=parameters, largest_energy=float(largest_energy), fake_points=fp) all_phase_data.append(phase_ds) fp_offset = len(nonvacant_elements) if fake_points else 0 running_total = [fp_offset] + list( np.cumsum([phase_ds['X'].shape[-2] for phase_ds in all_phase_data])) islice_by_phase = { phase_name: slice(running_total[phase_idx], running_total[phase_idx + 1], None) for phase_idx, phase_name in enumerate(sorted(active_phases)) } # speedup for single-phase case (found by profiling) if len(all_phase_data) > 1: concatenated_coords = all_phase_data[0].coords data_vars = all_phase_data[0].data_vars concatenated_data_vars = {} for var in data_vars.keys(): data_coords = data_vars[var][0] points_idx = data_coords.index('points') # concatenation axis arrs = [] for phase_data in all_phase_data: arrs.append(getattr(phase_data, var)) concat_data = np.concatenate(arrs, axis=points_idx) concatenated_data_vars[var] = (data_coords, concat_data) final_ds = LightDataset(data_vars=concatenated_data_vars, coords=concatenated_coords) else: final_ds = all_phase_data[0] final_ds.attrs['phase_indices'] = islice_by_phase if to_xarray: return final_ds.get_dataset() else: return final_ds