def _build_objective_functions(self): "Construct objective function callables for each phase." for phase_name, phase_obj in self._phases.items(): # Get the symbolic representation of the energy mod = self._models[phase_name] undefs = list(mod.ast.atoms(Symbol) - mod.ast.atoms(v.StateVariable)) for undef in undefs: mod.ast = mod.ast.xreplace({undef: float(0)}) logger.warning('Setting undefined symbol %s for phase %s to zero', undef, phase_name) # Construct an ordered list of the variables self._variables[phase_name], self._sublattice_dof[phase_name] = \ generate_dof(phase_obj, self.components) molefrac_dict = dict([(x, molefrac_ast(phase_obj, x)) \ for x in self.components if x != 'VA']) molefrac_jac_dict = dict() # Generate callables for the mole fractions for comp in self.components: if comp == 'VA': continue molefrac_jac_dict[comp] = [ \ make_callable(molefrac_dict[comp].diff(vx), \ self._variables[phase_name], \ ) for vx in self._variables[phase_name]] molefrac_dict[comp] = make_callable(molefrac_dict[comp], \ self._variables[phase_name]) # Build the "fast" representation of energy model subbed_ast = mod.ast.subs(self.statevars) self._phase_callables[phase_name] = \ make_callable(subbed_ast, \ self._variables[phase_name]) self._gradient_callables[phase_name] = [ \ make_callable(subbed_ast.diff(vx), \ self._variables[phase_name], \ ) for vx in self._variables[phase_name]] self._molefrac_callables[phase_name] = molefrac_dict self._molefrac_jac_callables[phase_name] = molefrac_jac_dict
def calculate(dbf, comps, phases, mode=None, output='GM', fake_points=False, **kwargs): """ Sample the property surface of 'output' containing the specified components and phases. Model parameters are taken from 'dbf' and any state variables (T, P, etc.) can be specified as keyword arguments. Parameters ---------- dbf : Database Thermodynamic database containing the relevant parameters. comps : str or sequence Names of components to consider in the calculation. phases : str or sequence Names of phases to consider in the calculation. mode : string, optional See 'make_callable' docstring for details. output : string, optional Model attribute to sample. fake_points : bool, optional (Default: False) If True, the first few points of the output surface will be fictitious points used to define an equilibrium hyperplane guaranteed to be above all the other points. This is used for convex hull computations. points : ndarray or a dict of phase names to ndarray, optional Columns of ndarrays must be internal degrees of freedom (site fractions), sorted. If this is not specified, points will be generated automatically. pdens : int, a dict of phase names to int, or a seq of both, optional Number of points to sample per degree of freedom. model : Model, a dict of phase names to Model, or a seq of both, optional Model class to use for each phase. Returns ------- xray.Dataset of the sampled attribute as a function of state variables Examples -------- None yet. """ # Here we check for any keyword arguments that are special, i.e., # there may be keyword arguments that aren't state variables pdens_dict = unpack_kwarg(kwargs.pop('pdens', 2000), default_arg=2000) points_dict = unpack_kwarg(kwargs.pop('points', None), default_arg=None) model_dict = unpack_kwarg(kwargs.pop('model', Model), default_arg=Model) callable_dict = unpack_kwarg(kwargs.pop('callables', None), default_arg=None) if isinstance(phases, str): phases = [phases] if isinstance(comps, str): comps = [comps] components = [x for x in sorted(comps) if not x.startswith('VA')] # Convert keyword strings to proper state variable objects # If we don't do this, sympy will get confused during substitution statevar_dict = collections.OrderedDict((v.StateVariable(key), unpack_condition(value)) \ for (key, value) in sorted(kwargs.items())) str_statevar_dict = collections.OrderedDict((str(key), unpack_condition(value)) \ for (key, value) in statevar_dict.items()) all_phase_data = [] comp_sets = {} largest_energy = -np.inf maximum_internal_dof = 0 # Consider only the active phases active_phases = dict((name.upper(), dbf.phases[name.upper()]) \ for name in unpack_phases(phases)) for phase_name, phase_obj in sorted(active_phases.items()): # Build the symbolic representation of the energy mod = model_dict[phase_name] # if this is an object type, we need to construct it if isinstance(mod, type): try: model_dict[phase_name] = mod = mod(dbf, comps, phase_name) except DofError: # we can't build the specified phase because the # specified components aren't found in every sublattice # we'll just skip it logger.warning( """Suspending specified phase %s due to some sublattices containing only unspecified components""", phase_name) continue if points_dict[phase_name] is None: try: out = getattr(mod, output) maximum_internal_dof = max(maximum_internal_dof, len(out.atoms(v.SiteFraction))) except AttributeError: raise AttributeError( 'Missing Model attribute {0} specified for {1}'.format( output, mod.__class__)) else: maximum_internal_dof = max( maximum_internal_dof, np.asarray(points_dict[phase_name]).shape[-1]) for phase_name, phase_obj in sorted(active_phases.items()): try: mod = model_dict[phase_name] except KeyError: continue # Construct an ordered list of the variables variables, sublattice_dof = generate_dof(phase_obj, mod.components) # Build the "fast" representation of that model if callable_dict[phase_name] is None: out = getattr(mod, output) # As a last resort, treat undefined symbols as zero # But warn the user when we do this # This is consistent with TC's behavior undefs = list(out.atoms(Symbol) - out.atoms(v.StateVariable)) for undef in undefs: out = out.xreplace({undef: float(0)}) logger.warning( 'Setting undefined symbol %s for phase %s to zero', undef, phase_name) comp_sets[phase_name] = make_callable(out, \ list(statevar_dict.keys()) + variables, mode=mode) else: comp_sets[phase_name] = callable_dict[phase_name] points = points_dict[phase_name] if points is None: # Eliminate pure vacancy endmembers from the calculation vacancy_indices = list() for idx, sublattice in enumerate(phase_obj.constituents): active_in_subl = sorted( set(phase_obj.constituents[idx]).intersection(comps)) if 'VA' in active_in_subl and 'VA' in sorted(comps): vacancy_indices.append(active_in_subl.index('VA')) if len(vacancy_indices) != len(phase_obj.constituents): vacancy_indices = None logger.debug('vacancy_indices: %s', vacancy_indices) # Add all endmembers to guarantee their presence points = endmember_matrix(sublattice_dof, vacancy_indices=vacancy_indices) # Sample composition space for more points if sum(sublattice_dof) > len(sublattice_dof): points = np.concatenate( (points, point_sample(sublattice_dof, pdof=pdens_dict[phase_name]))) # If there are nontrivial sublattices with vacancies in them, # generate a set of points where their fraction is zero and renormalize for idx, sublattice in enumerate(phase_obj.constituents): if 'VA' in set(sublattice) and len(sublattice) > 1: var_idx = variables.index( v.SiteFraction(phase_name, idx, 'VA')) addtl_pts = np.copy(points) # set vacancy fraction to log-spaced between 1e-10 and 1e-6 addtl_pts[:, var_idx] = np.power( 10.0, -10.0 * (1.0 - addtl_pts[:, var_idx])) # renormalize site fractions cur_idx = 0 for ctx in sublattice_dof: end_idx = cur_idx + ctx addtl_pts[:, cur_idx:end_idx] /= \ addtl_pts[:, cur_idx:end_idx].sum(axis=1)[:, None] cur_idx = end_idx # add to points matrix points = np.concatenate((points, addtl_pts), axis=0) # Filter out nan's that may have slipped in if we sampled too high a vacancy concentration # Issues with this appear to be platform-dependent points = points[~np.isnan(points).any(axis=-1)] # Ensure that points has the correct dimensions and dtype points = np.atleast_2d(np.asarray(points, dtype=np.float)) phase_ds = _compute_phase_values(phase_obj, components, variables, str_statevar_dict, points, comp_sets[phase_name], output, maximum_internal_dof) # largest_energy is really only relevant if fake_points is set if fake_points: largest_energy = max(phase_ds[output].max(), largest_energy) all_phase_data.append(phase_ds) if fake_points: if output != 'GM': raise ValueError( 'fake_points=True should only be used with output=\'GM\'') phase_ds = _generate_fake_points(components, statevar_dict, largest_energy, output, maximum_internal_dof) final_ds = xray.concat(itertools.chain([phase_ds], all_phase_data), dim='points') else: # speedup for single-phase case (found by profiling) if len(all_phase_data) > 1: final_ds = xray.concat(all_phase_data, dim='points') else: final_ds = all_phase_data[0] if (not fake_points) and (len(all_phase_data) == 1): pass else: # Reset the points dimension to use a single global index final_ds['points'] = np.arange(len(final_ds.points)) return final_ds
def energy_surf(dbf, comps, phases, mode=None, **kwargs): """ Sample the energy surface of a system containing the specified components and phases. Model parameters are taken from 'dbf' and any state variables (T, P, etc.) can be specified as keyword arguments. Parameters ---------- dbf : Database Thermodynamic database containing the relevant parameters. comps : list Names of components to consider in the calculation. phases : list Names of phases to consider in the calculation. pdens : int, a dict of phase names to int, or a list of both, optional Number of points to sample per degree of freedom. Returns ------- DataFrame of the energy as a function of composition, temperature, etc. 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) model_dict = unpack_kwarg(kwargs.pop('model', Model), default_arg=Model) # Convert keyword strings to proper state variable objects # If we don't do this, sympy will get confused during substitution statevar_dict = \ dict((v.StateVariable(key), value) \ for (key, value) in kwargs.items()) # Generate all combinations of state variables for 'map' calculation # Wrap single values of state variables in lists # Use 'kwargs' because we want state variable names to be stringified statevar_values = [_listify(val) for val in kwargs.values()] statevars_to_map = [dict(zip(kwargs.keys(), prod)) \ for prod in itertools.product(*statevar_values)] # Consider only the active phases active_phases = dict((name.upper(), dbf.phases[name.upper()]) \ for name in phases) comp_sets = {} # Construct a list to hold all the data all_phase_data = [] for phase_name, phase_obj in sorted(active_phases.items()): # Build the symbolic representation of the energy mod = model_dict[phase_name] # if this is an object type, we need to construct it if isinstance(mod, type): try: mod = mod(dbf, comps, phase_name) except DofError: # we can't build the specified phase because the # specified components aren't found in every sublattice # we'll just skip it logger.warning("""Suspending specified phase %s due to some sublattices containing only unspecified components""", phase_name) continue # As a last resort, treat undefined symbols as zero # But warn the user when we do this # This is consistent with TC's behavior undefs = list(mod.ast.atoms(Symbol) - mod.ast.atoms(v.StateVariable)) for undef in undefs: mod.ast = mod.ast.xreplace({undef: float(0)}) logger.warning('Setting undefined symbol %s for phase %s to zero', undef, phase_name) # Construct an ordered list of the variables variables, sublattice_dof = generate_dof(phase_obj, mod.components) # Build the "fast" representation of that model comp_sets[phase_name] = make_callable(mod.ast, \ list(statevar_dict.keys()) + variables, mode=mode) # Get the site ratios in each sublattice site_ratios = list(phase_obj.sublattices) # Eliminate pure vacancy endmembers from the calculation vacancy_indices = list() for idx, sublattice in enumerate(phase_obj.constituents): if 'VA' in sorted(sublattice) and 'VA' in sorted(comps): vacancy_indices.append(sorted(sublattice).index('VA')) if len(vacancy_indices) != len(phase_obj.constituents): vacancy_indices = None logger.debug('vacancy_indices: %s', vacancy_indices) # Add all endmembers to guarantee their presence points = endmember_matrix(sublattice_dof, vacancy_indices=vacancy_indices) # Sample composition space for more points if sum(sublattice_dof) > len(sublattice_dof): points = np.concatenate((points, point_sample(sublattice_dof, pdof=pdens_dict[phase_name]) )) # If there are nontrivial sublattices with vacancies in them, # generate a set of points where their fraction is zero and renormalize for idx, sublattice in enumerate(phase_obj.constituents): if 'VA' in set(sublattice) and len(sublattice) > 1: var_idx = variables.index(v.SiteFraction(phase_name, idx, 'VA')) addtl_pts = np.copy(points) # set vacancy fraction to log-spaced between 1e-10 and 1e-6 addtl_pts[:, var_idx] = np.power(10.0, -10.0*(1.0 - addtl_pts[:, var_idx])) # renormalize site fractions cur_idx = 0 for ctx in sublattice_dof: end_idx = cur_idx + ctx addtl_pts[:, cur_idx:end_idx] /= \ addtl_pts[:, cur_idx:end_idx].sum(axis=1)[:, None] cur_idx = end_idx # add to points matrix points = np.concatenate((points, addtl_pts), axis=0) data_dict = {'Phase': phase_name} # Generate input d.o.f matrix for all state variable combinations for statevars in statevars_to_map: # Prefill the state variable arguments to the energy function energy_func = \ lambda *args: comp_sets[phase_name]( *itertools.chain(list(statevars.values()), args)) # Get the stable points and energies for this configuration # Set max refinements equal to the number of independent dof mxr = sum(phase_obj.sublattices) - len(phase_obj.sublattices) refined_points, energies = \ refine_energy_surf(points, None, phase_obj, comps, variables, energy_func, max_iterations=-1) try: data_dict['GM'].extend(energies) for statevar in kwargs.keys(): data_dict[statevar].extend( list(np.repeat(list(statevars.values()), len(refined_points)))) except KeyError: data_dict['GM'] = list(energies) for statevar in kwargs.keys(): data_dict[statevar] = \ list(np.repeat(list(statevars.values()), len(refined_points))) # Map the internal degrees of freedom to global coordinates # Normalize site ratios # Normalize by the sum of site ratios times a factor # related to the site fraction of vacancies site_ratio_normalization = np.zeros(len(refined_points)) for idx, sublattice in enumerate(phase_obj.constituents): vacancy_column = np.ones(len(refined_points)) if 'VA' in set(sublattice): var_idx = variables.index(v.SiteFraction(phase_name, idx, 'VA')) vacancy_column -= refined_points[:, var_idx] site_ratio_normalization += site_ratios[idx] * vacancy_column for comp in sorted(comps): if comp == 'VA': continue avector = [float(cur_var.species == comp) * \ site_ratios[cur_var.sublattice_index] for cur_var in variables] try: data_dict['X('+comp+')'].extend(list(np.divide(np.dot( refined_points[:, :], avector), site_ratio_normalization))) except KeyError: data_dict['X('+comp+')'] = list(np.divide(np.dot( refined_points[:, :], avector), site_ratio_normalization)) # Copy coordinate information into data_dict # TODO: Is there a more memory-efficient way to deal with this? # Perhaps with hierarchical indexing... try: for column_idx, data in enumerate(refined_points.T): data_dict[str(variables[column_idx])].extend(list(data)) except KeyError: for column_idx, data in enumerate(refined_points.T): data_dict[str(variables[column_idx])] = list(data) all_phase_data.append(pd.DataFrame(data_dict)) # all_phases_data now contains energy surface information for the system return pd.concat(all_phase_data, axis=0, join='outer', \ ignore_index=True, verify_integrity=False)
def calculate(dbf, comps, phases, mode=None, output='GM', fake_points=False, broadcast=True, tmpman=None, **kwargs): """ Sample the property surface of 'output' containing the specified components and phases. Model parameters are taken from 'dbf' and any state variables (T, P, etc.) can be specified as keyword arguments. Parameters ---------- dbf : Database Thermodynamic database containing the relevant parameters. comps : str or sequence Names of components to consider in the calculation. phases : str or sequence Names of phases to consider in the calculation. mode : string, optional See 'make_callable' docstring for details. output : string, optional Model attribute to sample. fake_points : bool, optional (Default: False) If True, the first few points of the output surface will be fictitious points used to define an equilibrium hyperplane guaranteed to be above all the other points. This is used for convex hull computations. broadcast : bool, optional If True, broadcast given state variable lists against each other to create a grid. If False, assume state variables are given as equal-length lists. tmpman : TempfileManager, optional Context manager for temporary file creation during the calculation. points : ndarray or a dict of phase names to ndarray, optional Columns of ndarrays must be internal degrees of freedom (site fractions), sorted. If this is not specified, points will be generated automatically. pdens : int, a dict of phase names to int, or a seq of both, optional Number of points to sample per degree of freedom. model : Model, a dict of phase names to Model, or a seq of both, optional Model class to use for each phase. sampler : callable, a dict of phase names to callable, or a seq of both, optional Function to sample phase constitution space. Must have same signature as 'pycalphad.core.utils.point_sample' grid_points : bool, a dict of phase names to bool, or a seq of both, optional (Default: True) Whether to add evenly spaced points between end-members. The density of points is determined by 'pdens' Returns ------- Dataset of the sampled attribute as a function of state variables Examples -------- None yet. """ # Here we check for any keyword arguments that are special, i.e., # there may be keyword arguments that aren't state variables pdens_dict = unpack_kwarg(kwargs.pop('pdens', 2000), default_arg=2000) points_dict = unpack_kwarg(kwargs.pop('points', None), default_arg=None) model_dict = unpack_kwarg(kwargs.pop('model', Model), default_arg=Model) callable_dict = unpack_kwarg(kwargs.pop('callables', None), default_arg=None) sampler_dict = unpack_kwarg(kwargs.pop('sampler', None), default_arg=None) fixedgrid_dict = unpack_kwarg(kwargs.pop('grid_points', True), default_arg=True) if isinstance(phases, str): phases = [phases] if isinstance(comps, str): comps = [comps] if points_dict is None and broadcast is False: raise ValueError('The \'points\' keyword argument must be specified if broadcast=False is also given.') components = [x for x in sorted(comps) if not x.startswith('VA')] # Convert keyword strings to proper state variable objects # If we don't do this, sympy will get confused during substitution statevar_dict = collections.OrderedDict((v.StateVariable(key), unpack_condition(value)) \ for (key, value) in sorted(kwargs.items())) str_statevar_dict = collections.OrderedDict((str(key), unpack_condition(value)) \ for (key, value) in statevar_dict.items()) all_phase_data = [] comp_sets = {} largest_energy = -np.inf maximum_internal_dof = 0 # Consider only the active phases active_phases = dict((name.upper(), dbf.phases[name.upper()]) \ for name in unpack_phases(phases)) for phase_name, phase_obj in sorted(active_phases.items()): # Build the symbolic representation of the energy mod = model_dict[phase_name] # if this is an object type, we need to construct it if isinstance(mod, type): try: model_dict[phase_name] = mod = mod(dbf, comps, phase_name) except DofError: # we can't build the specified phase because the # specified components aren't found in every sublattice # we'll just skip it logger.warning("""Suspending specified phase %s due to some sublattices containing only unspecified components""", phase_name) continue if points_dict[phase_name] is None: try: out = getattr(mod, output) maximum_internal_dof = max(maximum_internal_dof, len(out.atoms(v.SiteFraction))) except AttributeError: raise AttributeError('Missing Model attribute {0} specified for {1}' .format(output, mod.__class__)) else: maximum_internal_dof = max(maximum_internal_dof, np.asarray(points_dict[phase_name]).shape[-1]) for phase_name, phase_obj in sorted(active_phases.items()): try: mod = model_dict[phase_name] except KeyError: continue # Construct an ordered list of the variables variables, sublattice_dof = generate_dof(phase_obj, mod.components) # Build the "fast" representation of that model if callable_dict[phase_name] is None: out = getattr(mod, output) # As a last resort, treat undefined symbols as zero # But warn the user when we do this # This is consistent with TC's behavior undefs = list(out.atoms(Symbol) - out.atoms(v.StateVariable)) for undef in undefs: out = out.xreplace({undef: float(0)}) logger.warning('Setting undefined symbol %s for phase %s to zero', undef, phase_name) comp_sets[phase_name] = build_functions(out, list(statevar_dict.keys()) + variables, tmpman=tmpman, include_obj=True, include_grad=False, include_hess=False) else: comp_sets[phase_name] = callable_dict[phase_name] points = points_dict[phase_name] if points is None: # Eliminate pure vacancy endmembers from the calculation vacancy_indices = list() for idx, sublattice in enumerate(phase_obj.constituents): active_in_subl = sorted(set(phase_obj.constituents[idx]).intersection(comps)) if 'VA' in active_in_subl and 'VA' in sorted(comps): vacancy_indices.append(active_in_subl.index('VA')) if len(vacancy_indices) != len(phase_obj.constituents): vacancy_indices = None logger.debug('vacancy_indices: %s', vacancy_indices) # Add all endmembers to guarantee their presence points = endmember_matrix(sublattice_dof, vacancy_indices=vacancy_indices) if fixedgrid_dict[phase_name] is True: # Sample along the edges of the endmembers # These constitution space edges are often the equilibrium points! em_pairs = list(itertools.combinations(points, 2)) for first_em, second_em in em_pairs: extra_points = first_em * np.linspace(0, 1, pdens_dict[phase_name])[np.newaxis].T + \ second_em * np.linspace(0, 1, pdens_dict[phase_name])[::-1][np.newaxis].T points = np.concatenate((points, extra_points)) # Sample composition space for more points if sum(sublattice_dof) > len(sublattice_dof): sampler = sampler_dict[phase_name] if sampler is None: sampler = point_sample points = np.concatenate((points, sampler(sublattice_dof, pdof=pdens_dict[phase_name]) )) # If there are nontrivial sublattices with vacancies in them, # generate a set of points where their fraction is zero and renormalize for idx, sublattice in enumerate(phase_obj.constituents): if 'VA' in set(sublattice) and len(sublattice) > 1: var_idx = variables.index(v.SiteFraction(phase_name, idx, 'VA')) addtl_pts = np.copy(points) # set vacancy fraction to log-spaced between 1e-10 and 1e-6 addtl_pts[:, var_idx] = np.power(10.0, -10.0*(1.0 - addtl_pts[:, var_idx])) # renormalize site fractions cur_idx = 0 for ctx in sublattice_dof: end_idx = cur_idx + ctx addtl_pts[:, cur_idx:end_idx] /= \ addtl_pts[:, cur_idx:end_idx].sum(axis=1)[:, None] cur_idx = end_idx # add to points matrix points = np.concatenate((points, addtl_pts), axis=0) # Filter out nan's that may have slipped in if we sampled too high a vacancy concentration # Issues with this appear to be platform-dependent points = points[~np.isnan(points).any(axis=-1)] # Ensure that points has the correct dimensions and dtype points = np.atleast_2d(np.asarray(points, dtype=np.float)) phase_ds = _compute_phase_values(phase_obj, components, variables, str_statevar_dict, points, comp_sets[phase_name], output, maximum_internal_dof, broadcast=broadcast) # largest_energy is really only relevant if fake_points is set if fake_points: largest_energy = max(phase_ds[output].max(), largest_energy) all_phase_data.append(phase_ds) if fake_points: if output != 'GM': raise ValueError('fake_points=True should only be used with output=\'GM\'') phase_ds = _generate_fake_points(components, statevar_dict, largest_energy, output, maximum_internal_dof, broadcast) final_ds = concat(itertools.chain([phase_ds], all_phase_data), dim='points') else: # speedup for single-phase case (found by profiling) if len(all_phase_data) > 1: final_ds = concat(all_phase_data, dim='points') else: final_ds = all_phase_data[0] if (not fake_points) and (len(all_phase_data) == 1): pass else: # Reset the points dimension to use a single global index final_ds['points'] = np.arange(len(final_ds.points)) return final_ds
def energy_surf(dbf, comps, phases, mode=None, output='GM', **kwargs): """ Sample the property surface of 'output' containing the specified components and phases. Model parameters are taken from 'dbf' and any state variables (T, P, etc.) can be specified as keyword arguments. Parameters ---------- dbf : Database Thermodynamic database containing the relevant parameters. comps : list Names of components to consider in the calculation. phases : list Names of phases to consider in the calculation. mode : string, optional See 'make_callable' docstring for details. output : string, optional Model attribute to sample. pdens : int, a dict of phase names to int, or a list of both, optional Number of points to sample per degree of freedom. model : Model, a dict of phase names to Model, or a list of both, optional Model class to use for each phase. Returns ------- DataFrame of the output as a function of composition, temperature, etc. Examples -------- None yet. """ warnings.warn('Use pycalphad.calculate() instead', DeprecationWarning, stacklevel=2) # Here we check for any keyword arguments that are special, i.e., # there may be keyword arguments that aren't state variables pdens_dict = unpack_kwarg(kwargs.pop('pdens', 2000), default_arg=2000) model_dict = unpack_kwarg(kwargs.pop('model', Model), default_arg=Model) callable_dict = unpack_kwarg(kwargs.pop('callables', None), default_arg=None) # Convert keyword strings to proper state variable objects # If we don't do this, sympy will get confused during substitution statevar_dict = \ collections.OrderedDict((v.StateVariable(key), value) \ for (key, value) in sorted(kwargs.items())) # Generate all combinations of state variables for 'map' calculation # Wrap single values of state variables in lists # Use 'kwargs' because we want state variable names to be stringified statevar_values = [_listify(val) for val in statevar_dict.values()] statevars_to_map = np.array(list(itertools.product(*statevar_values))) # Consider only the active phases active_phases = dict((name.upper(), dbf.phases[name.upper()]) \ for name in phases) comp_sets = {} # Construct a list to hold all the data all_phase_data = [] for phase_name, phase_obj in sorted(active_phases.items()): # Build the symbolic representation of the energy mod = model_dict[phase_name] # if this is an object type, we need to construct it if isinstance(mod, type): try: mod = mod(dbf, comps, phase_name) except DofError: # we can't build the specified phase because the # specified components aren't found in every sublattice # we'll just skip it logger.warning("""Suspending specified phase %s due to some sublattices containing only unspecified components""", phase_name) continue try: out = getattr(mod, output) except AttributeError: raise AttributeError('Missing Model attribute {0} specified for {1}' .format(output, mod.__class__)) # Construct an ordered list of the variables variables, sublattice_dof = generate_dof(phase_obj, mod.components) site_ratios = list(phase_obj.sublattices) # Build the "fast" representation of that model if callable_dict[phase_name] is None: # As a last resort, treat undefined symbols as zero # But warn the user when we do this # This is consistent with TC's behavior undefs = list(out.atoms(Symbol) - out.atoms(v.StateVariable)) for undef in undefs: out = out.xreplace({undef: float(0)}) logger.warning('Setting undefined symbol %s for phase %s to zero', undef, phase_name) comp_sets[phase_name] = make_callable(out, \ list(statevar_dict.keys()) + variables, mode=mode) else: comp_sets[phase_name] = callable_dict[phase_name] # Eliminate pure vacancy endmembers from the calculation vacancy_indices = list() for idx, sublattice in enumerate(phase_obj.constituents): if 'VA' in sorted(sublattice) and 'VA' in sorted(comps): vacancy_indices.append(sorted(sublattice).index('VA')) if len(vacancy_indices) != len(phase_obj.constituents): vacancy_indices = None logger.debug('vacancy_indices: %s', vacancy_indices) # Add all endmembers to guarantee their presence points = endmember_matrix(sublattice_dof, vacancy_indices=vacancy_indices) # Sample composition space for more points if sum(sublattice_dof) > len(sublattice_dof): points = np.concatenate((points, point_sample(sublattice_dof, pdof=pdens_dict[phase_name]) )) # If there are nontrivial sublattices with vacancies in them, # generate a set of points where their fraction is zero and renormalize for idx, sublattice in enumerate(phase_obj.constituents): if 'VA' in set(sublattice) and len(sublattice) > 1: var_idx = variables.index(v.SiteFraction(phase_name, idx, 'VA')) addtl_pts = np.copy(points) # set vacancy fraction to log-spaced between 1e-10 and 1e-6 addtl_pts[:, var_idx] = np.power(10.0, -10.0*(1.0 - addtl_pts[:, var_idx])) # renormalize site fractions cur_idx = 0 for ctx in sublattice_dof: end_idx = cur_idx + ctx addtl_pts[:, cur_idx:end_idx] /= \ addtl_pts[:, cur_idx:end_idx].sum(axis=1)[:, None] cur_idx = end_idx # add to points matrix points = np.concatenate((points, addtl_pts), axis=0) data_dict = {'Phase': phase_name} # Broadcast compositions and state variables along orthogonal axes # This lets us eliminate an expensive Python loop data_dict[output] = \ comp_sets[phase_name](*itertools.chain( np.transpose(statevars_to_map[:, :, np.newaxis], (1, 2, 0)), np.transpose(points[:, :, np.newaxis], (1, 0, 2)))).T.ravel() # Save state variables, with values indexed appropriately statevar_vals = np.repeat(statevars_to_map, len(points), axis=0).T data_dict.update({str(statevar): vals for statevar, vals \ in zip(statevar_dict.keys(), statevar_vals)}) # Map the internal degrees of freedom to global coordinates # Normalize site ratios by the sum of site ratios times a factor # related to the site fraction of vacancies site_ratio_normalization = np.zeros(len(points)) for idx, sublattice in enumerate(phase_obj.constituents): vacancy_column = np.ones(len(points)) if 'VA' in set(sublattice): var_idx = variables.index(v.SiteFraction(phase_name, idx, 'VA')) vacancy_column -= points[:, var_idx] site_ratio_normalization += site_ratios[idx] * vacancy_column for comp in sorted(comps): if comp == 'VA': continue avector = [float(vxx.species == comp) * \ site_ratios[vxx.sublattice_index] for vxx in variables] data_dict['X('+comp+')'] = np.tile(np.divide(np.dot( points[:, :], avector), site_ratio_normalization), statevars_to_map.shape[0]) # Copy coordinate information into data_dict # TODO: Is there a more memory-efficient way to deal with this? # Perhaps with hierarchical indexing... var_fmt = 'Y({0},{1},{2})' data_dict.update({var_fmt.format(vxx.phase_name, vxx.sublattice_index, vxx.species): \ np.tile(vals, statevars_to_map.shape[0]) \ for vxx, vals in zip(variables, points.T)}) all_phase_data.append(pd.DataFrame(data_dict)) # all_phases_data now contains energy surface information for the system return pd.concat(all_phase_data, axis=0, join='outer', \ ignore_index=True, verify_integrity=False)
def minimize(self, simplex, phase_fractions=None): """ Accept a list of simplex vertices and return the values of the variables that minimize the energy under the constraints. """ # Generate phase fraction variables # Track the multiplicity of phases with a Counter object composition_sets = Counter() all_variables = [] # starting point x_0 = [] # scaling factor -- set to minimum energy of starting simplex # Scaling the objective to be of order '10' seems to result in # sufficient precision (at least 5 significant figures). scaling_factor = abs(simplex['GM'].min()) / 10.0 # a list of tuples for where each phase's variable indices # start and end index_ranges = [] #print(list(enumerate(simplex.iterrows()))) #print((simplex.iterrows())) #print('END') for m_idx, vertex in enumerate(simplex.iterrows()): vertex = vertex[1] # increase multiplicity by one composition_sets[vertex['Phase']] += 1 # create new phase fraction variable all_variables.append( v.PhaseFraction(vertex['Phase'], composition_sets[vertex['Phase']]) ) start = len(x_0) # default position is centroid of the simplex if phase_fractions is None: x_0.append(1.0/len(list(simplex.iterrows()))) else: # use the provided guess for the phase fraction x_0.append(phase_fractions[m_idx]) # add site fraction variables all_variables.extend(self._variables[vertex['Phase']]) # add starting point for variable for varname in self._variables[vertex['Phase']]: x_0.append(vertex[str(varname)]) index_ranges.append([start, len(x_0)]) # Create master objective function def obj(input_x): "Objective function. Takes x vector as input. Returns scalar." objective = 0.0 for idx, vertex in enumerate(simplex.iterrows()): vertex = vertex[1] cur_x = input_x[index_ranges[idx][0]+1:index_ranges[idx][1]] #print('Phase: '+vertex['Phase']+' '+str(cur_x)) # phase fraction times value of objective for that phase objective += input_x[index_ranges[idx][0]] * \ self._phase_callables[vertex['Phase']]( *list(cur_x)) return objective / scaling_factor # Create master gradient function def gradient(input_x): "Accepts input vector and returns gradient vector." gradient = np.zeros(len(input_x)) for idx, vertex in enumerate(simplex.iterrows()): vertex = vertex[1] cur_x = input_x[index_ranges[idx][0]+1:index_ranges[idx][1]] #print('grad cur_x: '+str(cur_x)) # phase fraction derivative is just the phase energy gradient[index_ranges[idx][0]] = \ self._phase_callables[vertex['Phase']]( *list(cur_x)) # gradient for particular phase's variables # NOTE: We assume all phase d.o.f are independent here, # and we handle any coupling through the constraints for g_idx, grad in \ enumerate(self._gradient_callables[vertex['Phase']]): gradient[index_ranges[idx][0]+1+g_idx] = \ input_x[index_ranges[idx][0]] * \ grad(*list(cur_x)) #print('grad: '+str(gradient / scaling_factor)) return gradient / scaling_factor # Generate constraint sequence constraints = [] # phase fraction constraint def phasefrac_cons(input_x): "Accepts input vector and returns phase fraction constraint." output = 1.0 - sum([input_x[i[0]] for i in index_ranges])#** 2 return output def phasefrac_jac(input_x): "Accepts input vector and returns Jacobian of constraint." output_x = np.zeros(len(input_x)) for idx_range in index_ranges: output_x[idx_range[0]] = -1.0 #\ # -2.0*sum([input_x[i[0]] for i in index_ranges]) return output_x phasefrac_dict = dict() phasefrac_dict['type'] = 'eq' phasefrac_dict['fun'] = phasefrac_cons phasefrac_dict['jac'] = phasefrac_jac constraints.append(phasefrac_dict) # bounds constraint def nonneg_cons(input_x, idx): "Accepts input vector and returns non-negativity constraint." output = input_x[idx] #print('nonneg_cons: '+str(output)) return output def nonneg_jac(input_x, idx): "Accepts input vector and returns Jacobian of constraint." output_x = np.zeros(len(input_x)) output_x[idx] = 1.0 return output_x for idx in range(len(all_variables)): nonneg_dict = dict() nonneg_dict['type'] = 'ineq' nonneg_dict['fun'] = nonneg_cons nonneg_dict['jac'] = nonneg_jac nonneg_dict['args'] = [idx] constraints.append(nonneg_dict) # Generate all site fraction constraints for idx_range in index_ranges: # need to generate constraint for each sublattice dofs = self._sublattice_dof[all_variables[idx_range[0]].phase_name] cur_idx = idx_range[0]+1 for dof in dofs: sitefrac_dict = dict() sitefrac_dict['type'] = 'eq' sitefrac_dict['fun'] = sitefrac_cons sitefrac_dict['jac'] = sitefrac_jac sitefrac_dict['args'] = [[cur_idx, cur_idx+dof]] cur_idx += dof if dof > 0: constraints.append(sitefrac_dict) # All other constraints, e.g., mass balance def molefrac_cons(input_x, species, fix_val, all_variables, phases): """ Accept input vector, species and fixed value. Returns constraint. """ output = -fix_val for idx, vertex in enumerate(simplex.iterrows()): vertex = vertex[1] cur_x = input_x[index_ranges[idx][0]+1:index_ranges[idx][1]] res = self._molefrac_callables[vertex['Phase']][species](*cur_x) output += input_x[index_ranges[idx][0]] * res #print('molefrac_cons: '+str(output)) return output def molefrac_jac(input_x, species, fix_val, all_variables, phases): "Accepts input vector and returns Jacobian vector." output_x = np.zeros(len(input_x)) for idx, vertex in enumerate(simplex.iterrows()): vertex = vertex[1] cur_x = input_x[index_ranges[idx][0]+1:index_ranges[idx][1]] output_x[index_ranges[idx][0]] = \ self._molefrac_callables[vertex['Phase']][species](*cur_x) for g_idx, grad in \ enumerate(self._molefrac_jac_callables[vertex['Phase']][species]): output_x[index_ranges[idx][0]+1+g_idx] = \ input_x[index_ranges[idx][0]] * \ grad(*list(cur_x)) #print('molefrac_jac '+str(output_x)) return output_x eqs = len([x for x in constraints if x['type'] == 'eq']) if eqs < len(x_0): for condition, value in self.conditions.items(): if isinstance(condition, v.Composition): # mass balance constraint for mole fraction molefrac_dict = dict() molefrac_dict['type'] = 'eq' molefrac_dict['fun'] = molefrac_cons molefrac_dict['jac'] = molefrac_jac molefrac_dict['args'] = \ [condition.species, value, all_variables, self._phases] constraints.append(molefrac_dict) else: logger.warning("""Dropping mass balance constraints due to zero internal degrees of freedom""") # Run optimization res = scipy.optimize.minimize(obj, x_0, method='slsqp', jac=gradient,\ constraints=constraints, options={'maxiter': 1000}) # rescale final values back to original res['raw_fun'] = copy.deepcopy(res['fun']) res['raw_jac'] = copy.deepcopy(res['jac']) res['raw_x'] = copy.deepcopy(res['x']) res['fun'] *= scaling_factor res['jac'] *= scaling_factor # force tiny numerical values to be positive res['x'] = np.maximum(res['x'], np.zeros(1, dtype=np.float64)) logger.debug(res) if not res['success']: logger.error('Energy minimization failed') return None # Build result object eq_res = EquilibriumResult(self._phases, self.components, self.statevars, res['fun'], zip(all_variables, res['x'])) return eq_res