def calculate_(species: Sequence[v.Species], phases: Sequence[str], str_statevar_dict: Dict[str, np.ndarray], models: Dict[str, Model], phase_records: Dict[str, PhaseRecord], output: Optional[str] = 'GM', points: Optional[Dict[str, np.ndarray]] = None, pdens: Optional[int] = 50, broadcast: Optional[bool] = True, fake_points: Optional[bool] = False, ) -> LightDataset: """ Quickly sample phase internal degree of freedom with virtually no overhead. """ points_dict = unpack_kwarg(points, default_arg=None) pdens_dict = unpack_kwarg(pdens, default_arg=50) nonvacant_components = [x for x in sorted(species) if x.number_of_atoms > 0] maximum_internal_dof = max(prx.phase_dof for prx in phase_records.values()) all_phase_data = [] for phase_name in sorted(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, point_sample, True, pdens_dict[phase_name]) points = np.atleast_2d(points) fp = fake_points and (phase_name == sorted(phases)[0]) phase_ds = _compute_phase_values(nonvacant_components, str_statevar_dict, points, phase_record, output, maximum_internal_dof, broadcast=broadcast, largest_energy=float(1e10), fake_points=fp, parameters={}) all_phase_data.append(phase_ds) # assumes phase_records all have the same nonvacant pure elements, # even if those elements are not present in this phase record fp_offset = len(tuple(phase_records.values())[0].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(phases))} 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 return final_ds
def equilibrium(dbf, comps, phases, conditions, **kwargs): """ Calculate the equilibrium state of a system containing the specified components and phases, under the specified conditions. Model parameters are taken from 'dbf'. 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. verbose : bool, optional (Default: True) Show progress of calculations. grid_opts : dict, optional Keyword arguments to pass to the initial grid routine. Returns ------- Structured equilibrium calculation. Examples -------- None yet. """ active_phases = unpack_phases(phases) or sorted(dbf.phases.keys()) comps = sorted(comps) indep_vars = ['T', 'P'] grid_opts = kwargs.pop('grid_opts', dict()) verbose = kwargs.pop('verbose', True) phase_records = dict() callable_dict = kwargs.pop('callables', dict()) grad_callable_dict = kwargs.pop('grad_callables', dict()) hess_callable_dict = kwargs.pop('hess_callables', dict()) points_dict = dict() maximum_internal_dof = 0 conds = OrderedDict((key, unpack_condition(value)) for key, value in sorted(conditions.items(), key=str)) str_conds = OrderedDict((str(key), value) for key, value in conds.items()) indep_vals = list([float(x) for x in np.atleast_1d(val)] for key, val in str_conds.items() if key in indep_vars) components = [x for x in sorted(comps) if not x.startswith('VA')] # Construct models for each phase; prioritize user models models = unpack_kwarg(kwargs.pop('model', Model), default_arg=Model) if verbose: print('Components:', ' '.join(comps)) print('Phases:', end=' ') for name in active_phases: mod = models[name] if isinstance(mod, type): models[name] = mod = mod(dbf, comps, name) variables = sorted(mod.energy.atoms(v.StateVariable).union( {key for key in conditions.keys() if key in [v.T, v.P]}), key=str) site_fracs = sorted(mod.energy.atoms(v.SiteFraction), key=str) maximum_internal_dof = max(maximum_internal_dof, len(site_fracs)) # Extra factor '1e-100...' is to work around an annoying broadcasting bug for zero gradient entries #models[name].models['_broadcaster'] = 1e-100 * Mul(*variables) ** 3 out = models[name].energy undefs = list(out.atoms(Symbol) - out.atoms(v.StateVariable)) for undef in undefs: out = out.xreplace({undef: float(0)}) callable_dict[name], grad_callable_dict[name], hess_callable_dict[name] = \ build_functions(out, [v.P, v.T] + site_fracs) # Adjust gradient by the approximate chemical potentials hyperplane = Add(*[ v.MU(i) * mole_fraction(dbf.phases[name], comps, i) for i in comps if i != 'VA' ]) plane_obj, plane_grad, plane_hess = build_functions( hyperplane, [v.MU(i) for i in comps if i != 'VA'] + site_fracs) phase_records[name.upper()] = PhaseRecord( variables=variables, grad=grad_callable_dict[name], hess=hess_callable_dict[name], plane_grad=plane_grad, plane_hess=plane_hess) if verbose: print(name, end=' ') if verbose: print('[done]', end='\n') # 'calculate' accepts conditions through its keyword arguments grid_opts.update( {key: value for key, value in str_conds.items() if key in indep_vars}) if 'pdens' not in grid_opts: grid_opts['pdens'] = 100 coord_dict = str_conds.copy() coord_dict['vertex'] = np.arange(len(components)) grid_shape = np.meshgrid(*coord_dict.values(), indexing='ij', sparse=False)[0].shape coord_dict['component'] = components if verbose: print('Computing initial grid', end=' ') grid = calculate(dbf, comps, active_phases, output='GM', model=models, callables=callable_dict, fake_points=True, **grid_opts) if verbose: print('[{0} points, {1}]'.format(len(grid.points), sizeof_fmt(grid.nbytes)), end='\n') properties = xray.Dataset( { 'NP': (list(str_conds.keys()) + ['vertex'], np.empty(grid_shape)), 'GM': (list(str_conds.keys()), np.empty(grid_shape[:-1])), 'MU': (list(str_conds.keys()) + ['component'], np.empty(grid_shape)), 'points': (list(str_conds.keys()) + ['vertex'], np.empty(grid_shape, dtype=np.int)) }, coords=coord_dict, attrs={'iterations': 1}, ) # Store the potentials from the previous iteration current_potentials = properties.MU.copy() for iteration in range(MAX_ITERATIONS): if verbose: print('Computing convex hull [iteration {}]'.format( properties.attrs['iterations'])) # lower_convex_hull will modify properties lower_convex_hull(grid, properties) progress = np.abs(current_potentials - properties.MU).values converged = (progress < MIN_PROGRESS).all(axis=-1) if verbose: print('progress', progress.max(), '[{} conditions updated]'.format(np.sum(~converged))) if progress.max() < MIN_PROGRESS: if verbose: print('Convergence achieved') break current_potentials[...] = properties.MU.values if verbose: print('Refining convex hull') # Insert extra dimensions for non-T,P conditions so GM broadcasts correctly energy_broadcast_shape = grid.GM.values.shape[:len(indep_vals)] + \ (1,) * (len(str_conds) - len(indep_vals)) + (grid.GM.values.shape[-1],) driving_forces = np.einsum('...i,...i', properties.MU.values[..., np.newaxis, :].astype(np.float), grid.X.values[np.index_exp[...] + (np.newaxis,) * (len(str_conds) - len(indep_vals)) + np.index_exp[:, :]].astype(np.float)) - \ grid.GM.values.view().reshape(energy_broadcast_shape) for name in active_phases: dof = len(models[name].energy.atoms(v.SiteFraction)) current_phase_indices = (grid.Phase.values == name ).reshape(energy_broadcast_shape[:-1] + (-1, )) # Broadcast to capture all conditions current_phase_indices = np.broadcast_arrays( current_phase_indices, np.empty(driving_forces.shape))[0] # This reshape is safe as long as phases have the same number of points at all indep. conditions current_phase_driving_forces = driving_forces[ current_phase_indices].reshape( current_phase_indices.shape[:-1] + (-1, )) # Note: This works as long as all points are in the same phase order for all T, P current_site_fractions = grid.Y.values[..., current_phase_indices[ (0, ) * len(str_conds)], :] if np.sum( current_site_fractions[(0, ) * len(indep_vals)][..., :dof]) == dof: # All site fractions are 1, aka zero internal degrees of freedom # Impossible to refine these points, so skip this phase points_dict[name] = current_site_fractions[ (0, ) * len(indep_vals)][..., :dof] continue # Find the N points with largest driving force for a given set of conditions # Remember that driving force has a sign, so we want the "most positive" values # N is the number of components, in this context # N points define a 'best simplex' for every set of conditions # We also need to restrict ourselves to one phase at a time trial_indices = np.argpartition(current_phase_driving_forces, -len(components), axis=-1)[..., -len(components):] trial_indices = trial_indices.ravel() statevar_indices = np.unravel_index( np.arange( np.multiply.reduce(properties.GM.values.shape + (len(components), ))), properties.GM.values.shape + (len(components), ))[:len(indep_vals)] points = current_site_fractions[np.index_exp[statevar_indices + (trial_indices, )]] points.shape = properties.points.shape[:-1] + ( -1, maximum_internal_dof) # The Y arrays have been padded, so we should slice off the padding points = points[..., :dof] #print('Starting points shape: ', points.shape) #print(points) if len(points) == 0: if name in points_dict: del points_dict[name] # No nearly stable points: skip this phase continue num_vars = len(phase_records[name].variables) plane_grad = phase_records[name].plane_grad plane_hess = phase_records[name].plane_hess statevar_grid = np.meshgrid(*itertools.chain(indep_vals), sparse=True, indexing='ij') # TODO: A more sophisticated treatment of constraints num_constraints = len(dbf.phases[name].sublattices) constraint_jac = np.zeros( (num_constraints, num_vars - len(indep_vars))) # Independent variables are always fixed (in this limited implementation) #for idx in range(len(indep_vals)): # constraint_jac[idx, idx] = 1 # This is for site fraction balance constraints var_idx = 0 #len(indep_vals) for idx in range(len(dbf.phases[name].sublattices)): active_in_subl = set( dbf.phases[name].constituents[idx]).intersection(comps) constraint_jac[idx, var_idx:var_idx + len(active_in_subl)] = 1 var_idx += len(active_in_subl) newton_iteration = 0 while newton_iteration < MAX_NEWTON_ITERATIONS: flattened_points = points.reshape( points.shape[:len(indep_vals)] + (-1, points.shape[-1])) grad_args = itertools.chain( [i[..., None] for i in statevar_grid], [ flattened_points[..., i] for i in range(flattened_points.shape[-1]) ]) grad = np.array(phase_records[name].grad(*grad_args), dtype=np.float) # Remove derivatives wrt T,P grad = grad[..., len(indep_vars):] grad.shape = points.shape grad[np.isnan(grad).any( axis=-1 )] = 0 # This is necessary for gradients on the edge of space hess_args = itertools.chain( [i[..., None] for i in statevar_grid], [ flattened_points[..., i] for i in range(flattened_points.shape[-1]) ]) hess = np.array(phase_records[name].hess(*hess_args), dtype=np.float) # Remove derivatives wrt T,P hess = hess[..., len(indep_vars):, len(indep_vars):] hess.shape = points.shape + (hess.shape[-1], ) hess[np.isnan(hess).any(axis=(-2, -1))] = np.eye(hess.shape[-1]) plane_args = itertools.chain([ properties.MU.values[..., i][..., None] for i in range(properties.MU.shape[-1]) ], [points[..., i] for i in range(points.shape[-1])]) cast_grad = np.array(plane_grad(*plane_args), dtype=np.float) # Remove derivatives wrt chemical potentials cast_grad = cast_grad[..., properties.MU.shape[-1]:] grad = grad - cast_grad plane_args = itertools.chain([ properties.MU.values[..., i][..., None] for i in range(properties.MU.shape[-1]) ], [points[..., i] for i in range(points.shape[-1])]) cast_hess = np.array(plane_hess(*plane_args), dtype=np.float) # Remove derivatives wrt chemical potentials cast_hess = cast_hess[..., properties.MU.shape[-1]:, properties.MU.shape[-1]:] cast_hess = -cast_hess + hess hess = cast_hess.astype(np.float, copy=False) try: e_matrix = np.linalg.inv(hess) except np.linalg.LinAlgError: print(hess) raise current = calculate( dbf, comps, name, output='GM', model=models, callables=callable_dict, fake_points=False, points=points.reshape(points.shape[:len(indep_vals)] + (-1, points.shape[-1])), **grid_opts) current_plane = np.multiply( current.X.values.reshape(points.shape[:-1] + (len(components), )), properties.MU.values[..., np.newaxis, :]).sum(axis=-1) current_df = current.GM.values.reshape( points.shape[:-1]) - current_plane #print('Inv hess check: ', np.isnan(e_matrix).any()) #print('grad check: ', np.isnan(grad).any()) dy_unconstrained = -np.einsum('...ij,...j->...i', e_matrix, grad) #print('dy_unconstrained check: ', np.isnan(dy_unconstrained).any()) proj_matrix = np.dot(e_matrix, constraint_jac.T) inv_matrix = np.rollaxis(np.dot(constraint_jac, proj_matrix), 0, -1) inv_term = np.linalg.inv(inv_matrix) #print('inv_term check: ', np.isnan(inv_term).any()) first_term = np.einsum('...ij,...jk->...ik', proj_matrix, inv_term) #print('first_term check: ', np.isnan(first_term).any()) # Normally a term for the residual here # We only choose starting points which obey the constraints, so r = 0 cons_summation = np.einsum('...i,...ji->...j', dy_unconstrained, constraint_jac) #print('cons_summation check: ', np.isnan(cons_summation).any()) cons_correction = np.einsum('...ij,...j->...i', first_term, cons_summation) #print('cons_correction check: ', np.isnan(cons_correction).any()) dy_constrained = dy_unconstrained - cons_correction #print('dy_constrained check: ', np.isnan(dy_constrained).any()) # TODO: Support for adaptive changing independent variable steps new_direction = dy_constrained #print('new_direction', new_direction) #print('points', points) # Backtracking line search if np.isnan(new_direction).any(): print('new_direction', new_direction) #print('Convergence angle:', -(grad*new_direction).sum(axis=-1) / (np.linalg.norm(grad, axis=-1) * np.linalg.norm(new_direction, axis=-1))) new_points = points + INITIAL_STEP_SIZE * new_direction alpha = np.full(new_points.shape[:-1], INITIAL_STEP_SIZE, dtype=np.float) alpha[np.all(np.linalg.norm(new_direction, axis=-1) < MIN_DIRECTION_NORM, axis=-1)] = 0 negative_points = np.any(new_points < 0., axis=-1) while np.any(negative_points): alpha[negative_points] *= 0.5 new_points = points + alpha[..., np.newaxis] * new_direction negative_points = np.any(new_points < 0., axis=-1) # Backtracking line search # alpha now contains maximum possible values that keep us inside the space # but we don't just want to take the biggest step; we want the biggest step which reduces energy new_points = new_points.reshape( new_points.shape[:len(indep_vals)] + (-1, new_points.shape[-1])) candidates = calculate(dbf, comps, name, output='GM', model=models, callables=callable_dict, fake_points=False, points=new_points, **grid_opts) candidate_plane = np.multiply( candidates.X.values.reshape(points.shape[:-1] + (len(components), )), properties.MU.values[..., np.newaxis, :]).sum(axis=-1) energy_diff = (candidates.GM.values.reshape( new_direction.shape[:-1]) - candidate_plane) - current_df new_points.shape = new_direction.shape bad_steps = energy_diff > alpha * 1e-4 * (new_direction * grad).sum(axis=-1) backtracking_iterations = 0 while np.any(bad_steps): alpha[bad_steps] *= 0.5 new_points = points + alpha[..., np.newaxis] * new_direction #print('new_points', new_points) #print('bad_steps', bad_steps) new_points = new_points.reshape( new_points.shape[:len(indep_vals)] + (-1, new_points.shape[-1])) candidates = calculate(dbf, comps, name, output='GM', model=models, callables=callable_dict, fake_points=False, points=new_points, **grid_opts) candidate_plane = np.multiply( candidates.X.values.reshape(points.shape[:-1] + (len(components), )), properties.MU.values[..., np.newaxis, :]).sum(axis=-1) energy_diff = (candidates.GM.values.reshape( new_direction.shape[:-1]) - candidate_plane) - current_df #print('energy_diff', energy_diff) new_points.shape = new_direction.shape bad_steps = energy_diff > alpha * 1e-4 * ( new_direction * grad).sum(axis=-1) backtracking_iterations += 1 if backtracking_iterations > MAX_BACKTRACKING: break biggest_step = np.max( np.linalg.norm(new_points - points, axis=-1)) if biggest_step < 1e-2: if verbose: print('N-R convergence on mini-iteration', newton_iteration, '[{}]'.format(name)) points = new_points break if verbose: #print('Biggest step:', biggest_step) #print('points', points) #print('grad of points', grad) #print('new_direction', new_direction) #print('alpha', alpha) #print('new_points', new_points) pass points = new_points newton_iteration += 1 new_points = points.reshape(points.shape[:len(indep_vals)] + (-1, points.shape[-1])) new_points = np.concatenate( (current_site_fractions[..., :dof], new_points), axis=-2) points_dict[name] = new_points if verbose: print('Rebuilding grid', end=' ') grid = calculate(dbf, comps, active_phases, output='GM', model=models, callables=callable_dict, fake_points=True, points=points_dict, **grid_opts) if verbose: print('[{0} points, {1}]'.format(len(grid.points), sizeof_fmt(grid.nbytes)), end='\n') properties.attrs['iterations'] += 1 # One last call to ensure 'properties' and 'grid' are consistent with one another lower_convex_hull(grid, properties) ravelled_X_view = grid['X'].values.view().reshape( -1, grid['X'].values.shape[-1]) ravelled_Y_view = grid['Y'].values.view().reshape( -1, grid['Y'].values.shape[-1]) ravelled_Phase_view = grid['Phase'].values.view().reshape(-1) # Copy final point values from the grid and drop the index array # For some reason direct construction doesn't work. We have to create empty and then assign. properties['X'] = xray.DataArray( np.empty_like(ravelled_X_view[properties['points'].values]), dims=properties['points'].dims + ('component', )) properties['X'].values[...] = ravelled_X_view[properties['points'].values] properties['Y'] = xray.DataArray( np.empty_like(ravelled_Y_view[properties['points'].values]), dims=properties['points'].dims + ('internal_dof', )) properties['Y'].values[...] = ravelled_Y_view[properties['points'].values] # TODO: What about invariant reactions? We should perform a final driving force calculation here. # We can handle that in the same post-processing step where we identify single-phase regions. properties['Phase'] = xray.DataArray(np.empty_like( ravelled_Phase_view[properties['points'].values]), dims=properties['points'].dims) properties['Phase'].values[...] = ravelled_Phase_view[ properties['points'].values] del properties['points'] return properties
def equilibrium(dbf, comps, phases, conditions, output=None, model=None, verbose=False, broadcast=True, calc_opts=None, scheduler=dask.local.get_sync, parameters=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. Returns ------- Structured equilibrium calculation, or Dask graph if scheduler=None. Examples -------- None yet. """ if not broadcast: raise NotImplementedError('Broadcasting cannot yet be disabled') from pycalphad import __version__ as pycalphad_version 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)) comps = sorted(comps) if len(set(comps) - set(dbf.elements)) > 0: raise EquilibriumError('Components not found in database: {}'.format( ','.join(set(comps) - set(dbf.elements)))) indep_vars = ['T', 'P'] calc_opts = calc_opts if calc_opts is not None else dict() model = model if model is not None else FallbackModel phase_records = dict() diagnostic = kwargs.pop('_diagnostic', False) callable_dict = kwargs.pop('callables', dict()) grad_callable_dict = kwargs.pop('grad_callables', dict()) hess_callable_dict = kwargs.pop('hess_callables', dict()) parameters = parameters if parameters is not None else dict() if isinstance(parameters, dict): parameters = OrderedDict(sorted(parameters.items(), key=str)) param_symbols = tuple(parameters.keys()) param_values = np.atleast_1d( np.array(list(parameters.values()), dtype=np.float)) maximum_internal_dof = 0 # 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)) str_conds = OrderedDict((str(key), value) for key, value in conds.items()) num_calcs = np.prod([len(i) for i in str_conds.values()]) build_functions = compiled_build_functions backend_mode = 'compiled' if kwargs.get('_backend', None): backend_mode = kwargs['_backend'] if verbose: backend_dict = { 'compiled': 'Compiled (autowrap)', 'interpreted': 'Interpreted (autograd)' } print('Calculation Backend: {}'.format( backend_dict.get(backend_mode, 'Custom'))) indep_vals = list([float(x) for x in np.atleast_1d(val)] for key, val in str_conds.items() if key in indep_vars) components = [x for x in sorted(comps) if not x.startswith('VA')] # Construct models for each phase; prioritize user models models = unpack_kwarg(model, default_arg=FallbackModel) if verbose: print('Components:', ' '.join(comps)) print('Phases:', end=' ') max_phase_name_len = max(len(name) for name in active_phases) # Need to allow for '_FAKE_' psuedo-phase max_phase_name_len = max(max_phase_name_len, 6) for name in active_phases: mod = models[name] if isinstance(mod, type): models[name] = mod = mod(dbf, comps, name, parameters=parameters) if isinstance(mod, CompiledModel): phase_records[name.upper()] = PhaseRecord_from_compiledmodel( mod, param_values) maximum_internal_dof = max(maximum_internal_dof, sum(mod.sublattice_dof)) else: site_fracs = mod.site_fractions variables = sorted(site_fracs, key=str) maximum_internal_dof = max(maximum_internal_dof, len(site_fracs)) out = models[name].energy if (not callable_dict.get(name, False)) or not (grad_callable_dict.get(name, False)) \ or (not hess_callable_dict.get(name, False)): # Only force undefineds to zero if we're not overriding them undefs = list( out.atoms(Symbol) - out.atoms(v.StateVariable) - set(param_symbols)) for undef in undefs: out = out.xreplace({undef: float(0)}) cf, gf, hf = build_functions(out, tuple([v.P, v.T] + site_fracs), parameters=param_symbols) if callable_dict.get(name, None) is None: callable_dict[name] = cf if grad_callable_dict.get(name, None) is None: grad_callable_dict[name] = gf if hess_callable_dict.get(name, None) is None: hess_callable_dict[name] = hf phase_records[name.upper()] = PhaseRecord_from_cython( comps, variables, np.array(dbf.phases[name].sublattices, dtype=np.float), param_values, callable_dict[name], grad_callable_dict[name], hess_callable_dict[name]) if verbose: print(name, end=' ') if verbose: print('[done]', end='\n') # 'calculate' accepts conditions through its keyword arguments grid_opts = calc_opts.copy() grid_opts.update( {key: value for key, value in str_conds.items() if key in indep_vars}) if 'pdens' not in grid_opts: grid_opts['pdens'] = 500 coord_dict = str_conds.copy() coord_dict['vertex'] = np.arange(len(components)) grid_shape = np.meshgrid(*coord_dict.values(), indexing='ij', sparse=False)[0].shape coord_dict['component'] = components grid = delayed(calculate, pure=False)(dbf, comps, active_phases, output='GM', model=models, callables=callable_dict, fake_points=True, parameters=parameters, **grid_opts) properties = delayed(Dataset, pure=False)( { 'NP': (list(str_conds.keys()) + ['vertex'], np.empty(grid_shape)), 'GM': (list(str_conds.keys()), np.empty(grid_shape[:-1])), 'MU': (list(str_conds.keys()) + ['component'], np.empty(grid_shape)), 'X': (list(str_conds.keys()) + ['vertex', 'component'], np.empty(grid_shape + (grid_shape[-1], ))), 'Y': (list(str_conds.keys()) + ['vertex', 'internal_dof'], np.empty(grid_shape + (maximum_internal_dof, ))), 'Phase': (list(str_conds.keys()) + ['vertex'], np.empty(grid_shape, dtype='U%s' % max_phase_name_len)), 'points': (list(str_conds.keys()) + ['vertex'], np.empty(grid_shape, dtype=np.int32)) }, coords=coord_dict, attrs={ 'engine': 'pycalphad %s' % pycalphad_version }, ) # One last call to ensure 'properties' and 'grid' are consistent with one another properties = delayed(lower_convex_hull, pure=False)(grid, properties) 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()), verbose) 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()), verbose) # Compute equilibrium values of any additional user-specified properties output = output if isinstance(output, (list, tuple, set)) else [output] # 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 for phase_name, mod in models.items(): if isinstance(mod, CompiledModel) or isinstance( mod, FallbackModel): models[phase_name] = Model(dbf, comps, phase_name, parameters=parameters) eqcal = delayed(_eqcalculate, pure=False)(dbf, comps, active_phases, conditions, out, data=properties, per_phase=per_phase, model=models, **calc_opts) properties = delayed(properties.merge, pure=False)(eqcal, inplace=True, compat='equals') if scheduler is not None: properties = dask.compute(properties, get=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 calculate(dbf, comps, phases, mode=None, output='GM', fake_points=False, broadcast=True, parameters=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 SymPy Symbol to numbers, for overriding the values of parameters in the Database. 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) 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] all_phase_data = [] largest_energy = 1e10 # Consider only the active phases list_of_possible_phases = filter_phases(dbf, comps) active_phases = sorted(set(list_of_possible_phases).intersection(set(phases))) active_phases = {name: dbf.phases[name] for name in active_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)) models = instantiate_models(dbf, comps, list(active_phases.keys()), model=kwargs.pop('model', None), parameters=parameters) 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 = collections.OrderedDict(sorted(statevar_dict.items(), key=lambda x: str(x[0]))) phase_records = build_phase_records(dbf, comps, active_phases, statevar_dict, models=models, parameters=parameters, output=output, callables=callables, verbose=kwargs.pop('verbose', False)) str_statevar_dict = collections.OrderedDict((str(key), unpack_condition(value)) \ for (key, value) in statevar_dict.items()) maximum_internal_dof = max(len(models[phase_name].site_fractions) for phase_name in active_phases) for phase_name, phase_obj in sorted(active_phases.items()): mod = models[phase_name] phase_record = phase_records[phase_name] points = points_dict[phase_name] variables, sublattice_dof = generate_dof(phase_obj, mod.components) if points is None: points = _sample_phase_constitution(phase_name, phase_obj.constituents, sublattice_dof, comps, tuple(variables), 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.keys())[0]) phase_ds = _compute_phase_values(nonvacant_components, str_statevar_dict, points, phase_record, output, maximum_internal_dof, broadcast=broadcast, largest_energy=float(largest_energy), fake_points=fp) all_phase_data.append(phase_ds) # speedup for single-phase case (found by profiling) if len(all_phase_data) > 1: final_ds = concat(all_phase_data, dim='points') final_ds['points'].values = np.arange(len(final_ds['points'])) final_ds.coords['points'].values = np.arange(len(final_ds['points'])) else: final_ds = all_phase_data[0] return final_ds
def calculate(dbf, comps, phases, mode=None, output='GM', fake_points=False, broadcast=True, parameters=None, to_xarray=True, **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 SymPy Symbol to numbers, for overriding the values of parameters in the Database. 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) 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] 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 = { 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)) models = instantiate_models(dbf, comps, list(active_phases.keys()), model=kwargs.pop('model', None), parameters=parameters) 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 = collections.OrderedDict( sorted(statevar_dict.items(), key=lambda x: str(x[0]))) 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)) str_statevar_dict = collections.OrderedDict((str(key), unpack_condition(value)) \ for (key, value) in statevar_dict.items()) maximum_internal_dof = max( len(models[phase_name].site_fractions) for phase_name in active_phases) for phase_name, phase_obj in sorted(active_phases.items()): mod = models[phase_name] phase_record = phase_records[phase_name] points = points_dict[phase_name] variables, sublattice_dof = generate_dof(phase_obj, mod.components) if points is None: points = _sample_phase_constitution( phase_name, phase_obj.constituents, sublattice_dof, comps, tuple(variables), 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.keys())[0]) phase_ds = _compute_phase_values(nonvacant_components, str_statevar_dict, points, phase_record, output, maximum_internal_dof, broadcast=broadcast, largest_energy=float(largest_energy), fake_points=fp) all_phase_data.append(phase_ds) # 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] if to_xarray: return final_ds.get_dataset() else: return final_ds
build_functions = compiled_build_functions backend_mode = 'compiled' if kwargs.get('_backend', None): backend_mode = kwargs['_backend'] if verbose: backend_dict = { 'compiled': 'Compiled (autowrap)', 'interpreted': 'Interpreted (autograd)' } print('Calculation Backend: {}'.format( backend_dict.get(backend_mode, 'Custom'))) indep_vals = list([float(x) for x in np.atleast_1d(val)] for key, val in str_conds.items() if key in indep_vars) components = [x for x in sorted(comps) if not x.startswith('VA')] # Construct models for each phase; prioritize user models models = unpack_kwarg(model, default_arg=FallbackModel) if verbose: print('Components:', ' '.join(comps)) print('Phases:', end=' ') max_phase_name_len = max(len(name) for name in active_phases) # Need to allow for '_FAKE_' psuedo-phase max_phase_name_len = max(max_phase_name_len, 6) for name in active_phases: mod = models[name] if isinstance(mod, type): models[name] = mod = mod(dbf, comps, name, parameters=parameters) if isinstance(mod, CompiledModel): phase_records[name.upper()] = PhaseRecord_from_compiledmodel( mod, param_values) maximum_internal_dof = max(maximum_internal_dof, sum(mod.sublattice_dof))
def equilibrium(dbf, comps, phases, conditions, **kwargs): """ Calculate the equilibrium state of a system containing the specified components and phases, under the specified conditions. Model parameters are taken from 'dbf'. 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. verbose : bool, optional (Default: True) Show progress of calculations. grid_opts : dict, optional Keyword arguments to pass to the initial grid routine. Returns ------- Structured equilibrium calculation. Examples -------- None yet. """ active_phases = unpack_phases(phases) or sorted(dbf.phases.keys()) comps = sorted(comps) indep_vars = ['T', 'P'] grid_opts = kwargs.pop('grid_opts', dict()) verbose = kwargs.pop('verbose', True) phase_records = dict() callable_dict = kwargs.pop('callables', dict()) grad_callable_dict = kwargs.pop('grad_callables', dict()) points_dict = dict() maximum_internal_dof = 0 # Construct models for each phase; prioritize user models models = unpack_kwarg(kwargs.pop('model', Model), default_arg=Model) if verbose: print('Components:', ' '.join(comps)) print('Phases:', end=' ') for name in active_phases: mod = models[name] if isinstance(mod, type): models[name] = mod = mod(dbf, comps, name) variables = sorted(mod.energy.atoms(v.StateVariable).union( {key for key in conditions.keys() if key in [v.T, v.P]}), key=str) site_fracs = sorted(mod.energy.atoms(v.SiteFraction), key=str) maximum_internal_dof = max(maximum_internal_dof, len(site_fracs)) # Extra factor '1e-100...' is to work around an annoying broadcasting bug for zero gradient entries models[name].models['_broadcaster'] = 1e-100 * Mul(*variables)**3 out = models[name].energy if name not in callable_dict: undefs = list(out.atoms(Symbol) - out.atoms(v.StateVariable)) for undef in undefs: out = out.xreplace({undef: float(0)}) # callable_dict takes variables in a different order due to calculate() pecularities callable_dict[name] = make_callable( out, sorted((key for key in conditions.keys() if key in [v.T, v.P]), key=str) + site_fracs) if name not in grad_callable_dict: grad_func = make_callable( Matrix([out]).jacobian(variables), variables) else: grad_func = grad_callable_dict[name] # Adjust gradient by the approximate chemical potentials plane_vars = sorted(models[name].energy.atoms(v.SiteFraction), key=str) hyperplane = Add(*[ v.MU(i) * mole_fraction(dbf.phases[name], comps, i) for i in comps if i != 'VA' ]) # Workaround an annoying bug with zero gradient entries # This forces numerically zero entries to broadcast correctly hyperplane += 1e-100 * Mul(*([v.MU(i) for i in comps if i != 'VA'] + plane_vars + [v.T, v.P]))**3 plane_grad = make_callable( Matrix([hyperplane]).jacobian(variables), [v.MU(i) for i in comps if i != 'VA'] + plane_vars + [v.T, v.P]) plane_hess = make_callable(hessian(hyperplane, variables), [v.MU(i) for i in comps if i != 'VA'] + plane_vars + [v.T, v.P]) phase_records[name.upper()] = PhaseRecord(variables=variables, grad=grad_func, plane_grad=plane_grad, plane_hess=plane_hess) if verbose: print(name, end=' ') if verbose: print('[done]', end='\n') conds = OrderedDict((key, unpack_condition(value)) for key, value in sorted(conditions.items(), key=str)) str_conds = OrderedDict((str(key), value) for key, value in conds.items()) indep_vals = list([float(x) for x in np.atleast_1d(val)] for key, val in str_conds.items() if key in indep_vars) components = [x for x in sorted(comps) if not x.startswith('VA')] # 'calculate' accepts conditions through its keyword arguments grid_opts.update( {key: value for key, value in str_conds.items() if key in indep_vars}) if 'pdens' not in grid_opts: grid_opts['pdens'] = 10 coord_dict = str_conds.copy() coord_dict['vertex'] = np.arange(len(components)) grid_shape = np.meshgrid(*coord_dict.values(), indexing='ij', sparse=False)[0].shape coord_dict['component'] = components if verbose: print('Computing initial grid', end=' ') grid = calculate(dbf, comps, active_phases, output='GM', model=models, callables=callable_dict, fake_points=True, **grid_opts) if verbose: print('[{0} points, {1}]'.format(len(grid.points), sizeof_fmt(grid.nbytes)), end='\n') properties = xray.Dataset( { 'NP': (list(str_conds.keys()) + ['vertex'], np.empty(grid_shape)), 'GM': (list(str_conds.keys()), np.empty(grid_shape[:-1])), 'MU': (list(str_conds.keys()) + ['component'], np.empty(grid_shape)), 'points': (list(str_conds.keys()) + ['vertex'], np.empty(grid_shape, dtype=np.int)) }, coords=coord_dict, attrs={'iterations': 1}, ) # Store the potentials from the previous iteration current_potentials = properties.MU.copy() for iteration in range(MAX_ITERATIONS): if verbose: print('Computing convex hull [iteration {}]'.format( properties.attrs['iterations'])) # lower_convex_hull will modify properties lower_convex_hull(grid, properties) progress = np.abs(current_potentials - properties.MU).max().values if verbose: print('progress', progress) if progress < MIN_PROGRESS: if verbose: print('Convergence achieved') break current_potentials[...] = properties.MU.values if verbose: print('Refining convex hull') # Insert extra dimensions for non-T,P conditions so GM broadcasts correctly energy_broadcast_shape = grid.GM.values.shape[:len(indep_vals)] + \ (1,) * (len(str_conds) - len(indep_vals)) + (grid.GM.values.shape[-1],) driving_forces = np.einsum('...i,...i', properties.MU.values[..., np.newaxis, :], grid.X.values[np.index_exp[...] + (np.newaxis,) * (len(str_conds) - len(indep_vals)) + np.index_exp[:, :]]) - \ grid.GM.values.view().reshape(energy_broadcast_shape) for name in active_phases: dof = len(models[name].energy.atoms(v.SiteFraction)) current_phase_indices = (grid.Phase.values == name ).reshape(energy_broadcast_shape[:-1] + (-1, )) # Broadcast to capture all conditions current_phase_indices = np.broadcast_arrays( current_phase_indices, np.empty(driving_forces.shape))[0] # This reshape is safe as long as phases have the same number of points at all indep. conditions current_phase_driving_forces = driving_forces[ current_phase_indices].reshape( current_phase_indices.shape[:-1] + (-1, )) # Note: This works as long as all points are in the same phase order for all T, P current_site_fractions = grid.Y.values[..., current_phase_indices[ (0, ) * len(str_conds)], :] if np.sum( current_site_fractions[(0, ) * len(indep_vals)][..., :dof]) == dof: # All site fractions are 1, aka zero internal degrees of freedom # Impossible to refine these points, so skip this phase points_dict[name] = current_site_fractions[ (0, ) * len(indep_vals)][..., :dof] continue # Find the N points with largest driving force for a given set of conditions # Remember that driving force has a sign, so we want the "most positive" values # N is the number of components, in this context # N points define a 'best simplex' for every set of conditions # We also need to restrict ourselves to one phase at a time trial_indices = np.argpartition(current_phase_driving_forces, -len(components), axis=-1)[..., -len(components):] trial_indices = trial_indices.ravel() statevar_indices = np.unravel_index( np.arange( np.multiply.reduce(properties.GM.values.shape + (len(components), ))), properties.GM.values.shape + (len(components), ))[:len(indep_vals)] points = current_site_fractions[np.index_exp[statevar_indices + (trial_indices, )]] points.shape = properties.points.shape[:-1] + ( -1, maximum_internal_dof) # The Y arrays have been padded, so we should slice off the padding points = points[..., :dof] # Workaround for derivative issues at endmembers points[points == 0.] = MIN_SITE_FRACTION if len(points) == 0: if name in points_dict: del points_dict[name] # No nearly stable points: skip this phase continue num_vars = len(phase_records[name].variables) plane_grad = phase_records[name].plane_grad plane_hess = phase_records[name].plane_hess statevar_grid = np.meshgrid(*itertools.chain(indep_vals), sparse=True, indexing='xy') # TODO: A more sophisticated treatment of constraints num_constraints = len(indep_vals) + len( dbf.phases[name].sublattices) constraint_jac = np.zeros((num_constraints, num_vars)) # Independent variables are always fixed (in this limited implementation) for idx in range(len(indep_vals)): constraint_jac[idx, idx] = 1 # This is for site fraction balance constraints var_idx = len(indep_vals) for idx in range(len(dbf.phases[name].sublattices)): active_in_subl = set( dbf.phases[name].constituents[idx]).intersection(comps) constraint_jac[len(indep_vals) + idx, var_idx:var_idx + len(active_in_subl)] = 1 var_idx += len(active_in_subl) grad = phase_records[name].grad( *itertools.chain(statevar_grid, points.T)) if grad.dtype == 'object': # Workaround a bug in zero gradient entries grad_zeros = np.zeros(points.T.shape[1:], dtype=np.float) for i in np.arange(grad.shape[0]): if isinstance(grad[i], int): grad[i] = grad_zeros grad = np.array(grad.tolist(), dtype=np.float) bcasts = np.broadcast_arrays( *itertools.chain(properties.MU.values.T, points.T)) cast_grad = -plane_grad(*itertools.chain(bcasts, [0], [0])) cast_grad = cast_grad.T + grad.T grad = cast_grad grad.shape = grad.shape[:-1] # Remove extraneous dimension # This Hessian is an approximation updated using the BFGS method # See Nocedal and Wright, ch.3, p. 198 # Initialize as identity matrix hess = broadcast_to(np.eye(num_vars), grad.shape + (grad.shape[-1], )).copy() newton_iteration = 0 while newton_iteration < MAX_NEWTON_ITERATIONS: e_matrix = np.linalg.inv(hess) dy_unconstrained = -np.einsum('...ij,...j->...i', e_matrix, grad) proj_matrix = np.dot(e_matrix, constraint_jac.T) inv_matrix = np.rollaxis(np.dot(constraint_jac, proj_matrix), 0, -1) inv_term = np.linalg.inv(inv_matrix) first_term = np.einsum('...ij,...jk->...ik', proj_matrix, inv_term) # Normally a term for the residual here # We only choose starting points which obey the constraints, so r = 0 cons_summation = np.einsum('...i,...ji->...j', dy_unconstrained, constraint_jac) cons_correction = np.einsum('...ij,...j->...i', first_term, cons_summation) dy_constrained = dy_unconstrained - cons_correction # TODO: Support for adaptive changing independent variable steps new_direction = dy_constrained[..., len(indep_vals):] # Backtracking line search new_points = points + INITIAL_STEP_SIZE * new_direction alpha = np.full(new_points.shape[:-1], INITIAL_STEP_SIZE, dtype=np.float) negative_points = np.any(new_points < 0., axis=-1) while np.any(negative_points): alpha[negative_points] *= 0.1 new_points = points + alpha[..., np.newaxis] * new_direction negative_points = np.any(new_points < 0., axis=-1) # If we made "near" zero progress on any points, don't update the Hessian until # we've rebuilt the convex hull # Nocedal and Wright recommend against skipping Hessian updates # They recommend using a damped update approach, pp. 538-539 of their book # TODO: Check the projected gradient norm, not the step length if np.any( np.max(np.abs(alpha[..., np.newaxis] * new_direction), axis=-1) < MIN_STEP_LENGTH): break # Workaround for derivative issues at endmembers new_points[new_points == 0.] = 1e-16 # BFGS update to Hessian new_grad = phase_records[name].grad( *itertools.chain(statevar_grid, new_points.T)) if new_grad.dtype == 'object': # Workaround a bug in zero gradient entries grad_zeros = np.zeros(new_points.T.shape[1:], dtype=np.float) for i in np.arange(new_grad.shape[0]): if isinstance(new_grad[i], int): new_grad[i] = grad_zeros new_grad = np.array(new_grad.tolist(), dtype=np.float) bcasts = np.broadcast_arrays( *itertools.chain(properties.MU.values.T, new_points.T)) cast_grad = -plane_grad(*itertools.chain(bcasts, [0], [0])) cast_grad = cast_grad.T + new_grad.T new_grad = cast_grad new_grad.shape = new_grad.shape[: -1] # Remove extraneous dimension # Notation used here consistent with Nocedal and Wright s_k = np.empty(points.shape[:-1] + (points.shape[-1] + len(indep_vals), )) # Zero out independent variable changes for now s_k[..., :len(indep_vals)] = 0 s_k[..., len(indep_vals):] = new_points - points y_k = new_grad - grad s_s_term = np.einsum('...j,...k->...jk', s_k, s_k) s_b_s_term = np.einsum('...i,...ij,...j', s_k, hess, s_k) y_y_y_s_term = np.einsum('...j,...k->...jk', y_k, y_k) / \ np.einsum('...i,...i', y_k, s_k)[..., np.newaxis, np.newaxis] update = np.einsum('...ij,...jk,...kl->...il', hess, s_s_term, hess) / \ s_b_s_term[..., np.newaxis, np.newaxis] + y_y_y_s_term hess = hess - update cast_hess = -plane_hess( *itertools.chain(bcasts, [0], [0])).T + hess hess = -cast_hess #TODO: Why does this fix things? # TODO: Verify that the chosen step lengths reduce the energy points = new_points grad = new_grad newton_iteration += 1 new_points = new_points.reshape( new_points.shape[:len(indep_vals)] + (-1, new_points.shape[-1])) new_points = np.concatenate( (current_site_fractions[..., :dof], new_points), axis=-2) points_dict[name] = new_points if verbose: print('Rebuilding grid', end=' ') grid = calculate(dbf, comps, active_phases, output='GM', model=models, callables=callable_dict, fake_points=True, points=points_dict, **grid_opts) if verbose: print('[{0} points, {1}]'.format(len(grid.points), sizeof_fmt(grid.nbytes)), end='\n') properties.attrs['iterations'] += 1 # One last call to ensure 'properties' and 'grid' are consistent with one another lower_convex_hull(grid, properties) ravelled_X_view = grid['X'].values.view().reshape( -1, grid['X'].values.shape[-1]) ravelled_Y_view = grid['Y'].values.view().reshape( -1, grid['Y'].values.shape[-1]) ravelled_Phase_view = grid['Phase'].values.view().reshape(-1) # Copy final point values from the grid and drop the index array # For some reason direct construction doesn't work. We have to create empty and then assign. properties['X'] = xray.DataArray( np.empty_like(ravelled_X_view[properties['points'].values]), dims=properties['points'].dims + ('component', )) properties['X'].values[...] = ravelled_X_view[properties['points'].values] properties['Y'] = xray.DataArray( np.empty_like(ravelled_Y_view[properties['points'].values]), dims=properties['points'].dims + ('internal_dof', )) properties['Y'].values[...] = ravelled_Y_view[properties['points'].values] # TODO: What about invariant reactions? We should perform a final driving force calculation here. # We can handle that in the same post-processing step where we identify single-phase regions. properties['Phase'] = xray.DataArray(np.empty_like( ravelled_Phase_view[properties['points'].values]), dims=properties['points'].dims) properties['Phase'].values[...] = ravelled_Phase_view[ properties['points'].values] del properties['points'] return properties
def equilibrium(dbf, comps, phases, conditions, **kwargs): """ Calculate the equilibrium state of a system containing the specified components and phases, under the specified conditions. Model parameters are taken from 'dbf'. 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. verbose : bool, optional (Default: True) Show progress of calculations. grid_opts : dict, optional Keyword arguments to pass to the initial grid routine. Returns ------- Structured equilibrium calculation. Examples -------- None yet. """ active_phases = unpack_phases(phases) or sorted(dbf.phases.keys()) comps = sorted(comps) indep_vars = ['T', 'P'] grid_opts = kwargs.pop('grid_opts', dict()) verbose = kwargs.pop('verbose', True) phase_records = dict() callable_dict = kwargs.pop('callables', dict()) grad_callable_dict = kwargs.pop('grad_callables', dict()) points_dict = dict() maximum_internal_dof = 0 # Construct models for each phase; prioritize user models models = unpack_kwarg(kwargs.pop('model', Model), default_arg=Model) if verbose: print('Components:', ' '.join(comps)) print('Phases:', end=' ') for name in active_phases: mod = models[name] if isinstance(mod, type): models[name] = mod = mod(dbf, comps, name) variables = sorted(mod.energy.atoms(v.StateVariable).union({key for key in conditions.keys() if key in [v.T, v.P]}), key=str) site_fracs = sorted(mod.energy.atoms(v.SiteFraction), key=str) maximum_internal_dof = max(maximum_internal_dof, len(site_fracs)) # Extra factor '1e-100...' is to work around an annoying broadcasting bug for zero gradient entries models[name].models['_broadcaster'] = 1e-100 * Mul(*variables) ** 3 out = models[name].energy if name not in callable_dict: undefs = list(out.atoms(Symbol) - out.atoms(v.StateVariable)) for undef in undefs: out = out.xreplace({undef: float(0)}) # callable_dict takes variables in a different order due to calculate() pecularities callable_dict[name] = make_callable(out, sorted((key for key in conditions.keys() if key in [v.T, v.P]), key=str) + site_fracs) if name not in grad_callable_dict: grad_func = make_callable(Matrix([out]).jacobian(variables), variables) else: grad_func = grad_callable_dict[name] # Adjust gradient by the approximate chemical potentials plane_vars = sorted(models[name].energy.atoms(v.SiteFraction), key=str) hyperplane = Add(*[v.MU(i)*mole_fraction(dbf.phases[name], comps, i) for i in comps if i != 'VA']) # Workaround an annoying bug with zero gradient entries # This forces numerically zero entries to broadcast correctly hyperplane += 1e-100 * Mul(*([v.MU(i) for i in comps if i != 'VA'] + plane_vars + [v.T, v.P])) ** 3 plane_grad = make_callable(Matrix([hyperplane]).jacobian(variables), [v.MU(i) for i in comps if i != 'VA'] + plane_vars + [v.T, v.P]) plane_hess = make_callable(hessian(hyperplane, variables), [v.MU(i) for i in comps if i != 'VA'] + plane_vars + [v.T, v.P]) phase_records[name.upper()] = PhaseRecord(variables=variables, grad=grad_func, plane_grad=plane_grad, plane_hess=plane_hess) if verbose: print(name, end=' ') if verbose: print('[done]', end='\n') conds = OrderedDict((key, unpack_condition(value)) for key, value in sorted(conditions.items(), key=str)) str_conds = OrderedDict((str(key), value) for key, value in conds.items()) indep_vals = list([float(x) for x in np.atleast_1d(val)] for key, val in str_conds.items() if key in indep_vars) components = [x for x in sorted(comps) if not x.startswith('VA')] # 'calculate' accepts conditions through its keyword arguments grid_opts.update({key: value for key, value in str_conds.items() if key in indep_vars}) if 'pdens' not in grid_opts: grid_opts['pdens'] = 10 coord_dict = str_conds.copy() coord_dict['vertex'] = np.arange(len(components)) grid_shape = np.meshgrid(*coord_dict.values(), indexing='ij', sparse=False)[0].shape coord_dict['component'] = components if verbose: print('Computing initial grid', end=' ') grid = calculate(dbf, comps, active_phases, output='GM', model=models, callables=callable_dict, fake_points=True, **grid_opts) if verbose: print('[{0} points, {1}]'.format(len(grid.points), sizeof_fmt(grid.nbytes)), end='\n') properties = xray.Dataset({'NP': (list(str_conds.keys()) + ['vertex'], np.empty(grid_shape)), 'GM': (list(str_conds.keys()), np.empty(grid_shape[:-1])), 'MU': (list(str_conds.keys()) + ['component'], np.empty(grid_shape)), 'points': (list(str_conds.keys()) + ['vertex'], np.empty(grid_shape, dtype=np.int)) }, coords=coord_dict, attrs={'iterations': 1}, ) # Store the potentials from the previous iteration current_potentials = properties.MU.copy() for iteration in range(MAX_ITERATIONS): if verbose: print('Computing convex hull [iteration {}]'.format(properties.attrs['iterations'])) # lower_convex_hull will modify properties lower_convex_hull(grid, properties) progress = np.abs(current_potentials - properties.MU).max().values if verbose: print('progress', progress) if progress < MIN_PROGRESS: if verbose: print('Convergence achieved') break current_potentials[...] = properties.MU.values if verbose: print('Refining convex hull') # Insert extra dimensions for non-T,P conditions so GM broadcasts correctly energy_broadcast_shape = grid.GM.values.shape[:len(indep_vals)] + \ (1,) * (len(str_conds) - len(indep_vals)) + (grid.GM.values.shape[-1],) driving_forces = np.einsum('...i,...i', properties.MU.values[..., np.newaxis, :], grid.X.values[np.index_exp[...] + (np.newaxis,) * (len(str_conds) - len(indep_vals)) + np.index_exp[:, :]]) - \ grid.GM.values.view().reshape(energy_broadcast_shape) for name in active_phases: dof = len(models[name].energy.atoms(v.SiteFraction)) current_phase_indices = (grid.Phase.values == name).reshape(energy_broadcast_shape[:-1] + (-1,)) # Broadcast to capture all conditions current_phase_indices = np.broadcast_arrays(current_phase_indices, np.empty(driving_forces.shape))[0] # This reshape is safe as long as phases have the same number of points at all indep. conditions current_phase_driving_forces = driving_forces[current_phase_indices].reshape( current_phase_indices.shape[:-1] + (-1,)) # Note: This works as long as all points are in the same phase order for all T, P current_site_fractions = grid.Y.values[..., current_phase_indices[(0,) * len(str_conds)], :] if np.sum(current_site_fractions[(0,) * len(indep_vals)][..., :dof]) == dof: # All site fractions are 1, aka zero internal degrees of freedom # Impossible to refine these points, so skip this phase points_dict[name] = current_site_fractions[(0,) * len(indep_vals)][..., :dof] continue # Find the N points with largest driving force for a given set of conditions # Remember that driving force has a sign, so we want the "most positive" values # N is the number of components, in this context # N points define a 'best simplex' for every set of conditions # We also need to restrict ourselves to one phase at a time trial_indices = np.argpartition(current_phase_driving_forces, -len(components), axis=-1)[..., -len(components):] trial_indices = trial_indices.ravel() statevar_indices = np.unravel_index(np.arange(np.multiply.reduce(properties.GM.values.shape + (len(components),))), properties.GM.values.shape + (len(components),))[:len(indep_vals)] points = current_site_fractions[np.index_exp[statevar_indices + (trial_indices,)]] points.shape = properties.points.shape[:-1] + (-1, maximum_internal_dof) # The Y arrays have been padded, so we should slice off the padding points = points[..., :dof] # Workaround for derivative issues at endmembers points[points == 0.] = MIN_SITE_FRACTION if len(points) == 0: if name in points_dict: del points_dict[name] # No nearly stable points: skip this phase continue num_vars = len(phase_records[name].variables) plane_grad = phase_records[name].plane_grad plane_hess = phase_records[name].plane_hess statevar_grid = np.meshgrid(*itertools.chain(indep_vals), sparse=True, indexing='xy') # TODO: A more sophisticated treatment of constraints num_constraints = len(indep_vals) + len(dbf.phases[name].sublattices) constraint_jac = np.zeros((num_constraints, num_vars)) # Independent variables are always fixed (in this limited implementation) for idx in range(len(indep_vals)): constraint_jac[idx, idx] = 1 # This is for site fraction balance constraints var_idx = len(indep_vals) for idx in range(len(dbf.phases[name].sublattices)): active_in_subl = set(dbf.phases[name].constituents[idx]).intersection(comps) constraint_jac[len(indep_vals) + idx, var_idx:var_idx + len(active_in_subl)] = 1 var_idx += len(active_in_subl) grad = phase_records[name].grad(*itertools.chain(statevar_grid, points.T)) if grad.dtype == 'object': # Workaround a bug in zero gradient entries grad_zeros = np.zeros(points.T.shape[1:], dtype=np.float) for i in np.arange(grad.shape[0]): if isinstance(grad[i], int): grad[i] = grad_zeros grad = np.array(grad.tolist(), dtype=np.float) bcasts = np.broadcast_arrays(*itertools.chain(properties.MU.values.T, points.T)) cast_grad = -plane_grad(*itertools.chain(bcasts, [0], [0])) cast_grad = cast_grad.T + grad.T grad = cast_grad grad.shape = grad.shape[:-1] # Remove extraneous dimension # This Hessian is an approximation updated using the BFGS method # See Nocedal and Wright, ch.3, p. 198 # Initialize as identity matrix hess = broadcast_to(np.eye(num_vars), grad.shape + (grad.shape[-1],)).copy() newton_iteration = 0 while newton_iteration < MAX_NEWTON_ITERATIONS: e_matrix = np.linalg.inv(hess) dy_unconstrained = -np.einsum('...ij,...j->...i', e_matrix, grad) proj_matrix = np.dot(e_matrix, constraint_jac.T) inv_matrix = np.rollaxis(np.dot(constraint_jac, proj_matrix), 0, -1) inv_term = np.linalg.inv(inv_matrix) first_term = np.einsum('...ij,...jk->...ik', proj_matrix, inv_term) # Normally a term for the residual here # We only choose starting points which obey the constraints, so r = 0 cons_summation = np.einsum('...i,...ji->...j', dy_unconstrained, constraint_jac) cons_correction = np.einsum('...ij,...j->...i', first_term, cons_summation) dy_constrained = dy_unconstrained - cons_correction # TODO: Support for adaptive changing independent variable steps new_direction = dy_constrained[..., len(indep_vals):] # Backtracking line search new_points = points + INITIAL_STEP_SIZE * new_direction alpha = np.full(new_points.shape[:-1], INITIAL_STEP_SIZE, dtype=np.float) negative_points = np.any(new_points < 0., axis=-1) while np.any(negative_points): alpha[negative_points] *= 0.1 new_points = points + alpha[..., np.newaxis] * new_direction negative_points = np.any(new_points < 0., axis=-1) # If we made "near" zero progress on any points, don't update the Hessian until # we've rebuilt the convex hull # Nocedal and Wright recommend against skipping Hessian updates # They recommend using a damped update approach, pp. 538-539 of their book # TODO: Check the projected gradient norm, not the step length if np.any(np.max(np.abs(alpha[..., np.newaxis] * new_direction), axis=-1) < MIN_STEP_LENGTH): break # Workaround for derivative issues at endmembers new_points[new_points == 0.] = 1e-16 # BFGS update to Hessian new_grad = phase_records[name].grad(*itertools.chain(statevar_grid, new_points.T)) if new_grad.dtype == 'object': # Workaround a bug in zero gradient entries grad_zeros = np.zeros(new_points.T.shape[1:], dtype=np.float) for i in np.arange(new_grad.shape[0]): if isinstance(new_grad[i], int): new_grad[i] = grad_zeros new_grad = np.array(new_grad.tolist(), dtype=np.float) bcasts = np.broadcast_arrays(*itertools.chain(properties.MU.values.T, new_points.T)) cast_grad = -plane_grad(*itertools.chain(bcasts, [0], [0])) cast_grad = cast_grad.T + new_grad.T new_grad = cast_grad new_grad.shape = new_grad.shape[:-1] # Remove extraneous dimension # Notation used here consistent with Nocedal and Wright s_k = np.empty(points.shape[:-1] + (points.shape[-1] + len(indep_vals),)) # Zero out independent variable changes for now s_k[..., :len(indep_vals)] = 0 s_k[..., len(indep_vals):] = new_points - points y_k = new_grad - grad s_s_term = np.einsum('...j,...k->...jk', s_k, s_k) s_b_s_term = np.einsum('...i,...ij,...j', s_k, hess, s_k) y_y_y_s_term = np.einsum('...j,...k->...jk', y_k, y_k) / \ np.einsum('...i,...i', y_k, s_k)[..., np.newaxis, np.newaxis] update = np.einsum('...ij,...jk,...kl->...il', hess, s_s_term, hess) / \ s_b_s_term[..., np.newaxis, np.newaxis] + y_y_y_s_term hess = hess - update cast_hess = -plane_hess(*itertools.chain(bcasts, [0], [0])).T + hess hess = -cast_hess #TODO: Why does this fix things? # TODO: Verify that the chosen step lengths reduce the energy points = new_points grad = new_grad newton_iteration += 1 new_points = new_points.reshape(new_points.shape[:len(indep_vals)] + (-1, new_points.shape[-1])) new_points = np.concatenate((current_site_fractions[..., :dof], new_points), axis=-2) points_dict[name] = new_points if verbose: print('Rebuilding grid', end=' ') grid = calculate(dbf, comps, active_phases, output='GM', model=models, callables=callable_dict, fake_points=True, points=points_dict, **grid_opts) if verbose: print('[{0} points, {1}]'.format(len(grid.points), sizeof_fmt(grid.nbytes)), end='\n') properties.attrs['iterations'] += 1 # One last call to ensure 'properties' and 'grid' are consistent with one another lower_convex_hull(grid, properties) ravelled_X_view = grid['X'].values.view().reshape(-1, grid['X'].values.shape[-1]) ravelled_Y_view = grid['Y'].values.view().reshape(-1, grid['Y'].values.shape[-1]) ravelled_Phase_view = grid['Phase'].values.view().reshape(-1) # Copy final point values from the grid and drop the index array # For some reason direct construction doesn't work. We have to create empty and then assign. properties['X'] = xray.DataArray(np.empty_like(ravelled_X_view[properties['points'].values]), dims=properties['points'].dims + ('component',)) properties['X'].values[...] = ravelled_X_view[properties['points'].values] properties['Y'] = xray.DataArray(np.empty_like(ravelled_Y_view[properties['points'].values]), dims=properties['points'].dims + ('internal_dof',)) properties['Y'].values[...] = ravelled_Y_view[properties['points'].values] # TODO: What about invariant reactions? We should perform a final driving force calculation here. # We can handle that in the same post-processing step where we identify single-phase regions. properties['Phase'] = xray.DataArray(np.empty_like(ravelled_Phase_view[properties['points'].values]), dims=properties['points'].dims) properties['Phase'].values[...] = ravelled_Phase_view[properties['points'].values] del properties['points'] return properties
def eq_callables_dict(dbf, comps, phases, model=None, param_symbols=None, output='GM', build_gradients=True): """ Create a dictionary of callable dictionaries for phases in equilibrium Parameters ---------- dbf : pycalphad.Database A pycalphad 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``. param_symbols : list SymPy Symbol objects that will be preserved in the callable functions. output : str Output property of the particular Model to sample build_gradients : bool Whether or not to build gradient functions. Defaults to True. Returns ------- dict Dictionary of keyword argument callables to pass to equilibrium. Notes ----- Based on the pycalphad equilibrium method for building phases as of commit 37ff75ce. Examples -------- >>> dbf = Database('AL-NI.tdb') >>> comps = ['AL', 'NI', 'VA'] >>> phases = ['FCC_L12', 'BCC_B2', 'LIQUID', 'AL3NI5', 'AL3NI2', 'AL3NI'] >>> eq_callables = eq_callables_dict(dbf, comps, phases) >>> equilibrium(dbf, comps, phases, conditions, **eq_callables) """ comps = sorted(unpack_components(dbf, comps)) pure_elements = get_pure_elements(dbf, comps) eq_callables = { 'massfuncs': {}, 'massgradfuncs': {}, 'callables': {}, 'grad_callables': {}, 'hess_callables': {}, } models = unpack_kwarg(model, default_arg=Model) param_symbols = param_symbols if param_symbols is not None else [] # wrap param symbols in Symbols if they are strings if all([isinstance(sym, string_types) for sym in param_symbols]): param_symbols = [Symbol(sym) for sym in param_symbols] param_values = np.zeros_like(param_symbols, dtype=np.float64) phase_records = {} # create models # starting from pycalphad for name in phases: mod = models[name] if isinstance(mod, type): models[name] = mod = mod(dbf, comps, name) 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__)) # Build the callables of the output # Only force undefineds to zero if we're not overriding them undefs = list( out.atoms(Symbol) - out.atoms(v.StateVariable) - set(param_symbols)) for undef in undefs: out = out.xreplace({undef: float(0)}) 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 hf = None eq_callables['callables'][name] = cf eq_callables['grad_callables'][name] = gf eq_callables['hess_callables'][name] = hf # 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 eq_callables['massfuncs'][name] = mcf eq_callables['massgradfuncs'][name] = mgf # creating the phase records triggers the compile phase_records[name.upper()] = PhaseRecord_from_cython( comps, variables, np.array(dbf.phases[name].sublattices, dtype=np.float), param_values, eq_callables['callables'][name], eq_callables['grad_callables'][name], eq_callables['hess_callables'][name], eq_callables['massfuncs'][name], eq_callables['massgradfuncs'][name]) # finally, add the models to the eq_callables eq_callables['model'] = dict(models) return eq_callables
def equilibrium(dbf, comps, phases, conditions, output=None, model=None, verbose=False, broadcast=True, calc_opts=None, scheduler=dask.local.get_sync, parameters=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. Returns ------- Structured equilibrium calculation, or Dask graph if scheduler=None. Examples -------- None yet. """ if not broadcast: raise NotImplementedError('Broadcasting cannot yet be disabled') from pycalphad import __version__ as pycalphad_version 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))]))) indep_vars = ['T', 'P'] calc_opts = calc_opts if calc_opts is not None else dict() model = model if model is not None else Model phase_records = dict() diagnostic = kwargs.pop('_diagnostic', False) callable_dict = kwargs.pop('callables', dict()) mass_dict = unpack_kwarg(kwargs.pop('massfuncs', None), default_arg=None) mass_grad_dict = unpack_kwarg(kwargs.pop('massgradfuncs', None), default_arg=None) grad_callable_dict = kwargs.pop('grad_callables', dict()) hess_callable_dict = kwargs.pop('hess_callables', dict()) parameters = parameters if parameters is not None else dict() if isinstance(parameters, dict): parameters = OrderedDict(sorted(parameters.items(), key=str)) param_symbols = tuple(parameters.keys()) param_values = np.atleast_1d(np.array(list(parameters.values()), dtype=np.float)) maximum_internal_dof = 0 # 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)) str_conds = OrderedDict((str(key), value) for key, value in conds.items()) num_calcs = np.prod([len(i) for i in str_conds.values()]) indep_vals = list([float(x) for x in np.atleast_1d(val)] for key, val in str_conds.items() if key in indep_vars) 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'])) # Construct models for each phase; prioritize user models models = unpack_kwarg(model, default_arg=Model) if verbose: print('Components:', ' '.join([str(x) for x in comps])) print('Phases:', end=' ') max_phase_name_len = max(len(name) for name in active_phases) # Need to allow for '_FAKE_' psuedo-phase max_phase_name_len = max(max_phase_name_len, 6) for name in active_phases: mod = models[name] if isinstance(mod, type): models[name] = mod = mod(dbf, comps, name, parameters=parameters) site_fracs = mod.site_fractions variables = sorted(site_fracs, key=str) maximum_internal_dof = max(maximum_internal_dof, len(site_fracs)) out = models[name].energy if (not callable_dict.get(name, False)) or not (grad_callable_dict.get(name, False)): # 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)) and not (x in param_symbols)] for undef in undefs: out = out.xreplace({undef: float(0)}) cf, gf = build_functions(out, tuple([v.P, v.T] + site_fracs), parameters=param_symbols) hf = None if callable_dict.get(name, None) is None: callable_dict[name] = cf if grad_callable_dict.get(name, None) is None: grad_callable_dict[name] = gf if hess_callable_dict.get(name, None) is None: hess_callable_dict[name] = hf if (mass_dict[name] is None) or (mass_grad_dict[name] is None): # TODO: In principle, we should also check for undefs in mod.moles() tup1, tup2 = zip(*[build_functions(mod.moles(el), [v.P, v.T] + variables, include_obj=True, include_grad=True, parameters=param_symbols) for el in pure_elements]) if mass_dict[name] is None: mass_dict[name] = tup1 if mass_grad_dict[name] is None: mass_grad_dict[name] = tup2 phase_records[name.upper()] = PhaseRecord_from_cython(comps, variables, np.array(dbf.phases[name].sublattices, dtype=np.float), param_values, callable_dict[name], grad_callable_dict[name], hess_callable_dict[name], mass_dict[name], mass_grad_dict[name]) if verbose: print(name, end=' ') if verbose: print('[done]', end='\n') # 'calculate' accepts conditions through its keyword arguments grid_opts = calc_opts.copy() grid_opts.update({key: value for key, value in str_conds.items() if key in indep_vars}) if 'pdens' not in grid_opts: grid_opts['pdens'] = 500 coord_dict = str_conds.copy() coord_dict['vertex'] = np.arange(len(pure_elements)) grid_shape = np.meshgrid(*coord_dict.values(), indexing='ij', sparse=False)[0].shape coord_dict['component'] = pure_elements grid = delayed(calculate, pure=False)(dbf, comps, active_phases, output='GM', model=models, callables=callable_dict, massfuncs=mass_dict, fake_points=True, parameters=parameters, **grid_opts) properties = delayed(Dataset, pure=False)({'NP': (list(str_conds.keys()) + ['vertex'], np.empty(grid_shape)), 'GM': (list(str_conds.keys()), np.empty(grid_shape[:-1])), 'MU': (list(str_conds.keys()) + ['component'], np.empty(grid_shape)), 'X': (list(str_conds.keys()) + ['vertex', 'component'], np.empty(grid_shape + (grid_shape[-1],))), 'Y': (list(str_conds.keys()) + ['vertex', 'internal_dof'], np.empty(grid_shape + (maximum_internal_dof,))), 'Phase': (list(str_conds.keys()) + ['vertex'], np.empty(grid_shape, dtype='U%s' % max_phase_name_len)), 'points': (list(str_conds.keys()) + ['vertex'], np.empty(grid_shape, dtype=np.int32)) }, coords=coord_dict, attrs={'engine': 'pycalphad %s' % pycalphad_version}, ) # One last call to ensure 'properties' and 'grid' are consistent with one another properties = delayed(lower_convex_hull, pure=False)(grid, properties) 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()), verbose) 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()), verbose) # Compute equilibrium values of any additional user-specified properties output = output if isinstance(output, (list, tuple, set)) else [output] # 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, model=models, **calc_opts) properties = delayed(properties.merge, pure=False)(eqcal, inplace=True, compat='equals') if scheduler is not None: properties = dask.compute(properties, get=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_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: Database, species: Sequence[v.Species], phases: Sequence[str], str_statevar_dict: Dict[str, np.ndarray], models: Dict[str, Model], phase_records: Dict[str, PhaseRecord], output: Optional[str] = 'GM', points: Optional[Dict[str, np.ndarray]] = None, pdens: Optional[int] = 2000, broadcast: Optional[bool] = True, fake_points: Optional[bool] = False, ) -> LightDataset: """ Quickly sample phase internal degree of freedom with virtually no overhead. """ points_dict = unpack_kwarg(points, default_arg=None) pdens_dict = unpack_kwarg(pdens, default_arg=2000) nonvacant_components = [ x for x in sorted(species) if x.number_of_atoms > 0 ] maximum_internal_dof = max(prx.phase_dof for prx in phase_records.values()) all_phase_data = [] for phase_name in sorted(phases): phase_obj = dbf.phases[phase_name] mod = models[phase_name] phase_record = phase_records[phase_name] points = points_dict[phase_name] variables, sublattice_dof = generate_dof(phase_obj, mod.components) if points is None: points = _sample_phase_constitution(phase_name, phase_obj.constituents, sublattice_dof, species, tuple(variables), point_sample, True, pdens_dict[phase_name]) points = np.atleast_2d(points) fp = fake_points and (phase_name == sorted(phases)[0]) phase_ds = _compute_phase_values(nonvacant_components, str_statevar_dict, points, phase_record, output, maximum_internal_dof, broadcast=broadcast, largest_energy=float(1e10), fake_points=fp) all_phase_data.append(phase_ds) 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] return final_ds
def equilibrium(dbf, comps, phases, conditions, **kwargs): """ Calculate the equilibrium state of a system containing the specified components and phases, under the specified conditions. Model parameters are taken from 'dbf'. 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. verbose : bool, optional (Default: True) Show progress of calculations. grid_opts : dict, optional Keyword arguments to pass to the initial grid routine. Returns ------- Structured equilibrium calculation. Examples -------- None yet. """ active_phases = unpack_phases(phases) or sorted(dbf.phases.keys()) comps = sorted(comps) indep_vars = ['T', 'P'] grid_opts = kwargs.pop('grid_opts', dict()) verbose = kwargs.pop('verbose', True) phase_records = dict() callable_dict = kwargs.pop('callables', dict()) grad_callable_dict = kwargs.pop('grad_callables', dict()) hess_callable_dict = kwargs.pop('hess_callables', dict()) points_dict = dict() maximum_internal_dof = 0 conds = OrderedDict((key, unpack_condition(value)) for key, value in sorted(conditions.items(), key=str)) str_conds = OrderedDict((str(key), value) for key, value in conds.items()) indep_vals = list([float(x) for x in np.atleast_1d(val)] for key, val in str_conds.items() if key in indep_vars) components = [x for x in sorted(comps) if not x.startswith('VA')] # Construct models for each phase; prioritize user models models = unpack_kwarg(kwargs.pop('model', Model), default_arg=Model) if verbose: print('Components:', ' '.join(comps)) print('Phases:', end=' ') for name in active_phases: mod = models[name] if isinstance(mod, type): models[name] = mod = mod(dbf, comps, name) variables = sorted(mod.energy.atoms(v.StateVariable).union({key for key in conditions.keys() if key in [v.T, v.P]}), key=str) site_fracs = sorted(mod.energy.atoms(v.SiteFraction), key=str) maximum_internal_dof = max(maximum_internal_dof, len(site_fracs)) # Extra factor '1e-100...' is to work around an annoying broadcasting bug for zero gradient entries #models[name].models['_broadcaster'] = 1e-100 * Mul(*variables) ** 3 out = models[name].energy undefs = list(out.atoms(Symbol) - out.atoms(v.StateVariable)) for undef in undefs: out = out.xreplace({undef: float(0)}) callable_dict[name], grad_callable_dict[name], hess_callable_dict[name] = \ build_functions(out, [v.P, v.T] + site_fracs) # Adjust gradient by the approximate chemical potentials hyperplane = Add(*[v.MU(i)*mole_fraction(dbf.phases[name], comps, i) for i in comps if i != 'VA']) plane_obj, plane_grad, plane_hess = build_functions(hyperplane, [v.MU(i) for i in comps if i != 'VA']+site_fracs) phase_records[name.upper()] = PhaseRecord(variables=variables, grad=grad_callable_dict[name], hess=hess_callable_dict[name], plane_grad=plane_grad, plane_hess=plane_hess) if verbose: print(name, end=' ') if verbose: print('[done]', end='\n') # 'calculate' accepts conditions through its keyword arguments grid_opts.update({key: value for key, value in str_conds.items() if key in indep_vars}) if 'pdens' not in grid_opts: grid_opts['pdens'] = 100 coord_dict = str_conds.copy() coord_dict['vertex'] = np.arange(len(components)) grid_shape = np.meshgrid(*coord_dict.values(), indexing='ij', sparse=False)[0].shape coord_dict['component'] = components if verbose: print('Computing initial grid', end=' ') grid = calculate(dbf, comps, active_phases, output='GM', model=models, callables=callable_dict, fake_points=True, **grid_opts) if verbose: print('[{0} points, {1}]'.format(len(grid.points), sizeof_fmt(grid.nbytes)), end='\n') properties = xray.Dataset({'NP': (list(str_conds.keys()) + ['vertex'], np.empty(grid_shape)), 'GM': (list(str_conds.keys()), np.empty(grid_shape[:-1])), 'MU': (list(str_conds.keys()) + ['component'], np.empty(grid_shape)), 'points': (list(str_conds.keys()) + ['vertex'], np.empty(grid_shape, dtype=np.int)) }, coords=coord_dict, attrs={'iterations': 1}, ) # Store the potentials from the previous iteration current_potentials = properties.MU.copy() for iteration in range(MAX_ITERATIONS): if verbose: print('Computing convex hull [iteration {}]'.format(properties.attrs['iterations'])) # lower_convex_hull will modify properties lower_convex_hull(grid, properties) progress = np.abs(current_potentials - properties.MU).values converged = (progress < MIN_PROGRESS).all(axis=-1) if verbose: print('progress', progress.max(), '[{} conditions updated]'.format(np.sum(~converged))) if progress.max() < MIN_PROGRESS: if verbose: print('Convergence achieved') break current_potentials[...] = properties.MU.values if verbose: print('Refining convex hull') # Insert extra dimensions for non-T,P conditions so GM broadcasts correctly energy_broadcast_shape = grid.GM.values.shape[:len(indep_vals)] + \ (1,) * (len(str_conds) - len(indep_vals)) + (grid.GM.values.shape[-1],) driving_forces = np.einsum('...i,...i', properties.MU.values[..., np.newaxis, :].astype(np.float), grid.X.values[np.index_exp[...] + (np.newaxis,) * (len(str_conds) - len(indep_vals)) + np.index_exp[:, :]].astype(np.float)) - \ grid.GM.values.view().reshape(energy_broadcast_shape) for name in active_phases: dof = len(models[name].energy.atoms(v.SiteFraction)) current_phase_indices = (grid.Phase.values == name).reshape(energy_broadcast_shape[:-1] + (-1,)) # Broadcast to capture all conditions current_phase_indices = np.broadcast_arrays(current_phase_indices, np.empty(driving_forces.shape))[0] # This reshape is safe as long as phases have the same number of points at all indep. conditions current_phase_driving_forces = driving_forces[current_phase_indices].reshape( current_phase_indices.shape[:-1] + (-1,)) # Note: This works as long as all points are in the same phase order for all T, P current_site_fractions = grid.Y.values[..., current_phase_indices[(0,) * len(str_conds)], :] if np.sum(current_site_fractions[(0,) * len(indep_vals)][..., :dof]) == dof: # All site fractions are 1, aka zero internal degrees of freedom # Impossible to refine these points, so skip this phase points_dict[name] = current_site_fractions[(0,) * len(indep_vals)][..., :dof] continue # Find the N points with largest driving force for a given set of conditions # Remember that driving force has a sign, so we want the "most positive" values # N is the number of components, in this context # N points define a 'best simplex' for every set of conditions # We also need to restrict ourselves to one phase at a time trial_indices = np.argpartition(current_phase_driving_forces, -len(components), axis=-1)[..., -len(components):] trial_indices = trial_indices.ravel() statevar_indices = np.unravel_index(np.arange(np.multiply.reduce(properties.GM.values.shape + (len(components),))), properties.GM.values.shape + (len(components),))[:len(indep_vals)] points = current_site_fractions[np.index_exp[statevar_indices + (trial_indices,)]] points.shape = properties.points.shape[:-1] + (-1, maximum_internal_dof) # The Y arrays have been padded, so we should slice off the padding points = points[..., :dof] #print('Starting points shape: ', points.shape) #print(points) if len(points) == 0: if name in points_dict: del points_dict[name] # No nearly stable points: skip this phase continue num_vars = len(phase_records[name].variables) plane_grad = phase_records[name].plane_grad plane_hess = phase_records[name].plane_hess statevar_grid = np.meshgrid(*itertools.chain(indep_vals), sparse=True, indexing='ij') # TODO: A more sophisticated treatment of constraints num_constraints = len(dbf.phases[name].sublattices) constraint_jac = np.zeros((num_constraints, num_vars-len(indep_vars))) # Independent variables are always fixed (in this limited implementation) #for idx in range(len(indep_vals)): # constraint_jac[idx, idx] = 1 # This is for site fraction balance constraints var_idx = 0#len(indep_vals) for idx in range(len(dbf.phases[name].sublattices)): active_in_subl = set(dbf.phases[name].constituents[idx]).intersection(comps) constraint_jac[idx, var_idx:var_idx + len(active_in_subl)] = 1 var_idx += len(active_in_subl) newton_iteration = 0 while newton_iteration < MAX_NEWTON_ITERATIONS: flattened_points = points.reshape(points.shape[:len(indep_vals)] + (-1, points.shape[-1])) grad_args = itertools.chain([i[..., None] for i in statevar_grid], [flattened_points[..., i] for i in range(flattened_points.shape[-1])]) grad = np.array(phase_records[name].grad(*grad_args), dtype=np.float) # Remove derivatives wrt T,P grad = grad[..., len(indep_vars):] grad.shape = points.shape grad[np.isnan(grad).any(axis=-1)] = 0 # This is necessary for gradients on the edge of space hess_args = itertools.chain([i[..., None] for i in statevar_grid], [flattened_points[..., i] for i in range(flattened_points.shape[-1])]) hess = np.array(phase_records[name].hess(*hess_args), dtype=np.float) # Remove derivatives wrt T,P hess = hess[..., len(indep_vars):, len(indep_vars):] hess.shape = points.shape + (hess.shape[-1],) hess[np.isnan(hess).any(axis=(-2, -1))] = np.eye(hess.shape[-1]) plane_args = itertools.chain([properties.MU.values[..., i][..., None] for i in range(properties.MU.shape[-1])], [points[..., i] for i in range(points.shape[-1])]) cast_grad = np.array(plane_grad(*plane_args), dtype=np.float) # Remove derivatives wrt chemical potentials cast_grad = cast_grad[..., properties.MU.shape[-1]:] grad = grad - cast_grad plane_args = itertools.chain([properties.MU.values[..., i][..., None] for i in range(properties.MU.shape[-1])], [points[..., i] for i in range(points.shape[-1])]) cast_hess = np.array(plane_hess(*plane_args), dtype=np.float) # Remove derivatives wrt chemical potentials cast_hess = cast_hess[..., properties.MU.shape[-1]:, properties.MU.shape[-1]:] cast_hess = -cast_hess + hess hess = cast_hess.astype(np.float, copy=False) try: e_matrix = np.linalg.inv(hess) except np.linalg.LinAlgError: print(hess) raise current = calculate(dbf, comps, name, output='GM', model=models, callables=callable_dict, fake_points=False, points=points.reshape(points.shape[:len(indep_vals)] + (-1, points.shape[-1])), **grid_opts) current_plane = np.multiply(current.X.values.reshape(points.shape[:-1] + (len(components),)), properties.MU.values[..., np.newaxis, :]).sum(axis=-1) current_df = current.GM.values.reshape(points.shape[:-1]) - current_plane #print('Inv hess check: ', np.isnan(e_matrix).any()) #print('grad check: ', np.isnan(grad).any()) dy_unconstrained = -np.einsum('...ij,...j->...i', e_matrix, grad) #print('dy_unconstrained check: ', np.isnan(dy_unconstrained).any()) proj_matrix = np.dot(e_matrix, constraint_jac.T) inv_matrix = np.rollaxis(np.dot(constraint_jac, proj_matrix), 0, -1) inv_term = np.linalg.inv(inv_matrix) #print('inv_term check: ', np.isnan(inv_term).any()) first_term = np.einsum('...ij,...jk->...ik', proj_matrix, inv_term) #print('first_term check: ', np.isnan(first_term).any()) # Normally a term for the residual here # We only choose starting points which obey the constraints, so r = 0 cons_summation = np.einsum('...i,...ji->...j', dy_unconstrained, constraint_jac) #print('cons_summation check: ', np.isnan(cons_summation).any()) cons_correction = np.einsum('...ij,...j->...i', first_term, cons_summation) #print('cons_correction check: ', np.isnan(cons_correction).any()) dy_constrained = dy_unconstrained - cons_correction #print('dy_constrained check: ', np.isnan(dy_constrained).any()) # TODO: Support for adaptive changing independent variable steps new_direction = dy_constrained #print('new_direction', new_direction) #print('points', points) # Backtracking line search if np.isnan(new_direction).any(): print('new_direction', new_direction) #print('Convergence angle:', -(grad*new_direction).sum(axis=-1) / (np.linalg.norm(grad, axis=-1) * np.linalg.norm(new_direction, axis=-1))) new_points = points + INITIAL_STEP_SIZE * new_direction alpha = np.full(new_points.shape[:-1], INITIAL_STEP_SIZE, dtype=np.float) alpha[np.all(np.linalg.norm(new_direction, axis=-1) < MIN_DIRECTION_NORM, axis=-1)] = 0 negative_points = np.any(new_points < 0., axis=-1) while np.any(negative_points): alpha[negative_points] *= 0.5 new_points = points + alpha[..., np.newaxis] * new_direction negative_points = np.any(new_points < 0., axis=-1) # Backtracking line search # alpha now contains maximum possible values that keep us inside the space # but we don't just want to take the biggest step; we want the biggest step which reduces energy new_points = new_points.reshape(new_points.shape[:len(indep_vals)] + (-1, new_points.shape[-1])) candidates = calculate(dbf, comps, name, output='GM', model=models, callables=callable_dict, fake_points=False, points=new_points, **grid_opts) candidate_plane = np.multiply(candidates.X.values.reshape(points.shape[:-1] + (len(components),)), properties.MU.values[..., np.newaxis, :]).sum(axis=-1) energy_diff = (candidates.GM.values.reshape(new_direction.shape[:-1]) - candidate_plane) - current_df new_points.shape = new_direction.shape bad_steps = energy_diff > alpha * 1e-4 * (new_direction * grad).sum(axis=-1) backtracking_iterations = 0 while np.any(bad_steps): alpha[bad_steps] *= 0.5 new_points = points + alpha[..., np.newaxis] * new_direction #print('new_points', new_points) #print('bad_steps', bad_steps) new_points = new_points.reshape(new_points.shape[:len(indep_vals)] + (-1, new_points.shape[-1])) candidates = calculate(dbf, comps, name, output='GM', model=models, callables=callable_dict, fake_points=False, points=new_points, **grid_opts) candidate_plane = np.multiply(candidates.X.values.reshape(points.shape[:-1] + (len(components),)), properties.MU.values[..., np.newaxis, :]).sum(axis=-1) energy_diff = (candidates.GM.values.reshape(new_direction.shape[:-1]) - candidate_plane) - current_df #print('energy_diff', energy_diff) new_points.shape = new_direction.shape bad_steps = energy_diff > alpha * 1e-4 * (new_direction * grad).sum(axis=-1) backtracking_iterations += 1 if backtracking_iterations > MAX_BACKTRACKING: break biggest_step = np.max(np.linalg.norm(new_points - points, axis=-1)) if biggest_step < 1e-2: if verbose: print('N-R convergence on mini-iteration', newton_iteration, '[{}]'.format(name)) points = new_points break if verbose: #print('Biggest step:', biggest_step) #print('points', points) #print('grad of points', grad) #print('new_direction', new_direction) #print('alpha', alpha) #print('new_points', new_points) pass points = new_points newton_iteration += 1 new_points = points.reshape(points.shape[:len(indep_vals)] + (-1, points.shape[-1])) new_points = np.concatenate((current_site_fractions[..., :dof], new_points), axis=-2) points_dict[name] = new_points if verbose: print('Rebuilding grid', end=' ') grid = calculate(dbf, comps, active_phases, output='GM', model=models, callables=callable_dict, fake_points=True, points=points_dict, **grid_opts) if verbose: print('[{0} points, {1}]'.format(len(grid.points), sizeof_fmt(grid.nbytes)), end='\n') properties.attrs['iterations'] += 1 # One last call to ensure 'properties' and 'grid' are consistent with one another lower_convex_hull(grid, properties) ravelled_X_view = grid['X'].values.view().reshape(-1, grid['X'].values.shape[-1]) ravelled_Y_view = grid['Y'].values.view().reshape(-1, grid['Y'].values.shape[-1]) ravelled_Phase_view = grid['Phase'].values.view().reshape(-1) # Copy final point values from the grid and drop the index array # For some reason direct construction doesn't work. We have to create empty and then assign. properties['X'] = xray.DataArray(np.empty_like(ravelled_X_view[properties['points'].values]), dims=properties['points'].dims + ('component',)) properties['X'].values[...] = ravelled_X_view[properties['points'].values] properties['Y'] = xray.DataArray(np.empty_like(ravelled_Y_view[properties['points'].values]), dims=properties['points'].dims + ('internal_dof',)) properties['Y'].values[...] = ravelled_Y_view[properties['points'].values] # TODO: What about invariant reactions? We should perform a final driving force calculation here. # We can handle that in the same post-processing step where we identify single-phase regions. properties['Phase'] = xray.DataArray(np.empty_like(ravelled_Phase_view[properties['points'].values]), dims=properties['points'].dims) properties['Phase'].values[...] = ravelled_Phase_view[properties['points'].values] del properties['points'] return properties
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 calculate(dbf, comps, phases, mode=None, output='GM', fake_points=False, broadcast=True, parameters=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 SymPy Symbol to numbers, for overriding the values of parameters in the Database. 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) mass_dict = unpack_kwarg(kwargs.pop('massfuncs', 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) parameters = parameters or dict() if isinstance(parameters, dict): parameters = OrderedDict(sorted(parameters.items(), key=str)) param_symbols = tuple(parameters.keys()) param_values = np.atleast_1d(np.array(list(parameters.values()), dtype=np.float)) 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] # 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), unpack_condition(value)) for (key, value) in kwargs.items()) # XXX: CompiledModel assumes P, T are the only state variables if statevar_dict.get(v.P, None) is None: statevar_dict[v.P] = 101325 if statevar_dict.get(v.T, None) is None: statevar_dict[v.T] = 300 # Sort after default state variable check to fix gh-116 statevar_dict = collections.OrderedDict(sorted(statevar_dict.items(), key=lambda x: str(x[0]))) str_statevar_dict = collections.OrderedDict((str(key), unpack_condition(value)) \ for (key, value) in statevar_dict.items()) all_phase_data = [] comp_sets = {} largest_energy = 1e30 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, parameters=parameters) except DofError: # we can't build the specified phase because the # specified components aren't found in every sublattice # we'll just skip it warnings.warn("""Suspending specified phase {} due to some sublattices containing only unspecified components""".format(phase_name)) continue if points_dict[phase_name] is None: maximum_internal_dof = max(maximum_internal_dof, sum(len(x) for x in mod.constituents)) 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 # this is a phase model we couldn't construct for whatever reason; skip it if isinstance(mod, type): 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: try: out = getattr(mod, output) except AttributeError: raise AttributeError('Missing Model attribute {0} specified for {1}' .format(output, mod.__class__)) # 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)}) warnings.warn('Setting undefined symbol {0} for phase {1} to zero'.format(undef, phase_name)) comp_sets[phase_name] = build_functions(out, list(statevar_dict.keys()) + variables, include_obj=True, include_grad=False, parameters=param_symbols) else: comp_sets[phase_name] = callable_dict[phase_name] if mass_dict[phase_name] is None: pure_elements = [spec for spec in nonvacant_components if (len(spec.constituents.keys()) == 1 and list(spec.constituents.keys())[0] == spec.name) ] # TODO: In principle, we should also check for undefs in mod.moles() mass_dict[phase_name] = [build_functions(mod.moles(el), list(statevar_dict.keys()) + variables, include_obj=True, include_grad=False, parameters=param_symbols) for el in pure_elements] phase_record = PhaseRecord_from_cython(comps, list(statevar_dict.keys()) + variables, np.array(dbf.phases[phase_name].sublattices, dtype=np.float), param_values, comp_sets[phase_name], None, None, mass_dict[phase_name], None) points = points_dict[phase_name] if points is None: points = _sample_phase_constitution(phase_name, phase_obj.constituents, sublattice_dof, comps, tuple(variables), 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.keys())[0]) phase_ds = _compute_phase_values(nonvacant_components, str_statevar_dict, points, phase_record, output, maximum_internal_dof, broadcast=broadcast, largest_energy=float(largest_energy), fake_points=fp) all_phase_data.append(phase_ds) # speedup for single-phase case (found by profiling) if len(all_phase_data) > 1: final_ds = concat(all_phase_data, dim='points') final_ds['points'].values = np.arange(len(final_ds['points'])) final_ds.coords['points'].values = np.arange(len(final_ds['points'])) else: final_ds = all_phase_data[0] return final_ds
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 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